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<string,any> with proper DashData interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-24 20:20:43 +01:00
parent 106606f3fa
commit 87dbde5c59
6 changed files with 74 additions and 17 deletions

View File

@@ -15,8 +15,55 @@ import DashSessions from "../components/dashboard/DashSessions";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
// eslint-disable-next-line @typescript-eslint/no-explicit-any interface DashData {
type DashData = Record<string, any>; 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() { export default function Dashboard() {
const { user, updateUser, hasPermission } = useAuth(); const { user, updateUser, hasPermission } = useAuth();
@@ -373,11 +420,11 @@ export default function Dashboard() {
transition={{ duration: 0.25, delay: 0.12 }} transition={{ duration: 0.25, delay: 0.12 }}
> >
{hasPermission("settings.audit") && ( {hasPermission("settings.audit") && (
<DashActivityFeed activities={dashData?.recent_activity} /> <DashActivityFeed activities={dashData?.recent_activity ?? null} />
)} )}
{hasPermission("attendance.admin") && ( {hasPermission("attendance.admin") && (
<DashAttendanceToday attendance={dashData?.attendance} /> <DashAttendanceToday attendance={dashData?.attendance ?? null} />
)} )}
{/* Pravy sloupec: projekty + nabidky */} {/* Pravy sloupec: projekty + nabidky */}

View File

@@ -160,6 +160,7 @@ export default async function invoicesRoutes(
`Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`, `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`,
400, 400,
); );
return error(reply, "Neznámá chyba", 500);
} }
await logAudit({ await logAudit({
@@ -168,7 +169,7 @@ export default async function invoicesRoutes(
action: "update", action: "update",
entityType: "invoice", entityType: "invoice",
entityId: id, 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"); return success(reply, { id }, 200, "Faktura byla aktualizována");
}, },

View File

@@ -173,7 +173,7 @@ export default async function projectsRoutes(
const body = request.body as Record<string, unknown>; const body = request.body as Record<string, unknown>;
const deleteFiles = !!body?.delete_files; const deleteFiles = !!body?.delete_files;
const result = await deleteProject(id, deleteFiles); const result = await deleteProject(id, deleteFiles);
if (result && "error" in result) { if ("error" in result) {
if (result.error === "not_found") if (result.error === "not_found")
return error(reply, "Projekt nenalezen", 404); return error(reply, "Projekt nenalezen", 404);
if (result.error === "has_order") 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.", "Nelze smazat projekt propojený s objednávkou. Nejdříve smažte objednávku.",
400, 400,
); );
return error(reply, "Neznámá chyba", 500);
} }
await logAudit({ await logAudit({
@@ -190,7 +191,7 @@ export default async function projectsRoutes(
action: "delete", action: "delete",
entityType: "project", entityType: "project",
entityId: id, entityId: id,
description: `Smazán projekt ${(result as any).name}`, description: `Smazán projekt ${result.name}`,
}); });
return success(reply, null, 200, "Projekt smazán"); return success(reply, null, 200, "Projekt smazán");
}, },

View File

@@ -278,6 +278,7 @@ export default async function quotationsRoutes(
return error(reply, "Nabídka nenalezena", 404); return error(reply, "Nabídka nenalezena", 404);
if (result.error === "invalidated") if (result.error === "invalidated")
return error(reply, "Nelze upravit zneplatněnou nabídku", 400); 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 // Keep lock — user stays on the page after save
@@ -288,7 +289,7 @@ export default async function quotationsRoutes(
action: "update", action: "update",
entityType: "quotation", entityType: "quotation",
entityId: id, 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"); return success(reply, { id }, 200, "Nabídka byla uložena");
}, },

View File

@@ -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 prisma from "../config/database";
import { getBusinessDaysInMonth } from "../utils/czech-holidays"; 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 = [ const VALID_LEAVE_TYPES = [
"work", "work",
"vacation", "vacation",
@@ -717,13 +724,13 @@ export async function getPrintData(
const fundHours = getBusinessDaysInMonth(yr, mo - 1) * 8; const fundHours = getBusinessDaysInMonth(yr, mo - 1) * 8;
// Load project names for enrichment // Load project names for enrichment
const typedRecords = records as AttendanceWithRelations[];
const projectIds = [ const projectIds = [
...new Set( ...new Set(
records typedRecords
.flatMap( .flatMap(
(r) => (r) => r.attendance_project_logs?.map((l) => l.project_id) || [],
(r as any).attendance_project_logs?.map((l: any) => l.project_id) ||
[],
) )
.filter(Boolean), .filter(Boolean),
), ),
@@ -743,10 +750,10 @@ export async function getPrintData(
// Group records by user and calculate totals // Group records by user and calculate totals
const userTotals: Record<string, Record<string, unknown>> = {}; const userTotals: Record<string, Record<string, unknown>> = {};
for (const rec of records) { for (const rec of typedRecords) {
const uid = String(rec.user_id); const uid = String(rec.user_id);
if (!userTotals[uid]) { if (!userTotals[uid]) {
const u = (rec as any).users; const u = rec.users;
userTotals[uid] = { userTotals[uid] = {
name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`, name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`,
minutes: 0, minutes: 0,
@@ -765,7 +772,7 @@ export async function getPrintData(
// Build record with project_logs for frontend // Build record with project_logs for frontend
const projectLogs = const projectLogs =
(rec as any).attendance_project_logs?.map((log: any) => ({ rec.attendance_project_logs?.map((log) => ({
project_id: log.project_id, project_id: log.project_id,
project_name: projectMap[log.project_id] || null, project_name: projectMap[log.project_id] || null,
hours: log.hours, hours: log.hours,

View File

@@ -58,7 +58,7 @@ export async function listProjects(params: ListProjectsParams) {
responsible_user_name: p.users responsible_user_name: p.users
? `${p.users.first_name} ${p.users.last_name}`.trim() ? `${p.users.first_name} ${p.users.last_name}`.trim()
: null, : null,
order_number: (p.orders as any)?.order_number || null, order_number: p.orders?.order_number || null,
})); }));
return { data: enriched, total, page, limit }; return { data: enriched, total, page, limit };