mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-10 07:16:01 +00:00
feat(lancamentos): aprimora parcelamentos e protecoes
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TRANSACTION_CONDITIONS } from "@/features/transactions/lib/constants";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/shared/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -14,6 +20,61 @@ import { cn } from "@/shared/utils/ui";
|
||||
import { ConditionSelectContent } from "../../select-items";
|
||||
import type { ConditionSectionProps } from "./transaction-dialog-types";
|
||||
|
||||
function InlineStartInstallmentPicker({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
options: number[];
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = Number(value || "1");
|
||||
const selectedLabel =
|
||||
!Number.isNaN(selected) && selected > 0
|
||||
? `${selected}ª parcela`
|
||||
: "1ª parcela";
|
||||
const disabled = options.length === 0;
|
||||
|
||||
return (
|
||||
<div className="ml-1">
|
||||
<span className="text-xs text-muted-foreground">Começar em </span>
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-xs text-primary underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:text-muted-foreground disabled:hover:no-underline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedLabel}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 p-1" align="start">
|
||||
<div className="max-h-56 overflow-y-auto">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground",
|
||||
option === selected && "font-medium text-primary",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(String(option));
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{option}ª parcela
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConditionSection({
|
||||
formState,
|
||||
onFieldChange,
|
||||
@@ -37,11 +98,17 @@ export function ConditionSection({
|
||||
const installmentSummary =
|
||||
showInstallments &&
|
||||
formState.installmentCount &&
|
||||
amount &&
|
||||
!Number.isNaN(installmentCount) &&
|
||||
installmentCount > 0
|
||||
? getInstallmentLabel(installmentCount)
|
||||
: null;
|
||||
const startInstallmentOptions =
|
||||
showInstallments &&
|
||||
formState.installmentCount &&
|
||||
!Number.isNaN(installmentCount) &&
|
||||
installmentCount > 0
|
||||
? Array.from({ length: installmentCount }, (_, index) => index + 1)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
||||
@@ -96,6 +163,11 @@ export function ConditionSection({
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InlineStartInstallmentPicker
|
||||
value={formState.startInstallment}
|
||||
options={startInstallmentOptions}
|
||||
onChange={(value) => onFieldChange("startInstallment", value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface TransactionDialogProps {
|
||||
estabelecimentos: string[];
|
||||
transaction?: TransactionItem;
|
||||
defaultPeriod?: string;
|
||||
defaultAccountId?: string | null;
|
||||
defaultCardId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
|
||||
@@ -65,6 +65,7 @@ export function TransactionDialog({
|
||||
estabelecimentos,
|
||||
transaction,
|
||||
defaultPeriod,
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -88,6 +89,7 @@ export function TransactionDialog({
|
||||
|
||||
const [formState, setFormState] = useState<FormState>(() =>
|
||||
buildTransactionInitialState(transaction, defaultPayerId, defaultPeriod, {
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -112,6 +114,7 @@ export function TransactionDialog({
|
||||
defaultPayerId,
|
||||
defaultPeriod,
|
||||
{
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -151,6 +154,7 @@ export function TransactionDialog({
|
||||
transaction,
|
||||
defaultPayerId,
|
||||
defaultPeriod,
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -327,6 +331,12 @@ export function TransactionDialog({
|
||||
formState.condition === "Parcelado" && formState.installmentCount
|
||||
? Number(formState.installmentCount)
|
||||
: undefined,
|
||||
startInstallment:
|
||||
mode === "create" &&
|
||||
formState.condition === "Parcelado" &&
|
||||
formState.startInstallment
|
||||
? Number(formState.startInstallment)
|
||||
: undefined,
|
||||
recurrenceCount:
|
||||
formState.condition === "Recorrente" && formState.recurrenceCount
|
||||
? Number(formState.recurrenceCount)
|
||||
|
||||
@@ -63,6 +63,7 @@ interface TransactionsPageProps {
|
||||
categoryFilterOptions: TransactionFilterOption[];
|
||||
accountCardFilterOptions: AccountCardFilterOption[];
|
||||
selectedPeriod: string;
|
||||
defaultAccountId?: string | null;
|
||||
estabelecimentos: string[];
|
||||
allowCreate?: boolean;
|
||||
noteAsColumn?: boolean;
|
||||
@@ -96,6 +97,7 @@ export function TransactionsPage({
|
||||
categoryFilterOptions,
|
||||
accountCardFilterOptions,
|
||||
selectedPeriod,
|
||||
defaultAccountId,
|
||||
estabelecimentos,
|
||||
allowCreate = true,
|
||||
noteAsColumn = false,
|
||||
@@ -562,6 +564,7 @@ export function TransactionsPage({
|
||||
categoryOptions={categoryOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
defaultCardId={defaultCardId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCardSelection={lockCardSelection}
|
||||
@@ -585,6 +588,7 @@ export function TransactionsPage({
|
||||
categoryOptions={categoryOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
defaultCardId={defaultCardId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCardSelection={lockCardSelection}
|
||||
@@ -648,6 +652,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={transactionToCopy ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
/>
|
||||
|
||||
@@ -669,6 +674,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={transactionToImport ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
isImporting={true}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
/>
|
||||
@@ -697,6 +703,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={selectedTransaction ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
onBulkEditRequest={handleBulkEditRequest}
|
||||
onSplitEditRequest={handleSplitEditRequest}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
|
||||
import { RiCalendarCheckLine } from "@remixicon/react";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { useTransition } from "react";
|
||||
@@ -164,16 +164,14 @@ export function AnticipationCard({
|
||||
onClick={handleViewLancamento}
|
||||
disabled={isPending}
|
||||
>
|
||||
<RiEyeLine className="mr-2 size-4" />
|
||||
Ver Lançamento
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
{canCancel && (
|
||||
<ConfirmActionDialog
|
||||
trigger={
|
||||
<Button variant="destructive" size="sm" disabled={isPending}>
|
||||
<RiCloseLine className="mr-2 size-4" />
|
||||
Cancelar Antecipação
|
||||
Desfazer Antecipação
|
||||
</Button>
|
||||
}
|
||||
title="Cancelar antecipação?"
|
||||
|
||||
@@ -426,7 +426,7 @@ function buildColumns({
|
||||
const initial = displayName.charAt(0).toUpperCase() || "?";
|
||||
const content = (
|
||||
<>
|
||||
<Avatar className="size-7">
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={avatarSrc} alt={`Avatar de ${label}`} />
|
||||
<AvatarFallback className="text-xs font-medium uppercase">
|
||||
{initial}
|
||||
@@ -477,15 +477,21 @@ function buildColumns({
|
||||
const content = (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{logoSrc && (
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={`Logo de ${label}`}
|
||||
width={30}
|
||||
height={30}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={logoSrc} alt={`Logo de ${label}`} />
|
||||
<AvatarFallback className="text-xs font-medium uppercase">
|
||||
{label}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="truncate">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate underline-offset-2",
|
||||
isOwnData && href && "group-hover:underline",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -503,7 +509,7 @@ function buildColumns({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={href} className="hover:underline">
|
||||
<Link href={href} className="group">
|
||||
{content}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -654,14 +660,14 @@ function buildColumns({
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.categoriaName !== "Pagamentos" &&
|
||||
{!row.original.readonly &&
|
||||
row.original.userId === currentUserId && (
|
||||
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Copiar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.categoriaName !== "Pagamentos" &&
|
||||
{!row.original.readonly &&
|
||||
row.original.userId !== currentUserId && (
|
||||
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
|
||||
@@ -174,7 +174,7 @@ export function TransactionsTable({
|
||||
: getPaginationRowModel(),
|
||||
manualPagination: isServerPaginated,
|
||||
pageCount: serverPagination?.totalPages,
|
||||
enableRowSelection: true,
|
||||
enableRowSelection: (row) => !row.original.readonly,
|
||||
});
|
||||
|
||||
const rowModel = table.getRowModel();
|
||||
|
||||
Reference in New Issue
Block a user