feat(cc-049): Priorities — smart focus list from tasks, goals, decisions
This commit is contained in:
parent
7ac5e98be9
commit
7c2931b2c8
1 changed files with 234 additions and 0 deletions
234
src/app/dashboard/priorities/page.tsx
Normal file
234
src/app/dashboard/priorities/page.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiFetch, type Goal, type Decision } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
export default function PrioritiesPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
|
||||
const { data: tasks = [] } = useQuery({
|
||||
queryKey: ['tasks-prio', tenantId],
|
||||
queryFn: () => apiFetch<Task[]>('/v1/tasks', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const { data: goals = [] } = useQuery({
|
||||
queryKey: ['goals-prio', tenantId],
|
||||
queryFn: () => apiFetch<Goal[]>('/v1/goals', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const { data: decisions = [] } = useQuery({
|
||||
queryKey: ['decisions-prio', tenantId],
|
||||
queryFn: () => apiFetch<Decision[]>('/v1/decisions', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const in7Days = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const activeTasks = tasks.filter((t) => t.status !== 'done');
|
||||
const overdueTasks = activeTasks.filter((t) => t.dueDate && new Date(t.dueDate) < now);
|
||||
const urgentTasks = activeTasks.filter(
|
||||
(t) =>
|
||||
t.dueDate &&
|
||||
new Date(t.dueDate) >= now &&
|
||||
new Date(t.dueDate) <= in7Days &&
|
||||
!overdueTasks.includes(t),
|
||||
);
|
||||
const inProgressTasks = activeTasks.filter(
|
||||
(t) => t.status === 'in_progress' && !overdueTasks.includes(t) && !urgentTasks.includes(t),
|
||||
);
|
||||
|
||||
const riskyGoals = goals.filter((g) => g.status === 'at_risk');
|
||||
const overdueReviews = decisions.filter(
|
||||
(d) => d.selectedOption && d.reviewDueAt && new Date(d.reviewDueAt) < now,
|
||||
);
|
||||
const pendingDecisions = decisions.filter((d) => !d.selectedOption);
|
||||
|
||||
const allClear =
|
||||
overdueTasks.length === 0 &&
|
||||
urgentTasks.length === 0 &&
|
||||
riskyGoals.length === 0 &&
|
||||
overdueReviews.length === 0;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Priorități</h1>
|
||||
<p className="mt-1 text-sm text-ink-faint">
|
||||
Focus list generat automat din sarcini, obiective și decizii.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{allClear && (
|
||||
<div className="card p-10 text-center">
|
||||
<p className="text-lg font-medium text-signal-ok">✓ Totul este în regulă</p>
|
||||
<p className="mt-1 text-sm text-ink-faint">
|
||||
Nu există sarcini restante, obiective la risc sau decizii blocate.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{overdueTasks.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-signal-danger">
|
||||
<span className="h-2 w-2 rounded-full bg-signal-danger" />
|
||||
Urgente — Restante ({overdueTasks.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{overdueTasks.map((t) => (
|
||||
<li
|
||||
key={t.id}
|
||||
className="card flex items-center gap-4 border-l-2 border-signal-danger px-4 py-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-ink">{t.title}</p>
|
||||
{t.dueDate && (
|
||||
<p className="mt-0.5 text-[11px] text-signal-danger">
|
||||
Limită: {new Date(t.dueDate).toLocaleDateString('ro-RO')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/tasks"
|
||||
className="shrink-0 text-xs text-bronze-deep hover:underline"
|
||||
>
|
||||
Gestionează
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{urgentTasks.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-signal-warn">
|
||||
<span className="h-2 w-2 rounded-full bg-signal-warn" />
|
||||
Scadente în 7 zile ({urgentTasks.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{urgentTasks.map((t) => (
|
||||
<li key={t.id} className="card flex items-center gap-4 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm text-ink">{t.title}</p>
|
||||
{t.dueDate && (
|
||||
<p className="mt-0.5 text-[11px] text-signal-warn">
|
||||
Limită: {new Date(t.dueDate).toLocaleDateString('ro-RO')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{inProgressTasks.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-ink-faint">
|
||||
<span className="h-2 w-2 rounded-full bg-bronze-deep" />
|
||||
În lucru ({inProgressTasks.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{inProgressTasks.slice(0, 5).map((t) => (
|
||||
<li key={t.id} className="card px-4 py-3">
|
||||
<p className="truncate text-sm text-ink">{t.title}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{riskyGoals.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-signal-warn">
|
||||
<span className="h-2 w-2 rounded-full bg-signal-warn" />
|
||||
Obiective la risc ({riskyGoals.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{riskyGoals.map((g) => (
|
||||
<li key={g.id} className="card flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm text-ink">{g.metric}</p>
|
||||
<p className="mt-0.5 text-[11px] text-ink-faint">
|
||||
Target: {g.target} · {g.horizon}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/goals"
|
||||
className="shrink-0 text-xs text-bronze-deep hover:underline"
|
||||
>
|
||||
Actualizează
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{overdueReviews.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-signal-danger">
|
||||
<span className="h-2 w-2 rounded-full bg-signal-danger" />
|
||||
Review decizii întârziate ({overdueReviews.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{overdueReviews.map((d) => (
|
||||
<li
|
||||
key={d.id}
|
||||
className="card flex items-center gap-4 border-l-2 border-signal-danger px-4 py-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm text-ink">{d.context}</p>
|
||||
<p className="mt-0.5 text-[11px] text-signal-danger">
|
||||
Review due: {new Date(d.reviewDueAt!).toLocaleDateString('ro-RO')}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/decisions/register"
|
||||
className="shrink-0 text-xs text-bronze-deep hover:underline"
|
||||
>
|
||||
Revizuiește
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{pendingDecisions.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-ink-faint">
|
||||
<span className="h-2 w-2 rounded-full border border-ink-line bg-paper-sunken" />
|
||||
Decizii fără opțiune aleasă ({pendingDecisions.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{pendingDecisions.slice(0, 5).map((d) => (
|
||||
<li key={d.id} className="card flex items-center gap-4 px-4 py-3">
|
||||
<p className="flex-1 truncate text-sm text-ink">{d.context}</p>
|
||||
<Link
|
||||
href="/dashboard/decisions/register"
|
||||
className="shrink-0 text-xs text-bronze-deep hover:underline"
|
||||
>
|
||||
Decide
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue