chore: add pre-commit hooks, tighten ESLint, activate Sentry DSN, publish CI coverage (Phase 1)

- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files
- Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error
- Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin
- Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments
- Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example
- Add coverage artifact upload step to CI test job
- Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:49:29 +02:00
parent 605fd7cea1
commit 82acc56b8d
38 changed files with 2901 additions and 1251 deletions
@@ -10,7 +10,9 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { VACATION_TYPE_LABELS } from "~/lib/status-styles.js";
const VACATION_TYPES = Object.values(VacationType);
const REQUESTABLE_VACATION_TYPES = VACATION_TYPES.filter((type) => type !== VacationType.PUBLIC_HOLIDAY);
const REQUESTABLE_VACATION_TYPES = VACATION_TYPES.filter(
(type) => type !== VacationType.PUBLIC_HOLIDAY,
);
const HOLIDAY_SOURCE_LABELS = {
CALENDAR: "Calendar",
@@ -75,7 +77,11 @@ function getHolidaySourceLabel(source: string): string {
return source;
}
export function VacationModal({ resourceId: initialResourceId, onClose, onSuccess }: VacationModalProps) {
export function VacationModal({
resourceId: initialResourceId,
onClose,
onSuccess,
}: VacationModalProps) {
const [resourceId, setResourceId] = useState(initialResourceId ?? "");
const [type, setType] = useState<VacationType>(VacationType.ANNUAL);
const [startDate, setStartDate] = useState("");
@@ -126,17 +132,17 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
},
{
enabled:
!!resourceId
&& !!debouncedStart
&& !!debouncedEnd
&& (!isHalfDay || debouncedStart === debouncedEnd),
!!resourceId &&
!!debouncedStart &&
!!debouncedEnd &&
(!isHalfDay || debouncedStart === debouncedEnd),
staleTime: 10_000,
},
);
const utils = trpc.useUtils();
// @ts-ignore TS2589: tRPC infers union type too deeply for CreateVacationRequestSchema with .superRefine()
// @ts-expect-error TS2589: tRPC infers union type too deeply for CreateVacationRequestSchema with .superRefine()
const createMutation = trpc.vacation.create.useMutation({
onSuccess: async () => {
await utils.vacation.list.invalidate();
@@ -177,14 +183,17 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
const inputClass =
"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm dark:bg-gray-900 dark:text-gray-100";
const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
const resourceList: { id: string; displayName: string; eid: string }[] = resources?.resources ?? [];
const resourceList: { id: string; displayName: string; eid: string }[] =
resources?.resources ?? [];
return (
<AnimatedModal open={true} onClose={onClose} maxWidth="max-w-lg" className="mx-4">
<div>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Request Vacation</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Request Vacation
</h2>
<button
type="button"
onClick={onClose}
@@ -200,7 +209,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
{!initialResourceId && (
<div>
<label htmlFor="vac-resource" className={labelClass}>
Resource <span className="text-red-500">*</span><InfoTooltip content="The employee this vacation request is for." />
Resource <span className="text-red-500">*</span>
<InfoTooltip content="The employee this vacation request is for." />
</label>
<select
id="vac-resource"
@@ -222,7 +232,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
{/* Type */}
<div>
<label htmlFor="vac-type" className={labelClass}>
Type <span className="text-red-500">*</span><InfoTooltip content="ANNUAL = paid leave (deducted from entitlement) · SICK = sick leave · OTHER = special leave. Public holidays come from Holiday Calendars and are excluded automatically." />
Type <span className="text-red-500">*</span>
<InfoTooltip content="ANNUAL = paid leave (deducted from entitlement) · SICK = sick leave · OTHER = special leave. Public holidays come from Holiday Calendars and are excluded automatically." />
</label>
<select
id="vac-type"
@@ -242,7 +253,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="vac-start" className={labelClass}>
Start Date <span className="text-red-500">*</span><InfoTooltip content="First day of leave (inclusive)." />
Start Date <span className="text-red-500">*</span>
<InfoTooltip content="First day of leave (inclusive)." />
</label>
<DateInput
id="vac-start"
@@ -254,7 +266,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
</div>
<div>
<label htmlFor="vac-end" className={labelClass}>
End Date <span className="text-red-500">*</span><InfoTooltip content="Last day of leave (inclusive)." />
End Date <span className="text-red-500">*</span>
<InfoTooltip content="Last day of leave (inclusive)." />
</label>
<DateInput
id="vac-end"
@@ -276,7 +289,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
onChange={(e) => setIsHalfDay(e.target.checked)}
className="rounded border-gray-300 dark:border-gray-600 text-brand-600 focus:ring-brand-500"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Half day</span><InfoTooltip content="Request only half a day off (morning or afternoon). Counts as 0.5 days against entitlement." />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Half day</span>
<InfoTooltip content="Request only half a day off (morning or afternoon). Counts as 0.5 days against entitlement." />
</label>
{isHalfDay && (
<div className="flex gap-3">
@@ -330,7 +344,10 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
return (
<li key={v.id}>
{r?.displayName ?? "—"}{" "}
<span className="text-blue-500">({new Date(v.startDate).toLocaleDateString("en-GB")} {new Date(v.endDate).toLocaleDateString("en-GB")})</span>
<span className="text-blue-500">
({new Date(v.startDate).toLocaleDateString("en-GB")} {" "}
{new Date(v.endDate).toLocaleDateString("en-GB")})
</span>
</li>
);
})}
@@ -374,26 +391,46 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
</div>
{buildHolidayBasisLabel(previewQuery.data).length > 0 && (
<div data-testid="vacation-preview-holiday-basis" className="rounded-md bg-white/70 px-3 py-2 text-xs sm:text-sm">
<div
data-testid="vacation-preview-holiday-basis"
className="rounded-md bg-white/70 px-3 py-2 text-xs sm:text-sm"
>
<span className="font-medium">Holiday basis:</span>{" "}
{buildHolidayBasisLabel(previewQuery.data).join(" / ")}
</div>
)}
{(previewQuery.data.holidayContext.sources.hasCalendarHolidays || previewQuery.data.holidayContext.sources.hasLegacyPublicHolidayEntries) && (
<div data-testid="vacation-preview-holiday-sources" className="rounded-md bg-white/70 px-3 py-2 text-xs sm:text-sm">
{(previewQuery.data.holidayContext.sources.hasCalendarHolidays ||
previewQuery.data.holidayContext.sources.hasLegacyPublicHolidayEntries) && (
<div
data-testid="vacation-preview-holiday-sources"
className="rounded-md bg-white/70 px-3 py-2 text-xs sm:text-sm"
>
<span className="font-medium">Sources:</span>{" "}
{[
previewQuery.data.holidayContext.sources.hasCalendarHolidays ? "Holiday Calendar" : null,
previewQuery.data.holidayContext.sources.hasLegacyPublicHolidayEntries ? "Legacy public holiday entries" : null,
].filter(Boolean).join(" + ")}
previewQuery.data.holidayContext.sources.hasCalendarHolidays
? "Holiday Calendar"
: null,
previewQuery.data.holidayContext.sources.hasLegacyPublicHolidayEntries
? "Legacy public holiday entries"
: null,
]
.filter(Boolean)
.join(" + ")}
</div>
)}
{previewQuery.data.publicHolidayDates.length > 0 && (
<div data-testid="vacation-preview-public-holidays" className="text-xs sm:text-sm">
<div
data-testid="vacation-preview-public-holidays"
className="text-xs sm:text-sm"
>
<span className="font-medium">Excluded public holidays:</span>{" "}
{previewQuery.data.holidayDetails.map((holiday) => `${holiday.date} (${getHolidaySourceLabel(holiday.source)})`).join(", ")}
{previewQuery.data.holidayDetails
.map(
(holiday) => `${holiday.date} (${getHolidaySourceLabel(holiday.source)})`,
)
.join(", ")}
</div>
)}
@@ -406,9 +443,7 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
)}
{previewQuery.error && (
<div className="mt-2 text-xs text-red-700">
{previewQuery.error.message}
</div>
<div className="mt-2 text-xs text-red-700">{previewQuery.error.message}</div>
)}
</div>
)}