mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-10 19:21:46 +00:00
refactor: compartilha utilitários e refina widgets e calendário
This commit is contained in:
@@ -136,6 +136,8 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
onCreate(day);
|
||||
};
|
||||
|
||||
const overflowCount = day.events.length - previewEvents.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
@@ -143,7 +145,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
onClick={() => onSelect(day)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border border-transparent bg-card/80 p-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent transition-colors duration-300",
|
||||
"group flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border border-transparent bg-card/80 p-2 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent",
|
||||
!day.isCurrentMonth && "opacity-60",
|
||||
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
|
||||
)}
|
||||
@@ -153,7 +155,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
className={cn(
|
||||
"text-sm font-semibold leading-none",
|
||||
day.isToday
|
||||
? "text-orange-100 bg-primary size-5 rounded-full flex items-center justify-center"
|
||||
? "text-primary-foreground bg-primary size-5 rounded-full flex items-center justify-center"
|
||||
: "text-foreground/90",
|
||||
)}
|
||||
>
|
||||
@@ -162,7 +164,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateClick}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
className="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground opacity-0 transition-all group-hover:opacity-100 hover:bg-primary/20 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
aria-label={`Criar lançamento em ${day.date}`}
|
||||
>
|
||||
<RiAddLine className="size-3.5" />
|
||||
@@ -170,13 +172,14 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
{previewEvents.map((event) => (
|
||||
{day.isCurrentMonth &&
|
||||
previewEvents.map((event) => (
|
||||
<DayEventPreview key={event.id} event={event} />
|
||||
))}
|
||||
|
||||
{hasOverflow ? (
|
||||
{day.isCurrentMonth && hasOverflow ? (
|
||||
<span className="text-xs font-medium text-primary/80">
|
||||
+ ver mais
|
||||
+{overflowCount} mais
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -69,8 +69,6 @@ const renderLancamento = (
|
||||
</span>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Badge variant={"outline"}>{event.transaction.condition}</Badge>
|
||||
<Badge variant={"outline"}>{event.transaction.paymentMethod}</Badge>
|
||||
<Badge variant={"outline"}>{event.transaction.categoriaName}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,9 +196,6 @@ export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!day}>
|
||||
Novo lançamento
|
||||
</Button>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import type { CategoryHistoryData } from "@/features/dashboard/categories/category-history-queries";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Card, CardContent } from "@/shared/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -170,8 +169,7 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-auto">
|
||||
<CardContent className="space-y-2.5">
|
||||
<div className="space-y-2.5">
|
||||
<div className="space-y-2">
|
||||
{selectedCategoryDetails.length > 0 && (
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
@@ -437,7 +435,6 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ export function GoalProgressItem({
|
||||
const progressValue = clampGoalProgress(item.usedPercentage, 0, 100);
|
||||
const percentageDelta = item.usedPercentage - 100;
|
||||
|
||||
const isExceeded = item.status === "exceeded";
|
||||
|
||||
return (
|
||||
<div className="transition-all duration-300 py-2">
|
||||
<div className="group transition-all duration-300 py-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2">
|
||||
<CategoryIconBadge
|
||||
@@ -54,7 +56,7 @@ export function GoalProgressItem({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
className="opacity-30 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onEdit(item)}
|
||||
aria-label={`Editar orçamento de ${item.categoryName}`}
|
||||
>
|
||||
@@ -63,7 +65,14 @@ export function GoalProgressItem({
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-11 mt-1.5">
|
||||
<Progress value={progressValue} />
|
||||
<Progress
|
||||
value={progressValue}
|
||||
className={
|
||||
isExceeded
|
||||
? "[&_[data-slot=progress-indicator]]:bg-destructive bg-destructive/20"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/components/ui/tooltip";
|
||||
import { getPaymentMethodIcon } from "@/shared/utils/icons";
|
||||
|
||||
type InstallmentExpenseListItemProps = {
|
||||
expense: InstallmentExpense;
|
||||
@@ -27,6 +28,10 @@ export function InstallmentExpenseListItem({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 transition-all duration-300 py-2">
|
||||
<div className="flex size-9.5 shrink-0 items-center justify-center rounded-full bg-muted text-foreground">
|
||||
{getPaymentMethodIcon(expense.paymentMethod)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
@@ -61,7 +66,7 @@ export function InstallmentExpenseListItem({
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{endDate ? `Termina em ${endDate}` : null}
|
||||
{" | Restante "}
|
||||
{" · Restante "}
|
||||
<MoneyValues
|
||||
amount={remainingAmount}
|
||||
className="inline-block font-medium"
|
||||
|
||||
@@ -15,32 +15,18 @@ import {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/shared/components/ui/avatar";
|
||||
import { CardContent } from "@/shared/components/ui/card";
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
||||
import { buildInitials } from "@/shared/utils/initials";
|
||||
import { formatPercentage } from "@/shared/utils/percentage";
|
||||
|
||||
type PayersWidgetProps = {
|
||||
payers: 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 PayersWidget({ payers }: PayersWidgetProps) {
|
||||
return (
|
||||
<CardContent className="flex flex-col gap-4 px-0">
|
||||
<div className="flex flex-col">
|
||||
{payers.length === 0 ? (
|
||||
<WidgetEmptyState
|
||||
icon={<RiGroupLine className="size-6 text-muted-foreground" />}
|
||||
@@ -123,6 +109,6 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,18 +5,7 @@ import { CardContent } from "@/shared/components/ui/card";
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { resolveLogoSrc } from "@/shared/lib/logo";
|
||||
import type { PayerCardUsageItem } from "@/shared/lib/payers/details";
|
||||
|
||||
const buildInitials = (value: string) => {
|
||||
const parts = value.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return "CC";
|
||||
if (parts.length === 1) {
|
||||
const firstPart = parts[0];
|
||||
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CC";
|
||||
}
|
||||
const firstChar = parts[0]?.[0] ?? "";
|
||||
const secondChar = parts[1]?.[0] ?? "";
|
||||
return `${firstChar}${secondChar}`.toUpperCase() || "CC";
|
||||
};
|
||||
import { buildInitials } from "@/shared/utils/initials";
|
||||
|
||||
type PagadorCardUsageCardProps = {
|
||||
items: PayerCardUsageItem[];
|
||||
|
||||
@@ -12,23 +12,12 @@ import {
|
||||
} from "@/shared/components/ui/card";
|
||||
import { Progress } from "@/shared/components/ui/progress";
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { buildInitials } from "@/shared/utils/initials";
|
||||
|
||||
type EstablishmentsListProps = {
|
||||
establishments: TopEstabelecimentosData["establishments"];
|
||||
};
|
||||
|
||||
const buildInitials = (value: string) => {
|
||||
const parts = value.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return "ES";
|
||||
if (parts.length === 1) {
|
||||
const firstPart = parts[0];
|
||||
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "ES";
|
||||
}
|
||||
const firstChar = parts[0]?.[0] ?? "";
|
||||
const secondChar = parts[1]?.[0] ?? "";
|
||||
return `${firstChar}${secondChar}`.toUpperCase() || "ES";
|
||||
};
|
||||
|
||||
export function EstablishmentsList({
|
||||
establishments,
|
||||
}: EstablishmentsListProps) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export type WidgetCardProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children: React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -37,11 +37,11 @@ export default function WidgetCard({
|
||||
<CardHeader>
|
||||
<div className="flex w-full items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-1 tracking-tight lowercase">
|
||||
<span className="size-4">{icon}</span>
|
||||
<CardTitle className="flex items-center gap-1 tracking-tight">
|
||||
{icon && <span className="size-4">{icon}</span>}
|
||||
{title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground text-sm lowercase mt-1.5 tracking-tight">
|
||||
<CardDescription className="text-muted-foreground text-sm mt-1.5 tracking-tight">
|
||||
{subtitle}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
15
src/shared/utils/initials.ts
Normal file
15
src/shared/utils/initials.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Builds a 2-character initials string from a name.
|
||||
* Falls back to the provided `fallback` (default "??") when the name is empty.
|
||||
*/
|
||||
export function buildInitials(value: string, fallback = "??"): string {
|
||||
const parts = value.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return fallback;
|
||||
if (parts.length === 1) {
|
||||
const firstPart = parts[0];
|
||||
return firstPart ? firstPart.slice(0, 2).toUpperCase() : fallback;
|
||||
}
|
||||
const firstChar = parts[0]?.[0] ?? "";
|
||||
const secondChar = parts[1]?.[0] ?? "";
|
||||
return `${firstChar}${secondChar}`.toUpperCase() || fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user