forked from git.gladyson/openmonetis
feat: adicionar página de anotações arquivadas e componente de notificação
- Implementa a página de anotações arquivadas, que busca as notas arquivadas do usuário e as exibe utilizando o componente NotesPage. - Cria o componente NotificationBell para gerenciar e exibir notificações de pagamentos, incluindo a formatação de datas e valores monetários. O componente também apresenta um sistema de tooltip e dropdown para interação do usuário.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { anotacoes } from "@/db/schema";
|
import { anotacoes } from "@/db/schema";
|
||||||
import { type ActionResult, handleActionError } from "@/lib/actions/helpers";
|
import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers";
|
||||||
import { revalidateForEntity } from "@/lib/actions/helpers";
|
import type { ActionResult } from "@/lib/actions/types";
|
||||||
import { uuidSchema } from "@/lib/schemas/common";
|
import { uuidSchema } from "@/lib/schemas/common";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
@@ -142,3 +142,45 @@ export async function deleteNoteAction(
|
|||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arquivarNoteSchema = z.object({
|
||||||
|
id: uuidSchema("Anotação"),
|
||||||
|
arquivada: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type NoteArquivarInput = z.infer<typeof arquivarNoteSchema>;
|
||||||
|
|
||||||
|
export async function arquivarAnotacaoAction(
|
||||||
|
input: NoteArquivarInput
|
||||||
|
): Promise<ActionResult> {
|
||||||
|
try {
|
||||||
|
const user = await getUser();
|
||||||
|
const data = arquivarNoteSchema.parse(input);
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(anotacoes)
|
||||||
|
.set({
|
||||||
|
arquivada: data.arquivada,
|
||||||
|
})
|
||||||
|
.where(and(eq(anotacoes.id, data.id), eq(anotacoes.userId, user.id)))
|
||||||
|
.returning({ id: anotacoes.id });
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Anotação não encontrada.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidateForEntity("anotacoes");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: data.arquivada
|
||||||
|
? "Anotação arquivada com sucesso."
|
||||||
|
: "Anotação desarquivada com sucesso."
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleActionError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
14
app/(dashboard)/anotacoes/arquivadas/page.tsx
Normal file
14
app/(dashboard)/anotacoes/arquivadas/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { NotesPage } from "@/components/anotacoes/notes-page";
|
||||||
|
import { getUserId } from "@/lib/auth/server";
|
||||||
|
import { fetchArquivadasForUser } from "../data";
|
||||||
|
|
||||||
|
export default async function ArquivadasPage() {
|
||||||
|
const userId = await getUserId();
|
||||||
|
const notes = await fetchArquivadasForUser(userId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-start gap-6">
|
||||||
|
<NotesPage notes={notes} isArquivadas={true} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { anotacoes, type Anotacao } from "@/db/schema";
|
import { anotacoes, type Anotacao } from "@/db/schema";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,12 +14,13 @@ export type NoteData = {
|
|||||||
description: string;
|
description: string;
|
||||||
type: "nota" | "tarefa";
|
type: "nota" | "tarefa";
|
||||||
tasks?: Task[];
|
tasks?: Task[];
|
||||||
|
arquivada: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
|
export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
|
||||||
const noteRows = await db.query.anotacoes.findMany({
|
const noteRows = await db.query.anotacoes.findMany({
|
||||||
where: eq(anotacoes.userId, userId),
|
where: and(eq(anotacoes.userId, userId), eq(anotacoes.arquivada, false)),
|
||||||
orderBy: (note: typeof anotacoes.$inferSelect, { desc }: { desc: (field: unknown) => unknown }) => [desc(note.createdAt)],
|
orderBy: (note: typeof anotacoes.$inferSelect, { desc }: { desc: (field: unknown) => unknown }) => [desc(note.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,6 +43,38 @@ export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
|
|||||||
description: (note.description ?? "").trim(),
|
description: (note.description ?? "").trim(),
|
||||||
type: (note.type ?? "nota") as "nota" | "tarefa",
|
type: (note.type ?? "nota") as "nota" | "tarefa",
|
||||||
tasks,
|
tasks,
|
||||||
|
arquivada: note.arquivada,
|
||||||
|
createdAt: note.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArquivadasForUser(userId: string): Promise<NoteData[]> {
|
||||||
|
const noteRows = await db.query.anotacoes.findMany({
|
||||||
|
where: and(eq(anotacoes.userId, userId), eq(anotacoes.arquivada, true)),
|
||||||
|
orderBy: (note: typeof anotacoes.$inferSelect, { desc }: { desc: (field: unknown) => unknown }) => [desc(note.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return noteRows.map((note: Anotacao) => {
|
||||||
|
let tasks: Task[] | undefined;
|
||||||
|
|
||||||
|
// Parse tasks if they exist
|
||||||
|
if (note.tasks) {
|
||||||
|
try {
|
||||||
|
tasks = JSON.parse(note.tasks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse tasks for note", note.id, error);
|
||||||
|
tasks = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
title: (note.title ?? "").trim(),
|
||||||
|
description: (note.description ?? "").trim(),
|
||||||
|
type: (note.type ?? "nota") as "nota" | "tarefa",
|
||||||
|
tasks,
|
||||||
|
arquivada: note.arquivada,
|
||||||
createdAt: note.createdAt.toISOString(),
|
createdAt: note.createdAt.toISOString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
|
RiArchiveLine,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
RiEyeLine,
|
||||||
|
RiInboxUnarchiveLine,
|
||||||
RiPencilLine,
|
RiPencilLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@@ -20,9 +22,18 @@ interface NoteCardProps {
|
|||||||
onEdit?: (note: Note) => void;
|
onEdit?: (note: Note) => void;
|
||||||
onDetails?: (note: Note) => void;
|
onDetails?: (note: Note) => void;
|
||||||
onRemove?: (note: Note) => void;
|
onRemove?: (note: Note) => void;
|
||||||
|
onArquivar?: (note: Note) => void;
|
||||||
|
isArquivadas?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
export function NoteCard({
|
||||||
|
note,
|
||||||
|
onEdit,
|
||||||
|
onDetails,
|
||||||
|
onRemove,
|
||||||
|
onArquivar,
|
||||||
|
isArquivadas = false,
|
||||||
|
}: NoteCardProps) {
|
||||||
const { formattedDate, displayTitle } = useMemo(() => {
|
const { formattedDate, displayTitle } = useMemo(() => {
|
||||||
const resolvedTitle = note.title.trim().length
|
const resolvedTitle = note.title.trim().length
|
||||||
? note.title
|
? note.title
|
||||||
@@ -52,6 +63,16 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
|||||||
onClick: onDetails,
|
onClick: onDetails,
|
||||||
variant: "default" as const,
|
variant: "default" as const,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: isArquivadas ? "desarquivar" : "arquivar",
|
||||||
|
icon: isArquivadas ? (
|
||||||
|
<RiInboxUnarchiveLine className="size-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<RiArchiveLine className="size-4" aria-hidden />
|
||||||
|
),
|
||||||
|
onClick: onArquivar,
|
||||||
|
variant: "default" as const,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "remover",
|
label: "remover",
|
||||||
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
||||||
@@ -68,20 +89,17 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
|||||||
<h3 className="text-lg font-semibold leading-tight text-foreground wrap-break-word">
|
<h3 className="text-lg font-semibold leading-tight text-foreground wrap-break-word">
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
</h3>
|
</h3>
|
||||||
|
</div>
|
||||||
{isTask && (
|
{isTask && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{completedCount}/{totalCount} concluídas
|
{completedCount}/{totalCount} concluídas
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground whitespace-nowrap">
|
|
||||||
{formattedDate}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isTask ? (
|
{isTask ? (
|
||||||
<div className="flex-1 overflow-auto space-y-2">
|
<div className="flex-1 overflow-auto space-y-2 mt-2">
|
||||||
{tasks.slice(0, 4).map((task) => (
|
{tasks.slice(0, 5).map((task) => (
|
||||||
<div key={task.id} className="flex items-start gap-2 text-sm">
|
<div key={task.id} className="flex items-start gap-2 text-sm">
|
||||||
<div
|
<div
|
||||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
||||||
@@ -96,24 +114,22 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`leading-relaxed ${
|
className={`leading-relaxed ${
|
||||||
task.completed
|
task.completed ? "text-muted-foreground" : "text-foreground"
|
||||||
? "line-through text-muted-foreground"
|
|
||||||
: "text-foreground"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.text}
|
{task.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{tasks.length > 4 && (
|
{tasks.length > 5 && (
|
||||||
<p className="text-xs text-muted-foreground pl-5 py-1">
|
<p className="text-xs text-muted-foreground pl-5 py-1">
|
||||||
+{tasks.length - 4}{" "}
|
+{tasks.length - 5}
|
||||||
{tasks.length - 4 === 1 ? "tarefa" : "tarefas"}...
|
{tasks.length - 5 === 1 ? "tarefa" : "tarefas"}...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="flex-1 overflow-auto whitespace-pre-line text-sm text-muted-foreground wrap-break-word leading-relaxed">
|
<p className="flex-1 overflow-auto whitespace-pre-line text-sm text-muted-foreground wrap-break-word leading-relaxed mt-2">
|
||||||
{note.description}
|
{note.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { RiCheckLine } from "@remixicon/react";
|
import { RiCheckLine } from "@remixicon/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
import type { Note } from "./types";
|
import type { Note } from "./types";
|
||||||
|
|
||||||
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
|
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
|
||||||
@@ -71,9 +72,9 @@ export function NoteDetailsDialog({
|
|||||||
{isTask ? (
|
{isTask ? (
|
||||||
<div className="max-h-[320px] overflow-auto space-y-3">
|
<div className="max-h-[320px] overflow-auto space-y-3">
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<div
|
<Card
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className="flex items-start gap-3 p-3 rounded-lg border bg-card"
|
className="flex gap-3 p-3 flex-row items-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
||||||
@@ -88,14 +89,12 @@ export function NoteDetailsDialog({
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${
|
className={`text-sm ${
|
||||||
task.completed
|
task.completed ? "text-muted-foreground" : "text-foreground"
|
||||||
? "line-through text-muted-foreground"
|
|
||||||
: "text-foreground"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.text}
|
{task.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
useTransition,
|
useTransition,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
import type { Note, NoteFormValues, Task } from "./types";
|
import type { Note, NoteFormValues, Task } from "./types";
|
||||||
|
|
||||||
type NoteDialogMode = "create" | "update";
|
type NoteDialogMode = "create" | "update";
|
||||||
@@ -404,11 +405,12 @@ export function NoteDialog({
|
|||||||
</label>
|
</label>
|
||||||
<div className="space-y-2 max-h-[240px] overflow-y-auto pr-1">
|
<div className="space-y-2 max-h-[240px] overflow-y-auto pr-1">
|
||||||
{formState.tasks.map((task) => (
|
{formState.tasks.map((task) => (
|
||||||
<div
|
<Card
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
className="flex items-center gap-3 px-3 py-2 flex-row mt-1"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
className="data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600"
|
||||||
checked={task.completed}
|
checked={task.completed}
|
||||||
onCheckedChange={() => handleToggleTask(task.id)}
|
onCheckedChange={() => handleToggleTask(task.id)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
@@ -419,7 +421,7 @@ export function NoteDialog({
|
|||||||
<span
|
<span
|
||||||
className={`flex-1 text-sm wrap-break-word ${
|
className={`flex-1 text-sm wrap-break-word ${
|
||||||
task.completed
|
task.completed
|
||||||
? "line-through text-muted-foreground"
|
? "text-muted-foreground"
|
||||||
: "text-foreground"
|
: "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -436,7 +438,7 @@ export function NoteDialog({
|
|||||||
>
|
>
|
||||||
<RiDeleteBinLine className="h-4 w-4" />
|
<RiDeleteBinLine className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteNoteAction } from "@/app/(dashboard)/anotacoes/actions";
|
import { arquivarAnotacaoAction, deleteNoteAction } from "@/app/(dashboard)/anotacoes/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -15,9 +15,10 @@ import type { Note } from "./types";
|
|||||||
|
|
||||||
interface NotesPageProps {
|
interface NotesPageProps {
|
||||||
notes: Note[];
|
notes: Note[];
|
||||||
|
isArquivadas?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotesPage({ notes }: NotesPageProps) {
|
export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
||||||
@@ -25,6 +26,8 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
const [noteDetails, setNoteDetails] = useState<Note | null>(null);
|
const [noteDetails, setNoteDetails] = useState<Note | null>(null);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const [noteToRemove, setNoteToRemove] = useState<Note | null>(null);
|
const [noteToRemove, setNoteToRemove] = useState<Note | null>(null);
|
||||||
|
const [arquivarOpen, setArquivarOpen] = useState(false);
|
||||||
|
const [noteToArquivar, setNoteToArquivar] = useState<Note | null>(null);
|
||||||
|
|
||||||
const sortedNotes = useMemo(
|
const sortedNotes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -60,6 +63,13 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleArquivarOpenChange = useCallback((open: boolean) => {
|
||||||
|
setArquivarOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setNoteToArquivar(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEditRequest = useCallback((note: Note) => {
|
const handleEditRequest = useCallback((note: Note) => {
|
||||||
setNoteToEdit(note);
|
setNoteToEdit(note);
|
||||||
setEditOpen(true);
|
setEditOpen(true);
|
||||||
@@ -75,6 +85,30 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
setRemoveOpen(true);
|
setRemoveOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleArquivarRequest = useCallback((note: Note) => {
|
||||||
|
setNoteToArquivar(note);
|
||||||
|
setArquivarOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleArquivarConfirm = useCallback(async () => {
|
||||||
|
if (!noteToArquivar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await arquivarAnotacaoAction({
|
||||||
|
id: noteToArquivar.id,
|
||||||
|
arquivada: !isArquivadas,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(result.error);
|
||||||
|
throw new Error(result.error);
|
||||||
|
}, [noteToArquivar, isArquivadas]);
|
||||||
|
|
||||||
const handleRemoveConfirm = useCallback(async () => {
|
const handleRemoveConfirm = useCallback(async () => {
|
||||||
if (!noteToRemove) {
|
if (!noteToRemove) {
|
||||||
return;
|
return;
|
||||||
@@ -97,9 +131,22 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
: "Remover anotação?"
|
: "Remover anotação?"
|
||||||
: "Remover anotação?";
|
: "Remover anotação?";
|
||||||
|
|
||||||
|
const arquivarTitle = noteToArquivar
|
||||||
|
? noteToArquivar.title.trim().length
|
||||||
|
? isArquivadas
|
||||||
|
? `Desarquivar anotação "${noteToArquivar.title}"?`
|
||||||
|
: `Arquivar anotação "${noteToArquivar.title}"?`
|
||||||
|
: isArquivadas
|
||||||
|
? "Desarquivar anotação?"
|
||||||
|
: "Arquivar anotação?"
|
||||||
|
: isArquivadas
|
||||||
|
? "Desarquivar anotação?"
|
||||||
|
: "Arquivar anotação?";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
|
{!isArquivadas && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<NoteDialog
|
<NoteDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
@@ -113,13 +160,22 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sortedNotes.length === 0 ? (
|
{sortedNotes.length === 0 ? (
|
||||||
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
media={<RiTodoLine className="size-6 text-primary" />}
|
media={<RiTodoLine className="size-6 text-primary" />}
|
||||||
title="Nenhuma anotação registrada"
|
title={
|
||||||
description="Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
|
isArquivadas
|
||||||
|
? "Nenhuma anotação arquivada"
|
||||||
|
: "Nenhuma anotação registrada"
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
isArquivadas
|
||||||
|
? "As anotações arquivadas aparecerão aqui."
|
||||||
|
: "Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@@ -131,6 +187,8 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
onEdit={handleEditRequest}
|
onEdit={handleEditRequest}
|
||||||
onDetails={handleDetailsRequest}
|
onDetails={handleDetailsRequest}
|
||||||
onRemove={handleRemoveRequest}
|
onRemove={handleRemoveRequest}
|
||||||
|
onArquivar={handleArquivarRequest}
|
||||||
|
isArquivadas={isArquivadas}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -150,6 +208,21 @@ export function NotesPage({ notes }: NotesPageProps) {
|
|||||||
onOpenChange={handleDetailsOpenChange}
|
onOpenChange={handleDetailsOpenChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmActionDialog
|
||||||
|
open={arquivarOpen}
|
||||||
|
onOpenChange={handleArquivarOpenChange}
|
||||||
|
title={arquivarTitle}
|
||||||
|
description={
|
||||||
|
isArquivadas
|
||||||
|
? "A anotação será movida de volta para a lista principal."
|
||||||
|
: "A anotação será movida para arquivadas."
|
||||||
|
}
|
||||||
|
confirmLabel={isArquivadas ? "Desarquivar" : "Arquivar"}
|
||||||
|
confirmVariant="default"
|
||||||
|
pendingLabel={isArquivadas ? "Desarquivando..." : "Arquivando..."}
|
||||||
|
onConfirm={handleArquivarConfirm}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmActionDialog
|
<ConfirmActionDialog
|
||||||
open={removeOpen}
|
open={removeOpen}
|
||||||
onOpenChange={handleRemoveOpenChange}
|
onOpenChange={handleRemoveOpenChange}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface Note {
|
|||||||
description: string;
|
description: string;
|
||||||
type: NoteType;
|
type: NoteType;
|
||||||
tasks?: Task[];
|
tasks?: Task[];
|
||||||
|
arquivada: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import type { CalendarEvent } from "@/components/calendario/types";
|
|||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
const LEGEND_ITEMS: Array<{
|
const LEGEND_ITEMS: Array<{
|
||||||
type: CalendarEvent["type"];
|
type?: CalendarEvent["type"];
|
||||||
label: string;
|
label: string;
|
||||||
|
dotColor?: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ type: "lancamento", label: "Lançamento financeiro" },
|
{ type: "lancamento", label: "Lançamentos" },
|
||||||
{ type: "boleto", label: "Boleto com vencimento" },
|
{ type: "boleto", label: "Boleto com vencimento" },
|
||||||
{ type: "cartao", label: "Vencimento de cartão" },
|
{ type: "cartao", label: "Vencimento de cartão" },
|
||||||
|
{ label: "Pagamento fatura", dotColor: "bg-green-600" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CalendarLegend() {
|
export function CalendarLegend() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
|
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
|
||||||
{LEGEND_ITEMS.map((item) => {
|
{LEGEND_ITEMS.map((item, index) => {
|
||||||
const style = EVENT_TYPE_STYLES[item.type];
|
const dotColor =
|
||||||
|
item.dotColor || (item.type ? EVENT_TYPE_STYLES[item.type].dot : "");
|
||||||
return (
|
return (
|
||||||
<span key={item.type} className="flex items-center gap-2">
|
<span key={item.type || index} className="flex items-center gap-2">
|
||||||
<span className={cn("size-3 rounded-full", style.dot)} />
|
<span className={cn("size-3 rounded-full", dotColor)} />
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ export const EVENT_TYPE_STYLES: Record<
|
|||||||
> = {
|
> = {
|
||||||
lancamento: {
|
lancamento: {
|
||||||
wrapper:
|
wrapper:
|
||||||
"bg-orange-100 text-orange-600 dark:bg-orange-800 dark:text-orange-50",
|
"bg-orange-100 text-orange-600 dark:bg-orange-900/10 dark:text-orange-50 border-l-4 border-orange-500",
|
||||||
dot: "bg-orange-600",
|
dot: "bg-orange-600",
|
||||||
},
|
},
|
||||||
boleto: {
|
boleto: {
|
||||||
wrapper:
|
wrapper:
|
||||||
"bg-emerald-100 text-emerald-600 dark:bg-emerald-800 dark:text-emerald-50",
|
"bg-blue-100 text-blue-600 dark:bg-blue-900/10 dark:text-blue-50 border-l-4 border-blue-500",
|
||||||
dot: "bg-emerald-600",
|
dot: "bg-blue-600",
|
||||||
},
|
},
|
||||||
cartao: {
|
cartao: {
|
||||||
wrapper: "bg-blue-100 text-blue-600 dark:bg-blue-800 dark:text-blue-50",
|
wrapper:
|
||||||
dot: "bg-blue-600",
|
"bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-50 border-l-4 border-violet-500",
|
||||||
|
dot: "bg-violet-600",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,10 +76,28 @@ const buildEventComplement = (event: CalendarEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPagamentoFatura = (event: CalendarEvent) => {
|
||||||
|
return (
|
||||||
|
event.type === "lancamento" &&
|
||||||
|
event.lancamento.name.startsWith("Pagamento fatura -")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventStyle = (event: CalendarEvent) => {
|
||||||
|
if (isPagamentoFatura(event)) {
|
||||||
|
return {
|
||||||
|
wrapper:
|
||||||
|
"bg-green-100 text-green-600 dark:bg-green-900/10 dark:text-green-50 border-l-4 border-green-500",
|
||||||
|
dot: "bg-green-600",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return eventStyles[event.type];
|
||||||
|
};
|
||||||
|
|
||||||
const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
||||||
const complement = buildEventComplement(event);
|
const complement = buildEventComplement(event);
|
||||||
const label = buildEventLabel(event);
|
const label = buildEventLabel(event);
|
||||||
const style = eventStyles[event.type];
|
const style = getEventStyle(event);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -88,7 +107,6 @@ const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-1">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<span className={cn("size-1.5 rounded-full", style.dot)} />
|
|
||||||
<span className="truncate">{label}</span>
|
<span className="truncate">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
{complement ? (
|
{complement ? (
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
|
||||||
|
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -9,12 +11,12 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { friendlyDate, parseLocalDateString } from "@/lib/utils/date";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import { useMemo, type ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
import MoneyValues from "../money-values";
|
||||||
import { parseDateKey } from "@/components/calendario/utils";
|
import { Badge } from "../ui/badge";
|
||||||
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
|
import { Card } from "../ui/card";
|
||||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
|
||||||
|
|
||||||
type EventModalProps = {
|
type EventModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -23,36 +25,26 @@ type EventModalProps = {
|
|||||||
onCreate: (date: string) => void;
|
onCreate: (date: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fullDateFormatter = new Intl.DateTimeFormat("pt-BR", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
const capitalize = (value: string) =>
|
|
||||||
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
|
||||||
|
|
||||||
const formatCurrency = (value: number, isReceita: boolean) => {
|
|
||||||
const formatted = currencyFormatter.format(value ?? 0);
|
|
||||||
return isReceita ? `+${formatted}` : formatted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventCard = ({
|
const EventCard = ({
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
|
isPagamentoFatura = false,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
type: CalendarEvent["type"];
|
type: CalendarEvent["type"];
|
||||||
|
isPagamentoFatura?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const style = EVENT_TYPE_STYLES[type];
|
const style = isPagamentoFatura
|
||||||
|
? { dot: "bg-green-600" }
|
||||||
|
: EVENT_TYPE_STYLES[type];
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 rounded-xl border border-border/60 bg-card/85 p-4">
|
<Card className="flex flex-row gap-2 p-3 mb-1">
|
||||||
<span
|
<span
|
||||||
className={cn("mt-1 size-2.5 shrink-0 rounded-full", style.dot)}
|
className={cn("mt-1 size-3 shrink-0 rounded-full", style.dot)}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-2">{children}</div>
|
<div className="flex flex-1 flex-col">{children}</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,41 +52,38 @@ const renderLancamento = (
|
|||||||
event: Extract<CalendarEvent, { type: "lancamento" }>
|
event: Extract<CalendarEvent, { type: "lancamento" }>
|
||||||
) => {
|
) => {
|
||||||
const isReceita = event.lancamento.transactionType === "Receita";
|
const isReceita = event.lancamento.transactionType === "Receita";
|
||||||
const subtitleParts = [
|
const isPagamentoFatura =
|
||||||
event.lancamento.categoriaName,
|
event.lancamento.name.startsWith("Pagamento fatura -");
|
||||||
event.lancamento.paymentMethod,
|
|
||||||
event.lancamento.pagadorName,
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventCard type="lancamento">
|
<EventCard type="lancamento" isPagamentoFatura={isPagamentoFatura}>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-semibold">{event.lancamento.name}</span>
|
<span
|
||||||
{subtitleParts.length ? (
|
className={`text-sm font-semibold leading-tight ${
|
||||||
<span className="text-xs text-muted-foreground">
|
isPagamentoFatura && "text-green-600 dark:text-green-400"
|
||||||
{subtitleParts.join(" • ")}
|
}`}
|
||||||
|
>
|
||||||
|
{event.lancamento.name}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Badge variant={"outline"}>{event.lancamento.condition}</Badge>
|
||||||
|
<Badge variant={"outline"}>{event.lancamento.paymentMethod}</Badge>
|
||||||
|
<Badge variant={"outline"}>{event.lancamento.categoriaName}</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-semibold",
|
"text-sm font-semibold whitespace-nowrap",
|
||||||
isReceita ? "text-emerald-600" : "text-foreground"
|
isReceita ? "text-green-600 dark:text-green-400" : "text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatCurrency(event.lancamento.amount, isReceita)}
|
<MoneyValues
|
||||||
</span>
|
showPositiveSign
|
||||||
</div>
|
className="text-base"
|
||||||
<div className="flex flex-wrap gap-2 text-[11px] font-medium text-muted-foreground">
|
amount={event.lancamento.amount}
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5">
|
/>
|
||||||
{capitalize(event.lancamento.transactionType)}
|
|
||||||
</span>
|
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5">
|
|
||||||
{event.lancamento.condition}
|
|
||||||
</span>
|
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5">
|
|
||||||
{event.lancamento.paymentMethod}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</EventCard>
|
</EventCard>
|
||||||
@@ -111,26 +100,25 @@ const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
|
|||||||
return (
|
return (
|
||||||
<EventCard type="boleto">
|
<EventCard type="boleto">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-semibold">{event.lancamento.name}</span>
|
<div className="flex gap-1 items-center">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-sm font-semibold leading-tight">
|
||||||
Boleto{formattedDueDate ? ` - Vence em ${formattedDueDate}` : ""}
|
{event.lancamento.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<span className="text-sm font-semibold text-foreground">
|
{formattedDueDate && (
|
||||||
{currencyFormatter.format(event.lancamento.amount ?? 0)}
|
<span className="text-xs text-muted-foreground leading-tight">
|
||||||
|
Vence em {formattedDueDate}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-flex w-fit items-center rounded-full px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
|
|
||||||
isPaid
|
|
||||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200"
|
|
||||||
: "bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200"
|
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
{isPaid ? "Pago" : "Pendente"}
|
|
||||||
|
<Badge variant={"outline"}>{isPaid ? "Pago" : "Pendente"}</Badge>
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold">
|
||||||
|
<MoneyValues amount={event.lancamento.amount} />
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</EventCard>
|
</EventCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -138,25 +126,18 @@ const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
|
|||||||
const renderCard = (event: Extract<CalendarEvent, { type: "cartao" }>) => (
|
const renderCard = (event: Extract<CalendarEvent, { type: "cartao" }>) => (
|
||||||
<EventCard type="cartao">
|
<EventCard type="cartao">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-semibold">Cartão {event.card.name}</span>
|
<div className="flex gap-1 items-center">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-sm font-semibold leading-tight">
|
||||||
Vencimento dia {event.card.dueDay}
|
Vencimento Fatura - {event.card.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Badge variant={"outline"}>{event.card.status ?? "Fatura"}</Badge>
|
||||||
|
</div>
|
||||||
{event.card.totalDue !== null ? (
|
{event.card.totalDue !== null ? (
|
||||||
<span className="text-sm font-semibold text-foreground">
|
<span className="font-semibold">
|
||||||
{currencyFormatter.format(event.card.totalDue)}
|
<MoneyValues amount={event.card.totalDue} />
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 text-[11px] font-medium text-muted-foreground">
|
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5">
|
|
||||||
Status: {event.card.status ?? "Indefinido"}
|
|
||||||
</span>
|
|
||||||
{event.card.closingDay ? (
|
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5">
|
|
||||||
Fechamento dia {event.card.closingDay}
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -177,11 +158,9 @@ const renderEvent = (event: CalendarEvent) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
|
export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
|
||||||
const formattedDate = useMemo(() => {
|
const formattedDate = !day
|
||||||
if (!day) return "";
|
? ""
|
||||||
const parsed = parseDateKey(day.date);
|
: friendlyDate(parseLocalDateString(day.date));
|
||||||
return capitalize(fullDateFormatter.format(parsed));
|
|
||||||
}, [day]);
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (!day) return;
|
if (!day) return;
|
||||||
@@ -201,7 +180,7 @@ export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
|
|||||||
<DialogDescription>{description}</DialogDescription>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="max-h-[380px] space-y-3 overflow-y-auto pr-2">
|
<div className="max-h-[380px] space-y-2 overflow-y-auto pr-2">
|
||||||
{day?.events.length ? (
|
{day?.events.length ? (
|
||||||
day.events.map((event) => (
|
day.events.map((event) => (
|
||||||
<div key={event.id}>{renderEvent(event)}</div>
|
<div key={event.id}>{renderEvent(event)}</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type DotIconProps = {
|
|||||||
export default function DotIcon({ bg_dot }: DotIconProps) {
|
export default function DotIcon({ bg_dot }: DotIconProps) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className={`${bg_dot} flex h-2 w-2 rounded-full`}></span>
|
<span className={`${bg_dot} flex size-2 rounded-full`}></span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChangelogNotification } from "@/components/changelog/changelog-notification";
|
import { ChangelogNotification } from "@/components/changelog/changelog-notification";
|
||||||
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
|
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
|
||||||
import { NotificationBell } from "@/components/notifications/notification-bell";
|
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
import { getUnreadUpdates } from "@/lib/changelog/data";
|
import { getUnreadUpdates } from "@/lib/changelog/data";
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ export function InsightsPage({ period, onAnalyze }: InsightsPageProps) {
|
|||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Privacy Warning */}
|
{/* Privacy Warning */}
|
||||||
<Alert className="border-none">
|
<Alert className="border-none">
|
||||||
<RiAlertLine className="size-4" />
|
<RiAlertLine className="size-4" color="red" />
|
||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm text-card-foreground">
|
||||||
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados
|
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados
|
||||||
financeiros serão enviados para o provedor de IA selecionado
|
financeiros serão enviados para o provedor de IA selecionado
|
||||||
(Anthropic, OpenAI, Google ou OpenRouter) para processamento.
|
(Anthropic, OpenAI, Google ou OpenRouter) para processamento.
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ const buildColumns = ({
|
|||||||
return (
|
return (
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={row.original.amount}
|
amount={row.original.amount}
|
||||||
|
showPositiveSign={isReceita}
|
||||||
className={cn(
|
className={cn(
|
||||||
"whitespace-nowrap",
|
"whitespace-nowrap",
|
||||||
isReceita
|
isReceita
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { usePrivacyMode } from "./privacy-provider";
|
|||||||
type Props = {
|
type Props = {
|
||||||
amount: number;
|
amount: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showPositiveSign?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function MoneyValues({ amount, className }: Props) {
|
function MoneyValues({ amount, className, showPositiveSign = false }: Props) {
|
||||||
const { privacyMode } = usePrivacyMode();
|
const { privacyMode } = usePrivacyMode();
|
||||||
|
|
||||||
const formattedValue = amount.toLocaleString("pt-BR", {
|
const formattedValue = amount.toLocaleString("pt-BR", {
|
||||||
@@ -18,6 +19,10 @@ function MoneyValues({ amount, className }: Props) {
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayValue = showPositiveSign && amount > 0
|
||||||
|
? `+${formattedValue}`
|
||||||
|
: formattedValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -27,13 +32,13 @@ function MoneyValues({ amount, className }: Props) {
|
|||||||
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
aria-label={privacyMode ? "Valor oculto" : formattedValue}
|
aria-label={privacyMode ? "Valor oculto" : displayValue}
|
||||||
data-privacy={privacyMode ? "hidden" : undefined}
|
data-privacy={privacyMode ? "hidden" : undefined}
|
||||||
title={
|
title={
|
||||||
privacyMode ? "Valor oculto - passe o mouse para revelar" : undefined
|
privacyMode ? "Valor oculto - passe o mouse para revelar" : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{formattedValue}
|
{displayValue}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
RiArchiveLine,
|
||||||
RiArrowLeftRightLine,
|
RiArrowLeftRightLine,
|
||||||
RiBankCardLine,
|
RiBankCardLine,
|
||||||
RiBankLine,
|
RiBankLine,
|
||||||
@@ -142,6 +143,14 @@ export function createSidebarNavData(pagadores: PagadorLike[]): SidebarNavData {
|
|||||||
title: "Anotações",
|
title: "Anotações",
|
||||||
url: "/anotacoes",
|
url: "/anotacoes",
|
||||||
icon: RiTodoLine,
|
icon: RiTodoLine,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Arquivadas",
|
||||||
|
url: "/anotacoes/arquivadas",
|
||||||
|
key: "anotacoes-arquivadas",
|
||||||
|
icon: RiArchiveLine,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Insights",
|
title: "Insights",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 border-transparent border drop-shadow-xs py-6 rounded-md hover:border-primary/50 transition-colors",
|
"bg-card text-card-foreground flex flex-col gap-6 border-transparent border drop-shadow-xs py-6 rounded-md hover:border-primary/50 transition-all ease-in-out duration-300",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ export const anotacoes = pgTable("anotacoes", {
|
|||||||
description: text("descricao"),
|
description: text("descricao"),
|
||||||
type: text("tipo").notNull().default("nota"), // "nota" ou "tarefa"
|
type: text("tipo").notNull().default("nota"), // "nota" ou "tarefa"
|
||||||
tasks: text("tasks"), // JSON stringificado com array de tarefas
|
tasks: text("tasks"), // JSON stringificado com array de tarefas
|
||||||
|
arquivada: boolean("arquivada").notNull().default(false),
|
||||||
createdAt: timestamp("created_at", {
|
createdAt: timestamp("created_at", {
|
||||||
mode: "date",
|
mode: "date",
|
||||||
withTimezone: true,
|
withTimezone: true,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const revalidateConfig = {
|
|||||||
categorias: ["/categorias"],
|
categorias: ["/categorias"],
|
||||||
orcamentos: ["/orcamentos"],
|
orcamentos: ["/orcamentos"],
|
||||||
pagadores: ["/pagadores"],
|
pagadores: ["/pagadores"],
|
||||||
anotacoes: ["/anotacoes"],
|
anotacoes: ["/anotacoes", "/anotacoes/arquivadas"],
|
||||||
lancamentos: ["/lancamentos", "/contas"],
|
lancamentos: ["/lancamentos", "/contas"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -28,10 +28,10 @@
|
|||||||
"docker:rebuild": "docker compose up --build --force-recreate"
|
"docker:rebuild": "docker compose up --build --force-recreate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.56",
|
"@ai-sdk/anthropic": "^3.0.1",
|
||||||
"@ai-sdk/google": "^2.0.46",
|
"@ai-sdk/google": "^3.0.1",
|
||||||
"@ai-sdk/openai": "^2.0.86",
|
"@ai-sdk/openai": "^3.0.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.5.3",
|
"@openrouter/ai-sdk-provider": "^1.5.4",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
@@ -56,41 +56,41 @@
|
|||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
"@vercel/speed-insights": "^1.3.1",
|
"@vercel/speed-insights": "^1.3.1",
|
||||||
"ai": "^5.0.113",
|
"ai": "^6.0.3",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"better-auth": "1.4.7",
|
"better-auth": "1.4.9",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.26",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.12.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"recharts": "3.6.0",
|
"recharts": "3.6.0",
|
||||||
"resend": "^6.6.0",
|
"resend": "^6.6.0",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"zod": "4.2.0"
|
"zod": "4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
"@types/d3-array": "^3.2.2",
|
"@types/d3-array": "^3.2.2",
|
||||||
"@types/node": "25.0.2",
|
"@types/node": "25.0.3",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"baseline-browser-mapping": "^2.9.7",
|
"baseline-browser-mapping": "^2.9.11",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "0.31.8",
|
"drizzle-kit": "0.31.8",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
"eslint-config-next": "16.0.10",
|
"eslint-config-next": "16.1.1",
|
||||||
"tailwindcss": "4.1.18",
|
"tailwindcss": "4.1.18",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
|
|||||||
1333
pnpm-lock.yaml
generated
1333
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user