mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
feat(pagadores): adicionar widget no dashboard e atualizar avatares
- Novo widget de pagadores no dashboard com resumo de transações - Substituir avatares SVG por PNG com melhor qualidade - Melhorar seção de pagador no diálogo de lançamentos - Adicionar ação para buscar pagadores por nome - Atualizar componentes de seleção (cartões, categorias, contas) - Melhorias no layout de ajustes e relatórios Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
RiAndroidLine,
|
||||
RiDownload2Line,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
RiQrCodeLine,
|
||||
RiShieldCheckLine,
|
||||
} from "@remixicon/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ApiTokensForm } from "./api-tokens-form";
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export function StatusSelectContent({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<DotIcon
|
||||
bg_dot={
|
||||
color={
|
||||
isActive
|
||||
? "bg-emerald-600 dark:bg-emerald-300"
|
||||
: "bg-slate-400 dark:bg-slate-500"
|
||||
|
||||
@@ -2,9 +2,9 @@ import { RiArrowDownLine, RiArrowUpLine } from "@remixicon/react";
|
||||
import type { CategoryType } from "@/lib/categorias/constants";
|
||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { CategoryIconBadge } from "./category-icon-badge";
|
||||
import { TypeBadge } from "../type-badge";
|
||||
import { Card } from "../ui/card";
|
||||
import { CategoryIconBadge } from "./category-icon-badge";
|
||||
|
||||
type CategorySummary = {
|
||||
id: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ export function TypeSelectContent({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<DotIcon
|
||||
bg_dot={
|
||||
color={
|
||||
isReceita
|
||||
? "bg-emerald-600 dark:bg-emerald-300"
|
||||
: "bg-rose-600 dark:bg-rose-300"
|
||||
|
||||
@@ -8,7 +8,7 @@ export function StatusSelectContent({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<DotIcon
|
||||
bg_dot={
|
||||
color={
|
||||
isActive
|
||||
? "bg-emerald-600 dark:bg-emerald-300"
|
||||
: "bg-slate-400 dark:bg-slate-500"
|
||||
|
||||
@@ -115,13 +115,24 @@ export function DashboardGridEditable({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleWidget = useCallback((widgetId: string) => {
|
||||
setHiddenWidgets((prev) =>
|
||||
prev.includes(widgetId)
|
||||
? prev.filter((id) => id !== widgetId)
|
||||
: [...prev, widgetId],
|
||||
);
|
||||
}, []);
|
||||
const handleToggleWidget = useCallback(
|
||||
(widgetId: string) => {
|
||||
const newHidden = hiddenWidgets.includes(widgetId)
|
||||
? hiddenWidgets.filter((id) => id !== widgetId)
|
||||
: [...hiddenWidgets, widgetId];
|
||||
|
||||
setHiddenWidgets(newHidden);
|
||||
|
||||
// Salvar automaticamente ao toggle
|
||||
startTransition(async () => {
|
||||
await updateWidgetPreferences({
|
||||
order: widgetOrder,
|
||||
hidden: newHidden,
|
||||
});
|
||||
});
|
||||
},
|
||||
[hiddenWidgets, widgetOrder],
|
||||
);
|
||||
|
||||
const handleHideWidget = useCallback((widgetId: string) => {
|
||||
setHiddenWidgets((prev) => [...prev, widgetId]);
|
||||
|
||||
96
components/dashboard/pagadores-widget.tsx
Normal file
96
components/dashboard/pagadores-widget.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RiExternalLinkLine,
|
||||
RiGroupLine,
|
||||
RiVerifiedBadgeFill,
|
||||
} from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import MoneyValues from "@/components/money-values";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import type { DashboardPagador } from "@/lib/dashboard/pagadores";
|
||||
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
||||
import { WidgetEmptyState } from "../widget-empty-state";
|
||||
|
||||
type PagadoresWidgetProps = {
|
||||
pagadores: DashboardPagador[];
|
||||
};
|
||||
|
||||
const buildInitials = (value: string) => {
|
||||
const parts = value.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return "??";
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
const firstPart = parts[0];
|
||||
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "??";
|
||||
}
|
||||
const firstChar = parts[0]?.[0] ?? "";
|
||||
const secondChar = parts[1]?.[0] ?? "";
|
||||
return `${firstChar}${secondChar}`.toUpperCase() || "??";
|
||||
};
|
||||
|
||||
export function PagadoresWidget({ pagadores }: PagadoresWidgetProps) {
|
||||
return (
|
||||
<CardContent className="flex flex-col gap-4 px-0">
|
||||
{pagadores.length === 0 ? (
|
||||
<WidgetEmptyState
|
||||
icon={<RiGroupLine className="size-6 text-muted-foreground" />}
|
||||
title="Nenhum pagador para o período"
|
||||
description="Quando houver despesas associadas a pagadores, eles aparecerão aqui."
|
||||
/>
|
||||
) : (
|
||||
<ul className="flex flex-col">
|
||||
{pagadores.map((pagador) => {
|
||||
const initials = buildInitials(pagador.name);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={pagador.id}
|
||||
className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 py-2">
|
||||
<Avatar className="size-10 shrink-0">
|
||||
<AvatarImage
|
||||
src={getAvatarSrc(pagador.avatarUrl)}
|
||||
alt={`Avatar de ${pagador.name}`}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="min-w-0">
|
||||
<Link
|
||||
prefetch
|
||||
href={`/pagadores/${pagador.id}`}
|
||||
className="inline-flex max-w-full items-center gap-1 text-sm text-foreground underline-offset-2 hover:text-primary hover:underline"
|
||||
>
|
||||
<span className="truncate">{pagador.name}</span>
|
||||
{pagador.isAdmin && (
|
||||
<RiVerifiedBadgeFill
|
||||
className="size-4 shrink-0 text-blue-500"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<RiExternalLinkLine
|
||||
className="size-3 shrink-0 text-muted-foreground"
|
||||
aria-hidden
|
||||
/>
|
||||
</Link>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{pagador.email ?? "Sem email cadastrado"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end">
|
||||
<MoneyValues amount={pagador.totalExpenses} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export interface SplitAndSettlementSectionProps extends BaseFieldSectionProps {
|
||||
export interface PagadorSectionProps extends BaseFieldSectionProps {
|
||||
pagadorOptions: SelectOption[];
|
||||
secondaryPagadorOptions: SelectOption[];
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface PaymentMethodSectionProps extends BaseFieldSectionProps {
|
||||
|
||||
@@ -141,6 +141,11 @@ export function LancamentoDialog({
|
||||
return groupAndSortCategorias(filtered);
|
||||
}, [categoriaOptions, formState.transactionType]);
|
||||
|
||||
const totalAmount = useMemo(() => {
|
||||
const parsed = Number.parseFloat(formState.amount);
|
||||
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
|
||||
}, [formState.amount]);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
|
||||
if (key === "period") {
|
||||
@@ -223,6 +228,12 @@ export function LancamentoDialog({
|
||||
? formState.secondaryPagadorId
|
||||
: undefined,
|
||||
isSplit: formState.isSplit,
|
||||
primarySplitAmount: formState.isSplit
|
||||
? Number.parseFloat(formState.primarySplitAmount) || undefined
|
||||
: undefined,
|
||||
secondarySplitAmount: formState.isSplit
|
||||
? Number.parseFloat(formState.secondarySplitAmount) || undefined
|
||||
: undefined,
|
||||
contaId: formState.contaId,
|
||||
cartaoId: formState.cartaoId,
|
||||
categoriaId: formState.categoriaId,
|
||||
@@ -402,6 +413,7 @@ export function LancamentoDialog({
|
||||
onFieldChange={handleFieldChange}
|
||||
pagadorOptions={pagadorOptions}
|
||||
secondaryPagadorOptions={secondaryPagadorOptions}
|
||||
totalAmount={totalAmount}
|
||||
/>
|
||||
|
||||
<PaymentMethodSection
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
@@ -16,63 +18,46 @@ export function PagadorSection({
|
||||
onFieldChange,
|
||||
pagadorOptions,
|
||||
secondaryPagadorOptions,
|
||||
totalAmount,
|
||||
}: PagadorSectionProps) {
|
||||
const handlePrimaryAmountChange = useCallback(
|
||||
(value: string) => {
|
||||
onFieldChange("primarySplitAmount", value);
|
||||
const numericValue = Number.parseFloat(value) || 0;
|
||||
const remaining = Math.max(0, totalAmount - numericValue);
|
||||
onFieldChange("secondarySplitAmount", remaining.toFixed(2));
|
||||
},
|
||||
[totalAmount, onFieldChange],
|
||||
);
|
||||
|
||||
const handleSecondaryAmountChange = useCallback(
|
||||
(value: string) => {
|
||||
onFieldChange("secondarySplitAmount", value);
|
||||
const numericValue = Number.parseFloat(value) || 0;
|
||||
const remaining = Math.max(0, totalAmount - numericValue);
|
||||
onFieldChange("primarySplitAmount", remaining.toFixed(2));
|
||||
},
|
||||
[totalAmount, onFieldChange],
|
||||
);
|
||||
|
||||
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>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={formState.secondaryPagadorId}
|
||||
onValueChange={(value) =>
|
||||
onFieldChange("secondaryPagadorId", value)
|
||||
}
|
||||
value={formState.pagadorId}
|
||||
onValueChange={(value) => onFieldChange("pagadorId", value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="secondaryPagador"
|
||||
disabled={secondaryPagadorOptions.length === 0}
|
||||
className={"w-full"}
|
||||
id="pagador"
|
||||
className={formState.isSplit ? "w-[55%]" : "w-full"}
|
||||
>
|
||||
<SelectValue placeholder="Selecione">
|
||||
{formState.secondaryPagadorId &&
|
||||
{formState.pagadorId &&
|
||||
(() => {
|
||||
const selectedOption = secondaryPagadorOptions.find(
|
||||
(opt) => opt.value === formState.secondaryPagadorId,
|
||||
const selectedOption = pagadorOptions.find(
|
||||
(opt) => opt.value === formState.pagadorId,
|
||||
);
|
||||
return selectedOption ? (
|
||||
<PagadorSelectContent
|
||||
@@ -84,7 +69,7 @@ export function PagadorSection({
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{secondaryPagadorOptions.map((option) => (
|
||||
{pagadorOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<PagadorSelectContent
|
||||
label={option.label}
|
||||
@@ -94,6 +79,65 @@ export function PagadorSection({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formState.isSplit && (
|
||||
<CurrencyInput
|
||||
value={formState.primarySplitAmount}
|
||||
onValueChange={handlePrimaryAmountChange}
|
||||
placeholder="R$ 0,00"
|
||||
className="h-9 w-[45%] text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formState.isSplit ? (
|
||||
<div className="w-full space-y-1 mb-1">
|
||||
<Label htmlFor="secondaryPagador">Dividir com</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={formState.secondaryPagadorId}
|
||||
onValueChange={(value) =>
|
||||
onFieldChange("secondaryPagadorId", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="secondaryPagador"
|
||||
disabled={secondaryPagadorOptions.length === 0}
|
||||
className="w-[55%]"
|
||||
>
|
||||
<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>
|
||||
<CurrencyInput
|
||||
value={formState.secondarySplitAmount}
|
||||
onValueChange={handleSecondaryAmountChange}
|
||||
placeholder="R$ 0,00"
|
||||
className="h-9 w-[45%] text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -148,7 +148,7 @@ export function PagadorInfoCard({
|
||||
alt={`Avatar de ${pagador.name}`}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
className="h-full w-full object-cover rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -214,7 +214,7 @@ export function PagadorInfoCard({
|
||||
{pagador.email}
|
||||
</Link>
|
||||
) : (
|
||||
"—"
|
||||
"Sem e-mail cadastrado"
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -260,7 +260,7 @@ export function PagadorInfoCard({
|
||||
pagador.note ? (
|
||||
<span className="text-muted-foreground">{pagador.note}</span>
|
||||
) : (
|
||||
"—"
|
||||
"Sem observações"
|
||||
)
|
||||
}
|
||||
className="sm:col-span-2"
|
||||
|
||||
@@ -63,7 +63,7 @@ export function PagadorCard({ pagador, onEdit, onRemove }: PagadorCardProps) {
|
||||
<p className="mt-1 text-xs text-muted-foreground">{pagador.email}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Sem Email cadastrado
|
||||
Sem email cadastrado
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
import { RiCheckLine, RiCloseCircleLine } from "@remixicon/react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
useCallback,
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||
import { useFormState } from "@/hooks/use-form-state";
|
||||
import {
|
||||
@@ -270,16 +268,9 @@ export function PagadorDialog({
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="flex flex-col gap-3">
|
||||
<div className="grid grid-cols-4 gap-3 sm:grid-cols-5">
|
||||
{availableAvatars.length === 0 ? (
|
||||
<div className="col-span-5 flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border/60 bg-muted/10 p-6 text-center text-sm text-muted-foreground">
|
||||
<RiCloseCircleLine className="size-6" />
|
||||
Nenhum avatar disponível. Adicione imagens em
|
||||
<span className="font-mono text-xs">public/avatares</span>
|
||||
.
|
||||
</div>
|
||||
) : null}
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<Label>Avatar</Label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{availableAvatars.map((avatar) => {
|
||||
const isSelected = avatar === formState.avatarUrl;
|
||||
return (
|
||||
@@ -287,22 +278,16 @@ export function PagadorDialog({
|
||||
type="button"
|
||||
key={avatar}
|
||||
onClick={() => updateField("avatarUrl", avatar)}
|
||||
className="group relative flex items-center justify-center overflow-hidden rounded-xl border border-border/70 p-2 transition-all hover:border-primary/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 data-[selected=true]:border-primary data-[selected=true]:bg-primary/10"
|
||||
className="group relative flex items-center justify-center rounded-full p-0.5 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
|
||||
data-selected={isSelected}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
<span className="absolute inset-0 rounded-xl border-2 border-primary/80 opacity-0 transition-opacity group-data-[selected=true]:opacity-100" />
|
||||
{isSelected ? (
|
||||
<span className="absolute right-1 top-1 flex size-4 items-center justify-center rounded-full bg-sidebar-foreground text-primary-foreground shadow-sm">
|
||||
<RiCheckLine className="size-3.5" />
|
||||
</span>
|
||||
) : null}
|
||||
<Image
|
||||
src={getAvatarSrc(avatar)}
|
||||
alt={`Avatar ${avatar}`}
|
||||
width={72}
|
||||
height={72}
|
||||
className="size-12 rounded-lg object-cover"
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-12 rounded-full object-cove hover:scale-110 transition-transform duration-200"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
@@ -312,12 +297,11 @@ export function PagadorDialog({
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pagador-note">Anotações</Label>
|
||||
<Textarea
|
||||
<Input
|
||||
id="pagador-note"
|
||||
rows={2}
|
||||
value={formState.note}
|
||||
onChange={(event) => updateField("note", event.target.value)}
|
||||
placeholder="Observações, preferências ou detalhes relevantes sobre este pagador"
|
||||
placeholder="Observações sobre este pagador"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function StatusSelectContent({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<DotIcon
|
||||
bg_dot={
|
||||
color={
|
||||
isActive
|
||||
? "bg-emerald-600 dark:bg-emerald-300"
|
||||
: "bg-slate-400 dark:bg-slate-500"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useMemo, useState, useTransition } from "react";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { CategoryReportSkeleton } from "@/components/skeletons/category-report-skeleton";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { CategoryChartData } from "@/lib/relatorios/fetch-category-chart-data";
|
||||
import type { CategoryReportData } from "@/lib/relatorios/types";
|
||||
@@ -123,36 +124,39 @@ export function CategoryReportPage({
|
||||
|
||||
{/* Empty States */}
|
||||
{!isPending && hasNoCategories && (
|
||||
<EmptyState
|
||||
title="Nenhuma categoria cadastrada"
|
||||
description="Você precisa cadastrar categorias antes de visualizar o relatório."
|
||||
media={<RiPieChartLine className="h-12 w-12" />}
|
||||
mediaVariant="icon"
|
||||
/>
|
||||
<Card className="flex w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
title="Nenhuma categoria cadastrada"
|
||||
description="Você precisa cadastrar categorias antes de visualizar o relatório."
|
||||
media={<RiPieChartLine className="size-6 text-primary" />}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!isPending &&
|
||||
!hasNoCategories &&
|
||||
hasNoData &&
|
||||
filters.selectedCategories.length === 0 && (
|
||||
<EmptyState
|
||||
title="Selecione pelo menos uma categoria"
|
||||
description="Use o filtro acima para selecionar as categorias que deseja visualizar no relatório."
|
||||
media={<RiFilter3Line className="h-12 w-12" />}
|
||||
mediaVariant="icon"
|
||||
/>
|
||||
<Card className="flex w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
title="Selecione pelo menos uma categoria"
|
||||
description="Use o filtro acima para selecionar as categorias que deseja visualizar no relatório."
|
||||
media={<RiFilter3Line className="size-6 text-primary" />}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!isPending &&
|
||||
!hasNoCategories &&
|
||||
hasNoData &&
|
||||
filters.selectedCategories.length > 0 && (
|
||||
<EmptyState
|
||||
title="Nenhum lançamento encontrado"
|
||||
description="Não há transações no período selecionado para as categorias filtradas."
|
||||
media={<RiPieChartLine className="h-12 w-12" />}
|
||||
mediaVariant="icon"
|
||||
/>
|
||||
<Card className="flex w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
title="Nenhum lançamento encontrado"
|
||||
description="Não há transações no período selecionado para as categorias filtradas."
|
||||
media={<RiPieChartLine className="size-6 text-primary" />}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Tabs: Table and Chart */}
|
||||
|
||||
@@ -24,10 +24,15 @@ export function NavUser({ user, pagadorAvatarUrl }: NavUserProps) {
|
||||
useSidebar();
|
||||
|
||||
const avatarSrc = useMemo(() => {
|
||||
// Priorizar o avatar do pagador admin quando disponível
|
||||
if (pagadorAvatarUrl) {
|
||||
return getAvatarSrc(pagadorAvatarUrl);
|
||||
}
|
||||
// Fallback para a imagem do usuário (Google, etc)
|
||||
if (user.image) {
|
||||
return user.image;
|
||||
}
|
||||
return getAvatarSrc(pagadorAvatarUrl);
|
||||
return getAvatarSrc(null);
|
||||
}, [user.image, pagadorAvatarUrl]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user