mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-10 03:11:46 +00:00
feat(finance): refina fluxos de transacoes e pagadores
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
DAYS_IN_MONTH,
|
||||
DEFAULT_CARD_BRANDS,
|
||||
DEFAULT_CARD_STATUS,
|
||||
} from "./constants";
|
||||
} from "@/lib/cartoes/constants";
|
||||
import type { CardFormValues } from "./types";
|
||||
|
||||
interface AccountOption {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
RiPencilLine,
|
||||
} from "@remixicon/react";
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
import MoneyValues from "@/components/shared/money-values";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { resolveCardBrandAsset } from "@/lib/cartoes/brand-assets";
|
||||
import { resolveLogoSrc } from "@/lib/logo";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import MoneyValues from "../money-values";
|
||||
|
||||
interface CardItemProps {
|
||||
name: string;
|
||||
@@ -40,26 +41,6 @@ interface CardItemProps {
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const BRAND_ASSETS: Record<string, string> = {
|
||||
visa: "/bandeiras/visa.svg",
|
||||
mastercard: "/bandeiras/mastercard.svg",
|
||||
amex: "/bandeiras/amex.svg",
|
||||
american: "/bandeiras/amex.svg",
|
||||
elo: "/bandeiras/elo.svg",
|
||||
hipercard: "/bandeiras/hipercard.svg",
|
||||
hiper: "/bandeiras/hipercard.svg",
|
||||
};
|
||||
|
||||
const resolveBrandAsset = (brand: string) => {
|
||||
const normalized = brand.trim().toLowerCase();
|
||||
|
||||
const match = (
|
||||
Object.keys(BRAND_ASSETS) as Array<keyof typeof BRAND_ASSETS>
|
||||
).find((entry) => normalized.includes(entry));
|
||||
|
||||
return match ? BRAND_ASSETS[match] : null;
|
||||
};
|
||||
|
||||
const formatDay = (value: string) => value.padStart(2, "0");
|
||||
|
||||
export function CardItem({
|
||||
@@ -83,7 +64,7 @@ export function CardItem({
|
||||
const limitTotal = limit ?? null;
|
||||
const used =
|
||||
limitInUse ??
|
||||
(limitTotal !== null && limitAvailable !== null
|
||||
(limitTotal !== null && limitAvailable != null
|
||||
? Math.max(limitTotal - limitAvailable, 0)
|
||||
: limitTotal !== null
|
||||
? 0
|
||||
@@ -100,62 +81,38 @@ export function CardItem({
|
||||
? Math.min(Math.max((used / limitTotal) * 100, 0), 100)
|
||||
: 0;
|
||||
|
||||
const logoPath = useMemo(() => {
|
||||
if (!logo) {
|
||||
return null;
|
||||
}
|
||||
const logoPath = resolveLogoSrc(logo);
|
||||
const brandAsset = resolveCardBrandAsset(brand);
|
||||
const isInactive = status?.toLowerCase() === "inativo";
|
||||
const metrics =
|
||||
limitTotal === null || used === null || available === null
|
||||
? null
|
||||
: [
|
||||
{ label: "Limite Total", value: limitTotal },
|
||||
{ label: "Em uso", value: used },
|
||||
{ label: "Disponível", value: available },
|
||||
];
|
||||
|
||||
if (
|
||||
logo.startsWith("http://") ||
|
||||
logo.startsWith("https://") ||
|
||||
logo.startsWith("data:")
|
||||
) {
|
||||
return logo;
|
||||
}
|
||||
|
||||
return logo.startsWith("/") ? logo : `/logos/${logo}`;
|
||||
}, [logo]);
|
||||
|
||||
const brandAsset = useMemo(() => resolveBrandAsset(brand), [brand]);
|
||||
|
||||
const isInactive = useMemo(
|
||||
() => status?.toLowerCase() === "inativo",
|
||||
[status],
|
||||
);
|
||||
|
||||
const metrics = useMemo(() => {
|
||||
if (limitTotal === null) return null;
|
||||
|
||||
return [
|
||||
{ label: "Limite Total", value: limitTotal },
|
||||
{ label: "Em uso", value: used },
|
||||
{ label: "Disponível", value: available },
|
||||
];
|
||||
}, [available, limitTotal, used]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "editar",
|
||||
icon: <RiPencilLine className="size-4" aria-hidden />,
|
||||
onClick: onEdit,
|
||||
className: "text-primary",
|
||||
},
|
||||
{
|
||||
label: "ver fatura",
|
||||
icon: <RiFileList2Line className="size-4" aria-hidden />,
|
||||
onClick: onInvoice,
|
||||
className: "text-primary",
|
||||
},
|
||||
{
|
||||
label: "remover",
|
||||
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
||||
onClick: onRemove,
|
||||
className: "text-destructive",
|
||||
},
|
||||
],
|
||||
[onEdit, onInvoice, onRemove],
|
||||
);
|
||||
const actions = [
|
||||
{
|
||||
label: "editar",
|
||||
icon: <RiPencilLine className="size-4" aria-hidden />,
|
||||
onClick: onEdit,
|
||||
className: "text-primary",
|
||||
},
|
||||
{
|
||||
label: "ver fatura",
|
||||
icon: <RiFileList2Line className="size-4" aria-hidden />,
|
||||
onClick: onInvoice,
|
||||
className: "text-primary",
|
||||
},
|
||||
{
|
||||
label: "remover",
|
||||
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
||||
onClick: onRemove,
|
||||
className: "text-destructive",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col p-6 w-full">
|
||||
|
||||
@@ -2,35 +2,17 @@
|
||||
|
||||
import { RiBankLine } from "@remixicon/react";
|
||||
import Image from "next/image";
|
||||
import DotIcon from "@/components/dot-icon";
|
||||
import StatusDot from "@/components/shared/status-dot";
|
||||
import { resolveCardBrandLogoSrc } from "@/lib/cartoes/brand-assets";
|
||||
import { resolveLogoSrc } from "@/lib/logo";
|
||||
|
||||
type SelectItemContentProps = {
|
||||
label: string;
|
||||
logo?: string | null;
|
||||
};
|
||||
|
||||
const resolveLogoSrc = (logo: string | null) => {
|
||||
if (!logo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = logo.split("/").filter(Boolean).pop() ?? logo;
|
||||
return `/logos/${fileName}`;
|
||||
};
|
||||
|
||||
const getBrandLogo = (brand: string): string | null => {
|
||||
const brandMap: Record<string, string> = {
|
||||
Visa: "visa.png",
|
||||
Mastercard: "mastercard.png",
|
||||
Elo: "elo.png",
|
||||
};
|
||||
|
||||
return brandMap[brand] ?? null;
|
||||
};
|
||||
|
||||
export function BrandSelectContent({ label }: { label: string }) {
|
||||
const brandLogo = getBrandLogo(label);
|
||||
const logoSrc = brandLogo ? `/logos/${brandLogo}` : null;
|
||||
const logoSrc = resolveCardBrandLogoSrc(label);
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -55,7 +37,7 @@ export function StatusSelectContent({ label }: { label: string }) {
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<DotIcon
|
||||
<StatusDot
|
||||
color={isActive ? "bg-success" : "bg-slate-400 dark:bg-slate-500"}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -2,25 +2,27 @@
|
||||
|
||||
import { RiAddCircleLine, RiBankCard2Line } from "@remixicon/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { deleteCardAction } from "@/app/(dashboard)/cartoes/actions";
|
||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||
import { ConfirmActionDialog } from "@/components/shared/confirm-action-dialog";
|
||||
import { EmptyState } from "@/components/shared/empty-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Card as UiCard } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { CardDialog } from "./card-dialog";
|
||||
import { CardItem } from "./card-item";
|
||||
import type { Card as CreditCard } from "./types";
|
||||
|
||||
type AccountOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
};
|
||||
|
||||
interface CardsPageProps {
|
||||
cards: Card[];
|
||||
archivedCards: Card[];
|
||||
cards: CreditCard[];
|
||||
archivedCards: CreditCard[];
|
||||
accounts: AccountOption[];
|
||||
logoOptions: string[];
|
||||
}
|
||||
@@ -34,56 +36,54 @@ export function CardsPage({
|
||||
const router = useRouter();
|
||||
const [activeTab, setActiveTab] = useState("ativos");
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
|
||||
const [selectedCard, setSelectedCard] = useState<CreditCard | null>(null);
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [cardToRemove, setCardToRemove] = useState<Card | null>(null);
|
||||
const [cardToRemove, setCardToRemove] = useState<CreditCard | null>(null);
|
||||
|
||||
const sortCards = useCallback(
|
||||
(list: Card[]) =>
|
||||
[...list].sort((a, b) =>
|
||||
const orderedCards = useMemo(
|
||||
() =>
|
||||
[...cards].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
|
||||
),
|
||||
[],
|
||||
[cards],
|
||||
);
|
||||
|
||||
const orderedCards = useMemo(() => sortCards(cards), [cards, sortCards]);
|
||||
const orderedArchivedCards = useMemo(
|
||||
() => sortCards(archivedCards),
|
||||
[archivedCards, sortCards],
|
||||
() =>
|
||||
[...archivedCards].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
|
||||
),
|
||||
[archivedCards],
|
||||
);
|
||||
|
||||
const handleEdit = useCallback((card: Card) => {
|
||||
const handleEdit = (card: CreditCard) => {
|
||||
setSelectedCard(card);
|
||||
setEditOpen(true);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleEditOpenChange = useCallback((open: boolean) => {
|
||||
const handleEditOpenChange = (open: boolean) => {
|
||||
setEditOpen(open);
|
||||
if (!open) {
|
||||
setSelectedCard(null);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleRemoveRequest = useCallback((card: Card) => {
|
||||
const handleRemoveRequest = (card: CreditCard) => {
|
||||
setCardToRemove(card);
|
||||
setRemoveOpen(true);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleInvoice = useCallback(
|
||||
(card: Card) => {
|
||||
router.push(`/cartoes/${card.id}/fatura`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
const handleInvoice = (card: CreditCard) => {
|
||||
router.push(`/cartoes/${card.id}/fatura`);
|
||||
};
|
||||
|
||||
const handleRemoveOpenChange = useCallback((open: boolean) => {
|
||||
const handleRemoveOpenChange = (open: boolean) => {
|
||||
setRemoveOpen(open);
|
||||
if (!open) {
|
||||
setCardToRemove(null);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleRemoveConfirm = useCallback(async () => {
|
||||
const handleRemoveConfirm = async () => {
|
||||
if (!cardToRemove) {
|
||||
return;
|
||||
}
|
||||
@@ -97,16 +97,16 @@ export function CardsPage({
|
||||
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}, [cardToRemove]);
|
||||
};
|
||||
|
||||
const removeTitle = cardToRemove
|
||||
? `Remover cartão "${cardToRemove.name}"?`
|
||||
: "Remover cartão?";
|
||||
|
||||
const renderCardList = (list: Card[], isArchived: boolean) => {
|
||||
const renderCardList = (list: CreditCard[], isArchived: boolean) => {
|
||||
if (list.length === 0) {
|
||||
return (
|
||||
<Card className="flex w-full items-center justify-center py-12">
|
||||
<UiCard className="flex w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
media={<RiBankCard2Line className="size-6 text-primary" />}
|
||||
title={
|
||||
@@ -120,7 +120,7 @@ export function CardsPage({
|
||||
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</UiCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const DEFAULT_CARD_BRANDS = ["Visa", "Mastercard", "Elo"] as const;
|
||||
|
||||
export const DEFAULT_CARD_STATUS = ["Ativo", "Inativo"] as const;
|
||||
|
||||
export const DAYS_IN_MONTH = Array.from({ length: 31 }, (_, index) =>
|
||||
String(index + 1).padStart(2, "0"),
|
||||
);
|
||||
Reference in New Issue
Block a user