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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user