feat(suppliers)!: full customers model - structured address + custom fields

The dodavatele modal is now an exact mirror of the customers modal
(minus the PDF field-order picker): Nazev+ARES, Ulice, Mesto+PSC, Zeme,
ICO+ARES, DIC, and the same "Vlastni pole" editor (maxWidth md).

Two migrations on sklad_suppliers:
- structured street/city/postal_code/country replace the free-text
  address blob (best-effort split: street / PSC regex / city)
- custom_fields LONGTEXT replaces contact_person/email/phone/notes;
  existing values are preserved as labeled custom fields

The encode/decode of the custom_fields blob is shared with customers
via the new utils/custom-fields.ts. The PO PDF supplier block renders
the structured address + custom-field lines like the customer block;
the supplier picker/detail selects and types follow. Legacy clients
sending the dropped keys are silently stripped (tested). Suppliers
list shows Ulice/Mesto instead of the dropped contact columns; search
covers name/ico/city.

BREAKING CHANGE: sklad_suppliers.address, contact_person, email,
phone, notes columns dropped (data migrated); deploy must run
prisma migrate deploy + generate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 20:11:22 +02:00
parent f1ce76d21d
commit db7a5c3d15
16 changed files with 578 additions and 232 deletions

View File

@@ -7,6 +7,10 @@ import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response";
import { contentDisposition } from "../../utils/content-disposition";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import {
encodeCustomFields,
decodeCustomFields,
} from "../../utils/custom-fields";
import { parseBody } from "../../schemas/common";
import { nasInvoicesManager } from "../../services/nas-financials-manager";
import {
@@ -226,7 +230,7 @@ export default async function warehouseRoutes(
// SUPPLIERS
// =============================================================
// GET /suppliers — paginated list, search by name/ico/contact_person
// GET /suppliers — paginated list, search by name/ico/city
fastify.get(
"/suppliers",
{ preHandler: requirePermission("warehouse.manage") },
@@ -240,7 +244,7 @@ export default async function warehouseRoutes(
where.OR = [
{ name: { contains: search } },
{ ico: { contains: search } },
{ contact_person: { contains: search } },
{ city: { contains: search } },
];
}
@@ -254,9 +258,16 @@ export default async function warehouseRoutes(
prisma.sklad_suppliers.count({ where }),
]);
// Decode the custom_fields blob into the array shape the modal edits
// (same model as customers).
const enriched = suppliers.map((s) => ({
...s,
custom_fields: decodeCustomFields(s.custom_fields).custom_fields,
}));
return paginated(
reply,
suppliers,
enriched,
buildPaginationMeta(total, page, limit),
);
},
@@ -275,7 +286,10 @@ export default async function warehouseRoutes(
});
if (!supplier) return error(reply, "Dodavatel nenalezen", 404);
return success(reply, supplier);
return success(reply, {
...supplier,
custom_fields: decodeCustomFields(supplier.custom_fields).custom_fields,
});
},
);
@@ -293,11 +307,12 @@ export default async function warehouseRoutes(
name: body.name,
ico: body.ico ?? null,
dic: body.dic ?? null,
contact_person: body.contact_person ?? null,
email: body.email ?? null,
phone: body.phone ?? null,
address: body.address ?? null,
notes: body.notes ?? null,
street: body.street ?? null,
city: body.city ?? null,
postal_code: body.postal_code ?? null,
country: body.country ?? null,
// Suppliers have no PDF field-order picker — order is always [].
custom_fields: encodeCustomFields(body.custom_fields, []),
is_active: body.is_active ?? true,
},
});
@@ -312,7 +327,7 @@ export default async function warehouseRoutes(
newValues: {
name: supplier.name,
ico: supplier.ico,
contact_person: supplier.contact_person,
city: supplier.city,
is_active: supplier.is_active,
},
});
@@ -342,12 +357,13 @@ export default async function warehouseRoutes(
if (body.name !== undefined) updateData.name = body.name;
if (body.ico !== undefined) updateData.ico = body.ico;
if (body.dic !== undefined) updateData.dic = body.dic;
if (body.contact_person !== undefined)
updateData.contact_person = body.contact_person;
if (body.email !== undefined) updateData.email = body.email;
if (body.phone !== undefined) updateData.phone = body.phone;
if (body.address !== undefined) updateData.address = body.address;
if (body.notes !== undefined) updateData.notes = body.notes;
if (body.street !== undefined) updateData.street = body.street;
if (body.city !== undefined) updateData.city = body.city;
if (body.postal_code !== undefined)
updateData.postal_code = body.postal_code;
if (body.country !== undefined) updateData.country = body.country;
if (body.custom_fields !== undefined)
updateData.custom_fields = encodeCustomFields(body.custom_fields, []);
if (body.is_active !== undefined) updateData.is_active = body.is_active;
const updated = await prisma.sklad_suppliers.update({
@@ -365,13 +381,13 @@ export default async function warehouseRoutes(
oldValues: {
name: existing.name,
ico: existing.ico,
contact_person: existing.contact_person,
city: existing.city,
is_active: existing.is_active,
},
newValues: {
name: updated.name,
ico: updated.ico,
contact_person: updated.contact_person,
city: updated.city,
is_active: updated.is_active,
},
});