Files
CapaKraken/packages/api/src/lib/holiday-auto-import.ts
T
Hartmut 6f34659587 feat: Sprint 3 — automation, intelligence, skill marketplace
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>
2026-03-19 21:39:05 +01:00

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,
};
}