fix(05): apply checker revisions — OFFER-04 client coverage, RESOLVED questions, wave fix, verify fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ files_modified:
|
||||
- src/app/admin/projects/[id]/page.tsx
|
||||
- src/app/admin/projects/project-actions.ts
|
||||
- src/components/admin/tabs/OffersTab.tsx
|
||||
- src/app/admin/clients/[id]/page.tsx
|
||||
requirements_addressed: [OFFER-04]
|
||||
autonomous: true
|
||||
must_haves:
|
||||
@@ -18,6 +19,7 @@ must_haves:
|
||||
- "The Offerte tab shows all active offers assigned to a project with micro name, duration, and accepted_total"
|
||||
- "Admin can set the accepted_total on a project offer assignment"
|
||||
- "ProjectFullDetail type includes projectOffers array"
|
||||
- "Client detail page /admin/clients/[id] shows active offers summary (offer public_name + project name) for all of that client's projects"
|
||||
artifacts:
|
||||
- path: "src/components/admin/tabs/OffersTab.tsx"
|
||||
provides: "Client component for assigning and viewing project offers"
|
||||
@@ -25,7 +27,14 @@ must_haves:
|
||||
- path: "src/app/admin/projects/project-actions.ts"
|
||||
provides: "Server actions for project offer assignment mutations"
|
||||
contains: "assignOfferToProject, removeProjectOffer, updateProjectOfferTotal"
|
||||
- path: "src/app/admin/clients/[id]/page.tsx"
|
||||
provides: "Client detail page extended with active offers summary section"
|
||||
contains: "active offers per project listed with public_name and project name"
|
||||
key_links:
|
||||
- from: "src/app/admin/clients/[id]/page.tsx"
|
||||
to: "src/lib/admin-queries.ts"
|
||||
via: "getClientWithProjectsAndOffers() or extended getClientWithProjects()"
|
||||
pattern: "activeOffers"
|
||||
- from: "src/app/admin/projects/[id]/page.tsx"
|
||||
to: "src/lib/admin-queries.ts"
|
||||
via: "getProjectFullDetail() — extended to include projectOffers"
|
||||
@@ -517,6 +526,131 @@ And after the last `<TabsContent>`:
|
||||
<done>OffersTab created; project workspace page has Offerte tab; admin can assign micro-offers and set accepted_total; TypeScript compiles</done>
|
||||
</task>
|
||||
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add getClientActiveOffers query + client detail page active offers summary</name>
|
||||
<files>
|
||||
src/lib/admin-queries.ts
|
||||
src/app/admin/clients/[id]/page.tsx
|
||||
</files>
|
||||
|
||||
<read_first>
|
||||
- src/lib/admin-queries.ts — read the getClientWithProjects function and ClientWithProjects type (to extend alongside, not replace)
|
||||
- src/app/admin/clients/[id]/page.tsx — read the FULL file; understand the current project card layout
|
||||
- src/db/schema.ts — confirm project_offers.micro_id, offer_micros.public_name, project_offers.project_id column names (after 05-01)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
**Add new query to `src/lib/admin-queries.ts`:**
|
||||
|
||||
Add `offer_micros` and `project_offers` to the existing schema imports at the top (they were added in Task 1 of this plan). Then add the following new exported type and function after `getClientWithProjects()`:
|
||||
|
||||
```typescript
|
||||
export type ClientActiveOfferSummary = {
|
||||
offer_id: string;
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
public_name: string; // offer_micros.public_name — NEVER internal_name
|
||||
accepted_total: string | null;
|
||||
};
|
||||
|
||||
export async function getClientActiveOffers(clientId: string): Promise<ClientActiveOfferSummary[]> {
|
||||
const projectRows = await db
|
||||
.select({ id: projects.id, name: projects.name })
|
||||
.from(projects)
|
||||
.where(eq(projects.client_id, clientId));
|
||||
|
||||
if (projectRows.length === 0) return [];
|
||||
|
||||
const projectIds = projectRows.map((p) => p.id);
|
||||
const projectNameMap = new Map(projectRows.map((p) => [p.id, p.name]));
|
||||
|
||||
// Explicit column selection — public_name only, NEVER internal_name
|
||||
const rows = await db
|
||||
.select({
|
||||
offer_id: project_offers.id,
|
||||
project_id: project_offers.project_id,
|
||||
public_name: offer_micros.public_name,
|
||||
accepted_total: project_offers.accepted_total,
|
||||
})
|
||||
.from(project_offers)
|
||||
.innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id))
|
||||
.where(inArray(project_offers.project_id, projectIds))
|
||||
.orderBy(asc(project_offers.created_at));
|
||||
|
||||
return rows.map((r) => ({
|
||||
offer_id: r.offer_id,
|
||||
project_id: r.project_id,
|
||||
project_name: projectNameMap.get(r.project_id) ?? "—",
|
||||
public_name: r.public_name,
|
||||
accepted_total: r.accepted_total ? String(r.accepted_total) : null,
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Note: `inArray`, `asc`, `eq` are already imported. `project_offers` and `offer_micros` schema imports were added in Task 1 of this plan.
|
||||
|
||||
**Extend `src/app/admin/clients/[id]/page.tsx`:**
|
||||
|
||||
Read the full file. It currently calls `getClientWithProjects(id)` and awaits a single result. Make these three targeted changes:
|
||||
|
||||
1. Add `getClientActiveOffers` to the existing import:
|
||||
```typescript
|
||||
import { getClientWithProjects, getClientActiveOffers } from "@/lib/admin-queries";
|
||||
```
|
||||
|
||||
2. Replace the single `await getClientWithProjects(id)` call with a parallel fetch:
|
||||
```typescript
|
||||
const [data, activeOffers] = await Promise.all([
|
||||
getClientWithProjects(id),
|
||||
getClientActiveOffers(id),
|
||||
]);
|
||||
if (!data) notFound();
|
||||
```
|
||||
|
||||
3. Add the "Offerte Attive" summary section at the bottom of the JSX return, after the `archivedProjects` block and before the closing `</div>` of the root element:
|
||||
```tsx
|
||||
{activeOffers.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<p className="text-xs text-[#71717a] font-semibold uppercase tracking-wider mb-3">
|
||||
Offerte Attive ({activeOffers.length})
|
||||
</p>
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] divide-y divide-[#e5e7eb]">
|
||||
{activeOffers.map((offer) => (
|
||||
<div key={offer.offer_id} className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#1a1a1a]">{offer.public_name}</p>
|
||||
<p className="text-xs text-[#71717a]">{offer.project_name}</p>
|
||||
</div>
|
||||
{offer.accepted_total && (
|
||||
<span className="text-sm font-mono text-[#1a1a1a]">
|
||||
€{parseFloat(offer.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0
|
||||
- `grep "getClientActiveOffers" src/lib/admin-queries.ts` matches (new function exported)
|
||||
- `grep "ClientActiveOfferSummary" src/lib/admin-queries.ts` matches (type exported)
|
||||
- `grep "getClientActiveOffers" "src/app/admin/clients/[id]/page.tsx"` matches (function called on page)
|
||||
- `grep "activeOffers" "src/app/admin/clients/[id]/page.tsx"` returns at least 2 matches (fetch + render)
|
||||
- `grep "internal_name" src/lib/admin-queries.ts` — must NOT appear in the new getClientActiveOffers query block
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>getClientActiveOffers() exported from admin-queries.ts; /admin/clients/[id] page shows active offers summary with public_name and project name; TypeScript compiles</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
@@ -537,13 +671,15 @@ And after the last `<TabsContent>`:
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After both tasks complete:
|
||||
After all three tasks complete:
|
||||
- `npx tsc --noEmit` exits 0
|
||||
- Visit `/admin/projects/[id]` → Offerte tab is visible
|
||||
- Offerte tab shows empty state when no offers assigned
|
||||
- Assign a micro-offer → appears in the active list
|
||||
- Set accepted_total by blurring the input → value persists on refresh
|
||||
- Remove an assignment → disappears from list
|
||||
- Visit `/admin/clients/[id]` for a client with active project offers → "Offerte Attive" section appears at the bottom with public_name and project name
|
||||
- Client with no active offers → no Offerte Attive section visible
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
@@ -552,6 +688,7 @@ After both tasks complete:
|
||||
3. Admin can update the accepted_total inline (onBlur save)
|
||||
4. Admin can remove a project offer assignment
|
||||
5. All changes revalidate /admin/forecast path
|
||||
6. Client detail page /admin/clients/[id] shows active offers summary (public_name + project name) for all projects belonging to that client
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user