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,4 +1,4 @@
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateProductionOrdersResult, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductDetailsAnalytics, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, RfmAnalytics, SellerAnalyticsItem, SellerDetailsAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyMaterialDetailsPayload, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types';
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateProductionOrdersResult, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, OlistStatus, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductDetailsAnalytics, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, RfmAnalytics, SellerAnalyticsItem, SellerDetailsAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyMaterialDetailsPayload, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types';
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
@@ -276,6 +276,30 @@ export const fetchCatalogSummary = async (): Promise<CatalogSummary> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchOlistStatus = async (): Promise<OlistStatus> => {
|
||||
const response = await authFetch('/olist/status', { 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;
|
||||
};
|
||||
|
||||
export const getOlistAuthorizationUrl = async (): Promise<string> => {
|
||||
const response = await authFetch('/olist/authorization-url', { method: 'POST' });
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok || !data?.url) throw new Error(data?.error || 'Não foi possível iniciar a conexão com a Olist.');
|
||||
return data.url as string;
|
||||
};
|
||||
|
||||
export const startOlistCompositionSync = async (fullSync = false): Promise<void> => {
|
||||
const response = await authFetch('/olist/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fullSync })
|
||||
});
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(data?.error || 'Não foi possível iniciar a sincronização Olist.');
|
||||
};
|
||||
|
||||
export const saveCatalogCategory = async (payload: CatalogCategoryPayload): Promise<CatalogCategory> => {
|
||||
const response = await authFetch('/catalog/categories', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -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" />
|
||||
|
||||
26
src/types.ts
26
src/types.ts
@@ -204,6 +204,32 @@ export type CatalogCategoryPayload = {
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export interface OlistSyncRun {
|
||||
id: number;
|
||||
trigger: 'manual' | 'scheduled';
|
||||
mode: 'full' | 'incremental';
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
productsScanned: number;
|
||||
manufacturedProducts: number;
|
||||
imported: number;
|
||||
componentCount: number;
|
||||
referenceCount: number;
|
||||
skippedReferenceCount: number;
|
||||
failed: number;
|
||||
errorMessage: string;
|
||||
details: Array<{ productId?: number; sku?: string; error?: string }>;
|
||||
}
|
||||
|
||||
export interface OlistStatus {
|
||||
configured: boolean;
|
||||
connected: boolean;
|
||||
tokenExpiresAt: string | null;
|
||||
syncInProgress: boolean;
|
||||
runs: OlistSyncRun[];
|
||||
}
|
||||
|
||||
export type CatalogProductPayload = {
|
||||
type: CatalogProductType;
|
||||
sku: string;
|
||||
|
||||
Reference in New Issue
Block a user