- /fedex: batch create, document upload/replace/delete, save shipment, imported list
- /api/orders, /api/invoices, /api/pb-file, /api/pb/refresh-token, /api/stock-codes
- /api/payments/bundle: generate + download-all remittance bundles
- shared app/components/DocCell.tsx and app/lib/{invoice-docs,pb-fetch,pb-token}.ts
- recon: invoice reconciliation reports and PowerShell fix-plan scripts
- .gitignore: exclude one-off scripts/_* debugging artefacts, recon/pdfs+txts, data backups
105 lines
No EOL
2.9 KiB
TypeScript
105 lines
No EOL
2.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { revalidatePath } from "next/cache";
|
|
import { getPb } from "../../../lib/pb";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const NAME_MAX = 100;
|
|
const NOTES_MAX = 1000;
|
|
const HEADER_MAX = 2000;
|
|
const FOOTNOTES_MAX = 4000;
|
|
|
|
function sanitizeLines(input: any): any[] | null {
|
|
if (!Array.isArray(input)) return null;
|
|
const out: any[] = [];
|
|
for (const raw of input) {
|
|
if (!raw || typeof raw !== "object") return null;
|
|
const code = String(raw.code || "").trim();
|
|
if (!code) return null; // every line must have a code
|
|
out.push({
|
|
stockId: String(raw.stockId || ""),
|
|
code,
|
|
description: String(raw.description || ""),
|
|
qty: Math.max(0, Number(raw.qty) || 0),
|
|
unitPrice: Math.max(0, Number(raw.unitPrice) || 0),
|
|
refInvoiceNo: String(raw.refInvoiceNo || ""),
|
|
refInvoiceDate: String(raw.refInvoiceDate || ""),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
let body: any;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Body must be JSON" }, { status: 400 });
|
|
}
|
|
|
|
const name = String(body.name || "").trim();
|
|
if (!name || name.length > NAME_MAX) {
|
|
return NextResponse.json(
|
|
{ error: `name is required (1-${NAME_MAX} chars)` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const notes = String(body.notes || "").trim();
|
|
if (notes.length > NOTES_MAX) {
|
|
return NextResponse.json(
|
|
{ error: `notes max ${NOTES_MAX} chars` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const headerText = String(body.header_text || "");
|
|
if (headerText.length > HEADER_MAX) {
|
|
return NextResponse.json(
|
|
{ error: `header text max ${HEADER_MAX} chars` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const footnotes = String(body.footnotes || "");
|
|
if (footnotes.length > FOOTNOTES_MAX) {
|
|
return NextResponse.json(
|
|
{ error: `footnotes max ${FOOTNOTES_MAX} chars` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const lines = sanitizeLines(body.lines);
|
|
if (lines === null) {
|
|
return NextResponse.json(
|
|
{ error: "lines must be a non-empty array of objects with a code field" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
if (lines.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "lines must contain at least one item" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const pb = getPb();
|
|
try {
|
|
const created = await pb.collection("JEI_orders").create({
|
|
name,
|
|
notes,
|
|
// Header + footnotes are saved PER ORDER so loading an order restores its
|
|
// own wording instead of leaving whatever was last typed in the boxes.
|
|
header_text: headerText,
|
|
footnotes,
|
|
lines,
|
|
});
|
|
revalidatePath("/order");
|
|
return NextResponse.json({ id: created.id, name: created.name });
|
|
} catch (e: any) {
|
|
return NextResponse.json(
|
|
{ error: `Failed to create order: ${e?.message || e}` },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
} |