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:
Felipe Coutinho
2025-11-15 15:49:36 -03:00
commit ea0b8618e0
441 changed files with 53569 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
"use client";
import { Label } from "@/components/ui/label";
import { DatePicker } from "@/components/ui/date-picker";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CurrencyInput } from "@/components/ui/currency-input";
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
import { RiCalculatorLine } from "@remixicon/react";
import { EstabelecimentoInput } from "../../shared/estabelecimento-input";
import type { BasicFieldsSectionProps } from "./lancamento-dialog-types";
export function BasicFieldsSection({
formState,
onFieldChange,
estabelecimentos,
monthOptions,
}: BasicFieldsSectionProps) {
return (
<>
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-1/2 space-y-1">
<Label htmlFor="purchaseDate">Data da transação</Label>
<DatePicker
id="purchaseDate"
value={formState.purchaseDate}
onChange={(value) => onFieldChange("purchaseDate", value)}
placeholder="Data da transação"
required
/>
</div>
<div className="w-1/2 space-y-1">
<Label htmlFor="period">Período</Label>
<Select
value={formState.period}
onValueChange={(value) => onFieldChange("period", value)}
>
<SelectTrigger id="period" className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{monthOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-1/2 space-y-1">
<Label htmlFor="name">Estabelecimento</Label>
<EstabelecimentoInput
id="name"
value={formState.name}
onChange={(value) => onFieldChange("name", value)}
estabelecimentos={estabelecimentos}
placeholder="Ex.: Padaria"
maxLength={20}
required
/>
</div>
<div className="w-1/2 space-y-1">
<Label htmlFor="amount">Valor</Label>
<div className="relative">
<CurrencyInput
id="amount"
value={formState.amount}
onValueChange={(value) => onFieldChange("amount", value)}
placeholder="R$ 0,00"
required
className="pr-10"
/>
<CalculatorDialogButton
variant="ghost"
size="icon-sm"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
>
<RiCalculatorLine className="h-4 w-4 text-muted-foreground" />
</CalculatorDialogButton>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import { Label } from "@/components/ui/label";
import { DatePicker } from "@/components/ui/date-picker";
import { cn } from "@/lib/utils/ui";
import type { BoletoFieldsSectionProps } from "./lancamento-dialog-types";
export function BoletoFieldsSection({
formState,
onFieldChange,
showPaymentDate,
}: BoletoFieldsSectionProps) {
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-2 w-full",
showPaymentDate ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="dueDate">Vencimento do boleto</Label>
<DatePicker
id="dueDate"
value={formState.dueDate}
onChange={(value) => onFieldChange("dueDate", value)}
placeholder="Selecione o vencimento"
/>
</div>
{showPaymentDate ? (
<div className="space-y-2 w-full md:w-1/2">
<Label htmlFor="boletoPaymentDate">Pagamento do boleto</Label>
<DatePicker
id="boletoPaymentDate"
value={formState.boletoPaymentDate}
onChange={(value) => onFieldChange("boletoPaymentDate", value)}
placeholder="Selecione a data de pagamento"
/>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,105 @@
"use client";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_TRANSACTION_TYPES } from "@/lib/lancamentos/constants";
import { cn } from "@/lib/utils/ui";
import {
CategoriaSelectContent,
TransactionTypeSelectContent,
} from "../../select-items";
import type { CategorySectionProps } from "./lancamento-dialog-types";
export function CategorySection({
formState,
onFieldChange,
categoriaOptions,
categoriaGroups,
isUpdateMode,
}: CategorySectionProps) {
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
{!isUpdateMode ? (
<div className="w-full space-y-1 md:w-1/2">
<Label htmlFor="transactionType">Tipo de transação</Label>
<Select
value={formState.transactionType}
onValueChange={(value) => onFieldChange("transactionType", value)}
>
<SelectTrigger id="transactionType" className="w-full">
<SelectValue placeholder="Selecione">
{formState.transactionType && (
<TransactionTypeSelectContent
label={formState.transactionType}
/>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_TRANSACTION_TYPES.filter(
(type) => type !== "Transferência"
).map((type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="categoria">Categoria</Label>
<Select
value={formState.categoriaId}
onValueChange={(value) => onFieldChange("categoriaId", value)}
>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione">
{formState.categoriaId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === formState.categoriaId
);
return selectedOption ? (
<CategoriaSelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
label={option.label}
icon={option.icon}
/>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>
</div>
);
}

View File

@@ -0,0 +1,95 @@
"use client";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_CONDITIONS } from "@/lib/lancamentos/constants";
import { cn } from "@/lib/utils/ui";
import { ConditionSelectContent } from "../../select-items";
import type { ConditionSectionProps } from "./lancamento-dialog-types";
export function ConditionSection({
formState,
onFieldChange,
showInstallments,
showRecurrence,
}: ConditionSectionProps) {
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-2 w-full",
showInstallments || showRecurrence ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="condition">Condição</Label>
<Select
value={formState.condition}
onValueChange={(value) => onFieldChange("condition", value)}
>
<SelectTrigger id="condition" className="w-full">
<SelectValue placeholder="Selecione">
{formState.condition && (
<ConditionSelectContent label={formState.condition} />
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_CONDITIONS.map((condition) => (
<SelectItem key={condition} value={condition}>
<ConditionSelectContent label={condition} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{showInstallments ? (
<div className="space-y-2 w-full md:w-1/2">
<Label htmlFor="installmentCount">Parcelado em</Label>
<Select
value={formState.installmentCount}
onValueChange={(value) => onFieldChange("installmentCount", value)}
>
<SelectTrigger id="installmentCount" className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{[...Array(24)].map((_, index) => (
<SelectItem key={index + 2} value={String(index + 2)}>
{index + 2}x
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
{showRecurrence ? (
<div className="space-y-2 w-full md:w-1/2">
<Label htmlFor="recurrenceCount">Lançamento fixo</Label>
<Select
value={formState.recurrenceCount}
onValueChange={(value) => onFieldChange("recurrenceCount", value)}
>
<SelectTrigger id="recurrenceCount" className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{[...Array(24)].map((_, index) => (
<SelectItem key={index + 2} value={String(index + 2)}>
Por {index + 2} meses
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,87 @@
import type { LancamentoFormState } from "@/lib/lancamentos/form-helpers";
import type { LancamentoItem, SelectOption } from "../../types";
export type FormState = LancamentoFormState;
export interface LancamentoDialogProps {
mode: "create" | "update";
trigger?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId?: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
estabelecimentos: string[];
lancamento?: LancamentoItem;
defaultPeriod?: string;
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
onBulkEditRequest?: (data: {
id: string;
name: string;
categoriaId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
}) => void;
}
export interface BaseFieldSectionProps {
formState: FormState;
onFieldChange: <Key extends keyof FormState>(
key: Key,
value: FormState[Key]
) => void;
}
export interface BasicFieldsSectionProps extends BaseFieldSectionProps {
estabelecimentos: string[];
monthOptions: Array<{ value: string; label: string }>;
}
export interface CategorySectionProps extends BaseFieldSectionProps {
categoriaOptions: SelectOption[];
categoriaGroups: Array<{
label: string;
options: SelectOption[];
}>;
isUpdateMode: boolean;
}
export interface SplitAndSettlementSectionProps extends BaseFieldSectionProps {
showSettledToggle: boolean;
}
export interface PagadorSectionProps extends BaseFieldSectionProps {
pagadorOptions: SelectOption[];
secondaryPagadorOptions: SelectOption[];
}
export interface PaymentMethodSectionProps extends BaseFieldSectionProps {
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
isUpdateMode: boolean;
disablePaymentMethod: boolean;
disableCartaoSelect: boolean;
}
export interface BoletoFieldsSectionProps extends BaseFieldSectionProps {
showPaymentDate: boolean;
}
export interface ConditionSectionProps extends BaseFieldSectionProps {
showInstallments: boolean;
showRecurrence: boolean;
}
export type NoteSectionProps = BaseFieldSectionProps;

View File

@@ -0,0 +1,421 @@
"use client";
import {
createLancamentoAction,
updateLancamentoAction,
} from "@/app/(dashboard)/lancamentos/actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useControlledState } from "@/hooks/use-controlled-state";
import {
filterSecondaryPagadorOptions,
groupAndSortCategorias,
} from "@/lib/lancamentos/categoria-helpers";
import {
applyFieldDependencies,
buildLancamentoInitialState,
} from "@/lib/lancamentos/form-helpers";
import { createMonthOptions } from "@/lib/utils/period";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner";
import { BasicFieldsSection } from "./basic-fields-section";
import { BoletoFieldsSection } from "./boleto-fields-section";
import { CategorySection } from "./category-section";
import { ConditionSection } from "./condition-section";
import type {
FormState,
LancamentoDialogProps,
} from "./lancamento-dialog-types";
import { NoteSection } from "./note-section";
import { PagadorSection } from "./pagador-section";
import { PaymentMethodSection } from "./payment-method-section";
import { SplitAndSettlementSection } from "./split-settlement-section";
export function LancamentoDialog({
mode,
trigger,
open,
onOpenChange,
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
estabelecimentos,
lancamento,
defaultPeriod,
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
lockCartaoSelection,
lockPaymentMethod,
onBulkEditRequest,
}: LancamentoDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
open,
false,
onOpenChange
);
const [formState, setFormState] = useState<FormState>(() =>
buildLancamentoInitialState(lancamento, defaultPagadorId, defaultPeriod, {
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
})
);
const [periodDirty, setPeriodDirty] = useState(false);
const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
if (dialogOpen) {
setFormState(
buildLancamentoInitialState(
lancamento,
defaultPagadorId,
defaultPeriod,
{
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
}
)
);
setErrorMessage(null);
setPeriodDirty(false);
}
}, [
dialogOpen,
lancamento,
defaultPagadorId,
defaultPeriod,
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
]);
const primaryPagador = formState.pagadorId;
const secondaryPagadorOptions = useMemo(
() => filterSecondaryPagadorOptions(splitPagadorOptions, primaryPagador),
[splitPagadorOptions, primaryPagador]
);
const categoriaGroups = useMemo(() => {
const filtered = categoriaOptions.filter(
(option) =>
option.group?.toLowerCase() === formState.transactionType.toLowerCase()
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, formState.transactionType]);
const monthOptions = useMemo(
() => createMonthOptions(formState.period),
[formState.period]
);
const handleFieldChange = useCallback(
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
if (key === "period") {
setPeriodDirty(true);
}
setFormState((prev) => {
const dependencies = applyFieldDependencies(
key,
value,
prev,
periodDirty
);
return {
...prev,
[key]: value,
...dependencies,
};
});
},
[periodDirty]
);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrorMessage(null);
if (!formState.purchaseDate) {
const message = "Informe a data da transação.";
setErrorMessage(message);
toast.error(message);
return;
}
if (!formState.name.trim()) {
const message = "Informe a descrição do lançamento.";
setErrorMessage(message);
toast.error(message);
return;
}
if (formState.isSplit && !formState.pagadorId) {
const message =
"Selecione o pagador principal para dividir o lançamento.";
setErrorMessage(message);
toast.error(message);
return;
}
if (formState.isSplit && !formState.secondaryPagadorId) {
const message =
"Selecione o pagador secundário para dividir o lançamento.";
setErrorMessage(message);
toast.error(message);
return;
}
const amountValue = Number(formState.amount);
if (Number.isNaN(amountValue)) {
const message = "Informe um valor válido.";
setErrorMessage(message);
toast.error(message);
return;
}
const sanitizedAmount = Math.abs(amountValue);
const payload = {
purchaseDate: formState.purchaseDate,
period: formState.period,
name: formState.name.trim(),
transactionType: formState.transactionType,
amount: sanitizedAmount,
condition: formState.condition,
paymentMethod: formState.paymentMethod,
pagadorId: formState.pagadorId,
secondaryPagadorId: formState.isSplit
? formState.secondaryPagadorId
: undefined,
isSplit: formState.isSplit,
contaId: formState.contaId,
cartaoId: formState.cartaoId,
categoriaId: formState.categoriaId,
note: formState.note.trim() || undefined,
isSettled:
formState.paymentMethod === "Cartão de crédito"
? null
: Boolean(formState.isSettled),
installmentCount:
formState.condition === "Parcelado" && formState.installmentCount
? Number(formState.installmentCount)
: undefined,
recurrenceCount:
formState.condition === "Recorrente" && formState.recurrenceCount
? Number(formState.recurrenceCount)
: undefined,
dueDate:
formState.paymentMethod === "Boleto" && formState.dueDate
? formState.dueDate
: undefined,
boletoPaymentDate:
mode === "update" &&
formState.paymentMethod === "Boleto" &&
formState.boletoPaymentDate
? formState.boletoPaymentDate
: undefined,
};
startTransition(async () => {
if (mode === "create") {
const result = await createLancamentoAction(payload);
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
return;
}
setErrorMessage(result.error);
toast.error(result.error);
return;
}
// Update mode
const hasSeriesId = Boolean(lancamento?.seriesId);
if (hasSeriesId && onBulkEditRequest) {
// Para lançamentos em série, abre o diálogo de bulk action
onBulkEditRequest({
id: lancamento?.id ?? "",
name: formState.name.trim(),
categoriaId: formState.categoriaId,
note: formState.note.trim() || "",
pagadorId: formState.pagadorId,
contaId: formState.contaId,
cartaoId: formState.cartaoId,
amount: sanitizedAmount,
dueDate:
formState.paymentMethod === "Boleto"
? formState.dueDate || null
: null,
boletoPaymentDate:
mode === "update" && formState.paymentMethod === "Boleto"
? formState.boletoPaymentDate || null
: null,
});
return;
}
// Atualização normal para lançamentos únicos ou todos os campos
const result = await updateLancamentoAction({
id: lancamento?.id ?? "",
...payload,
});
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
return;
}
setErrorMessage(result.error);
toast.error(result.error);
});
},
[
formState,
mode,
lancamento?.id,
lancamento?.seriesId,
setDialogOpen,
onBulkEditRequest,
]
);
const title = mode === "create" ? "Novo lançamento" : "Editar lançamento";
const description =
mode === "create"
? "Informe os dados abaixo para registrar um novo lançamento."
: "Atualize as informações do lançamento selecionado.";
const submitLabel = mode === "create" ? "Salvar lançamento" : "Atualizar";
const showInstallments = formState.condition === "Parcelado";
const showRecurrence = formState.condition === "Recorrente";
const showDueDate = formState.paymentMethod === "Boleto";
const showPaymentDate = mode === "update" && showDueDate;
const showSettledToggle = formState.paymentMethod !== "Cartão de crédito";
const isUpdateMode = mode === "update";
const disablePaymentMethod = Boolean(lockPaymentMethod && mode === "create");
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<form
className="space-y-2 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
onSubmit={handleSubmit}
>
<BasicFieldsSection
formState={formState}
onFieldChange={handleFieldChange}
estabelecimentos={estabelecimentos}
monthOptions={monthOptions}
/>
<CategorySection
formState={formState}
onFieldChange={handleFieldChange}
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
isUpdateMode={isUpdateMode}
/>
{!isUpdateMode ? (
<SplitAndSettlementSection
formState={formState}
onFieldChange={handleFieldChange}
showSettledToggle={showSettledToggle}
/>
) : null}
<PagadorSection
formState={formState}
onFieldChange={handleFieldChange}
pagadorOptions={pagadorOptions}
secondaryPagadorOptions={secondaryPagadorOptions}
/>
<PaymentMethodSection
formState={formState}
onFieldChange={handleFieldChange}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
isUpdateMode={isUpdateMode}
disablePaymentMethod={disablePaymentMethod}
disableCartaoSelect={disableCartaoSelect}
/>
{showDueDate ? (
<BoletoFieldsSection
formState={formState}
onFieldChange={handleFieldChange}
showPaymentDate={showPaymentDate}
/>
) : null}
{!isUpdateMode ? (
<ConditionSection
formState={formState}
onFieldChange={handleFieldChange}
showInstallments={showInstallments}
showRecurrence={showRecurrence}
/>
) : null}
<NoteSection
formState={formState}
onFieldChange={handleFieldChange}
/>
{errorMessage ? (
<p className="text-sm text-destructive">{errorMessage}</p>
) : null}
<DialogFooter className="gap-3">
<Button
type="button"
variant="outline"
onClick={() => setDialogOpen(false)}
disabled={isPending}
>
Cancelar
</Button>
<Button type="submit" disabled={isPending}>
{isPending ? "Salvando..." : submitLabel}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,20 @@
"use client";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { NoteSectionProps } from "./lancamento-dialog-types";
export function NoteSection({ formState, onFieldChange }: NoteSectionProps) {
return (
<div className="space-y-2">
<Label htmlFor="note">Anotação</Label>
<Textarea
id="note"
value={formState.note}
onChange={(event) => onFieldChange("note", event.target.value)}
placeholder="Adicione observações sobre o lançamento"
rows={2}
/>
</div>
);
}

View File

@@ -0,0 +1,101 @@
"use client";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PagadorSelectContent } from "../../select-items";
import type { PagadorSectionProps } from "./lancamento-dialog-types";
export function PagadorSection({
formState,
onFieldChange,
pagadorOptions,
secondaryPagadorOptions,
}: PagadorSectionProps) {
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-full space-y-1">
<Label htmlFor="pagador">Pagador</Label>
<Select
value={formState.pagadorId}
onValueChange={(value) => onFieldChange("pagadorId", value)}
>
<SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione">
{formState.pagadorId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === formState.pagadorId
);
return selectedOption ? (
<PagadorSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{formState.isSplit ? (
<div className="w-full space-y-1">
<Label htmlFor="secondaryPagador">Dividir com</Label>
<Select
value={formState.secondaryPagadorId}
onValueChange={(value) =>
onFieldChange("secondaryPagadorId", value)
}
>
<SelectTrigger
id="secondaryPagador"
disabled={secondaryPagadorOptions.length === 0}
className={"w-full"}
>
<SelectValue placeholder="Selecione">
{formState.secondaryPagadorId &&
(() => {
const selectedOption = secondaryPagadorOptions.find(
(opt) => opt.value === formState.secondaryPagadorId
);
return selectedOption ? (
<PagadorSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{secondaryPagadorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,288 @@
"use client";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import { cn } from "@/lib/utils/ui";
import {
PaymentMethodSelectContent,
ContaCartaoSelectContent,
} from "../../select-items";
import type { PaymentMethodSectionProps } from "./lancamento-dialog-types";
export function PaymentMethodSection({
formState,
onFieldChange,
contaOptions,
cartaoOptions,
isUpdateMode,
disablePaymentMethod,
disableCartaoSelect,
}: PaymentMethodSectionProps) {
const isCartaoSelected = formState.paymentMethod === "Cartão de crédito";
const showContaSelect = [
"Pix",
"Dinheiro",
"Boleto",
"Cartão de débito",
].includes(formState.paymentMethod);
return (
<>
{!isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-1 w-full",
isCartaoSelected || showContaSelect ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="paymentMethod">Forma de pagamento</Label>
<Select
value={formState.paymentMethod}
onValueChange={(value) => onFieldChange("paymentMethod", value)}
disabled={disablePaymentMethod}
>
<SelectTrigger
id="paymentMethod"
className="w-full"
disabled={disablePaymentMethod}
>
<SelectValue placeholder="Selecione" className="w-full">
{formState.paymentMethod && (
<PaymentMethodSelectContent label={formState.paymentMethod} />
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
<SelectItem key={method} value={method}>
<PaymentMethodSelectContent label={method} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{isCartaoSelected ? (
<div className="space-y-1 w-full md:w-1/2">
<Label htmlFor="cartao">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
disabled={disableCartaoSelect}
>
<SelectTrigger
id="cartao"
className="w-full"
disabled={disableCartaoSelect}
>
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : null}
{!isCartaoSelected && showContaSelect ? (
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="conta">Conta</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === formState.contaId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : null}
</div>
) : null}
{isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row">
{isCartaoSelected ? (
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="cartaoUpdate">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
>
<SelectTrigger id="cartaoUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : null}
{!isCartaoSelected && showContaSelect ? (
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full"
)}
>
<Label htmlFor="contaUpdate">Conta</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
>
<SelectTrigger id="contaUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === formState.contaId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : null}
</div>
) : null}
</>
);
}

View File

@@ -0,0 +1,58 @@
"use client";
import { cn } from "@/lib/utils/ui";
import { Checkbox } from "@/components/ui/checkbox";
import type { SplitAndSettlementSectionProps } from "./lancamento-dialog-types";
export function SplitAndSettlementSection({
formState,
onFieldChange,
showSettledToggle,
}: SplitAndSettlementSectionProps) {
return (
<div className="flex w-full flex-col gap-2 py-2 md:flex-row">
<div
className={cn(
"space-y-1",
showSettledToggle ? "md:w-1/2 md:pr-2" : "md:w-full"
)}
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-foreground">Dividir lançamento</p>
<p className="text-xs text-muted-foreground">
Selecione para atribuir parte do valor a outro pagador.
</p>
</div>
<Checkbox
checked={formState.isSplit}
onCheckedChange={(checked) =>
onFieldChange("isSplit", Boolean(checked))
}
aria-label="Dividir lançamento"
/>
</div>
</div>
{showSettledToggle ? (
<div className="space-y-1 md:w-1/2 md:pr-2">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-foreground">Marcar como pago</p>
<p className="text-xs text-muted-foreground">
Indica que o lançamento foi pago ou recebido.
</p>
</div>
<Checkbox
checked={Boolean(formState.isSettled)}
onCheckedChange={(checked) =>
onFieldChange("isSettled", Boolean(checked))
}
aria-label="Marcar como concluído"
/>
</div>
</div>
) : null}
</div>
);
}