186bb9ea19
- Pagamenti: totale ereditato dalla somma accepted_total delle offerte attive (con override) + selettore schema rate 1/2/3 step; nuova colonna payments.percent per rescalare gli importi preservando label/stato - Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata - Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili - Transcript del cliente visibili in tab Documenti admin e dashboard pubblica - Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry - CLAUDE.md: procedura Deploy & DB Access; push su main automatico Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import { getProjectFullDetail, getRecentTimeEntries } from "@/lib/admin-queries";
|
|
import { getTargetHourlyRate } from "@/lib/settings";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { PhasesTab } from "@/components/admin/tabs/PhasesTab";
|
|
import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab";
|
|
import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab";
|
|
import { CommentsTab } from "@/components/admin/tabs/CommentsTab";
|
|
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";
|
|
|
|
export const revalidate = 0;
|
|
|
|
export default async function ProjectDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = await params;
|
|
const [detail, targetHourlyRate] = await Promise.all([
|
|
getProjectFullDetail(id),
|
|
getTargetHourlyRate(),
|
|
]);
|
|
|
|
if (!detail) notFound();
|
|
|
|
const recentEntries = await getRecentTimeEntries(id, 5);
|
|
|
|
const {
|
|
project,
|
|
phases,
|
|
payments,
|
|
documents,
|
|
notes,
|
|
comments,
|
|
activeTimerEntryId,
|
|
activeTimerStartedAt,
|
|
totalTrackedSeconds,
|
|
projectOffers,
|
|
availableMicros,
|
|
offersAcceptedTotal,
|
|
transcripts,
|
|
} = detail;
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4">
|
|
<Link href="/admin/projects" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
|
|
← Progetti
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-[#1a1a1a]">{project.name}</h1>
|
|
<p className="text-sm text-[#71717a]">
|
|
<Link
|
|
href={`/admin/clients/${project.client.id}`}
|
|
className="hover:text-[#1a1a1a] hover:underline"
|
|
>
|
|
{project.client.name}
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="phases" className="w-full">
|
|
<TabsList className="mb-6">
|
|
<TabsTrigger value="phases">Fasi & Task</TabsTrigger>
|
|
<TabsTrigger value="payments">Pagamenti</TabsTrigger>
|
|
<TabsTrigger value="documents">Documenti</TabsTrigger>
|
|
<TabsTrigger value="notes">Note</TabsTrigger>
|
|
<TabsTrigger value="comments">Commenti</TabsTrigger>
|
|
<TabsTrigger value="timer">Timer</TabsTrigger>
|
|
<TabsTrigger value="offers">Offerte</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="phases">
|
|
<PhasesViewToggle
|
|
listView={<PhasesTab phases={phases} clientId={id} />}
|
|
phases={phases}
|
|
clientId={id}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="payments">
|
|
<PaymentsTab
|
|
payments={payments}
|
|
acceptedTotal={project.accepted_total ?? "0"}
|
|
clientId={id}
|
|
projectId={id}
|
|
offersAcceptedTotal={offersAcceptedTotal}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="documents">
|
|
<DocumentsTab documents={documents} clientId={id} transcripts={transcripts} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="notes">
|
|
<div className="space-y-4 max-w-lg">
|
|
{notes.length === 0 && (
|
|
<p className="text-sm text-[#71717a]">Nessuna nota ancora.</p>
|
|
)}
|
|
{notes.map((note) => (
|
|
<div key={note.id} className="bg-white rounded-lg border border-[#e5e7eb] p-4">
|
|
<p className="text-sm text-[#1a1a1a] whitespace-pre-wrap">{note.body}</p>
|
|
<p className="text-xs text-[#71717a] mt-2">
|
|
{new Date(note.created_at).toLocaleDateString("it-IT")}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="comments">
|
|
<CommentsTab comments={comments} phases={phases} clientId={id} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="timer">
|
|
<TimerTab
|
|
projectId={id}
|
|
acceptedTotal={project.accepted_total ?? "0"}
|
|
activeTimerEntryId={activeTimerEntryId}
|
|
activeTimerStartedAt={activeTimerStartedAt}
|
|
totalTrackedSeconds={totalTrackedSeconds}
|
|
targetHourlyRate={targetHourlyRate}
|
|
recentEntries={recentEntries}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="offers">
|
|
<OffersTab
|
|
projectId={id}
|
|
projectOffers={projectOffers}
|
|
availableMicros={availableMicros}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
} |