New alert service
This commit is contained in:
366
frontend/src/utils/i18n/alertRendering.ts
Normal file
366
frontend/src/utils/i18n/alertRendering.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user