import { FastifyInstance } from 'fastify'; import prisma from '../../config/database'; import { requireAuth } from '../../middleware/auth'; import { success } from '../../utils/response'; export default async function dashboardRoutes(fastify: FastifyInstance): Promise { fastify.get('/', { preHandler: requireAuth }, async (request, reply) => { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1); const authData = request.authData!; const userId = authData.userId; const perms = authData.permissions; const has = (p: string) => perms.includes(p); const result: Record = {}; // My shift — always available for authenticated users with attendance.record if (has('attendance.record')) { const myShift = await prisma.attendance.findFirst({ where: { user_id: userId, arrival_time: { not: null }, departure_time: null }, orderBy: { created_at: 'desc' }, }); result.my_shift = { has_ongoing: myShift !== null }; } // Attendance admin — only for attendance.admin if (has('attendance.admin')) { const [todayAttendance, onLeaveToday, usersCount] = await Promise.all([ prisma.attendance.findMany({ where: { shift_date: { gte: todayStart, lt: todayEnd }, OR: [{ leave_type: null }, { leave_type: 'work' }], }, include: { users: { select: { id: true, first_name: true, last_name: true } } }, orderBy: { arrival_time: 'asc' }, }), prisma.attendance.findMany({ where: { shift_date: { gte: todayStart, lt: todayEnd }, leave_type: { in: ['vacation', 'sick', 'holiday', 'unpaid'] }, }, include: { users: { select: { id: true, first_name: true, last_name: true } } }, }), prisma.users.count({ where: { is_active: true } }), ]); const userAttendanceMap = new Map(); for (const a of todayAttendance) { const existing = userAttendanceMap.get(a.users.id); if (!existing || (a.arrival_time && existing.arrival_time && a.arrival_time > existing.arrival_time)) { userAttendanceMap.set(a.users.id, a); } } let presentCount = 0; const attendanceUsers: Array<{ user_id: number; name: string; initials: string; status: string; arrived_at: string | null; leave_type?: string; }> = []; for (const a of userAttendanceMap.values()) { const user = a.users; const firstInitial = user.first_name?.charAt(0) ?? ''; const lastInitial = user.last_name?.charAt(0) ?? ''; let status = 'out'; if (a.arrival_time) { if (a.departure_time) status = 'out'; else if (a.break_start && !a.break_end) status = 'away'; else { status = 'in'; presentCount++; } } attendanceUsers.push({ user_id: user.id, name: `${user.first_name} ${user.last_name}`, initials: `${firstInitial}${lastInitial}`.toUpperCase(), status, arrived_at: a.arrival_time ? `${String(a.arrival_time.getHours()).padStart(2, '0')}:${String(a.arrival_time.getMinutes()).padStart(2, '0')}` : null, }); } const leaveUserIds = new Set(); for (const a of onLeaveToday) { if (leaveUserIds.has(a.users.id)) continue; leaveUserIds.add(a.users.id); const user = a.users; attendanceUsers.push({ user_id: user.id, name: `${user.first_name} ${user.last_name}`, initials: `${user.first_name?.charAt(0) ?? ''}${user.last_name?.charAt(0) ?? ''}`.toUpperCase(), status: 'leave', arrived_at: null, leave_type: (a.leave_type as string) || 'vacation', }); } result.attendance = { present_today: presentCount, total_active: usersCount, on_leave: leaveUserIds.size, users: attendanceUsers, }; result.users_count = usersCount; } // Offers — only for offers.view if (has('offers.view')) { const [openCount, convertedCount, expiredCount, createdThisMonth] = await Promise.all([ prisma.quotations.count({ where: { status: 'active' } }), prisma.quotations.count({ where: { status: 'converted' } }), prisma.quotations.count({ where: { status: 'expired' } }), prisma.quotations.count({ where: { created_at: { gte: monthStart, lt: monthEnd } } }), ]); result.offers = { open_count: openCount, converted_count: convertedCount, expired_count: expiredCount, created_this_month: createdThisMonth }; } // Projects — only for projects.view if (has('projects.view')) { const [activeCount, activeList] = await Promise.all([ prisma.projects.count({ where: { status: 'aktivni' } }), prisma.projects.findMany({ where: { status: 'aktivni' }, include: { customers: { select: { name: true } } }, orderBy: { created_at: 'desc' }, take: 5, }), ]); result.active_projects = activeCount; result.projects = { active_projects: activeList.map(p => ({ id: p.id, name: p.name ?? '', customer_name: p.customers?.name ?? null, })), }; } // Invoices — only for invoices.view if (has('invoices.view')) { const [unpaidCount, issuedThisMonth] = await Promise.all([ prisma.invoices.count({ where: { status: 'issued' } }), prisma.invoices.findMany({ where: { issue_date: { gte: monthStart, lt: monthEnd } }, include: { invoice_items: true }, }), ]); const revenueByCurrency: Record = {}; for (const inv of issuedThisMonth) { const currency = inv.currency ?? 'CZK'; let total = 0; for (const item of inv.invoice_items) { total += (Number(item.quantity) || 0) * (Number(item.unit_price) || 0); } revenueByCurrency[currency] = (revenueByCurrency[currency] ?? 0) + total; } result.invoices = { revenue_this_month: Object.entries(revenueByCurrency).map(([currency, amount]) => ({ amount: Math.round(amount * 100) / 100, currency, })), unpaid_count: unpaidCount, revenue_czk: revenueByCurrency['CZK'] != null ? Math.round(revenueByCurrency['CZK'] * 100) / 100 : null, }; result.unpaid_invoices = unpaidCount; } // Orders — only for orders.view if (has('orders.view')) { result.pending_orders = await prisma.orders.count({ where: { status: 'prijata' } }); } // Leave pending — only for attendance.approve if (has('attendance.approve')) { const count = await prisma.leave_requests.count({ where: { status: 'pending' } }); result.leave_pending = { count }; result.pending_leave_requests = count; } // Recent activity — only for settings.audit (admin) if (has('settings.audit')) { const logs = await prisma.audit_logs.findMany({ orderBy: { created_at: 'desc' }, take: 8, where: { action: { in: ['create', 'update', 'delete', 'login'] } }, select: { id: true, action: true, entity_type: true, description: true, username: true, created_at: true }, }); result.recent_activity = logs.map(log => ({ id: log.id, action: log.action, entity_type: log.entity_type ?? '', description: log.description ?? '', username: log.username ?? null, created_at: log.created_at ? log.created_at.toISOString() : '', })); } return success(reply, result); }); }