Remove unused font file and update font index; initialize database extensions with improved error handling; add EstabelecimentoLogo component for dynamic logo generation.

This commit is contained in:
Felipe Coutinho
2025-12-01 16:35:12 +00:00
parent c91edd0f31
commit 9cf89829f6
40 changed files with 6570 additions and 1114 deletions

View File

@@ -3,7 +3,7 @@
@custom-variant dark (&:is(.dark *));
@theme {
--spacing-custom-height-1: 28rem;
--spacing-custom-height-1: 29rem;
}
:root {

View File

@@ -2,10 +2,13 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { RiDeleteBin5Line, RiEyeLine, RiPencilLine } from "@remixicon/react";
import { CheckIcon } from "lucide-react";
import {
RiCheckLine,
RiDeleteBin5Line,
RiEyeLine,
RiPencilLine,
} from "@remixicon/react";
import { useMemo } from "react";
import type { Note } from "./types";
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
@@ -88,7 +91,7 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
}`}
>
{task.completed && (
<CheckIcon className="h-3 w-3 text-background" />
<RiCheckLine className="h-3 w-3 text-background" />
)}
</div>
<span

View File

@@ -11,9 +11,8 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { CheckIcon } from "lucide-react";
import { RiCheckLine } from "@remixicon/react";
import { useMemo } from "react";
import type { Note } from "./types";
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
@@ -84,7 +83,7 @@ export function NoteDetailsDialog({
}`}
>
{task.completed && (
<CheckIcon className="h-4 w-4 text-primary-foreground" />
<RiCheckLine className="h-4 w-4 text-primary-foreground" />
)}
</div>
<span

View File

