- feat: order confirmation PDF generation with VAT support
- feat: order confirmation modal with custom item editing
- fix: attendance negative duration clamping and switchProject timing
- fix: Quill editor locked to Tahoma 14px, PDF heading sizes
- fix: invoice/offer PDF font consistency (Tahoma enforcement)
- fix: invoice alert cron improvements
- fix: NAS financials manager edge cases
- refactor: numbering service with unique sequence constraints

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-23 17:23:10 +02:00
parent b197017644
commit 07cb428287
36 changed files with 2233 additions and 480 deletions

View File

@@ -419,17 +419,32 @@ export async function switchProject(userId: number, projectId: number | null) {
const now = new Date();
await prisma.attendance_project_logs.updateMany({
// End active project logs, ensuring ended_at is never before started_at
// (can happen when arrival_time was rounded up and now is still earlier)
const activeLogs = await prisma.attendance_project_logs.findMany({
where: { attendance_id: ongoing.id, ended_at: null },
data: { ended_at: now },
});
for (const log of activeLogs) {
const endedAt =
log.started_at && log.started_at > now ? log.started_at : now;
await prisma.attendance_project_logs.update({
where: { id: log.id },
data: { ended_at: endedAt },
});
}
if (projectId) {
const existingLogs = await prisma.attendance_project_logs.count({
where: { attendance_id: ongoing.id },
});
const isFirstProject = existingLogs === 0;
let startedAt = isFirstProject ? ongoing.arrival_time! : now;
if (startedAt > now) startedAt = now;
await prisma.attendance_project_logs.create({
data: {
attendance_id: ongoing.id,
project_id: projectId,
started_at: now,
started_at: startedAt,
ended_at: null,
},
});
@@ -630,19 +645,28 @@ export async function getProjectReport(year: number) {
},
include: {
users: { select: { id: true, first_name: true, last_name: true } },
attendance_project_logs: {
orderBy: { started_at: "asc" },
},
},
});
const projectIds = [
...new Set(records.filter((r) => r.project_id).map((r) => r.project_id!)),
];
// Collect all project ids from both attendance.project_id and project logs
const projectIds = new Set<number>();
for (const rec of records) {
if (rec.project_id) projectIds.add(rec.project_id);
for (const log of rec.attendance_project_logs) {
projectIds.add(log.project_id);
}
}
const projectsMap = new Map<
number,
{ name: string; project_number: string }
>();
if (projectIds.length > 0) {
if (projectIds.size > 0) {
const projects = await prisma.projects.findMany({
where: { id: { in: projectIds } },
where: { id: { in: [...projectIds] } },
select: { id: true, name: true, project_number: true },
});
for (const p of projects) {
@@ -686,32 +710,68 @@ export async function getProjectReport(year: number) {
>();
for (const rec of monthRecs) {
const hours = calcWorkedHours(
rec.arrival_time!,
rec.departure_time!,
rec.break_start,
rec.break_end,
);
const pid = rec.project_id;
if (!projectMap.has(pid)) {
const projInfo = pid ? projectsMap.get(pid) : undefined;
projectMap.set(pid, {
project_number: projInfo?.project_number || undefined,
project_name: projInfo?.name || undefined,
userMap: new Map(),
});
}
const pg = projectMap.get(pid)!;
const uid = rec.user_id;
const uName = rec.users
? `${rec.users.first_name} ${rec.users.last_name}`.trim()
: `User #${uid}`;
if (!pg.userMap.has(uid)) {
pg.userMap.set(uid, { name: uName, hours: 0 });
if (rec.attendance_project_logs.length === 0) {
// No detailed project logs — fall back to attendance.project_id
const pid = rec.project_id;
const hours = calcWorkedHours(
rec.arrival_time!,
rec.departure_time!,
rec.break_start,
rec.break_end,
);
if (!projectMap.has(pid)) {
const projInfo = pid ? projectsMap.get(pid) : undefined;
projectMap.set(pid, {
project_number: projInfo?.project_number || undefined,
project_name: projInfo?.name || undefined,
userMap: new Map(),
});
}
const pg = projectMap.get(pid)!;
if (!pg.userMap.has(uid)) {
pg.userMap.set(uid, { name: uName, hours: 0 });
}
pg.userMap.get(uid)!.hours += hours;
continue;
}
// Use detailed project logs (started_at/ended_at or hours/minutes)
for (const log of rec.attendance_project_logs) {
let hours = 0;
if (log.hours != null || log.minutes != null) {
hours = (Number(log.hours) || 0) + (Number(log.minutes) || 0) / 60;
} else if (log.started_at && log.ended_at) {
hours =
(new Date(log.ended_at).getTime() -
new Date(log.started_at).getTime()) /
(1000 * 60 * 60);
} else {
continue;
}
const pid = log.project_id;
if (!projectMap.has(pid)) {
const projInfo = projectsMap.get(pid);
projectMap.set(pid, {
project_number: projInfo?.project_number || undefined,
project_name: projInfo?.name || undefined,
userMap: new Map(),
});
}
const pg = projectMap.get(pid)!;
if (!pg.userMap.has(uid)) {
pg.userMap.set(uid, { name: uName, hours: 0 });
}
pg.userMap.get(uid)!.hours += hours;
}
pg.userMap.get(uid)!.hours += hours;
}
const projects = Array.from(projectMap.entries()).map(([pid, pg]) => ({
@@ -1315,10 +1375,20 @@ export async function punchAction(userId: number, data: PunchData) {
data: updateData,
});
await prisma.attendance_project_logs.updateMany({
// End active project logs, ensuring ended_at is never before started_at
const activeLogs = await prisma.attendance_project_logs.findMany({
where: { attendance_id: ongoing.id, ended_at: null },
data: { ended_at: departureTime },
});
for (const log of activeLogs) {
const endedAt =
log.started_at && log.started_at > departureTime
? log.started_at
: departureTime;
await prisma.attendance_project_logs.update({
where: { id: log.id },
data: { ended_at: endedAt },
});
}
return {
id: ongoing.id,