Paginate Olist monitor history
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m17s

This commit is contained in:
Cauê Faleiros
2026-08-13 13:44:59 -03:00
parent bee9e6dc2e
commit 5fbdd354c7
6 changed files with 134 additions and 45 deletions

View File

@@ -15,7 +15,10 @@ const router = express.Router();
router.get('/olist/status', verifySuperAdmin, async (req, res, next) => {
try {
res.json(await getOlistStatus());
res.json(await getOlistStatus({
runsPage: req.query.runsPage,
runsPageSize: req.query.runsPageSize
}));
} catch (error) {
next(error);
}
@@ -51,7 +54,10 @@ router.post('/olist/sync/stop', verifySuperAdmin, async (req, res, next) => {
router.get('/olist/production-order-imports', verifySuperAdmin, async (req, res, next) => {
try {
res.json(await listProductionOrderImportData());
res.json(await listProductionOrderImportData({
runsPage: req.query.runsPage,
runsPageSize: req.query.runsPageSize
}));
} catch (error) {
next(error);
}

View File

@@ -102,13 +102,24 @@ const getConnection = async () => {
return result.rows[0] || null;
};
const listSyncRuns = async (limit = 5) => {
const result = await pool.query(`
const listSyncRuns = async ({ page = 1, pageSize = 5 } = {}) => {
const normalizedPageSize = Math.max(1, Math.min(Number(pageSize) || 5, 20));
const normalizedPage = Math.max(1, Number(page) || 1);
const offset = (normalizedPage - 1) * normalizedPageSize;
const [runsResult, countResult] = await Promise.all([
pool.query(`
SELECT * FROM olist_sync_runs
ORDER BY started_at DESC
LIMIT $1;
`, [Math.max(1, Math.min(Number(limit) || 5, 20))]);
return result.rows.map(mapRun);
LIMIT $1 OFFSET $2;
`, [normalizedPageSize, offset]),
pool.query('SELECT COUNT(*)::int AS count FROM olist_sync_runs;')
]);
return {
items: runsResult.rows.map(mapRun),
total: Number(countResult.rows[0]?.count || 0),
page: normalizedPage,
pageSize: normalizedPageSize
};
};
const hasScheduledSyncToday = async () => {
@@ -252,8 +263,14 @@ const markInterruptedOlistRuns = async () => {
return result.rowCount || 0;
};
const getOlistStatus = async () => {
const [connection, runs, productCache] = await Promise.all([getConnection(), listSyncRuns(), getProductCacheProgress()]);
const getOlistStatus = async ({ runsPage = 1, runsPageSize = 5 } = {}) => {
const [connection, syncRuns, productCache, latestRunResult, completedRunResult] = await Promise.all([
getConnection(),
listSyncRuns({ page: runsPage, pageSize: runsPageSize }),
getProductCacheProgress(),
pool.query('SELECT * FROM olist_sync_runs ORDER BY started_at DESC LIMIT 1;'),
pool.query("SELECT EXISTS (SELECT 1 FROM olist_sync_runs WHERE status = 'completed') AS exists;")
]);
return {
configured: isOlistConfigured(),
connected: Boolean(connection),
@@ -261,7 +278,12 @@ const getOlistStatus = async () => {
syncInProgress: Boolean(activeSync),
nextScheduledSyncAt,
productCache,
runs
latestRun: latestRunResult.rows[0] ? mapRun(latestRunResult.rows[0]) : null,
hasCompletedRun: Boolean(completedRunResult.rows[0]?.exists),
runs: syncRuns.items,
runsTotal: syncRuns.total,
runsPage: syncRuns.page,
runsPageSize: syncRuns.pageSize
};
};

View File

@@ -218,9 +218,13 @@ const refreshYieldRecommendations = async (client) => {
`);
};
const listProductionOrderImportData = async () => {
const [runs, recommendations, supplierRecommendations] = await Promise.all([
pool.query(`SELECT id, filename, status, total_rows, imported_count, updated_count, skipped_count, failed_count, error_message, started_at, completed_at FROM production_order_import_runs ORDER BY started_at DESC LIMIT 10;`),
const listProductionOrderImportData = async ({ runsPage = 1, runsPageSize = 5 } = {}) => {
const normalizedRunsPageSize = Math.max(1, Math.min(Number(runsPageSize) || 5, 20));
const normalizedRunsPage = Math.max(1, Number(runsPage) || 1);
const runsOffset = (normalizedRunsPage - 1) * normalizedRunsPageSize;
const [runs, runsCount, recommendations, supplierRecommendations] = await Promise.all([
pool.query(`SELECT id, filename, status, total_rows, imported_count, updated_count, skipped_count, failed_count, error_message, started_at, completed_at FROM production_order_import_runs ORDER BY started_at DESC LIMIT $1 OFFSET $2;`, [normalizedRunsPageSize, runsOffset]),
pool.query('SELECT COUNT(*)::int AS count FROM production_order_import_runs;'),
pool.query(`SELECT family_key, sample_count, yield_sample_count, roll_sample_count, avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll, last_completed_date, updated_at FROM production_yield_recommendations ORDER BY family_key;`),
pool.query(`SELECT family_key, supplier, sample_count, yield_sample_count, roll_sample_count, avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll, last_completed_date, updated_at FROM production_yield_supplier_recommendations ORDER BY family_key, supplier;`)
]);
@@ -231,6 +235,9 @@ const listProductionOrderImportData = async () => {
failed: row.failed_count, errorMessage: row.error_message || '', startedAt: row.started_at,
completedAt: row.completed_at
})),
runsTotal: Number(runsCount.rows[0]?.count || 0),
runsPage: normalizedRunsPage,
runsPageSize: normalizedRunsPageSize,
recommendations: recommendations.rows.map(row => ({
familyKey: row.family_key, sampleCount: Number(row.sample_count), yieldSampleCount: Number(row.yield_sample_count),
rollSampleCount: Number(row.roll_sample_count), avgYieldPiecesPerKg: row.avg_yield_pieces_per_kg === null ? null : Number(row.avg_yield_pieces_per_kg),

View File

@@ -276,8 +276,9 @@ export const fetchCatalogSummary = async (): Promise<CatalogSummary> => {
}
};
export const fetchOlistStatus = async (): Promise<OlistStatus> => {
const response = await authFetch('/olist/status', { cache: 'no-store' });
export const fetchOlistStatus = async (runsPage = 1, runsPageSize = 5): Promise<OlistStatus> => {
const params = new URLSearchParams({ runsPage: String(runsPage), runsPageSize: String(runsPageSize) });
const response = await authFetch(`/olist/status?${params}`, { cache: 'no-store' });
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(data?.error || 'Não foi possível consultar a integração Olist.');
return data as OlistStatus;
@@ -314,8 +315,9 @@ export const fetchOlistRunDetails = async (runId: number, eventsPage = 1, produc
return data as OlistRunDetails;
};
export const fetchProductionOrderImportData = async (): Promise<ProductionOrderImportData> => {
const response = await authFetch('/olist/production-order-imports', { cache: 'no-store' });
export const fetchProductionOrderImportData = async (runsPage = 1, runsPageSize = 5): Promise<ProductionOrderImportData> => {
const params = new URLSearchParams({ runsPage: String(runsPage), runsPageSize: String(runsPageSize) });
const response = await authFetch(`/olist/production-order-imports?${params}`, { cache: 'no-store' });
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(data?.error || 'Não foi possível consultar as importações de OP.');
return data as ProductionOrderImportData;

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { AlertTriangle, CheckCircle2, CloudDownload, Database, Download, FileUp, Link2, Loader2, RefreshCw, ScrollText, Square, Timer, X, XCircle } from 'lucide-react';
import { AlertTriangle, CheckCircle2, ChevronLeft, ChevronRight, CloudDownload, Database, Download, FileUp, Link2, Loader2, RefreshCw, ScrollText, Square, Timer, X, XCircle } from 'lucide-react';
import { exportMissingProductionOrderDataCsv, fetchOlistRunDetails, fetchOlistStatus, fetchProductionOrderImportData, getOlistAuthorizationUrl, importFinalizedProductionOrderCsv, startOlistCompositionSync, stopOlistCompositionSync } from '../dataService';
import type { OlistRunDetails, OlistStatus, OlistSyncRun, ProductionOrderImportData } from '../types';
@@ -46,6 +46,34 @@ const yieldFamilyLabels: Record<string, string> = {
BLRM: 'Regata'
};
const PAGE_SIZE = 5;
const PaginationControls = ({
page,
pageSize,
total,
onChange,
label
}: {
page: number;
pageSize: number;
total: number;
onChange: (page: number) => void;
label: string;
}) => {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const start = total ? ((page - 1) * pageSize) + 1 : 0;
const end = Math.min(page * pageSize, total);
return <div className="flex flex-wrap items-center justify-between gap-3 border-t border-dark-border px-4 py-3">
<span className="text-xs font-semibold text-dark-muted">{total ? `${label} ${start}-${end} de ${total.toLocaleString('pt-BR')}` : `Nenhum ${label.toLowerCase()}`}</span>
<div className="flex items-center gap-2">
<button type="button" onClick={() => onChange(page - 1)} disabled={page <= 1} aria-label="Página anterior" title="Página anterior" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-dark-muted transition-colors hover:border-brand-primary hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"><ChevronLeft className="h-4 w-4" /></button>
<span className="min-w-16 text-center text-xs font-bold text-dark-muted">Página {page} de {totalPages}</span>
<button type="button" onClick={() => onChange(page + 1)} disabled={page >= totalPages} aria-label="Próxima página" title="Próxima página" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-dark-muted transition-colors hover:border-brand-primary hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"><ChevronRight className="h-4 w-4" /></button>
</div>
</div>;
};
const OlistSync = () => {
const [status, setStatus] = useState<OlistStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
@@ -60,18 +88,21 @@ const OlistSync = () => {
const [isImportingOrders, setIsImportingOrders] = useState(false);
const [isExportingPendingOrders, setIsExportingPendingOrders] = useState(false);
const [importMessage, setImportMessage] = useState('');
const [syncRunsPage, setSyncRunsPage] = useState(1);
const [importRunsPage, setImportRunsPage] = useState(1);
const [supplierReferencesPage, setSupplierReferencesPage] = useState(1);
const loadStatus = useCallback(async (initial = false) => {
if (initial) setIsLoading(true);
try {
setStatus(await fetchOlistStatus());
setStatus(await fetchOlistStatus(syncRunsPage, PAGE_SIZE));
setError('');
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : 'Não foi possível consultar a Olist.');
} finally {
if (initial) setIsLoading(false);
}
}, []);
}, [syncRunsPage]);
useEffect(() => {
const loadTimeout = window.setTimeout(() => void loadStatus(true), 0);
@@ -97,11 +128,11 @@ const OlistSync = () => {
const loadImportData = useCallback(async () => {
try {
setImportData(await fetchProductionOrderImportData());
setImportData(await fetchProductionOrderImportData(importRunsPage, PAGE_SIZE));
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : 'Não foi possível consultar as importações de OP.');
}
}, []);
}, [importRunsPage]);
useEffect(() => {
const timeoutId = window.setTimeout(() => void loadImportData(), 0);
@@ -114,13 +145,18 @@ const OlistSync = () => {
return () => window.clearInterval(intervalId);
}, [selectedRunId, runDetails?.run.status, detailsPages, loadRunDetails]);
const latestRun = status?.runs[0] || null;
const latestRun = status?.latestRun || null;
const tokenExpired = Boolean(status?.tokenExpiresAt && new Date(status.tokenExpiresAt).getTime() <= now);
const completionPercent = useMemo(() => {
const cache = status?.productCache;
if (!cache?.total) return 0;
return Math.round((cache.checked / cache.total) * 100);
}, [status?.productCache]);
const supplierReferences = useMemo(() => (importData?.supplierRecommendations || [])
.filter(item => item.recommendedUnitsPerRoll)
.sort((left, right) => left.familyKey.localeCompare(right.familyKey) || left.supplier.localeCompare(right.supplier)), [importData?.supplierRecommendations]);
const supplierReferencesTotalPages = Math.max(1, Math.ceil(supplierReferences.length / PAGE_SIZE));
const visibleSupplierReferences = supplierReferences.slice((Math.min(supplierReferencesPage, supplierReferencesTotalPages) - 1) * PAGE_SIZE, Math.min(supplierReferencesPage, supplierReferencesTotalPages) * PAGE_SIZE);
const connect = async () => {
setIsActionLoading(true);
@@ -135,7 +171,7 @@ const OlistSync = () => {
const startSync = async () => {
setIsActionLoading(true);
try {
await startOlistCompositionSync(!status?.runs.some(run => run.status === 'completed'));
await startOlistCompositionSync(!status?.hasCompletedRun);
await loadStatus();
} catch (actionError) {
setError(actionError instanceof Error ? actionError.message : 'Não foi possível iniciar a sincronização.');
@@ -167,8 +203,12 @@ const OlistSync = () => {
setImportMessage('');
try {
const result = await importFinalizedProductionOrderCsv(file.name, await file.text());
setImportRunsPage(1);
setImportData({
runs: result.runs,
runsTotal: result.runsTotal,
runsPage: result.runsPage,
runsPageSize: result.runsPageSize,
recommendations: result.recommendations,
supplierRecommendations: result.supplierRecommendations
});
@@ -294,15 +334,24 @@ const OlistSync = () => {
</div>
)}
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card">
<div className="border-b border-dark-border p-5"><h2 className="text-sm font-bold text-dark-text">Histórico de sincronizações</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Execuções automáticas e manuais da sincronização de composições.</p></div>
<div className="divide-y divide-dark-border">
{!status?.runs.length && <div className="p-8 text-center text-sm font-semibold text-dark-muted">Nenhuma sincronização iniciada.</div>}
{status?.runs.map(run => <div key={run.id} className="flex flex-col gap-3 p-4 md:flex-row md:items-center md:justify-between"><div><p className="text-sm font-bold text-dark-text">{run.mode === 'full' ? 'Carga completa' : 'Atualização incremental'} · {run.trigger === 'scheduled' ? 'Automática' : 'Manual'}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{formatDateTime(run.startedAt)} · duração {formatDuration(run.startedAt, run.status === 'running' ? null : run.completedAt, now)}</p></div><div className="flex flex-wrap items-center gap-3 text-xs font-bold text-dark-muted"><span>{run.imported} comp.</span><span>{run.referenceCount} refs.</span><span className={run.failed ? 'text-red-300' : ''}>{run.failed} falhas</span><span className={`rounded-full border px-2.5 py-1 ${runTone(run)}`}>{runStatusLabel(run)}</span><button type="button" onClick={() => openRunDetails(run.id)} title="Ver logs e produtos afetados" aria-label="Ver logs e produtos afetados" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><ScrollText className="h-4 w-4" /></button></div></div>)}
</div>
{!!status && <PaginationControls page={status.runsPage} pageSize={status.runsPageSize} total={status.runsTotal} label="Execuções" onChange={setSyncRunsPage} />}
</div>
<section className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card">
<div className="flex flex-col gap-2 border-b border-dark-border p-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-sm font-bold text-dark-text">OPs finalizadas importadas</h2>
<p className="mt-1 text-xs font-semibold text-dark-muted">CSV histórico para leitura de observações e recomendação de rendimento. Reimportar o mesmo arquivo atualiza as OPs sem duplicá-las.</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">Histórico CSV para recomendações de rendimento. Reimportar o mesmo arquivo atualiza as OPs sem duplicá-las.</p>
</div>
<button type="button" onClick={() => void loadImportData()} title="Atualizar importações" aria-label="Atualizar importações" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><RefreshCw className="h-4 w-4" /></button>
</div>
<div className="grid grid-cols-1 gap-3 p-5 md:grid-cols-5">
<div className="grid grid-cols-1 gap-3 p-5 sm:grid-cols-2 xl:grid-cols-5">
{(['BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM'] as const).map(familyKey => {
const recommendation = importData?.recommendations.find(item => item.familyKey === familyKey);
return <div key={familyKey} className="rounded-xl border border-dark-border bg-dark-input/50 p-3">
@@ -312,29 +361,24 @@ const OlistSync = () => {
</div>;
})}
</div>
{!!importData?.supplierRecommendations.length && <div className="border-t border-dark-border px-5 pb-5 pt-4">
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Referências por fornecedor</p>
<div className="mt-3 flex flex-wrap gap-2">
{importData.supplierRecommendations.filter(item => item.recommendedUnitsPerRoll).map(item => <span key={`${item.familyKey}-${item.supplier}`} className="rounded-full border border-brand-primary/30 bg-brand-primary/10 px-2.5 py-1 text-xs font-bold text-brand-primary">{item.familyKey} · {item.supplier}: {Math.round(item.recommendedUnitsPerRoll!).toLocaleString('pt-BR')} un./rolo</span>)}
<div className="border-t border-dark-border">
<div className="flex flex-col gap-1 px-5 py-4 sm:flex-row sm:items-center sm:justify-between"><div><p className="text-sm font-bold text-dark-text">Referências por fornecedor</p><p className="mt-1 text-xs font-semibold text-dark-muted">Sugestões calculadas a partir das OPs finalizadas com fornecedor e rendimento.</p></div><span className="text-xs font-bold text-dark-muted">{supplierReferences.length.toLocaleString('pt-BR')} referências</span></div>
<div className="divide-y divide-dark-border">
{!supplierReferences.length && <p className="px-5 pb-4 text-sm font-semibold text-dark-muted">Nenhuma referência por fornecedor disponível.</p>}
{visibleSupplierReferences.map(item => <div key={`${item.familyKey}-${item.supplier}`} className="grid gap-2 px-5 py-3 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center"><div><p className="text-sm font-bold text-dark-text">{item.supplier}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{item.familyKey} · {yieldFamilyLabels[item.familyKey] || 'Família de corte'} · {item.sampleCount.toLocaleString('pt-BR')} OPs</p></div><p className="text-sm font-bold text-brand-primary">{Math.round(item.recommendedUnitsPerRoll!).toLocaleString('pt-BR')} un./rolo</p></div>)}
</div>
</div>}
<div className="divide-y divide-dark-border">
{!importData?.runs.length && <p className="p-5 text-sm font-semibold text-dark-muted">Nenhum CSV de OPs finalizadas importado.</p>}
{importData?.runs.map(run => <div key={run.id} className="flex flex-col gap-2 p-4 md:flex-row md:items-center md:justify-between">
<div><p className="text-sm font-bold text-dark-text">{run.filename}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{formatDateTime(run.startedAt)} · {run.totalRows.toLocaleString('pt-BR')} linhas</p>{run.errorMessage && <p className="mt-1 line-clamp-1 text-xs font-semibold text-amber-200">{run.errorMessage}</p>}</div>
<div className="flex flex-wrap gap-3 text-xs font-bold text-dark-muted"><span>{run.imported} novas</span><span>{run.updated} atualizadas</span><span>{run.skipped} ignoradas</span><span className={run.failed ? 'text-red-300' : ''}>{run.failed} falhas</span></div>
</div>)}
{!!supplierReferences.length && <PaginationControls page={Math.min(supplierReferencesPage, supplierReferencesTotalPages)} pageSize={PAGE_SIZE} total={supplierReferences.length} label="Referências" onChange={setSupplierReferencesPage} />}
</div>
<div className="border-t border-dark-border">
<div className="flex items-center justify-between px-5 py-4"><div><p className="text-sm font-bold text-dark-text">Arquivos importados</p><p className="mt-1 text-xs font-semibold text-dark-muted">Status de cada CSV recebido da Olist.</p></div><span className="text-xs font-bold text-dark-muted">{(importData?.runsTotal || 0).toLocaleString('pt-BR')} arquivos</span></div>
<div className="divide-y divide-dark-border">
{!importData?.runs.length && <p className="px-5 pb-4 text-sm font-semibold text-dark-muted">Nenhum CSV de OPs finalizadas importado.</p>}
{importData?.runs.map(run => <div key={run.id} className="grid gap-3 px-5 py-4 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center"><div className="min-w-0"><p className="truncate text-sm font-bold text-dark-text">{run.filename}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{formatDateTime(run.startedAt)} · {run.totalRows.toLocaleString('pt-BR')} linhas</p>{run.errorMessage && <p className="mt-1 line-clamp-1 text-xs font-semibold text-amber-200">{run.errorMessage}</p>}</div><div className="flex flex-wrap gap-x-3 gap-y-1 text-xs font-bold text-dark-muted"><span>{run.imported} novas</span><span>{run.updated} atualizadas</span><span>{run.skipped} ignoradas</span><span className={run.failed ? 'text-red-300' : ''}>{run.failed} falhas</span></div></div>)}
</div>
{!!importData && <PaginationControls page={importData.runsPage} pageSize={importData.runsPageSize} total={importData.runsTotal} label="Arquivos" onChange={setImportRunsPage} />}
</div>
</section>
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card">
<div className="border-b border-dark-border p-5"><h2 className="text-sm font-bold text-dark-text">Histórico de sincronizações</h2></div>
<div className="divide-y divide-dark-border">
{!status?.runs.length && <div className="p-8 text-center text-sm font-semibold text-dark-muted">Nenhuma sincronização iniciada.</div>}
{status?.runs.map(run => <div key={run.id} className="flex flex-col gap-3 p-4 md:flex-row md:items-center md:justify-between"><div><p className="text-sm font-bold text-dark-text">{run.mode === 'full' ? 'Carga completa' : 'Atualização incremental'} · {run.trigger === 'scheduled' ? 'Automática' : 'Manual'}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{formatDateTime(run.startedAt)} · duração {formatDuration(run.startedAt, run.status === 'running' ? null : run.completedAt, now)}</p></div><div className="flex flex-wrap items-center gap-3 text-xs font-bold text-dark-muted"><span>{run.imported} comp.</span><span>{run.referenceCount} refs.</span><span className={run.failed ? 'text-red-300' : ''}>{run.failed} falhas</span><span className={`rounded-full border px-2.5 py-1 ${runTone(run)}`}>{runStatusLabel(run)}</span><button type="button" onClick={() => openRunDetails(run.id)} title="Ver logs e produtos afetados" aria-label="Ver logs e produtos afetados" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><ScrollText className="h-4 w-4" /></button></div></div>)}
</div>
</div>
{selectedRunId && (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/70 p-0 backdrop-blur-sm sm:items-center sm:p-6" role="dialog" aria-modal="true" aria-label="Logs e produtos afetados da sincronização Olist">
<div className="max-h-[92vh] w-full max-w-6xl overflow-y-auto rounded-t-2xl border border-dark-border bg-dark-card shadow-2xl sm:rounded-2xl">

View File

@@ -254,7 +254,12 @@ export interface OlistStatus {
pending: number;
manufactured: number;
};
latestRun: OlistSyncRun | null;
hasCompletedRun: boolean;
runs: OlistSyncRun[];
runsTotal: number;
runsPage: number;
runsPageSize: number;
}
export interface ProductionOrderImportRun {
@@ -289,6 +294,9 @@ export interface ProductionYieldSupplierRecommendation extends ProductionYieldRe
export interface ProductionOrderImportData {
runs: ProductionOrderImportRun[];
runsTotal: number;
runsPage: number;
runsPageSize: number;
recommendations: ProductionYieldRecommendation[];
supplierRecommendations: ProductionYieldSupplierRecommendation[];
}