feat(05-02): /admin/offers RSC page + ServiceCheckboxList + NavBar update

- Create src/app/admin/offers/page.tsx — RSC with macro/micro/service CRUD sections
- Create src/components/admin/offers/ServiceCheckboxList.tsx — client checkbox UI for service-to-micro assignment
- Update src/components/admin/NavBar.tsx — add Forecast and Offerte links (order: Statistiche | Forecast | Catalogo | Offerte | Impostazioni)
This commit is contained in:
2026-05-30 16:22:19 +02:00
parent 56f084912a
commit ce8f95a163
3 changed files with 295 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
import { getCatalogWithMicros, getAllOfferServices } from "@/lib/offer-queries";
import {
createMacro,
createMicro,
createOfferService,
deleteMacro,
deleteMicro,
toggleOfferServiceActive,
} from "./actions";
import { ServiceCheckboxList } from "@/components/admin/offers/ServiceCheckboxList";
export const revalidate = 0;
export default async function OffersPage() {
const [catalog, allServices] = await Promise.all([
getCatalogWithMicros(),
getAllOfferServices(),
]);
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>
<p className="font-semibold text-[#1a1a1a]">{macro.internal_name}</p>
<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} &middot;{" "}
{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>
);
}
+6
View File
@@ -18,9 +18,15 @@ export function NavBar() {
<Link href="/admin/analytics" className="text-sm text-white/70 hover:text-white transition-colors">
Statistiche
</Link>
<Link href="/admin/forecast" className="text-sm text-white/70 hover:text-white transition-colors">
Forecast
</Link>
<Link href="/admin/catalog" className="text-sm text-white/70 hover:text-white transition-colors">
Catalogo
</Link>
<Link href="/admin/offers" className="text-sm text-white/70 hover:text-white transition-colors">
Offerte
</Link>
<Link href="/admin/impostazioni" className="text-sm text-white/70 hover:text-white transition-colors">
Impostazioni
</Link>
@@ -0,0 +1,51 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { updateMicroOfferServices } from "@/app/admin/offers/actions";
export function ServiceCheckboxList({
allServices,
assignedIds,
microId,
}: {
allServices: Array<{ id: string; name: string; price: string }>;
assignedIds: string[];
microId: string;
}) {
const [selected, setSelected] = useState(new Set(assignedIds));
const [isPending, startTransition] = useTransition();
const router = useRouter();
function toggle(serviceId: string) {
const next = new Set(selected);
if (next.has(serviceId)) next.delete(serviceId);
else next.add(serviceId);
setSelected(next);
startTransition(async () => {
await updateMicroOfferServices(microId, [...next]);
router.refresh();
});
}
return (
<div className="space-y-1">
{allServices.map((svc) => (
<label key={svc.id} className="flex items-center gap-2 cursor-pointer text-sm">
<input
type="checkbox"
checked={selected.has(svc.id)}
onChange={() => toggle(svc.id)}
disabled={isPending}
className="rounded"
/>
<span>{svc.name}</span>
<span className="ml-auto text-xs text-[#71717a]">{parseFloat(svc.price).toFixed(2)}</span>
</label>
))}
{allServices.length === 0 && (
<p className="text-xs text-[#71717a]">Nessun servizio nel catalogo ancora.</p>
)}
</div>
);
}