223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
import { getAuthRoleLabel } from '@/business/auth/selectors';
|
|
import { APP_ROUTE_PATHS } from '@/shared/constants/routes';
|
|
import { DEFAULT_CAMPUS_LABEL } from '@/shared/constants/campusDisplay';
|
|
import type {
|
|
CampusInfo,
|
|
UserRole,
|
|
} from '@/shared/types/app';
|
|
import type { TopBarNotification } from '@/business/top-bar/types';
|
|
import type { CommunicationEventDto } from '@/shared/types/communications';
|
|
import type { PolicyViewModel } from '@/business/policies/types';
|
|
import type { SafetyProtocolViewModel } from '@/business/safety-protocols/types';
|
|
import type { PolicyAcknowledgmentDto } from '@/shared/types/policyDocuments';
|
|
import { isPolicyDocumentAcknowledged } from '@/business/policies/selectors';
|
|
import { ESA_FUNDING_POLICY_TAG } from '@/shared/constants/esaFunding';
|
|
|
|
export function getTopBarInitials(name: string): string {
|
|
return name
|
|
.trim()
|
|
.split(/\s+/u)
|
|
.filter(Boolean)
|
|
.map((part) => part[0])
|
|
.join('')
|
|
.slice(0, 2)
|
|
.toUpperCase();
|
|
}
|
|
|
|
export function getTopBarCampusLabel(campusInfo?: CampusInfo): string {
|
|
return campusInfo?.fullName ?? DEFAULT_CAMPUS_LABEL;
|
|
}
|
|
|
|
export function getTopBarRoleLabel(role: UserRole): string {
|
|
return getAuthRoleLabel(role);
|
|
}
|
|
|
|
export function countUnreadTopBarNotifications(
|
|
notifications: readonly TopBarNotification[],
|
|
): number {
|
|
return notifications.filter((notification) => notification.unread).length;
|
|
}
|
|
|
|
const ZONE_CHECKIN_NOTIFICATION_ID = 'zone-checkin-today';
|
|
const SAFETY_QUIZ_NOTIFICATION_ID = 'safety-quiz-weekly';
|
|
const SIGN_OF_WEEK_NOTIFICATION_ID = 'sign-of-week';
|
|
const WEEKLY_SIGN_SELECTION_NOTIFICATION_ID = 'weekly-sign-selection';
|
|
const WEEKLY_FRAME_NOTIFICATION_ID = 'weekly-frame-content';
|
|
const DAILY_ATTENDANCE_NOTIFICATION_ID = 'daily-attendance-content';
|
|
const EI_SELF_ASSESSMENT_NOTIFICATION_ID = 'ei-self-assessment';
|
|
const EI_PERSONALITY_QUIZ_NOTIFICATION_ID = 'ei-personality-quiz';
|
|
|
|
/**
|
|
* Builds the top-bar notification list from derived app state. Personal
|
|
* completion reminders are driven by backend-backed status queries; there is
|
|
* no persisted notifications store yet.
|
|
*/
|
|
export function buildTopBarNotifications(input: {
|
|
readonly needsZoneCheckIn: boolean;
|
|
readonly needsSafetyQuiz?: boolean;
|
|
readonly needsSignOfWeek?: boolean;
|
|
readonly needsWeeklySignSelection?: boolean;
|
|
readonly needsWeeklyFrameContent?: boolean;
|
|
readonly needsDailyAttendanceContent?: boolean;
|
|
readonly needsEiSelfAssessment?: boolean;
|
|
readonly needsPersonalityQuiz?: boolean;
|
|
readonly communicationEvents?: readonly CommunicationEventDto[];
|
|
readonly acknowledgedCommunicationEventIds?: ReadonlySet<string>;
|
|
readonly handbookPolicies?: readonly PolicyViewModel[];
|
|
readonly safetyProtocols?: readonly SafetyProtocolViewModel[];
|
|
readonly policyAcknowledgments?: readonly PolicyAcknowledgmentDto[];
|
|
}): readonly TopBarNotification[] {
|
|
const notifications: TopBarNotification[] = [];
|
|
|
|
if (input.needsZoneCheckIn) {
|
|
notifications.push({
|
|
id: ZONE_CHECKIN_NOTIFICATION_ID,
|
|
text: "You haven't logged your Emotional Zone today",
|
|
time: 'Today',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.zones,
|
|
});
|
|
}
|
|
|
|
if (input.needsSafetyQuiz) {
|
|
notifications.push({
|
|
id: SAFETY_QUIZ_NOTIFICATION_ID,
|
|
text: "You haven't completed this week's QBS safety quiz",
|
|
time: 'This week',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.qbs,
|
|
});
|
|
}
|
|
|
|
if (input.needsSignOfWeek) {
|
|
notifications.push({
|
|
id: SIGN_OF_WEEK_NOTIFICATION_ID,
|
|
text: "You haven't learned this week's sign",
|
|
time: 'This week',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.signs,
|
|
});
|
|
}
|
|
|
|
if (input.needsWeeklySignSelection) {
|
|
notifications.push({
|
|
id: WEEKLY_SIGN_SELECTION_NOTIFICATION_ID,
|
|
text: "Select this week's Sign of the Week",
|
|
time: 'This week',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.signs,
|
|
});
|
|
}
|
|
|
|
if (input.needsWeeklyFrameContent) {
|
|
notifications.push({
|
|
id: WEEKLY_FRAME_NOTIFICATION_ID,
|
|
text: "Publish this week's F.R.A.M.E. entry",
|
|
time: 'This week',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.frame,
|
|
});
|
|
}
|
|
|
|
if (input.needsDailyAttendanceContent) {
|
|
notifications.push({
|
|
id: DAILY_ATTENDANCE_NOTIFICATION_ID,
|
|
text: "Submit today's attendance",
|
|
time: 'Today',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.attendance,
|
|
});
|
|
}
|
|
|
|
if (input.needsEiSelfAssessment) {
|
|
notifications.push({
|
|
id: EI_SELF_ASSESSMENT_NOTIFICATION_ID,
|
|
text: "You haven't completed your EI self-assessment",
|
|
time: 'This week',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.ei,
|
|
});
|
|
}
|
|
|
|
if (input.needsPersonalityQuiz) {
|
|
notifications.push({
|
|
id: EI_PERSONALITY_QUIZ_NOTIFICATION_ID,
|
|
text: "You haven't completed your personality type quiz",
|
|
time: 'Once',
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.ei,
|
|
});
|
|
}
|
|
|
|
for (const event of input.communicationEvents ?? []) {
|
|
if (input.acknowledgedCommunicationEventIds?.has(event.id)) {
|
|
continue;
|
|
}
|
|
|
|
notifications.push({
|
|
id: `internal-alert-${event.id}`,
|
|
text: `Internal alert: ${event.title}`,
|
|
time: new Date(event.date).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}),
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.internalComm,
|
|
});
|
|
}
|
|
|
|
for (const policy of input.handbookPolicies ?? []) {
|
|
if (isPolicyDocumentAcknowledged(input.policyAcknowledgments ?? [], policy.id, policy.version)) {
|
|
continue;
|
|
}
|
|
|
|
const esaUpdatedSection = policy.tag === ESA_FUNDING_POLICY_TAG
|
|
? getEsaUpdatedSection(policy)
|
|
: null;
|
|
|
|
notifications.push({
|
|
id: `handbook-policy-${policy.id}-v${policy.version}`,
|
|
text: policy.tag === ESA_FUNDING_POLICY_TAG
|
|
? `Updated ESA funding section: ${esaUpdatedSection ?? policy.title}`
|
|
: `Unread handbook policy: ${policy.title}`,
|
|
time: `Version ${policy.version}`,
|
|
unread: true,
|
|
href: policy.tag === ESA_FUNDING_POLICY_TAG
|
|
? APP_ROUTE_PATHS.esa
|
|
: APP_ROUTE_PATHS.handbook,
|
|
});
|
|
}
|
|
|
|
for (const protocol of input.safetyProtocols ?? []) {
|
|
if (isPolicyDocumentAcknowledged(input.policyAcknowledgments ?? [], protocol.id, protocol.version)) {
|
|
continue;
|
|
}
|
|
|
|
notifications.push({
|
|
id: `safety-protocol-${protocol.id}-v${protocol.version}`,
|
|
text: `Unread safety protocol: ${protocol.title}`,
|
|
time: `Version ${protocol.version}`,
|
|
unread: true,
|
|
href: APP_ROUTE_PATHS.safety,
|
|
});
|
|
}
|
|
|
|
return notifications;
|
|
}
|
|
|
|
function getEsaUpdatedSection(policy: PolicyViewModel): string | null {
|
|
const marker = 'Updated section:';
|
|
const line = policy.content
|
|
.split('\n')
|
|
.map((item) => item.trim())
|
|
.find((item) => item.startsWith(marker));
|
|
|
|
if (!line) {
|
|
return null;
|
|
}
|
|
|
|
const sectionName = line.slice(marker.length).trim();
|
|
return sectionName.length > 0 ? sectionName : null;
|
|
}
|