Final MVP valid

This commit is contained in:
Marcelo
2026-01-21 01:45:57 +00:00
parent c183dda383
commit 511d80b629
29 changed files with 4827 additions and 381 deletions

View File

@@ -0,0 +1,24 @@
export const DOWNTIME_RANGES = ["24h", "7d", "30d", "mtd"] as const;
export type DowntimeRange = (typeof DOWNTIME_RANGES)[number];
export function coerceDowntimeRange(v?: string | null): DowntimeRange {
const s = (v ?? "").toLowerCase();
return (DOWNTIME_RANGES as readonly string[]).includes(s) ? (s as DowntimeRange) : "7d";
}
// server-friendly helper
export function rangeToStart(range: DowntimeRange) {
const now = new Date();
if (range === "24h") return new Date(Date.now() - 24 * 60 * 60 * 1000);
if (range === "30d") return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
if (range === "mtd") return new Date(now.getFullYear(), now.getMonth(), 1);
return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
}
// UI label helper (replaces ternaries everywhere)
export const DOWNTIME_RANGE_LABEL: Record<DowntimeRange, string> = {
"24h": "Last 24h",
"7d": "Last 7d",
"30d": "Last 30d",
"mtd": "MTD",
};

View File

@@ -144,3 +144,71 @@ export function buildInviteEmail(params: {
return { subject, text, html };
}
export function buildDowntimeActionAssignedEmail(params: {
appName: string;
orgName: string;
actionTitle: string;
assigneeName: string;
dueDate: string | null;
actionUrl: string;
priority: string;
status: string;
}) {
const dueLabel = params.dueDate ? `Due ${params.dueDate}` : "No due date";
const subject = `Action assigned: ${params.actionTitle}`;
const text =
`Hi ${params.assigneeName},\n\n` +
`You have been assigned an action in ${params.orgName} (${params.appName}).\n\n` +
`Title: ${params.actionTitle}\n` +
`Status: ${params.status}\n` +
`Priority: ${params.priority}\n` +
`${dueLabel}\n\n` +
`Open in Control Tower:\n${params.actionUrl}\n\n` +
`If you did not expect this assignment, please contact your admin.`;
const html =
`<p>Hi ${params.assigneeName},</p>` +
`<p>You have been assigned an action in ${params.orgName} (${params.appName}).</p>` +
`<p><strong>Title:</strong> ${params.actionTitle}<br />` +
`<strong>Status:</strong> ${params.status}<br />` +
`<strong>Priority:</strong> ${params.priority}<br />` +
`<strong>${dueLabel}</strong></p>` +
`<p><a href="${params.actionUrl}">Open in Control Tower</a></p>` +
`<p>If you did not expect this assignment, please contact your admin.</p>`;
return { subject, text, html };
}
export function buildDowntimeActionReminderEmail(params: {
appName: string;
orgName: string;
actionTitle: string;
assigneeName: string;
dueDate: string | null;
actionUrl: string;
priority: string;
status: string;
}) {
const dueLabel = params.dueDate ? `Due ${params.dueDate}` : "No due date";
const subject = `Reminder: ${params.actionTitle}`;
const text =
`Hi ${params.assigneeName},\n\n` +
`Reminder for your action in ${params.orgName} (${params.appName}).\n\n` +
`Title: ${params.actionTitle}\n` +
`Status: ${params.status}\n` +
`Priority: ${params.priority}\n` +
`${dueLabel}\n\n` +
`Open in Control Tower:\n${params.actionUrl}\n\n` +
`If you have already completed this action, you can mark it done in the app.`;
const html =
`<p>Hi ${params.assigneeName},</p>` +
`<p>Reminder for your action in ${params.orgName} (${params.appName}).</p>` +
`<p><strong>Title:</strong> ${params.actionTitle}<br />` +
`<strong>Status:</strong> ${params.status}<br />` +
`<strong>Priority:</strong> ${params.priority}<br />` +
`<strong>${dueLabel}</strong></p>` +
`<p><a href="${params.actionUrl}">Open in Control Tower</a></p>` +
`<p>If you have already completed this action, you can mark it done in the app.</p>`;
return { subject, text, html };
}

View File

@@ -9,12 +9,12 @@
"common.close": "Close",
"common.save": "Save",
"common.copy": "Copy",
"nav.overview": "Overview",
"nav.machines": "Machines",
"nav.reports": "Reports",
"nav.alerts": "Alerts",
"nav.financial": "Financial",
"nav.settings": "Settings",
"nav.overview": "Overview",
"nav.machines": "Machines",
"nav.reports": "Reports",
"nav.alerts": "Alerts",
"nav.financial": "Financial",
"nav.settings": "Settings",
"sidebar.productTitle": "MIS",
"sidebar.productSubtitle": "Control Tower",
"sidebar.userFallback": "User",
@@ -170,11 +170,11 @@
"machine.detail.tooltip.deviation": "Deviation",
"machine.detail.kpi.updated": "Updated {time}",
"machine.detail.currentWorkOrder": "Current Work Order",
"machine.detail.recentEvents": "Critical Events",
"machine.detail.recentEvents": "Critical Events",
"machine.detail.noEvents": "No events yet.",
"machine.detail.cycleTarget": "Cycle target",
"machine.detail.mini.events": "Detected Events",
"machine.detail.mini.events.subtitle": "Canonical events (all)",
"machine.detail.mini.events.subtitle": "Canonical events (all)",
"machine.detail.mini.deviation": "Actual vs Standard Cycle",
"machine.detail.mini.deviation.subtitle": "Average deviation",
"machine.detail.mini.impact": "Production Impact",
@@ -219,106 +219,106 @@
"reports.scrapTrend": "Scrap Trend",
"reports.topLossDrivers": "Top Loss Drivers",
"reports.qualitySummary": "Quality Summary",
"reports.notes": "Notes for Ops",
"alerts.title": "Alerts",
"alerts.subtitle": "Alert history with filters and drilldowns.",
"alerts.comingSoon": "Alert configuration UI is coming soon.",
"alerts.loading": "Loading alerts...",
"alerts.error.loadPolicy": "Failed to load alert policy.",
"alerts.error.savePolicy": "Failed to save alert policy.",
"alerts.error.loadContacts": "Failed to load alert contacts.",
"alerts.error.saveContacts": "Failed to save alert contact.",
"alerts.error.deleteContact": "Failed to delete alert contact.",
"alerts.error.createContact": "Failed to create alert contact.",
"alerts.policy.title": "Alert policy",
"alerts.policy.subtitle": "Configure escalation by role, channel, and duration.",
"alerts.policy.save": "Save policy",
"alerts.policy.saving": "Saving...",
"alerts.policy.defaults": "Default escalation (per role)",
"alerts.policy.enabled": "Enabled",
"alerts.policy.afterMinutes": "After minutes",
"alerts.policy.channels": "Channels",
"alerts.policy.repeatMinutes": "Repeat (min)",
"alerts.policy.readOnly": "You can view alert policy settings, but only owners can edit.",
"alerts.policy.defaultsHelp": "Defaults apply when a specific event is reset or not customized.",
"alerts.policy.eventSelectLabel": "Event type",
"alerts.policy.eventSelectHelper": "Adjust escalation rules for a single event type.",
"alerts.policy.applyDefaults": "Apply defaults",
"alerts.event.macrostop": "Macrostop",
"alerts.event.microstop": "Microstop",
"alerts.event.slow-cycle": "Slow cycle",
"alerts.event.offline": "Offline",
"alerts.event.error": "Error",
"alerts.contacts.title": "Alert contacts",
"alerts.contacts.subtitle": "External recipients and role targeting.",
"alerts.contacts.name": "Name",
"alerts.contacts.roleScope": "Role scope",
"alerts.contacts.email": "Email",
"alerts.contacts.phone": "Phone",
"alerts.contacts.eventTypes": "Event types (optional)",
"alerts.contacts.eventTypesPlaceholder": "macrostop, microstop, offline",
"alerts.contacts.eventTypesHelper": "Leave empty to receive all event types.",
"alerts.contacts.add": "Add contact",
"alerts.contacts.creating": "Adding...",
"alerts.contacts.empty": "No alert contacts yet.",
"alerts.contacts.save": "Save",
"alerts.contacts.saving": "Saving...",
"alerts.contacts.delete": "Delete",
"alerts.contacts.deleting": "Deleting...",
"alerts.contacts.active": "Active",
"alerts.contacts.linkedUser": "Linked user (edit in profile)",
"alerts.contacts.role.custom": "Custom",
"alerts.contacts.role.member": "Member",
"alerts.contacts.role.admin": "Admin",
"alerts.contacts.role.owner": "Owner",
"alerts.contacts.readOnly": "You can view contacts, but only owners can add or edit.",
"alerts.inbox.title": "Alerts Inbox",
"alerts.inbox.loading": "Loading alerts...",
"alerts.inbox.loadingFilters": "Loading filters...",
"alerts.inbox.empty": "No alerts found.",
"alerts.inbox.error": "Failed to load alerts.",
"alerts.inbox.range.24h": "Last 24 hours",
"alerts.inbox.range.7d": "Last 7 days",
"alerts.inbox.range.30d": "Last 30 days",
"alerts.inbox.range.custom": "Custom",
"alerts.inbox.filters.title": "Filters",
"alerts.inbox.filters.range": "Range",
"alerts.inbox.filters.start": "Start",
"alerts.inbox.filters.end": "End",
"alerts.inbox.filters.machine": "Machine",
"alerts.inbox.filters.site": "Site",
"alerts.inbox.filters.shift": "Shift",
"alerts.inbox.filters.type": "Classification",
"alerts.inbox.filters.severity": "Severity",
"alerts.inbox.filters.status": "Status",
"alerts.inbox.filters.search": "Search",
"alerts.inbox.filters.searchPlaceholder": "Title, description, machine...",
"alerts.inbox.filters.includeUpdates": "Include updates",
"alerts.inbox.filters.allMachines": "All machines",
"alerts.inbox.filters.allSites": "All sites",
"alerts.inbox.filters.allShifts": "All shifts",
"alerts.inbox.filters.allTypes": "All types",
"alerts.inbox.filters.allSeverities": "All severities",
"alerts.inbox.filters.allStatuses": "All statuses",
"alerts.inbox.table.time": "Time",
"alerts.inbox.table.machine": "Machine",
"alerts.inbox.table.site": "Site",
"alerts.inbox.table.shift": "Shift",
"alerts.inbox.table.type": "Type",
"alerts.inbox.table.severity": "Severity",
"alerts.inbox.table.status": "Status",
"alerts.inbox.table.duration": "Duration",
"alerts.inbox.table.title": "Title",
"alerts.inbox.table.unknown": "Unknown",
"alerts.inbox.status.active": "Active",
"alerts.inbox.status.resolved": "Resolved",
"alerts.inbox.status.unknown": "Unknown",
"alerts.inbox.duration.na": "n/a",
"alerts.inbox.duration.sec": "s",
"alerts.inbox.duration.min": " min",
"alerts.inbox.duration.hr": " h",
"alerts.inbox.meta.workOrder": "WO",
"alerts.inbox.meta.sku": "SKU",
"reports.notes": "Notes for Ops",
"alerts.title": "Alerts",
"alerts.subtitle": "Alert history with filters and drilldowns.",
"alerts.comingSoon": "Alert configuration UI is coming soon.",
"alerts.loading": "Loading alerts...",
"alerts.error.loadPolicy": "Failed to load alert policy.",
"alerts.error.savePolicy": "Failed to save alert policy.",
"alerts.error.loadContacts": "Failed to load alert contacts.",
"alerts.error.saveContacts": "Failed to save alert contact.",
"alerts.error.deleteContact": "Failed to delete alert contact.",
"alerts.error.createContact": "Failed to create alert contact.",
"alerts.policy.title": "Alert policy",
"alerts.policy.subtitle": "Configure escalation by role, channel, and duration.",
"alerts.policy.save": "Save policy",
"alerts.policy.saving": "Saving...",
"alerts.policy.defaults": "Default escalation (per role)",
"alerts.policy.enabled": "Enabled",
"alerts.policy.afterMinutes": "After minutes",
"alerts.policy.channels": "Channels",
"alerts.policy.repeatMinutes": "Repeat (min)",
"alerts.policy.readOnly": "You can view alert policy settings, but only owners can edit.",
"alerts.policy.defaultsHelp": "Defaults apply when a specific event is reset or not customized.",
"alerts.policy.eventSelectLabel": "Event type",
"alerts.policy.eventSelectHelper": "Adjust escalation rules for a single event type.",
"alerts.policy.applyDefaults": "Apply defaults",
"alerts.event.macrostop": "Macrostop",
"alerts.event.microstop": "Microstop",
"alerts.event.slow-cycle": "Slow cycle",
"alerts.event.offline": "Offline",
"alerts.event.error": "Error",
"alerts.contacts.title": "Alert contacts",
"alerts.contacts.subtitle": "External recipients and role targeting.",
"alerts.contacts.name": "Name",
"alerts.contacts.roleScope": "Role scope",
"alerts.contacts.email": "Email",
"alerts.contacts.phone": "Phone",
"alerts.contacts.eventTypes": "Event types (optional)",
"alerts.contacts.eventTypesPlaceholder": "macrostop, microstop, offline",
"alerts.contacts.eventTypesHelper": "Leave empty to receive all event types.",
"alerts.contacts.add": "Add contact",
"alerts.contacts.creating": "Adding...",
"alerts.contacts.empty": "No alert contacts yet.",
"alerts.contacts.save": "Save",
"alerts.contacts.saving": "Saving...",
"alerts.contacts.delete": "Delete",
"alerts.contacts.deleting": "Deleting...",
"alerts.contacts.active": "Active",
"alerts.contacts.linkedUser": "Linked user (edit in profile)",
"alerts.contacts.role.custom": "Custom",
"alerts.contacts.role.member": "Member",
"alerts.contacts.role.admin": "Admin",
"alerts.contacts.role.owner": "Owner",
"alerts.contacts.readOnly": "You can view contacts, but only owners can add or edit.",
"alerts.inbox.title": "Alerts Inbox",
"alerts.inbox.loading": "Loading alerts...",
"alerts.inbox.loadingFilters": "Loading filters...",
"alerts.inbox.empty": "No alerts found.",
"alerts.inbox.error": "Failed to load alerts.",
"alerts.inbox.range.24h": "Last 24 hours",
"alerts.inbox.range.7d": "Last 7 days",
"alerts.inbox.range.30d": "Last 30 days",
"alerts.inbox.range.custom": "Custom",
"alerts.inbox.filters.title": "Filters",
"alerts.inbox.filters.range": "Range",
"alerts.inbox.filters.start": "Start",
"alerts.inbox.filters.end": "End",
"alerts.inbox.filters.machine": "Machine",
"alerts.inbox.filters.site": "Site",
"alerts.inbox.filters.shift": "Shift",
"alerts.inbox.filters.type": "Classification",
"alerts.inbox.filters.severity": "Severity",
"alerts.inbox.filters.status": "Status",
"alerts.inbox.filters.search": "Search",
"alerts.inbox.filters.searchPlaceholder": "Title, description, machine...",
"alerts.inbox.filters.includeUpdates": "Include updates",
"alerts.inbox.filters.allMachines": "All machines",
"alerts.inbox.filters.allSites": "All sites",
"alerts.inbox.filters.allShifts": "All shifts",
"alerts.inbox.filters.allTypes": "All types",
"alerts.inbox.filters.allSeverities": "All severities",
"alerts.inbox.filters.allStatuses": "All statuses",
"alerts.inbox.table.time": "Time",
"alerts.inbox.table.machine": "Machine",
"alerts.inbox.table.site": "Site",
"alerts.inbox.table.shift": "Shift",
"alerts.inbox.table.type": "Type",
"alerts.inbox.table.severity": "Severity",
"alerts.inbox.table.status": "Status",
"alerts.inbox.table.duration": "Duration",
"alerts.inbox.table.title": "Title",
"alerts.inbox.table.unknown": "Unknown",
"alerts.inbox.status.active": "Active",
"alerts.inbox.status.resolved": "Resolved",
"alerts.inbox.status.unknown": "Unknown",
"alerts.inbox.duration.na": "n/a",
"alerts.inbox.duration.sec": "s",
"alerts.inbox.duration.min": " min",
"alerts.inbox.duration.hr": " h",
"alerts.inbox.meta.workOrder": "WO",
"alerts.inbox.meta.sku": "SKU",
"reports.notes.suggested": "Suggested actions",
"reports.notes.none": "No insights yet. Generate reports after data collection.",
"reports.noTrend": "No trend data yet.",
@@ -355,14 +355,14 @@
"reports.pdf.cycleDistribution": "Cycle Time Distribution",
"reports.pdf.notes": "Notes for Ops",
"reports.pdf.none": "None",
"settings.title": "Settings",
"settings.subtitle": "Live configuration for shifts, alerts, and defaults.",
"settings.tabs.general": "General",
"settings.tabs.shifts": "Shifts",
"settings.tabs.thresholds": "Thresholds",
"settings.tabs.alerts": "Alerts",
"settings.tabs.financial": "Financial",
"settings.tabs.team": "Team",
"settings.title": "Settings",
"settings.subtitle": "Live configuration for shifts, alerts, and defaults.",
"settings.tabs.general": "General",
"settings.tabs.shifts": "Shifts",
"settings.tabs.thresholds": "Thresholds",
"settings.tabs.alerts": "Alerts",
"settings.tabs.financial": "Financial",
"settings.tabs.team": "Team",
"settings.loading": "Loading settings...",
"settings.loadingTeam": "Loading team...",
"settings.refresh": "Refresh",
@@ -442,68 +442,75 @@
"settings.role.admin": "Admin",
"settings.role.member": "Member",
"settings.role.inactive": "Inactive",
"settings.integrations": "Integrations",
"settings.integrations.webhook": "Webhook URL",
"settings.integrations.erp": "ERP Sync",
"settings.integrations.erpNotConfigured": "Not configured",
"financial.title": "Financial Impact",
"financial.subtitle": "Translate downtime, slow cycles, and scrap into money.",
"financial.ownerOnly": "Financial impact is available only to owners.",
"financial.costsMoved": "Cost settings are now in",
"financial.costsMovedLink": "Settings -> Financial",
"financial.export.html": "HTML",
"financial.export.csv": "CSV",
"financial.totalLoss": "Total Loss",
"financial.currencyLabel": "Currency: {currency}",
"financial.noImpact": "No impact data yet.",
"financial.chart.title": "Lost Money Over Time",
"financial.chart.subtitle": "Stacked by event type",
"financial.range.day": "Day",
"financial.range.week": "Week",
"financial.range.month": "Month",
"financial.filters.title": "Filters",
"financial.filters.machine": "Machine",
"financial.filters.location": "Location",
"financial.filters.sku": "SKU",
"financial.filters.currency": "Currency",
"financial.filters.allMachines": "All machines",
"financial.filters.allLocations": "All locations",
"financial.filters.skuPlaceholder": "Filter by SKU",
"financial.filters.currencyPlaceholder": "MXN",
"financial.loadingMachines": "Loading machines...",
"financial.config.title": "Cost Parameters",
"financial.config.subtitle": "Defaults apply to all machines unless overridden.",
"financial.config.applyOrg": "Apply org defaults to all machines",
"financial.config.save": "Save",
"financial.config.saving": "Saving...",
"financial.config.saved": "Saved",
"financial.config.saveFailed": "Save failed",
"financial.config.orgDefaults": "Org Defaults",
"financial.config.locationOverrides": "Location Overrides",
"financial.config.machineOverrides": "Machine Overrides",
"financial.config.productOverrides": "Product Overrides",
"financial.config.addLocation": "Add location override",
"financial.config.addMachine": "Add machine override",
"financial.config.addProduct": "Add product override",
"financial.config.noneLocation": "No location overrides yet.",
"financial.config.noneMachine": "No machine overrides yet.",
"financial.config.noneProduct": "No product overrides yet.",
"financial.config.location": "Location",
"financial.config.selectLocation": "Select location",
"financial.config.machine": "Machine",
"financial.config.selectMachine": "Select machine",
"financial.config.currency": "Currency",
"financial.config.sku": "SKU",
"financial.config.rawMaterialUnit": "Raw material / unit",
"financial.config.ownerOnly": "Financial cost settings are available only to owners.",
"financial.config.loading": "Loading financials...",
"financial.field.machineCostPerMin": "Machine cost / min",
"financial.field.operatorCostPerMin": "Operator cost / min",
"financial.field.ratedRunningKw": "Running kW",
"financial.field.idleKw": "Idle kW",
"financial.field.kwhRate": "kWh rate",
"financial.field.energyMultiplier": "Energy multiplier",
"financial.field.energyCostPerMin": "Energy cost / min",
"financial.field.scrapCostPerUnit": "Scrap cost / unit",
"financial.field.rawMaterialCostPerUnit": "Raw material / unit"
}
"settings.integrations": "Integrations",
"settings.integrations.webhook": "Webhook URL",
"settings.integrations.erp": "ERP Sync",
"settings.integrations.erpNotConfigured": "Not configured",
"financial.title": "Financial Impact",
"financial.subtitle": "Translate downtime, slow cycles, and scrap into money.",
"financial.ownerOnly": "Financial impact is available only to owners.",
"financial.costsMoved": "Cost settings are now in",
"financial.costsMovedLink": "Settings -> Financial",
"financial.export.html": "HTML",
"financial.export.csv": "CSV",
"financial.totalLoss": "Total Loss",
"financial.currencyLabel": "Currency: {currency}",
"financial.noImpact": "No impact data yet.",
"financial.chart.title": "Lost Money Over Time",
"financial.chart.subtitle": "Stacked by event type",
"financial.range.day": "Day",
"financial.range.week": "Week",
"financial.range.month": "Month",
"financial.filters.title": "Filters",
"financial.filters.machine": "Machine",
"financial.filters.location": "Location",
"financial.filters.sku": "SKU",
"financial.filters.currency": "Currency",
"financial.filters.allMachines": "All machines",
"financial.filters.allLocations": "All locations",
"financial.filters.skuPlaceholder": "Filter by SKU",
"financial.filters.currencyPlaceholder": "MXN",
"financial.loadingMachines": "Loading machines...",
"financial.config.title": "Cost Parameters",
"financial.config.subtitle": "Defaults apply to all machines unless overridden.",
"financial.config.applyOrg": "Apply org defaults to all machines",
"financial.config.save": "Save",
"financial.config.saving": "Saving...",
"financial.config.saved": "Saved",
"financial.config.saveFailed": "Save failed",
"financial.config.orgDefaults": "Org Defaults",
"financial.config.locationOverrides": "Location Overrides",
"financial.config.machineOverrides": "Machine Overrides",
"financial.config.productOverrides": "Product Overrides",
"financial.config.addLocation": "Add location override",
"financial.config.addMachine": "Add machine override",
"financial.config.addProduct": "Add product override",
"financial.config.noneLocation": "No location overrides yet.",
"financial.config.noneMachine": "No machine overrides yet.",
"financial.config.noneProduct": "No product overrides yet.",
"financial.config.location": "Location",
"financial.config.selectLocation": "Select location",
"financial.config.machine": "Machine",
"financial.config.selectMachine": "Select machine",
"financial.config.currency": "Currency",
"financial.config.sku": "SKU",
"financial.config.rawMaterialUnit": "Raw material / unit",
"financial.config.ownerOnly": "Financial cost settings are available only to owners.",
"financial.config.loading": "Loading financials...",
"financial.field.machineCostPerMin": "Machine cost / min",
"financial.field.operatorCostPerMin": "Operator cost / min",
"financial.field.ratedRunningKw": "Running kW",
"financial.field.idleKw": "Idle kW",
"financial.field.kwhRate": "kWh rate",
"financial.field.energyMultiplier": "Energy multiplier",
"financial.field.energyCostPerMin": "Energy cost / min",
"financial.field.scrapCostPerUnit": "Scrap cost / unit",
"financial.field.rawMaterialCostPerUnit": "Raw material / unit",
"nav.downtime": "Downtime",
"settings.tabs.modules": "Modules",
"settings.modules.title": "Modules",
"settings.modules.subtitle": "Enable/disable UI modules depending on how the plant operates.",
"settings.modules.screenless.title": "Screenless mode",
"settings.modules.screenless.helper": "Hide the Downtime module from navigation (for plants without Node-RED reason capture).",
"settings.modules.note": "This setting is org-wide."
}

