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";
import { useMemo } from "react";
import { useMemo, useState } from "react";
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 { getDeadlineType } from "../services/deadline-catalog";
import { getDeadlineDisplayStatus } from "../services/deadline-service";
@@ -29,6 +31,8 @@ interface TimelineMilestone {
interface DeadlineTimelineProps {
/** All tracked deadlines on this entry */
deadlines: TrackedDeadline[];
/** Callback to resolve a sub-deadline inline (id, note) */
onResolveInline?: (deadlineId: string, note: string) => void;
}
// ── Build timeline from deadlines ──
@@ -286,7 +290,7 @@ function formatFullDate(iso: string): string {
// ── Component ──
export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) {
export function DeadlineTimeline({ deadlines, onResolveInline }: DeadlineTimelineProps) {
const { mainDeadlines, groups } = useMemo(
() => buildTimeline(deadlines),
[deadlines],
@@ -395,6 +399,7 @@ export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) {
<MilestoneTimeline
milestones={milestones}
mainDeadline={main.deadline}
onResolveInline={onResolveInline}
/>
)}
</div>
@@ -449,10 +454,14 @@ function ProgressBar({
function MilestoneTimeline({
milestones,
mainDeadline,
onResolveInline,
}: {
milestones: TimelineMilestone[];
mainDeadline: TrackedDeadline;
onResolveInline?: (deadlineId: string, note: string) => void;
}) {
const [resolvingId, setResolvingId] = useState<string | null>(null);
const [resolveNote, setResolveNote] = useState("");
return (
<div className="mt-3 space-y-0">
{/* Timeline track */}
@@ -501,8 +510,16 @@ function MilestoneTimeline({
{/* Milestone labels below */}
<div className="space-y-1.5 pt-1">
{milestones.map((ms) => (
<div key={ms.deadline.id} className="flex items-start gap-2 text-[10px]">
{milestones.map((ms) => {
const canResolve =
onResolveInline &&
ms.deadline.resolution === "pending" &&
ms.status !== "expired";
const isResolving = resolvingId === ms.deadline.id;
return (
<div key={ms.deadline.id} className="text-[10px]">
<div className="flex items-start gap-2">
<span
className={cn(
"mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full",
@@ -526,7 +543,7 @@ function MilestoneTimeline({
>
{ms.label}
</span>
<span className="text-muted-foreground">
<span className="text-muted-foreground" title={`Data maximă: ${formatFullDate(ms.dueDate)}`}>
{formatShortDate(ms.dueDate)}
</span>
<span
@@ -542,13 +559,76 @@ function MilestoneTimeline({
>
{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>
</div>
))}
{/* 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>
);
@@ -617,6 +617,12 @@ export function RegistraturaModule() {
onClose={handleCloseViaReply}
onDelete={handleDelete}
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}
/>
@@ -63,6 +63,8 @@ interface RegistryEntryDetailProps {
onDelete: (id: string) => void;
/** Create a new entry linked as reply (conex) to this entry */
onReply?: (entry: RegistryEntry) => void;
/** Resolve a tracked deadline inline */
onResolveDeadline?: (entryId: string, deadlineId: string, resolution: string, note: string) => void;
allEntries: RegistryEntry[];
}
@@ -152,6 +154,7 @@ export function RegistryEntryDetail({
onClose,
onDelete,
onReply,
onResolveDeadline,
allEntries,
}: RegistryEntryDetailProps) {
const [previewIndex, setPreviewIndex] = useState<number | null>(null);
@@ -595,7 +598,12 @@ export function RegistryEntryDetail({
{/* ── Legal deadlines (timeline view) ── */}
{(entry.trackedDeadlines ?? []).length > 0 && (
<DetailSection title="Termene legale">
<DeadlineTimeline deadlines={entry.trackedDeadlines!} />
<DeadlineTimeline
deadlines={entry.trackedDeadlines!}
onResolveInline={onResolveDeadline ? (deadlineId, note) => {
onResolveDeadline(entry.id, deadlineId, "completed", note);
} : undefined}
/>
</DetailSection>
)}