New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -0,0 +1,366 @@
/**
* Alert Rendering Utilities - i18n Parameter Substitution
*
* Centralized rendering functions for alert system with proper i18n support.
* Uses new type system from /api/types/events.ts
*/
import { TFunction } from 'i18next';
import type {
EventResponse,
Alert,
Notification,
Recommendation,
SmartAction,
UrgencyContext,
I18nDisplayContext,
AIReasoningContext,
isAlert,
isNotification,
isRecommendation,
} from '../../api/types/events';
// ============================================================
// EVENT CONTENT RENDERING
// ============================================================
/**
* Render event title with parameter substitution
*/
export function renderEventTitle(
event: EventResponse,
t: TFunction
): string {
try {
const { title_key, title_params } = event.i18n;
return t(title_key, title_params || {});
} catch (error) {
console.error('Error rendering event title:', error);
return event.i18n.title_key || 'Untitled Event';
}
}
/**
* Render event message with parameter substitution
*/
export function renderEventMessage(
event: EventResponse,
t: TFunction
): string {
try {
const { message_key, message_params } = event.i18n;
return t(message_key, message_params || {});
} catch (error) {
console.error('Error rendering event message:', error);
return event.i18n.message_key || 'No message available';
}
}
// ============================================================
// SMART ACTION RENDERING
// ============================================================
/**
* Render action label with parameter substitution
*/
export function renderActionLabel(
action: SmartAction,
t: TFunction
): string {
try {
return t(action.label_key, action.label_params || {});
} catch (error) {
console.error('Error rendering action label:', error);
return action.label_key || 'Action';
}
}
/**
* Render action consequence with parameter substitution
*/
export function renderActionConsequence(
action: SmartAction,
t: TFunction
): string | null {
if (!action.consequence_key) return null;
try {
return t(action.consequence_key, action.consequence_params || {});
} catch (error) {
console.error('Error rendering action consequence:', error);
return null;
}
}
/**
* Render disabled reason with parameter substitution
*/
export function renderDisabledReason(
action: SmartAction,
t: TFunction
): string | null {
// Try i18n key first
if (action.disabled_reason_key) {
try {
return t(action.disabled_reason_key, action.disabled_reason_params || {});
} catch (error) {
console.error('Error rendering disabled reason:', error);
}
}
// Fallback to plain text
return action.disabled_reason || null;
}
// ============================================================
// AI REASONING RENDERING
// ============================================================
/**
* Render AI reasoning summary with parameter substitution
*/
export function renderAIReasoning(
event: Alert,
t: TFunction
): string | null {
if (!event.ai_reasoning?.summary_key) return null;
try {
return t(
event.ai_reasoning.summary_key,
event.ai_reasoning.summary_params || {}
);
} catch (error) {
console.error('Error rendering AI reasoning:', error);
return null;
}
}
// ============================================================
// URGENCY CONTEXT RENDERING
// ============================================================
/**
* Render urgency reason with parameter substitution
*/
export function renderUrgencyReason(
urgency: UrgencyContext,
t: TFunction
): string | null {
if (!urgency.urgency_reason_key) return null;
try {
return t(urgency.urgency_reason_key, urgency.urgency_reason_params || {});
} catch (error) {
console.error('Error rendering urgency reason:', error);
return null;
}
}
// ============================================================
// SAFE RENDERING WITH FALLBACKS
// ============================================================
/**
* Safely render any i18n context with fallback
*/
export function safeRenderI18n(
key: string | undefined,
params: Record<string, any> | undefined,
t: TFunction,
fallback: string = ''
): string {
if (!key) return fallback;
try {
return t(key, params || {});
} catch (error) {
console.error(`Error rendering i18n key ${key}:`, error);
return fallback || key;
}
}
// ============================================================
// EVENT TYPE HELPERS
// ============================================================
/**
* Get event type display name
*/
export function getEventTypeLabel(event: EventResponse, t: TFunction): string {
if (isAlert(event)) {
return t('common.event_types.alert', 'Alert');
} else if (isNotification(event)) {
return t('common.event_types.notification', 'Notification');
} else if (isRecommendation(event)) {
return t('common.event_types.recommendation', 'Recommendation');
}
return t('common.event_types.unknown', 'Event');
}
/**
* Get priority level display name
*/
export function getPriorityLevelLabel(
level: string,
t: TFunction
): string {
const key = `common.priority_levels.${level}`;
return t(key, level.charAt(0).toUpperCase() + level.slice(1));
}
/**
* Get status display name
*/
export function getStatusLabel(status: string, t: TFunction): string {
const key = `common.statuses.${status}`;
return t(key, status.charAt(0).toUpperCase() + level.slice(1));
}
// ============================================================
// FORMATTING HELPERS
// ============================================================
/**
* Format countdown time (for escalation alerts)
*/
export function formatCountdown(seconds: number | undefined): string {
if (!seconds) return '';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
/**
* Format time until consequence (hours)
*/
export function formatTimeUntilConsequence(hours: number | undefined, t: TFunction): string {
if (!hours) return '';
if (hours < 1) {
const minutes = Math.round(hours * 60);
return t('common.time.minutes', { count: minutes }, `${minutes} minutes`);
} else if (hours < 24) {
const roundedHours = Math.round(hours);
return t('common.time.hours', { count: roundedHours }, `${roundedHours} hours`);
} else {
const days = Math.round(hours / 24);
return t('common.time.days', { count: days }, `${days} days`);
}
}
/**
* Format deadline as relative time
*/
export function formatDeadline(deadline: string | undefined, t: TFunction): string {
if (!deadline) return '';
try {
const deadlineDate = new Date(deadline);
const now = new Date();
const diffMs = deadlineDate.getTime() - now.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
if (diffHours < 0) {
return t('common.time.overdue', 'Overdue');
}
return formatTimeUntilConsequence(diffHours, t);
} catch (error) {
console.error('Error formatting deadline:', error);
return '';
}
}
// ============================================================
// CURRENCY FORMATTING
// ============================================================
/**
* Format currency value from params
*/
export function formatCurrency(value: number | undefined, currency: string = 'EUR'): string {
if (value === undefined || value === null) return '';
try {
return new Intl.NumberFormat('en-EU', {
style: 'currency',
currency,
}).format(value);
} catch (error) {
return `${value} ${currency}`;
}
}
// ============================================================
// COMPLETE EVENT RENDERING
// ============================================================
/**
* Render complete event with all i18n content
*/
export interface RenderedEvent {
title: string;
message: string;
actions: Array<{
label: string;
consequence: string | null;
disabledReason: string | null;
original: SmartAction;
}>;
aiReasoning: string | null;
urgencyReason: string | null;
}
/**
* Render all event content at once
*/
export function renderCompleteEvent(
event: EventResponse,
t: TFunction
): RenderedEvent {
const rendered: RenderedEvent = {
title: renderEventTitle(event, t),
message: renderEventMessage(event, t),
actions: [],
aiReasoning: null,
urgencyReason: null,
};
// Render actions (only for alerts)
if (isAlert(event)) {
rendered.actions = event.smart_actions.map((action) => ({
label: renderActionLabel(action, t),
consequence: renderActionConsequence(action, t),
disabledReason: renderDisabledReason(action, t),
original: action,
}));
rendered.aiReasoning = renderAIReasoning(event, t);
if (event.urgency) {
rendered.urgencyReason = renderUrgencyReason(event.urgency, t);
}
}
// Render suggested actions (only for recommendations)
if (isRecommendation(event) && event.suggested_actions) {
rendered.actions = event.suggested_actions.map((action) => ({
label: renderActionLabel(action, t),
consequence: renderActionConsequence(action, t),
disabledReason: renderDisabledReason(action, t),
original: action,
}));
}
return rendered;
}