chore: merge executor worktree (worktree-agent-a26188c3b00c610ed)
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
phase: 12-offer-composition-drag-drop-csv-import
|
||||||
|
plan: 04
|
||||||
|
subsystem: ui
|
||||||
|
tags: [next.js, react, tailwind, shadcn]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 12-offer-composition-drag-drop-csv-import (Plan 03)
|
||||||
|
provides: getOfferListCards(), getOfferFieldOptions(), createOfferMacro server action
|
||||||
|
provides:
|
||||||
|
- "/admin/offers list page rewritten as category-filterable, archive-aware offer card grid (UI-SPEC section 1)"
|
||||||
|
- "OfferListClient component: category filter chips, archive toggle, '+ Nuova Offerta' inline create form, card grid linking to /admin/offers/[id]/edit"
|
||||||
|
affects: [12-05-offer-editor-page]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Thin server-component list page (Promise.all fetch -> client component), mirroring src/app/admin/catalog/page.tsx"
|
||||||
|
- "useTransition + router.refresh() for FormData server-action create flow without returning the new row's id"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/components/admin/offers/OfferListClient.tsx
|
||||||
|
modified:
|
||||||
|
- src/app/admin/offers/page.tsx
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Legacy macro/micro/offer_services CRUD UI removed from /admin/offers entirely per D-1 and UI-SPEC section 1; underlying actions.ts exports (createMacro, createMicro, createOfferService, deleteMacro, deleteMicro, updateMicroOfferServices) left untouched and unreferenced — zero-risk dead exports, confirmed via grep no other usages exist"
|
||||||
|
- "Card grid links to /admin/offers/${id}/edit (Plan 05's route) — link target correct even though Plan 05 may not have landed yet at verification time"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "'+ Nuova Offerta' inline create-form pattern: toggle visibility, startTransition(createOfferMacro(formData)), then router.refresh() — admin clicks the new card to open the editor (no auto-navigation, no id returned from the server action)"
|
||||||
|
|
||||||
|
requirements-completed: [OFFER-18]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 10min
|
||||||
|
completed: 2026-06-15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 12 Plan 04: Offer List Page Redesign Summary
|
||||||
|
|
||||||
|
**Rewrote `/admin/offers` from a legacy flat macro/micro/offer_services CRUD page into a category-filterable, archive-aware offer card grid with a "+ Nuova Offerta" inline creation flow, per UI-SPEC section 1.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 10 min
|
||||||
|
- **Started:** 2026-06-15T08:15:00Z (approx)
|
||||||
|
- **Completed:** 2026-06-15T08:25:30Z
|
||||||
|
- **Tasks:** 2 completed
|
||||||
|
- **Files modified:** 2 (1 rewritten, 1 created)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- `src/app/admin/offers/page.tsx` rewritten as a thin server component: `Promise.all([getOfferListCards(), getOfferFieldOptions()])` feeding `OfferListClient`, matching the `catalog/page.tsx` pattern (239 lines of legacy macro/micro/offer_services CRUD forms removed)
|
||||||
|
- New `src/components/admin/offers/OfferListClient.tsx`:
|
||||||
|
- Category filter chip row ("Tutti" + dynamic `categoryOptions`), active state `#1A463C` border/text, inactive `#e5e7eb`/`#71717a`
|
||||||
|
- "Mostra offerte archiviate" checkbox toggle (default off), filters via `useMemo` on `activeCategory` + `showArchived`
|
||||||
|
- Responsive card grid (1/2/3 columns) — each card is a `Link` to `/admin/offers/${card.id}/edit`, showing `internal_name`, `description` (line-clamp-2), category `Badge`, and red "Archiviata" text for archived offers
|
||||||
|
- Empty state: "Nessuna offerta" / "Inizia creando la tua prima offerta" + "+ Nuova Offerta" CTA
|
||||||
|
- "+ Nuova Offerta" header CTA toggles an inline form (`Input name="internal_name"` + "Crea" button); on submit calls `createOfferMacro` via `startTransition`, then `router.refresh()` — new card appears in the grid
|
||||||
|
- All literal copy strings verbatim from UI-SPEC copywriting contract table (Italian)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Rewrite /admin/offers page.tsx as the new list page entry point** - `68dc1b6` (feat)
|
||||||
|
2. **Task 2: Build OfferListClient — category filter, archive toggle, card grid, create CTA** - `7df4b9c` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (this commit, follows)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/app/admin/offers/page.tsx` - Thin server component: fetches `getOfferListCards()` + `getOfferFieldOptions()`, renders `OfferListClient`
|
||||||
|
- `src/components/admin/offers/OfferListClient.tsx` - Client component: category filter chips, archive toggle, responsive card grid, "+ Nuova Offerta" inline create form using `createOfferMacro`
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Removed all legacy macro/micro/offer_services CRUD UI from `/admin/offers` per D-1 and UI-SPEC section 1; confirmed via `grep -rn` that no other file in `src/app`/`src/components` references `createMacro`, `createMicro`, `createOfferService`, `deleteMacro`, `deleteMicro`, `toggleOfferServiceActive`, `getCatalogWithMicros`, `getAllOfferServices`, or `ServiceCheckboxList` outside the old page — these remain exported from `actions.ts`/`offer-queries.ts` as zero-risk dead code (per plan's explicit instruction not to remove them)
|
||||||
|
- Used shadcn `Button`, `Badge`, and `Input` components for CTA, category badge, and create-form text input respectively, matching the project's existing component library usage in `CatalogSearch.tsx`
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written. Both tasks implemented the documented component structure, state shape, filter logic, and copy strings verbatim.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required. Pure UI change consuming Plan 03's existing query/action exports.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- `/admin/offers` now renders the new card-grid list per UI-SPEC section 1; OFFER-18 satisfied (category filter + archive toggle, default hides archived)
|
||||||
|
- Each card links to `/admin/offers/${id}/edit` — Plan 05 builds this route in a parallel worktree; link target is correct regardless of Plan 05's landing order
|
||||||
|
- `npx tsc --noEmit` passes with zero errors across the full project; `npx eslint` clean for both modified/created files
|
||||||
|
- No legacy macro/micro/offer_services CRUD routes remain; underlying actions/queries left as unreferenced exports for future cleanup (out of this plan's scope)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 12-offer-composition-drag-drop-csv-import*
|
||||||
|
*Completed: 2026-06-15*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
All created/modified files verified present on disk:
|
||||||
|
|
||||||
|
- FOUND: src/app/admin/offers/page.tsx
|
||||||
|
- FOUND: src/components/admin/offers/OfferListClient.tsx
|
||||||
|
- FOUND: .planning/phases/12-offer-composition-drag-drop-csv-import/12-04-SUMMARY.md
|
||||||
|
|
||||||
|
All task commits verified present in git log:
|
||||||
|
|
||||||
|
- FOUND: 68dc1b6 (Task 1)
|
||||||
|
- FOUND: 7df4b9c (Task 2)
|
||||||
|
- FOUND: ca37126 (docs: plan summary)
|
||||||
@@ -1,238 +1,17 @@
|
|||||||
import { getCatalogWithMicros, getAllOfferServices } from "@/lib/offer-queries";
|
import { getOfferListCards, getOfferFieldOptions } from "@/lib/offer-queries";
|
||||||
import {
|
import { OfferListClient } from "@/components/admin/offers/OfferListClient";
|
||||||
createMacro,
|
|
||||||
createMicro,
|
|
||||||
createOfferService,
|
|
||||||
deleteMacro,
|
|
||||||
deleteMicro,
|
|
||||||
toggleOfferServiceActive,
|
|
||||||
} from "./actions";
|
|
||||||
import { ServiceCheckboxList } from "@/components/admin/offers/ServiceCheckboxList";
|
|
||||||
|
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
export default async function OffersPage() {
|
export default async function OffersPage() {
|
||||||
const [catalog, allServices] = await Promise.all([
|
const [cards, options] = await Promise.all([
|
||||||
getCatalogWithMicros(),
|
getOfferListCards(),
|
||||||
getAllOfferServices(),
|
getOfferFieldOptions(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto py-8 px-4 space-y-10">
|
|
||||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">Catalogo Offerte</h1>
|
|
||||||
|
|
||||||
{/* ── Sezione macro-offerte ── */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-lg font-semibold text-[#1a1a1a] mb-4">Macro-Offerte</h2>
|
|
||||||
|
|
||||||
{/* Create macro form */}
|
|
||||||
<form action={createMacro} className="bg-white rounded-lg border border-[#e5e7eb] p-4 mb-6 space-y-3 max-w-lg">
|
|
||||||
<p className="text-sm font-medium">Nuova Macro-Offerta</p>
|
|
||||||
<input
|
|
||||||
name="internal_name"
|
|
||||||
placeholder="Nome interno (es. Entry Offer)"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="public_name"
|
|
||||||
placeholder="Nome pubblico (es. Starter Branding)"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
name="transformation_promise"
|
|
||||||
placeholder="Promessa di trasformazione"
|
|
||||||
rows={2}
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-[#1A463C] text-white text-sm px-4 py-1.5 rounded hover:bg-[#163a31]"
|
|
||||||
>
|
|
||||||
Aggiungi Macro
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* List macros */}
|
|
||||||
<div className="space-y-8">
|
|
||||||
{catalog.map((macro) => (
|
|
||||||
<div key={macro.id} className="bg-white rounded-lg border border-[#e5e7eb] p-6">
|
|
||||||
<div className="flex items-start justify-between mb-1">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-[#1a1a1a]">{macro.internal_name}</p>
|
<OfferListClient cards={cards} categoryOptions={options.categoria} />
|
||||||
<p className="text-sm text-[#71717a]">Pubblico: {macro.public_name}</p>
|
|
||||||
{macro.transformation_promise && (
|
|
||||||
<p className="text-xs text-[#71717a] mt-1 italic">{macro.transformation_promise}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<form action={deleteMacro.bind(null, macro.id)}>
|
|
||||||
<button type="submit" className="text-xs text-red-600 hover:underline">
|
|
||||||
Elimina
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Micro-offers under this macro */}
|
|
||||||
<div className="mt-4 space-y-4 pl-4 border-l-2 border-[#e5e7eb]">
|
|
||||||
<p className="text-xs font-semibold text-[#71717a] uppercase tracking-wider">Micro-Offerte</p>
|
|
||||||
|
|
||||||
{macro.micros.map((micro) => (
|
|
||||||
<div key={micro.id} className="bg-[#f9f9f9] rounded-lg p-4 space-y-3">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">{micro.internal_name}</p>
|
|
||||||
<p className="text-xs text-[#71717a]">
|
|
||||||
Pubblico: {micro.public_name} ·{" "}
|
|
||||||
{micro.duration_months} {micro.duration_months === 1 ? "mese" : "mesi"}
|
|
||||||
</p>
|
|
||||||
{micro.transformation_promise && (
|
|
||||||
<p className="text-xs text-[#71717a] italic">{micro.transformation_promise}</p>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-[#1a1a1a] mt-1 font-medium">
|
|
||||||
Prezzo cumulativo: €{parseFloat(micro.cumulative_price).toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form action={deleteMicro.bind(null, micro.id)}>
|
|
||||||
<button type="submit" className="text-xs text-red-600 hover:underline">
|
|
||||||
Elimina
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/* Service assignment checkbox list */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-[#71717a] mb-2">Servizi inclusi:</p>
|
|
||||||
<ServiceCheckboxList
|
|
||||||
allServices={allServices.map((s) => ({
|
|
||||||
id: s.id,
|
|
||||||
name: s.name,
|
|
||||||
price: String(s.price),
|
|
||||||
}))}
|
|
||||||
assignedIds={micro.services.map((s) => s.id)}
|
|
||||||
microId={micro.id}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Create micro form */}
|
|
||||||
<form action={createMicro} className="bg-white rounded border border-[#e5e7eb] p-3 space-y-2">
|
|
||||||
<input type="hidden" name="macro_id" value={macro.id} />
|
|
||||||
<p className="text-xs font-medium">Nuova Micro-Offerta</p>
|
|
||||||
<input
|
|
||||||
name="internal_name"
|
|
||||||
placeholder="Nome interno"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-2 py-1 text-xs"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="public_name"
|
|
||||||
placeholder="Nome pubblico"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-2 py-1 text-xs"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
name="transformation_promise"
|
|
||||||
placeholder="Promessa di trasformazione"
|
|
||||||
rows={2}
|
|
||||||
className="w-full border rounded px-2 py-1 text-xs"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="text-xs">Durata (mesi):</label>
|
|
||||||
<input
|
|
||||||
name="duration_months"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
defaultValue="1"
|
|
||||||
required
|
|
||||||
className="w-16 border rounded px-2 py-1 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-[#1A463C] text-white text-xs px-3 py-1 rounded hover:bg-[#163a31]"
|
|
||||||
>
|
|
||||||
Aggiungi Micro
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{catalog.length === 0 && (
|
|
||||||
<p className="text-sm text-[#71717a]">Nessuna macro-offerta ancora. Creane una sopra.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ── Sezione servizi offerta ── */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-lg font-semibold text-[#1a1a1a] mb-4">Servizi Offerta</h2>
|
|
||||||
<p className="text-sm text-[#71717a] mb-4">
|
|
||||||
I servizi qui sono diversi dal catalogo preventivi — hanno una descrizione della
|
|
||||||
trasformazione e vengono raggruppati nelle micro-offerte.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* List services */}
|
|
||||||
<div className="bg-white rounded-lg border border-[#e5e7eb] divide-y divide-[#e5e7eb] mb-6">
|
|
||||||
{allServices.map((svc) => (
|
|
||||||
<div key={svc.id} className="flex items-center justify-between px-4 py-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">{svc.name}</p>
|
|
||||||
{svc.transformation_description && (
|
|
||||||
<p className="text-xs text-[#71717a]">{svc.transformation_description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-sm font-mono">€{parseFloat(String(svc.price)).toFixed(2)}</span>
|
|
||||||
<form action={toggleOfferServiceActive.bind(null, svc.id, !svc.active)}>
|
|
||||||
<button type="submit" className="text-xs text-[#71717a] hover:text-[#1a1a1a]">
|
|
||||||
{svc.active ? "Disattiva" : "Attiva"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{allServices.length === 0 && (
|
|
||||||
<p className="px-4 py-3 text-sm text-[#71717a]">Nessun servizio ancora.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create service form */}
|
|
||||||
<form
|
|
||||||
action={createOfferService}
|
|
||||||
className="bg-white rounded-lg border border-[#e5e7eb] p-4 space-y-3 max-w-lg"
|
|
||||||
>
|
|
||||||
<p className="text-sm font-medium">Nuovo Servizio Offerta</p>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
placeholder="Nome servizio"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="price"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="Prezzo (€)"
|
|
||||||
required
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
name="transformation_description"
|
|
||||||
placeholder="Descrizione trasformazione (marketing)"
|
|
||||||
rows={2}
|
|
||||||
className="w-full border rounded px-3 py-1.5 text-sm"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-[#1A463C] text-white text-sm px-4 py-1.5 rounded hover:bg-[#163a31]"
|
|
||||||
>
|
|
||||||
Aggiungi Servizio
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { createOfferMacro } from "@/app/admin/offers/actions";
|
||||||
|
import type { OfferListCard } from "@/lib/offer-queries";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
export function OfferListClient({
|
||||||
|
cards,
|
||||||
|
categoryOptions,
|
||||||
|
}: {
|
||||||
|
cards: OfferListCard[];
|
||||||
|
categoryOptions: string[];
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
||||||
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||||
|
const [newOfferName, setNewOfferName] = useState("");
|
||||||
|
|
||||||
|
const filteredCards = useMemo(() => {
|
||||||
|
return cards.filter(
|
||||||
|
(c) =>
|
||||||
|
(activeCategory === null || c.category === activeCategory) &&
|
||||||
|
(showArchived || !c.is_archived)
|
||||||
|
);
|
||||||
|
}, [cards, activeCategory, showArchived]);
|
||||||
|
|
||||||
|
function handleCreateSubmit(formData: FormData) {
|
||||||
|
startTransition(async () => {
|
||||||
|
await createOfferMacro(formData);
|
||||||
|
setShowCreateForm(false);
|
||||||
|
setNewOfferName("");
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCta = (
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bg-[#1A463C] text-white hover:bg-[#163a31]"
|
||||||
|
onClick={() => setShowCreateForm((v) => !v)}
|
||||||
|
>
|
||||||
|
+ Nuova Offerta
|
||||||
|
</Button>
|
||||||
|
{showCreateForm && (
|
||||||
|
<form
|
||||||
|
action={handleCreateSubmit}
|
||||||
|
className="flex items-center gap-2 bg-white border border-[#e5e7eb] rounded-lg p-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="internal_name"
|
||||||
|
required
|
||||||
|
placeholder="Nome offerta..."
|
||||||
|
value={newOfferName}
|
||||||
|
onChange={(e) => setNewOfferName(e.target.value)}
|
||||||
|
className="h-9 w-48"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
disabled={isPending}
|
||||||
|
className="bg-[#1A463C] text-white hover:bg-[#163a31]"
|
||||||
|
>
|
||||||
|
Crea
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Page header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-[20px] font-semibold text-[#1a1a1a]">Offerte</h2>
|
||||||
|
{createCta}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter row */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCategory(null)}
|
||||||
|
className={`rounded-full border px-3 py-1 text-sm transition-colors ${
|
||||||
|
activeCategory === null
|
||||||
|
? "border-[#1A463C] text-[#1A463C]"
|
||||||
|
: "border-[#e5e7eb] text-[#71717a]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Tutti
|
||||||
|
</button>
|
||||||
|
{categoryOptions.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCategory(category)}
|
||||||
|
className={`rounded-full border px-3 py-1 text-sm transition-colors ${
|
||||||
|
activeCategory === category
|
||||||
|
? "border-[#1A463C] text-[#1A463C]"
|
||||||
|
: "border-[#e5e7eb] text-[#71717a]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 text-sm text-[#71717a]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showArchived}
|
||||||
|
onChange={(e) => setShowArchived(e.target.checked)}
|
||||||
|
className="h-4 w-4 rounded border-[#e5e7eb]"
|
||||||
|
/>
|
||||||
|
Mostra offerte archiviate
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card grid / empty state */}
|
||||||
|
{filteredCards.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-[#e5e7eb] bg-white py-16 text-center">
|
||||||
|
<h3 className="text-[16px] font-semibold text-[#1a1a1a]">Nessuna offerta</h3>
|
||||||
|
<p className="text-sm text-[#71717a]">Inizia creando la tua prima offerta</p>
|
||||||
|
<div className="mt-2">{createCta}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredCards.map((card) => (
|
||||||
|
<Link
|
||||||
|
key={card.id}
|
||||||
|
href={`/admin/offers/${card.id}/edit`}
|
||||||
|
className="cursor-pointer rounded-lg border border-[#e5e7eb] bg-white p-4 transition-shadow hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]"
|
||||||
|
>
|
||||||
|
<h3 className="text-[16px] font-semibold text-[#1a1a1a]">{card.internal_name}</h3>
|
||||||
|
{card.description && (
|
||||||
|
<p className="mt-1 line-clamp-2 text-sm text-[#71717a]">{card.description}</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-3 flex items-center gap-2">
|
||||||
|
{card.category && <Badge variant="secondary">{card.category}</Badge>}
|
||||||
|
{card.is_archived && (
|
||||||
|
<span className="text-xs font-semibold text-[#dc2626]">Archiviata</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user