mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
Refatoração estrutural sem mudanças funcionais. Saldo líquido: −428 linhas. Removido: - 14 funções/constantes mortas verificadas via grep no repo todo: validateCategoriaOwnership, getInstallmentAnticipationsAction, getAnticipationDetailsAction, formatDecimalForDb, currencyFormatterNoCents, optionalDecimalSchema, formatMonthLabel, getGoalProgressStatusColorClass, MONTH_PERIOD_PARAM, calculateRemainingInstallments, e 5 funções fetch* não usadas em inbox/queries.ts. - 1 tipo morto (ImportRow) + 2 órfãos consequentes (InstallmentAnticipationWithRelations, GoalProgressStatus convertido em interno). - ~30 export keywords desnecessários (símbolos usados apenas no próprio arquivo). - Re-exports mortos em barrels: EstablishmentLogoPicker, CategoryReportSkeleton, WidgetSkeleton, toNameKey. - Arquivo features/reports/types.ts (barrel inteiro era órfão). Padronizado (PT-BR→EN em identificadores expostos): - 4 constantes globais (LANCAMENTOS_* → TRANSACTIONS_*). - 12 tipos/interfaces (Lancamento*/Pagador*/Estabelecimento* → equivalentes EN). - 13 funções/components exportados (fetchPagador*, EstabelecimentoInput, PagadorInfoCard, etc.). - 5 props cross-file (preLancamentosCount → inboxPendingCount, pagadorAvatarUrl → payerAvatarUrl, etc.). - Mantidas em PT-BR conforme exceção do CLAUDE.md: variáveis locais (pagador, categoria, lancamento), accessor key pagadorName (persistida em preferências), strings de UI. Reorganizado: - transactions/: 14 helpers soltos na raiz movidos para lib/; barrel actions.ts reduzido de 76 linhas de wrappers para 14 linhas de re-exports puros; anticipation-actions.ts movido para actions/anticipation.ts. - dashboard/: 8 helpers soltos consolidados em dashboard/lib/. - reports/: 5 query files na raiz consolidados em reports/lib/. - payers/: detail-actions.ts (21KB) e detail-queries.ts movidos para payers/lib/. - shared/components/: 9 dos 16 componentes soltos agrupados em brand/, widgets/, feedback/. - shared/lib/fetch-json.ts movido para shared/utils/fetch-json.ts. Validação: pnpm exec tsc --noEmit (0 erros), biome check (0 issues), knip (sem unused). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
131 lines
4.0 KiB
TypeScript
131 lines
4.0 KiB
TypeScript
import { RiAtLine, RiCalendarEventLine } from "@remixicon/react";
|
|
import { format } from "date-fns";
|
|
import { ptBR } from "date-fns/locale";
|
|
import { EmptyState } from "@/shared/components/feedback/empty-state";
|
|
import { Card } from "@/shared/components/ui/card";
|
|
import { InboxCard } from "./inbox-card";
|
|
import type { InboxItem } from "./types";
|
|
|
|
// O Companion envia hora local de Brasília com 'Z' literal (não converte para UTC).
|
|
// Por isso, o timestamp armazenado no DB já tem a data correta de Brasília como componente UTC.
|
|
// Basta fatiar o ISO string sem nenhum ajuste para obter a data de Brasília do item.
|
|
function getItemDateKey(date: Date): string {
|
|
return date.toISOString().slice(0, 10);
|
|
}
|
|
|
|
// Para "hoje" e "ontem", precisamos da data real de Brasília (UTC-3).
|
|
function getBrasiliaDateKey(date: Date): string {
|
|
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
|
|
return new Date(date.getTime() - BRASILIA_OFFSET_MS)
|
|
.toISOString()
|
|
.slice(0, 10);
|
|
}
|
|
|
|
function getGroupLabel(dateKey: string): string {
|
|
const now = new Date();
|
|
const todayKey = getBrasiliaDateKey(now);
|
|
const yesterdayKey = getBrasiliaDateKey(
|
|
new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
|
);
|
|
if (dateKey === todayKey) return "Hoje";
|
|
if (dateKey === yesterdayKey) return "Ontem";
|
|
const [year, month, day] = dateKey.split("-").map(Number);
|
|
return format(new Date(year, (month ?? 1) - 1, day), "d 'de' MMMM", {
|
|
locale: ptBR,
|
|
});
|
|
}
|
|
|
|
function groupItemsByDay(
|
|
items: InboxItem[],
|
|
): { label: string; items: InboxItem[] }[] {
|
|
const groups = new Map<string, InboxItem[]>();
|
|
for (const item of items) {
|
|
const key = getItemDateKey(new Date(item.notificationTimestamp));
|
|
const group = groups.get(key);
|
|
if (group) {
|
|
group.push(item);
|
|
} else {
|
|
groups.set(key, [item]);
|
|
}
|
|
}
|
|
const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a));
|
|
return sortedKeys.map((key) => ({
|
|
label: getGroupLabel(key),
|
|
items: groups.get(key) ?? [],
|
|
}));
|
|
}
|
|
|
|
type InboxItemsListProps = {
|
|
items: InboxItem[];
|
|
readonly?: boolean;
|
|
activeApp: string | null;
|
|
appLogoMap: Record<string, string>;
|
|
selectedIds: string[];
|
|
onProcess?: (item: InboxItem) => void;
|
|
onDiscard?: (item: InboxItem) => void;
|
|
onViewDetails?: (item: InboxItem) => void;
|
|
onDelete?: (item: InboxItem) => void;
|
|
onRestoreToPending?: (item: InboxItem) => void;
|
|
onSelectToggle: (id: string) => void;
|
|
};
|
|
|
|
export function InboxItemsList({
|
|
items,
|
|
readonly,
|
|
activeApp,
|
|
appLogoMap,
|
|
selectedIds,
|
|
onProcess,
|
|
onDiscard,
|
|
onViewDetails,
|
|
onDelete,
|
|
onRestoreToPending,
|
|
onSelectToggle,
|
|
}: InboxItemsListProps) {
|
|
if (items.length === 0) {
|
|
const message = activeApp
|
|
? "Nenhuma notificação deste app"
|
|
: readonly
|
|
? "Nenhuma notificação nesta aba"
|
|
: "Nenhum pré-lançamento pendente";
|
|
return (
|
|
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
|
<EmptyState
|
|
media={<RiAtLine className="size-6 text-primary" />}
|
|
title={message}
|
|
description="As notificações capturadas pelo app OpenMonetis Companion aparecerão aqui. Saiba mais em Ajustes > Companion."
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const groups = groupItemsByDay(items);
|
|
|
|
return (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{groups.flatMap((group) =>
|
|
group.items.map((item) => (
|
|
<div key={item.id} className="flex flex-col gap-1.5">
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<RiCalendarEventLine className="size-3 shrink-0" />
|
|
<span className="text-xs font-medium">{group.label}</span>
|
|
</div>
|
|
<InboxCard
|
|
item={item}
|
|
readonly={readonly}
|
|
appLogoMap={appLogoMap}
|
|
onProcess={readonly ? undefined : onProcess}
|
|
onDiscard={readonly ? undefined : onDiscard}
|
|
onViewDetails={readonly ? undefined : onViewDetails}
|
|
onDelete={readonly ? onDelete : undefined}
|
|
onRestoreToPending={readonly ? onRestoreToPending : undefined}
|
|
selected={selectedIds.includes(item.id)}
|
|
onSelectToggle={onSelectToggle}
|
|
/>
|
|
</div>
|
|
)),
|
|
)}
|
|
</div>
|
|
);
|
|
}
|