mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-15 13:01:47 +00:00
feat(reports): polimento visual e prefetch de logos na análise de parcelas
This commit is contained in:
@@ -1,16 +1,24 @@
|
|||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import { InstallmentAnalysisPage } from "@/features/dashboard/components/installment-analysis/installment-analysis-page";
|
import { InstallmentAnalysisPage } from "@/features/dashboard/components/installment-analysis/installment-analysis-page";
|
||||||
import { fetchInstallmentAnalysis } from "@/features/dashboard/expenses/installment-analysis-queries";
|
import { fetchInstallmentAnalysis } from "@/features/dashboard/expenses/installment-analysis-queries";
|
||||||
|
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
|
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
await connection();
|
await connection();
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = await fetchInstallmentAnalysis(user.id);
|
const data = await fetchInstallmentAnalysis(user.id);
|
||||||
|
const logoMappings = await prefetchLogoMappings(
|
||||||
|
user.id,
|
||||||
|
data.installmentGroups.map((group) => group.name),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 pb-8">
|
<main className="flex flex-col gap-4 pb-8">
|
||||||
<InstallmentAnalysisPage data={data} />
|
<LogoPrefetchProvider mappings={logoMappings}>
|
||||||
|
<InstallmentAnalysisPage data={data} />
|
||||||
|
</LogoPrefetchProvider>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import {
|
import {
|
||||||
RiBankCard2Line,
|
RiBankCard2Line,
|
||||||
RiCheckboxCircleFill,
|
RiCheckboxCircleFill,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiTimeLine,
|
RiTimeLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
|
||||||
import MoneyValues from "@/shared/components/money-values";
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/components/ui/dialog";
|
} from "@/shared/components/ui/dialog";
|
||||||
import { Progress } from "@/shared/components/ui/progress";
|
import { Progress } from "@/shared/components/ui/progress";
|
||||||
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
||||||
import { cn } from "@/shared/utils";
|
import { cn } from "@/shared/utils";
|
||||||
import type { InstallmentGroup } from "./types";
|
import type { InstallmentGroup } from "./types";
|
||||||
|
|
||||||
@@ -79,6 +81,8 @@ export function InstallmentGroupCard({
|
|||||||
(sum, i) => sum + i.amount,
|
(sum, i) => sum + i.amount,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
const cardLogoSrc = resolveLogoSrc(group.cartaoLogo);
|
||||||
|
const cardName = group.cartaoName ?? "Compra parcelada";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -111,25 +115,24 @@ export function InstallmentGroupCard({
|
|||||||
{/* Info principal */}
|
{/* Info principal */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
{group.cartaoLogo ? (
|
<EstablishmentLogo name={group.name} size={40} />
|
||||||
<Image
|
|
||||||
src={`/logos/${group.cartaoLogo}`}
|
|
||||||
alt={group.cartaoName ?? "Cartão"}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
className="size-10 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="size-10 flex items-center justify-center">
|
|
||||||
<RiBankCard2Line className="size-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<CardTitle className="text-base truncate">
|
<CardTitle className="text-base truncate">
|
||||||
{group.name}
|
{group.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="flex min-w-0 items-center gap-1 text-xs">
|
||||||
{group.cartaoName ?? "Compra parcelada"}
|
{cardLogoSrc ? (
|
||||||
|
<Image
|
||||||
|
src={cardLogoSrc}
|
||||||
|
alt={`Logo do cartão ${cardName}`}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
className="size-4.5 shrink-0 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RiBankCard2Line className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">{cardName}</span>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +150,7 @@ export function InstallmentGroupCard({
|
|||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{/* Grid de valores */}
|
{/* Grid de valores */}
|
||||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-muted/50 mb-4">
|
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-primary/5 mb-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-muted-foreground font-medium">
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
Valor total
|
Valor total
|
||||||
@@ -165,7 +168,7 @@ export function InstallmentGroupCard({
|
|||||||
amount={pendingAmount}
|
amount={pendingAmount}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold",
|
"text-lg font-semibold",
|
||||||
pendingAmount > 0 ? "text-amber-600" : "text-success-600",
|
pendingAmount > 0 ? "text-primary" : "text-success",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,14 +186,18 @@ export function InstallmentGroupCard({
|
|||||||
</div>
|
</div>
|
||||||
{unpaidCount > 0 && (
|
{unpaidCount > 0 && (
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
<div className="flex items-center gap-1 text-muted-foreground">
|
||||||
<RiTimeLine className="size-3.5 text-amber-600" />
|
<RiTimeLine className="size-3.5" />
|
||||||
<span>
|
<span>
|
||||||
{unpaidCount} {unpaidCount === 1 ? "pendente" : "pendentes"}
|
{unpaidCount} {unpaidCount === 1 ? "pendente" : "pendentes"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Progress value={progress} className="h-2.5" />
|
<Progress
|
||||||
|
value={progress}
|
||||||
|
className="h-2.5 bg-muted"
|
||||||
|
indicatorClassName="bg-success"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Valor selecionado */}
|
{/* Valor selecionado */}
|
||||||
@@ -212,13 +219,13 @@ export function InstallmentGroupCard({
|
|||||||
{/* Botão para abrir detalhes */}
|
{/* Botão para abrir detalhes */}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full gap-1.5"
|
className="w-full gap-1.5"
|
||||||
onClick={() => setIsDetailsOpen(true)}
|
onClick={() => setIsDetailsOpen(true)}
|
||||||
>
|
>
|
||||||
<RiEyeLine className="size-4" />
|
<RiFileList2Line className="size-4" />
|
||||||
Ver detalhes ({group.pendingInstallments.length} parcelas)
|
detalhes ({group.pendingInstallments.length} parcelas)
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -228,18 +235,26 @@ export function InstallmentGroupCard({
|
|||||||
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{group.cartaoLogo ? (
|
<EstablishmentLogo name={group.name} size={32} />
|
||||||
<img
|
<div className="min-w-0">
|
||||||
src={`/logos/${group.cartaoLogo}`}
|
<DialogTitle className="truncate text-base">
|
||||||
alt={group.cartaoName ?? "Cartão"}
|
{group.name}
|
||||||
className="size-8 rounded-full object-cover"
|
</DialogTitle>
|
||||||
/>
|
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
) : (
|
{cardLogoSrc ? (
|
||||||
<div className="size-8 rounded-full bg-muted flex items-center justify-center">
|
<Image
|
||||||
<RiBankCard2Line className="size-4 text-muted-foreground" />
|
src={cardLogoSrc}
|
||||||
|
alt={`Logo do cartão ${cardName}`}
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
className="size-3.5 shrink-0 rounded-full object-cover opacity-75"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RiBankCard2Line className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">{cardName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
<DialogTitle className="text-base">{group.name}</DialogTitle>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
Detalhes das parcelas do grupo {group.name}
|
Detalhes das parcelas do grupo {group.name}
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ export async function fetchInstallmentAnalysis(
|
|||||||
cartaoLogo: cards.logo,
|
cartaoLogo: cards.logo,
|
||||||
})
|
})
|
||||||
.from(transactions)
|
.from(transactions)
|
||||||
.leftJoin(cards, eq(transactions.cardId, cards.id))
|
.leftJoin(
|
||||||
|
cards,
|
||||||
|
and(eq(transactions.cardId, cards.id), eq(cards.userId, userId)),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(transactions.userId, userId),
|
eq(transactions.userId, userId),
|
||||||
|
|||||||
Reference in New Issue
Block a user