Compare commits

...

3 Commits

Author SHA1 Message Date
668397104a Adopt @lavamoat/allow-scripts to gate npm install scripts
All checks were successful
CI / Go tests & lint (push) Successful in 1m34s
CI / Frontend tests & type-check (push) Successful in 1m15s
Disables dependency lifecycle scripts by default via .npmrc
(ignore-scripts=true) so arbitrary packages cannot execute code at
install time. An explicit allowlist in web/package.json opts specific
packages back in, and CI/Docker/Taskfile now run allow-scripts after
npm install to apply it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 11:29:08 -03:00
0446e8f8a7 Merge pull request 'Implement time off management (Issue #3)' (#12) from feature/time-off-management into main
All checks were successful
CI / Go tests & lint (push) Successful in 8s
CI / Frontend tests & type-check (push) Successful in 26s
Reviewed-on: #12
2026-04-09 19:09:16 +00:00
975225e650 Fix time-off date display and improve UI
All checks were successful
CI / Go tests & lint (push) Successful in 9s
CI / Frontend tests & type-check (push) Successful in 29s
CI / Go tests & lint (pull_request) Successful in 9s
CI / Frontend tests & type-check (pull_request) Successful in 25s
Scan datetime columns directly into time.Time instead of strings in the
timeoff store — the intermediate string parse silently failed with
parseTime=true, producing zero-value dates. Display dates with month
names and filter admin's own entry from the volunteer dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 16:07:42 -03:00
8 changed files with 1304 additions and 25 deletions

View File

@@ -36,7 +36,7 @@ jobs:
- name: Install dependencies
working-directory: web
run: npm install
run: npm install && npm exec -- allow-scripts
- name: Type check
working-directory: web

View File

@@ -1,8 +1,8 @@
# Stage 1: Build React frontend
FROM node:22-alpine AS frontend
WORKDIR /app/web
COPY web/package*.json ./
RUN npm install
COPY web/package*.json web/.npmrc ./
RUN npm install && npm exec -- allow-scripts
COPY web/ ./
RUN npm run build

View File

@@ -15,7 +15,9 @@ tasks:
web:install:
desc: Install frontend dependencies
dir: "{{.WEB_DIR}}"
cmd: npm install
cmds:
- npm install
- npm exec -- allow-scripts
sources:
- package.json
generates:

View File

@@ -80,25 +80,20 @@ func (s *Store) Create(ctx context.Context, volunteerID int64, in CreateInput) (
func (s *Store) GetByID(ctx context.Context, id int64) (*Request, error) {
req := &Request{}
var startsAt, endsAt, createdAt, updatedAt string
var reason sql.NullString
var reviewedBy sql.NullInt64
var reviewedAt sql.NullString
var reviewedAt sql.NullTime
err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at
FROM time_off_requests WHERE id = ?`, id,
).Scan(&req.ID, &req.VolunteerID, &startsAt, &endsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &createdAt, &updatedAt)
).Scan(&req.ID, &req.VolunteerID, &req.StartsAt, &req.EndsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &req.CreatedAt, &req.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get time off request: %w", err)
}
req.StartsAt, _ = time.Parse("2006-01-02 15:04:05", startsAt)
req.EndsAt, _ = time.Parse("2006-01-02 15:04:05", endsAt)
req.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
req.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
if reason.Valid {
req.Reason = reason.String
}
@@ -106,8 +101,7 @@ func (s *Store) GetByID(ctx context.Context, id int64) (*Request, error) {
req.ReviewedBy = &reviewedBy.Int64
}
if reviewedAt.Valid {
t, _ := time.Parse("2006-01-02 15:04:05", reviewedAt.String)
req.ReviewedAt = &t
req.ReviewedAt = &reviewedAt.Time
}
return req, nil
}
@@ -130,17 +124,12 @@ func (s *Store) List(ctx context.Context, volunteerID int64) ([]Request, error)
var requests []Request
for rows.Next() {
var req Request
var startsAt, endsAt, createdAt, updatedAt string
var reason sql.NullString
var reviewedBy sql.NullInt64
var reviewedAt sql.NullString
if err := rows.Scan(&req.ID, &req.VolunteerID, &startsAt, &endsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &createdAt, &updatedAt); err != nil {
var reviewedAt sql.NullTime
if err := rows.Scan(&req.ID, &req.VolunteerID, &req.StartsAt, &req.EndsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &req.CreatedAt, &req.UpdatedAt); err != nil {
return nil, err
}
req.StartsAt, _ = time.Parse("2006-01-02 15:04:05", startsAt)
req.EndsAt, _ = time.Parse("2006-01-02 15:04:05", endsAt)
req.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
req.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
if reason.Valid {
req.Reason = reason.String
}
@@ -148,8 +137,7 @@ func (s *Store) List(ctx context.Context, volunteerID int64) ([]Request, error)
req.ReviewedBy = &reviewedBy.Int64
}
if reviewedAt.Valid {
t, _ := time.Parse("2006-01-02 15:04:05", reviewedAt.String)
req.ReviewedAt = &t
req.ReviewedAt = &reviewedAt.Time
}
requests = append(requests, req)
}

1
web/.npmrc Normal file
View File

@@ -0,0 +1 @@
ignore-scripts=true

1281
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
"@lavamoat/preinstall-always-fail": "^3.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1"
@@ -15,6 +16,7 @@
"test": "vitest run"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^5.0.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -28,5 +30,10 @@
"typescript": "^6.0.2",
"vite": "^8.0.8",
"vitest": "^4.1.4"
},
"lavamoat": {
"allowScripts": {
"@lavamoat/preinstall-always-fail": false
}
}
}

View File

@@ -157,7 +157,7 @@ export default function TimeOff() {
onChange={e => setForm(f => ({ ...f, volunteer_id: Number(e.target.value) }))}
>
<option value={0}>Myself</option>
{volunteers.map(v => (
{volunteers.filter(v => v.id !== volunteerID).map(v => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</select>
@@ -226,8 +226,8 @@ export default function TimeOff() {
{requests.map(r => (
<tr key={r.id}>
{role === 'admin' && <td>{volunteerName(r.volunteer_id)}</td>}
<td>{new Date(r.starts_at).toLocaleDateString()}</td>
<td>{new Date(r.ends_at).toLocaleDateString()}</td>
<td>{new Date(r.starts_at).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}</td>
<td>{new Date(r.ends_at).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}</td>
<td>{r.reason ?? '—'}</td>
<td><span className={statusClass(r.status)}>{r.status}</span></td>
<td>