mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat(dashboard): refina layout e widgets do painel
This commit is contained in:
@@ -2,6 +2,7 @@ import { fetchDashboardNotifications } from "@/features/dashboard/notifications-
|
|||||||
import { fetchPendingInboxCount } from "@/features/inbox/queries";
|
import { fetchPendingInboxCount } from "@/features/inbox/queries";
|
||||||
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
||||||
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
||||||
|
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
||||||
import { getUserSession } from "@/shared/lib/auth/server";
|
import { getUserSession } from "@/shared/lib/auth/server";
|
||||||
import { fetchPayersWithAccess } from "@/shared/lib/payers/access";
|
import { fetchPayersWithAccess } from "@/shared/lib/payers/access";
|
||||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||||
@@ -48,7 +49,17 @@ export default async function DashboardLayout({
|
|||||||
notificationsSnapshot={notificationsSnapshot}
|
notificationsSnapshot={notificationsSnapshot}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex flex-1 flex-col pt-16">
|
<div className="relative flex flex-1 flex-col pt-16">
|
||||||
<div className="pointer-events-none absolute inset-0" />
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-80 overflow-hidden">
|
||||||
|
<DotPattern
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
cx={1.25}
|
||||||
|
cy={1.25}
|
||||||
|
cr={1.25}
|
||||||
|
className="text-primary/10 mask-[linear-gradient(to_bottom,black_0%,transparent_100%)]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-linear-to-b from-primary/6 to-transparent" />
|
||||||
|
</div>
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -174,12 +174,11 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
|
|||||||
{selectedCategoryDetails.length > 0 && (
|
{selectedCategoryDetails.length > 0 && (
|
||||||
<div className="flex items-start justify-between gap-4 mb-4">
|
<div className="flex items-start justify-between gap-4 mb-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedCategoryDetails.map((category) => {
|
{selectedCategoryDetails.map((category, colorIndex) => {
|
||||||
if (!category) return null;
|
if (!category) return null;
|
||||||
const IconComponent = category.icon
|
const IconComponent = category.icon
|
||||||
? getIconComponent(category.icon)
|
? getIconComponent(category.icon)
|
||||||
: null;
|
: null;
|
||||||
const colorIndex = selectedCategories.indexOf(category.id);
|
|
||||||
const color = CHART_COLORS[colorIndex % CHART_COLORS.length];
|
const color = CHART_COLORS[colorIndex % CHART_COLORS.length];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ import type { DashboardCardMetrics } from "@/features/dashboard/dashboard-metric
|
|||||||
import MoneyValues from "@/shared/components/money-values";
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardFooter,
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/shared/components/ui/card";
|
} from "@/shared/components/ui/card";
|
||||||
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
import { formatPercentage } from "@/shared/utils/percentage";
|
import { formatPercentage } from "@/shared/utils/percentage";
|
||||||
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
|
||||||
type DashboardMetricsCardsProps = {
|
type DashboardMetricsCardsProps = {
|
||||||
metrics: DashboardCardMetrics;
|
metrics: DashboardCardMetrics;
|
||||||
@@ -28,35 +31,35 @@ const TREND_THRESHOLD = 0.005;
|
|||||||
const CARDS = [
|
const CARDS = [
|
||||||
{
|
{
|
||||||
label: "Receitas",
|
label: "Receitas",
|
||||||
|
subtitle: "Entradas do período",
|
||||||
key: "receitas",
|
key: "receitas",
|
||||||
icon: RiArrowUpLine,
|
icon: RiArrowUpLine,
|
||||||
invertTrend: false,
|
invertTrend: false,
|
||||||
cardClass: "",
|
|
||||||
iconClass: "text-success",
|
iconClass: "text-success",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Despesas",
|
label: "Despesas",
|
||||||
|
subtitle: "Saídas do período",
|
||||||
key: "despesas",
|
key: "despesas",
|
||||||
icon: RiArrowDownLine,
|
icon: RiArrowDownLine,
|
||||||
invertTrend: true,
|
invertTrend: true,
|
||||||
cardClass: "",
|
|
||||||
iconClass: "text-destructive",
|
iconClass: "text-destructive",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Balanço",
|
label: "Balanço",
|
||||||
|
subtitle: "Receitas menos despesas",
|
||||||
key: "balanco",
|
key: "balanco",
|
||||||
icon: RiScalesLine,
|
icon: RiScalesLine,
|
||||||
invertTrend: false,
|
invertTrend: false,
|
||||||
cardClass: "",
|
iconClass: "text-warning",
|
||||||
iconClass: "text-amber-500",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Previsto",
|
label: "Previsto",
|
||||||
|
subtitle: "Saldo acumulado projetado",
|
||||||
key: "previsto",
|
key: "previsto",
|
||||||
icon: RiCalendarCheckLine,
|
icon: RiCalendarCheckLine,
|
||||||
invertTrend: false,
|
invertTrend: false,
|
||||||
cardClass: "border border-dashed",
|
iconClass: "text-primary",
|
||||||
iconClass: "",
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -91,8 +94,8 @@ const getPercentChange = (current: number, previous: number): string => {
|
|||||||
: "—";
|
: "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTrendColor = (trend: Trend, invertTrend: boolean): string => {
|
const getTrendBadgeClass = (trend: Trend, invertTrend: boolean): string => {
|
||||||
if (trend === "flat") return "text-muted-foreground";
|
if (trend === "flat") return "bg-muted text-muted-foreground";
|
||||||
const isPositive = invertTrend ? trend === "down" : trend === "up";
|
const isPositive = invertTrend ? trend === "down" : trend === "up";
|
||||||
return isPositive ? "text-success" : "text-destructive";
|
return isPositive ? "text-success" : "text-destructive";
|
||||||
};
|
};
|
||||||
@@ -101,36 +104,61 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||||
{CARDS.map(
|
{CARDS.map(
|
||||||
({ label, key, icon: Icon, invertTrend, cardClass, iconClass }) => {
|
({ label, subtitle, key, icon: Icon, invertTrend, iconClass }) => {
|
||||||
const metric = metrics[key];
|
const metric = metrics[key];
|
||||||
const trend = getTrend(metric.current, metric.previous);
|
const trend = getTrend(metric.current, metric.previous);
|
||||||
const TrendIcon = TREND_ICONS[trend];
|
const TrendIcon = TREND_ICONS[trend];
|
||||||
const trendColor = getTrendColor(trend, invertTrend);
|
const trendBadgeClass = getTrendBadgeClass(trend, invertTrend);
|
||||||
|
const percentChange = getPercentChange(
|
||||||
|
metric.current,
|
||||||
|
metric.previous,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card key={label} className="gap-2 overflow-hidden">
|
||||||
key={label}
|
|
||||||
className={`@container/card flex flex-col justify-between min-h-34 ${cardClass}`}
|
|
||||||
>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-1 tracking-tight lowercase">
|
<div className="flex items-start justify-between">
|
||||||
<Icon className={`size-4 ${iconClass}`} />
|
<div>
|
||||||
{label}
|
<CardTitle className="flex items-center gap-1 tracking-tight">
|
||||||
</CardTitle>
|
<Icon
|
||||||
<div className="flex items-baseline gap-2 mt-auto pt-2">
|
className={cn("size-4", iconClass)}
|
||||||
<MoneyValues className="text-2xl" amount={metric.current} />
|
aria-hidden
|
||||||
<div className={`flex items-center text-xs ${trendColor}`}>
|
/>
|
||||||
<TrendIcon size={14} />
|
{label}
|
||||||
{getPercentChange(metric.current, metric.previous)}
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-1.5 tracking-tight">
|
||||||
|
{subtitle}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Separator className="mt-1" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="text-sm">
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
<CardContent className="flex flex-col gap-3">
|
||||||
<span>vs. mês anterior</span>
|
<div className="flex flex-wrap items-center justify-between gap-2 mt-1">
|
||||||
<MoneyValues amount={metric.previous} />
|
<MoneyValues
|
||||||
|
className="text-[1.55rem] leading-none font-medium"
|
||||||
|
amount={metric.current}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 text-xs font-medium",
|
||||||
|
trendBadgeClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TrendIcon className="size-3.5" aria-hidden />
|
||||||
|
<span>{percentChange}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
<MoneyValues
|
||||||
|
className="inline text-xs font-medium text-muted-foreground"
|
||||||
|
amount={metric.previous}
|
||||||
|
/>
|
||||||
|
<span className="ml-1">no mês anterior</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ export function DashboardWelcome({ name }: { name?: string | null }) {
|
|||||||
const greeting = getGreeting();
|
const greeting = getGreeting();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="p-2">
|
<section className="py-4">
|
||||||
<div className="tracking-tight">
|
<div className="tracking-tight">
|
||||||
<h1 className="text-xl">
|
<h1 className="text-xl">
|
||||||
{greeting}, <span className="text-primary">{displayName}</span>
|
{greeting}, {displayName}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm mt-1 text-muted-foreground">{formattedDate}</p>
|
<h2 className="text-sm mt-1 text-muted-foreground">{formattedDate}</h2>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,20 +40,20 @@ export function NoteListItem({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="link"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="opacity-30 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
className="transition-opacity text-primary hover:opacity-80"
|
||||||
onClick={() => onOpenEdit(note)}
|
onClick={() => onOpenEdit(note)}
|
||||||
aria-label={`Editar anotação ${displayTitle}`}
|
aria-label={`Editar anotação ${displayTitle}`}
|
||||||
>
|
>
|
||||||
<RiPencilLine className="size-4" />
|
<RiPencilLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="link"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="opacity-30 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
className="transition-opacity text-primary hover:opacity-80"
|
||||||
onClick={() => onOpenDetails(note)}
|
onClick={() => onOpenDetails(note)}
|
||||||
aria-label={`Ver detalhes da anotação ${displayTitle}`}
|
aria-label={`Ver detalhes da anotação ${displayTitle}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function MonthNavigation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="sticky top-16 z-10 flex w-full flex-row p-4 backdrop-blur-sm bg-card/50">
|
<Card className="sticky top-16 z-10 flex w-full flex-row p-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
direction="left"
|
direction="left"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler
|
|||||||
import { Logo } from "@/shared/components/logo";
|
import { Logo } from "@/shared/components/logo";
|
||||||
import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell";
|
import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell";
|
||||||
import { RefreshPageButton } from "@/shared/components/refresh-page-button";
|
import { RefreshPageButton } from "@/shared/components/refresh-page-button";
|
||||||
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
|
||||||
import { NavMenu } from "./nav-menu";
|
import { NavMenu } from "./nav-menu";
|
||||||
import { NavbarUser } from "./navbar-user";
|
import { NavbarUser } from "./navbar-user";
|
||||||
|
|
||||||
@@ -32,14 +31,6 @@ export function AppNavbar({
|
|||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center bg-primary">
|
<header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center bg-primary">
|
||||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<DotPattern
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
cx={1.25}
|
|
||||||
cy={1.25}
|
|
||||||
cr={1.25}
|
|
||||||
className="text-black/5 mask-[linear-gradient(to_right,transparent,black_5%,black_55%,transparent)]"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-b from-white/8 via-transparent to-black/6" />
|
<div className="absolute inset-0 bg-linear-to-b from-white/8 via-transparent to-black/6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ export function DashboardMetricsCardsSkeleton() {
|
|||||||
{Array.from({ length: 4 }).map((_, index) => (
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
<Card
|
<Card
|
||||||
key={index}
|
key={index}
|
||||||
className="@container/card flex flex-col justify-between min-h-32"
|
className="@container/card min-h-36 justify-between gap-0"
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader className="gap-4 pb-3">
|
||||||
<CardTitle className="flex items-center gap-1">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Skeleton className="size-4 rounded-md bg-foreground/10" />
|
<Skeleton className="size-8 rounded-md bg-foreground/10" />
|
||||||
<Skeleton className="h-4 w-20 rounded-md bg-foreground/10" />
|
<Skeleton className="h-4 w-24 rounded-md bg-foreground/10" />
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-baseline gap-2 mt-auto pt-4">
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<Skeleton className="h-9 w-32 rounded-md bg-foreground/10" />
|
<Skeleton className="h-10 w-36 rounded-md bg-foreground/10" />
|
||||||
<Skeleton className="h-4 w-12 rounded-md bg-foreground/10" />
|
<Skeleton className="h-7 w-20 rounded-full bg-foreground/10" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardFooter className="text-sm">
|
<CardFooter className="items-start pt-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex flex-col items-start gap-1.5">
|
||||||
<Skeleton className="h-3 w-20 rounded-md bg-foreground/10" />
|
<Skeleton className="h-3 w-24 rounded-md bg-foreground/10" />
|
||||||
<Skeleton className="h-3 w-16 rounded-md bg-foreground/10" />
|
<Skeleton className="h-4 w-20 rounded-md bg-foreground/10" />
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function formatPercentageChange(value: number | null): string {
|
|||||||
|
|
||||||
return formatPercentage(value, {
|
return formatPercentage(value, {
|
||||||
...formatterOptions,
|
...formatterOptions,
|
||||||
absolute: true,
|
|
||||||
signDisplay: value === 0 ? "auto" : "always",
|
signDisplay: value === 0 ? "auto" : "always",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user