feat: NAS storage for invoices/offers, code cleanup, date/time fixes

- NAS storage for created invoices (PDF via puppeteer), received invoices,
  and offers with auto-save on create/edit
- Deterministic file paths derived from DB fields (no file_path column needed)
- Separate NAS mount points: NAS_FINANCIALS_PATH, NAS_OFFERS_PATH
- Invoice language field (cs/en) stored per invoice, replaces lang modal
- Invoices list filtered by month/year matching KPI card selection
- Centralized date helpers (src/utils/date.ts) replacing all .toISOString()
  calls that returned UTC instead of local time
- Attendance project switching uses exact time (not rounded)
- Comment cleanup: removed ~100 unnecessary/Czech comments
- Removed as-any casts in orders and attendance
- Prisma migrations: add invoice language, drop received_invoices BLOB columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-26 10:36:39 +01:00
parent 0317ba3168
commit baceb88347
60 changed files with 2475 additions and 563 deletions

View File

@@ -1,6 +1,7 @@
import { attendance_leave_type, Prisma } from "@prisma/client";
import prisma from "../config/database";
import { getBusinessDaysInMonth } from "../utils/czech-holidays";
import { localDateStr } from "../utils/date";
type AttendanceWithRelations = Prisma.attendanceGetPayload<{
include: {
@@ -254,7 +255,6 @@ export async function getStatus(userId: number) {
};
// 5) Project logs for ongoing shift
// Collect all project IDs from completed shifts for name lookup
const completedProjectIds = new Set<number>();
for (const shift of todayShiftsRaw) {
for (const log of shift.attendance_project_logs) {
@@ -262,7 +262,6 @@ export async function getStatus(userId: number) {
}
}
// Fetch project names for completed shifts
const completedProjectNames = new Map<number, string>();
if (completedProjectIds.size > 0) {
const projects = await prisma.projects.findMany({
@@ -277,7 +276,6 @@ export async function getStatus(userId: number) {
}
}
// Enrich today's completed shifts with project names
const todayShifts = todayShiftsRaw.map((shift) => ({
...shift,
project_logs: shift.attendance_project_logs.map((l) => ({
@@ -337,7 +335,7 @@ export async function getStatus(userId: number) {
today_shifts: todayShifts,
leave_balance: leaveBalance,
monthly_fund: monthlyFund,
date: now.toISOString().split("T")[0],
date: localDateStr(now),
project_logs: projectLogs,
active_project_id: activeProjectId,
};
@@ -759,7 +757,6 @@ export async function getPrintData(
const fundHours = getBusinessDaysInMonth(yr, mo - 1) * 8;
// Load project names for enrichment
const typedRecords = records as AttendanceWithRelations[];
const projectIds = [
@@ -784,7 +781,6 @@ export async function getPrintData(
}
}
// Group records by user and calculate totals
const userTotals: Record<string, Record<string, unknown>> = {};
for (const rec of typedRecords) {
const uid = String(rec.user_id);
@@ -806,7 +802,6 @@ export async function getPrintData(
};
}
// Build record with project_logs for frontend
const projectLogs =
rec.attendance_project_logs?.map((log) => ({
project_id: log.project_id,
@@ -843,7 +838,6 @@ export async function getPrintData(
}
}
// Calculate fund coverage per user
for (const uid of Object.keys(userTotals)) {
const ut = userTotals[uid];
const workedH = Math.round(((ut.minutes as number) / 60) * 10) / 10;
@@ -864,7 +858,6 @@ export async function getPrintData(
);
}
// Leave balances
const leaveBalances: Record<string, Record<string, number>> = {};
const balanceRecords = await prisma.leave_balances.findMany({
where: { year: yr },
@@ -878,7 +871,6 @@ export async function getPrintData(
};
}
// Selected user name
let selectedUserName = "";
if (filterUserId) {
const u = users.find((u) => u.id === filterUserId);
@@ -912,7 +904,8 @@ export async function getActiveProjects() {
});
return activeProjects.map((p) => ({
id: p.id,
name: p.project_number ? `${p.project_number} ${p.name}` : p.name,
name: p.name,
project_number: p.project_number ?? "",
}));
}
@@ -1069,9 +1062,7 @@ export async function bulkCreateAttendance(data: BulkAttendanceData) {
select: { user_id: true, shift_date: true },
});
const existingSet = new Set(
existing.map(
(r) => `${r.user_id}:${r.shift_date.toISOString().split("T")[0]}`,
),
existing.map((r) => `${r.user_id}:${localDateStr(r.shift_date)}`),
);
let inserted = 0;
@@ -1080,7 +1071,7 @@ export async function bulkCreateAttendance(data: BulkAttendanceData) {
for (const userId of data.user_ids.map(Number)) {
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(yr, mo - 1, day);
const dateStr = date.toISOString().split("T")[0];
const dateStr = localDateStr(date);
const dow = date.getDay();
if (dow === 0 || dow === 6) continue;
@@ -1136,7 +1127,7 @@ export async function createLeave(data: LeaveData, authUserId: number) {
while (current <= end) {
const dow = current.getDay();
if (dow !== 0 && dow !== 6) {
const dateStr = current.toISOString().split("T")[0];
const dateStr = localDateStr(current);
const shiftDate = new Date(
Date.UTC(
current.getFullYear(),
@@ -1161,7 +1152,6 @@ export async function createLeave(data: LeaveData, authUserId: number) {
current.setDate(current.getDate() + 1);
}
// Update leave balance for vacation/sick
const totalLeaveHours =
created * (data.leave_hours ? Number(data.leave_hours) : 8);
if (