feat: inline resolve for sub-deadlines + milestone date tooltips

- DeadlineTimeline gains onResolveInline callback prop
- Milestone labels show small green checkmark button for resolvable items
- Clicking opens inline text input (motiv + OK/Cancel, Enter/Escape)
- RegistryEntryDetail wires resolve via onResolveDeadline prop
- Milestone date labels show "Data maximă: ..." on hover
- Auto-refreshes viewed entry after inline resolve

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-12 19:27:03 +02:00
parent 0f928b08e9
commit 55c807dd1b
3 changed files with 138 additions and 44 deletions
@@ -1,7 +1,9 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import { CheckCircle2 } from "lucide-react";
import type { TrackedDeadline } from "../types"; import type { TrackedDeadline } from "../types";
import { getDeadlineType } from "../services/deadline-catalog"; import { getDeadlineType } from "../services/deadline-catalog";
import { getDeadlineDisplayStatus } from "../services/deadline-service"; import { getDeadlineDisplayStatus } from "../services/deadline-service";
@@ -29,6 +31,8 @@ interface TimelineMilestone {
interface DeadlineTimelineProps { interface DeadlineTimelineProps {
/** All tracked deadlines on this entry */ /** All tracked deadlines on this entry */
deadlines: TrackedDeadline[]; deadlines: TrackedDeadline[];
/** Callback to resolve a sub-deadline inline (id, note) */
onResolveInline?: (deadlineId: string, note: string) => void;
} }
// ── Build timeline from deadlines ── // ── Build timeline from deadlines ──
@@ -286,7 +290,7 @@ function formatFullDate(iso: string): string {
// ── Component ── // ── Component ──
export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) { export function DeadlineTimeline({ deadlines, onResolveInline }: DeadlineTimelineProps) {
const { mainDeadlines, groups } = useMemo( const { mainDeadlines, groups } = useMemo(
() => buildTimeline(deadlines), () => buildTimeline(deadlines),
[deadlines], [deadlines],
@@ -395,6 +399,7 @@ export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) {
<MilestoneTimeline <MilestoneTimeline
milestones={milestones} milestones={milestones}
mainDeadline={main.deadline} mainDeadline={main.deadline}
onResolveInline={onResolveInline}
/> />
)} )}
</div> </div>
@@ -449,10 +454,14 @@ function ProgressBar({
function MilestoneTimeline({ function MilestoneTimeline({
milestones, milestones,
mainDeadline, mainDeadline,
onResolveInline,
}: { }: {
milestones: TimelineMilestone[]; milestones: TimelineMilestone[];
mainDeadline: TrackedDeadline; mainDeadline: TrackedDeadline;
onResolveInline?: (deadlineId: string, note: string) => void;
}) { }) {
const [resolvingId, setResolvingId] = useState<string | null>(null);
const [resolveNote, setResolveNote] = useState("");
return ( return (
<div className="mt-3 space-y-0"> <div className="mt-3 space-y-0">
{/* Timeline track */} {/* Timeline track */}
@@ -501,54 +510,125 @@ function MilestoneTimeline({
{/* Milestone labels below */} {/* Milestone labels below */}
<div className="space-y-1.5 pt-1"> <div className="space-y-1.5 pt-1">
{milestones.map((ms) => ( {milestones.map((ms) => {
<div key={ms.deadline.id} className="flex items-start gap-2 text-[10px]"> const canResolve =
<span onResolveInline &&
className={cn( ms.deadline.resolution === "pending" &&
"mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full", ms.status !== "expired";
ms.status === "completed" const isResolving = resolvingId === ms.deadline.id;
? "bg-green-500"
: ms.status === "expired" return (
? "bg-muted-foreground/40" <div key={ms.deadline.id} className="text-[10px]">
: ms.status === "active" <div className="flex items-start gap-2">
? "bg-amber-500"
: "bg-border",
)}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span <span
className={cn( className={cn(
"font-medium", "mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full",
ms.status === "expired" && "text-muted-foreground line-through", ms.status === "completed"
ms.status === "completed" && "text-muted-foreground", ? "bg-green-500"
: ms.status === "expired"
? "bg-muted-foreground/40"
: ms.status === "active"
? "bg-amber-500"
: "bg-border",
)} )}
> />
{ms.label} <div className="min-w-0 flex-1">
</span> <div className="flex items-center gap-2">
<span className="text-muted-foreground"> <span
{formatShortDate(ms.dueDate)} className={cn(
</span> "font-medium",
<span ms.status === "expired" && "text-muted-foreground line-through",
className={cn( ms.status === "completed" && "text-muted-foreground",
ms.status === "expired" )}
? "text-muted-foreground italic" >
: ms.status === "active" {ms.label}
? "text-amber-600 dark:text-amber-400 font-medium" </span>
: ms.status === "completed" <span className="text-muted-foreground" title={`Data maximă: ${formatFullDate(ms.dueDate)}`}>
? "text-green-600 dark:text-green-400" {formatShortDate(ms.dueDate)}
: "text-muted-foreground", </span>
<span
className={cn(
ms.status === "expired"
? "text-muted-foreground italic"
: ms.status === "active"
? "text-amber-600 dark:text-amber-400 font-medium"
: ms.status === "completed"
? "text-green-600 dark:text-green-400"
: "text-muted-foreground",
)}
>
{ms.statusText}
</span>
{canResolve && !isResolving && (
<button
onClick={() => {
setResolvingId(ms.deadline.id);
setResolveNote("");
}}
className="ml-1 flex items-center gap-0.5 text-green-600 hover:text-green-500 dark:text-green-400"
title="Rezolva rapid"
>
<CheckCircle2 className="h-3 w-3" />
</button>
)}
</div>
{ms.description && (
<p className="text-muted-foreground italic">{ms.description}</p>
)} )}
> </div>
{ms.statusText}
</span>
</div> </div>
{ms.description && (
<p className="text-muted-foreground italic">{ms.description}</p> {/* Inline resolve form */}
{isResolving && (
<div className="ml-3.5 mt-1 flex items-center gap-1.5 rounded border bg-muted/50 px-2 py-1">
<input
type="text"
placeholder="Motiv (ex: transmis pe email)"
value={resolveNote}
onChange={(e) => setResolveNote(e.target.value)}
className="flex-1 bg-transparent text-[10px] outline-none placeholder:text-muted-foreground/60"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && resolveNote.trim()) {
onResolveInline?.(ms.deadline.id, resolveNote.trim());
setResolvingId(null);
setResolveNote("");
}
if (e.key === "Escape") {
setResolvingId(null);
setResolveNote("");
}
}}
/>
<Button
size="sm"
variant="ghost"
className="h-5 px-1.5 text-[9px] text-green-600"
disabled={!resolveNote.trim()}
onClick={() => {
onResolveInline?.(ms.deadline.id, resolveNote.trim());
setResolvingId(null);
setResolveNote("");
}}
>
OK
</Button>
<Button
size="sm"
variant="ghost"
className="h-5 px-1 text-[9px] text-muted-foreground"
onClick={() => {
setResolvingId(null);
setResolveNote("");
}}
>
</Button>
</div>
)} )}
</div> </div>
</div> );
))} })}
</div> </div>
</div> </div>
); );
@@ -617,6 +617,12 @@ export function RegistraturaModule() {
onClose={handleCloseViaReply} onClose={handleCloseViaReply}
onDelete={handleDelete} onDelete={handleDelete}
onReply={handleConex} onReply={handleConex}
onResolveDeadline={async (entryId, deadlineId, resolution, note) => {
await resolveDeadline(entryId, deadlineId, resolution as DeadlineResolution, note);
// Refresh the viewing entry
const refreshed = await loadFullEntry(entryId);
if (refreshed) setViewingEntry(refreshed);
}}
allEntries={allEntries} allEntries={allEntries}
/> />
@@ -63,6 +63,8 @@ interface RegistryEntryDetailProps {
onDelete: (id: string) => void; onDelete: (id: string) => void;
/** Create a new entry linked as reply (conex) to this entry */ /** Create a new entry linked as reply (conex) to this entry */
onReply?: (entry: RegistryEntry) => void; onReply?: (entry: RegistryEntry) => void;
/** Resolve a tracked deadline inline */
onResolveDeadline?: (entryId: string, deadlineId: string, resolution: string, note: string) => void;
allEntries: RegistryEntry[]; allEntries: RegistryEntry[];
} }
@@ -152,6 +154,7 @@ export function RegistryEntryDetail({
onClose, onClose,
onDelete, onDelete,
onReply, onReply,
onResolveDeadline,
allEntries, allEntries,
}: RegistryEntryDetailProps) { }: RegistryEntryDetailProps) {
const [previewIndex, setPreviewIndex] = useState<number | null>(null); const [previewIndex, setPreviewIndex] = useState<number | null>(null);
@@ -595,7 +598,12 @@ export function RegistryEntryDetail({
{/* ── Legal deadlines (timeline view) ── */} {/* ── Legal deadlines (timeline view) ── */}
{(entry.trackedDeadlines ?? []).length > 0 && ( {(entry.trackedDeadlines ?? []).length > 0 && (
<DetailSection title="Termene legale"> <DetailSection title="Termene legale">
<DeadlineTimeline deadlines={entry.trackedDeadlines!} /> <DeadlineTimeline
deadlines={entry.trackedDeadlines!}
onResolveInline={onResolveDeadline ? (deadlineId, note) => {
onResolveDeadline(entry.id, deadlineId, "completed", note);
} : undefined}
/>
</DetailSection> </DetailSection>
)} )}