forked from git.gladyson/openmonetis
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:
@@ -3,7 +3,7 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--spacing-custom-height-1: 28rem;
|
--spacing-custom-height-1: 29rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
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 { RiDeleteBin5Line, RiEyeLine, RiPencilLine } from "@remixicon/react";
|
import {
|
||||||
import { CheckIcon } from "lucide-react";
|
RiCheckLine,
|
||||||
|
RiDeleteBin5Line,
|
||||||
|
RiEyeLine,
|
||||||
|
RiPencilLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
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", {
|
||||||
@@ -88,7 +91,7 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.completed && (
|
{task.completed && (
|
||||||
<CheckIcon className="h-3 w-3 text-background" />
|
<RiCheckLine className="h-3 w-3 text-background" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { RiCheckLine } from "@remixicon/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
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", {
|
||||||
@@ -84,7 +83,7 @@ export function NoteDetailsDialog({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.completed && (
|
{task.completed && (
|
||||||
<CheckIcon className="h-4 w-4 text-primary-foreground" />
|
<RiCheckLine className="h-4 w-4 text-primary-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||||
import { useFormState } from "@/hooks/use-form-state";
|
import { useFormState } from "@/hooks/use-form-state";
|
||||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -388,7 +388,7 @@ export function NoteDialog({
|
|||||||
disabled={isPending || !normalize(newTaskText)}
|
disabled={isPending || !normalize(newTaskText)}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<RiAddLine className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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"
|
className="h-8 w-8 p-0 shrink-0 text-muted-foreground hover:text-destructive"
|
||||||
aria-label={`Remover tarefa "${task.text}"`}
|
aria-label={`Remover tarefa "${task.text}"`}
|
||||||
>
|
>
|
||||||
<Trash2Icon className="h-4 w-4" />
|
<RiDeleteBinLine className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { toggleLancamentoSettlementAction } from "@/app/(dashboard)/lancamentos/actions";
|
import { toggleLancamentoSettlementAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardContent } from "@/components/ui/card";
|
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"
|
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 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">
|
<EstabelecimentoLogo name={boleto.name} size={38} />
|
||||||
<RiBarcodeFill className="size-5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="block truncate text-sm font-medium text-foreground">
|
<span className="block truncate text-sm font-medium text-foreground">
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ import {
|
|||||||
import { WidgetEmptyState } from "@/components/widget-empty-state";
|
import { WidgetEmptyState } from "@/components/widget-empty-state";
|
||||||
import type { CategoryHistoryData } from "@/lib/dashboard/categories/category-history";
|
import type { CategoryHistoryData } from "@/lib/dashboard/categories/category-history";
|
||||||
import { getIconComponent } from "@/lib/utils/icons";
|
import { getIconComponent } from "@/lib/utils/icons";
|
||||||
import { RiBarChartBoxLine, RiCloseLine } from "@remixicon/react";
|
import {
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
RiArrowDownSLine,
|
||||||
|
RiBarChartBoxLine,
|
||||||
|
RiCloseLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
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"
|
className="w-full justify-between hover:scale-none"
|
||||||
>
|
>
|
||||||
Selecionar categorias
|
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>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export function InstallmentExpensesWidget({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={expense.id}
|
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="min-w-0 flex-1">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -169,17 +169,17 @@ export function InstallmentExpensesWidget({
|
|||||||
</div>
|
</div>
|
||||||
<MoneyValues amount={expense.amount} className="shrink-0" />
|
<MoneyValues amount={expense.amount} className="shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<Progress value={progress} className="h-2" />
|
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground ">
|
||||||
Restantes {remainingInstallments}
|
{endDate && `Termina em ${endDate}`}
|
||||||
{endDate && ` - Termina em ${endDate}`}
|
{` - Restante (${remainingInstallments}) `}
|
||||||
{" - Restante "}
|
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={remainingAmount}
|
amount={remainingAmount}
|
||||||
className="inline-block font-medium"
|
className="inline-block font-medium"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<Progress value={progress} className="h-2 mt-1" />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-col items-end">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -504,7 +504,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
|
|||||||
Valor da fatura
|
Valor da fatura
|
||||||
</span>
|
</span>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={selectedInvoice.totalAmount}
|
amount={Math.abs(selectedInvoice.totalAmount)}
|
||||||
className="text-lg"
|
className="text-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
import { CATEGORY_TYPE_LABEL } from "@/lib/categorias/constants";
|
import { CATEGORY_TYPE_LABEL } from "@/lib/categorias/constants";
|
||||||
import type { PurchasesByCategoryData } from "@/lib/dashboard/purchases-by-category";
|
import type { PurchasesByCategoryData } from "@/lib/dashboard/purchases-by-category";
|
||||||
import { RiArrowDownLine, RiStore3Line } from "@remixicon/react";
|
import { RiArrowDownLine, RiStore3Line } from "@remixicon/react";
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { WidgetEmptyState } from "../widget-empty-state";
|
import { WidgetEmptyState } from "../widget-empty-state";
|
||||||
|
|
||||||
@@ -19,30 +19,6 @@ type PurchasesByCategoryWidgetProps = {
|
|||||||
data: PurchasesByCategoryData;
|
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 formatTransactionDate = (date: Date) => {
|
||||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
@@ -180,30 +156,13 @@ export function PurchasesByCategoryWidget({
|
|||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{currentTransactions.map((transaction) => {
|
{currentTransactions.map((transaction) => {
|
||||||
const logo = resolveLogoPath(transaction.logo);
|
|
||||||
const initials = buildInitials(transaction.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={transaction.id}
|
key={transaction.id}
|
||||||
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
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 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">
|
<EstabelecimentoLogo name={transaction.name} size={38} />
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
|
|||||||
@@ -1,37 +1,13 @@
|
|||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import type { RecentTransactionsData } from "@/lib/dashboard/recent-transactions";
|
import type { RecentTransactionsData } from "@/lib/dashboard/recent-transactions";
|
||||||
import { RiExchangeLine } from "@remixicon/react";
|
import { RiExchangeLine } from "@remixicon/react";
|
||||||
import Image from "next/image";
|
|
||||||
import { WidgetEmptyState } from "../widget-empty-state";
|
import { WidgetEmptyState } from "../widget-empty-state";
|
||||||
|
|
||||||
type RecentTransactionsWidgetProps = {
|
type RecentTransactionsWidgetProps = {
|
||||||
data: RecentTransactionsData;
|
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 formatTransactionDate = (date: Date) => {
|
||||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
@@ -59,32 +35,13 @@ export function RecentTransactionsWidget({
|
|||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{data.transactions.map((transaction) => {
|
{data.transactions.map((transaction) => {
|
||||||
const logo = resolveLogoPath(
|
|
||||||
transaction.cardLogo ?? transaction.accountLogo
|
|
||||||
);
|
|
||||||
const initials = buildInitials(transaction.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={transaction.id}
|
key={transaction.id}
|
||||||
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
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 min-w-0 flex-1 items-center gap-3">
|
||||||
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg">
|
<EstabelecimentoLogo name={transaction.name} size={38} />
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import type { RecurringExpensesData } from "@/lib/dashboard/expenses/recurring-expenses";
|
import type { RecurringExpensesData } from "@/lib/dashboard/expenses/recurring-expenses";
|
||||||
@@ -38,9 +39,7 @@ export function RecurringExpensesWidget({
|
|||||||
key={expense.id}
|
key={expense.id}
|
||||||
className="flex items-start gap-3 border-b border-dashed pb-2 last:border-b-0 last:pb-0"
|
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">
|
<EstabelecimentoLogo name={expense.name} size={38} />
|
||||||
<RiRefreshLine className="size-5 text-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -1,37 +1,13 @@
|
|||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import type { TopEstablishmentsData } from "@/lib/dashboard/top-establishments";
|
import type { TopEstablishmentsData } from "@/lib/dashboard/top-establishments";
|
||||||
import { RiStore2Line } from "@remixicon/react";
|
import { RiStore2Line } from "@remixicon/react";
|
||||||
import Image from "next/image";
|
|
||||||
import { WidgetEmptyState } from "../widget-empty-state";
|
import { WidgetEmptyState } from "../widget-empty-state";
|
||||||
|
|
||||||
type TopEstablishmentsWidgetProps = {
|
type TopEstablishmentsWidgetProps = {
|
||||||
data: TopEstablishmentsData;
|
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) => {
|
const formatOccurrencesLabel = (occurrences: number) => {
|
||||||
if (occurrences === 1) {
|
if (occurrences === 1) {
|
||||||
return "1 lançamento";
|
return "1 lançamento";
|
||||||
@@ -53,30 +29,13 @@ export function TopEstablishmentsWidget({
|
|||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{data.establishments.map((establishment) => {
|
{data.establishments.map((establishment) => {
|
||||||
const logo = resolveLogoPath(establishment.logo);
|
|
||||||
const initials = buildInitials(establishment.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={establishment.id}
|
key={establishment.id}
|
||||||
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
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 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">
|
<EstabelecimentoLogo name={establishment.name} size={38} />
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
import { Switch } from "@/components/ui/switch";
|
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 { RiArrowUpDoubleLine } from "@remixicon/react";
|
||||||
import Image from "next/image";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { WidgetEmptyState } from "../widget-empty-state";
|
import { WidgetEmptyState } from "../widget-empty-state";
|
||||||
|
|
||||||
@@ -13,30 +16,6 @@ type TopExpensesWidgetProps = {
|
|||||||
cardOnlyExpenses: TopExpensesData;
|
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 formatTransactionDate = (date: Date) => {
|
||||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
@@ -129,30 +108,13 @@ export function TopExpensesWidget({
|
|||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{data.expenses.map((expense) => {
|
{data.expenses.map((expense) => {
|
||||||
const logo = resolveLogoPath(expense.logo);
|
|
||||||
const initials = buildInitials(expense.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={expense.id}
|
key={expense.id}
|
||||||
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
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 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">
|
<EstabelecimentoLogo name={expense.name} size={38} />
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
|
|||||||
@@ -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 =
|
const description =
|
||||||
mode === "create"
|
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.";
|
: "Atualize as informações do lançamento selecionado.";
|
||||||
const submitLabel = mode === "create" ? "Salvar lançamento" : "Atualizar";
|
const submitLabel = mode === "create" ? "Salvar lançamento" : "Atualizar";
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ export function LancamentosPage({
|
|||||||
useState<LancamentoItem | null>(null);
|
useState<LancamentoItem | null>(null);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [createOpen, setCreateOpen] = 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 [massAddOpen, setMassAddOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [lancamentoToDelete, setLancamentoToDelete] =
|
const [lancamentoToDelete, setLancamentoToDelete] =
|
||||||
@@ -288,6 +291,11 @@ export function LancamentosPage({
|
|||||||
setEditOpen(true);
|
setEditOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleCopy = useCallback((item: LancamentoItem) => {
|
||||||
|
setLancamentoToCopy(item);
|
||||||
|
setCopyOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
|
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
|
||||||
if (item.seriesId) {
|
if (item.seriesId) {
|
||||||
setPendingDeleteData(item);
|
setPendingDeleteData(item);
|
||||||
@@ -323,6 +331,7 @@ export function LancamentosPage({
|
|||||||
onCreate={allowCreate ? handleCreate : undefined}
|
onCreate={allowCreate ? handleCreate : undefined}
|
||||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
onCopy={handleCopy}
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onBulkDelete={handleMultipleBulkDelete}
|
onBulkDelete={handleMultipleBulkDelete}
|
||||||
onViewDetails={handleViewDetails}
|
onViewDetails={handleViewDetails}
|
||||||
@@ -352,6 +361,26 @@ export function LancamentosPage({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : 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
|
<LancamentoDialog
|
||||||
mode="update"
|
mode="update"
|
||||||
open={editOpen && !!selectedLancamento}
|
open={editOpen && !!selectedLancamento}
|
||||||
|
|||||||
72
components/lancamentos/shared/estabelecimento-logo.tsx
Normal file
72
components/lancamentos/shared/estabelecimento-logo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
components/lancamentos/shared/index.ts
Normal file
4
components/lancamentos/shared/index.ts
Normal 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";
|
||||||
@@ -1,15 +1,5 @@
|
|||||||
"use client";
|
"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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,15 +29,25 @@ import {
|
|||||||
LANCAMENTO_TRANSACTION_TYPES,
|
LANCAMENTO_TRANSACTION_TYPES,
|
||||||
} from "@/lib/lancamentos/constants";
|
} from "@/lib/lancamentos/constants";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
TransactionTypeSelectContent,
|
|
||||||
ConditionSelectContent,
|
|
||||||
PaymentMethodSelectContent,
|
|
||||||
CategoriaSelectContent,
|
CategoriaSelectContent,
|
||||||
PagadorSelectContent,
|
ConditionSelectContent,
|
||||||
ContaCartaoSelectContent,
|
ContaCartaoSelectContent,
|
||||||
|
PagadorSelectContent,
|
||||||
|
PaymentMethodSelectContent,
|
||||||
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
|
|
||||||
|
import { RiCheckLine, RiExpandUpDownLine } from "@remixicon/react";
|
||||||
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
||||||
|
|
||||||
const FILTER_EMPTY_VALUE = "__all";
|
const FILTER_EMPTY_VALUE = "__all";
|
||||||
@@ -337,7 +337,7 @@ export function LancamentosFilters({
|
|||||||
"Categoria"
|
"Categoria"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
|
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="start" className="w-[220px] p-0">
|
<PopoverContent align="start" className="w-[220px] p-0">
|
||||||
@@ -355,7 +355,7 @@ export function LancamentosFilters({
|
|||||||
>
|
>
|
||||||
Todas
|
Todas
|
||||||
{categoriaValue === FILTER_EMPTY_VALUE ? (
|
{categoriaValue === FILTER_EMPTY_VALUE ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<RiCheckLine className="ml-auto size-4" />
|
||||||
) : null}
|
) : null}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{categoriaOptions.map((option) => (
|
{categoriaOptions.map((option) => (
|
||||||
@@ -372,7 +372,7 @@ export function LancamentosFilters({
|
|||||||
icon={option.icon}
|
icon={option.icon}
|
||||||
/>
|
/>
|
||||||
{categoriaValue === option.slug ? (
|
{categoriaValue === option.slug ? (
|
||||||
<CheckIcon className="ml-auto size-4" />
|
<RiCheckLine className="ml-auto size-4" />
|
||||||
) : null}
|
) : null}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
RiEyeLine,
|
||||||
|
RiFileCopyLine,
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiHistoryLine,
|
RiHistoryLine,
|
||||||
RiMoreFill,
|
RiMoreFill,
|
||||||
@@ -72,6 +73,7 @@ import {
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { EstabelecimentoLogo } from "../shared/estabelecimento-logo";
|
||||||
import type {
|
import type {
|
||||||
ContaCartaoFilterOption,
|
ContaCartaoFilterOption,
|
||||||
LancamentoFilterOption,
|
LancamentoFilterOption,
|
||||||
@@ -90,6 +92,7 @@ const resolveLogoSrc = (logo: string | null) => {
|
|||||||
|
|
||||||
type BuildColumnsArgs = {
|
type BuildColumnsArgs = {
|
||||||
onEdit?: (item: LancamentoItem) => void;
|
onEdit?: (item: LancamentoItem) => void;
|
||||||
|
onCopy?: (item: LancamentoItem) => void;
|
||||||
onConfirmDelete?: (item: LancamentoItem) => void;
|
onConfirmDelete?: (item: LancamentoItem) => void;
|
||||||
onViewDetails?: (item: LancamentoItem) => void;
|
onViewDetails?: (item: LancamentoItem) => void;
|
||||||
onToggleSettlement?: (item: LancamentoItem) => void;
|
onToggleSettlement?: (item: LancamentoItem) => void;
|
||||||
@@ -101,6 +104,7 @@ type BuildColumnsArgs = {
|
|||||||
|
|
||||||
const buildColumns = ({
|
const buildColumns = ({
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onCopy,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
@@ -111,6 +115,7 @@ const buildColumns = ({
|
|||||||
}: BuildColumnsArgs): ColumnDef<LancamentoItem>[] => {
|
}: BuildColumnsArgs): ColumnDef<LancamentoItem>[] => {
|
||||||
const noop = () => undefined;
|
const noop = () => undefined;
|
||||||
const handleEdit = onEdit ?? noop;
|
const handleEdit = onEdit ?? noop;
|
||||||
|
const handleCopy = onCopy ?? noop;
|
||||||
const handleConfirmDelete = onConfirmDelete ?? noop;
|
const handleConfirmDelete = onConfirmDelete ?? noop;
|
||||||
const handleViewDetails = onViewDetails ?? noop;
|
const handleViewDetails = onViewDetails ?? noop;
|
||||||
const handleToggleSettlement = onToggleSettlement ?? noop;
|
const handleToggleSettlement = onToggleSettlement ?? noop;
|
||||||
@@ -180,6 +185,7 @@ const buildColumns = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
<EstabelecimentoLogo name={name} size={28} />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="line-clamp-2 max-w-[180px] font-bold truncate">
|
<span className="line-clamp-2 max-w-[180px] font-bold truncate">
|
||||||
@@ -522,6 +528,12 @@ const buildColumns = ({
|
|||||||
<RiPencilLine className="size-4" />
|
<RiPencilLine className="size-4" />
|
||||||
Editar
|
Editar
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{row.original.categoriaName !== "Pagamentos" && (
|
||||||
|
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
||||||
|
<RiFileCopyLine className="size-4" />
|
||||||
|
Copiar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onSelect={() => handleConfirmDelete(row.original)}
|
onSelect={() => handleConfirmDelete(row.original)}
|
||||||
@@ -583,6 +595,7 @@ type LancamentosTableProps = {
|
|||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
onMassAdd?: () => void;
|
onMassAdd?: () => void;
|
||||||
onEdit?: (item: LancamentoItem) => void;
|
onEdit?: (item: LancamentoItem) => void;
|
||||||
|
onCopy?: (item: LancamentoItem) => void;
|
||||||
onConfirmDelete?: (item: LancamentoItem) => void;
|
onConfirmDelete?: (item: LancamentoItem) => void;
|
||||||
onBulkDelete?: (items: LancamentoItem[]) => void;
|
onBulkDelete?: (items: LancamentoItem[]) => void;
|
||||||
onViewDetails?: (item: LancamentoItem) => void;
|
onViewDetails?: (item: LancamentoItem) => void;
|
||||||
@@ -602,6 +615,7 @@ export function LancamentosTable({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onMassAdd,
|
onMassAdd,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onCopy,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onBulkDelete,
|
onBulkDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
@@ -625,6 +639,7 @@ export function LancamentosTable({
|
|||||||
() =>
|
() =>
|
||||||
buildColumns({
|
buildColumns({
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onCopy,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
@@ -635,6 +650,7 @@ export function LancamentosTable({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onCopy,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
|||||||
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import {
|
import {
|
||||||
|
RiBankCard2Line,
|
||||||
|
RiBillLine,
|
||||||
|
RiExchangeDollarLine,
|
||||||
|
RiFileList3Line,
|
||||||
RiMailLine,
|
RiMailLine,
|
||||||
RiMailSendLine,
|
RiMailSendLine,
|
||||||
RiUser3Line,
|
RiUser3Line,
|
||||||
@@ -272,74 +276,149 @@ export function PagadorInfoCard({
|
|||||||
setConfirmOpen(open);
|
setConfirmOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Confirmar envio do resumo</DialogTitle>
|
<DialogTitle>Confirmar envio do resumo</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
O resumo de{" "}
|
Resumo de{" "}
|
||||||
<span className="font-semibold text-foreground">
|
<span className="font-semibold text-foreground">
|
||||||
{summary.periodLabel}
|
{summary.periodLabel}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
será enviado para{" "}
|
para{" "}
|
||||||
<span className="font-medium text-foreground">
|
<span className="font-medium text-foreground">
|
||||||
{pagador.email ?? "—"}
|
{pagador.email}
|
||||||
</span>
|
</span>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</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>
|
<div>
|
||||||
<span className="text-xs font-semibold uppercase text-muted-foreground/70">
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
Totais do mês
|
Total de Despesas
|
||||||
</span>
|
|
||||||
<p className="text-foreground">
|
|
||||||
{formatCurrency(summary.totalExpenses)} em despesas
|
|
||||||
registradas
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs">
|
<p className="text-2xl font-bold text-foreground">
|
||||||
Cartões: {formatCurrency(summary.paymentSplits.card)} -
|
{formatCurrency(summary.totalExpenses)}
|
||||||
Boletos: {formatCurrency(summary.paymentSplits.boleto)} -
|
</p>
|
||||||
Pix/Débito/Dinheiro:{" "}
|
</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)}
|
{formatCurrency(summary.paymentSplits.instant)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<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">
|
<div className="space-y-1">
|
||||||
<span className="text-xs font-semibold uppercase text-muted-foreground/70">
|
{summary.cardUsage.map((card, index) => (
|
||||||
Boletos
|
<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>
|
</span>
|
||||||
<p>
|
</div>
|
||||||
Pagos: {formatCurrency(summary.boletoStats.paidAmount)} (
|
))}
|
||||||
{summary.boletoStats.paidCount})
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Pendentes:{" "}
|
|
||||||
{formatCurrency(summary.boletoStats.pendingAmount)} (
|
|
||||||
{summary.boletoStats.pendingCount})
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
{/* Status de Boletos */}
|
||||||
<span>Inclui {summary.lancamentoCount} lançamentos.</span>
|
{(summary.boletoStats.paidCount > 0 ||
|
||||||
<span>Último envio: {lastMailLabel}</span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from "react"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import * as React from "react";
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-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">) {
|
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">) {
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
@@ -13,12 +13,12 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|||||||
<ol
|
<ol
|
||||||
data-slot="breadcrumb-list"
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
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)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbLink({
|
function BreadcrumbLink({
|
||||||
@@ -36,9 +36,9 @@ function BreadcrumbLink({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"a"> & {
|
}: React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -46,7 +46,7 @@ function BreadcrumbLink({
|
|||||||
className={cn("hover:text-foreground transition-colors", className)}
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
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)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
function BreadcrumbSeparator({
|
||||||
@@ -75,9 +75,9 @@ function BreadcrumbSeparator({
|
|||||||
className={cn("[&>svg]:size-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <RiArrowRightSLine />}
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
function BreadcrumbEllipsis({
|
||||||
@@ -92,18 +92,18 @@ function BreadcrumbEllipsis({
|
|||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="size-4" />
|
<RiMore2Line className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
BreadcrumbEllipsis,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis,
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
ChevronDownIcon,
|
RiArrowDownSLine,
|
||||||
ChevronLeftIcon,
|
RiArrowLeftSLine,
|
||||||
ChevronRightIcon,
|
RiArrowRightSLine,
|
||||||
} from "lucide-react"
|
} from "@remixicon/react";
|
||||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
import * as React from "react";
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||||
import { cn } from "@/lib/utils/ui"
|
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
@@ -21,15 +20,15 @@ function Calendar({
|
|||||||
components,
|
components,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayPicker> & {
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
||||||
}) {
|
}) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn(
|
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\_next>svg]:rotate-180`,
|
||||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
className
|
className
|
||||||
@@ -136,27 +135,30 @@ function Calendar({
|
|||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
Chevron: ({ className, orientation, ...props }) => {
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
if (orientation === "left") {
|
if (orientation === "left") {
|
||||||
return (
|
return (
|
||||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
<RiArrowLeftSLine
|
||||||
)
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orientation === "right") {
|
if (orientation === "right") {
|
||||||
return (
|
return (
|
||||||
<ChevronRightIcon
|
<RiArrowRightSLine
|
||||||
className={cn("size-4", className)}
|
className={cn("size-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
<RiArrowDownSLine className={cn("size-4", className)} {...props} />
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
DayButton: CalendarDayButton,
|
DayButton: CalendarDayButton,
|
||||||
WeekNumber: ({ children, ...props }) => {
|
WeekNumber: ({ children, ...props }) => {
|
||||||
@@ -166,13 +168,13 @@ function Calendar({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
...components,
|
...components,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarDayButton({
|
function CalendarDayButton({
|
||||||
@@ -181,12 +183,12 @@ function CalendarDayButton({
|
|||||||
modifiers,
|
modifiers,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayButton>) {
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
const ref = React.useRef<HTMLButtonElement>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (modifiers.focused) ref.current?.focus()
|
if (modifiers.focused) ref.current?.focus();
|
||||||
}, [modifiers.focused])
|
}, [modifiers.focused]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -210,7 +212,7 @@ function CalendarDayButton({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Calendar, CalendarDayButton }
|
export { Calendar, CalendarDayButton };
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils/ui";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import { CheckIcon } from "lucide-react"
|
import { RiCheckLine } from "@remixicon/react";
|
||||||
|
import * as React from "react";
|
||||||
import { cn } from "@/lib/utils/ui"
|
|
||||||
|
|
||||||
function Checkbox({
|
function Checkbox({
|
||||||
className,
|
className,
|
||||||
@@ -14,7 +13,7 @@ function Checkbox({
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -23,10 +22,10 @@ function Checkbox({
|
|||||||
data-slot="checkbox-indicator"
|
data-slot="checkbox-indicator"
|
||||||
className="grid place-content-center text-current transition-none"
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckIcon className="size-3.5" />
|
<RiCheckLine className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox };
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import * as React from "react";
|
||||||
import { SearchIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils/ui"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { RiSearchLine } from "@remixicon/react";
|
||||||
|
|
||||||
function Command({
|
function Command({
|
||||||
className,
|
className,
|
||||||
@@ -26,7 +26,7 @@ function Command({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandDialog({
|
function CommandDialog({
|
||||||
@@ -37,10 +37,10 @@ function CommandDialog({
|
|||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Dialog> & {
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
title?: string
|
title?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
@@ -52,12 +52,12 @@ function CommandDialog({
|
|||||||
className={cn("overflow-hidden p-0", className)}
|
className={cn("overflow-hidden p-0", className)}
|
||||||
showCloseButton={showCloseButton}
|
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}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandInput({
|
function CommandInput({
|
||||||
@@ -69,7 +69,7 @@ function CommandInput({
|
|||||||
data-slot="command-input-wrapper"
|
data-slot="command-input-wrapper"
|
||||||
className="flex h-9 items-center gap-2 border-b px-3"
|
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
|
<CommandPrimitive.Input
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -79,7 +79,7 @@ function CommandInput({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandList({
|
function CommandList({
|
||||||
@@ -95,7 +95,7 @@ function CommandList({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandEmpty({
|
function CommandEmpty({
|
||||||
@@ -107,7 +107,7 @@ function CommandEmpty({
|
|||||||
className="py-6 text-center text-sm"
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandGroup({
|
function CommandGroup({
|
||||||
@@ -118,12 +118,12 @@ function CommandGroup({
|
|||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandSeparator({
|
function CommandSeparator({
|
||||||
@@ -136,7 +136,7 @@ function CommandSeparator({
|
|||||||
className={cn("bg-border -mx-1 h-px", className)}
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItem({
|
function CommandItem({
|
||||||
@@ -152,7 +152,7 @@ function CommandItem({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandShortcut({
|
function CommandShortcut({
|
||||||
@@ -168,17 +168,17 @@ function CommandShortcut({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
}
|
CommandShortcut,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { RiCalendarLine } from "@remixicon/react";
|
import { RiCalendarLine } from "@remixicon/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
@@ -164,6 +164,8 @@ export function DatePicker({
|
|||||||
month={month}
|
month={month}
|
||||||
onMonthChange={setMonth}
|
onMonthChange={setMonth}
|
||||||
onSelect={handleCalendarSelect}
|
onSelect={handleCalendarSelect}
|
||||||
|
fromYear={2020}
|
||||||
|
toYear={new Date().getFullYear() + 10}
|
||||||
locale={{
|
locale={{
|
||||||
localize: {
|
localize: {
|
||||||
day: (n) => ["D", "S", "T", "Q", "Q", "S", "S"][n],
|
day: (n) => ["D", "S", "T", "Q", "Q", "S", "S"][n],
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as React from "react";
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils/ui"
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { RiCloseLine } from "@remixicon/react";
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
@@ -43,7 +43,7 @@ function DialogOverlay({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
@@ -52,7 +52,7 @@ function DialogContent({
|
|||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
@@ -71,13 +71,13 @@ function DialogContent({
|
|||||||
data-slot="dialog-close"
|
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"
|
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>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
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)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
@@ -100,7 +100,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
@@ -113,7 +113,7 @@ function DialogTitle({
|
|||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
@@ -126,7 +126,7 @@ function DialogDescription({
|
|||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -140,4 +140,4 @@ export {
|
|||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils/ui";
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
import { CircleIcon } from "lucide-react"
|
import { RiCircleLine } from "@remixicon/react";
|
||||||
|
import * as React from "react";
|
||||||
import { cn } from "@/lib/utils/ui"
|
|
||||||
|
|
||||||
function RadioGroup({
|
function RadioGroup({
|
||||||
className,
|
className,
|
||||||
@@ -16,7 +15,7 @@ function RadioGroup({
|
|||||||
className={cn("grid gap-3", className)}
|
className={cn("grid gap-3", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadioGroupItem({
|
function RadioGroupItem({
|
||||||
@@ -36,10 +35,10 @@ function RadioGroupItem({
|
|||||||
data-slot="radio-group-indicator"
|
data-slot="radio-group-indicator"
|
||||||
className="relative flex items-center justify-center"
|
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.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"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 { 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({
|
function Select({
|
||||||
...props
|
...props
|
||||||
@@ -44,7 +47,7 @@ function SelectTrigger({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
);
|
);
|
||||||
@@ -116,7 +119,7 @@ function SelectItem({
|
|||||||
>
|
>
|
||||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<RiCheckLine className="size-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
@@ -150,7 +153,7 @@ function SelectScrollUpButton({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon className="size-4" />
|
<RiArrowUpSLine className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,7 +171,7 @@ function SelectScrollDownButton({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4" />
|
<RiArrowDownSLine className="size-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import * as React from "react";
|
||||||
import { XIcon } from "lucide-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>) {
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTrigger({
|
function SheetTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetClose({
|
function SheetClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetPortal({
|
function SheetPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetOverlay({
|
function SheetOverlay({
|
||||||
@@ -41,7 +41,7 @@ function SheetOverlay({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetContent({
|
function SheetContent({
|
||||||
@@ -50,7 +50,7 @@ function SheetContent({
|
|||||||
side = "right",
|
side = "right",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
@@ -73,12 +73,12 @@ function SheetContent({
|
|||||||
>
|
>
|
||||||
{children}
|
{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">
|
<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>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
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)}
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
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)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTitle({
|
function SheetTitle({
|
||||||
@@ -111,7 +111,7 @@ function SheetTitle({
|
|||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetDescription({
|
function SheetDescription({
|
||||||
@@ -124,16 +124,16 @@ function SheetDescription({
|
|||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
}
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import {
|
|||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { RiLayoutLeft2Line } from "@remixicon/react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { PanelLeftIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||||
@@ -272,7 +272,7 @@ function SidebarTrigger({
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeftIcon />
|
<RiLayoutLeft2Line />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleCheckIcon,
|
RiAlertLine,
|
||||||
InfoIcon,
|
RiCheckboxCircleLine,
|
||||||
Loader2Icon,
|
RiCloseCircleLine,
|
||||||
OctagonXIcon,
|
RiInformationLine,
|
||||||
TriangleAlertIcon,
|
RiLoader4Line,
|
||||||
} from "lucide-react"
|
} from "@remixicon/react";
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: <RiCheckboxCircleLine className="size-4" />,
|
||||||
info: <InfoIcon className="size-4" />,
|
info: <RiInformationLine className="size-4" />,
|
||||||
warning: <TriangleAlertIcon className="size-4" />,
|
warning: <RiAlertLine className="size-4" />,
|
||||||
error: <OctagonXIcon className="size-4" />,
|
error: <RiCloseCircleLine className="size-4" />,
|
||||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
loading: <RiLoader4Line className="size-4 animate-spin" />,
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster };
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Loader2Icon } from "lucide-react"
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { RiLoader4Line } from "@remixicon/react";
|
||||||
import { cn } from "@/lib/utils/ui"
|
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
return (
|
return (
|
||||||
<Loader2Icon
|
<RiLoader4Line
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
className={cn("size-4 animate-spin", className)}
|
className={cn("size-4 animate-spin", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Spinner }
|
export { Spinner };
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* Common types for server actions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard action result type
|
* Standard action result type
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
// Carregar variáveis de ambiente explicitamente
|
// Carregar variáveis de ambiente explicitamente
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Output standalone para Docker (gera build otimizado com apenas deps necessárias)
|
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
experimental: {
|
experimental: {
|
||||||
turbopackFileSystemCacheForDev: true,
|
turbopackFileSystemCacheForDev: true,
|
||||||
|
|||||||
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opensheets",
|
"name": "opensheets",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
"docker:rebuild": "docker compose up --build --force-recreate"
|
"docker:rebuild": "docker compose up --build --force-recreate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.48",
|
"@ai-sdk/anthropic": "^2.0.52",
|
||||||
"@ai-sdk/google": "^2.0.43",
|
"@ai-sdk/google": "^2.0.44",
|
||||||
"@ai-sdk/openai": "^2.0.72",
|
"@ai-sdk/openai": "^2.0.75",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.5",
|
"@openrouter/ai-sdk-provider": "^1.2.8",
|
||||||
"@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",
|
||||||
@@ -52,25 +52,24 @@
|
|||||||
"@radix-ui/react-tooltip": "1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@remixicon/react": "4.7.0",
|
"@remixicon/react": "4.7.0",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.6.0",
|
||||||
"@vercel/speed-insights": "^1.2.0",
|
"@vercel/speed-insights": "^1.3.0",
|
||||||
"ai": "^5.0.101",
|
"ai": "^5.0.105",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"better-auth": "1.4.1",
|
"better-auth": "1.4.4",
|
||||||
"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.44.7",
|
"drizzle-orm": "0.44.7",
|
||||||
"lucide-react": "0.554.0",
|
"motion": "^12.23.25",
|
||||||
"motion": "^12.23.24",
|
"next": "16.0.6",
|
||||||
"next": "16.0.4",
|
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-day-picker": "^9.11.2",
|
"react-day-picker": "^9.11.3",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"recharts": "3.5.0",
|
"recharts": "3.5.1",
|
||||||
"resend": "^6.5.2",
|
"resend": "^6.5.2",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
@@ -89,9 +88,9 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "0.31.7",
|
"drizzle-kit": "0.31.7",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.4",
|
"eslint-config-next": "16.0.6",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.17",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6653
pnpm-lock.yaml
generated
6653
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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";
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
// const aeonik = localFont({
|
|
||||||
// src: [
|
|
||||||
// {
|
|
||||||
// path: "../fonts/aeonik-regular.otf",
|
|
||||||
// weight: "400",
|
|
||||||
// style: "normal",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// });
|
|
||||||
|
|
||||||
const anthropic_sans = localFont({
|
const anthropic_sans = localFont({
|
||||||
src: [
|
src: [
|
||||||
{
|
{
|
||||||
@@ -26,13 +16,8 @@ const funnel_display = Funnel_Display({
|
|||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const inter = Inter({
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["400", "500", "600", "700"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const main_font = funnel_display;
|
const main_font = funnel_display;
|
||||||
const money_font = anthropic_sans;
|
const money_font = anthropic_sans;
|
||||||
const title_font = anthropic_sans;
|
const title_font = funnel_display;
|
||||||
|
|
||||||
export { main_font, money_font, title_font };
|
export { main_font, money_font, title_font };
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
import { config } from "dotenv";
|
||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
/**
|
import * as fs from "fs";
|
||||||
* Script to initialize database extensions before running migrations
|
import * as path from "path";
|
||||||
* This ensures pgcrypto extension is available for gen_random_bytes()
|
import { Pool } from "pg";
|
||||||
*/
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
// Load environment variables from .env
|
// Load environment variables from .env
|
||||||
config();
|
config();
|
||||||
@@ -18,7 +11,7 @@ async function initDatabase() {
|
|||||||
const databaseUrl = process.env.DATABASE_URL;
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
if (!databaseUrl) {
|
if (!databaseUrl) {
|
||||||
console.error('DATABASE_URL environment variable is required');
|
console.error("DATABASE_URL environment variable is required");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,18 +19,23 @@ async function initDatabase() {
|
|||||||
const db = drizzle(pool);
|
const db = drizzle(pool);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🔧 Initializing database extensions...');
|
console.log("🔧 Initializing database extensions...");
|
||||||
|
|
||||||
// Read and execute init.sql as a single query
|
// Read and execute init.sql as a single query
|
||||||
const initSqlPath = path.join(process.cwd(), 'scripts', 'postgres', 'init.sql');
|
const initSqlPath = path.join(
|
||||||
const initSql = fs.readFileSync(initSqlPath, 'utf-8');
|
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);
|
await db.execute(initSql);
|
||||||
|
|
||||||
console.log('✅ Database initialization completed');
|
console.log("✅ Database initialization completed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Database initialization failed:', error);
|
console.error("❌ Database initialization failed:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
await pool.end();
|
await pool.end();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
|
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
-- Log de sucesso
|
-- Log de sucesso
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
Reference in New Issue
Block a user