Export production orders with missing data
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m2s

This commit is contained in:
Cauê Faleiros
2026-08-12 11:41:37 -03:00
parent cfdcc4f2f4
commit e7edb5c483
5 changed files with 117 additions and 5 deletions

View File

@@ -321,6 +321,24 @@ export const fetchProductionOrderImportData = async (): Promise<ProductionOrderI
return data as ProductionOrderImportData;
};
export const exportMissingProductionOrderDataCsv = async (): Promise<number> => {
const response = await authFetch('/olist/production-order-imports/pending-csv', { cache: 'no-store' });
const data = await response.json().catch(() => null);
if (!response.ok || !data?.csv || !data?.filename) {
throw new Error(data?.error || 'Não foi possível exportar as pendências das OPs.');
}
const url = URL.createObjectURL(new Blob([data.csv], { type: 'text/csv;charset=utf-8' }));
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = data.filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
return Number(data.totalRows) || 0;
};
export const importFinalizedProductionOrderCsv = async (filename: string, csv: string): Promise<ProductionOrderImportData & { runId: number; totalRows: number; imported: number; updated: number; skipped: number; failed: number; issues: Array<{ line?: number; number?: string; message: string }> }> => {
const response = await authFetch('/olist/production-order-imports', {
method: 'POST',

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { AlertTriangle, CheckCircle2, CloudDownload, Database, FileUp, Link2, Loader2, RefreshCw, ScrollText, Square, Timer, X, XCircle } from 'lucide-react';
import { fetchOlistRunDetails, fetchOlistStatus, fetchProductionOrderImportData, getOlistAuthorizationUrl, importFinalizedProductionOrderCsv, startOlistCompositionSync, stopOlistCompositionSync } from '../dataService';
import { AlertTriangle, CheckCircle2, 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';
const formatDateTime = (value: string | null) => {
@@ -50,6 +50,7 @@ const OlistSync = () => {
const [isDetailsLoading, setIsDetailsLoading] = useState(false);
const [importData, setImportData] = useState<ProductionOrderImportData | null>(null);
const [isImportingOrders, setIsImportingOrders] = useState(false);
const [isExportingPendingOrders, setIsExportingPendingOrders] = useState(false);
const [importMessage, setImportMessage] = useState('');
const loadStatus = useCallback(async (initial = false) => {
@@ -167,6 +168,19 @@ const OlistSync = () => {
}
};
const exportPendingOrders = async () => {
setIsExportingPendingOrders(true);
setImportMessage('');
try {
const totalRows = await exportMissingProductionOrderDataCsv();
setImportMessage(totalRows ? `${totalRows.toLocaleString('pt-BR')} OPs com pendências exportadas para completar.` : 'Nenhuma OP finalizada possui pendências de dados.');
} catch (actionError) {
setError(actionError instanceof Error ? actionError.message : 'Não foi possível exportar as pendências das OPs.');
} finally {
setIsExportingPendingOrders(false);
}
};
const openRunDetails = (runId: number) => {
const initialPages = { events: 1, products: 1 };
setDetailsPages(initialPages);
@@ -189,6 +203,9 @@ const OlistSync = () => {
<button type="button" onClick={() => void loadStatus()} className="inline-flex h-10 items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer">
<RefreshCw className="h-4 w-4" /> Atualizar
</button>
<button type="button" onClick={() => void exportPendingOrders()} disabled={isExportingPendingOrders} className="inline-flex h-10 items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer">
{isExportingPendingOrders ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4 text-brand-primary" />} Exportar pendências
</button>
<label className={`inline-flex h-10 items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary ${isImportingOrders ? 'cursor-wait opacity-60' : 'cursor-pointer'}`}>
{isImportingOrders ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileUp className="h-4 w-4 text-brand-primary" />} Importar OPs finalizadas
<input type="file" accept=".csv,text/csv" className="sr-only" disabled={isImportingOrders} onChange={(event) => { void importOrders(event.target.files?.[0] || null); event.currentTarget.value = ''; }} />
@@ -216,7 +233,7 @@ const OlistSync = () => {
</div>
{error && <div className="rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm font-bold text-red-300">{error}</div>}
{importMessage && <div className="rounded-2xl border border-emerald-400/30 bg-emerald-400/10 px-4 py-3 text-sm font-bold text-emerald-300">Importação concluída: {importMessage}</div>}
{importMessage && <div className="rounded-2xl border border-emerald-400/30 bg-emerald-400/10 px-4 py-3 text-sm font-bold text-emerald-300">{importMessage.startsWith('Nenhuma') || importMessage.includes('exportadas') ? 'Exportação pronta: ' : 'Importação concluída: '}{importMessage}</div>}
{!status?.configured && <div className="rounded-2xl border border-amber-400/30 bg-amber-400/10 px-4 py-3 text-sm font-bold text-amber-200">Configure as credenciais Olist V3 no ambiente antes de conectar.</div>}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">