Import Tiny product compositions

This commit is contained in:
Cauê Faleiros
2026-07-31 11:32:49 -03:00
parent b431802d50
commit 37d4a7732e
9 changed files with 368 additions and 15 deletions

View File

@@ -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, ProductDetailsAnalytics, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, RfmAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, 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, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductDetailsAnalytics, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, RfmAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types';
import { formatDateParam } from './dateRanges';
const API_URL = import.meta.env.VITE_API_URL || '/api';
@@ -345,6 +345,22 @@ export const deleteConsumptionReference = async (id: number): Promise<void> => {
}
};
export const importProductCompositions = async (payload: unknown): Promise<ProductCompositionImportSummary> => {
const response = await authFetch('/catalog/product-compositions/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(data?.error || 'Não foi possível importar as composições.');
}
analyticsCache.clear();
return data as ProductCompositionImportSummary;
};
export const fetchSupplySummary = async (): Promise<SupplySummary> => {
const emptySummary: SupplySummary = {
receipts: [],

View File

@@ -1,11 +1,12 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { ClipboardList, Loader2, Package, RefreshCw, Ruler, Save, Tags, Trash2 } from 'lucide-react';
import { ClipboardList, Loader2, Package, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react';
import {
deleteCatalogCategory,
deleteCatalogProduct,
deleteConsumptionReference,
fetchCatalogSummary,
importProductCompositions,
saveCatalogCategory,
saveCatalogProduct,
saveConsumptionReference
@@ -93,7 +94,9 @@ const getAverageYield = (reference: ConsumptionReference) => {
};
const getReferenceSourceLabel = (reference: ConsumptionReference) => (
reference.source === 'tiny_op' ? 'Tiny OP' : 'Manual'
reference.source === 'tiny_op'
? 'Tiny OP'
: reference.source === 'tiny_structure' ? 'Tiny Estrutura' : 'Manual'
);
const formatConsumptionPerPiece = (reference: ConsumptionReference) => {
@@ -111,6 +114,7 @@ const Registrations = () => {
const [status, setStatus] = useState<SaveStatus>('idle');
const [feedback, setFeedback] = useState('');
const appliedSkuPrefillRef = useRef('');
const compositionFileInputRef = useRef<HTMLInputElement>(null);
const [categoryForm, setCategoryForm] = useState({ name: '', description: '' });
const [productForm, setProductForm] = useState(defaultProductForm);
@@ -323,6 +327,29 @@ const Registrations = () => {
setSizeYields({});
}, 'Referencia salva.');
const importCompositionFile = async (file: File | null) => {
if (!file) return;
setStatus('saving');
setFeedback('Importando composições...');
try {
const payload = JSON.parse(await file.text()) as unknown;
const result = await importProductCompositions(payload);
await loadCatalog();
setStatus(result.failed ? 'error' : 'saved');
setFeedback(
`${result.imported} composições importadas, ${result.referenceCount} referências criadas/atualizadas` +
`${result.failed ? `, ${result.failed} falhas` : ''}.`
);
} catch (error) {
setStatus('error');
setFeedback(error instanceof Error ? error.message : 'Não foi possível importar o arquivo.');
} finally {
if (compositionFileInputRef.current) compositionFileInputRef.current.value = '';
}
};
const removeCategory = (category: CatalogCategory) => runAction(
() => deleteCatalogCategory(category.id),
`Categoria ${category.name} excluida.`
@@ -363,6 +390,22 @@ const Registrations = () => {
</p>
</div>
<div className="flex flex-wrap gap-2">
<input
ref={compositionFileInputRef}
type="file"
accept="application/json,.json"
className="hidden"
onChange={(event) => void importCompositionFile(event.target.files?.[0] || null)}
/>
<button
type="button"
onClick={() => compositionFileInputRef.current?.click()}
disabled={status === 'saving'}
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"
>
<Upload className="h-4 w-4 text-brand-primary" />
Importar composições
</button>
<Link
to="/planning-issues"
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"
@@ -674,7 +717,7 @@ const Registrations = () => {
{Object.keys(reference.sizeYields || {}).length ? 'Por tamanho' : 'Geral'}
</span>
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold ${
reference.source === 'tiny_op'
reference.source === 'tiny_op' || reference.source === 'tiny_structure'
? 'border-sky-400/30 bg-sky-400/10 text-sky-300'
: 'border-dark-border bg-dark-input text-dark-muted'
}`}>

View File

@@ -498,6 +498,25 @@ export interface ProductComposition {
components: ProductCompositionComponent[];
}
export interface ProductCompositionImportSummary {
imported: number;
failed: number;
componentCount: number;
referenceCount: number;
skippedReferenceCount: number;
issues: Array<{
type: string;
productSku?: string;
product?: string;
component?: string;
componentSku?: string;
tinyProductId?: string;
unit?: string;
count?: number;
message?: string;
}>;
}
export interface ClientAnalyticsItem {
customerKey: string;
clientToken: string;