feat: system settings, dynamic logos, template numbering, permission consolidation
- System settings page with tabs: Security, System, Firma
- Configurable attendance rules (break thresholds, rounding) from DB
- Configurable document numbering with template patterns ({YYYY}/{PREFIX}/{NNN})
- Dynamic logo upload (light/dark variants) served from DB instead of static files
- Email settings (SMTP from/name, alert/leave emails) configurable in UI
- Currency and VAT rate lists configurable, used across all modules
- Permissions simplified: offers.settings + settings.roles + settings.security → settings.manage
- Leaflet bundled locally, removed unpkg.com from CSP
- Silent catch blocks fixed with proper logging
- console.log replaced with app.log.info in server.ts
- Schema renamed: company-settings.schema → settings.schema
- App info section: version, Node.js, uptime, memory, DB status, NAS status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,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";
|
||||
import { getSystemSettings } from "./system-settings";
|
||||
|
||||
type AttendanceWithRelations = Prisma.attendanceGetPayload<{
|
||||
include: {
|
||||
@@ -48,13 +49,13 @@ function calcWorkedHours(
|
||||
return Math.max(0, mins) / 60;
|
||||
}
|
||||
|
||||
const roundUp15 = (d: Date) => {
|
||||
const ms = 15 * 60 * 1000;
|
||||
const roundUp = (d: Date, minutes: number) => {
|
||||
const ms = minutes * 60 * 1000;
|
||||
return new Date(Math.ceil(d.getTime() / ms) * ms);
|
||||
};
|
||||
|
||||
const roundDown15 = (d: Date) => {
|
||||
const ms = 15 * 60 * 1000;
|
||||
const roundDown = (d: Date, minutes: number) => {
|
||||
const ms = minutes * 60 * 1000;
|
||||
return new Date(Math.floor(d.getTime() / ms) * ms);
|
||||
};
|
||||
|
||||
@@ -1189,6 +1190,7 @@ export async function createLeave(data: LeaveData, authUserId: number) {
|
||||
}
|
||||
|
||||
export async function punchAction(userId: number, data: PunchData) {
|
||||
const settings = await getSystemSettings();
|
||||
const action = data.punch_action;
|
||||
const now = new Date();
|
||||
const y = now.getFullYear(),
|
||||
@@ -1223,7 +1225,7 @@ export async function punchAction(userId: number, data: PunchData) {
|
||||
return { error: "Máte již aktivní směnu. Nejdříve zaznamenejte odchod." };
|
||||
}
|
||||
|
||||
const arrivalTime = roundUp15(now);
|
||||
const arrivalTime = roundUp(now, settings.clock_rounding_minutes);
|
||||
const record = await prisma.attendance.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
@@ -1257,7 +1259,7 @@ export async function punchAction(userId: number, data: PunchData) {
|
||||
return { error: "Nemáte aktivní směnu." };
|
||||
}
|
||||
|
||||
const departureTime = roundDown15(now);
|
||||
const departureTime = roundDown(now, settings.clock_rounding_minutes);
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
departure_time: departureTime,
|
||||
@@ -1270,9 +1272,12 @@ export async function punchAction(userId: number, data: PunchData) {
|
||||
if (!ongoing.break_start && ongoing.arrival_time) {
|
||||
const shiftMs = departureTime.getTime() - ongoing.arrival_time.getTime();
|
||||
const shiftHours = shiftMs / (1000 * 60 * 60);
|
||||
if (shiftHours > 6) {
|
||||
if (shiftHours > settings.break_threshold_hours) {
|
||||
const midpoint = new Date(ongoing.arrival_time.getTime() + shiftMs / 2);
|
||||
const breakMins = shiftHours > 12 ? 30 : 15;
|
||||
const breakMins =
|
||||
shiftHours > settings.break_threshold_hours * 2
|
||||
? settings.break_duration_long
|
||||
: settings.break_duration_short;
|
||||
updateData.break_start = midpoint;
|
||||
updateData.break_end = new Date(
|
||||
midpoint.getTime() + breakMins * 60 * 1000,
|
||||
@@ -1311,9 +1316,11 @@ export async function punchAction(userId: number, data: PunchData) {
|
||||
return { error: "Nemáte aktivní směnu bez přestávky." };
|
||||
}
|
||||
|
||||
const ms10 = 10 * 60 * 1000;
|
||||
const breakStart = new Date(Math.round(now.getTime() / ms10) * ms10);
|
||||
const breakEnd = new Date(breakStart.getTime() + 30 * 60 * 1000);
|
||||
const msRound = settings.clock_rounding_minutes * 60 * 1000;
|
||||
const breakStart = new Date(Math.round(now.getTime() / msRound) * msRound);
|
||||
const breakEnd = new Date(
|
||||
breakStart.getTime() + settings.break_duration_long * 60 * 1000,
|
||||
);
|
||||
|
||||
await prisma.attendance.update({
|
||||
where: { id: ongoing.id },
|
||||
|
||||
Reference in New Issue
Block a user