Files
clienthub/src/app/admin/projects/[id]/page.tsx
T
simone 4b135ce67f refactor(progetti): via il tab Commenti e il timer dalla lista
Due rimozioni chieste esplicitamente, che tolgono due doppioni.

Il tab "Commenti" del progetto duplicava /admin/conversazioni. Non era una
scorciatoia: era la seconda copia. `buildEntityMap()` in conversations-queries
cammina clienti -> progetti -> fasi -> task -> deliverable e raccoglie TUTTI i
commenti con l'etichetta dell'entita' di origine, quindi l'inbox e' un
sovrainsieme stretto di quel tab. La lettura non perde niente.

Una cosa la perde, e va detta: dal tab si poteva rispondere sulla singola
entita', mentre `replyToConversation` salva sempre sul thread generale. Non e'
una regressione introdotta qui — e' una scelta di prodotto gia' presa e gia'
annotata in conversazioni/actions.ts — ma da oggi e' l'unica via, e il commento
la' sopra ora lo dice.

Il timer nella lista progetti era l'altro doppione: si avvia e si ferma dentro
il progetto, dove c'e' il contesto per sapere su cosa stai lavorando. Toglierlo
elimina anche una query per pagina (la scansione delle entry aperte).

Cade di conseguenza il codice rimasto senza chiamanti: CommentsTab.tsx,
`postAdminComment`, il campo `comments` di ProjectFullDetail con la sua query, e
i due campi activeTimer* di ProjectWithPayments. `totalTrackedSeconds` resta:
serve al calcolo del EUR/h, che in lista ci sta ancora.

Build e lint puliti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:30:58 +02:00

137 lines
4.4 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 { 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,
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 &amp; Task</TabsTrigger>
<TabsTrigger value="payments">Pagamenti</TabsTrigger>
<TabsTrigger value="documents">Documenti</TabsTrigger>
<TabsTrigger value="notes">Note</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="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>
);
}