feat: Client Portal redesign to Quiet Luxury + milestone stepper
- New MilestoneStepper primitive (horizontal per-phase progress) - Token-migrate portal shell, phase cards, sidebar cards, kanban to dual-theme - Soft-tint status pills (emerald/amber/muted) replacing solid badges - embedded prop on ClientDashboard fixes double-header in multi-project tabs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -146,3 +146,23 @@ across all future admin pages.
|
|||||||
| `LeadTable` | `src/components/admin/leads/LeadTable.tsx` | Luxury table restyle: `bg-card rounded-xl shadow-card`, uppercase muted headers, `hover:bg-muted/40` rows, uses `StatusBadge`. |
|
| `LeadTable` | `src/components/admin/leads/LeadTable.tsx` | Luxury table restyle: `bg-card rounded-xl shadow-card`, uppercase muted headers, `hover:bg-muted/40` rows, uses `StatusBadge`. |
|
||||||
| `LeadsKanbanBoard` | `src/components/admin/leads/LeadsKanbanBoard.tsx` | Kanban restyle: `bg-muted/60` columns with count pills, `bg-card` cards with `hover:border-primary/30`. Uses `@dnd-kit`, keeps all 6 lead stages. |
|
| `LeadsKanbanBoard` | `src/components/admin/leads/LeadsKanbanBoard.tsx` | Kanban restyle: `bg-muted/60` columns with count pills, `bg-card` cards with `hover:border-primary/30`. Uses `@dnd-kit`, keeps all 6 lead stages. |
|
||||||
| `LeadsViewToggle` | `src/components/admin/leads/LeadsViewToggle.tsx` | Wires `SearchInput` + `SegmentedToggle` together with the table/kanban views. |
|
| `LeadsViewToggle` | `src/components/admin/leads/LeadsViewToggle.tsx` | Wires `SearchInput` + `SegmentedToggle` together with the table/kanban views. |
|
||||||
|
| `MilestoneStepper` | `src/components/client/MilestoneStepper.tsx` | Client-portal horizontal milestone stepper: one node per real phase, connecting line filled by `global_progress_pct`. Done = filled `bg-primary` ✓, active = `border-primary` outline, upcoming = `bg-muted`. |
|
||||||
|
|
||||||
|
### Client Portal restyle notes
|
||||||
|
|
||||||
|
The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens
|
||||||
|
(applied 2026-07-10). Key conventions specific to the client-facing surface:
|
||||||
|
|
||||||
|
- **Header**: `bg-card` sticky bar, three zones — `iamcavalli | Client Portal`
|
||||||
|
(left), brand name centered, "Area Riservata Protetta" emerald pill (right).
|
||||||
|
- **Status pills** (phases, payments) use soft tints, not solid fills:
|
||||||
|
emerald-50/700 (done/saldato), amber-50/700 (in corso/da saldare),
|
||||||
|
`bg-muted`/`text-muted-foreground` (upcoming) — each with a `dark:` variant.
|
||||||
|
- **Phase cards** (`PhaseCard`): `rounded-xl border-border-light bg-card
|
||||||
|
shadow-card`, progress bar colored per status (emerald/amber/`bg-border`).
|
||||||
|
- Sidebar cards (`OffersSection`, `PaymentStatus`, `DocumentsSection`,
|
||||||
|
`NotesSection`, `TranscriptsSection`) all follow `rounded-xl border-border-light
|
||||||
|
bg-card shadow-card`.
|
||||||
|
- `ClientDashboard` takes an `embedded` prop: when rendered inside the
|
||||||
|
multi-project tabs wrapper (`page.tsx`), it drops its own header/footer/canvas
|
||||||
|
so the page-level chrome isn't duplicated.
|
||||||
|
|||||||
@@ -106,10 +106,10 @@ export default async function ClientPage({
|
|||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#f9f9f9] flex items-center justify-center">
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-xl font-bold text-[#1a1a1a]">{client.name}</h1>
|
<h1 className="text-xl font-bold text-foreground">{client.name}</h1>
|
||||||
<p className="text-sm text-[#71717a] mt-2">Nessun progetto disponibile al momento.</p>
|
<p className="text-sm text-muted-foreground mt-2">Nessun progetto disponibile al momento.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -132,22 +132,23 @@ export default async function ClientPage({
|
|||||||
const projectViews = await Promise.all(projects.map((p) => getProjectView(p.id)));
|
const projectViews = await Promise.all(projects.map((p) => getProjectView(p.id)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-background">
|
||||||
<header className="bg-white border-b border-[#e5e5e5] sticky top-0 z-10">
|
<header className="sticky top-0 z-50 flex flex-col items-center gap-4 border-b border-border-light bg-card px-6 py-5 shadow-card md:flex-row md:justify-between md:px-8">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-5">
|
<div className="flex w-full items-center gap-3 md:w-auto">
|
||||||
<div className="flex items-center">
|
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">iamcavalli</span>
|
||||||
<span className="text-xs font-semibold tracking-widest text-[#999999] uppercase w-28 shrink-0">
|
<span className="text-border">|</span>
|
||||||
iamcavalli
|
<span className="text-xs font-medium text-muted-foreground">Client Portal</span>
|
||||||
</span>
|
|
||||||
<h1 className="flex-1 text-center text-2xl sm:text-3xl font-bold text-[#1a1a1a] tracking-tight">
|
|
||||||
{client.brand_name}
|
|
||||||
</h1>
|
|
||||||
<div className="w-28 shrink-0" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-xl font-bold tracking-tight text-foreground">{client.brand_name}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="hidden items-center gap-2 rounded-full border border-emerald-100 bg-emerald-50 px-3 py-1 text-[11px] text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-400 md:flex">
|
||||||
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
|
||||||
|
Area Riservata Protetta
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 py-8">
|
||||||
<Tabs defaultValue={projects[0].id} className="w-full">
|
<Tabs defaultValue={projects[0].id} className="w-full">
|
||||||
<TabsList className="mb-6">
|
<TabsList className="mb-6">
|
||||||
{projects.map((p) => (
|
{projects.map((p) => (
|
||||||
@@ -166,9 +167,10 @@ export default async function ClientPage({
|
|||||||
view={projectViewToClientView(client, view)}
|
view={projectViewToClientView(client, view)}
|
||||||
token={client.token}
|
token={client.token}
|
||||||
comments={view.comments as unknown as Comment[]}
|
comments={view.comments as unknown as Comment[]}
|
||||||
|
embedded
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-[#71717a]">Progetto non disponibile.</p>
|
<p className="text-sm text-muted-foreground">Progetto non disponibile.</p>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
@@ -176,12 +178,8 @@ export default async function ClientPage({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="border-t border-[#e5e5e5] bg-[#f9f9f9] mt-10">
|
<footer className="mt-10 py-10 text-center text-xs text-muted-foreground">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-6">
|
|
||||||
<p className="text-xs text-[#999999] text-center">
|
|
||||||
Questa è la tua dashboard privata — non condividere il link.
|
Questa è la tua dashboard privata — non condividere il link.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ClientView } from '@/lib/client-view';
|
import type { ClientView } from '@/lib/client-view';
|
||||||
import type { Comment } from '@/db/schema';
|
import type { Comment } from '@/db/schema';
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { PhaseTimeline } from './phase-timeline';
|
import { PhaseTimeline } from './phase-timeline';
|
||||||
import { PaymentStatus } from './payment-status';
|
import { PaymentStatus } from './payment-status';
|
||||||
@@ -9,6 +8,7 @@ import { NotesSection } from './notes-section';
|
|||||||
import { TranscriptsSection } from './transcripts-section';
|
import { TranscriptsSection } from './transcripts-section';
|
||||||
import { PhaseViewToggle } from './client/kanban/PhaseViewToggle';
|
import { PhaseViewToggle } from './client/kanban/PhaseViewToggle';
|
||||||
import { OffersSection } from './client/OffersSection';
|
import { OffersSection } from './client/OffersSection';
|
||||||
|
import { MilestoneStepper } from './client/MilestoneStepper';
|
||||||
import { ChatProvider } from './client/ChatProvider';
|
import { ChatProvider } from './client/ChatProvider';
|
||||||
import { ChatPanel } from './client/ChatPanel';
|
import { ChatPanel } from './client/ChatPanel';
|
||||||
|
|
||||||
@@ -16,9 +16,12 @@ interface ClientDashboardProps {
|
|||||||
view: ClientView;
|
view: ClientView;
|
||||||
token: string;
|
token: string;
|
||||||
comments: Comment[];
|
comments: Comment[];
|
||||||
|
/** When rendered inside the multi-project tabs wrapper, the page already
|
||||||
|
* provides the portal header + footer — skip them here to avoid duplicates. */
|
||||||
|
embedded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClientDashboard({ view, token, comments }: ClientDashboardProps) {
|
export function ClientDashboard({ view, token, comments, embedded = false }: ClientDashboardProps) {
|
||||||
// Determine payment display mode based on active offers
|
// Determine payment display mode based on active offers
|
||||||
const retainerOffer = view.activeOffers?.find((o) => o.offer_type === "retainer");
|
const retainerOffer = view.activeOffers?.find((o) => o.offer_type === "retainer");
|
||||||
const hasRetainer = !!retainerOffer;
|
const hasRetainer = !!retainerOffer;
|
||||||
@@ -30,37 +33,38 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatProvider phases={view.phases} clientId={view.client.id}>
|
<ChatProvider phases={view.phases} clientId={view.client.id}>
|
||||||
<div className="min-h-screen bg-white">
|
<div className={embedded ? "" : "min-h-screen bg-background"}>
|
||||||
{/* Header */}
|
{/* Header portale — iamcavalli · Client Portal | brand | area protetta */}
|
||||||
<header className="bg-white border-b border-[#e5e5e5] sticky top-0 z-10">
|
{!embedded && (
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-5">
|
<header className="sticky top-0 z-50 flex flex-col items-center gap-4 border-b border-border-light bg-card px-6 py-5 shadow-card md:flex-row md:justify-between md:px-8">
|
||||||
<div className="flex items-center">
|
<div className="flex w-full items-center gap-3 md:w-auto">
|
||||||
<span className="text-xs font-semibold tracking-widest text-[#999999] uppercase w-28 shrink-0">
|
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
iamcavalli
|
iamcavalli
|
||||||
</span>
|
</span>
|
||||||
<h1 className="flex-1 text-center text-2xl sm:text-3xl font-bold text-[#1a1a1a] tracking-tight">
|
<span className="text-border">|</span>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Client Portal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-xl font-bold tracking-tight text-foreground">
|
||||||
{view.client.brand_name}
|
{view.client.brand_name}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-28 shrink-0" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center gap-2 rounded-full border border-emerald-100 bg-emerald-50 px-3 py-1 text-[11px] text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-400 md:flex">
|
||||||
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
|
||||||
|
Area Riservata Protetta
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Barra progresso globale — nascosta per retainer (fasi non tracciate) */}
|
{/* Milestone stepper — nascosto per retainer (fasi non tracciate) */}
|
||||||
{!hasRetainer && (
|
{!hasRetainer && (
|
||||||
<section className="bg-[#f9f9f9] border-b border-[#e5e5e5]">
|
<MilestoneStepper phases={view.phases} globalProgress={view.global_progress_pct} />
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<p className="text-sm font-semibold text-[#1a1a1a]">Avanzamento Progetto</p>
|
|
||||||
<p className="text-sm font-bold text-[#1a1a1a]">{view.global_progress_pct}%</p>
|
|
||||||
</div>
|
|
||||||
<Progress value={view.global_progress_pct} className="h-2" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Layout principale */}
|
{/* Layout principale */}
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 py-8 lg:py-10">
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
|
||||||
{/* ── Sidebar sinistra ── */}
|
{/* ── Sidebar sinistra ── */}
|
||||||
@@ -70,14 +74,14 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)
|
|||||||
{/* 1° Offerte Attive */}
|
{/* 1° Offerte Attive */}
|
||||||
{view.activeOffers && view.activeOffers.length > 0 && (
|
{view.activeOffers && view.activeOffers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-3">Offerte Attive</h2>
|
<h2 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-3">Offerte Attive</h2>
|
||||||
<OffersSection offers={view.activeOffers} />
|
<OffersSection offers={view.activeOffers} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 2° Pagamenti */}
|
{/* 2° Pagamenti */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-3">Pagamenti</h2>
|
<h2 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-3">Pagamenti</h2>
|
||||||
<PaymentStatus
|
<PaymentStatus
|
||||||
accepted_total={view.client.accepted_total}
|
accepted_total={view.client.accepted_total}
|
||||||
payments={view.payments}
|
payments={view.payments}
|
||||||
@@ -89,20 +93,20 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)
|
|||||||
|
|
||||||
{/* 3° Documenti & File */}
|
{/* 3° Documenti & File */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-3">Documenti & File</h2>
|
<h2 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-3">Documenti & File</h2>
|
||||||
<DocumentsSection documents={view.documents} />
|
<DocumentsSection documents={view.documents} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{view.notes.length > 0 && (
|
{view.notes.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-3">Note & Decisioni</h2>
|
<h2 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-3">Note & Decisioni</h2>
|
||||||
<NotesSection notes={view.notes} />
|
<NotesSection notes={view.notes} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{view.transcripts.length > 0 && (
|
{view.transcripts.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-3">Transcript Chiamate</h2>
|
<h2 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-3">Transcript Chiamate</h2>
|
||||||
<TranscriptsSection transcripts={view.transcripts} />
|
<TranscriptsSection transcripts={view.transcripts} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -113,16 +117,16 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)
|
|||||||
<main className="flex-1 min-w-0 order-1 lg:order-2 space-y-12">
|
<main className="flex-1 min-w-0 order-1 lg:order-2 space-y-12">
|
||||||
{hasRetainer ? (
|
{hasRetainer ? (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-bold text-[#71717a] uppercase tracking-wider mb-4">
|
<h2 className="text-lg font-bold text-foreground tracking-tight mb-4">
|
||||||
Fasi del Progetto
|
Fasi del Progetto
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-[#d4d4d4] bg-[#fafafa] px-8 py-12 text-center">
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-border bg-muted px-8 py-12 text-center shadow-card">
|
||||||
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-[#f0f0f0]">
|
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-card border border-border-light">
|
||||||
<RefreshCw className="h-5 w-5 text-[#71717a]" />
|
<RefreshCw className="h-5 w-5 text-muted-foreground" />
|
||||||
</span>
|
</span>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-sm font-semibold text-[#1a1a1a]">Servizio in abbonamento ricorrente</p>
|
<p className="text-sm font-semibold text-foreground">Servizio in abbonamento ricorrente</p>
|
||||||
<p className="text-xs text-[#71717a] max-w-xs leading-relaxed">
|
<p className="text-xs text-muted-foreground max-w-xs leading-relaxed">
|
||||||
Non ci sono fasi di progetto da tracciare. Le fasi compaiono qui quando viene avviata un'offerta a progetto.
|
Non ci sono fasi di progetto da tracciare. Le fasi compaiono qui quando viene avviata un'offerta a progetto.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,12 +145,8 @@ export function ClientDashboard({ view, token, comments }: ClientDashboardProps)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="border-t border-[#e5e5e5] bg-[#f9f9f9] mt-10">
|
<footer className="mt-10 py-10 text-center text-xs text-muted-foreground">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-6">
|
|
||||||
<p className="text-xs text-[#999999] text-center">
|
|
||||||
Questa è la tua dashboard privata — non condividere il link.
|
Questa è la tua dashboard privata — non condividere il link.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Floating chat panel — FAB + slide-in panel */}
|
{/* Floating chat panel — FAB + slide-in panel */}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { ClientView } from "@/lib/client-view";
|
||||||
|
|
||||||
|
type Phase = ClientView["phases"][number];
|
||||||
|
|
||||||
|
const statusWord: Record<"upcoming" | "active" | "done", string> = {
|
||||||
|
done: "Completata",
|
||||||
|
active: "In corso",
|
||||||
|
upcoming: "Da iniziare",
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusWordClass: Record<"upcoming" | "active" | "done", string> = {
|
||||||
|
done: "text-emerald-600 dark:text-emerald-400",
|
||||||
|
active: "text-amber-600 dark:text-amber-400",
|
||||||
|
upcoming: "text-muted-foreground",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal milestone stepper — replica del "STEP PROGRESS TIMELINE" del mock.
|
||||||
|
* Un nodo per fase reale (non 5 fissi); la linea di fondo si riempie in base a
|
||||||
|
* global_progress_pct.
|
||||||
|
*/
|
||||||
|
export function MilestoneStepper({
|
||||||
|
phases,
|
||||||
|
globalProgress,
|
||||||
|
}: {
|
||||||
|
phases: Phase[];
|
||||||
|
globalProgress: number;
|
||||||
|
}) {
|
||||||
|
if (phases.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-card border-b border-border-light py-8 px-6">
|
||||||
|
<div className="relative mx-auto max-w-[1200px]">
|
||||||
|
{/* Linea di connessione + riempimento dinamico */}
|
||||||
|
<div className="absolute left-[5%] right-[5%] top-[15px] z-0 h-1 rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-primary transition-[width] duration-500 ease-[cubic-bezier(0.4,0,0.2,1)]"
|
||||||
|
style={{ width: `${globalProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nodi milestone */}
|
||||||
|
<div
|
||||||
|
className="relative z-10 grid gap-4"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${phases.length}, minmax(0, 1fr))` }}
|
||||||
|
>
|
||||||
|
{phases.map((phase, i) => {
|
||||||
|
const s = phase.status;
|
||||||
|
return (
|
||||||
|
<div key={phase.id} className="flex flex-col items-center text-center">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"flex h-8 w-8 items-center justify-center rounded-full border-4 border-card text-xs font-bold shadow-md transition-all " +
|
||||||
|
(s === "done"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: s === "active"
|
||||||
|
? "border-primary bg-card text-primary"
|
||||||
|
: "bg-muted text-muted-foreground")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{s === "done" ? "✓" : i + 1}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"mt-2 text-[11px] font-bold " +
|
||||||
|
(s === "upcoming" ? "text-muted-foreground" : "text-foreground")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Step {i + 1}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground line-clamp-1">
|
||||||
|
{phase.title}
|
||||||
|
</span>
|
||||||
|
<span className={`mt-1 text-[9px] font-bold uppercase ${statusWordClass[s]}`}>
|
||||||
|
{statusWord[s]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,57 +29,57 @@ function OfferCard({ offer }: { offer: ActiveOffer }) {
|
|||||||
const hasServices = offer.services.length > 0;
|
const hasServices = offer.services.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-[#e5e5e5] overflow-hidden">
|
<div className="bg-card rounded-xl border border-border-light shadow-card overflow-hidden">
|
||||||
{/* Main info rows */}
|
{/* Main info rows — accento a barra sinistra (mock) */}
|
||||||
<div className="p-4 space-y-2">
|
<div className="p-5">
|
||||||
|
<div className="border-l-2 border-primary pl-3">
|
||||||
{/* Heading: macro public name */}
|
{/* Heading: macro public name */}
|
||||||
<p className="text-sm font-semibold text-[#1a1a1a]">{offer.offer_name}</p>
|
<p className="text-sm font-bold text-foreground">{offer.offer_name}</p>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="mt-2 space-y-1.5">
|
||||||
{/* "Valore incluso" — hidden when 0 */}
|
{/* "Valore incluso" — hidden when 0 */}
|
||||||
{hasPrice && (
|
{hasPrice && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-xs text-[#71717a]">Valore incluso</span>
|
<span className="text-muted-foreground">Valore incluso</span>
|
||||||
<span className="text-xs font-mono text-[#1a1a1a]">
|
<span className="font-mono text-foreground">€{price.toFixed(2)}</span>
|
||||||
€{price.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* "Prezzo finale" */}
|
{/* "Prezzo finale" */}
|
||||||
{offer.accepted_total && (
|
{offer.accepted_total && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-xs font-semibold text-[#1a1a1a]">Prezzo finale</span>
|
<span className="font-semibold text-foreground">Prezzo finale</span>
|
||||||
<span className="text-sm font-bold text-[#1A463C]">
|
<span className="text-sm font-bold text-primary">
|
||||||
€{parseFloat(offer.accepted_total).toFixed(2)}
|
€{parseFloat(offer.accepted_total).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Accordion "Cosa è compreso" — only when there are services */}
|
{/* Accordion "Cosa è compreso" — only when there are services */}
|
||||||
{hasServices && (
|
{hasServices && (
|
||||||
<div className="border-t border-[#e5e5e5]">
|
<div className="border-t border-border-light">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between px-4 py-2.5 text-xs font-semibold text-[#1a1a1a] hover:bg-[#f9f9f9] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1A463C] focus-visible:ring-inset"
|
className="w-full flex items-center justify-between px-5 py-2.5 text-xs font-semibold text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
>
|
>
|
||||||
<span>Cosa è compreso</span>
|
<span>Cosa è compreso</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-3.5 h-3.5 text-[#71717a] transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
className={`w-3.5 h-3.5 text-muted-foreground transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<ul className="px-4 pb-3 space-y-2">
|
<ul className="px-5 pb-3 space-y-2">
|
||||||
{offer.services.map((svc, i) => (
|
{offer.services.map((svc, i) => (
|
||||||
<li key={i} className="flex flex-col gap-0.5">
|
<li key={i} className="flex flex-col gap-0.5">
|
||||||
<span className="text-xs font-medium text-[#1a1a1a]">{svc.name}</span>
|
<span className="text-xs font-medium text-foreground">{svc.name}</span>
|
||||||
{svc.description && (
|
{svc.description && (
|
||||||
<span className="text-[11px] text-[#71717a] leading-snug">{svc.description}</span>
|
<span className="text-[11px] text-muted-foreground leading-snug">{svc.description}</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { ApproveButton } from "@/components/client/ApproveButton";
|
import { ApproveButton } from "@/components/client/ApproveButton";
|
||||||
import { useChatContext } from "@/components/client/ChatProvider";
|
import { useChatContext } from "@/components/client/ChatProvider";
|
||||||
import type { ClientView } from "@/lib/client-view";
|
import type { ClientView } from "@/lib/client-view";
|
||||||
@@ -16,53 +13,38 @@ const phaseStatusLabel: Record<"upcoming" | "active" | "done", string> = {
|
|||||||
done: "Completata",
|
done: "Completata",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Soft-tint status pill — replica del mock ("Quiet Luxury")
|
||||||
const phaseStatusStyle: Record<"upcoming" | "active" | "done", string> = {
|
const phaseStatusStyle: Record<"upcoming" | "active" | "done", string> = {
|
||||||
upcoming: "border-transparent bg-[#999999] text-white",
|
upcoming: "bg-muted text-muted-foreground border border-border",
|
||||||
active: "border-transparent bg-[#3b82f6] text-white",
|
active:
|
||||||
done: "border-transparent bg-[#1A463C] text-white",
|
"bg-amber-50 text-amber-700 border border-amber-100 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20",
|
||||||
|
done: "bg-emerald-50 text-emerald-700 border border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20",
|
||||||
};
|
};
|
||||||
|
|
||||||
function PhaseStatusIcon({ status }: { status: "upcoming" | "active" | "done" }) {
|
// Colore della barra di avanzamento per stato fase
|
||||||
if (status === "done") {
|
const phaseBarColor: Record<"upcoming" | "active" | "done", string> = {
|
||||||
return (
|
upcoming: "bg-border",
|
||||||
<svg className="w-5 h-5 text-[#1A463C]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
active: "bg-amber-500",
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
done: "bg-emerald-600",
|
||||||
</svg>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
if (status === "active") {
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5 text-[#3b82f6]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5 text-[#999999]" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<circle cx={12} cy={12} r={9} />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TaskStatusIcon({ status }: { status: "todo" | "in_progress" | "done" }) {
|
function TaskStatusIcon({ status }: { status: "todo" | "in_progress" | "done" }) {
|
||||||
if (status === "done") {
|
if (status === "done") {
|
||||||
return (
|
return (
|
||||||
<svg className="w-4 h-4 text-[#1A463C] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-[10px] font-bold text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400">
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
✓
|
||||||
</svg>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status === "in_progress") {
|
if (status === "in_progress") {
|
||||||
return (
|
return (
|
||||||
<svg className="w-4 h-4 text-[#3b82f6] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-amber-400 bg-card">
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
|
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
|
||||||
</svg>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<svg className="w-4 h-4 text-[#999999] shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
|
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-border bg-card" />
|
||||||
<circle cx={12} cy={12} r={9} />
|
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +62,7 @@ export function PhaseCard({
|
|||||||
const doneCount = phase.tasks.filter((t) => t.status === "done").length;
|
const doneCount = phase.tasks.filter((t) => t.status === "done").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-5">
|
<div className="rounded-xl border border-border-light bg-card p-6 shadow-card">
|
||||||
{/* Header — always visible, click to toggle */}
|
{/* Header — always visible, click to toggle */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -89,14 +71,13 @@ export function PhaseCard({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
<h3 className="text-sm font-bold text-foreground leading-snug min-w-0">{phase.title}</h3>
|
||||||
<PhaseStatusIcon status={phase.status} />
|
|
||||||
<h3 className="text-base font-bold text-[#1a1a1a] leading-snug">{phase.title}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Badge className={`text-xs ${phaseStatusStyle[phase.status]}`}>
|
<span
|
||||||
|
className={`rounded px-2.5 py-0.5 text-[10px] font-bold uppercase ${phaseStatusStyle[phase.status]}`}
|
||||||
|
>
|
||||||
{phaseStatusLabel[phase.status]}
|
{phaseStatusLabel[phase.status]}
|
||||||
</Badge>
|
</span>
|
||||||
{/* Chat bubble — opens panel pre-tagged with this phase */}
|
{/* Chat bubble — opens panel pre-tagged with this phase */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -105,14 +86,14 @@ export function PhaseCard({
|
|||||||
openChat(phase.id);
|
openChat(phase.id);
|
||||||
}}
|
}}
|
||||||
aria-label="Apri chat per questa fase"
|
aria-label="Apri chat per questa fase"
|
||||||
className="p-1 rounded-md text-[#999999] hover:text-[#1A463C] hover:bg-[#f0f7f4] transition-colors"
|
className="p-1 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.8} viewBox="0 0 24 24" aria-hidden="true">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.8} viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M21 12c0 4.418-4.03 8-9 8a9.77 9.77 0 01-4-.83L3 20l1.09-3.27C3.39 15.56 3 13.83 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M21 12c0 4.418-4.03 8-9 8a9.77 9.77 0 01-4-.83L3 20l1.09-3.27C3.39 15.56 3 13.83 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-[#999999] transition-transform ${open ? "rotate-180" : ""}`}
|
className={`w-4 h-4 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -124,38 +105,43 @@ export function PhaseCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar — always visible */}
|
{/* Progress bar — always visible, colore per stato */}
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
<p className="text-xs text-[#666666] font-medium">{doneCount} di {phase.tasks.length} task</p>
|
<p className="text-xs text-muted-foreground font-medium">{doneCount} di {phase.tasks.length} task</p>
|
||||||
<p className="text-xs font-semibold text-[#1a1a1a]">{phase.progress_pct}%</p>
|
<p className="text-xs font-semibold text-foreground">{phase.progress_pct}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${phaseBarColor[phase.status]}`}
|
||||||
|
style={{ width: `${phase.progress_pct}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={phase.progress_pct} className="h-1.5" />
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Collapsible task list */}
|
{/* Collapsible task list */}
|
||||||
{open && (
|
{open && (
|
||||||
<div className="mt-4">
|
<div className="mt-4 border-t border-border-light pt-4">
|
||||||
{phase.tasks.length === 0 ? (
|
{phase.tasks.length === 0 ? (
|
||||||
<p className="text-xs text-[#999999] italic">Nessun task ancora configurato.</p>
|
<p className="text-xs text-muted-foreground italic">Nessun task ancora configurato.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{phase.tasks.map((task) => (
|
{phase.tasks.map((task) => (
|
||||||
<li key={task.id} className="flex items-start gap-2.5">
|
<li key={task.id} className="flex items-start gap-3">
|
||||||
<TaskStatusIcon status={task.status} />
|
<TaskStatusIcon status={task.status} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p
|
<p
|
||||||
className={`text-sm leading-snug ${
|
className={`text-xs leading-snug ${
|
||||||
task.status === "done"
|
task.status === "done"
|
||||||
? "line-through text-[#999999]"
|
? "line-through text-muted-foreground"
|
||||||
: "text-[#1a1a1a]"
|
: "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.title}
|
{task.title}
|
||||||
</p>
|
</p>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-xs text-[#999999] mt-0.5 leading-snug">
|
<p className="text-[11px] text-muted-foreground mt-0.5 leading-snug">
|
||||||
{task.description}
|
{task.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -164,9 +150,9 @@ export function PhaseCard({
|
|||||||
{task.deliverables.map((d) => (
|
{task.deliverables.map((d) => (
|
||||||
<li
|
<li
|
||||||
key={d.id}
|
key={d.id}
|
||||||
className="bg-[#f9f9f9] rounded px-3 py-2 flex items-center justify-between gap-2"
|
className="bg-muted rounded-lg px-3 py-2 flex items-center justify-between gap-2"
|
||||||
>
|
>
|
||||||
<span className="text-xs text-[#666666] truncate font-medium">
|
<span className="text-xs text-muted-foreground truncate font-medium">
|
||||||
{d.title}
|
{d.title}
|
||||||
</span>
|
</span>
|
||||||
{(d.status === "pending" ||
|
{(d.status === "pending" ||
|
||||||
@@ -189,6 +175,6 @@ export function PhaseCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,29 @@ type Task = ClientView["phases"][number]["tasks"][number] & {
|
|||||||
phaseTitle: string;
|
phaseTitle: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string; dotClass: string; headerClass: string }[] = [
|
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string }[] = [
|
||||||
{ id: "todo", label: "Da fare", dotClass: "bg-[#d4d4d8]", headerClass: "text-[#71717a]" },
|
{ id: "todo", label: "Da fare" },
|
||||||
{ id: "in_progress", label: "In corso", dotClass: "bg-[#DEF168]", headerClass: "text-[#1A463C]" },
|
{ id: "in_progress", label: "In corso" },
|
||||||
{ id: "done", label: "Fatto", dotClass: "bg-[#1A463C]", headerClass: "text-[#1A463C]" },
|
{ id: "done", label: "Fatto" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function TaskCard({ task, token }: { task: Task; token: string }) {
|
function TaskCard({ task, token }: { task: Task; token: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-[#e5e7eb] px-3 py-2.5 shadow-sm">
|
<div className="bg-card rounded-lg border border-border-light px-4 py-4 shadow-sm hover:border-primary/30 hover:shadow-card-hover transition-all">
|
||||||
<p className="text-[10px] font-medium text-[#71717a] uppercase tracking-wide mb-1 truncate">
|
<p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wide mb-1 truncate">
|
||||||
{task.phaseTitle}
|
{task.phaseTitle}
|
||||||
</p>
|
</p>
|
||||||
<p className={`text-sm font-medium leading-snug mb-2 ${task.status === "done" ? "line-through text-[#71717a]" : "text-[#1a1a1a]"}`}>
|
<p className={`text-xs font-medium leading-snug ${task.status === "done" ? "line-through text-muted-foreground" : "text-foreground"}`}>
|
||||||
{task.title}
|
{task.title}
|
||||||
</p>
|
</p>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-xs text-[#71717a] mb-2 leading-snug">{task.description}</p>
|
<p className="text-[11px] text-muted-foreground mt-2 leading-snug">{task.description}</p>
|
||||||
)}
|
)}
|
||||||
{task.deliverables.length > 0 && (
|
{task.deliverables.length > 0 && (
|
||||||
<ul className="space-y-1.5 mt-2 pt-2 border-t border-[#f4f4f5]">
|
<ul className="space-y-1.5 mt-2 pt-2 border-t border-border-light">
|
||||||
{task.deliverables.map((d) => (
|
{task.deliverables.map((d) => (
|
||||||
<li key={d.id} className="flex items-center justify-between gap-2">
|
<li key={d.id} className="flex items-center justify-between gap-2">
|
||||||
<span className="text-xs text-[#666666] truncate">{d.title}</span>
|
<span className="text-xs text-muted-foreground truncate">{d.title}</span>
|
||||||
{(d.status === "pending" || d.status === "submitted" || d.approved_at !== null) && (
|
{(d.status === "pending" || d.status === "submitted" || d.approved_at !== null) && (
|
||||||
<ApproveButton deliverableId={d.id} token={token} approvedAt={d.approved_at} />
|
<ApproveButton deliverableId={d.id} token={token} approvedAt={d.approved_at} />
|
||||||
)}
|
)}
|
||||||
@@ -53,24 +53,21 @@ export function ClientKanban({ phases, token }: { phases: ClientView["phases"];
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
{COLUMNS.map((col) => (
|
{COLUMNS.map((col) => (
|
||||||
<div key={col.id} className="flex flex-col rounded-xl border border-[#e5e7eb] bg-[#f9f9f9] min-h-[200px]">
|
<div key={col.id} className="flex flex-col gap-4 rounded-xl border border-border-light bg-muted/60 p-4 min-h-[300px]">
|
||||||
<div className={`px-4 py-3 flex items-center justify-between ${col.headerClass}`}>
|
<div className="flex items-center justify-between border-b border-border-light pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{col.label}</span>
|
||||||
<span className={`w-2 h-2 rounded-full ${col.dotClass}`} />
|
<span className="text-[10px] font-bold tabular-nums bg-secondary text-secondary-foreground rounded-full px-2 py-0.5">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">{col.label}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
|
|
||||||
{tasksByStatus[col.id].length}
|
{tasksByStatus[col.id].length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 p-3 space-y-2">
|
<div className="flex-1 flex flex-col gap-3">
|
||||||
{tasksByStatus[col.id].map((task) => (
|
{tasksByStatus[col.id].map((task) => (
|
||||||
<TaskCard key={task.id} task={task} token={token} />
|
<TaskCard key={task.id} task={task} token={token} />
|
||||||
))}
|
))}
|
||||||
{tasksByStatus[col.id].length === 0 && (
|
{tasksByStatus[col.id].length === 0 && (
|
||||||
<p className="text-xs text-[#d4d4d8] italic text-center py-10">Nessun task</p>
|
<p className="text-xs text-muted-foreground italic text-center py-10">Nessun task</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ export function PhaseViewToggle({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-xl font-bold text-[#1a1a1a]">Fasi del Progetto</h2>
|
<h2 className="text-lg font-bold text-foreground tracking-tight">Fasi del Progetto</h2>
|
||||||
<div className="flex items-center gap-1 bg-[#f4f4f5] rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setView("timeline")}
|
onClick={() => setView("timeline")}
|
||||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
className={`px-5 py-1.5 rounded-md text-[11px] font-semibold transition-all ${
|
||||||
view === "timeline" ? "bg-white text-[#1A463C] shadow-sm" : "text-[#71717a] hover:text-[#1a1a1a]"
|
view === "timeline" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Timeline
|
Timeline
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView("kanban")}
|
onClick={() => setView("kanban")}
|
||||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
className={`px-5 py-1.5 rounded-md text-[11px] font-semibold transition-all ${
|
||||||
view === "kanban" ? "bg-white text-[#1A463C] shadow-sm" : "text-[#71717a] hover:text-[#1a1a1a]"
|
view === "kanban" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Kanban
|
Kanban
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ClientView } from '@/lib/client-view';
|
import type { ClientView } from '@/lib/client-view';
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
interface DocumentsSectionProps {
|
interface DocumentsSectionProps {
|
||||||
documents: ClientView['documents'];
|
documents: ClientView['documents'];
|
||||||
@@ -8,49 +7,29 @@ interface DocumentsSectionProps {
|
|||||||
export function DocumentsSection({ documents }: DocumentsSectionProps) {
|
export function DocumentsSection({ documents }: DocumentsSectionProps) {
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="text-sm text-[#999999] italic">
|
<p className="text-sm text-muted-foreground italic">
|
||||||
Nessun documento ancora condiviso.
|
Nessun documento ancora condiviso.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="rounded-xl border border-border-light bg-card shadow-card p-3 space-y-2">
|
||||||
{documents.map((doc) => (
|
{documents.map((doc) => (
|
||||||
<Card
|
|
||||||
key={doc.id}
|
|
||||||
className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-0 overflow-hidden"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
|
key={doc.id}
|
||||||
href={doc.url}
|
href={doc.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-between gap-3 px-5 py-4 group hover:bg-[#f9f9f9] transition-colors"
|
className="flex items-center justify-between gap-3 rounded-lg border border-border-light px-3 py-3 group hover:border-border transition-colors"
|
||||||
>
|
>
|
||||||
{/* Icona documento */}
|
<span className="text-xs font-medium text-foreground truncate">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-[#0066cc] shrink-0"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium text-[#1a1a1a] group-hover:text-[#0066cc] truncate transition-colors">
|
|
||||||
{doc.label}
|
{doc.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Icona external link */}
|
{/* Icona external link */}
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-[#999999] group-hover:text-[#0066cc] shrink-0 transition-colors"
|
className="w-4 h-4 text-muted-foreground group-hover:text-primary shrink-0 transition-colors"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -64,7 +43,6 @@ export function DocumentsSection({ documents }: DocumentsSectionProps) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ClientView } from '@/lib/client-view';
|
import type { ClientView } from '@/lib/client-view';
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
|
|
||||||
interface NotesSectionProps {
|
interface NotesSectionProps {
|
||||||
notes: ClientView['notes'];
|
notes: ClientView['notes'];
|
||||||
@@ -8,7 +7,7 @@ interface NotesSectionProps {
|
|||||||
export function NotesSection({ notes }: NotesSectionProps) {
|
export function NotesSection({ notes }: NotesSectionProps) {
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="text-sm text-[#999999] italic">
|
<p className="text-sm text-muted-foreground italic">
|
||||||
Nessuna nota ancora registrata. Le decisioni appariranno qui man mano che il progetto avanza.
|
Nessuna nota ancora registrata. Le decisioni appariranno qui man mano che il progetto avanza.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -28,20 +27,20 @@ export function NotesSection({ notes }: NotesSectionProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
key={note.id}
|
key={note.id}
|
||||||
className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-5"
|
className="rounded-xl border border-border-light bg-card shadow-card p-5"
|
||||||
>
|
>
|
||||||
{/* Testo nota — sola lettura (D-12: admin scrive, cliente legge) */}
|
{/* Testo nota — sola lettura (D-12: admin scrive, cliente legge) */}
|
||||||
<p className="text-sm text-[#1a1a1a] leading-relaxed whitespace-pre-wrap">
|
<p className="text-xs text-foreground leading-relaxed whitespace-pre-wrap">
|
||||||
{note.body}
|
{note.body}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* Timestamp */}
|
||||||
<p className="text-xs text-[#999999] mt-3">
|
<p className="text-[11px] text-muted-foreground mt-3">
|
||||||
{dateFormatted} alle {timeFormatted}
|
{dateFormatted} alle {timeFormatted}
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import type { ClientView } from '@/lib/client-view';
|
import type { ClientView } from '@/lib/client-view';
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
interface PaymentStatusProps {
|
interface PaymentStatusProps {
|
||||||
accepted_total: string;
|
accepted_total: string;
|
||||||
@@ -17,22 +15,22 @@ type PaymentStatusValue = 'da_saldare' | 'inviata' | 'saldato';
|
|||||||
|
|
||||||
const statusConfig: Record<
|
const statusConfig: Record<
|
||||||
PaymentStatusValue,
|
PaymentStatusValue,
|
||||||
{ label: string; badgeClass: string; dotClass: string }
|
{ label: string; pillClass: string }
|
||||||
> = {
|
> = {
|
||||||
da_saldare: {
|
da_saldare: {
|
||||||
label: 'Da Saldare',
|
label: 'Da Saldare',
|
||||||
badgeClass: 'border-transparent bg-[#2563eb] text-white',
|
pillClass:
|
||||||
dotClass: 'bg-[#2563eb]',
|
'bg-amber-50 text-amber-700 border-amber-100 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20',
|
||||||
},
|
},
|
||||||
inviata: {
|
inviata: {
|
||||||
label: 'Inviata',
|
label: 'Inviata',
|
||||||
badgeClass: 'border-transparent bg-[#ca8a04] text-white',
|
pillClass:
|
||||||
dotClass: 'bg-[#ca8a04]',
|
'bg-blue-50 text-blue-700 border-blue-100 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/20',
|
||||||
},
|
},
|
||||||
saldato: {
|
saldato: {
|
||||||
label: 'Saldato',
|
label: 'Saldato',
|
||||||
badgeClass: 'border-transparent bg-[#16a34a] text-white',
|
pillClass:
|
||||||
dotClass: 'bg-[#16a34a]',
|
'bg-emerald-50 text-emerald-700 border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,22 +50,22 @@ export function PaymentStatus({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none overflow-hidden">
|
<div className="rounded-xl border border-border-light bg-card shadow-card overflow-hidden">
|
||||||
{/* Totale — 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]">
|
<div className="px-5 py-5 border-b border-border-light bg-muted">
|
||||||
<p className="text-xs font-semibold text-[#666666] uppercase tracking-wider mb-1">
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
{totalLabel}
|
{totalLabel}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-3xl font-bold text-[#1a1a1a] tracking-tight">
|
<p className="text-2xl font-bold text-foreground tracking-tight">
|
||||||
{totalFormatted}
|
{totalFormatted}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Righe pagamento: solo etichetta + stato, MAI importo singolo — omesse se hideRows */}
|
{/* Righe pagamento: solo etichetta + stato, MAI importo singolo — omesse se hideRows */}
|
||||||
{!hideRows && (
|
{!hideRows && (
|
||||||
<div className="px-6 py-4 space-y-3">
|
<div className="px-5 py-4 space-y-2">
|
||||||
{payments.length === 0 ? (
|
{payments.length === 0 ? (
|
||||||
<p className="text-sm text-[#999999] italic py-2">
|
<p className="text-sm text-muted-foreground italic py-2">
|
||||||
Nessun piano di pagamento configurato.
|
Nessun piano di pagamento configurato.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -78,23 +76,18 @@ export function PaymentStatus({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={payment.id}
|
key={payment.id}
|
||||||
className="flex items-center justify-between gap-4 py-2"
|
className="flex items-center justify-between gap-4 rounded-lg border border-border-light p-3"
|
||||||
>
|
>
|
||||||
{/* Indicatore dot + etichetta */}
|
<p className="text-xs font-semibold text-foreground">
|
||||||
<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}
|
{payment.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Badge stato — nessun importo */}
|
{/* Pill stato — nessun importo */}
|
||||||
<Badge className={`text-xs shrink-0 ${config.badgeClass}`}>
|
<span
|
||||||
|
className={`rounded border px-2 py-1 text-[10px] font-bold uppercase shrink-0 ${config.pillClass}`}
|
||||||
|
>
|
||||||
{config.label}
|
{config.label}
|
||||||
</Badge>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -103,11 +96,11 @@ export function PaymentStatus({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Nota informativa */}
|
{/* Nota informativa */}
|
||||||
<div className="px-6 py-3 border-t border-[#e5e5e5] bg-[#f9f9f9]">
|
<div className="px-5 py-3 border-t border-border-light bg-muted">
|
||||||
<p className="text-xs text-[#999999] italic">
|
<p className="text-[11px] text-muted-foreground italic">
|
||||||
Per dettagli o domande sui pagamenti, contattaci direttamente.
|
Per dettagli o domande sui pagamenti, contattaci direttamente.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,42 +8,15 @@ interface PhaseTimelineProps {
|
|||||||
|
|
||||||
export function PhaseTimeline({ phases, token }: PhaseTimelineProps) {
|
export function PhaseTimeline({ phases, token }: PhaseTimelineProps) {
|
||||||
if (phases.length === 0) {
|
if (phases.length === 0) {
|
||||||
return <p className="text-sm text-[#999999] italic">Nessuna fase ancora configurata.</p>;
|
return <p className="text-sm text-muted-foreground italic">Nessuna fase ancora configurata.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-6">
|
||||||
{phases.map((phase, index) => {
|
{phases.map((phase) => {
|
||||||
const isLast = index === phases.length - 1;
|
|
||||||
// done phases start collapsed; active and upcoming start expanded
|
// done phases start collapsed; active and upcoming start expanded
|
||||||
const defaultOpen = phase.status !== 'done';
|
const defaultOpen = phase.status !== 'done';
|
||||||
|
return <PhaseCard key={phase.id} phase={phase} token={token} defaultOpen={defaultOpen} />;
|
||||||
return (
|
|
||||||
<div key={phase.id} className="flex gap-5">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-white border-2 border-[#e5e5e5] flex items-center justify-center shrink-0 z-10">
|
|
||||||
{phase.status === 'done' ? (
|
|
||||||
<svg className="w-5 h-5 text-[#1A463C]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
) : phase.status === 'active' ? (
|
|
||||||
<svg className="w-5 h-5 text-[#3b82f6]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5 text-[#999999]" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<circle cx={12} cy={12} r={9} />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isLast && <div className="flex-1 w-px bg-[#e5e5e5] my-2" style={{ minHeight: '2rem' }} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`flex-1 ${isLast ? 'pb-0' : 'pb-6'}`}>
|
|
||||||
<PhaseCard phase={phase} token={token} defaultOpen={defaultOpen} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
type Transcript = {
|
type Transcript = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
@@ -20,17 +19,17 @@ function TranscriptCard({ transcript }: { transcript: Transcript }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-0 overflow-hidden">
|
<div className="rounded-xl border border-border-light bg-card shadow-card overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-[#f9f9f9] transition-colors group"
|
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-muted transition-colors group"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{/* Icona microfono */}
|
{/* Icona microfono */}
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-[#999999] shrink-0"
|
className="w-4 h-4 text-muted-foreground shrink-0"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -44,14 +43,14 @@ function TranscriptCard({ transcript }: { transcript: Transcript }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium text-[#1a1a1a] truncate group-hover:text-[#1A463C] transition-colors">
|
<p className="text-xs font-semibold text-foreground truncate group-hover:text-primary transition-colors">
|
||||||
{transcript.title ?? "Transcript chiamata"}
|
{transcript.title ?? "Transcript chiamata"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-[#999999] mt-0.5">{date}</p>
|
<p className="text-[11px] text-muted-foreground mt-0.5">{date}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-[#999999] shrink-0 ml-3 transition-transform ${expanded ? "rotate-180" : ""}`}
|
className={`w-4 h-4 text-muted-foreground shrink-0 ml-3 transition-transform ${expanded ? "rotate-180" : ""}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -63,13 +62,13 @@ function TranscriptCard({ transcript }: { transcript: Transcript }) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="px-5 pb-5 pt-1 border-t border-[#e5e5e5]">
|
<div className="px-5 pb-5 pt-1 border-t border-border-light">
|
||||||
<pre className="text-xs text-[#444444] whitespace-pre-wrap font-sans leading-relaxed max-h-80 overflow-y-auto">
|
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap font-sans leading-relaxed max-h-80 overflow-y-auto">
|
||||||
{transcript.content}
|
{transcript.content}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user