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>
This commit is contained in:
2026-03-19 21:39:05 +01:00
parent 6e5b9ec85b
commit 6f34659587
16 changed files with 1906 additions and 4 deletions
+21 -2
View File
@@ -8,6 +8,7 @@ import { emitVacationCreated, emitVacationUpdated, emitNotificationCreated, emit
import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure } from "../trpc.js";
import { sendEmail } from "../lib/email.js";
import { anonymizeResource, anonymizeUser, getAnonymizationDirectory } from "../lib/anonymization.js";
import { checkVacationConflicts, checkBatchVacationConflicts } from "../lib/vacation-conflicts.js";
/** Types that consume from annual leave balance */
const BALANCE_TYPES = [VacationType.ANNUAL, VacationType.OTHER];
@@ -277,6 +278,10 @@ export const vacationRouter = createTRPCRouter({
select: { id: true },
});
// Check for team conflicts before approving (non-blocking)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const conflictResult = await checkVacationConflicts(ctx.db as any, input.id, userRecord?.id);
const updated = await ctx.db.vacation.update({
where: { id: input.id },
data: {
@@ -307,7 +312,7 @@ export const vacationRouter = createTRPCRouter({
void notifyVacationStatus(ctx.db, updated.id, updated.resourceId, VacationStatus.APPROVED);
}
return updated;
return { ...updated, warnings: conflictResult.warnings };
}),
/**
@@ -373,6 +378,14 @@ export const vacationRouter = createTRPCRouter({
select: { id: true, resourceId: true },
});
// Check for team conflicts before approving (non-blocking)
const conflictMap = await checkBatchVacationConflicts(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.db as any,
vacations.map((v) => v.id),
userRecord?.id,
);
await ctx.db.vacation.updateMany({
where: { id: { in: vacations.map((v) => v.id) } },
data: {
@@ -402,7 +415,13 @@ export const vacationRouter = createTRPCRouter({
});
}
return { approved: vacations.length };
// Flatten all warnings into a single array
const warnings: string[] = [];
for (const [, w] of conflictMap) {
warnings.push(...w);
}
return { approved: vacations.length, warnings };
}),
/**