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:
@@ -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 */}
|
||||||
|
|||||||
@@ -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");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user