feat(CC-089): add Daily Standup page (yesterday/today/blockers/mood -> saved as observations)
This commit is contained in:
parent
25a27dd9d7
commit
7a30bbf1ac
1 changed files with 163 additions and 0 deletions
163
src/app/dashboard/standup/page.tsx
Normal file
163
src/app/dashboard/standup/page.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
interface Task { id: string; title: string; status: string; priority: string | null; tags: string[]; updatedAt: string; createdAt: string; }
|
||||
interface Observation { id: string; metric: string; value: string; createdAt: string; }
|
||||
|
||||
const TODAY = new Date().toISOString().slice(0, 10);
|
||||
|
||||
export default function StandupPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const qc = useQueryClient();
|
||||
|
||||
const [yesterday, setYesterday] = useState('');
|
||||
const [today, setToday] = useState('');
|
||||
const [blockers, setBlockers] = useState('');
|
||||
const [mood, setMood] = useState(3);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const { data: tasks = [] } = useQuery({ queryKey: ['standup-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
|
||||
const { data: observations = [] } = useQuery({ queryKey: ['standup-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
|
||||
|
||||
const completedToday = useMemo(() =>
|
||||
tasks.filter((t) => t.status === 'completed' && t.updatedAt.startsWith(TODAY)),
|
||||
[tasks]);
|
||||
|
||||
const todayObs = useMemo(() =>
|
||||
observations.filter((o) => o.createdAt.startsWith(TODAY)),
|
||||
[observations]);
|
||||
|
||||
const upcoming = useMemo(() =>
|
||||
tasks.filter((t) => !['completed','cancelled'].includes(t.status) && t.priority === 'urgent').slice(0, 5),
|
||||
[tasks]);
|
||||
|
||||
const submitMut = useMutation({
|
||||
mutationFn: async () => {
|
||||
const promises = [];
|
||||
if (yesterday.trim()) {
|
||||
promises.push(apiFetch('/v1/observations', { tenantId, method: 'POST', body: {
|
||||
metric: 'standup-yesterday', value: yesterday.trim(),
|
||||
subjectType: 'daily-log', confidence: 1, observedAt: new Date().toISOString(),
|
||||
}}));
|
||||
}
|
||||
if (today.trim()) {
|
||||
promises.push(apiFetch('/v1/observations', { tenantId, method: 'POST', body: {
|
||||
metric: 'standup-today', value: today.trim(),
|
||||
subjectType: 'daily-log', confidence: 1, observedAt: new Date().toISOString(),
|
||||
}}));
|
||||
}
|
||||
if (blockers.trim()) {
|
||||
promises.push(apiFetch('/v1/observations', { tenantId, method: 'POST', body: {
|
||||
metric: 'standup-blockers', value: blockers.trim(),
|
||||
subjectType: 'daily-log', confidence: 1, observedAt: new Date().toISOString(),
|
||||
}}));
|
||||
}
|
||||
promises.push(apiFetch('/v1/observations', { tenantId, method: 'POST', body: {
|
||||
metric: 'energy-level', value: String(mood),
|
||||
unit: '/5', subjectType: 'personal', confidence: 1, observedAt: new Date().toISOString(),
|
||||
}}));
|
||||
await Promise.all(promises);
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['standup-obs', tenantId] });
|
||||
setSubmitted(true);
|
||||
},
|
||||
});
|
||||
|
||||
const dateLabel = new Date().toLocaleDateString('ro-RO', { weekday: 'long', day: 'numeric', month: 'long' });
|
||||
const MOODS = ['😴', '😐', '🙂', '😊', '🚀'];
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Daily Standup</h1>
|
||||
<p className="text-sm text-ink-faint mt-1 capitalize">{dateLabel}</p>
|
||||
</div>
|
||||
|
||||
{submitted ? (
|
||||
<div className="card p-8 text-center space-y-3">
|
||||
<p className="text-4xl">✅</p>
|
||||
<p className="text-lg font-semibold text-ink">Standup salvat!</p>
|
||||
<p className="text-sm text-ink-faint">Răspunsurile au fost logate ca observații în CEO OS.</p>
|
||||
<button onClick={() => { setSubmitted(false); setYesterday(''); setToday(''); setBlockers(''); setMood(3); }}
|
||||
className="rounded-lg border px-4 py-2 text-sm text-ink hover:bg-muted/50">
|
||||
Nou standup
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Context from CEO OS */}
|
||||
{(completedToday.length > 0 || todayObs.length > 0) && (
|
||||
<div className="card p-4 space-y-2 bg-primary/5 border-primary/20">
|
||||
<p className="text-xs font-semibold text-ink">Din CEO OS azi</p>
|
||||
{completedToday.length > 0 && <p className="text-xs text-signal-ok">✓ {completedToday.length} task-uri finalizate astăzi</p>}
|
||||
{todayObs.length > 0 && <p className="text-xs text-ink-faint">📊 {todayObs.length} observații logate</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standup form */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-ink">Ce ai realizat ieri?</label>
|
||||
<textarea value={yesterday} onChange={(e) => setYesterday(e.target.value)}
|
||||
placeholder="Descrie pe scurt ce ai terminat sau avansat…"
|
||||
rows={3} className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
|
||||
{completedToday.length > 0 && (
|
||||
<div className="text-[10px] text-ink-faint space-y-0.5">
|
||||
{completedToday.slice(0, 3).map((t) => (
|
||||
<button key={t.id} onClick={() => setYesterday((p) => p ? p + '\n• ' + t.title : '• ' + t.title)}
|
||||
className="block text-left hover:text-primary">+ {t.title}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-ink">Ce vei face azi?</label>
|
||||
<textarea value={today} onChange={(e) => setToday(e.target.value)}
|
||||
placeholder="Prioritățile pentru ziua de azi…"
|
||||
rows={3} className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
|
||||
{upcoming.length > 0 && (
|
||||
<div className="text-[10px] text-ink-faint space-y-0.5">
|
||||
{upcoming.slice(0, 3).map((t) => (
|
||||
<button key={t.id} onClick={() => setToday((p) => p ? p + '\n• ' + t.title : '• ' + t.title)}
|
||||
className="block text-left hover:text-primary">+ {t.title}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-ink">Blocaje? (opțional)</label>
|
||||
<textarea value={blockers} onChange={(e) => setBlockers(e.target.value)}
|
||||
placeholder="Ce te blochează sau necesită ajutor?"
|
||||
rows={2} className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-ink">Energie azi</label>
|
||||
<div className="flex gap-3">
|
||||
{MOODS.map((emoji, i) => (
|
||||
<button key={i} onClick={() => setMood(i + 1)}
|
||||
className={`text-2xl rounded-lg p-2 transition-all ${mood === i + 1 ? 'bg-primary/10 scale-110 ring-2 ring-primary/40' : 'hover:bg-muted/50'}`}>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={() => submitMut.mutate()} disabled={(!yesterday.trim() && !today.trim()) || submitMut.isPending}
|
||||
className="w-full rounded-lg bg-primary py-3 text-sm font-semibold text-white hover:bg-primary/90 disabled:opacity-50">
|
||||
{submitMut.isPending ? 'Se salvează…' : 'Salvează standup-ul'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue