mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-10 19:21:46 +00:00
fix(inbox): corrigir agrupamento de data por fuso de Brasilia
O Companion envia hora local com 'Z' literal (nao converte para UTC), entao o timestamp no DB ja carrega a data correta de Brasilia. Usava-se +3h no frontend, que deslocava a virada de dia para as 21h locais e fazia compras da tarde aparecerem como 'Ontem'. - getItemDateKey: remove offset (data UTC ja e a data de Brasilia) - getBrasiliaDateKey: usa UTC-3 apenas para calcular hoje/ontem - Paraleliza insercoes no batch endpoint com Promise.allSettled - Usa selectDistinct no fetchInboxSourceApps - Envolve InboxCard em memo e callbacks em useCallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,12 +86,10 @@ export async function POST(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { items } = inboxBatchSchema.parse(body);
|
const { items } = inboxBatchSchema.parse(body);
|
||||||
|
|
||||||
// Processar cada item
|
// Processar todos os itens em paralelo
|
||||||
const results: BatchResult[] = [];
|
const settled = await Promise.allSettled(
|
||||||
|
items.map((item) =>
|
||||||
for (const item of items) {
|
db
|
||||||
try {
|
|
||||||
const [inserted] = await db
|
|
||||||
.insert(inboxItems)
|
.insert(inboxItems)
|
||||||
.values({
|
.values({
|
||||||
userId: tokenRecord.userId,
|
userId: tokenRecord.userId,
|
||||||
@@ -104,22 +102,26 @@ export async function POST(request: Request) {
|
|||||||
parsedAmount: item.parsedAmount?.toString(),
|
parsedAmount: item.parsedAmount?.toString(),
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})
|
})
|
||||||
.returning({ id: inboxItems.id });
|
.returning({ id: inboxItems.id }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
results.push({
|
const results: BatchResult[] = settled.map((result, i) => {
|
||||||
clientId: item.clientId,
|
const item = items[i];
|
||||||
serverId: inserted.id,
|
if (result.status === "fulfilled") {
|
||||||
|
return {
|
||||||
|
clientId: item?.clientId,
|
||||||
|
serverId: result.value[0]?.id,
|
||||||
success: true,
|
success: true,
|
||||||
});
|
};
|
||||||
} catch (error) {
|
}
|
||||||
console.error("[API] Error processing batch item:", error);
|
console.error("[API] Error processing batch item:", result.reason);
|
||||||
results.push({
|
return {
|
||||||
clientId: item.clientId,
|
clientId: item?.clientId,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Erro ao processar notificação",
|
error: "Erro ao processar notificação",
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atualizar último uso do token
|
// Atualizar último uso do token
|
||||||
const clientIp =
|
const clientIp =
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { memo } from "react";
|
||||||
import MoneyValues from "@/shared/components/money-values";
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
@@ -62,7 +63,7 @@ interface InboxCardProps {
|
|||||||
onSelectToggle?: (id: string) => void;
|
onSelectToggle?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InboxCard({
|
export const InboxCard = memo(function InboxCard({
|
||||||
item,
|
item,
|
||||||
readonly,
|
readonly,
|
||||||
appLogoMap,
|
appLogoMap,
|
||||||
@@ -222,4 +223,4 @@ export function InboxCard({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ import { format } from "date-fns";
|
|||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
bulkDeleteInboxItemsAction,
|
bulkDeleteInboxItemsAction,
|
||||||
@@ -57,18 +63,25 @@ import type {
|
|||||||
SelectOption,
|
SelectOption,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
|
|
||||||
const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
|
const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
|
||||||
|
|
||||||
function getDateKey(date: Date): string {
|
// O Companion envia hora local de Brasília com 'Z' literal (não converte para UTC).
|
||||||
const adjusted = new Date(date.getTime() + BRASILIA_OFFSET_MS);
|
// Por isso, o timestamp armazenado no DB já tem a data correta de Brasília como componente UTC.
|
||||||
return adjusted.toISOString().slice(0, 10);
|
// 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 {
|
function getGroupLabel(dateKey: string): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const todayKey = getDateKey(now);
|
const todayKey = getBrasiliaDateKey(now);
|
||||||
const yesterdayKey = getDateKey(
|
const yesterdayKey = getBrasiliaDateKey(
|
||||||
new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||||
);
|
);
|
||||||
if (dateKey === todayKey) return "Hoje";
|
if (dateKey === todayKey) return "Hoje";
|
||||||
@@ -84,7 +97,7 @@ function groupItemsByDay(
|
|||||||
): { label: string; items: InboxItem[] }[] {
|
): { label: string; items: InboxItem[] }[] {
|
||||||
const groups = new Map<string, InboxItem[]>();
|
const groups = new Map<string, InboxItem[]>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const key = getDateKey(new Date(item.notificationTimestamp));
|
const key = getItemDateKey(new Date(item.notificationTimestamp));
|
||||||
const group = groups.get(key);
|
const group = groups.get(key);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.push(item);
|
group.push(item);
|
||||||
@@ -234,20 +247,20 @@ export function InboxPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProcessRequest = (item: InboxItem) => {
|
const handleProcessRequest = useCallback((item: InboxItem) => {
|
||||||
setItemToProcess(item);
|
setItemToProcess(item);
|
||||||
setProcessOpen(true);
|
setProcessOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDetailsRequest = (item: InboxItem) => {
|
const handleDetailsRequest = useCallback((item: InboxItem) => {
|
||||||
setItemDetails(item);
|
setItemDetails(item);
|
||||||
setDetailsOpen(true);
|
setDetailsOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDiscardRequest = (item: InboxItem) => {
|
const handleDiscardRequest = useCallback((item: InboxItem) => {
|
||||||
setItemToDiscard(item);
|
setItemToDiscard(item);
|
||||||
setDiscardOpen(true);
|
setDiscardOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDiscardConfirm = async () => {
|
const handleDiscardConfirm = async () => {
|
||||||
if (!itemToDiscard) return;
|
if (!itemToDiscard) return;
|
||||||
@@ -272,10 +285,10 @@ export function InboxPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRequest = (item: InboxItem) => {
|
const handleDeleteRequest = useCallback((item: InboxItem) => {
|
||||||
setItemToDelete(item);
|
setItemToDelete(item);
|
||||||
setDeleteOpen(true);
|
setDeleteOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!itemToDelete) return;
|
if (!itemToDelete) return;
|
||||||
@@ -300,10 +313,10 @@ export function InboxPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreRequest = (item: InboxItem) => {
|
const handleRestoreRequest = useCallback((item: InboxItem) => {
|
||||||
setItemToRestore(item);
|
setItemToRestore(item);
|
||||||
setRestoreOpen(true);
|
setRestoreOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleRestoreToPendingConfirm = async () => {
|
const handleRestoreToPendingConfirm = async () => {
|
||||||
if (!itemToRestore) return;
|
if (!itemToRestore) return;
|
||||||
@@ -326,13 +339,13 @@ export function InboxPage({
|
|||||||
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
|
setSelectedIds((current) => current.filter((id) => visibleIds.has(id)));
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const toggleSelection = (id: string) => {
|
const toggleSelection = useCallback((id: string) => {
|
||||||
setSelectedIds((current) =>
|
setSelectedIds((current) =>
|
||||||
current.includes(id)
|
current.includes(id)
|
||||||
? current.filter((value) => value !== id)
|
? current.filter((value) => value !== id)
|
||||||
: [...current, id],
|
: [...current, id],
|
||||||
);
|
);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const allSelected = items.length > 0 && selectedIds.length === items.length;
|
const allSelected = items.length > 0 && selectedIds.length === items.length;
|
||||||
|
|
||||||
|
|||||||
@@ -89,15 +89,14 @@ export async function fetchInboxSourceApps(
|
|||||||
status: InboxStatus,
|
status: InboxStatus,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ name: inboxItems.sourceAppName })
|
.selectDistinct({ name: inboxItems.sourceAppName })
|
||||||
.from(inboxItems)
|
.from(inboxItems)
|
||||||
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)));
|
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)));
|
||||||
|
|
||||||
const seen = new Set<string>();
|
return rows
|
||||||
for (const row of rows) {
|
.map((row) => row.name)
|
||||||
if (row.name) seen.add(row.name);
|
.filter((name): name is string => name !== null)
|
||||||
}
|
.sort();
|
||||||
return [...seen].sort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchInboxStatusCounts(
|
export async function fetchInboxStatusCounts(
|
||||||
|
|||||||
Reference in New Issue
Block a user