mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(lancamentos): refina filtros e tabela responsiva
This commit is contained in:
@@ -50,7 +50,9 @@ export function buildReadOnlyOptionSets(
|
|||||||
categoriaOptionsMap.set(item.categoryId, {
|
categoriaOptionsMap.set(item.categoryId, {
|
||||||
value: item.categoryId,
|
value: item.categoryId,
|
||||||
label: normalizeOptionLabel(item.categoriaName, "Category"),
|
label: normalizeOptionLabel(item.categoriaName, "Category"),
|
||||||
|
group: item.categoriaType,
|
||||||
slug: item.categoryId,
|
slug: item.categoryId,
|
||||||
|
icon: item.categoriaIcon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -67,6 +69,8 @@ export function buildReadOnlyOptionSets(
|
|||||||
(option) => ({
|
(option) => ({
|
||||||
slug: option.value,
|
slug: option.value,
|
||||||
label: option.label,
|
label: option.label,
|
||||||
|
type: option.group,
|
||||||
|
icon: option.icon,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RiCheckLine,
|
||||||
|
RiDeleteBin5Line,
|
||||||
|
RiFileCopyLine,
|
||||||
|
RiFileList2Line,
|
||||||
|
RiHistoryLine,
|
||||||
|
RiMoreFill,
|
||||||
|
RiPencilLine,
|
||||||
|
RiRefundLine,
|
||||||
|
RiTimeLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/shared/components/ui/dropdown-menu";
|
||||||
|
import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||||
|
import type { TransactionItem } from "../types";
|
||||||
|
|
||||||
|
type TransactionActionsMenuProps = {
|
||||||
|
item: TransactionItem;
|
||||||
|
currentUserId: string;
|
||||||
|
onEdit?: (item: TransactionItem) => void;
|
||||||
|
onCopy?: (item: TransactionItem) => void;
|
||||||
|
onImport?: (item: TransactionItem) => void;
|
||||||
|
onConfirmDelete?: (item: TransactionItem) => void;
|
||||||
|
onViewDetails?: (item: TransactionItem) => void;
|
||||||
|
onRefund?: (item: TransactionItem) => void;
|
||||||
|
onAnticipate?: (item: TransactionItem) => void;
|
||||||
|
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TransactionActionsMenu({
|
||||||
|
item,
|
||||||
|
currentUserId,
|
||||||
|
onEdit,
|
||||||
|
onCopy,
|
||||||
|
onImport,
|
||||||
|
onConfirmDelete,
|
||||||
|
onViewDetails,
|
||||||
|
onRefund,
|
||||||
|
onAnticipate,
|
||||||
|
onViewAnticipationHistory,
|
||||||
|
}: TransactionActionsMenuProps) {
|
||||||
|
const isOwnData = item.userId === currentUserId;
|
||||||
|
const canRefund =
|
||||||
|
isOwnData &&
|
||||||
|
item.transactionType === "Despesa" &&
|
||||||
|
item.condition === "À vista" &&
|
||||||
|
!item.splitGroupId &&
|
||||||
|
!item.readonly &&
|
||||||
|
!item.note?.startsWith(REFUND_NOTE_PREFIX);
|
||||||
|
const showInstallmentActions =
|
||||||
|
isOwnData && item.condition === "Parcelado" && item.seriesId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon-sm">
|
||||||
|
<RiMoreFill className="size-4" aria-hidden />
|
||||||
|
<span className="sr-only">Abrir ações do lançamento</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-44">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => onViewDetails?.(item)}
|
||||||
|
disabled={!onViewDetails}
|
||||||
|
>
|
||||||
|
<RiFileList2Line className="size-4" aria-hidden />
|
||||||
|
Detalhes
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{isOwnData ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => onEdit?.(item)}
|
||||||
|
disabled={item.readonly || !onEdit}
|
||||||
|
>
|
||||||
|
<RiPencilLine className="size-4" aria-hidden />
|
||||||
|
Editar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!item.readonly && isOwnData ? (
|
||||||
|
<DropdownMenuItem onSelect={() => onCopy?.(item)} disabled={!onCopy}>
|
||||||
|
<RiFileCopyLine className="size-4" aria-hidden />
|
||||||
|
Copiar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!item.readonly && !isOwnData ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => onImport?.(item)}
|
||||||
|
disabled={!onImport}
|
||||||
|
>
|
||||||
|
<RiFileCopyLine className="size-4" aria-hidden />
|
||||||
|
Importar para Minha Conta
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{canRefund ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => onRefund?.(item)}
|
||||||
|
disabled={!onRefund}
|
||||||
|
>
|
||||||
|
<RiRefundLine className="size-4" aria-hidden />
|
||||||
|
Reembolso
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isOwnData ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onSelect={() => onConfirmDelete?.(item)}
|
||||||
|
disabled={item.readonly || !onConfirmDelete}
|
||||||
|
>
|
||||||
|
<RiDeleteBin5Line className="size-4" aria-hidden />
|
||||||
|
Remover
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showInstallmentActions ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{!item.isAnticipated && onAnticipate ? (
|
||||||
|
<DropdownMenuItem onSelect={() => onAnticipate(item)}>
|
||||||
|
<RiTimeLine className="size-4" aria-hidden />
|
||||||
|
Antecipar Parcelas
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{onViewAnticipationHistory ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => onViewAnticipationHistory(item)}
|
||||||
|
>
|
||||||
|
<RiHistoryLine className="size-4" aria-hidden />
|
||||||
|
Histórico de Antecipações
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{item.isAnticipated ? (
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
<RiCheckLine className="size-4 text-success" aria-hidden />
|
||||||
|
Parcela Antecipada
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RiBankCard2Line,
|
||||||
|
RiCheckboxBlankCircleLine,
|
||||||
|
RiCheckboxCircleFill,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import {
|
||||||
|
CREDIT_CARD_PAYMENT_METHOD,
|
||||||
|
SETTLEABLE_PAYMENT_METHODS,
|
||||||
|
} from "@/features/transactions/lib/constants";
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { Spinner } from "@/shared/components/ui/spinner";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/shared/components/ui/tooltip";
|
||||||
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
import type { TransactionItem } from "../types";
|
||||||
|
|
||||||
|
type TransactionSettlementButtonProps = {
|
||||||
|
item: TransactionItem;
|
||||||
|
isLoading: boolean;
|
||||||
|
onToggle?: (item: TransactionItem) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TransactionSettlementButton({
|
||||||
|
item,
|
||||||
|
isLoading,
|
||||||
|
onToggle,
|
||||||
|
}: TransactionSettlementButtonProps) {
|
||||||
|
const isCreditCard = item.paymentMethod === CREDIT_CARD_PAYMENT_METHOD;
|
||||||
|
const canToggleSettlement = (
|
||||||
|
SETTLEABLE_PAYMENT_METHODS as readonly string[]
|
||||||
|
).includes(item.paymentMethod);
|
||||||
|
|
||||||
|
if (!canToggleSettlement && !isCreditCard) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreditCard) {
|
||||||
|
const invoicePaid = Boolean(item.isSettled);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
disabled
|
||||||
|
className={cn(
|
||||||
|
"transition-colors",
|
||||||
|
invoicePaid
|
||||||
|
? "bg-success/10 text-success"
|
||||||
|
: "text-muted-foreground/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{invoicePaid ? (
|
||||||
|
<RiCheckboxCircleFill className="size-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<RiBankCard2Line className="size-4" aria-hidden />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">
|
||||||
|
{invoicePaid
|
||||||
|
? "Fatura paga"
|
||||||
|
: "Lançamento de cartão de crédito"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-48 text-center">
|
||||||
|
{invoicePaid
|
||||||
|
? "Fatura paga"
|
||||||
|
: "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settled = Boolean(item.isSettled);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onToggle?.(item)}
|
||||||
|
disabled={isLoading || item.readonly}
|
||||||
|
className={cn(
|
||||||
|
"transition-colors",
|
||||||
|
settled
|
||||||
|
? "bg-success/10 text-success hover:bg-success/20 hover:text-success"
|
||||||
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
) : settled ? (
|
||||||
|
<RiCheckboxCircleFill className="size-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<RiCheckboxBlankCircleLine className="size-4" aria-hidden />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">
|
||||||
|
{settled ? "Desfazer pagamento" : "Marcar como pago"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{settled ? "Desfazer pagamento" : "Marcar como pago"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,28 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
RiAttachment2,
|
RiAttachment2,
|
||||||
RiBankCard2Line,
|
|
||||||
RiChat1Line,
|
RiChat1Line,
|
||||||
RiCheckboxBlankCircleLine,
|
|
||||||
RiCheckboxCircleFill,
|
|
||||||
RiCheckLine,
|
|
||||||
RiDeleteBin5Line,
|
|
||||||
RiFileCopyLine,
|
|
||||||
RiFileList2Line,
|
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiHistoryLine,
|
|
||||||
RiMoreFill,
|
|
||||||
RiPencilLine,
|
|
||||||
RiRefundLine,
|
|
||||||
RiTimeLine,
|
RiTimeLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { DEFAULT_TRANSACTIONS_COLUMN_ORDER } from "@/features/transactions/lib/column-order";
|
import { DEFAULT_TRANSACTIONS_COLUMN_ORDER } from "@/features/transactions/lib/column-order";
|
||||||
import {
|
|
||||||
CREDIT_CARD_PAYMENT_METHOD,
|
|
||||||
SETTLEABLE_PAYMENT_METHODS,
|
|
||||||
} from "@/features/transactions/lib/constants";
|
|
||||||
import {
|
import {
|
||||||
CategoryIconBadge,
|
CategoryIconBadge,
|
||||||
EstablishmentLogo,
|
EstablishmentLogo,
|
||||||
@@ -35,28 +20,20 @@ import {
|
|||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from "@/shared/components/ui/avatar";
|
} from "@/shared/components/ui/avatar";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
|
||||||
import { Checkbox } from "@/shared/components/ui/checkbox";
|
import { Checkbox } from "@/shared/components/ui/checkbox";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/shared/components/ui/dropdown-menu";
|
|
||||||
import { Spinner } from "@/shared/components/ui/spinner";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/shared/components/ui/tooltip";
|
} from "@/shared/components/ui/tooltip";
|
||||||
import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
|
||||||
import { resolveLogoSrc } from "@/shared/lib/logo";
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
||||||
import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
||||||
import { formatDate } from "@/shared/utils/date";
|
import { formatDate } from "@/shared/utils/date";
|
||||||
import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons";
|
import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons";
|
||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
import type { TransactionItem } from "../types";
|
import type { TransactionItem } from "../types";
|
||||||
|
import { TransactionActionsMenu } from "./transaction-actions-menu";
|
||||||
|
import { TransactionSettlementButton } from "./transaction-settlement-button";
|
||||||
|
|
||||||
type BuildColumnsArgs = {
|
type BuildColumnsArgs = {
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
@@ -551,195 +528,23 @@ function buildColumns({
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{(() => {
|
<TransactionSettlementButton
|
||||||
const paymentMethod = row.original.paymentMethod;
|
item={row.original}
|
||||||
const isCreditCard = paymentMethod === CREDIT_CARD_PAYMENT_METHOD;
|
isLoading={isSettlementLoading(row.original.id)}
|
||||||
const canToggleSettlement = (
|
onToggle={handleToggleSettlement}
|
||||||
SETTLEABLE_PAYMENT_METHODS as readonly string[]
|
/>
|
||||||
).includes(paymentMethod);
|
<TransactionActionsMenu
|
||||||
|
item={row.original}
|
||||||
if (!canToggleSettlement && !isCreditCard) return null;
|
currentUserId={currentUserId}
|
||||||
|
onEdit={handleEdit}
|
||||||
if (isCreditCard) {
|
onCopy={handleCopy}
|
||||||
const invoicePaid = Boolean(row.original.isSettled);
|
onImport={handleImport}
|
||||||
return (
|
onConfirmDelete={handleConfirmDelete}
|
||||||
<Tooltip>
|
onViewDetails={handleViewDetails}
|
||||||
<TooltipTrigger asChild>
|
onRefund={handleRefund}
|
||||||
<span className="inline-flex">
|
onAnticipate={handleAnticipate}
|
||||||
<Button
|
onViewAnticipationHistory={handleViewAnticipationHistory}
|
||||||
variant="ghost"
|
/>
|
||||||
size="icon-sm"
|
|
||||||
disabled
|
|
||||||
className={cn(
|
|
||||||
"transition-colors",
|
|
||||||
invoicePaid
|
|
||||||
? "bg-success/10 text-success"
|
|
||||||
: "text-muted-foreground/30",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{invoicePaid ? (
|
|
||||||
<RiCheckboxCircleFill className="size-4" />
|
|
||||||
) : (
|
|
||||||
<RiBankCard2Line className="size-4" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">
|
|
||||||
{invoicePaid
|
|
||||||
? "Fatura paga"
|
|
||||||
: "Lançamento de cartão de crédito"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-48 text-center">
|
|
||||||
{invoicePaid
|
|
||||||
? "Fatura paga"
|
|
||||||
: "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const readOnly = row.original.readonly;
|
|
||||||
const loading = isSettlementLoading(row.original.id);
|
|
||||||
const settled = Boolean(row.original.isSettled);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={() => handleToggleSettlement(row.original)}
|
|
||||||
disabled={loading || readOnly}
|
|
||||||
className={cn(
|
|
||||||
"transition-colors",
|
|
||||||
settled
|
|
||||||
? "bg-success/10 text-success hover:bg-success/20 hover:text-success"
|
|
||||||
: "text-muted-foreground hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<Spinner className="size-4" />
|
|
||||||
) : settled ? (
|
|
||||||
<RiCheckboxCircleFill className="size-4" />
|
|
||||||
) : (
|
|
||||||
<RiCheckboxBlankCircleLine className="size-4" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">
|
|
||||||
{settled ? "Desfazer pagamento" : "Marcar como pago"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">
|
|
||||||
{settled ? "Desfazer pagamento" : "Marcar como pago"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon-sm">
|
|
||||||
<RiMoreFill className="size-4" />
|
|
||||||
<span className="sr-only">Abrir ações do lançamento</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-44">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => handleViewDetails(row.original)}
|
|
||||||
>
|
|
||||||
<RiFileList2Line className="size-4" />
|
|
||||||
Detalhes
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{row.original.userId === currentUserId && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => handleEdit(row.original)}
|
|
||||||
disabled={row.original.readonly}
|
|
||||||
>
|
|
||||||
<RiPencilLine className="size-4" />
|
|
||||||
Editar
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{!row.original.readonly &&
|
|
||||||
row.original.userId === currentUserId && (
|
|
||||||
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
|
||||||
<RiFileCopyLine className="size-4" />
|
|
||||||
Copiar
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{!row.original.readonly &&
|
|
||||||
row.original.userId !== currentUserId && (
|
|
||||||
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
|
|
||||||
<RiFileCopyLine className="size-4" />
|
|
||||||
Importar para Minha Conta
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{(() => {
|
|
||||||
const item = row.original;
|
|
||||||
const canRefund =
|
|
||||||
item.userId === currentUserId &&
|
|
||||||
item.transactionType === "Despesa" &&
|
|
||||||
item.condition === "À vista" &&
|
|
||||||
!item.splitGroupId &&
|
|
||||||
!item.readonly &&
|
|
||||||
!item.note?.startsWith(REFUND_NOTE_PREFIX);
|
|
||||||
|
|
||||||
if (!canRefund) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem onSelect={() => handleRefund(item)}>
|
|
||||||
<RiRefundLine className="size-4" />
|
|
||||||
Reembolso
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
{row.original.userId === currentUserId && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
variant="destructive"
|
|
||||||
onSelect={() => handleConfirmDelete(row.original)}
|
|
||||||
disabled={row.original.readonly}
|
|
||||||
>
|
|
||||||
<RiDeleteBin5Line className="size-4" />
|
|
||||||
Remover
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{row.original.userId === currentUserId &&
|
|
||||||
row.original.condition === "Parcelado" &&
|
|
||||||
row.original.seriesId && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
{!row.original.isAnticipated && onAnticipate && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => handleAnticipate(row.original)}
|
|
||||||
>
|
|
||||||
<RiTimeLine className="size-4" />
|
|
||||||
Antecipar Parcelas
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{onViewAnticipationHistory && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() =>
|
|
||||||
handleViewAnticipationHistory(row.original)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RiHistoryLine className="size-4" />
|
|
||||||
Histórico de Antecipações
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{row.original.isAnticipated && (
|
|
||||||
<DropdownMenuItem disabled>
|
|
||||||
<RiCheckLine className="size-4 text-success" />
|
|
||||||
Parcela Antecipada
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
} from "@/shared/components/ui/select";
|
} from "@/shared/components/ui/select";
|
||||||
import { Separator } from "@/shared/components/ui/separator";
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
|
import { Spinner } from "@/shared/components/ui/spinner";
|
||||||
import { Switch } from "@/shared/components/ui/switch";
|
import { Switch } from "@/shared/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
ToggleGroup,
|
ToggleGroup,
|
||||||
@@ -193,6 +194,16 @@ type MultiOption = {
|
|||||||
render?: ReactNode;
|
render?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCategoryFilterGroup = (type?: string | null) => {
|
||||||
|
if (type === "receita") {
|
||||||
|
return "Receitas";
|
||||||
|
}
|
||||||
|
if (type === "despesa") {
|
||||||
|
return "Despesas";
|
||||||
|
}
|
||||||
|
return "Outras";
|
||||||
|
};
|
||||||
|
|
||||||
interface MultiSelectFilterProps {
|
interface MultiSelectFilterProps {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
options: MultiOption[];
|
options: MultiOption[];
|
||||||
@@ -290,7 +301,10 @@ function MultiSelectFilter({
|
|||||||
>
|
>
|
||||||
{triggerLabel}
|
{triggerLabel}
|
||||||
</span>
|
</span>
|
||||||
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
|
<RiExpandUpDownLine
|
||||||
|
className="ml-2 size-4 shrink-0 opacity-50"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="start" className="w-[260px] p-0">
|
<PopoverContent align="start" className="w-[260px] p-0">
|
||||||
@@ -331,7 +345,10 @@ function MultiSelectFilter({
|
|||||||
{option.render ?? option.label}
|
{option.render ?? option.label}
|
||||||
</span>
|
</span>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
<RiCheckLine className="ml-auto size-4 shrink-0" />
|
<RiCheckLine
|
||||||
|
className="ml-auto size-4 shrink-0"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
@@ -518,6 +535,7 @@ export function TransactionsFilters({
|
|||||||
categoryOptions.map((option) => ({
|
categoryOptions.map((option) => ({
|
||||||
value: option.slug,
|
value: option.slug,
|
||||||
label: option.label,
|
label: option.label,
|
||||||
|
group: getCategoryFilterGroup(option.type),
|
||||||
render: (
|
render: (
|
||||||
<CategorySelectContent label={option.label} icon={option.icon} />
|
<CategorySelectContent label={option.label} icon={option.icon} />
|
||||||
),
|
),
|
||||||
@@ -585,6 +603,7 @@ export function TransactionsFilters({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-busy={isPending}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center",
|
"flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center",
|
||||||
className,
|
className,
|
||||||
@@ -608,7 +627,7 @@ export function TransactionsFilters({
|
|||||||
aria-label="Limpar busca"
|
aria-label="Limpar busca"
|
||||||
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="size-4" />
|
<RiCloseLine className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -630,12 +649,19 @@ export function TransactionsFilters({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
|
className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
|
||||||
aria-label="Abrir filtros"
|
aria-label={isPending ? "Aplicando filtros" : "Abrir filtros"}
|
||||||
>
|
>
|
||||||
<RiFilterLine className="size-4" />
|
{isPending ? (
|
||||||
Filtros
|
<Spinner className="size-4" role="presentation" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<RiFilterLine className="size-4" aria-hidden />
|
||||||
|
)}
|
||||||
|
{isPending ? "Aplicando..." : "Filtros"}
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<span className="absolute -top-1 -right-1 size-3 rounded-full bg-primary" />
|
<span
|
||||||
|
className="absolute -top-1 -right-1 size-3 rounded-full bg-primary"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
@@ -649,7 +675,7 @@ export function TransactionsFilters({
|
|||||||
aria-label="Limpar filtros"
|
aria-label="Limpar filtros"
|
||||||
className="text-xs text-muted-foreground hover:text-foreground h-9 px-2"
|
className="text-xs text-muted-foreground hover:text-foreground h-9 px-2"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="size-3.5" />
|
<RiCloseLine className="size-3.5" aria-hidden />
|
||||||
Limpar
|
Limpar
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -746,6 +772,7 @@ export function TransactionsFilters({
|
|||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
searchable
|
searchable
|
||||||
searchPlaceholder="Buscar categoria..."
|
searchPlaceholder="Buscar categoria..."
|
||||||
|
groupOrder={["Despesas", "Receitas", "Outras"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -775,7 +802,7 @@ export function TransactionsFilters({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<label className="text-xs font-medium text-muted-foreground">
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
Período
|
Intervalo de datas
|
||||||
</label>
|
</label>
|
||||||
{hasDateRangeFilter ? (
|
{hasDateRangeFilter ? (
|
||||||
<button
|
<button
|
||||||
@@ -932,7 +959,11 @@ export function TransactionsFilters({
|
|||||||
|
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
<div className="flex items-center justify-between gap-3 rounded-md border border-dashed px-3 py-2">
|
<div className="flex items-center justify-between gap-3 rounded-md border border-dashed px-3 py-2">
|
||||||
<span className="text-xs text-muted-foreground">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
|
<span
|
||||||
|
className="text-xs text-muted-foreground"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
{hasActiveFilters
|
{hasActiveFilters
|
||||||
? `${activeFilterCount} ${
|
? `${activeFilterCount} ${
|
||||||
activeFilterCount === 1
|
activeFilterCount === 1
|
||||||
@@ -941,6 +972,20 @@ export function TransactionsFilters({
|
|||||||
}`
|
}`
|
||||||
: "Nenhum filtro ativo"}
|
: "Nenhum filtro ativo"}
|
||||||
</span>
|
</span>
|
||||||
|
{isPending ? (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<Spinner
|
||||||
|
className="size-3"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
Aplicando filtros...
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RiArrowLeftRightLine,
|
||||||
|
RiArrowRightDownLine,
|
||||||
|
RiArrowRightUpLine,
|
||||||
|
RiAttachment2,
|
||||||
|
RiCalendarEventLine,
|
||||||
|
RiChat1Line,
|
||||||
|
RiGroupLine,
|
||||||
|
RiTimeLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
|
||||||
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/shared/components/ui/tooltip";
|
||||||
|
import { formatDate } from "@/shared/utils/date";
|
||||||
|
import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons";
|
||||||
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
import type { TransactionItem } from "../types";
|
||||||
|
import { TransactionActionsMenu } from "./transaction-actions-menu";
|
||||||
|
import { TransactionSettlementButton } from "./transaction-settlement-button";
|
||||||
|
|
||||||
|
type TransactionsMobileListProps = {
|
||||||
|
data: TransactionItem[];
|
||||||
|
currentUserId: string;
|
||||||
|
onEdit?: (item: TransactionItem) => void;
|
||||||
|
onCopy?: (item: TransactionItem) => void;
|
||||||
|
onImport?: (item: TransactionItem) => void;
|
||||||
|
onConfirmDelete?: (item: TransactionItem) => void;
|
||||||
|
onViewDetails?: (item: TransactionItem) => void;
|
||||||
|
onRefund?: (item: TransactionItem) => void;
|
||||||
|
onToggleSettlement?: (item: TransactionItem) => void;
|
||||||
|
onAnticipate?: (item: TransactionItem) => void;
|
||||||
|
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||||
|
isSettlementLoading: (id: string) => boolean;
|
||||||
|
showActions?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TransactionsMobileList({
|
||||||
|
data,
|
||||||
|
currentUserId,
|
||||||
|
onEdit,
|
||||||
|
onCopy,
|
||||||
|
onImport,
|
||||||
|
onConfirmDelete,
|
||||||
|
onViewDetails,
|
||||||
|
onRefund,
|
||||||
|
onToggleSettlement,
|
||||||
|
onAnticipate,
|
||||||
|
onViewAnticipationHistory,
|
||||||
|
isSettlementLoading,
|
||||||
|
showActions = true,
|
||||||
|
}: TransactionsMobileListProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 md:hidden">
|
||||||
|
{data.map((item) => (
|
||||||
|
<TransactionMobileCard
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onImport={onImport}
|
||||||
|
onConfirmDelete={onConfirmDelete}
|
||||||
|
onViewDetails={onViewDetails}
|
||||||
|
onRefund={onRefund}
|
||||||
|
onToggleSettlement={onToggleSettlement}
|
||||||
|
onAnticipate={onAnticipate}
|
||||||
|
onViewAnticipationHistory={onViewAnticipationHistory}
|
||||||
|
isSettlementLoading={isSettlementLoading}
|
||||||
|
showActions={showActions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionMobileCardProps = Omit<TransactionsMobileListProps, "data"> & {
|
||||||
|
item: TransactionItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TransactionMobileCard({
|
||||||
|
item,
|
||||||
|
currentUserId,
|
||||||
|
onEdit,
|
||||||
|
onCopy,
|
||||||
|
onImport,
|
||||||
|
onConfirmDelete,
|
||||||
|
onViewDetails,
|
||||||
|
onRefund,
|
||||||
|
onToggleSettlement,
|
||||||
|
onAnticipate,
|
||||||
|
onViewAnticipationHistory,
|
||||||
|
isSettlementLoading,
|
||||||
|
showActions = true,
|
||||||
|
}: TransactionMobileCardProps) {
|
||||||
|
const installmentBadge =
|
||||||
|
item.currentInstallment && item.installmentCount
|
||||||
|
? `${item.currentInstallment} de ${item.installmentCount}`
|
||||||
|
: null;
|
||||||
|
const isBoleto = item.paymentMethod === "Boleto" && item.dueDate;
|
||||||
|
const dueDateLabel =
|
||||||
|
isBoleto && item.dueDate ? `Venc. ${formatDate(item.dueDate)}` : null;
|
||||||
|
const hasNote = Boolean(item.note?.trim().length);
|
||||||
|
const isLastInstallment =
|
||||||
|
item.currentInstallment === item.installmentCount &&
|
||||||
|
item.installmentCount &&
|
||||||
|
item.installmentCount > 1;
|
||||||
|
const isReceita = item.transactionType === "Receita";
|
||||||
|
const isTransfer = item.transactionType === "Transferência";
|
||||||
|
const isIncomingTransfer = isTransfer && Number(item.amount) > 0;
|
||||||
|
const payerLabel = item.pagadorName?.trim() || "Sem pessoa";
|
||||||
|
const payerDisplayName = payerLabel.split(/\s+/)[0] ?? payerLabel;
|
||||||
|
const paymentMethodLabel =
|
||||||
|
item.paymentMethod === "Transferência bancária"
|
||||||
|
? "Transf. bancária"
|
||||||
|
: item.paymentMethod;
|
||||||
|
|
||||||
|
const type =
|
||||||
|
item.categoriaName === "Saldo inicial"
|
||||||
|
? "Saldo inicial"
|
||||||
|
: item.transactionType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border bg-card px-3 py-2.5 shadow-xs",
|
||||||
|
item.paymentMethod === "Boleto" &&
|
||||||
|
item.dueDate &&
|
||||||
|
!item.isSettled &&
|
||||||
|
new Date(item.dueDate) < new Date() &&
|
||||||
|
"border-destructive/20 bg-destructive/3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<EstablishmentLogo name={item.name} size={34} />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex min-w-0 items-start justify-between gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="truncate text-sm font-semibold leading-tight">
|
||||||
|
{item.name}
|
||||||
|
</h3>
|
||||||
|
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<RiCalendarEventLine className="size-3.5" aria-hidden />
|
||||||
|
{formatDate(item.purchaseDate)}
|
||||||
|
</span>
|
||||||
|
{dueDateLabel ? (
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{dueDateLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className="truncate">{payerDisplayName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 text-right">
|
||||||
|
<MoneyValues
|
||||||
|
amount={item.amount}
|
||||||
|
showPositiveSign={isReceita || isIncomingTransfer}
|
||||||
|
className={cn(
|
||||||
|
"whitespace-nowrap text-sm font-semibold",
|
||||||
|
isReceita ? "text-success" : "text-foreground",
|
||||||
|
isTransfer && "text-info",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-2">
|
||||||
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||||
|
<IconBadge
|
||||||
|
label={type}
|
||||||
|
compact
|
||||||
|
className={getTransactionTypeIconClassName(type)}
|
||||||
|
>
|
||||||
|
{getTransactionTypeIcon(type)}
|
||||||
|
</IconBadge>
|
||||||
|
<IconBadge label={paymentMethodLabel} compact>
|
||||||
|
{getPaymentMethodIcon(item.paymentMethod)}
|
||||||
|
</IconBadge>
|
||||||
|
<IconBadge label={item.condition} compact>
|
||||||
|
{getConditionIcon(item.condition)}
|
||||||
|
</IconBadge>
|
||||||
|
{installmentBadge ? (
|
||||||
|
<Badge variant="outline" className="px-1.5 text-xs">
|
||||||
|
{installmentBadge}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{item.isDivided ? (
|
||||||
|
<IconBadge label="Dividido entre pessoas" compact>
|
||||||
|
<RiGroupLine className="size-3.5" aria-hidden />
|
||||||
|
</IconBadge>
|
||||||
|
) : null}
|
||||||
|
{isLastInstallment ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-flex size-6 items-center justify-center rounded-full border text-muted-foreground">
|
||||||
|
<Image
|
||||||
|
src="/icons/party.svg"
|
||||||
|
alt=""
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
className="size-3.5"
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Última parcela</span>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Última parcela!</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
{item.isAnticipated ? (
|
||||||
|
<IconBadge label="Parcela antecipada" compact>
|
||||||
|
<RiTimeLine className="size-3.5" aria-hidden />
|
||||||
|
</IconBadge>
|
||||||
|
) : null}
|
||||||
|
{hasNote ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-flex size-6 items-center justify-center rounded-full border text-muted-foreground">
|
||||||
|
<RiChat1Line className="size-3.5" aria-hidden />
|
||||||
|
<span className="sr-only">Ver anotação</span>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
className="max-w-xs whitespace-pre-line"
|
||||||
|
>
|
||||||
|
{item.note}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
{item.hasAttachments ? (
|
||||||
|
<IconBadge label="Possui anexos" compact>
|
||||||
|
<RiAttachment2 className="size-3.5" aria-hidden />
|
||||||
|
</IconBadge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{showActions ? (
|
||||||
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
<TransactionSettlementButton
|
||||||
|
item={item}
|
||||||
|
isLoading={isSettlementLoading(item.id)}
|
||||||
|
onToggle={onToggleSettlement}
|
||||||
|
/>
|
||||||
|
<TransactionActionsMenu
|
||||||
|
item={item}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onImport={onImport}
|
||||||
|
onConfirmDelete={onConfirmDelete}
|
||||||
|
onViewDetails={onViewDetails}
|
||||||
|
onRefund={onRefund}
|
||||||
|
onAnticipate={onAnticipate}
|
||||||
|
onViewAnticipationHistory={onViewAnticipationHistory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconBadge({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
compact = false,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: ReactNode;
|
||||||
|
compact?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
if (!children) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center rounded-full border text-xs text-muted-foreground",
|
||||||
|
compact ? "size-6 justify-center" : "gap-1 px-2 py-0.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{compact ? <span className="sr-only">{label}</span> : label}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransactionTypeIcon(type: string) {
|
||||||
|
if (type === "Receita" || type === "Saldo inicial") {
|
||||||
|
return <RiArrowRightDownLine className="size-3.5" aria-hidden />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "Transferência") {
|
||||||
|
return <RiArrowLeftRightLine className="size-3.5" aria-hidden />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RiArrowRightUpLine className="size-3.5" aria-hidden />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransactionTypeIconClassName(type: string) {
|
||||||
|
if (type === "Receita" || type === "Saldo inicial") {
|
||||||
|
return "border-success/30 bg-success/5 text-success";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "Transferência") {
|
||||||
|
return "border-info/30 bg-info/5 text-info";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "border-destructive/30 bg-destructive/5 text-destructive";
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import type {
|
|||||||
import { TransactionsBulkBar } from "./transactions-bulk-bar";
|
import { TransactionsBulkBar } from "./transactions-bulk-bar";
|
||||||
import { getTransactionColumns } from "./transactions-columns";
|
import { getTransactionColumns } from "./transactions-columns";
|
||||||
import { TransactionsFilters } from "./transactions-filters";
|
import { TransactionsFilters } from "./transactions-filters";
|
||||||
|
import { TransactionsMobileList } from "./transactions-mobile-list";
|
||||||
import { TransactionsPagination } from "./transactions-pagination";
|
import { TransactionsPagination } from "./transactions-pagination";
|
||||||
|
|
||||||
type TransactionsTableProps = {
|
type TransactionsTableProps = {
|
||||||
@@ -349,7 +350,23 @@ export function TransactionsTable({
|
|||||||
<CardContent className="px-2 py-4 sm:px-4">
|
<CardContent className="px-2 py-4 sm:px-4">
|
||||||
{hasRows ? (
|
{hasRows ? (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<TransactionsMobileList
|
||||||
|
data={rowModel.rows.map((row) => row.original)}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onImport={onImport}
|
||||||
|
onConfirmDelete={onConfirmDelete}
|
||||||
|
onViewDetails={onViewDetails}
|
||||||
|
onRefund={onRefund}
|
||||||
|
onToggleSettlement={onToggleSettlement}
|
||||||
|
onAnticipate={onAnticipate}
|
||||||
|
onViewAnticipationHistory={onViewAnticipationHistory}
|
||||||
|
isSettlementLoading={isSettlementLoading ?? (() => false)}
|
||||||
|
showActions={showActions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="hidden overflow-x-auto md:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export type TransactionFilterOption = {
|
|||||||
label: string;
|
label: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
|
type?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountCardFilterOption = TransactionFilterOption & {
|
export type AccountCardFilterOption = TransactionFilterOption & {
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ export type SlugMaps = {
|
|||||||
type FilterOption = {
|
type FilterOption = {
|
||||||
slug: string;
|
slug: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
icon?: string | null;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
type?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AccountCardFilterOption = FilterOption & {
|
type AccountCardFilterOption = FilterOption & {
|
||||||
@@ -686,7 +689,12 @@ export const buildOptionSets = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const categoryFilterOptions = sortByLabel(
|
const categoryFilterOptions = sortByLabel(
|
||||||
categoryFiltersRaw.map(({ slug, label, icon }) => ({ slug, label, icon })),
|
categoryFiltersRaw.map(({ slug, label, type, icon }) => ({
|
||||||
|
slug,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
icon,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const accountCardFilterOptions = sortByLabel(
|
const accountCardFilterOptions = sortByLabel(
|
||||||
|
|||||||
Reference in New Issue
Block a user