OffersTab uses useTransition + router.refresh() (Pattern 3) consistent with ServiceCheckboxList from Plan 02
accepted_total inline edit uses onBlur save to avoid accidental submissions during typing
getClientActiveOffers selects only public_name, never internal_name — enforced by explicit column selection and comment
getProjectFullDetail extended via additional queries in existing Promise.all — no separate function needed
assignOfferToProject inserts with nanoid default (schema $defaultFn) — no explicit id generation in action
OFFER-04
duration
completed
tasks_completed
files_modified
~15 minutes
2026-05-30
3
5
Phase 5 Plan 3: Project offer assignment — OffersTab in project workspace Summary
OffersTab client component for assigning micro-offers to projects with inline accepted_total editing, plus active-offers summary section on the client detail page showing public_name per project.
Performance
Duration: ~15 minutes
Started: 2026-05-30T00:00:00Z
Completed: 2026-05-30T00:00:00Z
Tasks: 3
Files modified: 5
Accomplishments
Offerte tab added to project workspace at /admin/projects/[id] — admin can assign micro-offers, set accepted_total, and remove assignments
Three new server actions (assignOfferToProject, removeProjectOffer, updateProjectOfferTotal) all guarded by requireAdmin() and revalidating /admin/forecast
getClientActiveOffers() query added to admin-queries.ts showing public_name + project name at /admin/clients/[id]
src/components/admin/tabs/OffersTab.tsx - Client component with assign form, offer list, inline accepted_total editing, and remove button
src/lib/admin-queries.ts - Added ProjectOfferWithMicro type, extended ProjectFullDetail, added project offer queries to getProjectFullDetail(), added ClientActiveOfferSummary type and getClientActiveOffers() function
src/app/admin/projects/project-actions.ts - Added assignOfferToProject, removeProjectOffer, updateProjectOfferTotal server actions with Zod validation
Used useTransition + router.refresh() pattern consistent with the rest of the admin UI (established in Plan 02)
onBlur for accepted_total inline save avoids mid-typing submissions while still feeling responsive
getClientActiveOffers uses explicit .select({ public_name: offer_micros.public_name }) with a comment — never internal_name — to enforce the CLAUDE.md architecture constraint (quote_items / offer internals not exposed to client)
Extended the existing Promise.all in getProjectFullDetail rather than adding a separate query — keeps the function's parallel structure intact
Deviations from Plan
None — plan executed exactly as written. All three tasks followed the plan's action specifications without modification.
Issues Encountered
None.
Known Stubs
None — all offer data is wired to live DB queries. OffersTab shows real project_offers rows from the database. getClientActiveOffers returns real data from the production DB.
Threat Flags
No new trust boundaries beyond those already in the plan's threat model (T-05-06, T-05-07, T-05-08). All three mutating server actions call requireAdmin() as their first operation. OffersTab renders micro_internal_name only under /admin/projects/* which is Auth.js session-guarded.
Self-Check
src/components/admin/tabs/OffersTab.tsx exists: FOUND
src/lib/admin-queries.ts — projectOffers field in ProjectFullDetail: FOUND
src/lib/admin-queries.ts — getClientActiveOffers exported: FOUND
src/app/admin/projects/project-actions.ts — 3 offer actions: FOUND
src/app/admin/projects/[id]/page.tsx — OffersTab import + usage: FOUND (2 matches)
src/app/admin/clients/[id]/page.tsx — activeOffers render: FOUND (4 matches)