fix: remove manual project creation, smart sequence release, received-invoices schema fix
- Remove ProjectCreate page, POST /projects endpoint, and next-number endpoint - Projects can only be created through orders (shared numbering sequence) - Remove dead CreateProjectSchema and createProject service function - Delete 'order' row from number_sequences (unused; code uses 'shared') - Smart sequence release: decrement last_number only when deleting the highest number - Fix received-invoices stats referencing non-existent is_deleted and amount_czk columns - Update deploy instructions in CLAUDE.md (npm install, prisma migrate deploy) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -493,7 +493,7 @@ export async function deleteInvoice(id: number) {
|
||||
const year = existing.invoice_number
|
||||
? Number(existing.invoice_number.split("/")[1]) || new Date().getFullYear()
|
||||
: new Date().getFullYear();
|
||||
await releaseInvoiceNumber(year);
|
||||
await releaseInvoiceNumber(year, existing.invoice_number ?? undefined);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -110,13 +110,51 @@ async function previewNextSequence(
|
||||
|
||||
/**
|
||||
* Release a sequence number back to the pool.
|
||||
* NOTE: Blindly decrementing can cause duplicate numbers if numbers were
|
||||
* allocated after this one. Sequence numbers are consumed but not returned
|
||||
* to the pool — this is intentionally a no-op.
|
||||
* Only decrements if the deleted entity held the current highest number,
|
||||
* preventing gaps while avoiding duplicate re-allocation.
|
||||
*/
|
||||
async function releaseSequence(_type: string, _year: number) {
|
||||
// No-op: decrementing can cause duplicate sequence numbers.
|
||||
// Sequence numbers are consumed but never returned to the pool.
|
||||
async function releaseSequence(
|
||||
type: string,
|
||||
year: number,
|
||||
deletedNumber?: string,
|
||||
) {
|
||||
if (!deletedNumber) return;
|
||||
|
||||
const existing = await prisma.$queryRaw<Array<{ last_number: number }>>`
|
||||
SELECT last_number FROM number_sequences
|
||||
WHERE \`type\` = ${type} AND \`year\` = ${year}
|
||||
`;
|
||||
if (existing.length === 0) return;
|
||||
|
||||
const settings = await getSettings();
|
||||
const pattern =
|
||||
type === "shared"
|
||||
? settings?.order_number_pattern || DEFAULT_ORDER_PATTERN
|
||||
: type === "invoice"
|
||||
? settings?.invoice_number_pattern || DEFAULT_INVOICE_PATTERN
|
||||
: settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
|
||||
const code =
|
||||
type === "shared"
|
||||
? settings?.order_type_code || "71"
|
||||
: type === "invoice"
|
||||
? settings?.invoice_type_code || "81"
|
||||
: "";
|
||||
const prefix = type === "offer" ? settings?.quotation_prefix || "NA" : "";
|
||||
|
||||
const highestNumber = applyPattern(pattern, {
|
||||
year,
|
||||
prefix,
|
||||
code,
|
||||
seq: existing[0].last_number,
|
||||
});
|
||||
|
||||
if (deletedNumber === highestNumber && existing[0].last_number > 0) {
|
||||
await prisma.$executeRaw`
|
||||
UPDATE number_sequences
|
||||
SET \`last_number\` = ${existing[0].last_number - 1}
|
||||
WHERE \`type\` = ${type} AND \`year\` = ${year}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify a shared number is not already used by an order or project. */
|
||||
@@ -265,19 +303,40 @@ export async function previewInvoiceNumber(
|
||||
return { number, next_number: number };
|
||||
}
|
||||
|
||||
/** Release an offer number back to the pool (decrement sequence). */
|
||||
export async function releaseOfferNumber(year?: number) {
|
||||
await releaseSequence("offer", year || new Date().getFullYear());
|
||||
/** Release an offer number back to the pool (decrement if highest). */
|
||||
export async function releaseOfferNumber(
|
||||
year?: number,
|
||||
deletedNumber?: string,
|
||||
) {
|
||||
await releaseSequence(
|
||||
"offer",
|
||||
year || new Date().getFullYear(),
|
||||
deletedNumber,
|
||||
);
|
||||
}
|
||||
|
||||
/** Release a shared number back to the pool (decrement sequence). */
|
||||
export async function releaseSharedNumber(year?: number) {
|
||||
await releaseSequence("shared", year || new Date().getFullYear());
|
||||
/** Release a shared number back to the pool (decrement if highest). */
|
||||
export async function releaseSharedNumber(
|
||||
year?: number,
|
||||
deletedNumber?: string,
|
||||
) {
|
||||
await releaseSequence(
|
||||
"shared",
|
||||
year || new Date().getFullYear(),
|
||||
deletedNumber,
|
||||
);
|
||||
}
|
||||
|
||||
/** Release an invoice number back to the pool (decrement sequence). */
|
||||
export async function releaseInvoiceNumber(year?: number) {
|
||||
await releaseSequence("invoice", year || new Date().getFullYear());
|
||||
/** Release an invoice number back to the pool (decrement if highest). */
|
||||
export async function releaseInvoiceNumber(
|
||||
year?: number,
|
||||
deletedNumber?: string,
|
||||
) {
|
||||
await releaseSequence(
|
||||
"invoice",
|
||||
year || new Date().getFullYear(),
|
||||
deletedNumber,
|
||||
);
|
||||
}
|
||||
|
||||
/** Preview what a pattern would produce (for settings UI) */
|
||||
|
||||
@@ -307,7 +307,7 @@ export async function deleteOffer(id: number) {
|
||||
const year = existing.created_at
|
||||
? new Date(existing.created_at).getFullYear()
|
||||
: new Date().getFullYear();
|
||||
await releaseOfferNumber(year);
|
||||
await releaseOfferNumber(year, existing.quotation_number ?? undefined);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -542,23 +542,10 @@ export async function deleteOrder(id: number) {
|
||||
await tx.orders.delete({ where: { id } });
|
||||
});
|
||||
|
||||
const releasedYears = new Set<number>();
|
||||
const year = existing.created_at
|
||||
? new Date(existing.created_at).getFullYear()
|
||||
: new Date().getFullYear();
|
||||
await releaseSharedNumber(year);
|
||||
releasedYears.add(year);
|
||||
|
||||
// Release the linked project's shared number(s) too
|
||||
for (const p of linkedProjects) {
|
||||
const pYear = p.created_at
|
||||
? new Date(p.created_at).getFullYear()
|
||||
: new Date().getFullYear();
|
||||
if (!releasedYears.has(pYear)) {
|
||||
await releaseSharedNumber(pYear);
|
||||
releasedYears.add(pYear);
|
||||
}
|
||||
}
|
||||
await releaseSharedNumber(year, existing.order_number ?? undefined);
|
||||
|
||||
return { data: { id, order_number: existing.order_number } };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import prisma from "../config/database";
|
||||
import {
|
||||
generateSharedNumber,
|
||||
previewSharedNumber,
|
||||
releaseSharedNumber,
|
||||
} from "./numbering.service";
|
||||
import { releaseSharedNumber } from "./numbering.service";
|
||||
import { NasFileManager } from "./nas-file-manager";
|
||||
|
||||
const nasFileManager = new NasFileManager();
|
||||
@@ -96,50 +92,6 @@ export async function getProject(id: number) {
|
||||
};
|
||||
}
|
||||
|
||||
import { isSharedNumberTaken } from "./numbering.service";
|
||||
|
||||
export async function createProject(body: Record<string, any>) {
|
||||
const projectNumber =
|
||||
body.project_number !== undefined && body.project_number !== null
|
||||
? String(body.project_number)
|
||||
: await generateSharedNumber();
|
||||
|
||||
if (body.project_number !== undefined && body.project_number !== null) {
|
||||
const taken = await isSharedNumberTaken(String(body.project_number));
|
||||
if (taken) {
|
||||
return { error: "Číslo projektu je již použito", status: 400 };
|
||||
}
|
||||
}
|
||||
|
||||
const project = await prisma.projects.create({
|
||||
data: {
|
||||
project_number: projectNumber,
|
||||
name: body.name ? String(body.name) : null,
|
||||
customer_id: body.customer_id != null ? Number(body.customer_id) : null,
|
||||
responsible_user_id:
|
||||
body.responsible_user_id != null
|
||||
? Number(body.responsible_user_id)
|
||||
: null,
|
||||
quotation_id:
|
||||
body.quotation_id != null ? Number(body.quotation_id) : null,
|
||||
order_id: body.order_id != null ? Number(body.order_id) : null,
|
||||
status: body.status ? String(body.status) : "aktivni",
|
||||
start_date: body.start_date ? new Date(String(body.start_date)) : null,
|
||||
end_date: body.end_date ? new Date(String(body.end_date)) : null,
|
||||
notes: body.notes ? String(body.notes) : null,
|
||||
},
|
||||
});
|
||||
|
||||
if (project.project_number && nasFileManager.isConfigured()) {
|
||||
nasFileManager.createProjectFolder(
|
||||
project.project_number,
|
||||
project.name || "",
|
||||
);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function updateProject(id: number, body: Record<string, any>) {
|
||||
const existing = await prisma.projects.findUnique({ where: { id } });
|
||||
if (!existing) return null;
|
||||
@@ -206,7 +158,7 @@ export async function deleteProject(id: number, deleteFiles: boolean = false) {
|
||||
const year = existing.created_at
|
||||
? new Date(existing.created_at).getFullYear()
|
||||
: new Date().getFullYear();
|
||||
await releaseSharedNumber(year);
|
||||
await releaseSharedNumber(year, existing.project_number ?? undefined);
|
||||
|
||||
return existing;
|
||||
}
|
||||
@@ -248,7 +200,3 @@ export async function deleteProjectNote(projectId: number, noteId: number) {
|
||||
await prisma.project_notes.delete({ where: { id: noteId } });
|
||||
return note;
|
||||
}
|
||||
|
||||
export async function getNextProjectNumber() {
|
||||
return previewSharedNumber();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user