6f34659587
Auto-Staffing Suggestions (A6): - generateAutoSuggestions() ranks top-3 resources on demand creation - Uses existing staffing engine (skill 40%, availability 30%, cost 20%, util 10%) - Creates in-app notification with match scores for managers - Triggered after createDemandRequirement and partial fillDemandRequirement Vacation Conflict Detection (A7): - checkVacationConflicts() warns when >50% chapter absent on same days - Returns warnings array in approve/batchApprove responses (advisory, non-blocking) - Creates VACATION_CONFLICT_WARNING notification for approver Weekly Chargeability Alerts (A10): - checkChargeabilityAlerts() finds resources >15pp below target - Cron endpoint: GET /api/cron/chargeability-alerts - Duplicate-safe by resourceId + month composite key Rate Card Auto-Apply (A11): - lookupRate() finds best matching rate card line (weighted scoring) - Auto-fills demand line rates in estimate create/updateDraft when rates are 0 - Marks auto-filled lines with metadata.autoAppliedRateCard - New lookupDemandLineRate query for on-demand UI lookups Public Holiday Auto-Import (A12): - autoImportPublicHolidays() generates holidays by resource federal state - Cron endpoint: GET /api/cron/public-holidays?year=2027 - Duplicate-safe, uses existing getPublicHolidays() from shared Skill Marketplace MVP (G6): - New page: /analytics/skill-marketplace with 3 sections - Skill Search: filter by name, proficiency, availability, sortable results - Skill Gap Heat Map: supply vs demand per skill, shortage/surplus indicators - Skill Distribution: top-20 horizontal bar chart (reuses SkillDistributionChart) - New getSkillMarketplace query in resource router - Sidebar nav link under Analytics for ADMIN/MANAGER/CONTROLLER Co-Authored-By: claude-flow <ruv@ruv.net>
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
/**
|
|
* Auto-import public holidays for all active resources.
|
|
*
|
|
* For each resource, determines the applicable federal state from:
|
|
* 1. resource.federalState (explicit, e.g. "BY")
|
|
* 2. Falls back to federal-only holidays when no state is set
|
|
*
|
|
* Creates Vacation entries with type PUBLIC_HOLIDAY and status APPROVED.
|
|
* Duplicate-safe: skips holidays that already exist (by date + type + resourceId).
|
|
*/
|
|
|
|
import { getPublicHolidays } from "@planarchy/shared";
|
|
|
|
interface MinimalVacation {
|
|
resourceId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
}
|
|
|
|
interface AutoImportDb {
|
|
resource: {
|
|
findMany: (args: {
|
|
where: { isActive: boolean };
|
|
select: { id: string; federalState: string };
|
|
}) => Promise<Array<{ id: string; federalState: string | null }>>;
|
|
};
|
|
vacation: {
|
|
findMany: (args: unknown) => Promise<MinimalVacation[]>;
|
|
createMany: (args: { data: unknown[]; skipDuplicates?: boolean }) => Promise<{ count: number }>;
|
|
};
|
|
}
|
|
|
|
export interface AutoImportResult {
|
|
year: number;
|
|
holidaysCreated: number;
|
|
resourcesProcessed: number;
|
|
skippedExisting: number;
|
|
}
|
|
|
|
/**
|
|
* Import public holidays for all active resources in a given year.
|
|
* Returns the number of holiday vacation records created.
|
|
*/
|
|
export async function autoImportPublicHolidays(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
db: any,
|
|
year: number,
|
|
): Promise<AutoImportResult> {
|
|
const resources: Array<{ id: string; federalState: string | null }> = await db.resource.findMany({
|
|
where: { isActive: true },
|
|
select: { id: true, federalState: true },
|
|
});
|
|
|
|
if (resources.length === 0) {
|
|
return { year, holidaysCreated: 0, resourcesProcessed: 0, skippedExisting: 0 };
|
|
}
|
|
|
|
// Group resources by federal state (null = federal-only holidays)
|
|
const byState = new Map<string | null, string[]>();
|
|
for (const resource of resources) {
|
|
const state = resource.federalState ?? null;
|
|
const group = byState.get(state) ?? [];
|
|
group.push(resource.id);
|
|
byState.set(state, group);
|
|
}
|
|
|
|
let totalCreated = 0;
|
|
let totalSkipped = 0;
|
|
|
|
for (const [state, resourceIds] of byState) {
|
|
const holidays = getPublicHolidays(year, state ?? undefined);
|
|
if (holidays.length === 0) continue;
|
|
|
|
for (const holiday of holidays) {
|
|
const holidayDate = new Date(holiday.date);
|
|
|
|
// Find existing records for this date + type to skip duplicates
|
|
const existing: MinimalVacation[] = await db.vacation.findMany({
|
|
where: {
|
|
resourceId: { in: resourceIds },
|
|
type: "PUBLIC_HOLIDAY",
|
|
startDate: holidayDate,
|
|
endDate: holidayDate,
|
|
},
|
|
select: { resourceId: true, startDate: true, endDate: true },
|
|
});
|
|
|
|
const existingResourceIds = new Set(existing.map((v: MinimalVacation) => v.resourceId));
|
|
const newResourceIds = resourceIds.filter((id) => !existingResourceIds.has(id));
|
|
|
|
totalSkipped += existingResourceIds.size;
|
|
|
|
if (newResourceIds.length === 0) continue;
|
|
|
|
const records = newResourceIds.map((resourceId) => ({
|
|
resourceId,
|
|
type: "PUBLIC_HOLIDAY",
|
|
status: "APPROVED",
|
|
startDate: holidayDate,
|
|
endDate: holidayDate,
|
|
note: holiday.name,
|
|
isHalfDay: false,
|
|
approvedAt: new Date(),
|
|
}));
|
|
|
|
const result = await db.vacation.createMany({
|
|
data: records,
|
|
skipDuplicates: true,
|
|
});
|
|
|
|
totalCreated += result.count;
|
|
}
|
|
}
|
|
|
|
return {
|
|
year,
|
|
holidaysCreated: totalCreated,
|
|
resourcesProcessed: resources.length,
|
|
skippedExisting: totalSkipped,
|
|
};
|
|
}
|