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>
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import {
|
|
addPhase,
|
|
addTask,
|
|
updateTaskStatus,
|
|
updatePhaseStatus,
|
|
} from "@/app/admin/clients/[id]/actions";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { DeletePhaseTaskButton } from "@/components/admin/DeletePhaseTaskButton";
|
|
import type { ClientFullDetail } from "@/lib/admin-queries";
|
|
|
|
type Props = {
|
|
phases: ClientFullDetail["phases"];
|
|
clientId: string;
|
|
};
|
|
|
|
const taskStatusOptions = [
|
|
{ value: "todo", label: "Da fare" },
|
|
{ value: "in_progress", label: "In corso" },
|
|
{ value: "done", label: "Fatto" },
|
|
];
|
|
|
|
const phaseStatusOptions = [
|
|
{ value: "upcoming", label: "Da iniziare" },
|
|
{ value: "active", label: "In corso" },
|
|
{ value: "done", label: "Completata" },
|
|
];
|
|
|
|
export async function PhasesTab({ phases, clientId }: Props) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Add phase form */}
|
|
<form
|
|
action={async (fd: FormData) => {
|
|
"use server";
|
|
await addPhase(clientId, fd);
|
|
}}
|
|
className="flex gap-2"
|
|
>
|
|
<Input
|
|
name="title"
|
|
placeholder="Nome nuova fase..."
|
|
className="max-w-xs"
|
|
required
|
|
/>
|
|
<Button type="submit" variant="outline" size="sm">
|
|
+ Fase
|
|
</Button>
|
|
</form>
|
|
|
|
{/* Phases list */}
|
|
{phases.length === 0 && (
|
|
<p className="text-sm text-gray-400">Nessuna fase ancora.</p>
|
|
)}
|
|
{phases.map((phase) => (
|
|
<div
|
|
key={phase.id}
|
|
className="border border-gray-200 rounded-lg p-4 bg-white"
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="font-semibold text-gray-900">{phase.title}</h3>
|
|
<div className="flex items-center gap-2">
|
|
<form
|
|
action={async (fd: FormData) => {
|
|
"use server";
|
|
await updatePhaseStatus(
|
|
phase.id,
|
|
clientId,
|
|
fd.get("status") as string
|
|
);
|
|
}}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<select
|
|
name="status"
|
|
defaultValue={phase.status}
|
|
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white"
|
|
>
|
|
{phaseStatusOptions.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Button type="submit" variant="ghost" size="sm" className="text-xs">
|
|
Salva
|
|
</Button>
|
|
</form>
|
|
<DeletePhaseTaskButton type="phase" phaseId={phase.id} clientId={clientId} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tasks */}
|
|
<div className="space-y-2 mb-3">
|
|
{phase.tasks.map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="flex items-center justify-between pl-3 border-l-2 border-gray-100"
|
|
>
|
|
<span className="text-sm text-gray-800">{task.title}</span>
|
|
<div className="flex items-center gap-1">
|
|
<form
|
|
action={async (fd: FormData) => {
|
|
"use server";
|
|
await updateTaskStatus(
|
|
task.id,
|
|
clientId,
|
|
fd.get("status") as string
|
|
);
|
|
}}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<select
|
|
name="status"
|
|
defaultValue={task.status}
|
|
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white"
|
|
>
|
|
{taskStatusOptions.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Button
|
|
type="submit"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-xs px-1"
|
|
>
|
|
✓
|
|
</Button>
|
|
</form>
|
|
<DeletePhaseTaskButton type="task" taskId={task.id} clientId={clientId} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Add task form */}
|
|
<form
|
|
action={async (fd: FormData) => {
|
|
"use server";
|
|
await addTask(phase.id, clientId, fd);
|
|
}}
|
|
className="flex gap-2 mt-2"
|
|
>
|
|
<Input
|
|
name="title"
|
|
placeholder="Nuovo task..."
|
|
className="text-sm max-w-xs"
|
|
required
|
|
/>
|
|
<Button type="submit" variant="ghost" size="sm" className="text-xs">
|
|
+ Task
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
} |