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:
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@planarchy/db";
|
||||
import { checkChargeabilityAlerts } from "@planarchy/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET /api/cron/chargeability-alerts
|
||||
*
|
||||
* Finds resources whose current-month chargeability is >15 percentage points
|
||||
* below their target and creates in-app notifications for managers.
|
||||
*
|
||||
* Duplicate-safe: only one alert per resource per month.
|
||||
*
|
||||
* Optionally protect with CRON_SECRET environment variable.
|
||||
* When set, requests must include `Authorization: Bearer <secret>`.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const cronSecret = process.env["CRON_SECRET"];
|
||||
if (cronSecret) {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (auth !== `Bearer ${cronSecret}`) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const alertsSent = await checkChargeabilityAlerts(prisma as any);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
alertsSent,
|
||||
checkedAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[cron/chargeability-alerts] Error:", error);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Internal error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@planarchy/db";
|
||||
import { autoImportPublicHolidays } from "@planarchy/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET /api/cron/public-holidays?year=2027
|
||||
*
|
||||
* Auto-imports public holidays for all active resources for a given year.
|
||||
* Each resource's federal state determines which state-specific holidays apply.
|
||||
* Duplicate-safe: existing holidays are skipped.
|
||||
*
|
||||
* Query params:
|
||||
* - year (optional): defaults to next year
|
||||
*
|
||||
* Optionally protected with CRON_SECRET environment variable.
|
||||
* When set, requests must include `Authorization: Bearer <secret>`.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const cronSecret = process.env["CRON_SECRET"];
|
||||
if (cronSecret) {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (auth !== `Bearer ${cronSecret}`) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const yearParam = searchParams.get("year");
|
||||
const year = yearParam ? parseInt(yearParam, 10) : new Date().getFullYear() + 1;
|
||||
|
||||
if (isNaN(year) || year < 2000 || year > 2100) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid year parameter. Must be between 2000 and 2100." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await autoImportPublicHolidays(prisma, year);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
year: result.year,
|
||||
holidaysCreated: result.holidaysCreated,
|
||||
resourcesProcessed: result.resourcesProcessed,
|
||||
skippedExisting: result.skippedExisting,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[cron/public-holidays] Error:", error);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Internal error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user