View File

@@ -9,12 +9,12 @@
"common.close": "Cerrar",
"common.save": "Guardar",
"common.copy": "Copiar",
"nav.overview": "Resumen",
"nav.machines": "Máquinas",
"nav.reports": "Reportes",
"nav.alerts": "Alertas",
"nav.financial": "Finanzas",
"nav.settings": "Configuración",
"nav.overview": "Resumen",
"nav.machines": "Máquinas",
"nav.reports": "Reportes",
"nav.alerts": "Alertas",
"nav.financial": "Finanzas",
"nav.settings": "Configuración",
"sidebar.productTitle": "MIS",
"sidebar.productSubtitle": "Control Tower",
"sidebar.userFallback": "Usuario",
@@ -170,11 +170,11 @@
"machine.detail.tooltip.deviation": "Desviación",
"machine.detail.kpi.updated": "Actualizado {time}",
"machine.detail.currentWorkOrder": "Orden de trabajo actual",
"machine.detail.recentEvents": "Eventos críticos",
"machine.detail.recentEvents": "Eventos críticos",
"machine.detail.noEvents": "Sin eventos aún.",
"machine.detail.cycleTarget": "Ciclo objetivo",
"machine.detail.mini.events": "Eventos detectados",
"machine.detail.mini.events.subtitle": "Eventos canónicos (todos)",
"machine.detail.mini.events.subtitle": "Eventos canónicos (todos)",
"machine.detail.mini.deviation": "Ciclo real vs estándar",
"machine.detail.mini.deviation.subtitle": "Desviación promedio",
"machine.detail.mini.impact": "Impacto en producción",
@@ -219,106 +219,106 @@
"reports.scrapTrend": "Tendencia de scrap",
"reports.topLossDrivers": "Principales causas de pérdida",
"reports.qualitySummary": "Resumen de calidad",
"reports.notes": "Notas para operaciones",
"alerts.title": "Alertas",
"alerts.subtitle": "Historial de alertas con filtros y detalle.",
"alerts.comingSoon": "La configuracion de alertas estara disponible pronto.",
"alerts.loading": "Cargando alertas...",
"alerts.error.loadPolicy": "No se pudo cargar la politica de alertas.",
"alerts.error.savePolicy": "No se pudo guardar la politica de alertas.",
"alerts.error.loadContacts": "No se pudieron cargar los contactos de alertas.",
"alerts.error.saveContacts": "No se pudo guardar el contacto de alertas.",
"alerts.error.deleteContact": "No se pudo eliminar el contacto de alertas.",
"alerts.error.createContact": "No se pudo crear el contacto de alertas.",
"alerts.policy.title": "Politica de alertas",
"alerts.policy.subtitle": "Configura escalamiento por rol, canal y duracion.",
"alerts.policy.save": "Guardar politica",
"alerts.policy.saving": "Guardando...",
"alerts.policy.defaults": "Escalamiento por defecto (por rol)",
"alerts.policy.enabled": "Habilitado",
"alerts.policy.afterMinutes": "Despues de minutos",
"alerts.policy.channels": "Canales",
"alerts.policy.repeatMinutes": "Repetir (min)",
"alerts.policy.readOnly": "Puedes ver la politica de alertas, pero solo propietarios pueden editar.",
"alerts.policy.defaultsHelp": "Los valores por defecto aplican cuando un evento se reinicia o no se personaliza.",
"alerts.policy.eventSelectLabel": "Tipo de evento",
"alerts.policy.eventSelectHelper": "Ajusta escalamiento para un solo tipo de evento.",
"alerts.policy.applyDefaults": "Aplicar por defecto",
"alerts.event.macrostop": "Macroparo",
"alerts.event.microstop": "Microparo",
"alerts.event.slow-cycle": "Ciclo lento",
"alerts.event.offline": "Fuera de linea",
"alerts.event.error": "Error",
"alerts.contacts.title": "Contactos de alertas",
"alerts.contacts.subtitle": "Destinatarios externos y alcance por rol.",
"alerts.contacts.name": "Nombre",
"alerts.contacts.roleScope": "Rol",
"alerts.contacts.email": "Correo",
"alerts.contacts.phone": "Telefono",
"alerts.contacts.eventTypes": "Tipos de evento (opcional)",
"alerts.contacts.eventTypesPlaceholder": "macroparo, microparo, fuera-de-linea",
"alerts.contacts.eventTypesHelper": "Deja vacío para recibir todos los tipos de evento.",
"alerts.contacts.add": "Agregar contacto",
"alerts.contacts.creating": "Agregando...",
"alerts.contacts.empty": "Sin contactos de alertas.",
"alerts.contacts.save": "Guardar",
"alerts.contacts.saving": "Guardando...",
"alerts.contacts.delete": "Eliminar",
"alerts.contacts.deleting": "Eliminando...",
"alerts.contacts.active": "Activo",
"alerts.contacts.linkedUser": "Usuario vinculado (editar en perfil)",
"alerts.contacts.role.custom": "Personalizado",
"alerts.contacts.role.member": "Miembro",
"alerts.contacts.role.admin": "Admin",
"alerts.contacts.role.owner": "Propietario",
"alerts.contacts.readOnly": "Puedes ver contactos, pero solo propietarios pueden agregar o editar.",
"alerts.inbox.title": "Bandeja de alertas",
"alerts.inbox.loading": "Cargando alertas...",
"alerts.inbox.loadingFilters": "Cargando filtros...",
"alerts.inbox.empty": "No se encontraron alertas.",
"alerts.inbox.error": "No se pudieron cargar las alertas.",
"alerts.inbox.range.24h": "Últimas 24 horas",
"alerts.inbox.range.7d": "Últimos 7 días",
"alerts.inbox.range.30d": "Últimos 30 días",
"alerts.inbox.range.custom": "Personalizado",
"alerts.inbox.filters.title": "Filtros",
"alerts.inbox.filters.range": "Rango",
"alerts.inbox.filters.start": "Inicio",
"alerts.inbox.filters.end": "Fin",
"alerts.inbox.filters.machine": "Máquina",
"alerts.inbox.filters.site": "Sitio",
"alerts.inbox.filters.shift": "Turno",
"alerts.inbox.filters.type": "Clasificación",
"alerts.inbox.filters.severity": "Severidad",
"alerts.inbox.filters.status": "Estado",
"alerts.inbox.filters.search": "Buscar",
"alerts.inbox.filters.searchPlaceholder": "Título, descripción, máquina...",
"alerts.inbox.filters.includeUpdates": "Incluir actualizaciones",
"alerts.inbox.filters.allMachines": "Todas las máquinas",
"alerts.inbox.filters.allSites": "Todos los sitios",
"alerts.inbox.filters.allShifts": "Todos los turnos",
"alerts.inbox.filters.allTypes": "Todas las clasificaciones",
"alerts.inbox.filters.allSeverities": "Todas las severidades",
"alerts.inbox.filters.allStatuses": "Todos los estados",
"alerts.inbox.table.time": "Hora",
"alerts.inbox.table.machine": "Máquina",
"alerts.inbox.table.site": "Sitio",
"alerts.inbox.table.shift": "Turno",
"alerts.inbox.table.type": "Tipo",
"alerts.inbox.table.severity": "Severidad",
"alerts.inbox.table.status": "Estado",
"alerts.inbox.table.duration": "Duración",
"alerts.inbox.table.title": "Título",
"alerts.inbox.table.unknown": "Sin dato",
"alerts.inbox.status.active": "Activa",
"alerts.inbox.status.resolved": "Resuelta",
"alerts.inbox.status.unknown": "Sin dato",
"alerts.inbox.duration.na": "n/d",
"alerts.inbox.duration.sec": "s",
"alerts.inbox.duration.min": " min",
"alerts.inbox.duration.hr": " h",
"alerts.inbox.meta.workOrder": "OT",
"alerts.inbox.meta.sku": "SKU",
"reports.notes": "Notas para operaciones",
"alerts.title": "Alertas",
"alerts.subtitle": "Historial de alertas con filtros y detalle.",
"alerts.comingSoon": "La configuracion de alertas estara disponible pronto.",
"alerts.loading": "Cargando alertas...",
"alerts.error.loadPolicy": "No se pudo cargar la politica de alertas.",
"alerts.error.savePolicy": "No se pudo guardar la politica de alertas.",
"alerts.error.loadContacts": "No se pudieron cargar los contactos de alertas.",
"alerts.error.saveContacts": "No se pudo guardar el contacto de alertas.",
"alerts.error.deleteContact": "No se pudo eliminar el contacto de alertas.",
"alerts.error.createContact": "No se pudo crear el contacto de alertas.",
"alerts.policy.title": "Politica de alertas",
"alerts.policy.subtitle": "Configura escalamiento por rol, canal y duracion.",
"alerts.policy.save": "Guardar politica",
"alerts.policy.saving": "Guardando...",
"alerts.policy.defaults": "Escalamiento por defecto (por rol)",
"alerts.policy.enabled": "Habilitado",
"alerts.policy.afterMinutes": "Despues de minutos",
"alerts.policy.channels": "Canales",
"alerts.policy.repeatMinutes": "Repetir (min)",
"alerts.policy.readOnly": "Puedes ver la politica de alertas, pero solo propietarios pueden editar.",
"alerts.policy.defaultsHelp": "Los valores por defecto aplican cuando un evento se reinicia o no se personaliza.",
"alerts.policy.eventSelectLabel": "Tipo de evento",
"alerts.policy.eventSelectHelper": "Ajusta escalamiento para un solo tipo de evento.",
"alerts.policy.applyDefaults": "Aplicar por defecto",
"alerts.event.macrostop": "Macroparo",
"alerts.event.microstop": "Microparo",
"alerts.event.slow-cycle": "Ciclo lento",
"alerts.event.offline": "Fuera de linea",
"alerts.event.error": "Error",
"alerts.contacts.title": "Contactos de alertas",
"alerts.contacts.subtitle": "Destinatarios externos y alcance por rol.",
"alerts.contacts.name": "Nombre",
"alerts.contacts.roleScope": "Rol",
"alerts.contacts.email": "Correo",
"alerts.contacts.phone": "Telefono",
"alerts.contacts.eventTypes": "Tipos de evento (opcional)",
"alerts.contacts.eventTypesPlaceholder": "macroparo, microparo, fuera-de-linea",
"alerts.contacts.eventTypesHelper": "Deja vacío para recibir todos los tipos de evento.",
"alerts.contacts.add": "Agregar contacto",
"alerts.contacts.creating": "Agregando...",
"alerts.contacts.empty": "Sin contactos de alertas.",
"alerts.contacts.save": "Guardar",
"alerts.contacts.saving": "Guardando...",
"alerts.contacts.delete": "Eliminar",
"alerts.contacts.deleting": "Eliminando...",
"alerts.contacts.active": "Activo",
"alerts.contacts.linkedUser": "Usuario vinculado (editar en perfil)",
"alerts.contacts.role.custom": "Personalizado",
"alerts.contacts.role.member": "Miembro",
"alerts.contacts.role.admin": "Admin",
"alerts.contacts.role.owner": "Propietario",
"alerts.contacts.readOnly": "Puedes ver contactos, pero solo propietarios pueden agregar o editar.",
"alerts.inbox.title": "Bandeja de alertas",
"alerts.inbox.loading": "Cargando alertas...",
"alerts.inbox.loadingFilters": "Cargando filtros...",
"alerts.inbox.empty": "No se encontraron alertas.",
"alerts.inbox.error": "No se pudieron cargar las alertas.",
"alerts.inbox.range.24h": "Últimas 24 horas",
"alerts.inbox.range.7d": "Últimos 7 días",
"alerts.inbox.range.30d": "Últimos 30 días",
"alerts.inbox.range.custom": "Personalizado",
"alerts.inbox.filters.title": "Filtros",
"alerts.inbox.filters.range": "Rango",
"alerts.inbox.filters.start": "Inicio",
"alerts.inbox.filters.end": "Fin",
"alerts.inbox.filters.machine": "Máquina",
"alerts.inbox.filters.site": "Sitio",
"alerts.inbox.filters.shift": "Turno",
"alerts.inbox.filters.type": "Clasificación",
"alerts.inbox.filters.severity": "Severidad",
"alerts.inbox.filters.status": "Estado",
"alerts.inbox.filters.search": "Buscar",
"alerts.inbox.filters.searchPlaceholder": "Título, descripción, máquina...",
"alerts.inbox.filters.includeUpdates": "Incluir actualizaciones",
"alerts.inbox.filters.allMachines": "Todas las máquinas",
"alerts.inbox.filters.allSites": "Todos los sitios",
"alerts.inbox.filters.allShifts": "Todos los turnos",
"alerts.inbox.filters.allTypes": "Todas las clasificaciones",
"alerts.inbox.filters.allSeverities": "Todas las severidades",
"alerts.inbox.filters.allStatuses": "Todos los estados",
"alerts.inbox.table.time": "Hora",
"alerts.inbox.table.machine": "Máquina",
"alerts.inbox.table.site": "Sitio",
"alerts.inbox.table.shift": "Turno",
"alerts.inbox.table.type": "Tipo",
"alerts.inbox.table.severity": "Severidad",
"alerts.inbox.table.status": "Estado",
"alerts.inbox.table.duration": "Duración",
"alerts.inbox.table.title": "Título",
"alerts.inbox.table.unknown": "Sin dato",
"alerts.inbox.status.active": "Activa",
"alerts.inbox.status.resolved": "Resuelta",
"alerts.inbox.status.unknown": "Sin dato",
"alerts.inbox.duration.na": "n/d",
"alerts.inbox.duration.sec": "s",
"alerts.inbox.duration.min": " min",
"alerts.inbox.duration.hr": " h",
"alerts.inbox.meta.workOrder": "OT",
"alerts.inbox.meta.sku": "SKU",
"reports.notes.suggested": "Acciones sugeridas",
"reports.notes.none": "Sin insights todavía. Genera reportes tras recolectar datos.",
"reports.noTrend": "Sin datos de tendencia.",
@@ -355,14 +355,14 @@
"reports.pdf.cycleDistribution": "Distribución de tiempos de ciclo",
"reports.pdf.notes": "Notas para operaciones",
"reports.pdf.none": "Ninguna",
"settings.title": "Configuración",
"settings.subtitle": "Configuración en vivo para turnos, alertas y valores predeterminados.",
"settings.tabs.general": "General",
"settings.tabs.shifts": "Turnos",
"settings.tabs.thresholds": "Umbrales",
"settings.tabs.alerts": "Alertas",
"settings.tabs.financial": "Finanzas",
"settings.tabs.team": "Equipo",
"settings.title": "Configuración",
"settings.subtitle": "Configuración en vivo para turnos, alertas y valores predeterminados.",
"settings.tabs.general": "General",
"settings.tabs.shifts": "Turnos",
"settings.tabs.thresholds": "Umbrales",
"settings.tabs.alerts": "Alertas",
"settings.tabs.financial": "Finanzas",
"settings.tabs.team": "Equipo",
"settings.loading": "Cargando configuración...",
"settings.loadingTeam": "Cargando equipo...",
"settings.refresh": "Actualizar",
@@ -442,68 +442,75 @@
"settings.role.admin": "Admin",
"settings.role.member": "Miembro",
"settings.role.inactive": "Inactivo",
"settings.integrations": "Integraciones",
"settings.integrations.webhook": "Webhook URL",
"settings.integrations.erp": "ERP Sync",
"settings.integrations.erpNotConfigured": "No configurado",
"financial.title": "Impacto financiero",
"financial.subtitle": "Convierte paros, ciclos lentos y scrap en dinero.",
"financial.ownerOnly": "El impacto financiero solo está disponible para propietarios.",
"financial.costsMoved": "Los costos ahora están en",
"financial.costsMovedLink": "Configuración -> Finanzas",
"financial.export.html": "HTML",
"financial.export.csv": "CSV",
"financial.totalLoss": "Pérdida total",
"financial.currencyLabel": "Moneda: {currency}",
"financial.noImpact": "Sin datos de impacto.",
"financial.chart.title": "Pérdida de dinero en el tiempo",
"financial.chart.subtitle": "Acumulado por tipo de evento",
"financial.range.day": "Día",
"financial.range.week": "Semana",
"financial.range.month": "Mes",
"financial.filters.title": "Filtros",
"financial.filters.machine": "Máquina",
"financial.filters.location": "Ubicación",
"financial.filters.sku": "SKU",
"financial.filters.currency": "Moneda",
"financial.filters.allMachines": "Todas las máquinas",
"financial.filters.allLocations": "Todas las ubicaciones",
"financial.filters.skuPlaceholder": "Filtrar por SKU",
"financial.filters.currencyPlaceholder": "MXN",
"financial.loadingMachines": "Cargando máquinas...",
"financial.config.title": "Parámetros de costo",
"financial.config.subtitle": "Los valores aplican a todas las máquinas salvo override.",
"financial.config.applyOrg": "Aplicar valores de organización a todas",
"financial.config.save": "Guardar",
"financial.config.saving": "Guardando...",
"financial.config.saved": "Guardado",
"financial.config.saveFailed": "No se pudo guardar",
"financial.config.orgDefaults": "Valores de organización",
"financial.config.locationOverrides": "Overrides por ubicación",
"financial.config.machineOverrides": "Overrides por máquina",
"financial.config.productOverrides": "Overrides por producto",
"financial.config.addLocation": "Agregar override de ubicación",
"financial.config.addMachine": "Agregar override de máquina",
"financial.config.addProduct": "Agregar override de producto",
"financial.config.noneLocation": "Sin overrides de ubicación.",
"financial.config.noneMachine": "Sin overrides de máquina.",
"financial.config.noneProduct": "Sin overrides de producto.",
"financial.config.location": "Ubicación",
"financial.config.selectLocation": "Selecciona ubicación",
"financial.config.machine": "Máquina",
"financial.config.selectMachine": "Selecciona máquina",
"financial.config.currency": "Moneda",
"financial.config.sku": "SKU",
"financial.config.rawMaterialUnit": "Materia prima / unidad",
"financial.config.ownerOnly": "Los costos financieros solo están disponibles para propietarios.",
"financial.config.loading": "Cargando finanzas...",
"financial.field.machineCostPerMin": "Costo máquina / min",
"financial.field.operatorCostPerMin": "Costo operador / min",
"financial.field.ratedRunningKw": "kW en operación",
"financial.field.idleKw": "kW en espera",
"financial.field.kwhRate": "Tarifa kWh",
"financial.field.energyMultiplier": "Multiplicador de energía",
"financial.field.energyCostPerMin": "Costo energía / min",
"financial.field.scrapCostPerUnit": "Costo scrap / unidad",
"financial.field.rawMaterialCostPerUnit": "Costo materia prima / unidad"
}
"settings.integrations": "Integraciones",
"settings.integrations.webhook": "Webhook URL",
"settings.integrations.erp": "ERP Sync",
"settings.integrations.erpNotConfigured": "No configurado",
"financial.title": "Impacto financiero",
"financial.subtitle": "Convierte paros, ciclos lentos y scrap en dinero.",
"financial.ownerOnly": "El impacto financiero solo está disponible para propietarios.",
"financial.costsMoved": "Los costos ahora están en",
"financial.costsMovedLink": "Configuración -> Finanzas",
"financial.export.html": "HTML",
"financial.export.csv": "CSV",
"financial.totalLoss": "Pérdida total",
"financial.currencyLabel": "Moneda: {currency}",
"financial.noImpact": "Sin datos de impacto.",
"financial.chart.title": "Pérdida de dinero en el tiempo",
"financial.chart.subtitle": "Acumulado por tipo de evento",
"financial.range.day": "Día",
"financial.range.week": "Semana",
"financial.range.month": "Mes",
"financial.filters.title": "Filtros",
"financial.filters.machine": "Máquina",
"financial.filters.location": "Ubicación",
"financial.filters.sku": "SKU",
"financial.filters.currency": "Moneda",
"financial.filters.allMachines": "Todas las máquinas",
"financial.filters.allLocations": "Todas las ubicaciones",
"financial.filters.skuPlaceholder": "Filtrar por SKU",
"financial.filters.currencyPlaceholder": "MXN",
"financial.loadingMachines": "Cargando máquinas...",
"financial.config.title": "Parámetros de costo",
"financial.config.subtitle": "Los valores aplican a todas las máquinas salvo override.",
"financial.config.applyOrg": "Aplicar valores de organización a todas",
"financial.config.save": "Guardar",
"financial.config.saving": "Guardando...",
"financial.config.saved": "Guardado",
"financial.config.saveFailed": "No se pudo guardar",
"financial.config.orgDefaults": "Valores de organización",
"financial.config.locationOverrides": "Overrides por ubicación",
"financial.config.machineOverrides": "Overrides por máquina",
"financial.config.productOverrides": "Overrides por producto",
"financial.config.addLocation": "Agregar override de ubicación",
"financial.config.addMachine": "Agregar override de máquina",
"financial.config.addProduct": "Agregar override de producto",
"financial.config.noneLocation": "Sin overrides de ubicación.",
"financial.config.noneMachine": "Sin overrides de máquina.",
"financial.config.noneProduct": "Sin overrides de producto.",
"financial.config.location": "Ubicación",
"financial.config.selectLocation": "Selecciona ubicación",
"financial.config.machine": "Máquina",
"financial.config.selectMachine": "Selecciona máquina",
"financial.config.currency": "Moneda",
"financial.config.sku": "SKU",
"financial.config.rawMaterialUnit": "Materia prima / unidad",
"financial.config.ownerOnly": "Los costos financieros solo están disponibles para propietarios.",
"financial.config.loading": "Cargando finanzas...",
"financial.field.machineCostPerMin": "Costo máquina / min",
"financial.field.operatorCostPerMin": "Costo operador / min",
"financial.field.ratedRunningKw": "kW en operación",
"financial.field.idleKw": "kW en espera",
"financial.field.kwhRate": "Tarifa kWh",
"financial.field.energyMultiplier": "Multiplicador de energía",
"financial.field.energyCostPerMin": "Costo energía / min",
"financial.field.scrapCostPerUnit": "Costo scrap / unidad",
"financial.field.rawMaterialCostPerUnit": "Costo materia prima / unidad",
"nav.downtime": "Downtime",
"settings.tabs.modules": "Módulos",
"settings.modules.title": "Módulos",
"settings.modules.subtitle": "Activa/desactiva módulos según cómo opera la planta.",
"settings.modules.screenless.title": "Modo sin pantalla",
"settings.modules.screenless.helper": "Oculta el módulo de Paros (Downtime) del menú (para plantas sin captura de razones en Node-RED).",
"settings.modules.note": "Este ajuste aplica a toda la organización."
}

52
lib/ui/screenlessMode.ts Normal file
View File

@@ -0,0 +1,52 @@
"use client";
import { useEffect, useState } from "react";
let current: boolean | null = null;
let inflight: Promise<void> | null = null;
const listeners = new Set<(next: boolean) => void>();
function notify(next: boolean) {
current = next;
listeners.forEach((fn) => fn(next));
}
async function loadScreenlessMode() {
if (inflight) return inflight;
inflight = (async () => {
try {
const res = await fetch("/api/settings", { cache: "no-store" });
const data = await res.json().catch(() => ({}));
if (res.ok && data?.ok) {
const mode = data?.settings?.modules?.screenlessMode === true;
notify(mode);
}
} catch {
// ignore fetch failures; keep current state
} finally {
inflight = null;
}
})();
return inflight;
}
export function useScreenlessMode() {
const [screenlessMode, setScreenlessMode] = useState(current ?? false);
useEffect(() => {
listeners.add(setScreenlessMode);
return () => {
listeners.delete(setScreenlessMode);
};
}, []);
useEffect(() => {
void loadScreenlessMode();
}, []);
function set(next: boolean) {
notify(next);
}
return { screenlessMode, setScreenlessMode: set };
}