64030afef3
1. AdminSidebar: removed yellow "Genera preventivo" CTA (duplicated Preventivi tab nav item) 2. Delete phase/task: new deletePhase/deleteTask server actions with FK cascade and phase-status recompute; DeletePhaseTaskButton client component with window.confirm; trash icons in PhasesTab per phase and per task 3. Public dashboard: extend activeOffers query with offer_macros join (offer_name, offer_type); reorder sidebar 1°Offerte 2°Pagamenti 3°Documenti; OffersSection shows macro public_name as heading (never tier letter/internal_name); PaymentStatus gains totalLabel/overrideAmount/hideRows props — retainer=monthly label + no Acconto/Saldo rows 4. Offer editor: Descrizione textarea (nota interna) and Modalità toggle moved directly under title; Tags section now contains only Categoria/Ticket/Tipo/Obiettivo 5. Basecamp chat: API route supports entity_type="phase" with authorization scoping; comments scope in client-view broadened to include phaseIds and client_id (general); CommentsTab updated with phase/general entities; ChatProvider React context; ChatPanel fixed slide-in panel with FAB (bottom-right) and composer tag selector (Generale + phases); PhaseCard gets chat-bubble icon pre-tagging that phase; inline ChatSection block removed from dashboard main column Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { postAdminComment } from "@/app/admin/clients/[id]/actions";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import type { Comment } from "@/db/schema";
|
|
import type { ClientFullDetail } from "@/lib/admin-queries";
|
|
|
|
type Props = {
|
|
comments: Comment[];
|
|
phases: ClientFullDetail["phases"];
|
|
clientId: string;
|
|
};
|
|
|
|
export async function CommentsTab({ comments, phases, clientId }: Props) {
|
|
// Build entity label map for display (phases, tasks, deliverables, and general)
|
|
const entityLabels: Record<string, string> = {
|
|
[clientId]: "Messaggio generale",
|
|
};
|
|
for (const phase of phases) {
|
|
entityLabels[phase.id] = `Fase: ${phase.title}`;
|
|
for (const task of phase.tasks) {
|
|
entityLabels[task.id] = `Task: ${task.title}`;
|
|
for (const d of task.deliverables) {
|
|
entityLabels[d.id] = `Deliverable: ${d.title}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build list of entities the admin can reply on
|
|
const entities: Array<{ id: string; type: string; label: string }> = [
|
|
{ id: clientId, type: "general", label: "Messaggio generale" },
|
|
];
|
|
for (const phase of phases) {
|
|
entities.push({ id: phase.id, type: "phase", label: `Fase: ${phase.title}` });
|
|
for (const task of phase.tasks) {
|
|
entities.push({ id: task.id, type: "task", label: `Task: ${task.title}` });
|
|
for (const d of task.deliverables) {
|
|
entities.push({
|
|
id: d.id,
|
|
type: "deliverable",
|
|
label: `Deliverable: ${d.title}`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-lg">
|
|
{/* Comment list */}
|
|
{comments.length === 0 && (
|
|
<p className="text-sm text-gray-400">Nessun commento ancora.</p>
|
|
)}
|
|
<div className="space-y-3">
|
|
{comments.map((c) => (
|
|
<div
|
|
key={c.id}
|
|
className={`flex gap-3 ${c.author === "admin" ? "flex-row-reverse" : ""}`}
|
|
>
|
|
<div
|
|
className={`rounded-lg px-3 py-2 text-sm max-w-xs ${
|
|
c.author === "admin"
|
|
? "bg-gray-900 text-white"
|
|
: "bg-white border border-gray-200 text-gray-800"
|
|
}`}
|
|
>
|
|
<p className="text-xs font-medium mb-1 opacity-60">
|
|
{c.author === "admin" ? "iamcavalli" : "Cliente"} —{" "}
|
|
{entityLabels[c.entity_id] ?? c.entity_id}
|
|
</p>
|
|
<p>{c.body}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Admin reply form */}
|
|
<form
|
|
action={async (fd: FormData) => {
|
|
"use server";
|
|
await postAdminComment(clientId, fd);
|
|
}}
|
|
className="bg-white border border-gray-200 rounded-lg p-4 space-y-3"
|
|
>
|
|
<h3 className="font-medium text-gray-900 text-sm">
|
|
Rispondi come admin
|
|
</h3>
|
|
<select
|
|
name="entity"
|
|
className="w-full text-sm border border-gray-200 rounded px-2 py-1.5 bg-white"
|
|
required
|
|
>
|
|
{entities.map((e) => (
|
|
<option key={e.id} value={`${e.type}:${e.id}`}>
|
|
{e.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Textarea
|
|
name="body"
|
|
placeholder="Scrivi un commento..."
|
|
rows={3}
|
|
required
|
|
/>
|
|
<Button type="submit" size="sm">
|
|
Invia risposta
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|