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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user