222 lines
6.5 KiB
TypeScript
222 lines
6.5 KiB
TypeScript
import {
|
|
FormDefinitionSchema,
|
|
FormSubmissionPayloadSchema,
|
|
type FormDefinition,
|
|
type FormSubmissionPayload,
|
|
} from "@hivegrid/form-widget";
|
|
|
|
export type HrM05Process = "leave" | "sick" | "request";
|
|
export type HrM05Values = FormSubmissionPayload["values"];
|
|
export type HrM05Translate = (key: string) => string;
|
|
export type HrM05MemberOption = {
|
|
userId: string;
|
|
displayName: string;
|
|
team: string | null;
|
|
roleNames: readonly string[];
|
|
};
|
|
|
|
const identity: HrM05Translate = (key) => key;
|
|
|
|
/**
|
|
* `FormFieldSchema.options` accepts at most 50 entries. A tenant with more
|
|
* active members used to overflow that cap, which made `FormDefinitionSchema`
|
|
* throw inside the loader and turned /hr/leave, /hr/sick and /hr/requests into
|
|
* 500s. Capping keeps the routes alive; the picker itself stays server-validated
|
|
* (`requireActiveHrMember`), so a member beyond the cap is still a legal target.
|
|
*/
|
|
const MAX_PICKER_OPTIONS = 50;
|
|
|
|
function cappedMembers(
|
|
members: readonly HrM05MemberOption[],
|
|
reservedOptions = 0,
|
|
): readonly HrM05MemberOption[] {
|
|
return members.slice(0, Math.max(0, MAX_PICKER_OPTIONS - reservedOptions));
|
|
}
|
|
|
|
export function parseHrM05Values(raw: string): HrM05Values | null {
|
|
try {
|
|
const parsed = FormSubmissionPayloadSchema.safeParse({
|
|
values: JSON.parse(raw) as unknown,
|
|
});
|
|
return parsed.success ? parsed.data.values : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function buildHrLeaveFormDefinition(
|
|
t: HrM05Translate = identity,
|
|
members: readonly HrM05MemberOption[] = [],
|
|
): FormDefinition {
|
|
return FormDefinitionSchema.parse({
|
|
title: t("hr.leave.action.new"),
|
|
description: t("hr.m05.leaveDescription"),
|
|
submitLabel: t("hr.leave.action.submit"),
|
|
successMessage: t("hr.m05.success"),
|
|
gdprConsentLabel: t("hr.m05.internalConsent"),
|
|
captchaEnabled: false,
|
|
fields: [
|
|
{
|
|
key: "requestKind",
|
|
type: "select",
|
|
label: t("hr.leave.field.kind"),
|
|
required: true,
|
|
options: [
|
|
"vacation",
|
|
"special_leave",
|
|
"unpaid_leave",
|
|
"overtime_compensation",
|
|
].map((value) => ({ value, label: t(`hr.leave.kind.${value}`) })),
|
|
},
|
|
{
|
|
key: "startDate",
|
|
type: "date",
|
|
label: t("hr.leave.field.start"),
|
|
required: true,
|
|
},
|
|
{
|
|
key: "endDate",
|
|
type: "date",
|
|
label: t("hr.leave.field.end"),
|
|
required: true,
|
|
},
|
|
{
|
|
key: "note",
|
|
type: "textarea",
|
|
label: t("hr.leave.field.note"),
|
|
required: false,
|
|
validationRules: { maxLength: 2000 },
|
|
},
|
|
...(members.length > 0
|
|
? [
|
|
{
|
|
key: "approverUserId",
|
|
type: "select" as const,
|
|
label: t("hr.leave.field.approver"),
|
|
required: false,
|
|
options: [
|
|
{ value: "__none__", label: t("hr.picker.noApprover") },
|
|
...cappedMembers(members, 1).map((member) => ({
|
|
value: member.userId,
|
|
label: `${member.displayName} · ${member.team ?? t("hr.picker.noTeam")} · ${
|
|
member.roleNames.join(", ") || t("hr.picker.noRole")
|
|
}`,
|
|
})),
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|
|
|
|
export function buildHrSickFormDefinition(
|
|
t: HrM05Translate = identity,
|
|
employeeOptions: readonly HrM05MemberOption[] = [],
|
|
): FormDefinition {
|
|
return FormDefinitionSchema.parse({
|
|
title: t("hr.sick.action.report"),
|
|
description: t("hr.m05.sickDescription"),
|
|
submitLabel: t("hr.sick.action.report"),
|
|
successMessage: t("hr.m05.success"),
|
|
gdprConsentLabel: t("hr.m05.internalConsent"),
|
|
captchaEnabled: false,
|
|
fields: [
|
|
{
|
|
key: "startDate",
|
|
type: "date",
|
|
label: t("hr.sick.field.start"),
|
|
required: true,
|
|
},
|
|
{
|
|
key: "expectedReturnDate",
|
|
type: "date",
|
|
label: t("hr.sick.field.expectedReturn"),
|
|
required: false,
|
|
},
|
|
{
|
|
key: "note",
|
|
type: "textarea",
|
|
label: t("hr.leave.field.note"),
|
|
required: false,
|
|
validationRules: { maxLength: 2000 },
|
|
},
|
|
...(employeeOptions.length > 0
|
|
? [
|
|
{
|
|
key: "employeeUserId",
|
|
type: "select" as const,
|
|
label: t("hr.sick.field.employee"),
|
|
required: true,
|
|
options: cappedMembers(employeeOptions).map((member) => ({
|
|
value: member.userId,
|
|
label: `${member.displayName} · ${member.team ?? t("hr.picker.noTeam")} · ${
|
|
member.roleNames.join(", ") || t("hr.picker.noRole")
|
|
}`,
|
|
})),
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|
|
|
|
export function buildHrRequestFormDefinition(
|
|
t: HrM05Translate = identity,
|
|
members: readonly HrM05MemberOption[] = [],
|
|
): FormDefinition {
|
|
return FormDefinitionSchema.parse({
|
|
title: t("hr.requests.action.new"),
|
|
description: t("hr.m05.requestDescription"),
|
|
submitLabel: t("hr.requests.action.submit"),
|
|
successMessage: t("hr.m05.success"),
|
|
gdprConsentLabel: t("hr.m05.internalConsent"),
|
|
captchaEnabled: false,
|
|
fields: [
|
|
{
|
|
key: "requestKind",
|
|
type: "select",
|
|
label: t("hr.requests.field.kind"),
|
|
required: true,
|
|
options: ["expense", "hardware", "training", "reference"].map((value) => ({
|
|
value,
|
|
label: t(`hr.requests.kind.${value}`),
|
|
})),
|
|
},
|
|
{
|
|
key: "title",
|
|
type: "text",
|
|
label: t("hr.requests.field.title"),
|
|
required: true,
|
|
validationRules: { maxLength: 255 },
|
|
},
|
|
{
|
|
key: "details",
|
|
type: "textarea",
|
|
label: t("hr.requests.field.details"),
|
|
required: false,
|
|
validationRules: { maxLength: 8000 },
|
|
},
|
|
...(members.length > 0
|
|
? [
|
|
{
|
|
key: "approverUserId",
|
|
type: "select" as const,
|
|
label: t("hr.requests.field.approver"),
|
|
required: false,
|
|
options: [
|
|
{ value: "__none__", label: t("hr.picker.noApprover") },
|
|
...cappedMembers(members, 1).map((member) => ({
|
|
value: member.userId,
|
|
label: `${member.displayName} · ${member.team ?? t("hr.picker.noTeam")} · ${
|
|
member.roleNames.join(", ") || t("hr.picker.noRole")
|
|
}`,
|
|
})),
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|