feat(dashboard): show the user's work plan for today

Adds a "Můj plán na dnešek" card near the clock-in: for the logged-in
user it shows today's work-plan entry — category (coloured dot + label),
the project/site (the "where"), the note, and a "součást rozsahu …" badge
only for genuinely multi-day ranges. A muted "Pro dnešek nemáte
naplánováno." empty state when nothing is scheduled.

Server: GET /api/admin/dashboard now returns today_plan, reusing
plan.service resolveCell (override-beats-range precedence) for the current
user + today's local (Prague) date, enriched with the category label +
colour so the widget needs no extra query. Gated by attendance.record /
attendance.manage (the plan permission); omitted otherwise. No migration.

Verified in Chrome: renders today's entry (Práce · OBJ-2026-006 — koko)
with the category colour. tsc -b --noEmit, npm run build, vitest 152/152.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 22:22:07 +02:00
parent 941caf9d85
commit 512f1fd92b
3 changed files with 145 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import { requireAuth } from "../../middleware/auth";
import { success } from "../../utils/response";
import { localTimeStr } from "../../utils/date";
import { toCzk } from "../../services/exchange-rates";
import { resolveCell } from "../../services/plan.service";
export default async function dashboardRoutes(
fastify: FastifyInstance,
@@ -42,6 +43,29 @@ export default async function dashboardRoutes(
result.my_shift = { has_ongoing: myShift !== null };
}
// Today's work plan (personal) — powers the "Můj plán na dnešek" card.
// resolveCell applies override-beats-range precedence; null = the user has
// plan access but nothing is scheduled today. The category key is enriched
// with its label + colour so the widget needs no extra query. todayStr uses
// local (Europe/Prague) calendar date — the plan's @db.Date day.
if (has("attendance.record") || has("attendance.manage")) {
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const cell = await resolveCell(userId, todayStr);
if (cell) {
const cat = await prisma.plan_categories.findUnique({
where: { key: cell.category },
select: { label: true, color: true },
});
result.today_plan = {
...cell,
category_label: cat?.label ?? cell.category,
category_color: cat?.color ?? null,
};
} else {
result.today_plan = null;
}
}
// Attendance admin — only for attendance.manage
if (has("attendance.manage")) {
const [todayAttendance, onLeaveToday, usersCount] = await Promise.all([