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 - name: Install dependencies
working-directory: web working-directory: web
run: npm install run: npm install && npm exec -- allow-scripts
- name: Type check - name: Type check
working-directory: web working-directory: web

View File

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

View File

@@ -15,7 +15,9 @@ tasks:
web:install: web:install:
desc: Install frontend dependencies desc: Install frontend dependencies
dir: "{{.WEB_DIR}}" dir: "{{.WEB_DIR}}"
cmd: npm install cmds:
- npm install
- npm exec -- allow-scripts
sources: sources:
- package.json - package.json
generates: 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) { func (s *Store) GetByID(ctx context.Context, id int64) (*Request, error) {
req := &Request{} req := &Request{}
var startsAt, endsAt, createdAt, updatedAt string
var reason sql.NullString var reason sql.NullString
var reviewedBy sql.NullInt64 var reviewedBy sql.NullInt64
var reviewedAt sql.NullString var reviewedAt sql.NullTime
err := s.db.QueryRowContext(ctx, err := s.db.QueryRowContext(ctx,
`SELECT id, volunteer_id, starts_at, ends_at, reason, status, reviewed_by, reviewed_at, created_at, updated_at `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, 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) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get time off request: %w", err) 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 { if reason.Valid {
req.Reason = reason.String req.Reason = reason.String
} }
@@ -106,8 +101,7 @@ func (s *Store) GetByID(ctx context.Context, id int64) (*Request, error) {
req.ReviewedBy = &reviewedBy.Int64 req.ReviewedBy = &reviewedBy.Int64
} }
if reviewedAt.Valid { if reviewedAt.Valid {
t, _ := time.Parse("2006-01-02 15:04:05", reviewedAt.String) req.ReviewedAt = &reviewedAt.Time
req.ReviewedAt = &t
} }
return req, nil return req, nil
} }
@@ -130,17 +124,12 @@ func (s *Store) List(ctx context.Context, volunteerID int64) ([]Request, error)
var requests []Request var requests []Request
for rows.Next() { for rows.Next() {
var req Request var req Request
var startsAt, endsAt, createdAt, updatedAt string
var reason sql.NullString var reason sql.NullString
var reviewedBy sql.NullInt64 var reviewedBy sql.NullInt64
var reviewedAt sql.NullString var reviewedAt sql.NullTime
if err := rows.Scan(&req.ID, &req.VolunteerID, &startsAt, &endsAt, &reason, &req.Status, &reviewedBy, &reviewedAt, &createdAt, &updatedAt); err != nil { 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 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 { if reason.Valid {
req.Reason = reason.String req.Reason = reason.String
} }
@@ -148,8 +137,7 @@ func (s *Store) List(ctx context.Context, volunteerID int64) ([]Request, error)
req.ReviewedBy = &reviewedBy.Int64 req.ReviewedBy = &reviewedBy.Int64
} }
if reviewedAt.Valid { if reviewedAt.Valid {
t, _ := time.Parse("2006-01-02 15:04:05", reviewedAt.String) req.ReviewedAt = &reviewedAt.Time
req.ReviewedAt = &t
} }
requests = append(requests, req) 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, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@lavamoat/preinstall-always-fail": "^3.0.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-router-dom": "^7.13.1" "react-router-dom": "^7.13.1"
@@ -15,6 +16,7 @@
"test": "vitest run" "test": "vitest run"
}, },
"devDependencies": { "devDependencies": {
"@lavamoat/allow-scripts": "^5.0.1",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -28,5 +30,10 @@
"typescript": "^6.0.2", "typescript": "^6.0.2",
"vite": "^8.0.8", "vite": "^8.0.8",
"vitest": "^4.1.4" "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) }))} onChange={e => setForm(f => ({ ...f, volunteer_id: Number(e.target.value) }))}
> >
<option value={0}>Myself</option> <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> <option key={v.id} value={v.id}>{v.name}</option>
))} ))}
</select> </select>
@@ -226,8 +226,8 @@ export default function TimeOff() {
{requests.map(r => ( {requests.map(r => (
<tr key={r.id}> <tr key={r.id}>
{role === 'admin' && <td>{volunteerName(r.volunteer_id)}</td>} {role === 'admin' && <td>{volunteerName(r.volunteer_id)}</td>}
<td>{new Date(r.starts_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()}</td> <td>{new Date(r.ends_at).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}</td>
<td>{r.reason ?? '—'}</td> <td>{r.reason ?? '—'}</td>
<td><span className={statusClass(r.status)}>{r.status}</span></td> <td><span className={statusClass(r.status)}>{r.status}</span></td>
<td> <td>