Add native Olist composition sync
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
This commit is contained in:
@@ -1,19 +1,23 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { ClipboardList, Loader2, Package, Pencil, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react';
|
||||
import { ClipboardList, CloudDownload, Link2, Loader2, Package, Pencil, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react';
|
||||
import {
|
||||
deleteCatalogCategory,
|
||||
deleteCatalogProduct,
|
||||
deleteConsumptionReference,
|
||||
fetchCatalogSummary,
|
||||
fetchOlistStatus,
|
||||
getOlistAuthorizationUrl,
|
||||
importProductCompositions,
|
||||
isSuperAdmin,
|
||||
saveCatalogCategory,
|
||||
saveCatalogProduct,
|
||||
saveConsumptionReference,
|
||||
startOlistCompositionSync,
|
||||
updateCatalogCategory
|
||||
} from '../dataService';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference } from '../types';
|
||||
import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference, OlistStatus } from '../types';
|
||||
import { formatColorLabel } from '../displayFormatters';
|
||||
|
||||
type RegistrationTab = 'products' | 'categories' | 'references';
|
||||
@@ -121,6 +125,8 @@ const Registrations = () => {
|
||||
});
|
||||
const [status, setStatus] = useState<SaveStatus>('idle');
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [olistStatus, setOlistStatus] = useState<OlistStatus | null>(null);
|
||||
const [olistActionLoading, setOlistActionLoading] = useState(false);
|
||||
const appliedSkuPrefillRef = useRef('');
|
||||
const compositionFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -139,6 +145,7 @@ const Registrations = () => {
|
||||
});
|
||||
const [sizeAreas, setSizeAreas] = useState<Record<string, string>>(defaultSizeAreas);
|
||||
const [sizeYields, setSizeYields] = useState<Record<string, string>>({});
|
||||
const canManageOlist = isSuperAdmin();
|
||||
|
||||
const loadCatalog = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -164,6 +171,26 @@ const Registrations = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadOlistStatus = useCallback(async () => {
|
||||
if (!canManageOlist) return;
|
||||
try {
|
||||
setOlistStatus(await fetchOlistStatus());
|
||||
} catch (error) {
|
||||
console.error('Olist status load failed', error);
|
||||
}
|
||||
}, [canManageOlist]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = window.setTimeout(() => void loadOlistStatus(), 0);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [loadOlistStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canManageOlist || !olistStatus?.syncInProgress) return;
|
||||
const intervalId = window.setInterval(() => void loadOlistStatus(), 5_000);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [canManageOlist, loadOlistStatus, olistStatus?.syncInProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedTab = searchParams.get('tab');
|
||||
const sku = (searchParams.get('sku') || '').trim();
|
||||
@@ -388,6 +415,32 @@ const Registrations = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const connectOlist = async () => {
|
||||
setOlistActionLoading(true);
|
||||
try {
|
||||
window.location.assign(await getOlistAuthorizationUrl());
|
||||
} catch (error) {
|
||||
setFeedback(error instanceof Error ? error.message : 'Não foi possível iniciar a conexão com a Olist.');
|
||||
setStatus('error');
|
||||
setOlistActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncOlist = async () => {
|
||||
setOlistActionLoading(true);
|
||||
try {
|
||||
await startOlistCompositionSync(!olistStatus?.runs.some(run => run.status === 'completed'));
|
||||
setFeedback('Sincronização Olist iniciada. Os dados serão atualizados ao concluir.');
|
||||
setStatus('saved');
|
||||
await loadOlistStatus();
|
||||
} catch (error) {
|
||||
setFeedback(error instanceof Error ? error.message : 'Não foi possível iniciar a sincronização Olist.');
|
||||
setStatus('error');
|
||||
} finally {
|
||||
setOlistActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeCategory = (category: CatalogCategory) => runAction(
|
||||
() => deleteCatalogCategory(category.id),
|
||||
`Categoria ${category.name} excluida.`
|
||||
@@ -440,6 +493,33 @@ const Registrations = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canManageOlist && olistStatus && (
|
||||
<>
|
||||
{olistStatus.connected ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void syncOlist()}
|
||||
disabled={olistActionLoading || olistStatus.syncInProgress}
|
||||
title="Sincronizar composições da Olist"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-xl border border-sky-400/30 bg-sky-400/10 px-4 text-sm font-bold text-sky-200 transition-colors hover:border-sky-300 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"
|
||||
>
|
||||
{olistActionLoading || olistStatus.syncInProgress ? <Loader2 className="h-4 w-4 animate-spin" /> : <CloudDownload className="h-4 w-4" />}
|
||||
{olistStatus.syncInProgress ? 'Sincronizando' : 'Sincronizar Olist'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void connectOlist()}
|
||||
disabled={!olistStatus.configured || olistActionLoading}
|
||||
title={olistStatus.configured ? 'Conectar conta Olist' : 'Configure as credenciais Olist V3 no ambiente'}
|
||||
className="inline-flex h-10 items-center justify-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"
|
||||
>
|
||||
{olistActionLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Link2 className="h-4 w-4 text-brand-primary" />}
|
||||
Conectar Olist
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
ref={compositionFileInputRef}
|
||||
type="file"
|
||||
@@ -505,6 +585,18 @@ const Registrations = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManageOlist && olistStatus?.runs[0] && (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-xs font-semibold text-dark-muted">
|
||||
<span className={olistStatus.runs[0].status === 'failed' ? 'text-red-300' : olistStatus.runs[0].status === 'running' ? 'text-sky-300' : 'text-emerald-300'}>
|
||||
Olist {olistStatus.runs[0].status === 'running' ? 'sincronizando' : olistStatus.runs[0].status === 'failed' ? 'com falha' : 'sincronizada'}
|
||||
</span>
|
||||
<span>{olistStatus.runs[0].mode === 'full' ? 'Carga completa' : 'Atualização incremental'}</span>
|
||||
<span>{olistStatus.runs[0].imported} composições</span>
|
||||
<span>{olistStatus.runs[0].referenceCount} referências</span>
|
||||
{olistStatus.runs[0].failed > 0 && <span className="text-red-300">{olistStatus.runs[0].failed} falhas</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-48 items-center justify-center rounded-2xl border border-dark-border bg-dark-card text-brand-primary">
|
||||
<Loader2 className="h-7 w-7 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user