diff --git a/apps/api/src/modules/domains/index.ts b/apps/api/src/modules/domains/index.ts index 1a16cd5..5783b57 100644 --- a/apps/api/src/modules/domains/index.ts +++ b/apps/api/src/modules/domains/index.ts @@ -20,7 +20,7 @@ async function snapshot(tld: string) { } const recView = (r: any) => ({ id: r.id, domain: r.domain, tld: r.tld, orgId: r.org_id, customer: r.org_name ?? null, customerNumber: r.customer_number ?? null, resourceId: r.resource_id, source: r.source, termMonths: r.term_months, costNetCents: r.cost_net_cents, costGrossCents: r.cost_gross_cents, setupCostCents: r.setup_cost_cents, sellNetCents: r.sell_net_cents, taxBp: r.tax_bp, sellGrossCents: r.sell_gross_cents, profitNetCents: r.sell_net_cents === null || r.cost_net_cents === null ? null : r.sell_net_cents - r.cost_net_cents, - procurement: r.procurement, orderedAt: r.ordered_at, orderedRef: r.ordered_ref, note: r.note, pricedAt: r.priced_at, createdAt: r.created_at }); + procurement: r.procurement, orderedAt: r.ordered_at, note: r.note, pricedAt: r.priced_at, createdAt: r.created_at }); const REC_SELECT = 'SELECT d.*, o.name AS org_name, o.customer_number FROM domain_records d LEFT JOIN organizations o ON o.id = d.org_id'; const SUGGEST = ['de', 'com', 'net', 'org', 'eu', 'info']; const marginIn = z.object({ type: z.enum(['percent', 'fixed']).nullable(), value: z.number().int().min(0).max(100_000_00).nullable() }).refine((m) => (m.type === null) === (m.value === null), 'Art und Wert gehören zusammen'); @@ -87,10 +87,10 @@ export const domainsModule: KcModule = { }); app.patch('/admin/domain-records/:id', async (req) => { const a = requirePermission(req, 'domains.write'); const { id } = z.object({ id: z.string().uuid() }).parse(req.params); - const b = z.object({ procurement: z.enum(['open', 'ordered', 'external']).optional(), orderedRef: z.string().max(100).nullable().optional(), note: z.string().max(300).nullable().optional(), orgId: z.string().uuid().nullable().optional(), reprice: z.boolean().optional() }).parse(req.body); + const b = z.object({ procurement: z.enum(['open', 'ordered', 'external']).optional(), orderedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), note: z.string().max(300).nullable().optional(), orgId: z.string().uuid().nullable().optional(), reprice: z.boolean().optional() }).parse(req.body); const rec = await one('SELECT * FROM domain_records WHERE id = ?', [id]); if (!rec) throw notFound(); if (b.procurement) await run('UPDATE domain_records SET procurement = ?, ordered_at = ? WHERE id = ?', [b.procurement, b.procurement === 'ordered' ? (rec.ordered_at ?? new Date()) : null, id]); - if (b.orderedRef !== undefined) await run('UPDATE domain_records SET ordered_ref = ? WHERE id = ?', [b.orderedRef, id]); + if (b.orderedAt) await run("UPDATE domain_records SET ordered_at = ?, procurement = IF(procurement = 'open', 'ordered', procurement) WHERE id = ?", [`${b.orderedAt} 00:00:00`, id]); if (b.note !== undefined) await run('UPDATE domain_records SET note = ? WHERE id = ?', [b.note, id]); if (b.orgId !== undefined) await run('UPDATE domain_records SET org_id = ? WHERE id = ?', [b.orgId, id]); if (b.reprice) { const p = await snapshot(rec.tld); await run('UPDATE domain_records SET term_months=?, cost_net_cents=?, cost_gross_cents=?, setup_cost_cents=?, sell_net_cents=?, tax_bp=?, sell_gross_cents=?, priced_at=UTC_TIMESTAMP(3) WHERE id = ?', [p.termMonths, p.costNet, p.costGross, p.setup, p.net, p.tax, p.gross, id]); } diff --git a/apps/web/src/components/DomainRecords.tsx b/apps/web/src/components/DomainRecords.tsx index 0006580..6badda3 100644 --- a/apps/web/src/components/DomainRecords.tsx +++ b/apps/web/src/components/DomainRecords.tsx @@ -4,9 +4,9 @@ import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { api, errMsg } from '@/lib/api'; import { useSession } from '@/lib/session'; -import { Alert, Empty, Field, eur, fmt } from '@/components/ui'; +import { Alert, Empty, Field, eur } from '@/components/ui'; -interface Rec { id: string; domain: string; tld: string; orgId: string | null; customer: string | null; customerNumber: string | null; source: string; termMonths: number | null; costNetCents: number | null; costGrossCents: number | null; setupCostCents: number; sellNetCents: number | null; taxBp: number; sellGrossCents: number | null; profitNetCents: number | null; procurement: 'open' | 'ordered' | 'external'; orderedAt: string | null; orderedRef: string | null; note: string | null } +interface Rec { id: string; domain: string; tld: string; orgId: string | null; customer: string | null; customerNumber: string | null; source: string; termMonths: number | null; costNetCents: number | null; costGrossCents: number | null; setupCostCents: number; sellNetCents: number | null; taxBp: number; sellGrossCents: number | null; profitNetCents: number | null; procurement: 'open' | 'ordered' | 'external'; orderedAt: string | null; note: string | null } interface Res { id: string; name: string; type: string; orgId: string | null } const PROC = { open: 'Bei KCS zu bestellen', ordered: 'Bei KCS bestellt', external: 'Anderer Anbieter' } as const; const money = (c: number | null) => c === null ? '–' : eur(c); @@ -14,8 +14,8 @@ const csvNum = (c: number | null) => c === null ? '' : (c / 100).toFixed(2).repl const csvCell = (v: string | number | null) => `"${String(v ?? '').replace(/"/g, '""')}"`; /** CSV für Excel (Semikolon, UTF-8 mit BOM): Bestellliste bzw. Gesamtaufstellung. */ function download(rows: Rec[], name: string) { - const head = ['Domain', 'Endung', 'Laufzeit (Monate)', 'Kunde', 'Kundennr.', 'EK brutto', 'EK netto', 'Setup EK brutto', 'VK brutto', 'VK netto', 'USt %', 'Gewinn netto', 'Status', 'bestellt am', 'Bestellnr. KCS', 'Notiz']; - const lines = rows.map((r) => [r.domain, r.tld, r.termMonths, r.customer, r.customerNumber, csvNum(r.costGrossCents), csvNum(r.costNetCents), csvNum(r.setupCostCents), csvNum(r.sellGrossCents), csvNum(r.sellNetCents), r.taxBp / 100, csvNum(r.profitNetCents), PROC[r.procurement], r.orderedAt ? new Date(r.orderedAt).toLocaleDateString('de-DE') : '', r.orderedRef, r.note].map(csvCell).join(';')); + const head = ['Domain', 'Endung', 'Laufzeit (Monate)', 'Kunde', 'Kundennr.', 'EK brutto', 'EK netto', 'Setup EK brutto', 'VK brutto', 'VK netto', 'USt %', 'Gewinn netto', 'Status', 'bestellt am', 'Notiz']; + const lines = rows.map((r) => [r.domain, r.tld, r.termMonths, r.customer, r.customerNumber, csvNum(r.costGrossCents), csvNum(r.costNetCents), csvNum(r.setupCostCents), csvNum(r.sellGrossCents), csvNum(r.sellNetCents), r.taxBp / 100, csvNum(r.profitNetCents), PROC[r.procurement], r.orderedAt ? new Date(r.orderedAt).toLocaleDateString('de-DE') : '', r.note].map(csvCell).join(';')); const url = URL.createObjectURL(new Blob(['' + [head.map(csvCell).join(';'), ...lines].join('\r\n')], { type: 'text/csv;charset=utf-8' })); const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } @@ -44,7 +44,7 @@ export function DomainRecords({ orgId }: { orgId?: string }) { {list.map((r) => (