refactor: deduplicate modals, notifications, confirms, comboboxes, proficiency

Modal Overlay (Finding 1 — 6 admin files):
- Migrated CountriesClient, ManagementLevelsClient, OrgUnitsClient,
  CalculationRulesClient, UtilizationCategoriesClient, RoleModal
  from inline fixed-overlay to AnimatedModal component
- Gains: animated transitions, backdrop blur, escape key for free

Notification Helper (Finding 9 — 9 API files, 14 call sites):
- New createNotification() + createNotificationsForUsers() in
  packages/api/src/lib/create-notification.ts
- Handles exactOptionalPropertyTypes spread + SSE emit internally
- Simplified: budget-alerts, estimate-reminders, auto-staffing,
  vacation-conflicts, chargeability-alerts, comment, vacation, notification

ConfirmDialog (Finding 3 — 11 files):
- Replaced all window.confirm() calls with ConfirmDialog component
- Files: CommentThread, EffortRules, ExperienceMultipliers,
  ManagementLevels, CalculationRules, Countries, RateCards,
  ApplyEffortRules, ApplyExperienceMultipliers, NotificationCenter,
  ReminderModal

EntityCombobox (Finding 4 — 3 files):
- New generic EntityCombobox<T> with customization hooks
- ResourceCombobox + ProjectCombobox rewritten as thin wrappers
- All consumers unchanged (backwards-compatible props)

Proficiency Constants (Finding 2 — 2 files):
- SkillsAnalytics + SkillMarketplace now import from skills/shared.tsx
- Deleted ~70 LOC of local duplicate definitions

Regression: 283 engine + 37 staffing tests pass. TypeScript clean.
AI Assistant: all 87 tools verified accessible.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-22 21:50:39 +01:00
parent c7b76e086d
commit ac845d72b7
29 changed files with 737 additions and 607 deletions
+102
View File
@@ -0,0 +1,102 @@
import { emitNotificationCreated } from "../sse/event-bus.js";
export interface CreateNotificationParams {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
db: { notification: { create: (args: any) => Promise<{ id: string; userId: string }> } };
userId: string;
type: string;
title: string;
body?: string | undefined;
link?: string | undefined;
entityId?: string | undefined;
entityType?: string | undefined;
category?: string | undefined;
priority?: string | undefined;
senderId?: string | undefined;
channel?: string | undefined;
taskStatus?: string | undefined;
taskAction?: string | undefined;
assigneeId?: string | undefined;
dueDate?: Date | undefined;
sourceId?: string | undefined;
/** Set to false to suppress the SSE emitNotificationCreated call. Default: true. */
emit?: boolean | undefined;
}
/**
* Create a single in-app notification and optionally emit an SSE event.
*
* Handles the `exactOptionalPropertyTypes` spread pattern internally so
* callers do not need to repeat the `...(val !== undefined ? { key: val } : {})` boilerplate.
*
* Returns the created notification's ID.
*/
export async function createNotification(
params: CreateNotificationParams,
): Promise<string> {
const {
db,
userId,
type,
title,
body,
link,
entityId,
entityType,
category,
priority,
senderId,
channel,
taskStatus,
taskAction,
assigneeId,
dueDate,
sourceId,
emit = true,
} = params;
const notification = await db.notification.create({
data: {
userId,
type,
title,
...(body !== undefined ? { body } : {}),
...(link !== undefined ? { link } : {}),
...(entityId !== undefined ? { entityId } : {}),
...(entityType !== undefined ? { entityType } : {}),
...(category !== undefined ? { category } : {}),
...(priority !== undefined ? { priority } : {}),
...(senderId !== undefined ? { senderId } : {}),
...(channel !== undefined ? { channel } : {}),
...(taskStatus !== undefined ? { taskStatus } : {}),
...(taskAction !== undefined ? { taskAction } : {}),
...(assigneeId !== undefined ? { assigneeId } : {}),
...(dueDate !== undefined ? { dueDate } : {}),
...(sourceId !== undefined ? { sourceId } : {}),
},
});
if (emit) {
emitNotificationCreated(userId, notification.id);
}
return notification.id;
}
/**
* Create one notification per user ID.
*
* Useful for fan-out scenarios (e.g. notifying all managers).
* Returns the count of notifications created.
*/
export async function createNotificationsForUsers(
params: Omit<CreateNotificationParams, "userId"> & { userIds: string[] },
): Promise<number> {
const { userIds, ...rest } = params;
let count = 0;
for (const userId of userIds) {
await createNotification({ ...rest, userId });
count++;
}
return count;
}