Import Tiny product compositions
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, 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: [],
|
||||
|
||||
@@ -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'
|
||||
}`}>
|
||||
|
||||
19
src/types.ts
19
src/types.ts
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user