feat(dashboard): add quick actions and new overview widgets
This commit is contained in:
157
components/dashboard/notes-widget.tsx
Normal file
157
components/dashboard/notes-widget.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { RiEyeLine, RiPencilLine, RiTodoLine } from "@remixicon/react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog";
|
||||
import { NoteDialog } from "@/components/anotacoes/note-dialog";
|
||||
import type { Note } from "@/components/anotacoes/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import type { DashboardNote } from "@/lib/dashboard/notes";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { WidgetEmptyState } from "../widget-empty-state";
|
||||
|
||||
type NotesWidgetProps = {
|
||||
notes: DashboardNote[];
|
||||
};
|
||||
|
||||
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
const buildDisplayTitle = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : "Anotação sem título";
|
||||
};
|
||||
|
||||
const mapDashboardNoteToNote = (note: DashboardNote): Note => ({
|
||||
id: note.id,
|
||||
title: note.title,
|
||||
description: note.description,
|
||||
type: note.type,
|
||||
tasks: note.tasks,
|
||||
arquivada: note.arquivada,
|
||||
createdAt: note.createdAt,
|
||||
});
|
||||
|
||||
const getTasksSummary = (note: DashboardNote) => {
|
||||
if (note.type !== "tarefa") {
|
||||
return "Nota";
|
||||
}
|
||||
|
||||
const tasks = note.tasks ?? [];
|
||||
const completed = tasks.filter((task) => task.completed).length;
|
||||
return `${completed}/${tasks.length} concluídas`;
|
||||
};
|
||||
|
||||
export function NotesWidget({ notes }: NotesWidgetProps) {
|
||||
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
const [noteDetails, setNoteDetails] = useState<Note | null>(null);
|
||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||||
|
||||
const mappedNotes = useMemo(() => notes.map(mapDashboardNoteToNote), [notes]);
|
||||
|
||||
const handleOpenEdit = useCallback((note: Note) => {
|
||||
setNoteToEdit(note);
|
||||
setIsEditOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOpenDetails = useCallback((note: Note) => {
|
||||
setNoteDetails(note);
|
||||
setIsDetailsOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditOpenChange = useCallback((open: boolean) => {
|
||||
setIsEditOpen(open);
|
||||
if (!open) {
|
||||
setNoteToEdit(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDetailsOpenChange = useCallback((open: boolean) => {
|
||||
setIsDetailsOpen(open);
|
||||
if (!open) {
|
||||
setNoteDetails(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContent className="flex flex-col gap-4 px-0">
|
||||
{mappedNotes.length === 0 ? (
|
||||
<WidgetEmptyState
|
||||
icon={<RiTodoLine className="size-6 text-muted-foreground" />}
|
||||
title="Nenhuma anotação ativa"
|
||||
description="Crie anotações para acompanhar lembretes e tarefas financeiras."
|
||||
/>
|
||||
) : (
|
||||
<ul className="flex flex-col">
|
||||
{mappedNotes.map((note) => (
|
||||
<li
|
||||
key={note.id}
|
||||
className="flex items-center justify-between gap-2 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{buildDisplayTitle(note.title)}
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 px-1.5 text-[10px]"
|
||||
>
|
||||
{getTasksSummary(note)}
|
||||
</Badge>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{DATE_FORMATTER.format(new Date(note.createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => handleOpenEdit(note)}
|
||||
aria-label={`Editar anotação ${buildDisplayTitle(note.title)}`}
|
||||
>
|
||||
<RiPencilLine className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => handleOpenDetails(note)}
|
||||
aria-label={`Ver detalhes da anotação ${buildDisplayTitle(
|
||||
note.title,
|
||||
)}`}
|
||||
>
|
||||
<RiEyeLine className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<NoteDialog
|
||||
mode="update"
|
||||
note={noteToEdit ?? undefined}
|
||||
open={isEditOpen}
|
||||
onOpenChange={handleEditOpenChange}
|
||||
/>
|
||||
|
||||
<NoteDetailsDialog
|
||||
note={noteDetails}
|
||||
open={isDetailsOpen}
|
||||
onOpenChange={handleDetailsOpenChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user