diff --git a/src/app/admin/projects/[id]/page.tsx b/src/app/admin/projects/[id]/page.tsx index 8ef0f70..c2889d5 100644 --- a/src/app/admin/projects/[id]/page.tsx +++ b/src/app/admin/projects/[id]/page.tsx @@ -8,6 +8,7 @@ import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab"; import { CommentsTab } from "@/components/admin/tabs/CommentsTab"; import { QuoteTab } from "@/components/admin/tabs/QuoteTab"; import { TimerTab } from "@/components/admin/tabs/TimerTab"; +import { OffersTab } from "@/components/admin/tabs/OffersTab"; import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle"; import Link from "next/link"; @@ -38,6 +39,8 @@ export default async function ProjectDetailPage({ activeTimerEntryId, activeTimerStartedAt, totalTrackedSeconds, + projectOffers, + availableMicros, } = detail; return ( @@ -71,6 +74,7 @@ export default async function ProjectDetailPage({ Commenti Preventivo Timer + Offerte @@ -133,6 +137,14 @@ export default async function ProjectDetailPage({ targetHourlyRate={targetHourlyRate} /> + + + + ); diff --git a/src/components/admin/tabs/OffersTab.tsx b/src/components/admin/tabs/OffersTab.tsx new file mode 100644 index 0000000..9099650 --- /dev/null +++ b/src/components/admin/tabs/OffersTab.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useTransition, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { + assignOfferToProject, + removeProjectOffer, + updateProjectOfferTotal, +} from "@/app/admin/projects/project-actions"; +import type { ProjectOfferWithMicro } from "@/lib/admin-queries"; + +type AvailableMicro = { + id: string; + internal_name: string; + public_name: string; + duration_months: number; +}; + +interface OffersTabProps { + projectId: string; + projectOffers: ProjectOfferWithMicro[]; + availableMicros: AvailableMicro[]; +} + +export function OffersTab({ projectId, projectOffers, availableMicros }: OffersTabProps) { + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + const formRef = useRef(null); + + function handleAssign(formData: FormData) { + startTransition(async () => { + await assignOfferToProject(formData); + formRef.current?.reset(); + router.refresh(); + }); + } + + function handleRemove(offerId: string) { + startTransition(async () => { + await removeProjectOffer(offerId, projectId); + router.refresh(); + }); + } + + function handleTotalUpdate(offerId: string, value: string) { + startTransition(async () => { + await updateProjectOfferTotal(offerId, projectId, value); + router.refresh(); + }); + } + + return ( +
+ {/* Active assignments */} +
+

Offerte Attive

+ {projectOffers.length === 0 ? ( +

Nessuna offerta assegnata a questo progetto.

+ ) : ( +
+ {projectOffers.map((offer) => ( +
+
+

{offer.micro_internal_name}

+

+ Pubblico: {offer.micro_public_name} · {offer.micro_duration_months}{" "} + {offer.micro_duration_months === 1 ? "mese" : "mesi"} +

+

+ Inizio: {new Date(offer.start_date).toLocaleDateString("it-IT")} +

+ {/* Accepted total inline edit */} +
+ + { + const val = e.currentTarget.value.trim(); + if (val !== (offer.accepted_total ?? "")) { + handleTotalUpdate(offer.id, val); + } + }} + className="w-24 border rounded px-2 py-0.5 text-xs" + /> +
+
+ +
+ ))} +
+ )} +
+ + {/* Assignment form */} +
+

Assegna Micro-Offerta

+
+ + +
+ + +
+ +
+ + +
+ + + + {availableMicros.length === 0 && ( +

+ Nessuna micro-offerta disponibile. Crea prima una micro-offerta in{" "} + Offerte. +

+ )} +
+
+
+ ); +}