refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,42 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { DayCell } from "@/components/calendario/day-cell";
|
||||
|
||||
import type { CalendarDay } from "@/components/calendario/types";
|
||||
import { WEEK_DAYS_SHORT } from "@/components/calendario/utils";
|
||||
import { DayCell } from "@/components/calendario/day-cell";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
|
||||
type CalendarGridProps = {
|
||||
days: CalendarDay[];
|
||||
onSelectDay: (day: CalendarDay) => void;
|
||||
onCreateDay: (day: CalendarDay) => void;
|
||||
days: CalendarDay[];
|
||||
onSelectDay: (day: CalendarDay) => void;
|
||||
onCreateDay: (day: CalendarDay) => void;
|
||||
};
|
||||
|
||||
export function CalendarGrid({
|
||||
days,
|
||||
onSelectDay,
|
||||
onCreateDay,
|
||||
days,
|
||||
onSelectDay,
|
||||
onCreateDay,
|
||||
}: CalendarGridProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg bg-card drop-shadow-xs px-2">
|
||||
<div className="grid grid-cols-7 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{WEEK_DAYS_SHORT.map((dayName) => (
|
||||
<span key={dayName} className="px-3 py-2 text-center">
|
||||
{dayName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg bg-card drop-shadow-xs px-2">
|
||||
<div className="grid grid-cols-7 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{WEEK_DAYS_SHORT.map((dayName) => (
|
||||
<span key={dayName} className="px-3 py-2 text-center">
|
||||
{dayName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-px bg-border/60 px-px pb-px pt-px">
|
||||
{days.map((day) => (
|
||||
<div
|
||||
key={day.date}
|
||||
className={cn("h-[150px] bg-card p-0.5", !day.isCurrentMonth && "")}
|
||||
>
|
||||
<DayCell day={day} onSelect={onSelectDay} onCreate={onCreateDay} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="grid grid-cols-7 gap-px bg-border/60 px-px pb-px pt-px">
|
||||
{days.map((day) => (
|
||||
<div
|
||||
key={day.date}
|
||||
className={cn("h-[150px] bg-card p-0.5", !day.isCurrentMonth && "")}
|
||||
>
|
||||
<DayCell day={day} onSelect={onSelectDay} onCreate={onCreateDay} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,29 +5,29 @@ import type { CalendarEvent } from "@/components/calendario/types";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
|
||||
const LEGEND_ITEMS: Array<{
|
||||
type?: CalendarEvent["type"];
|
||||
label: string;
|
||||
dotColor?: string;
|
||||
type?: CalendarEvent["type"];
|
||||
label: string;
|
||||
dotColor?: string;
|
||||
}> = [
|
||||
{ type: "lancamento", label: "Lançamentos" },
|
||||
{ type: "boleto", label: "Boleto com vencimento" },
|
||||
{ type: "cartao", label: "Vencimento de cartão" },
|
||||
{ label: "Pagamento fatura", dotColor: "bg-green-600" },
|
||||
{ type: "lancamento", label: "Lançamentos" },
|
||||
{ type: "boleto", label: "Boleto com vencimento" },
|
||||
{ type: "cartao", label: "Vencimento de cartão" },
|
||||
{ label: "Pagamento fatura", dotColor: "bg-green-600" },
|
||||
];
|
||||
|
||||
export function CalendarLegend() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
|
||||
{LEGEND_ITEMS.map((item, index) => {
|
||||
const dotColor =
|
||||
item.dotColor || (item.type ? EVENT_TYPE_STYLES[item.type].dot : "");
|
||||
return (
|
||||
<span key={item.type || index} className="flex items-center gap-2">
|
||||
<span className={cn("size-3 rounded-full", dotColor)} />
|
||||
{item.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
|
||||
{LEGEND_ITEMS.map((item, index) => {
|
||||
const dotColor =
|
||||
item.dotColor || (item.type ? EVENT_TYPE_STYLES[item.type].dot : "");
|
||||
return (
|
||||
<span key={item.type || index} className="flex items-center gap-2">
|
||||
<span className={cn("size-3 rounded-full", dotColor)} />
|
||||
{item.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,185 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { RiAddLine } from "@remixicon/react";
|
||||
import type { KeyboardEvent, MouseEvent } from "react";
|
||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { RiAddLine } from "@remixicon/react";
|
||||
import type { KeyboardEvent, MouseEvent } from "react";
|
||||
|
||||
type DayCellProps = {
|
||||
day: CalendarDay;
|
||||
onSelect: (day: CalendarDay) => void;
|
||||
onCreate: (day: CalendarDay) => void;
|
||||
day: CalendarDay;
|
||||
onSelect: (day: CalendarDay) => void;
|
||||
onCreate: (day: CalendarDay) => void;
|
||||
};
|
||||
|
||||
export const EVENT_TYPE_STYLES: Record<
|
||||
CalendarEvent["type"],
|
||||
{ wrapper: string; dot: string; accent?: string }
|
||||
CalendarEvent["type"],
|
||||
{ wrapper: string; dot: string; accent?: string }
|
||||
> = {
|
||||
lancamento: {
|
||||
wrapper:
|
||||
"bg-orange-100 text-orange-600 dark:bg-orange-900/10 dark:text-orange-50 border-l-4 border-orange-500",
|
||||
dot: "bg-orange-600",
|
||||
},
|
||||
boleto: {
|
||||
wrapper:
|
||||
"bg-blue-100 text-blue-600 dark:bg-blue-900/10 dark:text-blue-50 border-l-4 border-blue-500",
|
||||
dot: "bg-blue-600",
|
||||
},
|
||||
cartao: {
|
||||
wrapper:
|
||||
"bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-50 border-l-4 border-violet-500",
|
||||
dot: "bg-violet-600",
|
||||
},
|
||||
lancamento: {
|
||||
wrapper:
|
||||
"bg-orange-100 text-orange-600 dark:bg-orange-900/10 dark:text-orange-50 border-l-4 border-orange-500",
|
||||
dot: "bg-orange-600",
|
||||
},
|
||||
boleto: {
|
||||
wrapper:
|
||||
"bg-blue-100 text-blue-600 dark:bg-blue-900/10 dark:text-blue-50 border-l-4 border-blue-500",
|
||||
dot: "bg-blue-600",
|
||||
},
|
||||
cartao: {
|
||||
wrapper:
|
||||
"bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-50 border-l-4 border-violet-500",
|
||||
dot: "bg-violet-600",
|
||||
},
|
||||
};
|
||||
|
||||
const eventStyles = EVENT_TYPE_STYLES;
|
||||
|
||||
const formatCurrencyValue = (value: number | null | undefined) =>
|
||||
currencyFormatter.format(Math.abs(value ?? 0));
|
||||
currencyFormatter.format(Math.abs(value ?? 0));
|
||||
|
||||
const formatAmount = (event: Extract<CalendarEvent, { type: "lancamento" }>) =>
|
||||
formatCurrencyValue(event.lancamento.amount);
|
||||
formatCurrencyValue(event.lancamento.amount);
|
||||
|
||||
const buildEventLabel = (event: CalendarEvent) => {
|
||||
switch (event.type) {
|
||||
case "lancamento": {
|
||||
return event.lancamento.name;
|
||||
}
|
||||
case "boleto": {
|
||||
return event.lancamento.name;
|
||||
}
|
||||
case "cartao": {
|
||||
return event.card.name;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
switch (event.type) {
|
||||
case "lancamento": {
|
||||
return event.lancamento.name;
|
||||
}
|
||||
case "boleto": {
|
||||
return event.lancamento.name;
|
||||
}
|
||||
case "cartao": {
|
||||
return event.card.name;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const buildEventComplement = (event: CalendarEvent) => {
|
||||
switch (event.type) {
|
||||
case "lancamento": {
|
||||
return formatAmount(event);
|
||||
}
|
||||
case "boleto": {
|
||||
return formatCurrencyValue(event.lancamento.amount);
|
||||
}
|
||||
case "cartao": {
|
||||
if (event.card.totalDue !== null) {
|
||||
return formatCurrencyValue(event.card.totalDue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
switch (event.type) {
|
||||
case "lancamento": {
|
||||
return formatAmount(event);
|
||||
}
|
||||
case "boleto": {
|
||||
return formatCurrencyValue(event.lancamento.amount);
|
||||
}
|
||||
case "cartao": {
|
||||
if (event.card.totalDue !== null) {
|
||||
return formatCurrencyValue(event.card.totalDue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isPagamentoFatura = (event: CalendarEvent) => {
|
||||
return (
|
||||
event.type === "lancamento" &&
|
||||
event.lancamento.name.startsWith("Pagamento fatura -")
|
||||
);
|
||||
return (
|
||||
event.type === "lancamento" &&
|
||||
event.lancamento.name.startsWith("Pagamento fatura -")
|
||||
);
|
||||
};
|
||||
|
||||
const getEventStyle = (event: CalendarEvent) => {
|
||||
if (isPagamentoFatura(event)) {
|
||||
return {
|
||||
wrapper:
|
||||
"bg-green-100 text-green-600 dark:bg-green-900/10 dark:text-green-50 border-l-4 border-green-500",
|
||||
dot: "bg-green-600",
|
||||
};
|
||||
}
|
||||
return eventStyles[event.type];
|
||||
if (isPagamentoFatura(event)) {
|
||||
return {
|
||||
wrapper:
|
||||
"bg-green-100 text-green-600 dark:bg-green-900/10 dark:text-green-50 border-l-4 border-green-500",
|
||||
dot: "bg-green-600",
|
||||
};
|
||||
}
|
||||
return eventStyles[event.type];
|
||||
};
|
||||
|
||||
const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
||||
const complement = buildEventComplement(event);
|
||||
const label = buildEventLabel(event);
|
||||
const style = getEventStyle(event);
|
||||
const complement = buildEventComplement(event);
|
||||
const label = buildEventLabel(event);
|
||||
const style = getEventStyle(event);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded p-1 text-xs",
|
||||
style.wrapper
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
{complement ? (
|
||||
<span
|
||||
className={cn("shrink-0 font-semibold", style.accent ?? "text-xs")}
|
||||
>
|
||||
{complement}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded p-1 text-xs",
|
||||
style.wrapper,
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
{complement ? (
|
||||
<span
|
||||
className={cn("shrink-0 font-semibold", style.accent ?? "text-xs")}
|
||||
>
|
||||
{complement}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
||||
const previewEvents = day.events.slice(0, 3);
|
||||
const hasOverflow = day.events.length > 3;
|
||||
const previewEvents = day.events.slice(0, 3);
|
||||
const hasOverflow = day.events.length > 3;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" || event.key === " " || event.key === "Space") {
|
||||
event.preventDefault();
|
||||
onSelect(day);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" || event.key === " " || event.key === "Space") {
|
||||
event.preventDefault();
|
||||
onSelect(day);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
onCreate(day);
|
||||
};
|
||||
const handleCreateClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
onCreate(day);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
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-primary/10",
|
||||
!day.isCurrentMonth && "opacity-60",
|
||||
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span
|
||||
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-foreground/90"
|
||||
)}
|
||||
>
|
||||
{day.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateClick}
|
||||
className="flex size-6 items-center justify-center rounded-full border 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"
|
||||
aria-label={`Criar lançamento em ${day.date}`}
|
||||
>
|
||||
<RiAddLine className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
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-primary/10",
|
||||
!day.isCurrentMonth && "opacity-60",
|
||||
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span
|
||||
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-foreground/90",
|
||||
)}
|
||||
>
|
||||
{day.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateClick}
|
||||
className="flex size-6 items-center justify-center rounded-full border 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"
|
||||
aria-label={`Criar lançamento em ${day.date}`}
|
||||
>
|
||||
<RiAddLine className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
{previewEvents.map((event) => (
|
||||
<DayEventPreview key={event.id} event={event} />
|
||||
))}
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
{previewEvents.map((event) => (
|
||||
<DayEventPreview key={event.id} event={event} />
|
||||
))}
|
||||
|
||||
{hasOverflow ? (
|
||||
<span className="text-xs font-medium text-primary/80">
|
||||
+ ver mais
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{hasOverflow ? (
|
||||
<span className="text-xs font-medium text-primary/80">
|
||||
+ ver mais
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,208 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
|
||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { friendlyDate, parseLocalDateString } from "@/lib/utils/date";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import MoneyValues from "../money-values";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Card } from "../ui/card";
|
||||
|
||||
type EventModalProps = {
|
||||
open: boolean;
|
||||
day: CalendarDay | null;
|
||||
onClose: () => void;
|
||||
onCreate: (date: string) => void;
|
||||
open: boolean;
|
||||
day: CalendarDay | null;
|
||||
onClose: () => void;
|
||||
onCreate: (date: string) => void;
|
||||
};
|
||||
|
||||
const EventCard = ({
|
||||
children,
|
||||
type,
|
||||
isPagamentoFatura = false,
|
||||
children,
|
||||
type,
|
||||
isPagamentoFatura = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
type: CalendarEvent["type"];
|
||||
isPagamentoFatura?: boolean;
|
||||
children: ReactNode;
|
||||
type: CalendarEvent["type"];
|
||||
isPagamentoFatura?: boolean;
|
||||
}) => {
|
||||
const style = isPagamentoFatura
|
||||
? { dot: "bg-green-600" }
|
||||
: EVENT_TYPE_STYLES[type];
|
||||
return (
|
||||
<Card className="flex flex-row gap-2 p-3 mb-1">
|
||||
<span
|
||||
className={cn("mt-1 size-3 shrink-0 rounded-full", style.dot)}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</Card>
|
||||
);
|
||||
const style = isPagamentoFatura
|
||||
? { dot: "bg-green-600" }
|
||||
: EVENT_TYPE_STYLES[type];
|
||||
return (
|
||||
<Card className="flex flex-row gap-2 p-3 mb-1">
|
||||
<span
|
||||
className={cn("mt-1 size-3 shrink-0 rounded-full", style.dot)}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLancamento = (
|
||||
event: Extract<CalendarEvent, { type: "lancamento" }>
|
||||
event: Extract<CalendarEvent, { type: "lancamento" }>,
|
||||
) => {
|
||||
const isReceita = event.lancamento.transactionType === "Receita";
|
||||
const isPagamentoFatura =
|
||||
event.lancamento.name.startsWith("Pagamento fatura -");
|
||||
const isReceita = event.lancamento.transactionType === "Receita";
|
||||
const isPagamentoFatura =
|
||||
event.lancamento.name.startsWith("Pagamento fatura -");
|
||||
|
||||
return (
|
||||
<EventCard type="lancamento" isPagamentoFatura={isPagamentoFatura}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span
|
||||
className={`text-sm font-semibold leading-tight ${
|
||||
isPagamentoFatura && "text-green-600 dark:text-green-400"
|
||||
}`}
|
||||
>
|
||||
{event.lancamento.name}
|
||||
</span>
|
||||
return (
|
||||
<EventCard type="lancamento" isPagamentoFatura={isPagamentoFatura}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span
|
||||
className={`text-sm font-semibold leading-tight ${
|
||||
isPagamentoFatura && "text-green-600 dark:text-green-400"
|
||||
}`}
|
||||
>
|
||||
{event.lancamento.name}
|
||||
</span>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Badge variant={"outline"}>{event.lancamento.condition}</Badge>
|
||||
<Badge variant={"outline"}>{event.lancamento.paymentMethod}</Badge>
|
||||
<Badge variant={"outline"}>{event.lancamento.categoriaName}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-semibold whitespace-nowrap",
|
||||
isReceita ? "text-green-600 dark:text-green-400" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<MoneyValues
|
||||
showPositiveSign
|
||||
className="text-base"
|
||||
amount={event.lancamento.amount}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</EventCard>
|
||||
);
|
||||
<div className="flex gap-1">
|
||||
<Badge variant={"outline"}>{event.lancamento.condition}</Badge>
|
||||
<Badge variant={"outline"}>{event.lancamento.paymentMethod}</Badge>
|
||||
<Badge variant={"outline"}>{event.lancamento.categoriaName}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-semibold whitespace-nowrap",
|
||||
isReceita
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-foreground",
|
||||
)}
|
||||
>
|
||||
<MoneyValues
|
||||
showPositiveSign
|
||||
className="text-base"
|
||||
amount={event.lancamento.amount}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</EventCard>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
|
||||
const isPaid = Boolean(event.lancamento.isSettled);
|
||||
const dueDate = event.lancamento.dueDate;
|
||||
const formattedDueDate = dueDate
|
||||
? new Intl.DateTimeFormat("pt-BR").format(new Date(dueDate))
|
||||
: null;
|
||||
const isPaid = Boolean(event.lancamento.isSettled);
|
||||
const dueDate = event.lancamento.dueDate;
|
||||
const formattedDueDate = dueDate
|
||||
? new Intl.DateTimeFormat("pt-BR").format(new Date(dueDate))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<EventCard type="boleto">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="text-sm font-semibold leading-tight">
|
||||
{event.lancamento.name}
|
||||
</span>
|
||||
return (
|
||||
<EventCard type="boleto">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="text-sm font-semibold leading-tight">
|
||||
{event.lancamento.name}
|
||||
</span>
|
||||
|
||||
{formattedDueDate && (
|
||||
<span className="text-xs text-muted-foreground leading-tight">
|
||||
Vence em {formattedDueDate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{formattedDueDate && (
|
||||
<span className="text-xs text-muted-foreground leading-tight">
|
||||
Vence em {formattedDueDate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Badge variant={"outline"}>{isPaid ? "Pago" : "Pendente"}</Badge>
|
||||
</div>
|
||||
<span className="font-semibold">
|
||||
<MoneyValues amount={event.lancamento.amount} />
|
||||
</span>
|
||||
</div>
|
||||
</EventCard>
|
||||
);
|
||||
<Badge variant={"outline"}>{isPaid ? "Pago" : "Pendente"}</Badge>
|
||||
</div>
|
||||
<span className="font-semibold">
|
||||
<MoneyValues amount={event.lancamento.amount} />
|
||||
</span>
|
||||
</div>
|
||||
</EventCard>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCard = (event: Extract<CalendarEvent, { type: "cartao" }>) => (
|
||||
<EventCard type="cartao">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="text-sm font-semibold leading-tight">
|
||||
Vencimento Fatura - {event.card.name}
|
||||
</span>
|
||||
</div>
|
||||
<EventCard type="cartao">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="text-sm font-semibold leading-tight">
|
||||
Vencimento Fatura - {event.card.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Badge variant={"outline"}>{event.card.status ?? "Fatura"}</Badge>
|
||||
</div>
|
||||
{event.card.totalDue !== null ? (
|
||||
<span className="font-semibold">
|
||||
<MoneyValues amount={event.card.totalDue} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</EventCard>
|
||||
<Badge variant={"outline"}>{event.card.status ?? "Fatura"}</Badge>
|
||||
</div>
|
||||
{event.card.totalDue !== null ? (
|
||||
<span className="font-semibold">
|
||||
<MoneyValues amount={event.card.totalDue} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</EventCard>
|
||||
);
|
||||
|
||||
const renderEvent = (event: CalendarEvent) => {
|
||||
switch (event.type) {
|
||||
case "lancamento":
|
||||
return renderLancamento(event);
|
||||
case "boleto":
|
||||
return renderBoleto(event);
|
||||
case "cartao":
|
||||
return renderCard(event);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
switch (event.type) {
|
||||
case "lancamento":
|
||||
return renderLancamento(event);
|
||||
case "boleto":
|
||||
return renderBoleto(event);
|
||||
case "cartao":
|
||||
return renderCard(event);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
|
||||
const formattedDate = !day
|
||||
? ""
|
||||
: friendlyDate(parseLocalDateString(day.date));
|
||||
const formattedDate = !day
|
||||
? ""
|
||||
: friendlyDate(parseLocalDateString(day.date));
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!day) return;
|
||||
onClose();
|
||||
onCreate(day.date);
|
||||
};
|
||||
const handleCreate = () => {
|
||||
if (!day) return;
|
||||
onClose();
|
||||
onCreate(day.date);
|
||||
};
|
||||
|
||||
const description = day?.events.length
|
||||
? "Confira os lançamentos e vencimentos cadastrados para este dia."
|
||||
: "Nenhum lançamento encontrado para este dia. Você pode criar um novo lançamento agora.";
|
||||
const description = day?.events.length
|
||||
? "Confira os lançamentos e vencimentos cadastrados para este dia."
|
||||
: "Nenhum lançamento encontrado para este dia. Você pode criar um novo lançamento agora.";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => (!value ? onClose() : null)}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formattedDate}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => (!value ? onClose() : null)}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formattedDate}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-[380px] space-y-2 overflow-y-auto pr-2">
|
||||
{day?.events.length ? (
|
||||
day.events.map((event) => (
|
||||
<div key={event.id}>{renderEvent(event)}</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-border/60 bg-muted/30 p-6 text-center text-sm text-muted-foreground">
|
||||
Nenhum lançamento ou vencimento registrado. Clique em{" "}
|
||||
<span className="font-medium text-primary">Novo lançamento</span>{" "}
|
||||
para começar.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-[380px] space-y-2 overflow-y-auto pr-2">
|
||||
{day?.events.length ? (
|
||||
day.events.map((event) => (
|
||||
<div key={event.id}>{renderEvent(event)}</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-border/60 bg-muted/30 p-6 text-center text-sm text-muted-foreground">
|
||||
Nenhum lançamento ou vencimento registrado. Clique em{" "}
|
||||
<span className="font-medium text-primary">Novo lançamento</span>{" "}
|
||||
para começar.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!day}>
|
||||
Novo lançamento
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogFooter className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!day}>
|
||||
Novo lançamento
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,126 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
||||
|
||||
import type {
|
||||
CalendarDay,
|
||||
CalendarEvent,
|
||||
CalendarFormOptions,
|
||||
CalendarPeriod,
|
||||
} from "@/components/calendario/types";
|
||||
import { buildCalendarDays } from "@/components/calendario/utils";
|
||||
import { CalendarGrid } from "@/components/calendario/calendar-grid";
|
||||
import { CalendarLegend } from "@/components/calendario/calendar-legend";
|
||||
import { EventModal } from "@/components/calendario/event-modal";
|
||||
import type {
|
||||
CalendarDay,
|
||||
CalendarEvent,
|
||||
CalendarFormOptions,
|
||||
CalendarPeriod,
|
||||
} from "@/components/calendario/types";
|
||||
import { buildCalendarDays } from "@/components/calendario/utils";
|
||||
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
||||
|
||||
type MonthlyCalendarProps = {
|
||||
period: CalendarPeriod;
|
||||
events: CalendarEvent[];
|
||||
formOptions: CalendarFormOptions;
|
||||
period: CalendarPeriod;
|
||||
events: CalendarEvent[];
|
||||
formOptions: CalendarFormOptions;
|
||||
};
|
||||
|
||||
const parsePeriod = (period: string) => {
|
||||
const [yearStr, monthStr] = period.split("-");
|
||||
const year = Number.parseInt(yearStr ?? "", 10);
|
||||
const month = Number.parseInt(monthStr ?? "", 10);
|
||||
const [yearStr, monthStr] = period.split("-");
|
||||
const year = Number.parseInt(yearStr ?? "", 10);
|
||||
const month = Number.parseInt(monthStr ?? "", 10);
|
||||
|
||||
return { year, monthIndex: month - 1 };
|
||||
return { year, monthIndex: month - 1 };
|
||||
};
|
||||
|
||||
export function MonthlyCalendar({
|
||||
period,
|
||||
events,
|
||||
formOptions,
|
||||
period,
|
||||
events,
|
||||
formOptions,
|
||||
}: MonthlyCalendarProps) {
|
||||
const { year, monthIndex } = useMemo(
|
||||
() => parsePeriod(period.period),
|
||||
[period.period]
|
||||
);
|
||||
const { year, monthIndex } = useMemo(
|
||||
() => parsePeriod(period.period),
|
||||
[period.period],
|
||||
);
|
||||
|
||||
const eventsByDay = useMemo(() => {
|
||||
const map = new Map<string, CalendarEvent[]>();
|
||||
events.forEach((event) => {
|
||||
const list = map.get(event.date) ?? [];
|
||||
list.push(event);
|
||||
map.set(event.date, list);
|
||||
});
|
||||
return map;
|
||||
}, [events]);
|
||||
const eventsByDay = useMemo(() => {
|
||||
const map = new Map<string, CalendarEvent[]>();
|
||||
events.forEach((event) => {
|
||||
const list = map.get(event.date) ?? [];
|
||||
list.push(event);
|
||||
map.set(event.date, list);
|
||||
});
|
||||
return map;
|
||||
}, [events]);
|
||||
|
||||
const days = useMemo(
|
||||
() => buildCalendarDays({ year, monthIndex, events: eventsByDay }),
|
||||
[eventsByDay, monthIndex, year]
|
||||
);
|
||||
const days = useMemo(
|
||||
() => buildCalendarDays({ year, monthIndex, events: eventsByDay }),
|
||||
[eventsByDay, monthIndex, year],
|
||||
);
|
||||
|
||||
const [selectedDay, setSelectedDay] = useState<CalendarDay | null>(null);
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createDate, setCreateDate] = useState<string | null>(null);
|
||||
const [selectedDay, setSelectedDay] = useState<CalendarDay | null>(null);
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createDate, setCreateDate] = useState<string | null>(null);
|
||||
|
||||
const handleOpenCreate = useCallback((date: string) => {
|
||||
setCreateDate(date);
|
||||
setModalOpen(false);
|
||||
setCreateOpen(true);
|
||||
}, []);
|
||||
const handleOpenCreate = useCallback((date: string) => {
|
||||
setCreateDate(date);
|
||||
setModalOpen(false);
|
||||
setCreateOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDaySelect = useCallback((day: CalendarDay) => {
|
||||
setSelectedDay(day);
|
||||
setModalOpen(true);
|
||||
}, []);
|
||||
const handleDaySelect = useCallback((day: CalendarDay) => {
|
||||
setSelectedDay(day);
|
||||
setModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCreateFromCell = useCallback(
|
||||
(day: CalendarDay) => {
|
||||
handleOpenCreate(day.date);
|
||||
},
|
||||
[handleOpenCreate]
|
||||
);
|
||||
const handleCreateFromCell = useCallback(
|
||||
(day: CalendarDay) => {
|
||||
handleOpenCreate(day.date);
|
||||
},
|
||||
[handleOpenCreate],
|
||||
);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setModalOpen(false);
|
||||
setSelectedDay(null);
|
||||
}, []);
|
||||
const handleModalClose = useCallback(() => {
|
||||
setModalOpen(false);
|
||||
setSelectedDay(null);
|
||||
}, []);
|
||||
|
||||
const handleCreateDialogChange = useCallback((open: boolean) => {
|
||||
setCreateOpen(open);
|
||||
if (!open) {
|
||||
setCreateDate(null);
|
||||
}
|
||||
}, []);
|
||||
const handleCreateDialogChange = useCallback((open: boolean) => {
|
||||
setCreateOpen(open);
|
||||
if (!open) {
|
||||
setCreateDate(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<CalendarLegend />
|
||||
<CalendarGrid
|
||||
days={days}
|
||||
onSelectDay={handleDaySelect}
|
||||
onCreateDay={handleCreateFromCell}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<CalendarLegend />
|
||||
<CalendarGrid
|
||||
days={days}
|
||||
onSelectDay={handleDaySelect}
|
||||
onCreateDay={handleCreateFromCell}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EventModal
|
||||
open={isModalOpen}
|
||||
day={selectedDay}
|
||||
onClose={handleModalClose}
|
||||
onCreate={handleOpenCreate}
|
||||
/>
|
||||
<EventModal
|
||||
open={isModalOpen}
|
||||
day={selectedDay}
|
||||
onClose={handleModalClose}
|
||||
onCreate={handleOpenCreate}
|
||||
/>
|
||||
|
||||
<LancamentoDialog
|
||||
mode="create"
|
||||
open={createOpen}
|
||||
onOpenChange={handleCreateDialogChange}
|
||||
pagadorOptions={formOptions.pagadorOptions}
|
||||
splitPagadorOptions={formOptions.splitPagadorOptions}
|
||||
defaultPagadorId={formOptions.defaultPagadorId}
|
||||
contaOptions={formOptions.contaOptions}
|
||||
cartaoOptions={formOptions.cartaoOptions}
|
||||
categoriaOptions={formOptions.categoriaOptions}
|
||||
estabelecimentos={formOptions.estabelecimentos}
|
||||
defaultPeriod={period.period}
|
||||
defaultPurchaseDate={createDate ?? undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
<LancamentoDialog
|
||||
mode="create"
|
||||
open={createOpen}
|
||||
onOpenChange={handleCreateDialogChange}
|
||||
pagadorOptions={formOptions.pagadorOptions}
|
||||
splitPagadorOptions={formOptions.splitPagadorOptions}
|
||||
defaultPagadorId={formOptions.defaultPagadorId}
|
||||
contaOptions={formOptions.contaOptions}
|
||||
cartaoOptions={formOptions.cartaoOptions}
|
||||
categoriaOptions={formOptions.categoriaOptions}
|
||||
estabelecimentos={formOptions.estabelecimentos}
|
||||
defaultPeriod={period.period}
|
||||
defaultPurchaseDate={createDate ?? undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
import type { LancamentoItem, SelectOption } from "@/components/lancamentos/types";
|
||||
import type {
|
||||
LancamentoItem,
|
||||
SelectOption,
|
||||
} from "@/components/lancamentos/types";
|
||||
|
||||
export type CalendarEventType = "lancamento" | "boleto" | "cartao";
|
||||
|
||||
export type CalendarEvent =
|
||||
| {
|
||||
id: string;
|
||||
type: "lancamento";
|
||||
date: string;
|
||||
lancamento: LancamentoItem;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: "boleto";
|
||||
date: string;
|
||||
lancamento: LancamentoItem;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: "cartao";
|
||||
date: string;
|
||||
card: {
|
||||
id: string;
|
||||
name: string;
|
||||
dueDay: string;
|
||||
closingDay: string;
|
||||
brand: string | null;
|
||||
status: string;
|
||||
logo: string | null;
|
||||
totalDue: number | null;
|
||||
};
|
||||
};
|
||||
| {
|
||||
id: string;
|
||||
type: "lancamento";
|
||||
date: string;
|
||||
lancamento: LancamentoItem;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: "boleto";
|
||||
date: string;
|
||||
lancamento: LancamentoItem;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: "cartao";
|
||||
date: string;
|
||||
card: {
|
||||
id: string;
|
||||
name: string;
|
||||
dueDay: string;
|
||||
closingDay: string;
|
||||
brand: string | null;
|
||||
status: string;
|
||||
logo: string | null;
|
||||
totalDue: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type CalendarPeriod = {
|
||||
period: string;
|
||||
monthName: string;
|
||||
year: number;
|
||||
period: string;
|
||||
monthName: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type CalendarDay = {
|
||||
date: string;
|
||||
label: string;
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
events: CalendarEvent[];
|
||||
date: string;
|
||||
label: string;
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
events: CalendarEvent[];
|
||||
};
|
||||
|
||||
export type CalendarFormOptions = {
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
};
|
||||
|
||||
export type CalendarData = {
|
||||
events: CalendarEvent[];
|
||||
formOptions: CalendarFormOptions;
|
||||
events: CalendarEvent[];
|
||||
formOptions: CalendarFormOptions;
|
||||
};
|
||||
|
||||
@@ -3,59 +3,67 @@ import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||
export const formatDateKey = (date: Date) => date.toISOString().slice(0, 10);
|
||||
|
||||
export const parseDateKey = (value: string) => {
|
||||
const [yearStr, monthStr, dayStr] = value.split("-");
|
||||
const year = Number.parseInt(yearStr ?? "", 10);
|
||||
const month = Number.parseInt(monthStr ?? "", 10);
|
||||
const day = Number.parseInt(dayStr ?? "", 10);
|
||||
const [yearStr, monthStr, dayStr] = value.split("-");
|
||||
const year = Number.parseInt(yearStr ?? "", 10);
|
||||
const month = Number.parseInt(monthStr ?? "", 10);
|
||||
const day = Number.parseInt(dayStr ?? "", 10);
|
||||
|
||||
return new Date(Date.UTC(year, (month ?? 1) - 1, day ?? 1));
|
||||
return new Date(Date.UTC(year, (month ?? 1) - 1, day ?? 1));
|
||||
};
|
||||
|
||||
const getWeekdayIndex = (date: Date) => {
|
||||
const day = date.getUTCDay(); // 0 (domingo) - 6 (sábado)
|
||||
// Ajusta para segunda-feira como primeiro dia
|
||||
return day === 0 ? 6 : day - 1;
|
||||
const day = date.getUTCDay(); // 0 (domingo) - 6 (sábado)
|
||||
// Ajusta para segunda-feira como primeiro dia
|
||||
return day === 0 ? 6 : day - 1;
|
||||
};
|
||||
|
||||
export const buildCalendarDays = ({
|
||||
year,
|
||||
monthIndex,
|
||||
events,
|
||||
year,
|
||||
monthIndex,
|
||||
events,
|
||||
}: {
|
||||
year: number;
|
||||
monthIndex: number;
|
||||
events: Map<string, CalendarEvent[]>;
|
||||
year: number;
|
||||
monthIndex: number;
|
||||
events: Map<string, CalendarEvent[]>;
|
||||
}): CalendarDay[] => {
|
||||
const startOfMonth = new Date(Date.UTC(year, monthIndex, 1));
|
||||
const offset = getWeekdayIndex(startOfMonth);
|
||||
const startDate = new Date(Date.UTC(year, monthIndex, 1 - offset));
|
||||
const totalCells = 42; // 6 semanas
|
||||
const now = new Date();
|
||||
const todayKey = formatDateKey(
|
||||
new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()))
|
||||
);
|
||||
const startOfMonth = new Date(Date.UTC(year, monthIndex, 1));
|
||||
const offset = getWeekdayIndex(startOfMonth);
|
||||
const startDate = new Date(Date.UTC(year, monthIndex, 1 - offset));
|
||||
const totalCells = 42; // 6 semanas
|
||||
const now = new Date();
|
||||
const todayKey = formatDateKey(
|
||||
new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())),
|
||||
);
|
||||
|
||||
const days: CalendarDay[] = [];
|
||||
const days: CalendarDay[] = [];
|
||||
|
||||
for (let index = 0; index < totalCells; index += 1) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + index);
|
||||
for (let index = 0; index < totalCells; index += 1) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + index);
|
||||
|
||||
const dateKey = formatDateKey(currentDate);
|
||||
const isCurrentMonth = currentDate.getUTCMonth() === monthIndex;
|
||||
const dateLabel = currentDate.getUTCDate().toString();
|
||||
const eventsForDay = events.get(dateKey) ?? [];
|
||||
const dateKey = formatDateKey(currentDate);
|
||||
const isCurrentMonth = currentDate.getUTCMonth() === monthIndex;
|
||||
const dateLabel = currentDate.getUTCDate().toString();
|
||||
const eventsForDay = events.get(dateKey) ?? [];
|
||||
|
||||
days.push({
|
||||
date: dateKey,
|
||||
label: dateLabel,
|
||||
isCurrentMonth,
|
||||
isToday: dateKey === todayKey,
|
||||
events: eventsForDay,
|
||||
});
|
||||
}
|
||||
days.push({
|
||||
date: dateKey,
|
||||
label: dateLabel,
|
||||
isCurrentMonth,
|
||||
isToday: dateKey === todayKey,
|
||||
events: eventsForDay,
|
||||
});
|
||||
}
|
||||
|
||||
return days;
|
||||
return days;
|
||||
};
|
||||
|
||||
export const WEEK_DAYS_SHORT = ["Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"];
|
||||
export const WEEK_DAYS_SHORT = [
|
||||
"Seg",
|
||||
"Ter",
|
||||
"Qua",
|
||||
"Qui",
|
||||
"Sex",
|
||||
"Sáb",
|
||||
"Dom",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user