```
---
### `src/components/admin/tabs/QuoteTab.tsx` (component client, CRUD)
**Analog:** `src/components/admin/tabs/PaymentsTab.tsx`
**Pattern:** Async server component (not client) that receives props (items, services, acceptedTotal, clientId), renders multiple form sections, each with its own Server Action call.
**PaymentsTab structure** (lines 22–54):
```typescript
export async function PaymentsTab({ payments, acceptedTotal, clientId }: Props) {
return (
Totale preventivo
{payments.map((p) => (
{/* ... */}
))}
);
}
```
**For QuoteTab:** Structure as three sections:
1. Add items (dropdown catalog + qty OR toggle to custom label/price/qty)
2. Quote items table (Voce | Qty | Unit Price | Subtotal | Delete)
3. Accepted total (editable input + Save button)
Each section is its own form with inline Server Action call. Use same card styling (`bg-white border border-[#e5e7eb] rounded-lg p-4`).
---
### `src/app/admin/clients/[id]/quote-actions.ts` (server-actions, CRUD)
**Analog:** `src/app/admin/clients/[id]/actions.ts`
**Pattern:** Identical to catalog actions — Zod validation, FormData parsing, numeric precision handling.
**Numeric precision pattern** (lines 192–211):
```typescript
export async function updateAcceptedTotal(clientId: string, formData: FormData) {
const raw = (formData.get("accepted_total") as string)?.trim();
const val = parseFloat(raw);
if (isNaN(val) || val < 0) throw new Error("Importo non valido");
await db
.update(clients)
.set({ accepted_total: val.toFixed(2) })
.where(eq(clients.id, clientId));
revalidatePath(`/admin/clients/${clientId}`);
}
```
**For Quote Actions:** Implement:
- `addQuoteItem(clientId, formData)` — parse service_id (nullable), custom_label (nullable), quantity, unit_price. Calculate subtotal. Insert into quote_items.
- `removeQuoteItem(quoteItemId, clientId)` — delete from quote_items.
- `updateAcceptedTotal(clientId, formData)` — identical to existing pattern in actions.ts.
All paths: `revalidatePath(/admin/clients/${clientId})`.
---
### `src/components/admin/NavBar.tsx` (component, request-response — MODIFIED)
**Analog:** `src/components/admin/NavBar.tsx`
**Current structure** (lines 7–29):
```typescript
export function NavBar() {
return (
);
}
```
**Modification:** Add new Link after "Statistiche":
```typescript
Catalogo
```
---
### `src/db/schema.ts` (config — MODIFIED)
**Analog:** `src/db/schema.ts`
**Current quote_items definition** (lines 159–172):
```typescript
export const quote_items = pgTable("quote_items", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
client_id: text("client_id")
.notNull()
.references(() => clients.id, { onDelete: "cascade" }),
service_id: text("service_id")
.notNull()
.references(() => service_catalog.id, { onDelete: "restrict" }),
quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(),
unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(),
subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(),
});
```
**Required changes:**
1. **Make service_id nullable** (line 166–168):
```typescript
service_id: text("service_id")
.references(() => service_catalog.id, { onDelete: "restrict" }),
// removed .notNull()
```
2. **Add custom_label field** (after subtotal):
```typescript
custom_label: text("custom_label"),
```
**After schema changes:**
- Run `npx drizzle-kit push` to apply migrations to database
- Verify no TypeScript errors in types (QuoteItem type will auto-update)
---
## Shared Patterns
### Form Validation (All CRUD Actions)
**Source:** `src/app/admin/clients/[id]/actions.ts` lines 20–24, 138–141
**Pattern:** Use Zod schema with `.safeParse()`, throw first error message.
**Apply to:** All catalog and quote actions
```typescript
import { z } from "zod";
const serviceSchema = z.object({
name: z.string().min(1, "Nome richiesto"),
description: z.string().optional(),
unit_price: z.coerce.number().positive("Prezzo deve essere positivo"),
});
export async function createService(formData: FormData) {
const parsed = serviceSchema.safeParse({
name: formData.get("name"),
description: formData.get("description") ?? "",
unit_price: formData.get("unit_price"),
});
if (!parsed.success) throw new Error(parsed.error.issues[0].message);
await db.insert(service_catalog).values(parsed.data);
revalidatePath("/admin/catalog");
}
```
### Inline Edit Component Pattern (ServiceTable, ServiceRow)
**Source:** `src/components/admin/DocumentRow.tsx` lines 10–114
**Pattern:**
- "use client" directive
- useState for `editing`, `error`
- useTransition for async form submission
- useRouter for refresh
- Toggle render: editing mode (form inputs) vs read mode (display + hover buttons)
- Server Action called inline in form action
**Apply to:** ServiceTable with per-row inline edit.
### Currency Formatting
**Source:** `src/components/admin/ClientRow.tsx` line 33
**Pattern:**
```typescript
€{parseFloat(amount).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
```
**Apply to:** All price displays in ServiceTable and QuoteTab.
### Table Styling
**Source:** `src/app/admin/page.tsx` lines 46–64
**Pattern:**
```typescript
Colonna
{items.map(item => (
…
))}
```
**Apply to:** ServiceTable layout in catalog/page.tsx
### Card Styling (Forms, Sections)
**Source:** `src/components/admin/tabs/DocumentsTab.tsx` line 18
**Pattern:**
```typescript
Titolo
{/* content */}
```
**Apply to:** All form sections in QuoteTab and ServiceTable.
### Label + Input Grid
**Source:** `src/components/admin/tabs/DocumentsTab.tsx` lines 20–39
**Pattern:**
```typescript
```
**Apply to:** All form inputs in catalog and quote builders.
### Numeric Input Pattern
**Source:** `src/components/admin/tabs/PaymentsTab.tsx` lines 36–45
**Pattern:**
```typescript
```
**Apply to:** All price/quantity inputs; use `step="0.01"` for EUR precision.
---
## No Analog Found
No files require external patterns. All code patterns (Server Actions, inline edit, table layout, form validation) exist in the codebase.
---
## Query Pattern (for page data fetching)
**Not extracted as code** — will be implemented in quote-actions.ts and documented in planning phase.
Example from RESEARCH.md:
```typescript
// Get all active services for dropdown
const activeServices = await db
.select()
.from(service_catalog)
.where(eq(service_catalog.active, true))
.orderBy(asc(service_catalog.name));
// Get quote items with service names
const items = await db
.select({
id: quote_items.id,
label: sql`COALESCE(${service_catalog.name}, ${quote_items.custom_label})`,
quantity: quote_items.quantity,
unit_price: quote_items.unit_price,
subtotal: quote_items.subtotal,
})
.from(quote_items)
.leftJoin(service_catalog, eq(quote_items.service_id, service_catalog.id))
.where(eq(quote_items.client_id, clientId));
```
---
## Metadata
**Analog search scope:** `/src/app/admin/`, `/src/components/admin/`, `/src/app/admin/clients/[id]/`
**Files scanned:** 13 analog files
**Pattern extraction date:** 2026-05-17
**Coverage summary:**
- Exact match (same role + data flow): 7/7
- Role-match (same role, similar flow): 0
- No analog: 0
**Key insights:**
- Phase 2 established Server Actions + Zod pattern — directly reusable for Phase 3 CRUD
- Inline edit pattern from DocumentRow is the gold standard for catalog service editing
- PaymentsTab structure fits QuoteTab exactly (multiple form sections, each with own Server Action)
- Table styling is consistent across admin interface — use directly
- No new dependencies or libraries needed — all patterns are vanilla React + Next.js built-ins