feat: sidebar CTA removal, delete phase/task, public dashboard UX, chat panel
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>
This commit is contained in:
@@ -5,6 +5,12 @@ import { Badge } from '@/components/ui/badge';
|
||||
interface PaymentStatusProps {
|
||||
accepted_total: string;
|
||||
payments: ClientView['payments'];
|
||||
/** Label for the total row. Defaults to "Totale Investimento" */
|
||||
totalLabel?: string;
|
||||
/** Override the displayed amount (retainer: show offer accepted_total, not project total) */
|
||||
overrideAmount?: string;
|
||||
/** When true, hide individual payment rows (Acconto/Saldo) — used for retainer offers */
|
||||
hideRows?: boolean;
|
||||
}
|
||||
|
||||
type PaymentStatusValue = 'da_saldare' | 'inviata' | 'saldato';
|
||||
@@ -30,8 +36,15 @@ const statusConfig: Record<
|
||||
},
|
||||
};
|
||||
|
||||
export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps) {
|
||||
const totalFormatted = parseFloat(accepted_total || '0').toLocaleString('it-IT', {
|
||||
export function PaymentStatus({
|
||||
accepted_total,
|
||||
payments,
|
||||
totalLabel = 'Totale Investimento',
|
||||
overrideAmount,
|
||||
hideRows = false,
|
||||
}: PaymentStatusProps) {
|
||||
const displayAmount = overrideAmount ?? accepted_total;
|
||||
const totalFormatted = parseFloat(displayAmount || '0').toLocaleString('it-IT', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 2,
|
||||
@@ -40,52 +53,54 @@ export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps)
|
||||
|
||||
return (
|
||||
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none overflow-hidden">
|
||||
{/* Totale accettato — unico importo visibile al cliente (LOCKED) */}
|
||||
{/* Totale — unico importo visibile al cliente (LOCKED) */}
|
||||
<div className="px-6 py-5 border-b border-[#e5e5e5] bg-[#f9f9f9]">
|
||||
<p className="text-xs font-semibold text-[#666666] uppercase tracking-wider mb-1">
|
||||
Totale Preventivo Accettato
|
||||
{totalLabel}
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[#1a1a1a] tracking-tight">
|
||||
{totalFormatted}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Righe pagamento: solo etichetta + stato, MAI importo singolo */}
|
||||
<div className="px-6 py-4 space-y-3">
|
||||
{payments.length === 0 ? (
|
||||
<p className="text-sm text-[#999999] italic py-2">
|
||||
Nessun piano di pagamento configurato.
|
||||
</p>
|
||||
) : (
|
||||
payments.map((payment) => {
|
||||
const status = payment.status as PaymentStatusValue;
|
||||
const config = statusConfig[status] ?? statusConfig.da_saldare;
|
||||
{/* Righe pagamento: solo etichetta + stato, MAI importo singolo — omesse se hideRows */}
|
||||
{!hideRows && (
|
||||
<div className="px-6 py-4 space-y-3">
|
||||
{payments.length === 0 ? (
|
||||
<p className="text-sm text-[#999999] italic py-2">
|
||||
Nessun piano di pagamento configurato.
|
||||
</p>
|
||||
) : (
|
||||
payments.map((payment) => {
|
||||
const status = payment.status as PaymentStatusValue;
|
||||
const config = statusConfig[status] ?? statusConfig.da_saldare;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={payment.id}
|
||||
className="flex items-center justify-between gap-4 py-2"
|
||||
>
|
||||
{/* Indicatore dot + etichetta */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${config.dotClass}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-sm font-medium text-[#1a1a1a]">
|
||||
{payment.label}
|
||||
</p>
|
||||
return (
|
||||
<div
|
||||
key={payment.id}
|
||||
className="flex items-center justify-between gap-4 py-2"
|
||||
>
|
||||
{/* Indicatore dot + etichetta */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${config.dotClass}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-sm font-medium text-[#1a1a1a]">
|
||||
{payment.label}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Badge stato — nessun importo */}
|
||||
<Badge className={`text-xs shrink-0 ${config.badgeClass}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Badge stato — nessun importo */}
|
||||
<Badge className={`text-xs shrink-0 ${config.badgeClass}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nota informativa */}
|
||||
<div className="px-6 py-3 border-t border-[#e5e5e5] bg-[#f9f9f9]">
|
||||
@@ -95,4 +110,4 @@ export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps)
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user