1.5.2
- 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user