Add editable supply material details
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 49s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 49s
This commit is contained in:
@@ -23,13 +23,14 @@ import {
|
||||
Truck,
|
||||
Trash2,
|
||||
Warehouse,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { buildConsumptionReferencePath } from '../catalogLinks';
|
||||
import { buildSkuEditPath } from '../catalogLinks';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||
import { classifyCutFamily } from '../analytics/cutting';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, isSuperAdmin } from '../dataService';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, isSuperAdmin, saveSupplyMaterialDetails } from '../dataService';
|
||||
import { parseProductName } from '../productParsing';
|
||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||
import { getPlanningStock } from '../planningStock';
|
||||
@@ -2117,14 +2118,6 @@ const AffectedProducts = ({ products }: { products: SupplyPurchaseNeed['products
|
||||
>
|
||||
#{product.productId} · {product.name}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to={buildConsumptionReferencePath({ sku: product.productId, name: product.name })}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center border-l border-dark-border text-sky-300 transition-colors hover:bg-sky-400/10"
|
||||
title={`Editar composição de ${product.name}`}
|
||||
aria-label={`Editar composição de ${product.name}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to={`/products/${product.productId}`}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center border-l border-dark-border text-brand-primary transition-colors hover:bg-brand-primary/10"
|
||||
@@ -2140,7 +2133,7 @@ const AffectedProducts = ({ products }: { products: SupplyPurchaseNeed['products
|
||||
);
|
||||
};
|
||||
|
||||
const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
const PurchaseMaterialQueue = ({ needs, onEditMaterial }: { needs: SupplyPurchaseNeed[]; onEditMaterial: (need: SupplyPurchaseNeed) => void }) => (
|
||||
<>
|
||||
<div className="border-b border-dark-border bg-dark-input/40 px-4 py-3">
|
||||
<div className="grid gap-3 text-[10px] font-bold uppercase tracking-widest text-dark-muted xl:grid-cols-[minmax(300px,1.35fr)_minmax(380px,1fr)_150px]">
|
||||
@@ -2182,7 +2175,7 @@ const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
||||
{need.colors.length ? need.colors.join(', ') : 'Todas as cores'}
|
||||
<span className="px-1.5 text-dark-border">•</span>
|
||||
{need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado'}
|
||||
{need.preferredSupplier || (need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2209,6 +2202,15 @@ const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Impacto</p>
|
||||
<p className="mt-1 text-sm font-bold text-dark-text">{products.length} {products.length === 1 ? 'SKU' : 'SKUs'} dependem</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un. planejadas</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEditMaterial(need)}
|
||||
className="mt-3 inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-dark-border bg-dark-input text-sky-300 transition-colors hover:border-sky-400/50 hover:bg-sky-400/10"
|
||||
title={`Editar dados de ${getNeedDisplayName(need)}`}
|
||||
aria-label={`Editar dados de ${getNeedDisplayName(need)}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2220,7 +2222,7 @@ const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
const PurchaseServiceQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
const PurchaseServiceQueue = ({ needs, onEditMaterial }: { needs: SupplyPurchaseNeed[]; onEditMaterial: (need: SupplyPurchaseNeed) => void }) => (
|
||||
<>
|
||||
<div className="border-b border-dark-border bg-dark-input/40 px-4 py-3">
|
||||
<div className="grid gap-3 text-[10px] font-bold uppercase tracking-widest text-dark-muted xl:grid-cols-[minmax(320px,1.4fr)_minmax(260px,1fr)_180px]">
|
||||
@@ -2247,7 +2249,7 @@ const PurchaseServiceQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
||||
{need.colors.length ? need.colors.join(', ') : 'Todas as cores'}
|
||||
<span className="px-1.5 text-dark-border">•</span>
|
||||
{need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado'}
|
||||
{need.preferredSupplier || (need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2266,6 +2268,15 @@ const PurchaseServiceQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Impacto</p>
|
||||
<p className="mt-1 text-sm font-bold text-dark-text">{products.length} {products.length === 1 ? 'SKU' : 'SKUs'} dependem</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un. planejadas</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEditMaterial(need)}
|
||||
className="mt-3 inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-dark-border bg-dark-input text-sky-300 transition-colors hover:border-sky-400/50 hover:bg-sky-400/10"
|
||||
title={`Editar dados de ${getNeedDisplayName(need)}`}
|
||||
aria-label={`Editar dados de ${getNeedDisplayName(need)}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2277,11 +2288,95 @@ const PurchaseServiceQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
const getMaterialRegistrationSku = (material: string) => {
|
||||
const [candidate] = material.trim().split(/\s+/, 1);
|
||||
return /^[A-Z0-9_-]{2,}$/i.test(candidate) ? candidate : '';
|
||||
};
|
||||
|
||||
const MaterialDetailsModal = ({
|
||||
need,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
need: SupplyPurchaseNeed;
|
||||
onClose: () => void;
|
||||
onSave: (payload: { displayName: string; preferredSupplier: string; notes: string }) => Promise<void>;
|
||||
}) => {
|
||||
const [form, setForm] = useState({
|
||||
displayName: need.material,
|
||||
preferredSupplier: need.preferredSupplier || need.suppliers[0] || '',
|
||||
notes: need.notes || '',
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const registrationPath = buildSkuEditPath({
|
||||
sku: getMaterialRegistrationSku(need.material),
|
||||
name: need.material,
|
||||
type: 'raw_material',
|
||||
});
|
||||
|
||||
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setIsSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
await onSave(form);
|
||||
onClose();
|
||||
} catch (saveError) {
|
||||
setError(saveError instanceof Error ? saveError.message : 'Não foi possível salvar os dados do material.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/65 p-4">
|
||||
<form onSubmit={(event) => void submit(event)} className="w-full max-w-lg overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-2xl">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-dark-border p-5">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-sky-300">Dados do material</p>
|
||||
<h2 className="mt-1 truncate text-lg font-bold text-dark-text" title={need.material}>{need.material}</h2>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} className="inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-dark-muted transition-colors hover:bg-dark-input hover:text-dark-text" aria-label="Fechar edição">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4 p-5">
|
||||
<label className="block text-xs font-bold text-dark-muted">
|
||||
Nome para exibição
|
||||
<input value={form.displayName} onChange={(event) => setForm(current => ({ ...current, displayName: event.target.value }))} className={`${inputClassName} mt-1`} />
|
||||
</label>
|
||||
<label className="block text-xs font-bold text-dark-muted">
|
||||
Fornecedor preferencial
|
||||
<input value={form.preferredSupplier} onChange={(event) => setForm(current => ({ ...current, preferredSupplier: event.target.value }))} placeholder="Opcional" className={`${inputClassName} mt-1`} />
|
||||
</label>
|
||||
<label className="block text-xs font-bold text-dark-muted">
|
||||
Observações operacionais
|
||||
<textarea value={form.notes} onChange={(event) => setForm(current => ({ ...current, notes: event.target.value }))} placeholder="Opcional" className="mt-1 min-h-24 w-full resize-y rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" />
|
||||
</label>
|
||||
{error && <p className="rounded-lg border border-red-400/30 bg-red-400/10 px-3 py-2 text-sm font-semibold text-red-300">{error}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col-reverse gap-2 border-t border-dark-border p-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<RouterLink to={registrationPath} className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary">
|
||||
<PackageSearch className="h-4 w-4 text-brand-primary" />
|
||||
Abrir Cadastros
|
||||
</RouterLink>
|
||||
<button type="submit" disabled={isSaving} className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer">
|
||||
<Save className="h-4 w-4" />
|
||||
{isSaving ? 'Salvando...' : 'Salvar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PurchaseNeedsScreen = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [editingMaterial, setEditingMaterial] = useState<SupplyPurchaseNeed | null>(null);
|
||||
const requestedTab = searchParams.get('tab');
|
||||
const requestedFilter = searchParams.get('filter');
|
||||
const activeTab: PurchaseNeedTab = requestedTab === 'services' ? 'services' : 'materials';
|
||||
@@ -2437,7 +2532,9 @@ const PurchaseNeedsScreen = () => {
|
||||
) : visibleNeeds.length ? (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
{activeTab === 'materials' ? <PurchaseMaterialQueue needs={paginatedNeeds} /> : <PurchaseServiceQueue needs={paginatedNeeds} />}
|
||||
{activeTab === 'materials'
|
||||
? <PurchaseMaterialQueue needs={paginatedNeeds} onEditMaterial={setEditingMaterial} />
|
||||
: <PurchaseServiceQueue needs={paginatedNeeds} onEditMaterial={setEditingMaterial} />}
|
||||
</div>
|
||||
<PaginationControls
|
||||
totalItems={visibleNeeds.length}
|
||||
@@ -2465,6 +2562,17 @@ const PurchaseNeedsScreen = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editingMaterial && (
|
||||
<MaterialDetailsModal
|
||||
key={editingMaterial.materialKey}
|
||||
need={editingMaterial}
|
||||
onClose={() => setEditingMaterial(null)}
|
||||
onSave={async (payload) => {
|
||||
await saveSupplyMaterialDetails(editingMaterial.materialKey, payload);
|
||||
await loadSummary();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user