refactor: extract attendance business logic into attendance.service.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
999
src/services/attendance.service.ts
Normal file
999
src/services/attendance.service.ts
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
import { attendance_leave_type } from '@prisma/client';
|
||||||
|
import prisma from '../config/database';
|
||||||
|
|
||||||
|
const VALID_LEAVE_TYPES = ['work', 'vacation', 'sick', 'holiday', 'unpaid'] as const;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen',
|
||||||
|
'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function countWorkingDays(year: number, month: number): number {
|
||||||
|
let count = 0;
|
||||||
|
const cur = new Date(year, month, 1);
|
||||||
|
while (cur.getMonth() === month) {
|
||||||
|
const dow = cur.getDay();
|
||||||
|
if (dow !== 0 && dow !== 6) count++;
|
||||||
|
cur.setDate(cur.getDate() + 1);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcWorkedHours(arrival: Date, departure: Date, breakStart: Date | null, breakEnd: Date | null): number {
|
||||||
|
let mins = (departure.getTime() - arrival.getTime()) / 60000;
|
||||||
|
if (breakStart && breakEnd) {
|
||||||
|
mins -= (breakEnd.getTime() - breakStart.getTime()) / 60000;
|
||||||
|
}
|
||||||
|
return Math.max(0, mins) / 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundUp15 = (d: Date) => {
|
||||||
|
const ms = 15 * 60 * 1000;
|
||||||
|
return new Date(Math.ceil(d.getTime() / ms) * ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundDown15 = (d: Date) => {
|
||||||
|
const ms = 15 * 60 * 1000;
|
||||||
|
return new Date(Math.floor(d.getTime() / ms) * ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Interfaces ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ListAttendanceParams {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
skip: number;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
userId?: number;
|
||||||
|
isAdmin: boolean;
|
||||||
|
authUserId: number;
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PunchData {
|
||||||
|
punch_action: string;
|
||||||
|
latitude?: number | string | null;
|
||||||
|
longitude?: number | string | null;
|
||||||
|
accuracy?: number | string | null;
|
||||||
|
address?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAttendanceData {
|
||||||
|
user_id?: number;
|
||||||
|
shift_date: string;
|
||||||
|
arrival_time?: string | null;
|
||||||
|
arrival_lat?: number | null;
|
||||||
|
arrival_lng?: number | null;
|
||||||
|
arrival_accuracy?: number | null;
|
||||||
|
arrival_address?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
departure_lat?: number | null;
|
||||||
|
departure_lng?: number | null;
|
||||||
|
departure_accuracy?: number | null;
|
||||||
|
departure_address?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
project_id?: number | null;
|
||||||
|
leave_type: string;
|
||||||
|
leave_hours?: number | null;
|
||||||
|
project_logs?: Array<{ project_id: number; hours?: number; minutes?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAttendanceData {
|
||||||
|
arrival_time?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
break_start?: string | null;
|
||||||
|
break_end?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
project_id?: number | null;
|
||||||
|
leave_type?: string;
|
||||||
|
leave_hours?: number | null;
|
||||||
|
project_logs?: Array<{ project_id: number; hours?: number; minutes?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeaveData {
|
||||||
|
user_id?: number;
|
||||||
|
date_from: string;
|
||||||
|
date_to?: string;
|
||||||
|
leave_type: string;
|
||||||
|
leave_hours?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkAttendanceData {
|
||||||
|
month: string;
|
||||||
|
user_ids: number[];
|
||||||
|
arrival_time: string;
|
||||||
|
departure_time: string;
|
||||||
|
break_start_time: string;
|
||||||
|
break_end_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalancesData {
|
||||||
|
user_id: number;
|
||||||
|
year?: number;
|
||||||
|
action_type: string;
|
||||||
|
vacation_total?: number | null;
|
||||||
|
vacation_used?: number | null;
|
||||||
|
sick_used?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Service Functions ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getStatus(userId: number) {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
|
||||||
|
const todayStart = new Date(y, m, d, 0, 0, 0);
|
||||||
|
const todayEnd = new Date(y, m, d, 23, 59, 59);
|
||||||
|
|
||||||
|
// 1) Ongoing shift (no departure)
|
||||||
|
const ongoingShift = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null } },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) Today's completed shifts
|
||||||
|
const todayShifts = await prisma.attendance.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
shift_date: { gte: todayStart, lte: todayEnd },
|
||||||
|
departure_time: { not: null },
|
||||||
|
},
|
||||||
|
orderBy: { arrival_time: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Leave balance
|
||||||
|
const balance = await prisma.leave_balances.findFirst({
|
||||||
|
where: { user_id: userId, year: y },
|
||||||
|
});
|
||||||
|
const leaveBalance = {
|
||||||
|
vacation_total: balance ? Number(balance.vacation_total) : 160,
|
||||||
|
vacation_used: balance ? Number(balance.vacation_used) : 0,
|
||||||
|
vacation_remaining: balance ? Number(balance.vacation_total) - Number(balance.vacation_used) : 160,
|
||||||
|
sick_used: balance ? Number(balance.sick_used) : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4) Monthly fund
|
||||||
|
const monthStart = new Date(y, m, 1);
|
||||||
|
const monthEnd = new Date(y, m + 1, 0, 23, 59, 59);
|
||||||
|
const monthRecords = await prisma.attendance.findMany({
|
||||||
|
where: { user_id: userId, shift_date: { gte: monthStart, lte: monthEnd } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const workingDays = countWorkingDays(y, m);
|
||||||
|
const fund = workingDays * 8;
|
||||||
|
|
||||||
|
let workedHours = 0;
|
||||||
|
let vacationHours = 0;
|
||||||
|
let sickHours = 0;
|
||||||
|
let holidayHours = 0;
|
||||||
|
let unpaidHours = 0;
|
||||||
|
|
||||||
|
for (const rec of monthRecords) {
|
||||||
|
const lt = (rec.leave_type as string) || 'work';
|
||||||
|
if (lt !== 'work') {
|
||||||
|
const hrs = Number(rec.leave_hours) || 8;
|
||||||
|
if (lt === 'vacation') vacationHours += hrs;
|
||||||
|
else if (lt === 'sick') sickHours += hrs;
|
||||||
|
else if (lt === 'holiday') holidayHours += hrs;
|
||||||
|
else if (lt === 'unpaid') unpaidHours += hrs;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rec.arrival_time && rec.departure_time) {
|
||||||
|
workedHours += calcWorkedHours(rec.arrival_time, rec.departure_time, rec.break_start, rec.break_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const worked = Math.round(workedHours * 100) / 100;
|
||||||
|
const holidayDays = monthRecords.filter(r => (r.leave_type as string) === 'holiday').length;
|
||||||
|
const adjustedFund = Math.max(0, (workingDays - holidayDays) * 8);
|
||||||
|
const leaveHours = vacationHours + sickHours;
|
||||||
|
const covered = worked + leaveHours;
|
||||||
|
const remaining = Math.max(0, adjustedFund - covered);
|
||||||
|
const overtime = Math.max(0, covered - adjustedFund);
|
||||||
|
|
||||||
|
const monthlyFund = {
|
||||||
|
month_name: `${MONTH_NAMES[m]} ${y}`,
|
||||||
|
fund: adjustedFund,
|
||||||
|
business_days: workingDays - holidayDays,
|
||||||
|
worked,
|
||||||
|
covered,
|
||||||
|
remaining,
|
||||||
|
overtime,
|
||||||
|
leave_hours: leaveHours,
|
||||||
|
vacation_hours: vacationHours,
|
||||||
|
sick_hours: sickHours,
|
||||||
|
holiday_hours: holidayHours,
|
||||||
|
unpaid_hours: unpaidHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5) Project logs for ongoing shift
|
||||||
|
let projectLogs: Array<{ id: number; attendance_id: number; project_id: number; started_at: Date | null; ended_at: Date | null; project_name?: string }> = [];
|
||||||
|
let activeProjectId: number | null = null;
|
||||||
|
|
||||||
|
if (ongoingShift) {
|
||||||
|
const logs = await prisma.attendance_project_logs.findMany({
|
||||||
|
where: { attendance_id: ongoingShift.id },
|
||||||
|
orderBy: { started_at: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectIds = [...new Set(logs.map(l => l.project_id))];
|
||||||
|
const projectNames = new Map<number, string>();
|
||||||
|
if (projectIds.length > 0) {
|
||||||
|
const projects = await prisma.projects.findMany({
|
||||||
|
where: { id: { in: projectIds } },
|
||||||
|
select: { id: true, name: true, project_number: true },
|
||||||
|
});
|
||||||
|
for (const p of projects) {
|
||||||
|
projectNames.set(p.id, p.project_number ? `${p.project_number} – ${p.name}` : (p.name || ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectLogs = logs.map(l => ({
|
||||||
|
...l,
|
||||||
|
project_name: projectNames.get(l.project_id) || `Projekt #${l.project_id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeLog = logs.find(l => l.ended_at === null);
|
||||||
|
if (activeLog) {
|
||||||
|
activeProjectId = activeLog.project_id;
|
||||||
|
} else {
|
||||||
|
activeProjectId = ongoingShift.project_id ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ongoing_shift: ongoingShift,
|
||||||
|
today_shifts: todayShifts,
|
||||||
|
leave_balance: leaveBalance,
|
||||||
|
monthly_fund: monthlyFund,
|
||||||
|
date: now.toISOString().split('T')[0],
|
||||||
|
project_logs: projectLogs,
|
||||||
|
active_project_id: activeProjectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveNotes(userId: number, notes: string | null) {
|
||||||
|
const ongoing = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null } },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (!ongoing) return { error: 'Nemáte aktivní směnu.' };
|
||||||
|
|
||||||
|
await prisma.attendance.update({
|
||||||
|
where: { id: ongoing.id },
|
||||||
|
data: { notes: notes ? String(notes) : null },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAddress(userId: number, address: string | null, punchAction: string) {
|
||||||
|
const latest = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (!latest) return { error: 'Nenalezen záznam' };
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
if (punchAction === 'departure') {
|
||||||
|
data.departure_address = address;
|
||||||
|
} else {
|
||||||
|
data.arrival_address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.attendance.update({ where: { id: latest.id }, data });
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchProject(userId: number, projectId: number | null) {
|
||||||
|
const ongoing = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null } },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (!ongoing) return { error: 'Nemáte aktivní směnu.' };
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await prisma.attendance_project_logs.updateMany({
|
||||||
|
where: { attendance_id: ongoing.id, ended_at: null },
|
||||||
|
data: { ended_at: now },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
await prisma.attendance_project_logs.create({
|
||||||
|
data: {
|
||||||
|
attendance_id: ongoing.id,
|
||||||
|
project_id: projectId,
|
||||||
|
started_at: now,
|
||||||
|
ended_at: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.attendance.update({
|
||||||
|
where: { id: ongoing.id },
|
||||||
|
data: { project_id: projectId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBalances(year: number) {
|
||||||
|
const users = await prisma.users.findMany({
|
||||||
|
where: { is_active: true },
|
||||||
|
select: { id: true, first_name: true, last_name: true },
|
||||||
|
orderBy: { last_name: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const balances: Record<string, { name: string; vacation_total: number; vacation_used: number; vacation_remaining: number; sick_used: number }> = {};
|
||||||
|
for (const u of users) {
|
||||||
|
const lb = await prisma.leave_balances.findFirst({ where: { user_id: u.id, year } });
|
||||||
|
const vTotal = lb ? Number(lb.vacation_total) : 160;
|
||||||
|
const vUsed = lb ? Number(lb.vacation_used) : 0;
|
||||||
|
const sUsed = lb ? Number(lb.sick_used) : 0;
|
||||||
|
balances[String(u.id)] = {
|
||||||
|
name: `${u.first_name} ${u.last_name}`.trim(),
|
||||||
|
vacation_total: vTotal,
|
||||||
|
vacation_used: vUsed,
|
||||||
|
vacation_remaining: vTotal - vUsed,
|
||||||
|
sick_used: sUsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })),
|
||||||
|
balances,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkfund(year: number) {
|
||||||
|
const users = await prisma.users.findMany({
|
||||||
|
where: { is_active: true },
|
||||||
|
select: { id: true, first_name: true, last_name: true },
|
||||||
|
orderBy: { last_name: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearStart = new Date(year, 0, 1);
|
||||||
|
const yearEnd = new Date(year, 11, 31, 23, 59, 59);
|
||||||
|
const allRecords = await prisma.attendance.findMany({
|
||||||
|
where: { shift_date: { gte: yearStart, lte: yearEnd } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const months: Record<string, { month_name: string; fund: number; business_days: number; users: Record<string, { name: string; worked: number; covered: number; overtime: number; missing: number }> }> = {};
|
||||||
|
|
||||||
|
for (let m = 0; m < 12; m++) {
|
||||||
|
const bizDays = countWorkingDays(year, m);
|
||||||
|
const fund = bizDays * 8;
|
||||||
|
const monthStart = new Date(year, m, 1);
|
||||||
|
const monthEnd = new Date(year, m + 1, 0, 23, 59, 59);
|
||||||
|
|
||||||
|
const monthUsers: Record<string, { name: string; worked: number; covered: number; overtime: number; missing: number }> = {};
|
||||||
|
|
||||||
|
for (const u of users) {
|
||||||
|
const recs = allRecords.filter(r => r.user_id === u.id && r.shift_date >= monthStart && r.shift_date <= monthEnd);
|
||||||
|
let worked = 0;
|
||||||
|
let vacationHours = 0;
|
||||||
|
let sickHours = 0;
|
||||||
|
let holidayDays = 0;
|
||||||
|
|
||||||
|
for (const rec of recs) {
|
||||||
|
const lt = (rec.leave_type as string) || 'work';
|
||||||
|
if (lt === 'work') {
|
||||||
|
if (rec.arrival_time && rec.departure_time) {
|
||||||
|
worked += calcWorkedHours(rec.arrival_time, rec.departure_time, rec.break_start, rec.break_end);
|
||||||
|
}
|
||||||
|
} else if (lt === 'vacation') {
|
||||||
|
vacationHours += Number(rec.leave_hours) || 8;
|
||||||
|
} else if (lt === 'sick') {
|
||||||
|
sickHours += Number(rec.leave_hours) || 8;
|
||||||
|
} else if (lt === 'holiday') {
|
||||||
|
holidayDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userFund = Math.max(0, (bizDays - holidayDays) * 8);
|
||||||
|
const workedRound = Math.round(worked * 10) / 10;
|
||||||
|
const leaveHours = vacationHours + sickHours;
|
||||||
|
const covered = Math.round((worked + leaveHours) * 10) / 10;
|
||||||
|
const missing = Math.max(0, Math.round((userFund - covered) * 10) / 10);
|
||||||
|
const overtime = Math.max(0, Math.round((covered - userFund) * 10) / 10);
|
||||||
|
|
||||||
|
monthUsers[String(u.id)] = {
|
||||||
|
name: `${u.first_name} ${u.last_name}`.trim(),
|
||||||
|
worked: workedRound,
|
||||||
|
covered,
|
||||||
|
overtime,
|
||||||
|
missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
months[String(m + 1)] = {
|
||||||
|
month_name: MONTH_NAMES[m],
|
||||||
|
fund,
|
||||||
|
business_days: bizDays,
|
||||||
|
users: monthUsers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
months,
|
||||||
|
users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })),
|
||||||
|
balances: {},
|
||||||
|
holidays: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectReport(year: number) {
|
||||||
|
const yearStart = new Date(year, 0, 1);
|
||||||
|
const yearEnd = new Date(year, 11, 31, 23, 59, 59);
|
||||||
|
|
||||||
|
const records = await prisma.attendance.findMany({
|
||||||
|
where: {
|
||||||
|
shift_date: { gte: yearStart, lte: yearEnd },
|
||||||
|
leave_type: 'work',
|
||||||
|
arrival_time: { not: null },
|
||||||
|
departure_time: { not: null },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
users: { select: { id: true, first_name: true, last_name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectIds = [...new Set(records.filter(r => r.project_id).map(r => r.project_id!))];
|
||||||
|
const projectsMap = new Map<number, { name: string; project_number: string }>();
|
||||||
|
if (projectIds.length > 0) {
|
||||||
|
const projects = await prisma.projects.findMany({
|
||||||
|
where: { id: { in: projectIds } },
|
||||||
|
select: { id: true, name: true, project_number: true },
|
||||||
|
});
|
||||||
|
for (const p of projects) {
|
||||||
|
projectsMap.set(p.id, { name: p.name || '', project_number: p.project_number || '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const months: Record<string, { month_name: string; projects: Array<{ project_id: number | null; project_number?: string; project_name?: string; hours: number; users: Array<{ user_id: number; user_name: string; hours: number }> }> }> = {};
|
||||||
|
|
||||||
|
for (let m = 0; m < 12; m++) {
|
||||||
|
const monthStart = new Date(year, m, 1);
|
||||||
|
const monthEnd = new Date(year, m + 1, 0, 23, 59, 59);
|
||||||
|
const monthRecs = records.filter(r => r.shift_date >= monthStart && r.shift_date <= monthEnd);
|
||||||
|
|
||||||
|
if (monthRecs.length === 0) continue;
|
||||||
|
|
||||||
|
const projectMap = new Map<number | null, { project_number?: string; project_name?: string; userMap: Map<number, { name: string; hours: 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 });
|
||||||
|
}
|
||||||
|
pg.userMap.get(uid)!.hours += hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = Array.from(projectMap.entries()).map(([pid, pg]) => ({
|
||||||
|
project_id: pid,
|
||||||
|
project_number: pg.project_number,
|
||||||
|
project_name: pg.project_name,
|
||||||
|
hours: Math.round(Array.from(pg.userMap.values()).reduce((s, u) => s + u.hours, 0) * 10) / 10,
|
||||||
|
users: Array.from(pg.userMap.entries()).map(([uid, ud]) => ({
|
||||||
|
user_id: uid,
|
||||||
|
user_name: ud.name,
|
||||||
|
hours: Math.round(ud.hours * 10) / 10,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
months[String(m + 1)] = {
|
||||||
|
month_name: MONTH_NAMES[m],
|
||||||
|
projects,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { months };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPrintData(monthStr: string, filterUserId: number | null) {
|
||||||
|
const [yearStr, monthNumStr] = monthStr.split('-');
|
||||||
|
const yr = Number(yearStr);
|
||||||
|
const mo = Number(monthNumStr);
|
||||||
|
|
||||||
|
const monthStart = new Date(yr, mo - 1, 1);
|
||||||
|
const monthEnd = new Date(yr, mo, 0, 23, 59, 59);
|
||||||
|
|
||||||
|
const users = await prisma.users.findMany({
|
||||||
|
where: { is_active: true },
|
||||||
|
select: { id: true, first_name: true, last_name: true },
|
||||||
|
orderBy: { last_name: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = {
|
||||||
|
shift_date: { gte: monthStart, lte: monthEnd },
|
||||||
|
};
|
||||||
|
if (filterUserId) where.user_id = filterUserId;
|
||||||
|
|
||||||
|
const records = await prisma.attendance.findMany({
|
||||||
|
where,
|
||||||
|
include: { users: { select: { id: true, first_name: true, last_name: true } } },
|
||||||
|
orderBy: [{ user_id: 'asc' }, { shift_date: 'asc' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const userTotals: Record<string, { name: string; worked: number; vacation: number; sick: number; holiday: number; unpaid: number }> = {};
|
||||||
|
for (const rec of records) {
|
||||||
|
const uid = String(rec.user_id);
|
||||||
|
if (!userTotals[uid]) {
|
||||||
|
const u = rec.users;
|
||||||
|
userTotals[uid] = { name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`, worked: 0, vacation: 0, sick: 0, holiday: 0, unpaid: 0 };
|
||||||
|
}
|
||||||
|
const lt = (rec.leave_type as string) || 'work';
|
||||||
|
if (lt !== 'work') {
|
||||||
|
const hrs = Number(rec.leave_hours) || 8;
|
||||||
|
if (lt === 'vacation') userTotals[uid].vacation += hrs;
|
||||||
|
else if (lt === 'sick') userTotals[uid].sick += hrs;
|
||||||
|
else if (lt === 'holiday') userTotals[uid].holiday += hrs;
|
||||||
|
else if (lt === 'unpaid') userTotals[uid].unpaid += hrs;
|
||||||
|
} else if (rec.arrival_time && rec.departure_time) {
|
||||||
|
userTotals[uid].worked += calcWorkedHours(rec.arrival_time, rec.departure_time, rec.break_start, rec.break_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const uid of Object.keys(userTotals)) {
|
||||||
|
userTotals[uid].worked = Math.round(userTotals[uid].worked * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bizDays = countWorkingDays(yr, mo - 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user_totals: userTotals,
|
||||||
|
users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })),
|
||||||
|
month: monthStr,
|
||||||
|
month_name: `${MONTH_NAMES[mo - 1]} ${yr}`,
|
||||||
|
selected_user: filterUserId,
|
||||||
|
year: yr,
|
||||||
|
fund: { business_days: bizDays, hours: bizDays * 8 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveProjects() {
|
||||||
|
const activeProjects = await prisma.projects.findMany({
|
||||||
|
where: { status: 'aktivni' },
|
||||||
|
select: { id: true, name: true, project_number: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
});
|
||||||
|
return activeProjects.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.project_number ? `${p.project_number} – ${p.name}` : p.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectLogs(attendanceId: number) {
|
||||||
|
return prisma.attendance_project_logs.findMany({
|
||||||
|
where: { attendance_id: attendanceId },
|
||||||
|
orderBy: { started_at: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocationRecord(id: number) {
|
||||||
|
return prisma.attendance.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { users: { select: { id: true, first_name: true, last_name: true } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAttendance(params: ListAttendanceParams) {
|
||||||
|
const { page, limit, skip, order, isAdmin, authUserId } = params;
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (!isAdmin) {
|
||||||
|
where.user_id = authUserId;
|
||||||
|
} else if (params.userId) {
|
||||||
|
where.user_id = params.userId;
|
||||||
|
}
|
||||||
|
if (params.month && params.year) {
|
||||||
|
where.shift_date = {
|
||||||
|
gte: new Date(params.year, params.month - 1, 1),
|
||||||
|
lt: new Date(params.year, params.month, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [records, total] = await Promise.all([
|
||||||
|
prisma.attendance.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { shift_date: order },
|
||||||
|
include: {
|
||||||
|
users: { select: { id: true, first_name: true, last_name: true, username: true } },
|
||||||
|
attendance_project_logs: { orderBy: { started_at: 'asc' } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.attendance.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allProjectIds = new Set<number>();
|
||||||
|
for (const rec of records) {
|
||||||
|
if (rec.project_id) allProjectIds.add(rec.project_id);
|
||||||
|
for (const log of rec.attendance_project_logs) {
|
||||||
|
allProjectIds.add(log.project_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const projectNameMap = new Map<number, string>();
|
||||||
|
if (allProjectIds.size > 0) {
|
||||||
|
const projects = await prisma.projects.findMany({
|
||||||
|
where: { id: { in: [...allProjectIds] } },
|
||||||
|
select: { id: true, name: true, project_number: true },
|
||||||
|
});
|
||||||
|
for (const p of projects) {
|
||||||
|
projectNameMap.set(p.id, p.project_number ? `${p.project_number} – ${p.name}` : (p.name || ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enriched = records.map(rec => {
|
||||||
|
const logs = rec.attendance_project_logs.map(l => ({
|
||||||
|
...l,
|
||||||
|
project_name: projectNameMap.get(l.project_id) || `Projekt #${l.project_id}`,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...rec,
|
||||||
|
project_name: rec.project_id ? (projectNameMap.get(rec.project_id) || null) : null,
|
||||||
|
project_logs: logs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { records: enriched, total, page, limit };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleBalances(data: BalancesData) {
|
||||||
|
const yr = data.year || new Date().getFullYear();
|
||||||
|
|
||||||
|
if (data.action_type === 'edit') {
|
||||||
|
await prisma.leave_balances.upsert({
|
||||||
|
where: { user_id_year: { user_id: data.user_id, year: yr } },
|
||||||
|
update: {
|
||||||
|
vacation_total: data.vacation_total != null ? Number(data.vacation_total) : undefined,
|
||||||
|
vacation_used: data.vacation_used != null ? Number(data.vacation_used) : undefined,
|
||||||
|
sick_used: data.sick_used != null ? Number(data.sick_used) : undefined,
|
||||||
|
updated_at: new Date(),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
user_id: data.user_id,
|
||||||
|
year: yr,
|
||||||
|
vacation_total: Number(data.vacation_total) || 160,
|
||||||
|
vacation_used: Number(data.vacation_used) || 0,
|
||||||
|
sick_used: Number(data.sick_used) || 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Bilance byla uložena', year: yr };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.action_type === 'reset') {
|
||||||
|
await prisma.leave_balances.upsert({
|
||||||
|
where: { user_id_year: { user_id: data.user_id, year: yr } },
|
||||||
|
update: { vacation_used: 0, sick_used: 0, updated_at: new Date() },
|
||||||
|
create: { user_id: data.user_id, year: yr, vacation_total: 160, vacation_used: 0, sick_used: 0 },
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Bilance byla resetována', year: yr };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: 'Neplatný typ akce' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkCreateAttendance(data: BulkAttendanceData) {
|
||||||
|
const [yrStr, moStr] = data.month.split('-');
|
||||||
|
const yr = Number(yrStr);
|
||||||
|
const mo = Number(moStr);
|
||||||
|
const daysInMonth = new Date(yr, mo, 0).getDate();
|
||||||
|
|
||||||
|
const dateFrom = new Date(yr, mo - 1, 1);
|
||||||
|
const dateTo = new Date(yr, mo, 0, 23, 59, 59);
|
||||||
|
const existing = await prisma.attendance.findMany({
|
||||||
|
where: { user_id: { in: data.user_ids.map(Number) }, shift_date: { gte: dateFrom, lte: dateTo } },
|
||||||
|
select: { user_id: true, shift_date: true },
|
||||||
|
});
|
||||||
|
const existingSet = new Set(existing.map(r => `${r.user_id}:${r.shift_date.toISOString().split('T')[0]}`));
|
||||||
|
|
||||||
|
let inserted = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
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 dow = date.getDay();
|
||||||
|
|
||||||
|
if (dow === 0 || dow === 6) continue;
|
||||||
|
|
||||||
|
if (existingSet.has(`${userId}:${dateStr}`)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftDate = new Date(Date.UTC(yr, mo - 1, day, 12, 0, 0));
|
||||||
|
await prisma.attendance.create({
|
||||||
|
data: {
|
||||||
|
user_id: userId,
|
||||||
|
shift_date: shiftDate,
|
||||||
|
arrival_time: new Date(`${dateStr}T${data.arrival_time}:00`),
|
||||||
|
departure_time: new Date(`${dateStr}T${data.departure_time}:00`),
|
||||||
|
break_start: new Date(`${dateStr}T${data.break_start_time}:00`),
|
||||||
|
break_end: new Date(`${dateStr}T${data.break_end_time}:00`),
|
||||||
|
leave_type: 'work',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
inserted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = `Vytvořeno ${inserted} záznamů`;
|
||||||
|
if (skipped > 0) msg += ` (${skipped} přeskočeno — již existují)`;
|
||||||
|
|
||||||
|
return { inserted, skipped, message: msg };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLeave(data: LeaveData, authUserId: number) {
|
||||||
|
const userId = data.user_id ?? authUserId;
|
||||||
|
const dateFrom = data.date_from;
|
||||||
|
const dateTo = data.date_to || dateFrom;
|
||||||
|
const leaveTypeStr = data.leave_type;
|
||||||
|
if (!VALID_LEAVE_TYPES.includes(leaveTypeStr as typeof VALID_LEAVE_TYPES[number])) {
|
||||||
|
return { error: 'Neplatný typ nepřítomnosti' };
|
||||||
|
}
|
||||||
|
const leaveType = leaveTypeStr as attendance_leave_type;
|
||||||
|
|
||||||
|
if (!dateFrom) return { error: 'Datum je povinné' };
|
||||||
|
|
||||||
|
const start = new Date(dateFrom);
|
||||||
|
const end = new Date(dateTo);
|
||||||
|
let created = 0;
|
||||||
|
|
||||||
|
const current = new Date(start);
|
||||||
|
while (current <= end) {
|
||||||
|
const dow = current.getDay();
|
||||||
|
if (dow !== 0 && dow !== 6) {
|
||||||
|
const dateStr = current.toISOString().split('T')[0];
|
||||||
|
const shiftDate = new Date(Date.UTC(current.getFullYear(), current.getMonth(), current.getDate(), 12, 0, 0));
|
||||||
|
await prisma.attendance.create({
|
||||||
|
data: {
|
||||||
|
user_id: userId,
|
||||||
|
shift_date: shiftDate,
|
||||||
|
leave_type: leaveType,
|
||||||
|
leave_hours: data.leave_hours ? Number(data.leave_hours) : 8,
|
||||||
|
notes: data.notes ? String(data.notes) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update leave balance for vacation/sick
|
||||||
|
const totalLeaveHours = created * (data.leave_hours ? Number(data.leave_hours) : 8);
|
||||||
|
if ((leaveType === 'vacation' || leaveType === 'sick') && totalLeaveHours > 0) {
|
||||||
|
const year = new Date(dateFrom).getFullYear();
|
||||||
|
const existingBalance = await prisma.leave_balances.findFirst({
|
||||||
|
where: { user_id: userId, year },
|
||||||
|
});
|
||||||
|
if (existingBalance) {
|
||||||
|
const updateField = leaveType === 'vacation' ? 'vacation_used' : 'sick_used';
|
||||||
|
await prisma.leave_balances.update({
|
||||||
|
where: { id: existingBalance.id },
|
||||||
|
data: { [updateField]: Number(existingBalance[updateField]) + totalLeaveHours, updated_at: new Date() },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await prisma.leave_balances.create({
|
||||||
|
data: {
|
||||||
|
user_id: userId,
|
||||||
|
year,
|
||||||
|
vacation_total: 160,
|
||||||
|
vacation_used: leaveType === 'vacation' ? totalLeaveHours : 0,
|
||||||
|
sick_used: leaveType === 'sick' ? totalLeaveHours : 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { created, message: `Vytvořeno ${created} záznamů nepřítomnosti` };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function punchAction(userId: number, data: PunchData) {
|
||||||
|
const action = data.punch_action;
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
|
||||||
|
const today = new Date(Date.UTC(y, m, d, 12, 0, 0));
|
||||||
|
|
||||||
|
const gpsLat = data.latitude != null && data.latitude !== '' ? Number(data.latitude) : null;
|
||||||
|
const gpsLng = data.longitude != null && data.longitude !== '' ? Number(data.longitude) : null;
|
||||||
|
const gpsAcc = data.accuracy != null && data.accuracy !== '' ? Number(data.accuracy) : null;
|
||||||
|
const gpsAddr = data.address ?? null;
|
||||||
|
|
||||||
|
if (action === 'arrival') {
|
||||||
|
const ongoing = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null } },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (ongoing) {
|
||||||
|
return { error: 'Máte již aktivní směnu. Nejdříve zaznamenejte odchod.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrivalTime = roundUp15(now);
|
||||||
|
const record = await prisma.attendance.create({
|
||||||
|
data: {
|
||||||
|
user_id: userId,
|
||||||
|
shift_date: today,
|
||||||
|
arrival_time: arrivalTime,
|
||||||
|
arrival_lat: gpsLat,
|
||||||
|
arrival_lng: gpsLng,
|
||||||
|
arrival_accuracy: gpsAcc,
|
||||||
|
arrival_address: gpsAddr,
|
||||||
|
leave_type: 'work',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: record.id, status: 201, message: 'Příchod zaznamenán', auditAction: 'create' as const, auditDescription: 'Zaznamenán příchod' };
|
||||||
|
|
||||||
|
} else if (action === 'departure') {
|
||||||
|
const ongoing = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null } },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (!ongoing) {
|
||||||
|
return { error: 'Nemáte aktivní směnu.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const departureTime = roundDown15(now);
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
departure_time: departureTime,
|
||||||
|
departure_lat: gpsLat,
|
||||||
|
departure_lng: gpsLng,
|
||||||
|
departure_accuracy: gpsAcc,
|
||||||
|
departure_address: gpsAddr,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ongoing.break_start && ongoing.arrival_time) {
|
||||||
|
const shiftMs = departureTime.getTime() - ongoing.arrival_time.getTime();
|
||||||
|
const shiftHours = shiftMs / (1000 * 60 * 60);
|
||||||
|
if (shiftHours > 6) {
|
||||||
|
const midpoint = new Date(ongoing.arrival_time.getTime() + shiftMs / 2);
|
||||||
|
const breakMins = shiftHours > 12 ? 30 : 15;
|
||||||
|
updateData.break_start = midpoint;
|
||||||
|
updateData.break_end = new Date(midpoint.getTime() + breakMins * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.attendance.update({ where: { id: ongoing.id }, data: updateData });
|
||||||
|
|
||||||
|
await prisma.attendance_project_logs.updateMany({
|
||||||
|
where: { attendance_id: ongoing.id, ended_at: null },
|
||||||
|
data: { ended_at: departureTime },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: ongoing.id, status: 200, message: 'Odchod zaznamenán', auditAction: 'update' as const, auditDescription: 'Zaznamenán odchod' };
|
||||||
|
|
||||||
|
} else if (action === 'break_start') {
|
||||||
|
const ongoing = await prisma.attendance.findFirst({
|
||||||
|
where: { user_id: userId, departure_time: null, arrival_time: { not: null }, break_start: null },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
if (!ongoing) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
await prisma.attendance.update({
|
||||||
|
where: { id: ongoing.id },
|
||||||
|
data: { break_start: breakStart, break_end: breakEnd },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: ongoing.id, status: 200, message: 'Přestávka zaznamenána', auditAction: 'update' as const, auditDescription: 'Zaznamenána přestávka' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: 'Neplatná akce' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAttendance(data: CreateAttendanceData, authUserId: number) {
|
||||||
|
const record = await prisma.attendance.create({
|
||||||
|
data: {
|
||||||
|
user_id: data.user_id ?? authUserId,
|
||||||
|
shift_date: new Date(data.shift_date),
|
||||||
|
arrival_time: data.arrival_time ? new Date(data.arrival_time) : null,
|
||||||
|
arrival_lat: data.arrival_lat ?? null,
|
||||||
|
arrival_lng: data.arrival_lng ?? null,
|
||||||
|
arrival_accuracy: data.arrival_accuracy ?? null,
|
||||||
|
arrival_address: data.arrival_address ?? null,
|
||||||
|
departure_time: data.departure_time ? new Date(data.departure_time) : null,
|
||||||
|
departure_lat: data.departure_lat ?? null,
|
||||||
|
departure_lng: data.departure_lng ?? null,
|
||||||
|
departure_accuracy: data.departure_accuracy ?? null,
|
||||||
|
departure_address: data.departure_address ?? null,
|
||||||
|
notes: data.notes ?? null,
|
||||||
|
project_id: data.project_id ?? null,
|
||||||
|
leave_type: data.leave_type as attendance_leave_type,
|
||||||
|
leave_hours: data.leave_hours ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data.project_logs)) {
|
||||||
|
const logs = data.project_logs
|
||||||
|
.filter(l => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0));
|
||||||
|
if (logs.length > 0) {
|
||||||
|
await prisma.attendance_project_logs.createMany({
|
||||||
|
data: logs.map(l => ({
|
||||||
|
attendance_id: record.id,
|
||||||
|
project_id: Number(l.project_id),
|
||||||
|
hours: Number(l.hours) || 0,
|
||||||
|
minutes: Number(l.minutes) || 0,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: record.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAttendance(id: number, data: UpdateAttendanceData, authUserId: number, isAdmin: boolean) {
|
||||||
|
const existing = await prisma.attendance.findUnique({ where: { id } });
|
||||||
|
if (!existing) return { error: 'Záznam nenalezen', status: 404 };
|
||||||
|
|
||||||
|
if (existing.user_id !== authUserId && !isAdmin) {
|
||||||
|
return { error: 'Nemáte oprávnění upravit tento záznam', status: 403 };
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.attendance.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
arrival_time: data.arrival_time !== undefined ? (data.arrival_time ? new Date(String(data.arrival_time)) : null) : undefined,
|
||||||
|
departure_time: data.departure_time !== undefined ? (data.departure_time ? new Date(String(data.departure_time)) : null) : undefined,
|
||||||
|
break_start: data.break_start !== undefined ? (data.break_start ? new Date(String(data.break_start)) : null) : undefined,
|
||||||
|
break_end: data.break_end !== undefined ? (data.break_end ? new Date(String(data.break_end)) : null) : undefined,
|
||||||
|
notes: data.notes !== undefined ? (data.notes ? String(data.notes) : null) : undefined,
|
||||||
|
project_id: data.project_id !== undefined ? (data.project_id ? Number(data.project_id) : null) : undefined,
|
||||||
|
leave_type: data.leave_type !== undefined ? String(data.leave_type) as attendance_leave_type : undefined,
|
||||||
|
leave_hours: data.leave_hours !== undefined ? (data.leave_hours ? Number(data.leave_hours) : null) : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data.project_logs)) {
|
||||||
|
await prisma.attendance_project_logs.deleteMany({ where: { attendance_id: id } });
|
||||||
|
const logs = data.project_logs
|
||||||
|
.filter(l => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0));
|
||||||
|
if (logs.length > 0) {
|
||||||
|
await prisma.attendance_project_logs.createMany({
|
||||||
|
data: logs.map(l => ({
|
||||||
|
attendance_id: id,
|
||||||
|
project_id: Number(l.project_id),
|
||||||
|
hours: Number(l.hours) || 0,
|
||||||
|
minutes: Number(l.minutes) || 0,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAttendance(id: number) {
|
||||||
|
const existing = await prisma.attendance.findUnique({ where: { id } });
|
||||||
|
if (!existing) return { error: 'Záznam nenalezen' };
|
||||||
|
|
||||||
|
await prisma.attendance.delete({ where: { id } });
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user