@@ -20,7 +20,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { useControlledState } from "@/hooks/use-controlled-state";
import { useFormState } from "@/hooks/use-form-state";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import {
type ReactNode,
useCallback,
@@ -388,7 +388,7 @@ export function NoteDialog({
disabled={isPending || !normalize(newTaskText)}
className="shrink-0"
>
<PlusIcon className="h-4 w-4" />
<RiAddLine className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
@@ -434,7 +434,7 @@ export function NoteDialog({
className="h-8 w-8 p-0 shrink-0 text-muted-foreground hover:text-destructive"
aria-label={`Remover tarefa "${task.text}"`}
>
<Trash2Icon className="h-4 w-4" />
<RiDeleteBinLine className="h-4 w-4" />
</Button>
</div>
))}

View File

@@ -1,6 +1,7 @@
"use client";
import { toggleLancamentoSettlementAction } from "@/app/(dashboard)/lancamentos/actions";
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
@@ -171,9 +172,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0"
>
<div className="flex min-w-0 flex-1 items-center gap-3 py-2">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted">
<RiBarcodeFill className="size-5" />
</div>
<EstabelecimentoLogo name={boleto.name} size={38} />
<div className="min-w-0">
<span className="block truncate text-sm font-medium text-foreground">

View File

@@ -22,8 +22,11 @@ import {
import { WidgetEmptyState } from "@/components/widget-empty-state";
import type { CategoryHistoryData } from "@/lib/dashboard/categories/category-history";
import { getIconComponent } from "@/lib/utils/icons";
import { RiBarChartBoxLine, RiCloseLine } from "@remixicon/react";
import { ChevronDownIcon } from "lucide-react";
import {
RiArrowDownSLine,
RiBarChartBoxLine,
RiCloseLine,
} from "@remixicon/react";
import { useEffect, useMemo, useState } from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
@@ -265,7 +268,7 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
className="w-full justify-between hover:scale-none"
>
Selecionar categorias
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<RiArrowDownSLine className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent

View File

@@ -134,7 +134,7 @@ export function InstallmentExpensesWidget({
return (
<li
key={expense.id}
className="flex items-center gap-3 border-b border-dashed pb-2 last:border-b-0 last:pb-0"
className="flex items-center gap-3 border-b border-dashed pb-3 last:border-b-0 last:pb-0"
>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
@@ -169,17 +169,17 @@ export function InstallmentExpensesWidget({
</div>
<MoneyValues amount={expense.amount} className="shrink-0" />
</div>
<Progress value={progress} className="h-2" />
<p className="text-xs text-muted-foreground mt-1">
Restantes {remainingInstallments}
{endDate && ` - Termina em ${endDate}`}
{" - Restante "}
<p className="text-xs text-muted-foreground ">
{endDate && `Termina em ${endDate}`}
{` - Restante (${remainingInstallments}) `}
<MoneyValues
amount={remainingAmount}
className="inline-block font-medium"
/>
</p>
<Progress value={progress} className="h-2 mt-1" />
</div>
</li>
);

View File

@@ -369,7 +369,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
</div>
<div className="flex shrink-0 flex-col items-end">
<MoneyValues amount={invoice.totalAmount} />
<MoneyValues amount={Math.abs(invoice.totalAmount)} />
<div className="flex items-center gap-2">
<Button
type="button"
@@ -504,7 +504,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
Valor da fatura
</span>
<MoneyValues
amount={selectedInvoice.totalAmount}
amount={Math.abs(selectedInvoice.totalAmount)}
className="text-lg"
/>
</div>

View File

@@ -1,5 +1,6 @@
"use client";
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import {
Select,
@@ -11,7 +12,6 @@ import {
import { CATEGORY_TYPE_LABEL } from "@/lib/categorias/constants";
import type { PurchasesByCategoryData } from "@/lib/dashboard/purchases-by-category";
import { RiArrowDownLine, RiStore3Line } from "@remixicon/react";
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
import { WidgetEmptyState } from "../widget-empty-state";
@@ -19,30 +19,6 @@ type PurchasesByCategoryWidgetProps = {
data: PurchasesByCategoryData;
};
const resolveLogoPath = (logo: string | null) => {
if (!logo) {
return null;
}
if (/^(https?:\/\/|data:)/.test(logo)) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
const buildInitials = (value: string) => {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "LC";
}
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "LC";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "LC";
};
const formatTransactionDate = (date: Date) => {
const formatter = new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
@@ -180,30 +156,13 @@ export function PurchasesByCategoryWidget({
) : (
<ul className="flex flex-col">
{currentTransactions.map((transaction) => {
const logo = resolveLogoPath(transaction.logo);
const initials = buildInitials(transaction.name);
return (
<li
key={transaction.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
{logo ? (
<Image
src={logo}
alt={`Logo de ${transaction.name}`}
width={48}
height={48}
className="h-full w-full object-contain"
/>
) : (
<span className="text-sm font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<EstabelecimentoLogo name={transaction.name} size={38} />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">

View File

@@ -1,37 +1,13 @@
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import type { RecentTransactionsData } from "@/lib/dashboard/recent-transactions";
import { RiExchangeLine } from "@remixicon/react";
import Image from "next/image";
import { WidgetEmptyState } from "../widget-empty-state";
type RecentTransactionsWidgetProps = {
data: RecentTransactionsData;
};
const resolveLogoPath = (logo: string | null) => {
if (!logo) {
return null;
}
if (/^(https?:\/\/|data:)/.test(logo)) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
const buildInitials = (value: string) => {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "LC";
}
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "LC";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "LC";
};
const formatTransactionDate = (date: Date) => {
const formatter = new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
@@ -59,32 +35,13 @@ export function RecentTransactionsWidget({
) : (
<ul className="flex flex-col">
{data.transactions.map((transaction) => {
const logo = resolveLogoPath(
transaction.cardLogo ?? transaction.accountLogo
);
const initials = buildInitials(transaction.name);
return (
<li
key={transaction.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg">
{logo ? (
<Image
src={logo}
alt={`Logo de ${transaction.name}`}
width={48}
height={48}
className="h-full w-full object-contain"
/>
) : (
<span className="text-sm font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<EstabelecimentoLogo name={transaction.name} size={38} />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">

View File

@@ -1,3 +1,4 @@
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import { CardContent } from "@/components/ui/card";
import type { RecurringExpensesData } from "@/lib/dashboard/expenses/recurring-expenses";
@@ -38,9 +39,7 @@ export function RecurringExpensesWidget({
key={expense.id}
className="flex items-start gap-3 border-b border-dashed pb-2 last:border-b-0 last:pb-0"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted">
<RiRefreshLine className="size-5 text-foreground" />
</div>
<EstabelecimentoLogo name={expense.name} size={38} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">

View File

@@ -1,37 +1,13 @@
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import type { TopEstablishmentsData } from "@/lib/dashboard/top-establishments";
import { RiStore2Line } from "@remixicon/react";
import Image from "next/image";
import { WidgetEmptyState } from "../widget-empty-state";
type TopEstablishmentsWidgetProps = {
data: TopEstablishmentsData;
};
const resolveLogoPath = (logo: string | null) => {
if (!logo) {
return null;
}
if (/^(https?:\/\/|data:)/.test(logo)) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
const buildInitials = (value: string) => {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "LC";
}
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "LC";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "LC";
};
const formatOccurrencesLabel = (occurrences: number) => {
if (occurrences === 1) {
return "1 lançamento";
@@ -53,30 +29,13 @@ export function TopEstablishmentsWidget({
) : (
<ul className="flex flex-col">
{data.establishments.map((establishment) => {
const logo = resolveLogoPath(establishment.logo);
const initials = buildInitials(establishment.name);
return (
<li
key={establishment.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
{logo ? (
<Image
src={logo}
alt={`Logo de ${establishment.name}`}
width={48}
height={48}
className="h-full w-full object-contain"
/>
) : (
<span className="text-sm font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<EstabelecimentoLogo name={establishment.name} size={38} />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">

View File

@@ -1,10 +1,13 @@
"use client";
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import { Switch } from "@/components/ui/switch";
import type { TopExpense, TopExpensesData } from "@/lib/dashboard/expenses/top-expenses";
import type {
TopExpense,
TopExpensesData,
} from "@/lib/dashboard/expenses/top-expenses";
import { RiArrowUpDoubleLine } from "@remixicon/react";
import Image from "next/image";
import { useMemo, useState } from "react";
import { WidgetEmptyState } from "../widget-empty-state";
@@ -13,30 +16,6 @@ type TopExpensesWidgetProps = {
cardOnlyExpenses: TopExpensesData;
};
const resolveLogoPath = (logo: string | null) => {
if (!logo) {
return null;
}
if (/^(https?:\/\/|data:)/.test(logo)) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
const buildInitials = (value: string) => {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "LC";
}
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "LC";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "LC";
};
const formatTransactionDate = (date: Date) => {
const formatter = new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
@@ -129,30 +108,13 @@ export function TopExpensesWidget({
) : (
<ul className="flex flex-col">
{data.expenses.map((expense) => {
const logo = resolveLogoPath(expense.logo);
const initials = buildInitials(expense.name);
return (
<li
key={expense.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
{logo ? (
<Image
src={logo}
alt={`Logo de ${expense.name}`}
width={48}
height={48}
className="h-full w-full object-contain"
/>
) : (
<span className="text-sm font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<EstabelecimentoLogo name={expense.name} size={38} />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">

View File

@@ -306,10 +306,17 @@ export function LancamentoDialog({
]
);
const title = mode === "create" ? "Novo lançamento" : "Editar lançamento";
const isCopyMode = mode === "create" && Boolean(lancamento);
const title = mode === "create"
? isCopyMode
? "Copiar lançamento"
: "Novo lançamento"
: "Editar lançamento";
const description =
mode === "create"
? "Informe os dados abaixo para registrar um novo lançamento."
? isCopyMode
? "Os dados do lançamento foram copiados. Revise e ajuste conforme necessário antes de salvar."
: "Informe os dados abaixo para registrar um novo lançamento."
: "Atualize as informações do lançamento selecionado.";
const submitLabel = mode === "create" ? "Salvar lançamento" : "Atualizar";

View File

@@ -69,6 +69,9 @@ export function LancamentosPage({
useState<LancamentoItem | null>(null);
const [editOpen, setEditOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [copyOpen, setCopyOpen] = useState(false);
const [lancamentoToCopy, setLancamentoToCopy] =
useState<LancamentoItem | null>(null);
const [massAddOpen, setMassAddOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [lancamentoToDelete, setLancamentoToDelete] =
@@ -288,6 +291,11 @@ export function LancamentosPage({
setEditOpen(true);
}, []);
const handleCopy = useCallback((item: LancamentoItem) => {
setLancamentoToCopy(item);
setCopyOpen(true);
}, []);
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
if (item.seriesId) {
setPendingDeleteData(item);
@@ -323,6 +331,7 @@ export function LancamentosPage({
onCreate={allowCreate ? handleCreate : undefined}
onMassAdd={allowCreate ? handleMassAdd : undefined}
onEdit={handleEdit}
onCopy={handleCopy}
onConfirmDelete={handleConfirmDelete}
onBulkDelete={handleMultipleBulkDelete}
onViewDetails={handleViewDetails}
@@ -352,6 +361,26 @@ export function LancamentosPage({
/>
) : null}
<LancamentoDialog
mode="create"
open={copyOpen && !!lancamentoToCopy}
onOpenChange={(open) => {
setCopyOpen(open);
if (!open) {
setLancamentoToCopy(null);
}
}}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
estabelecimentos={estabelecimentos}
lancamento={lancamentoToCopy ?? undefined}
defaultPeriod={selectedPeriod}
/>
<LancamentoDialog
mode="update"
open={editOpen && !!selectedLancamento}

View File

@@ -0,0 +1,72 @@
"use client";
import { cn } from "@/lib/utils/ui";
interface EstabelecimentoLogoProps {
name: string;
size?: number;
className?: string;
}
const COLOR_PALETTE = [
"bg-purple-400 dark:bg-purple-600",
"bg-pink-400 dark:bg-pink-600",
"bg-red-400 dark:bg-red-600",
"bg-orange-400 dark:bg-orange-600",
"bg-indigo-400 dark:bg-indigo-600",
"bg-violet-400 dark:bg-violet-600",
"bg-fuchsia-400 dark:bg-fuchsia-600",
"bg-rose-400 dark:bg-rose-600",
"bg-amber-400 dark:bg-amber-600",
"bg-emerald-400 dark:bg-emerald-600",
];
function getInitials(name: string): string {
if (!name || !name.trim()) return "?";
const words = name.trim().split(/\s+/);
if (words.length === 1) {
return words[0]?.[0]?.toUpperCase() || "?";
}
const firstInitial = words[0]?.[0]?.toUpperCase() || "";
const secondInitial = words[1]?.[0]?.toUpperCase() || "";
return `${firstInitial}${secondInitial}`;
}
function generateColorFromName(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % COLOR_PALETTE.length;
return COLOR_PALETTE[index] || "bg-gray-400";
}
export function EstabelecimentoLogo({
name,
size = 32,
className,
}: EstabelecimentoLogoProps) {
const initials = getInitials(name);
const colorClass = generateColorFromName(name);
return (
<div
className={cn(
"flex items-center justify-center rounded-md text-white font-medium shrink-0 ",
colorClass,
className
)}
style={{
width: size,
height: size,
fontSize: size * 0.4,
}}
>
{initials}
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { AnticipationCard } from "./anticipation-card";
export { EstabelecimentoInput } from "./estabelecimento-input";
export { InstallmentTimeline } from "./installment-timeline";
export { EstabelecimentoLogo } from "./estabelecimento-logo";

View File

@@ -1,15 +1,5 @@
"use client";
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
type ReactNode,
} from "react";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,15 +29,25 @@ import {
LANCAMENTO_TRANSACTION_TYPES,
} from "@/lib/lancamentos/constants";
import { cn } from "@/lib/utils/ui";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
type ReactNode,
} from "react";
import {
TransactionTypeSelectContent,
ConditionSelectContent,
PaymentMethodSelectContent,
CategoriaSelectContent,
PagadorSelectContent,
ConditionSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
PaymentMethodSelectContent,
TransactionTypeSelectContent,
} from "../select-items";
import { RiCheckLine, RiExpandUpDownLine } from "@remixicon/react";
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
const FILTER_EMPTY_VALUE = "__all";
@@ -337,7 +337,7 @@ export function LancamentosFilters({
"Categoria"
)}
</span>
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[220px] p-0">
@@ -355,7 +355,7 @@ export function LancamentosFilters({
>
Todas
{categoriaValue === FILTER_EMPTY_VALUE ? (
<CheckIcon className="ml-auto size-4" />
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
{categoriaOptions.map((option) => (
@@ -372,7 +372,7 @@ export function LancamentosFilters({
icon={option.icon}
/>
{categoriaValue === option.slug ? (
<CheckIcon className="ml-auto size-4" />
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
))}

View File

@@ -51,6 +51,7 @@ import {
RiCheckLine,
RiDeleteBin5Line,
RiEyeLine,
RiFileCopyLine,
RiGroupLine,
RiHistoryLine,
RiMoreFill,
@@ -72,6 +73,7 @@ import {
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { EstabelecimentoLogo } from "../shared/estabelecimento-logo";
import type {
ContaCartaoFilterOption,
LancamentoFilterOption,
@@ -90,6 +92,7 @@ const resolveLogoSrc = (logo: string | null) => {
type BuildColumnsArgs = {
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
@@ -101,6 +104,7 @@ type BuildColumnsArgs = {
const buildColumns = ({
onEdit,
onCopy,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -111,6 +115,7 @@ const buildColumns = ({
}: BuildColumnsArgs): ColumnDef<LancamentoItem>[] => {
const noop = () => undefined;
const handleEdit = onEdit ?? noop;
const handleCopy = onCopy ?? noop;
const handleConfirmDelete = onConfirmDelete ?? noop;
const handleViewDetails = onViewDetails ?? noop;
const handleToggleSettlement = onToggleSettlement ?? noop;
@@ -180,6 +185,7 @@ const buildColumns = ({
return (
<span className="flex items-center gap-2">
<EstabelecimentoLogo name={name} size={28} />
<Tooltip>
<TooltipTrigger asChild>
<span className="line-clamp-2 max-w-[180px] font-bold truncate">
@@ -522,6 +528,12 @@ const buildColumns = ({
<RiPencilLine className="size-4" />
Editar
</DropdownMenuItem>
{row.original.categoriaName !== "Pagamentos" && (
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
<RiFileCopyLine className="size-4" />
Copiar
</DropdownMenuItem>
)}
<DropdownMenuItem
variant="destructive"
onSelect={() => handleConfirmDelete(row.original)}
@@ -583,6 +595,7 @@ type LancamentosTableProps = {
onCreate?: () => void;
onMassAdd?: () => void;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onBulkDelete?: (items: LancamentoItem[]) => void;
onViewDetails?: (item: LancamentoItem) => void;
@@ -602,6 +615,7 @@ export function LancamentosTable({
onCreate,
onMassAdd,
onEdit,
onCopy,
onConfirmDelete,
onBulkDelete,
onViewDetails,
@@ -625,6 +639,7 @@ export function LancamentosTable({
() =>
buildColumns({
onEdit,
onCopy,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -635,6 +650,7 @@ export function LancamentosTable({
}),
[
onEdit,
onCopy,
onConfirmDelete,
onViewDetails,
onToggleSettlement,

View File

@@ -16,6 +16,10 @@ import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { getAvatarSrc } from "@/lib/pagadores/utils";
import { cn } from "@/lib/utils/ui";
import {
RiBankCard2Line,
RiBillLine,
RiExchangeDollarLine,
RiFileList3Line,
RiMailLine,
RiMailSendLine,
RiUser3Line,
@@ -272,74 +276,149 @@ export function PagadorInfoCard({
setConfirmOpen(open);
}}
>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Confirmar envio do resumo</DialogTitle>
<DialogDescription>
O resumo de{" "}
Resumo de{" "}
<span className="font-semibold text-foreground">
{summary.periodLabel}
</span>{" "}
será enviado para{" "}
para{" "}
<span className="font-medium text-foreground">
{pagador.email ?? "—"}
{pagador.email}
</span>
</DialogDescription>
</DialogHeader>
<div className="space-y-3 rounded-lg border border-dashed border-border/70 bg-muted/30 p-4 text-sm text-muted-foreground">
<div className="space-y-4">
{/* Total Geral */}
<div className="rounded-lg border bg-muted/30 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
<RiExchangeDollarLine className="size-5 text-primary" />
</div>
<div>
<span className="text-xs font-semibold uppercase text-muted-foreground/70">
Totais do s
</span>
<p className="text-foreground">
{formatCurrency(summary.totalExpenses)} em despesas
registradas
<p className="text-sm font-medium text-muted-foreground">
Total de Despesas
</p>
<p className="text-xs">
Cartões: {formatCurrency(summary.paymentSplits.card)} -
Boletos: {formatCurrency(summary.paymentSplits.boleto)} -
Pix/Débito/Dinheiro:{" "}
<p className="text-2xl font-bold text-foreground">
{formatCurrency(summary.totalExpenses)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">
{summary.lancamentoCount} lançamentos
</p>
</div>
</div>
</div>
{/* Grid de Formas de Pagamento */}
<div className="grid gap-3 sm:grid-cols-3">
{/* Cartões */}
<div className="rounded-lg border bg-background p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiBankCard2Line className="size-4" />
<span className="text-xs font-semibold uppercase">
Cartões
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.card)}
</p>
</div>
{/* Boletos */}
<div className="rounded-lg border bg-background p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiBillLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Boletos
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.boleto)}
</p>
</div>
{/* Instantâneo */}
<div className="rounded-lg border bg-background p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiExchangeDollarLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Pix/Débito
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.instant)}
</p>
</div>
<div>
<span className="text-xs font-semibold uppercase text-muted-foreground/70">
Principais cartões
</span>
<p>
{summary.cardUsage.length
? summary.cardUsage
.map(
(item) =>
`${item.name}: ${formatCurrency(item.amount)}`
)
.join(" - ")
: "Sem lançamentos com cartão no período."}
</p>
</div>
<div className="rounded-lg border border-border/60 p-3">
{/* Detalhes Adicionais */}
<div className="space-y-3">
{/* Cartões Utilizados */}
{summary.cardUsage.length > 0 && (
<div className="rounded-lg border bg-muted/20 p-3">
<div className="flex items-center gap-2 mb-2">
<RiBankCard2Line className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Cartões Utilizados
</span>
</div>
<div className="space-y-1">
<span className="text-xs font-semibold uppercase text-muted-foreground/70">
Boletos
{summary.cardUsage.map((card, index) => (
<div
key={index}
className="flex items-center justify-between text-sm"
>
<span className="text-foreground">{card.name}</span>
<span className="font-medium text-foreground">
{formatCurrency(card.amount)}
</span>
<p>
Pagos: {formatCurrency(summary.boletoStats.paidAmount)} (
{summary.boletoStats.paidCount})
</p>
<p>
Pendentes:{" "}
{formatCurrency(summary.boletoStats.pendingAmount)} (
{summary.boletoStats.pendingCount})
</p>
</div>
))}
</div>
</div>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Inclui {summary.lancamentoCount} lançamentos.</span>
<span>Último envio: {lastMailLabel}</span>
{/* Status de Boletos */}
{(summary.boletoStats.paidCount > 0 ||
summary.boletoStats.pendingCount > 0) && (
<div className="rounded-lg border bg-muted/20 p-3">
<div className="flex items-center gap-2 mb-2">
<RiBillLine className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Status de Boletos
</span>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<p className="text-xs text-muted-foreground">Pagos</p>
<p className="text-sm font-semibold text-green-600">
{formatCurrency(summary.boletoStats.paidAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.paidCount})
</span>
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Pendentes
</p>
<p className="text-sm font-semibold text-amber-600">
{formatCurrency(summary.boletoStats.pendingAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.pendingCount})
</span>
</p>
</div>
</div>
</div>
)}
</div>
</div>

View File

@@ -1,11 +1,11 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import { RiArrowRightSLine, RiMore2Line } from "@remixicon/react";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
@@ -13,12 +13,12 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-word sm:gap-2.5",
className
)}
{...props}
/>
)
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -28,7 +28,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
);
}
function BreadcrumbLink({
@@ -36,9 +36,9 @@ function BreadcrumbLink({
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
@@ -46,7 +46,7 @@ function BreadcrumbLink({
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
@@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
);
}
function BreadcrumbSeparator({
@@ -75,9 +75,9 @@ function BreadcrumbSeparator({
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
{children ?? <RiArrowRightSLine />}
</li>
)
);
}
function BreadcrumbEllipsis({
@@ -92,18 +92,18 @@ function BreadcrumbEllipsis({
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<RiMore2Line className="size-4" />
<span className="sr-only">More</span>
</span>
)
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
};

View File

@@ -1,15 +1,14 @@
"use client"
"use client";
import * as React from "react"
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils/ui";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils/ui"
import { Button, buttonVariants } from "@/components/ui/button"
RiArrowDownSLine,
RiArrowLeftSLine,
RiArrowRightSLine,
} from "@remixicon/react";
import * as React from "react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
function Calendar({
className,
@@ -21,15 +20,15 @@ function Calendar({
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames()
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
@@ -136,27 +135,30 @@ function Calendar({
className={cn(className)}
{...props}
/>
)
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
<RiArrowLeftSLine
className={cn("size-4", className)}
{...props}
/>
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
<RiArrowRightSLine
className={cn("size-4", className)}
{...props}
/>
)
);
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
<RiArrowDownSLine className={cn("size-4", className)} {...props} />
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
@@ -166,13 +168,13 @@ function Calendar({
{children}
</div>
</td>
)
);
},
...components,
}}
{...props}
/>
)
);
}
function CalendarDayButton({
@@ -181,12 +183,12 @@ function CalendarDayButton({
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null)
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
@@ -210,7 +212,7 @@ function CalendarDayButton({
)}
{...props}
/>
)
);
}
export { Calendar, CalendarDayButton }
export { Calendar, CalendarDayButton };

View File

@@ -1,10 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { RiCheckLine } from "@remixicon/react";
import * as React from "react";
function Checkbox({
className,
@@ -14,7 +13,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-lg border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
@@ -23,10 +22,10 @@ function Checkbox({
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
<RiCheckLine className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
);
}
export { Checkbox }
export { Checkbox };

View File

@@ -1,17 +1,17 @@
"use client"
"use client";
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { Command as CommandPrimitive } from "cmdk";
import * as React from "react";
import { cn } from "@/lib/utils/ui"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils/ui";
import { RiSearchLine } from "@remixicon/react";
function Command({
className,
@@ -26,7 +26,7 @@ function Command({
)}
{...props}
/>
)
);
}
function CommandDialog({
@@ -37,10 +37,10 @@ function CommandDialog({
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
@@ -52,12 +52,12 @@ function CommandDialog({
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<Command className="**:[[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
);
}
function CommandInput({
@@ -69,7 +69,7 @@ function CommandInput({
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<RiSearchLine className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
@@ -79,7 +79,7 @@ function CommandInput({
{...props}
/>
</div>
)
);
}
function CommandList({
@@ -95,7 +95,7 @@ function CommandList({
)}
{...props}
/>
)
);
}
function CommandEmpty({
@@ -107,7 +107,7 @@ function CommandEmpty({
className="py-6 text-center text-sm"
{...props}
/>
)
);
}
function CommandGroup({
@@ -118,12 +118,12 @@ function CommandGroup({
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
"text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
);
}
function CommandSeparator({
@@ -136,7 +136,7 @@ function CommandSeparator({
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
);
}
function CommandItem({
@@ -152,7 +152,7 @@ function CommandItem({
)}
{...props}
/>
)
);
}
function CommandShortcut({
@@ -168,17 +168,17 @@ function CommandShortcut({
)}
{...props}
/>
)
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandShortcut,
CommandList,
CommandSeparator,
}
CommandShortcut,
};

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { RiCalendarLine } from "@remixicon/react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
@@ -164,6 +164,8 @@ export function DatePicker({
month={month}
onMonthChange={setMonth}
onSelect={handleCalendarSelect}
fromYear={2020}
toYear={new Date().getFullYear() + 10}
locale={{
localize: {
day: (n) => ["D", "S", "T", "Q", "Q", "S", "S"][n],

View File

@@ -1,33 +1,33 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import { RiCloseLine } from "@remixicon/react";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
@@ -43,7 +43,7 @@ function DialogOverlay({
)}
{...props}
/>
)
);
}
function DialogContent({
@@ -52,7 +52,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
@@ -71,13 +71,13 @@ function DialogContent({
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<RiCloseLine />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -100,7 +100,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function DialogTitle({
@@ -113,7 +113,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
);
}
function DialogDescription({
@@ -126,7 +126,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
export {
@@ -140,4 +140,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
}
};

View File

@@ -1,10 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { RiCircleLine } from "@remixicon/react";
import * as React from "react";
function RadioGroup({
className,
@@ -16,7 +15,7 @@ function RadioGroup({
className={cn("grid gap-3", className)}
{...props}
/>
)
);
}
function RadioGroupItem({
@@ -36,10 +35,10 @@ function RadioGroupItem({
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
<RiCircleLine className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
);
}
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };

View File

@@ -1,10 +1,13 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils/ui";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
RiArrowDownSLine,
RiArrowUpSLine,
RiCheckLine,
} from "@remixicon/react";
import * as React from "react";
function Select({
...props
@@ -44,7 +47,7 @@ function SelectTrigger({
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
<RiArrowDownSLine className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
@@ -116,7 +119,7 @@ function SelectItem({
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
<RiCheckLine className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
@@ -150,7 +153,7 @@ function SelectScrollUpButton({
)}
{...props}
>
<ChevronUpIcon className="size-4" />
<RiArrowUpSLine className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
@@ -168,7 +171,7 @@ function SelectScrollDownButton({
)}
{...props}
>
<ChevronDownIcon className="size-4" />
<RiArrowDownSLine className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}

View File

@@ -1,31 +1,31 @@
"use client"
"use client";
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import * as SheetPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import { RiCloseLine } from "@remixicon/react";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
@@ -41,7 +41,7 @@ function SheetOverlay({
)}
{...props}
/>
)
);
}
function SheetContent({
@@ -50,7 +50,7 @@ function SheetContent({
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
@@ -73,12 +73,12 @@ function SheetContent({
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<RiCloseLine className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
);
}
function SheetTitle({
@@ -111,7 +111,7 @@ function SheetTitle({
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
);
}
function SheetDescription({
@@ -124,16 +124,16 @@ function SheetDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
};

View File

@@ -20,8 +20,8 @@ import {
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils/ui";
import { Slot } from "@radix-ui/react-slot";
import { RiLayoutLeft2Line } from "@remixicon/react";
import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import * as React from "react";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
@@ -272,7 +272,7 @@ function SidebarTrigger({
}}
{...props}
>
<PanelLeftIcon />
<RiLayoutLeft2Line />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);

View File

@@ -1,28 +1,28 @@
"use client"
"use client";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
RiAlertLine,
RiCheckboxCircleLine,
RiCloseCircleLine,
RiInformationLine,
RiLoader4Line,
} from "@remixicon/react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
success: <RiCheckboxCircleLine className="size-4" />,
info: <RiInformationLine className="size-4" />,
warning: <RiAlertLine className="size-4" />,
error: <RiCloseCircleLine className="size-4" />,
loading: <RiLoader4Line className="size-4 animate-spin" />,
}}
style={
{
@@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

@@ -1,16 +1,15 @@
import { Loader2Icon } from "lucide-react"
import { cn } from "@/lib/utils/ui"
import { cn } from "@/lib/utils/ui";
import { RiLoader4Line } from "@remixicon/react";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
<RiLoader4Line
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
);
}
export { Spinner }
export { Spinner };

View File

@@ -1,7 +1,3 @@
/**
* Common types for server actions
*/
/**
* Standard action result type
*/

View File

@@ -1,11 +1,10 @@
import type { NextConfig } from "next";
import dotenv from "dotenv";
import type { NextConfig } from "next";
// Carregar variáveis de ambiente explicitamente
dotenv.config();
const nextConfig: NextConfig = {
// Output standalone para Docker (gera build otimizado com apenas deps necessárias)
output: "standalone",
experimental: {
turbopackFileSystemCacheForDev: true,

View File

@@ -1,6 +1,6 @@
{
"name": "opensheets",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@@ -27,10 +27,10 @@
"docker:rebuild": "docker compose up --build --force-recreate"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.48",
"@ai-sdk/google": "^2.0.43",
"@ai-sdk/openai": "^2.0.72",
"@openrouter/ai-sdk-provider": "^1.2.5",
"@ai-sdk/anthropic": "^2.0.52",
"@ai-sdk/google": "^2.0.44",
"@ai-sdk/openai": "^2.0.75",
"@openrouter/ai-sdk-provider": "^1.2.8",
"@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
@@ -52,25 +52,24 @@
"@radix-ui/react-tooltip": "1.2.8",
"@remixicon/react": "4.7.0",
"@tanstack/react-table": "8.21.3",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"ai": "^5.0.101",
"@vercel/analytics": "^1.6.0",
"@vercel/speed-insights": "^1.3.0",
"ai": "^5.0.105",
"babel-plugin-react-compiler": "^1.0.0",
"better-auth": "1.4.1",
"better-auth": "1.4.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "0.44.7",
"lucide-react": "0.554.0",
"motion": "^12.23.24",
"next": "16.0.4",
"motion": "^12.23.25",
"next": "16.0.6",
"next-themes": "0.4.6",
"pg": "8.16.3",
"react": "19.2.0",
"react-day-picker": "^9.11.2",
"react-day-picker": "^9.11.3",
"react-dom": "19.2.0",
"recharts": "3.5.0",
"recharts": "3.5.1",
"resend": "^6.5.2",
"sonner": "2.0.7",
"tailwind-merge": "3.4.0",
@@ -89,9 +88,9 @@
"dotenv": "^17.2.3",
"drizzle-kit": "0.31.7",
"eslint": "9.39.1",
"eslint-config-next": "16.0.4",
"eslint-config-next": "16.0.6",
"tailwindcss": "4.1.17",
"tsx": "4.20.6",
"tsx": "4.21.0",
"typescript": "5.9.3"
}
}

6653
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,16 +1,6 @@
import { Inter, Funnel_Display } from "next/font/google";
import { Funnel_Display } from "next/font/google";
import localFont from "next/font/local";
// const aeonik = localFont({
// src: [
// {
// path: "../fonts/aeonik-regular.otf",
// weight: "400",
// style: "normal",
// },
// ],
// });
const anthropic_sans = localFont({
src: [
{
@@ -26,13 +16,8 @@ const funnel_display = Funnel_Display({
weight: ["400", "500", "600", "700"],
});
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const main_font = funnel_display;
const money_font = anthropic_sans;
const title_font = anthropic_sans;
const title_font = funnel_display;
export { main_font, money_font, title_font };

View File

@@ -1,15 +1,8 @@
#!/usr/bin/env node
/**
* Script to initialize database extensions before running migrations
* This ensures pgcrypto extension is available for gen_random_bytes()
*/
import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as fs from 'fs';
import * as path from 'path';
import { config } from "dotenv";
import { drizzle } from "drizzle-orm/node-postgres";
import * as fs from "fs";
import * as path from "path";
import { Pool } from "pg";
// Load environment variables from .env
config();
@@ -18,7 +11,7 @@ async function initDatabase() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error('DATABASE_URL environment variable is required');
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
@@ -26,18 +19,23 @@ async function initDatabase() {
const db = drizzle(pool);
try {
console.log('🔧 Initializing database extensions...');
console.log("🔧 Initializing database extensions...");
// Read and execute init.sql as a single query
const initSqlPath = path.join(process.cwd(), 'scripts', 'postgres', 'init.sql');
const initSql = fs.readFileSync(initSqlPath, 'utf-8');
const initSqlPath = path.join(
process.cwd(),
"scripts",
"postgres",
"init.sql"
);
const initSql = fs.readFileSync(initSqlPath, "utf-8");
console.log('Executing init.sql...');
console.log("Executing init.sql...");
await db.execute(initSql);
console.log('✅ Database initialization completed');
console.log("✅ Database initialization completed");
} catch (error) {
console.error('❌ Database initialization failed:', error);
console.error("❌ Database initialization failed:", error);
process.exit(1);
} finally {
await pool.end();

View File

@@ -3,7 +3,6 @@
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Log de sucesso
DO $$
BEGIN