feat: wire offer→project phases/tasks, redo Offerte tab, cleanup project tabs

- importOfferIntoProject(): materializes project phases/tasks from the assigned
  tier's services grouped by services.fase (merge-by-title, dedup tasks)
- assignOfferToProject: optional import_phases flag triggers the import
- getProjectFullDetail: projectOffers/availableMicros now include macro name +
  tier_letter (dropdown shows "Offerta — Tier X"); availableMicros filters
  non-archived macros
- OffersTab redone: shows macro + tier badge, "Importa fasi e task" checkbox
- removed project "Preventivo" tab + deleted QuoteTab.tsx (legacy quote_items)
- sidebar: Lead before Clienti
- no DB migration (reuses existing tables)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:08:28 +02:00
parent f5f90cd643
commit 1824cb643f
7 changed files with 169 additions and 369 deletions
+37 -9
View File
@@ -14,6 +14,8 @@ type AvailableMicro = {
internal_name: string;
public_name: string;
duration_months: number;
tier_letter: string | null;
macro_internal_name: string;
};
interface OffersTabProps {
@@ -22,6 +24,10 @@ interface OffersTabProps {
availableMicros: AvailableMicro[];
}
function durationLabel(months: number): string {
return `${months} ${months === 1 ? "mese" : "mesi"}`;
}
export function OffersTab({ projectId, projectOffers, availableMicros }: OffersTabProps) {
const [isPending, startTransition] = useTransition();
const router = useRouter();
@@ -64,10 +70,16 @@ export function OffersTab({ projectId, projectOffers, availableMicros }: OffersT
className="bg-white rounded-lg border border-[#e5e7eb] p-4 flex items-start justify-between gap-4"
>
<div className="flex-1">
<p className="text-sm font-medium text-[#1a1a1a]">{offer.micro_internal_name}</p>
<p className="text-sm font-medium text-[#1a1a1a]">
{offer.macro_internal_name}
{offer.tier_letter && (
<span className="ml-2 inline-block rounded bg-[#1A463C]/10 text-[#1A463C] text-[11px] font-semibold px-1.5 py-0.5 align-middle">
Tier {offer.tier_letter}
</span>
)}
</p>
<p className="text-xs text-[#71717a]">
Pubblico: {offer.micro_public_name} &middot; {offer.micro_duration_months}{" "}
{offer.micro_duration_months === 1 ? "mese" : "mesi"}
Pubblico: {offer.micro_public_name} &middot; {durationLabel(offer.micro_duration_months)}
</p>
<p className="text-xs text-[#71717a] mt-1">
Inizio: {new Date(offer.start_date).toLocaleDateString("it-IT")}
@@ -107,7 +119,7 @@ export function OffersTab({ projectId, projectOffers, availableMicros }: OffersT
{/* Assignment form */}
<div>
<h3 className="text-sm font-semibold text-[#1a1a1a] mb-3">Assegna Micro-Offerta</h3>
<h3 className="text-sm font-semibold text-[#1a1a1a] mb-3">Assegna Offerta</h3>
<form
ref={formRef}
action={handleAssign}
@@ -116,17 +128,18 @@ export function OffersTab({ projectId, projectOffers, availableMicros }: OffersT
<input type="hidden" name="project_id" value={projectId} />
<div>
<label className="text-xs text-[#71717a] block mb-1">Micro-offerta</label>
<label className="text-xs text-[#71717a] block mb-1">Offerta</label>
<select
name="micro_id"
required
className="w-full border rounded px-3 py-1.5 text-sm"
>
<option value="">Seleziona micro-offerta...</option>
<option value="">Seleziona offerta...</option>
{availableMicros.map((m) => (
<option key={m.id} value={m.id}>
{m.internal_name} ({m.duration_months}{" "}
{m.duration_months === 1 ? "mese" : "mesi"})
{m.macro_internal_name}
{m.tier_letter ? ` — Tier ${m.tier_letter}` : ""} · {m.public_name} (
{durationLabel(m.duration_months)})
</option>
))}
</select>
@@ -144,6 +157,21 @@ export function OffersTab({ projectId, projectOffers, availableMicros }: OffersT
/>
</div>
<label className="flex items-start gap-2 text-xs text-[#1a1a1a] cursor-pointer">
<input
type="checkbox"
name="import_phases"
defaultChecked
className="mt-0.5 h-3.5 w-3.5 accent-[#1A463C]"
/>
<span>
Importa fasi e task dall&apos;offerta
<span className="block text-[#71717a]">
Crea le fasi raggruppando i servizi per &laquo;Fase&raquo;; ogni servizio diventa un task.
</span>
</span>
</label>
<button
type="submit"
disabled={isPending || availableMicros.length === 0}
@@ -154,7 +182,7 @@ export function OffersTab({ projectId, projectOffers, availableMicros }: OffersT
{availableMicros.length === 0 && (
<p className="text-xs text-[#71717a]">
Nessuna micro-offerta disponibile. Crea prima una micro-offerta in{" "}
Nessuna offerta disponibile. Crea prima un&apos;offerta in{" "}
<a href="/admin/offers" className="underline">Offerte</a>.
</p>
)}