mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter - Implementados ícones para modos claro e escuro do ChatGPT - Criado script de inicialização para PostgreSQL com extensão pgcrypto - Adicionado script de configuração de ambiente que faz backup do .env - Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
205
components/lancamentos/shared/anticipation-card.tsx
Normal file
205
components/lancamentos/shared/anticipation-card.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { cancelInstallmentAnticipationAction } from "@/app/(dashboard)/lancamentos/anticipation-actions";
|
||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import type { InstallmentAnticipationWithRelations } from "@/lib/installments/anticipation-types";
|
||||
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import MoneyValues from "@/components/money-values";
|
||||
|
||||
interface AnticipationCardProps {
|
||||
anticipation: InstallmentAnticipationWithRelations;
|
||||
onViewLancamento?: (lancamentoId: string) => void;
|
||||
onCanceled?: () => void;
|
||||
}
|
||||
|
||||
const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const formatPeriodLabel = (period: string) => {
|
||||
const [year, month] = period.split("-").map(Number);
|
||||
if (!year || !month) {
|
||||
return period;
|
||||
}
|
||||
const date = new Date(year, month - 1, 1);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return period;
|
||||
}
|
||||
const label = monthFormatter.format(date);
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
};
|
||||
|
||||
export function AnticipationCard({
|
||||
anticipation,
|
||||
onViewLancamento,
|
||||
onCanceled,
|
||||
}: AnticipationCardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const isSettled = anticipation.lancamento.isSettled === true;
|
||||
const canCancel = !isSettled;
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return format(date, "dd 'de' MMMM 'de' yyyy", { locale: ptBR });
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
startTransition(async () => {
|
||||
const result = await cancelInstallmentAnticipationAction({
|
||||
anticipationId: anticipation.id,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
onCanceled?.();
|
||||
} else {
|
||||
toast.error(result.error || "Erro ao cancelar antecipação");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewLancamento = () => {
|
||||
onViewLancamento?.(anticipation.lancamentoId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-base">
|
||||
{anticipation.installmentCount}{" "}
|
||||
{anticipation.installmentCount === 1
|
||||
? "parcela antecipada"
|
||||
: "parcelas antecipadas"}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<RiCalendarCheckLine className="mr-1 inline size-3.5" />
|
||||
{formatDate(anticipation.anticipationDate)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{formatPeriodLabel(anticipation.anticipationPeriod)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Valor Original</dt>
|
||||
<dd className="mt-1 font-medium tabular-nums">
|
||||
<MoneyValues amount={Number(anticipation.totalAmount)} />
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{Number(anticipation.discount) > 0 && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Desconto</dt>
|
||||
<dd className="mt-1 font-medium tabular-nums text-green-600">
|
||||
- <MoneyValues amount={Number(anticipation.discount)} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={Number(anticipation.discount) > 0 ? "col-span-2 border-t pt-3" : ""}>
|
||||
<dt className="text-muted-foreground">
|
||||
{Number(anticipation.discount) > 0 ? "Valor Final" : "Valor Total"}
|
||||
</dt>
|
||||
<dd className="mt-1 text-lg font-semibold tabular-nums text-primary">
|
||||
<MoneyValues
|
||||
amount={
|
||||
Number(anticipation.totalAmount) < 0
|
||||
? Number(anticipation.totalAmount) + Number(anticipation.discount)
|
||||
: Number(anticipation.totalAmount) - Number(anticipation.discount)
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Status do Lançamento</dt>
|
||||
<dd className="mt-1">
|
||||
<Badge variant={isSettled ? "success" : "outline"}>
|
||||
{isSettled ? "Pago" : "Pendente"}
|
||||
</Badge>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{anticipation.pagador && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Pagador</dt>
|
||||
<dd className="mt-1 font-medium">{anticipation.pagador.name}</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anticipation.categoria && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Categoria</dt>
|
||||
<dd className="mt-1 font-medium">
|
||||
{anticipation.categoria.name}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{anticipation.note && (
|
||||
<div className="rounded-lg border bg-muted/20 p-3">
|
||||
<dt className="text-xs font-medium text-muted-foreground">
|
||||
Observação
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm">{anticipation.note}</dd>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-wrap items-center justify-between gap-2 border-t pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleViewLancamento}
|
||||
disabled={isPending}
|
||||
>
|
||||
<RiEyeLine className="mr-2 size-4" />
|
||||
Ver Lançamento
|
||||
</Button>
|
||||
|
||||
{canCancel && (
|
||||
<ConfirmActionDialog
|
||||
trigger={
|
||||
<Button variant="destructive" size="sm" disabled={isPending}>
|
||||
<RiCloseLine className="mr-2 size-4" />
|
||||
Cancelar Antecipação
|
||||
</Button>
|
||||
}
|
||||
title="Cancelar antecipação?"
|
||||
description="Esta ação irá reverter a antecipação e restaurar as parcelas originais. O lançamento de antecipação será removido."
|
||||
confirmLabel="Cancelar Antecipação"
|
||||
confirmVariant="destructive"
|
||||
pendingLabel="Cancelando..."
|
||||
onConfirm={handleCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSettled && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Não é possível cancelar uma antecipação paga
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
129
components/lancamentos/shared/estabelecimento-input.tsx
Normal file
129
components/lancamentos/shared/estabelecimento-input.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { RiCheckLine, RiSearchLine } from "@remixicon/react";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
|
||||
export interface EstabelecimentoInputProps {
|
||||
id?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
estabelecimentos: string[];
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export function EstabelecimentoInput({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
estabelecimentos = [],
|
||||
placeholder = "Ex.: Padaria",
|
||||
required = false,
|
||||
maxLength = 20,
|
||||
}: EstabelecimentoInputProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [searchValue, setSearchValue] = React.useState("");
|
||||
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
onChange(selectedValue);
|
||||
setOpen(false);
|
||||
setSearchValue("");
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
onChange(newValue);
|
||||
setSearchValue(newValue);
|
||||
|
||||
// Open popover when user types and there are suggestions
|
||||
if (newValue.length > 0 && estabelecimentos.length > 0) {
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEstabelecimentos = React.useMemo(() => {
|
||||
if (!searchValue) return estabelecimentos;
|
||||
|
||||
const lowerSearch = searchValue.toLowerCase();
|
||||
return estabelecimentos.filter((item) =>
|
||||
item.toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
}, [estabelecimentos, searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
maxLength={maxLength}
|
||||
autoComplete="off"
|
||||
onFocus={() => {
|
||||
if (estabelecimentos.length > 0) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{estabelecimentos.length > 0 && (
|
||||
<RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
{estabelecimentos.length > 0 && (
|
||||
<PopoverContent
|
||||
className="p-0 w-[--radix-popover-trigger-width]"
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<CommandList className="max-h-[300px] overflow-y-auto">
|
||||
<CommandEmpty className="p-6">
|
||||
Nenhum estabelecimento encontrado.
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="p-1">
|
||||
{filteredEstabelecimentos.map((item) => (
|
||||
<CommandItem
|
||||
key={item}
|
||||
value={item}
|
||||
onSelect={() => handleSelect(item)}
|
||||
className="cursor-pointer gap-1"
|
||||
>
|
||||
<RiCheckLine
|
||||
className={cn(
|
||||
"size-4 shrink-0",
|
||||
value === item
|
||||
? "opacity-100 text-green-500"
|
||||
: "opacity-5"
|
||||
)}
|
||||
/>
|
||||
<span className="truncate flex-1">{item}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
92
components/lancamentos/shared/installment-timeline.tsx
Normal file
92
components/lancamentos/shared/installment-timeline.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
calculateLastInstallmentDate,
|
||||
formatCurrentInstallment,
|
||||
formatLastInstallmentDate,
|
||||
formatPurchaseDate,
|
||||
} from "@/lib/installments/utils";
|
||||
import { RiArrowDownFill, RiCheckLine } from "@remixicon/react";
|
||||
|
||||
type InstallmentTimelineProps = {
|
||||
purchaseDate: Date;
|
||||
currentInstallment: number;
|
||||
totalInstallments: number;
|
||||
period: string;
|
||||
};
|
||||
|
||||
export function InstallmentTimeline({
|
||||
purchaseDate,
|
||||
currentInstallment,
|
||||
totalInstallments,
|
||||
period,
|
||||
}: InstallmentTimelineProps) {
|
||||
const lastInstallmentDate = calculateLastInstallmentDate(
|
||||
period,
|
||||
currentInstallment,
|
||||
totalInstallments
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-between px-4 py-4">
|
||||
{/* Linha de conexão */}
|
||||
<div className="absolute left-0 right-0 top-6 h-0.5 bg-border">
|
||||
<div
|
||||
className="h-full bg-blue-600 transition-all duration-300"
|
||||
style={{
|
||||
width: `${
|
||||
((currentInstallment - 1) / (totalInstallments - 1)) * 100
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Ponto 1: Data de Compra */}
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div className="flex size-4 items-center justify-center rounded-full border-2 border-blue-600 bg-blue-600 shadow-sm">
|
||||
<RiCheckLine className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
Data de Compra
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatPurchaseDate(purchaseDate)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ponto 2: Parcela Atual */}
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div
|
||||
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-orange-600 bg-orange-600`}
|
||||
>
|
||||
<RiArrowDownFill className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
Parcela Atual
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatCurrentInstallment(currentInstallment, totalInstallments)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ponto 3: Última Parcela */}
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div
|
||||
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-green-600 bg-green-600`}
|
||||
>
|
||||
<RiCheckLine className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
Última Parcela
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatLastInstallmentDate(lastInstallmentDate)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user