ceo-web/src/app/dashboard/decisions/workspace/page.tsx

365 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
import type { Decision } from '../../../../lib/api';
export default function DecisionWorkspacePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const [selectedId, setSelectedId] = useState<string | null>(null);
const [context, setContext] = useState('');
const [options, setOptions] = useState<string[]>(['', '']);
const [assumptions, setAssumptions] = useState<string[]>(['']);
const [selectedOption, setSelectedOption] = useState('');
const [outcomeReview, setOutcomeReview] = useState('');
const [reviewDueAt, setReviewDueAt] = useState('');
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const { data: decisions = [], isLoading } = useQuery({
queryKey: ['decisions', tenantId],
queryFn: () => apiFetch<Decision[]>('/v1/decisions?limit=50', { tenantId }),
enabled: Boolean(tenantId),
});
useEffect(() => {
if (!selectedId) return;
const d = decisions.find((d) => d.id === selectedId);
if (!d) return;
setContext(d.context);
setOptions(d.options.length ? d.options : ['', '']);
setAssumptions(d.assumptions.length ? d.assumptions : ['']);
setSelectedOption(d.selectedOption ?? '');
setOutcomeReview(d.outcomeReview ?? '');
setReviewDueAt(d.reviewDueAt ? d.reviewDueAt.slice(0, 10) : '');
setDirty(false);
setSaveError(null);
}, [selectedId, decisions]);
async function handleNew() {
if (!tenantId) return;
try {
const created = await apiFetch<Decision>('/v1/decisions', {
method: 'POST',
body: { context: 'Situație nouă — actualizează contextul', options: [], assumptions: [] },
tenantId,
});
await qc.invalidateQueries({ queryKey: ['decisions', tenantId] });
setSelectedId(created.id);
} catch (e) {
console.error('[workspace] create failed', e);
}
}
async function handleSave() {
if (!selectedId || !tenantId) return;
setSaving(true);
setSaveError(null);
try {
await apiFetch(`/v1/decisions/${selectedId}`, {
method: 'PATCH',
body: {
context,
options: options.filter((o) => o.trim()),
assumptions: assumptions.filter((a) => a.trim()),
selectedOption: selectedOption || null,
...(outcomeReview ? { outcomeReview } : {}),
...(reviewDueAt ? { reviewDueAt: new Date(reviewDueAt).toISOString() } : { reviewDueAt: null }),
},
tenantId,
});
await qc.invalidateQueries({ queryKey: ['decisions', tenantId] });
setDirty(false);
} catch (e) {
setSaveError(e instanceof Error ? e.message : 'Eroare la salvare');
} finally {
setSaving(false);
}
}
const selected = decisions.find((d) => d.id === selectedId);
const inProgress = decisions.filter((d) => !d.selectedOption);
const decided = decisions.filter((d) => !!d.selectedOption);
const validOptions = options.filter((o) => o.trim());
return (
<div className="flex gap-0" style={{ height: 'calc(100vh - 7rem)' }}>
{/* Sidebar */}
<aside className="w-60 shrink-0 border-r border-border/50 flex flex-col overflow-hidden">
<div className="p-4 border-b border-border/50 shrink-0">
<h1 className="font-semibold text-sm text-ink">Decision Workspace</h1>
<p className="text-xs text-ink-faint mt-0.5">Analizează și documentează decizii</p>
</div>
<div className="p-3 shrink-0">
<button
onClick={handleNew}
className="w-full rounded-lg bg-primary px-3 py-2 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
+ Decizie nouă
</button>
</div>
<div className="flex-1 overflow-y-auto px-2 pb-4 space-y-1">
{isLoading && (
<p className="px-2 py-4 text-xs text-ink-faint text-center">Se încarcă</p>
)}
{!isLoading && decisions.length === 0 && (
<p className="px-2 py-4 text-xs text-ink-faint text-center">
Nicio decizie.<br />Apasă + Decizie nouă
</p>
)}
{inProgress.length > 0 && (
<>
<p className="px-2 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-ink-faint">
În analiză ({inProgress.length})
</p>
{inProgress.map((d) => (
<button
key={d.id}
onClick={() => setSelectedId(d.id)}
className={`w-full text-left rounded-lg px-3 py-2.5 transition-colors ${
selectedId === d.id ? 'bg-primary/10 text-primary' : 'hover:bg-muted/50 text-ink'
}`}
>
<p className="text-xs font-medium line-clamp-2 leading-snug">{d.context}</p>
<p className="text-[10px] text-ink-faint mt-0.5">
{new Date(d.createdAt).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' })}
</p>
</button>
))}
</>
)}
{decided.length > 0 && (
<>
<p className="px-2 pt-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-ink-faint">
Decise ({decided.length})
</p>
{decided.map((d) => (
<button
key={d.id}
onClick={() => setSelectedId(d.id)}
className={`w-full text-left rounded-lg px-3 py-2.5 transition-colors ${
selectedId === d.id ? 'bg-primary/10 text-primary' : 'hover:bg-muted/50 text-ink-faint'
}`}
>
<p className="text-xs line-clamp-2 leading-snug">{d.context}</p>
</button>
))}
</>
)}
</div>
</aside>
{/* Main editor */}
{!selectedId ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-3">
<p className="text-4xl"></p>
<p className="text-sm text-ink-faint max-w-xs">
Selectează o decizie din stânga sau creează una nouă pentru a documenta analiza
</p>
<button
onClick={handleNew}
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
+ Decizie nouă
</button>
</div>
</div>
) : (
<main className="flex-1 overflow-y-auto p-6">
<div className="max-w-2xl space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-full ${
selected?.selectedOption
? 'bg-signal-ok/10 text-signal-ok'
: 'bg-signal-warn/10 text-signal-warn'
}`}>
{selected?.selectedOption ? '✓ Decisă' : '○ În analiză'}
</span>
{selected?.reviewDueAt && (
<span className="text-[11px] text-ink-faint">
Review {new Date(selected.reviewDueAt).toLocaleDateString('ro-RO')}
</span>
)}
</div>
<div className="flex items-center gap-3">
{saveError && <span className="text-xs text-signal-danger">{saveError}</span>}
{dirty ? (
<button
onClick={handleSave}
disabled={saving}
className="rounded-lg bg-primary px-4 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{saving ? 'Se salvează…' : 'Salvează'}
</button>
) : (
<span className="text-xs text-ink-faint">Salvat</span>
)}
</div>
</div>
{/* Context */}
<section className="space-y-2">
<label className="block text-xs font-semibold uppercase tracking-wider text-ink-faint">
Context & Situație
</label>
<textarea
value={context}
onChange={(e) => { setContext(e.target.value); setDirty(true); }}
rows={4}
placeholder="Descrie situația completă, ce trebuie decis și de ce contează…"
className="w-full rounded-lg border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring resize-none"
/>
</section>
{/* Options */}
<section className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Opțiuni ({validOptions.length})
</label>
<button
onClick={() => { setOptions([...options, '']); setDirty(true); }}
className="text-xs text-bronze-deep hover:underline"
>
+ Adaugă
</button>
</div>
<div className="space-y-2">
{options.map((opt, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-xs text-ink-faint font-mono w-4 shrink-0 text-right">{i + 1}.</span>
<input
value={opt}
onChange={(e) => {
const next = [...options];
next[i] = e.target.value;
setOptions(next);
setDirty(true);
}}
placeholder={`Opțiunea ${i + 1}`}
className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
{options.length > 1 && (
<button
onClick={() => { setOptions(options.filter((_, j) => j !== i)); setDirty(true); }}
className="text-xs text-signal-danger/60 hover:text-signal-danger"
>
</button>
)}
</div>
))}
</div>
</section>
{/* Assumptions */}
<section className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Ipoteze & Riscuri
</label>
<button
onClick={() => { setAssumptions([...assumptions, '']); setDirty(true); }}
className="text-xs text-bronze-deep hover:underline"
>
+ Adaugă
</button>
</div>
<div className="space-y-2">
{assumptions.map((a, i) => (
<div key={i} className="flex items-center gap-2">
<input
value={a}
onChange={(e) => {
const next = [...assumptions];
next[i] = e.target.value;
setAssumptions(next);
setDirty(true);
}}
placeholder={`Ipoteză sau risc ${i + 1}`}
className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
{assumptions.length > 1 && (
<button
onClick={() => { setAssumptions(assumptions.filter((_, j) => j !== i)); setDirty(true); }}
className="text-xs text-signal-danger/60 hover:text-signal-danger"
>
</button>
)}
</div>
))}
</div>
</section>
{/* Decision box */}
<section className="rounded-xl border-2 border-primary/25 bg-primary/5 p-5 space-y-4">
<h3 className="text-xs font-semibold uppercase tracking-wider text-primary/80">
Concluzie & Decizie
</h3>
<div className="space-y-1">
<label className="block text-xs text-ink-faint">Opțiunea aleasă</label>
{validOptions.length > 0 ? (
<select
value={selectedOption}
onChange={(e) => { setSelectedOption(e.target.value); setDirty(true); }}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value=""> Decizie nepronunțată </option>
{validOptions.map((opt, i) => (
<option key={i} value={opt}>{opt}</option>
))}
</select>
) : (
<input
value={selectedOption}
onChange={(e) => { setSelectedOption(e.target.value); setDirty(true); }}
placeholder="Introdu decizia luată…"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
)}
</div>
<div className="space-y-1">
<label className="block text-xs text-ink-faint">Data de review a rezultatelor</label>
<input
type="date"
value={reviewDueAt}
onChange={(e) => { setReviewDueAt(e.target.value); setDirty(true); }}
className="rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
</section>
{/* Outcome review */}
{selectedOption && (
<section className="space-y-2">
<label className="block text-xs font-semibold uppercase tracking-wider text-ink-faint">
Review Rezultat
</label>
<p className="text-xs text-ink-faint">
Ce s-a întâmplat? Ce ipoteză s-a dovedit greșită? Ce ai învățat?
</p>
<textarea
value={outcomeReview}
onChange={(e) => { setOutcomeReview(e.target.value); setDirty(true); }}
rows={4}
placeholder="Retrospectivă după ce decizia a produs efecte…"
className="w-full rounded-lg border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring resize-none"
/>
</section>
)}
</div>
</main>
)}
</div>
);
}