From 87dbde5c59aee2a01cfad53efb63e19557359847 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 24 Mar 2026 20:20:43 +0100 Subject: [PATCH] fix: remove as-any casts, type Dashboard data properly - Route handlers: add exhaustive return after error checks so TypeScript narrows the union and result properties are accessible without casts - attendance.service: use Prisma.attendanceGetPayload for included relations - projects.service: remove unnecessary cast on orders relation - Dashboard.tsx: replace Record with proper DashData interface Co-Authored-By: Claude Opus 4.6 (1M context) --- src/admin/pages/Dashboard.tsx | 55 +++++++++++++++++++++++++++--- src/routes/admin/invoices.ts | 3 +- src/routes/admin/projects.ts | 5 +-- src/routes/admin/quotations.ts | 3 +- src/services/attendance.service.ts | 23 ++++++++----- src/services/projects.service.ts | 2 +- 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/admin/pages/Dashboard.tsx b/src/admin/pages/Dashboard.tsx index a533ca5..bf48b13 100644 --- a/src/admin/pages/Dashboard.tsx +++ b/src/admin/pages/Dashboard.tsx @@ -15,8 +15,55 @@ import DashSessions from "../components/dashboard/DashSessions"; const API_BASE = "/api/admin"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type DashData = Record; +interface DashData { + my_shift?: { has_ongoing: boolean }; + attendance?: { + present_today: number; + total_active: number; + on_leave: number; + users: Array<{ + user_id: number | string; + name: string; + initials?: string; + status: string; + leave_type?: string; + arrived_at?: string; + }>; + }; + offers?: { + open_count: number; + converted_count: number; + expired_count: number; + created_this_month: number; + }; + projects?: { + active_projects: Array<{ + id: number; + name: string; + customer_name: string | null; + }>; + }; + invoices?: { + revenue_this_month: Array<{ amount: number; currency: string }>; + unpaid_count: number; + revenue_czk: number | null; + }; + leave_pending?: { count: number }; + recent_activity?: Array<{ + id: number | string; + action: string; + entity_type: string; + description: string; + username?: string; + created_at: string; + }>; + users_count?: number; + active_projects?: number; + pending_orders?: number; + unpaid_invoices?: number; + pending_leave_requests?: number; + [key: string]: unknown; +} export default function Dashboard() { const { user, updateUser, hasPermission } = useAuth(); @@ -373,11 +420,11 @@ export default function Dashboard() { transition={{ duration: 0.25, delay: 0.12 }} > {hasPermission("settings.audit") && ( - + )} {hasPermission("attendance.admin") && ( - + )} {/* Pravy sloupec: projekty + nabidky */} diff --git a/src/routes/admin/invoices.ts b/src/routes/admin/invoices.ts index 72a6ca5..e21b238 100644 --- a/src/routes/admin/invoices.ts +++ b/src/routes/admin/invoices.ts @@ -160,6 +160,7 @@ export default async function invoicesRoutes( `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`, 400, ); + return error(reply, "Neznámá chyba", 500); } await logAudit({ @@ -168,7 +169,7 @@ export default async function invoicesRoutes( action: "update", entityType: "invoice", entityId: id, - description: `Upravena faktura ${(result as any).invoice_number}`, + description: `Upravena faktura ${result.invoice_number}`, }); return success(reply, { id }, 200, "Faktura byla aktualizována"); }, diff --git a/src/routes/admin/projects.ts b/src/routes/admin/projects.ts index 96d45e5..f2b6546 100644 --- a/src/routes/admin/projects.ts +++ b/src/routes/admin/projects.ts @@ -173,7 +173,7 @@ export default async function projectsRoutes( const body = request.body as Record; const deleteFiles = !!body?.delete_files; const result = await deleteProject(id, deleteFiles); - if (result && "error" in result) { + if ("error" in result) { if (result.error === "not_found") return error(reply, "Projekt nenalezen", 404); if (result.error === "has_order") @@ -182,6 +182,7 @@ export default async function projectsRoutes( "Nelze smazat projekt propojený s objednávkou. Nejdříve smažte objednávku.", 400, ); + return error(reply, "Neznámá chyba", 500); } await logAudit({ @@ -190,7 +191,7 @@ export default async function projectsRoutes( action: "delete", entityType: "project", entityId: id, - description: `Smazán projekt ${(result as any).name}`, + description: `Smazán projekt ${result.name}`, }); return success(reply, null, 200, "Projekt smazán"); }, diff --git a/src/routes/admin/quotations.ts b/src/routes/admin/quotations.ts index 4b1920b..af49b7c 100644 --- a/src/routes/admin/quotations.ts +++ b/src/routes/admin/quotations.ts @@ -278,6 +278,7 @@ export default async function quotationsRoutes( return error(reply, "Nabídka nenalezena", 404); if (result.error === "invalidated") return error(reply, "Nelze upravit zneplatněnou nabídku", 400); + return error(reply, "Neznámá chyba", 500); } // Keep lock — user stays on the page after save @@ -288,7 +289,7 @@ export default async function quotationsRoutes( action: "update", entityType: "quotation", entityId: id, - description: `Upravena nabídka ${(result as any).quotation_number}`, + description: `Upravena nabídka ${result.quotation_number}`, }); return success(reply, { id }, 200, "Nabídka byla uložena"); }, diff --git a/src/services/attendance.service.ts b/src/services/attendance.service.ts index 24238ae..bd6c59a 100644 --- a/src/services/attendance.service.ts +++ b/src/services/attendance.service.ts @@ -1,7 +1,14 @@ -import { attendance_leave_type } from "@prisma/client"; +import { attendance_leave_type, Prisma } from "@prisma/client"; import prisma from "../config/database"; import { getBusinessDaysInMonth } from "../utils/czech-holidays"; +type AttendanceWithRelations = Prisma.attendanceGetPayload<{ + include: { + users: { select: { id: true; first_name: true; last_name: true } }; + attendance_project_logs: true; + }; +}>; + const VALID_LEAVE_TYPES = [ "work", "vacation", @@ -717,13 +724,13 @@ export async function getPrintData( const fundHours = getBusinessDaysInMonth(yr, mo - 1) * 8; // Load project names for enrichment + const typedRecords = records as AttendanceWithRelations[]; + const projectIds = [ ...new Set( - records + typedRecords .flatMap( - (r) => - (r as any).attendance_project_logs?.map((l: any) => l.project_id) || - [], + (r) => r.attendance_project_logs?.map((l) => l.project_id) || [], ) .filter(Boolean), ), @@ -743,10 +750,10 @@ export async function getPrintData( // Group records by user and calculate totals const userTotals: Record> = {}; - for (const rec of records) { + for (const rec of typedRecords) { const uid = String(rec.user_id); if (!userTotals[uid]) { - const u = (rec as any).users; + const u = rec.users; userTotals[uid] = { name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`, minutes: 0, @@ -765,7 +772,7 @@ export async function getPrintData( // Build record with project_logs for frontend const projectLogs = - (rec as any).attendance_project_logs?.map((log: any) => ({ + rec.attendance_project_logs?.map((log) => ({ project_id: log.project_id, project_name: projectMap[log.project_id] || null, hours: log.hours, diff --git a/src/services/projects.service.ts b/src/services/projects.service.ts index 2f4659f..20d2c99 100644 --- a/src/services/projects.service.ts +++ b/src/services/projects.service.ts @@ -58,7 +58,7 @@ export async function listProjects(params: ListProjectsParams) { responsible_user_name: p.users ? `${p.users.first_name} ${p.users.last_name}`.trim() : null, - order_number: (p.orders as any)?.order_number || null, + order_number: p.orders?.order_number || null, })); return { data: enriched, total, page, limit };