mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Suppliers as a managed list with per-spool assignments (#2996)
This commit is contained in:
@@ -20,6 +20,7 @@ import { MATERIALS } from './spool-form/constants';
|
||||
import { FilamentSection } from './spool-form/FilamentSection';
|
||||
import { ColorSection } from './spool-form/ColorSection';
|
||||
import { AdditionalSection } from './spool-form/AdditionalSection';
|
||||
import { SupplierSection, type SupplierLinkDraft } from './spool-form/SupplierSection';
|
||||
import { SpoolmanFilamentPicker } from './spool-form/SpoolmanFilamentPicker';
|
||||
import { PrinterProfilesSection } from './spool-form/PrinterProfilesSection';
|
||||
import { normaliseFlow } from '../utils/nozzleFlow';
|
||||
@@ -81,6 +82,13 @@ export function SpoolFormModal({
|
||||
// through core_weight.
|
||||
const [coreWeightTouched, setCoreWeightTouched] = useState(false);
|
||||
const [locationIdTouched, setLocationIdTouched] = useState(false);
|
||||
// Supplier assignments (#2988). Held outside SpoolFormData — they are
|
||||
// relational and saved through their own replace-all endpoint. An untouched
|
||||
// create does not send them, so it keeps the backend's inherited
|
||||
// assignments instead of wiping them with an empty list; a copy always
|
||||
// sends them, see saveSupplierLinks.
|
||||
const [supplierLinks, setSupplierLinks] = useState<SupplierLinkDraft[]>([]);
|
||||
const [supplierLinksTouched, setSupplierLinksTouched] = useState(false);
|
||||
const [quickAdd, setQuickAdd] = useState(false);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
|
||||
@@ -446,10 +454,23 @@ export function SpoolFormModal({
|
||||
} else {
|
||||
setSelectedProfiles(new Map());
|
||||
}
|
||||
// Supplier assignments (#2988) — copied on copy: they describe where
|
||||
// the product is bought, which is what a copy shares.
|
||||
setSupplierLinks(
|
||||
(spool.suppliers ?? []).map((link) => ({
|
||||
supplier_id: link.supplier_id,
|
||||
supplier_name: link.supplier_name,
|
||||
supplier_article_number: link.supplier_article_number ?? '',
|
||||
quoted_price_per_kg: link.quoted_price_per_kg,
|
||||
// Where a COPY was bought is unknown — only a real edit keeps it.
|
||||
is_purchase_source: isCopying ? false : link.is_purchase_source,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setFormData(defaultFormData);
|
||||
setPresetInputValue('');
|
||||
setSelectedProfiles(new Map());
|
||||
setSupplierLinks([]);
|
||||
}
|
||||
// Reset on every open, not just the create path (#1905). The modal keeps
|
||||
// its state while closed, and the Quick Add toggle only renders in create
|
||||
@@ -471,6 +492,7 @@ export function SpoolFormModal({
|
||||
// inheriting. Only Spoolman spools report the flag (#2908).
|
||||
setCoreWeightTouched(isCopying && spool?.core_weight_is_inherited === false);
|
||||
setLocationIdTouched(false);
|
||||
setSupplierLinksTouched(false);
|
||||
}
|
||||
}, [isOpen, spool, mode, isCopying]);
|
||||
|
||||
@@ -582,6 +604,8 @@ export function SpoolFormModal({
|
||||
if (newSpool?.id) {
|
||||
const ok = await savePrinterProfiles(newSpool.id);
|
||||
if (!ok) return;
|
||||
const suppliersOk = await saveSupplierLinks(newSpool.id);
|
||||
if (!suppliersOk) return;
|
||||
}
|
||||
await refreshSpoolQueries();
|
||||
if (onSpoolsCreated) onSpoolsCreated([newSpool]);
|
||||
@@ -625,6 +649,12 @@ export function SpoolFormModal({
|
||||
await savePrinterProfiles(s.id);
|
||||
}
|
||||
}
|
||||
// Every copy of a bulk add shares the same supplier assignments (#2988).
|
||||
if (shouldSaveSupplierLinks) {
|
||||
for (const s of createdSpools) {
|
||||
await saveSupplierLinks(s.id);
|
||||
}
|
||||
}
|
||||
await refreshSpoolQueries();
|
||||
if (onSpoolsCreated) onSpoolsCreated(createdSpools);
|
||||
if (spoolmanResult && spoolmanResult.failed_count > 0) {
|
||||
@@ -658,6 +688,8 @@ export function SpoolFormModal({
|
||||
if (spool?.id) {
|
||||
const ok = await savePrinterProfiles(spool.id);
|
||||
if (!ok) return;
|
||||
const suppliersOk = await saveSupplierLinks(spool.id);
|
||||
if (!suppliersOk) return;
|
||||
}
|
||||
await refreshSpoolQueries();
|
||||
showToast(t('inventory.spoolUpdated'), 'success');
|
||||
@@ -774,6 +806,41 @@ export function SpoolFormModal({
|
||||
},
|
||||
});
|
||||
|
||||
// Supplier assignments (#2988): replace-all save.
|
||||
//
|
||||
// Skipped on an untouched create so the backend's inheritance can fill the
|
||||
// new spool in — an empty list would wipe what it just attached. A COPY is
|
||||
// the opposite case and always saves: the dialog seeded the chips from the
|
||||
// spool being copied and showed them, so they have to be what the copy
|
||||
// gets. Inheritance cannot stand in for that — it keys on the (material,
|
||||
// subtype, brand, color_name) tuple and so resolves to the NEWEST spool of
|
||||
// the product rather than the one on screen, and in Spoolman mode there is
|
||||
// no inheritance at all.
|
||||
const shouldSaveSupplierLinks = supplierLinksTouched || isCopying;
|
||||
|
||||
const saveSupplierLinks = async (spoolId: number): Promise<boolean> => {
|
||||
if (!shouldSaveSupplierLinks) return true;
|
||||
// Spoolman parity (#2988): the assignment rows live Bambuddy-side either
|
||||
// way; only the endpoint differs (twin table keyed by the remote id).
|
||||
const save = spoolmanMode ? api.setSpoolmanSpoolSuppliers : api.setSpoolSuppliers;
|
||||
try {
|
||||
await save(
|
||||
spoolId,
|
||||
supplierLinks.map((link) => ({
|
||||
supplier_id: link.supplier_id,
|
||||
supplier_article_number: link.supplier_article_number.trim() || null,
|
||||
quoted_price_per_kg: link.quoted_price_per_kg,
|
||||
is_purchase_source: link.is_purchase_source,
|
||||
})),
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('SpoolFormModal.saveSupplierLinks failed:', err);
|
||||
showToast(t('inventory.suppliers.saveFailed'), 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Save everything the Printers tab holds: one K profile per hotend and the
|
||||
// per-printer-model preset overrides. Returns false if either write failed,
|
||||
// which keeps the modal open so the user does not lose what they picked.
|
||||
@@ -1117,6 +1184,21 @@ export function SpoolFormModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Suppliers (#2988) — both inventories: the assignments live
|
||||
Bambuddy-side either way (Spoolman's vendor is the
|
||||
manufacturer, not the seller), so the same section renders
|
||||
in Spoolman mode and saves to the twin endpoint. */}
|
||||
<div>
|
||||
<SupplierSection
|
||||
links={supplierLinks}
|
||||
onChange={(next) => {
|
||||
setSupplierLinks(next);
|
||||
setSupplierLinksTouched(true);
|
||||
}}
|
||||
currencySymbol={currencySymbol}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Usage History (only when editing internal inventory; Spoolman tracks its own) */}
|
||||
{isEditing && spool && !spoolmanMode && (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user