diff --git a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx b/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx index 713c16bb..7d6731ca 100644 --- a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx +++ b/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx @@ -4,7 +4,7 @@ import type { OnboardingStepProps } from './OnboardingWizard'; interface CompanyInfo { name: string; - type: 'artisan' | 'industrial' | 'chain' | 'mixed'; + type: 'artisan' | 'dependent'; size: 'small' | 'medium' | 'large'; locations: number; specialties: string[]; @@ -25,10 +25,8 @@ interface CompanyInfo { } const BAKERY_TYPES = [ - { value: 'artisan', label: 'Artesanal', description: 'Producción tradicional y manual' }, - { value: 'industrial', label: 'Industrial', description: 'Producción automatizada a gran escala' }, - { value: 'chain', label: 'Cadena', description: 'Múltiples ubicaciones con procesos estandarizados' }, - { value: 'mixed', label: 'Mixta', description: 'Combinación de métodos artesanales e industriales' }, + { value: 'artisan', label: 'Panadería Artesanal Local', description: 'Producción propia y tradicional en el local' }, + { value: 'dependent', label: 'Panadería Dependiente', description: 'Dependiente de un panadero central' }, ]; const BAKERY_SIZES = [ diff --git a/frontend/src/pages/app/communications/alerts/AlertsPage.tsx b/frontend/src/pages/app/communications/alerts/AlertsPage.tsx deleted file mode 100644 index d357d15f..00000000 --- a/frontend/src/pages/app/communications/alerts/AlertsPage.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import React, { useState } from 'react'; -import { Bell, AlertTriangle, AlertCircle, CheckCircle, Clock, Settings, Filter, Search } from 'lucide-react'; -import { Button, Card, Badge, Input } from '../../../../components/ui'; -import { PageHeader } from '../../../../components/layout'; - -const AlertsPage: React.FC = () => { - const [selectedFilter, setSelectedFilter] = useState('all'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedAlert, setSelectedAlert] = useState(null); - - const alerts = [ - { - id: '1', - type: 'critical', - category: 'inventory', - title: 'Stock Crítico - Harina de Trigo', - message: 'Quedan solo 5kg de harina de trigo. El stock mínimo es de 20kg.', - timestamp: '2024-01-26 10:30:00', - read: false, - actionRequired: true, - priority: 'high', - source: 'Sistema de Inventario', - details: { - currentStock: '5kg', - minimumStock: '20kg', - supplier: 'Molinos del Sur', - estimatedDepletion: '1 día' - } - }, - { - id: '2', - type: 'warning', - category: 'production', - title: 'Retraso en Producción', - message: 'El lote de croissants #CR-024 lleva 45 minutos de retraso.', - timestamp: '2024-01-26 09:15:00', - read: false, - actionRequired: true, - priority: 'medium', - source: 'Control de Producción', - details: { - batchId: 'CR-024', - expectedTime: '2.5h', - actualTime: '3.25h', - delayReason: 'Problema con el horno #2' - } - }, - { - id: '3', - type: 'info', - category: 'sales', - title: 'Pico de Ventas Detectado', - message: 'Las ventas han aumentado un 35% en la última hora.', - timestamp: '2024-01-26 08:45:00', - read: true, - actionRequired: false, - priority: 'low', - source: 'Sistema de Ventas', - details: { - increase: '35%', - period: 'Última hora', - topProducts: ['Croissants', 'Pan Integral', 'Empanadas'], - expectedRevenue: '€320' - } - }, - { - id: '4', - type: 'success', - category: 'quality', - title: 'Control de Calidad Completado', - message: 'Lote de pan integral #PI-156 aprobado con puntuación de 9.2/10.', - timestamp: '2024-01-26 07:30:00', - read: true, - actionRequired: false, - priority: 'low', - source: 'Control de Calidad', - details: { - batchId: 'PI-156', - score: '9.2/10', - inspector: 'María González', - testsPassed: '15/15' - } - }, - { - id: '5', - type: 'critical', - category: 'equipment', - title: 'Fallo del Horno Principal', - message: 'El horno #1 ha presentado una falla en el sistema de temperatura.', - timestamp: '2024-01-25 16:20:00', - read: false, - actionRequired: true, - priority: 'high', - source: 'Monitoreo de Equipos', - details: { - equipment: 'Horno #1', - error: 'Sistema de temperatura', - impact: 'Producción reducida 50%', - technician: 'Pendiente' - } - }, - { - id: '6', - type: 'warning', - category: 'staff', - title: 'Ausentismo del Personal', - message: '2 empleados del turno matutino no se han presentado.', - timestamp: '2024-01-25 07:00:00', - read: true, - actionRequired: true, - priority: 'medium', - source: 'Gestión de Personal', - details: { - absentEmployees: ['Juan Pérez', 'Ana García'], - shift: 'Matutino', - coverage: '75%', - replacement: 'Solicitada' - } - } - ]; - - const alertStats = { - total: alerts.length, - unread: alerts.filter(a => !a.read).length, - critical: alerts.filter(a => a.type === 'critical').length, - actionRequired: alerts.filter(a => a.actionRequired).length - }; - - const categories = [ - { value: 'all', label: 'Todas', count: alerts.length }, - { value: 'inventory', label: 'Inventario', count: alerts.filter(a => a.category === 'inventory').length }, - { value: 'production', label: 'Producción', count: alerts.filter(a => a.category === 'production').length }, - { value: 'sales', label: 'Ventas', count: alerts.filter(a => a.category === 'sales').length }, - { value: 'quality', label: 'Calidad', count: alerts.filter(a => a.category === 'quality').length }, - { value: 'equipment', label: 'Equipos', count: alerts.filter(a => a.category === 'equipment').length }, - { value: 'staff', label: 'Personal', count: alerts.filter(a => a.category === 'staff').length } - ]; - - const getAlertIcon = (type: string) => { - const iconProps = { className: "w-5 h-5" }; - switch (type) { - case 'critical': return ; - case 'warning': return ; - case 'success': return ; - default: return ; - } - }; - - const getAlertColor = (type: string) => { - switch (type) { - case 'critical': return 'red'; - case 'warning': return 'yellow'; - case 'success': return 'green'; - default: return 'blue'; - } - }; - - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': return 'bg-[var(--color-error)]/10 text-[var(--color-error)]'; - case 'medium': return 'bg-yellow-100 text-yellow-800'; - case 'low': return 'bg-[var(--color-success)]/10 text-[var(--color-success)]'; - default: return 'bg-[var(--bg-tertiary)] text-[var(--text-primary)]'; - } - }; - - const filteredAlerts = alerts.filter(alert => { - const matchesFilter = selectedFilter === 'all' || alert.category === selectedFilter; - const matchesSearch = alert.title.toLowerCase().includes(searchTerm.toLowerCase()) || - alert.message.toLowerCase().includes(searchTerm.toLowerCase()); - return matchesFilter && matchesSearch; - }); - - const handleMarkAsRead = (alertId: string) => { - // Handle mark as read logic - console.log('Marking alert as read:', alertId); - }; - - const handleDismissAlert = (alertId: string) => { - // Handle dismiss alert logic - console.log('Dismissing alert:', alertId); - }; - - const formatTimeAgo = (timestamp: string) => { - const now = new Date(); - const alertTime = new Date(timestamp); - const diffInMs = now.getTime() - alertTime.getTime(); - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); - const diffInMins = Math.floor(diffInMs / (1000 * 60)); - - if (diffInHours > 0) { - return `hace ${diffInHours}h`; - } else { - return `hace ${diffInMins}m`; - } - }; - - return ( -
- - - -
- } - /> - - {/* Alert Stats */} -
- -
-
-

Total Alertas

-

{alertStats.total}

-
-
- -
-
-
- - -
-
-

Sin Leer

-

{alertStats.unread}

-
-
- -
-
-
- - -
-
-

Críticas

-

{alertStats.critical}

-
-
- -
-
-
- - -
-
-

Acción Requerida

-

{alertStats.actionRequired}

-
-
- -
-
-
-
- - {/* Filters and Search */} - -
-
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
-
- - -
-
-
- - {/* Alerts List */} -
- {filteredAlerts.map((alert) => ( - setSelectedAlert(selectedAlert === alert.id ? null : alert.id)} - > -
-
-
- {getAlertIcon(alert.type)} -
- -
-
-

{alert.title}

- {!alert.read && ( -
- )} - - {alert.type === 'critical' ? 'Crítica' : - alert.type === 'warning' ? 'Advertencia' : - alert.type === 'success' ? 'Éxito' : 'Info'} - - - Prioridad {alert.priority === 'high' ? 'Alta' : alert.priority === 'medium' ? 'Media' : 'Baja'} - -
- -

{alert.message}

- -
- {formatTimeAgo(alert.timestamp)} - - {alert.source} - {alert.actionRequired && ( - <> - - Acción Requerida - - )} -
-
-
- -
- {!alert.read && ( - - )} - -
-
- - {/* Alert Details - Expandible */} - {selectedAlert === alert.id && ( -
-

Detalles de la Alerta

-
- {Object.entries(alert.details).map(([key, value]) => ( -
-

- {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} -

-

{value}

-
- ))} -
- - {alert.actionRequired && ( -
- - - -
- )} -
- )} -
- ))} -
- - {filteredAlerts.length === 0 && ( - - -

No hay alertas

-

- No se encontraron alertas que coincidan con los filtros seleccionados. -

-
- )} - - ); -}; - -export default AlertsPage; \ No newline at end of file diff --git a/frontend/src/pages/app/communications/alerts/AlertsPage.tsx.backup b/frontend/src/pages/app/communications/alerts/AlertsPage.tsx.backup deleted file mode 100644 index 009c4f49..00000000 --- a/frontend/src/pages/app/communications/alerts/AlertsPage.tsx.backup +++ /dev/null @@ -1,414 +0,0 @@ -import React, { useState } from 'react'; -import { Bell, AlertTriangle, AlertCircle, CheckCircle, Clock, Settings, Filter, Search } from 'lucide-react'; -import { Button, Card, Badge, Input } from '../../../../components/ui'; -import { PageHeader } from '../../../../components/layout'; - -const AlertsPage: React.FC = () => { - const [selectedFilter, setSelectedFilter] = useState('all'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedAlert, setSelectedAlert] = useState(null); - - const alerts = [ - { - id: '1', - type: 'critical', - category: 'inventory', - title: 'Stock Crítico - Harina de Trigo', - message: 'Quedan solo 5kg de harina de trigo. El stock mínimo es de 20kg.', - timestamp: '2024-01-26 10:30:00', - read: false, - actionRequired: true, - priority: 'high', - source: 'Sistema de Inventario', - details: { - currentStock: '5kg', - minimumStock: '20kg', - supplier: 'Molinos del Sur', - estimatedDepletion: '1 día' - } - }, - { - id: '2', - type: 'warning', - category: 'production', - title: 'Retraso en Producción', - message: 'El lote de croissants #CR-024 lleva 45 minutos de retraso.', - timestamp: '2024-01-26 09:15:00', - read: false, - actionRequired: true, - priority: 'medium', - source: 'Control de Producción', - details: { - batchId: 'CR-024', - expectedTime: '2.5h', - actualTime: '3.25h', - delayReason: 'Problema con el horno #2' - } - }, - { - id: '3', - type: 'info', - category: 'sales', - title: 'Pico de Ventas Detectado', - message: 'Las ventas han aumentado un 35% en la última hora.', - timestamp: '2024-01-26 08:45:00', - read: true, - actionRequired: false, - priority: 'low', - source: 'Sistema de Ventas', - details: { - increase: '35%', - period: 'Última hora', - topProducts: ['Croissants', 'Pan Integral', 'Empanadas'], - expectedRevenue: '€320' - } - }, - { - id: '4', - type: 'success', - category: 'quality', - title: 'Control de Calidad Completado', - message: 'Lote de pan integral #PI-156 aprobado con puntuación de 9.2/10.', - timestamp: '2024-01-26 07:30:00', - read: true, - actionRequired: false, - priority: 'low', - source: 'Control de Calidad', - details: { - batchId: 'PI-156', - score: '9.2/10', - inspector: 'María González', - testsPassed: '15/15' - } - }, - { - id: '5', - type: 'critical', - category: 'equipment', - title: 'Fallo del Horno Principal', - message: 'El horno #1 ha presentado una falla en el sistema de temperatura.', - timestamp: '2024-01-25 16:20:00', - read: false, - actionRequired: true, - priority: 'high', - source: 'Monitoreo de Equipos', - details: { - equipment: 'Horno #1', - error: 'Sistema de temperatura', - impact: 'Producción reducida 50%', - technician: 'Pendiente' - } - }, - { - id: '6', - type: 'warning', - category: 'staff', - title: 'Ausentismo del Personal', - message: '2 empleados del turno matutino no se han presentado.', - timestamp: '2024-01-25 07:00:00', - read: true, - actionRequired: true, - priority: 'medium', - source: 'Gestión de Personal', - details: { - absentEmployees: ['Juan Pérez', 'Ana García'], - shift: 'Matutino', - coverage: '75%', - replacement: 'Solicitada' - } - } - ]; - - const alertStats = { - total: alerts.length, - unread: alerts.filter(a => !a.read).length, - critical: alerts.filter(a => a.type === 'critical').length, - actionRequired: alerts.filter(a => a.actionRequired).length - }; - - const categories = [ - { value: 'all', label: 'Todas', count: alerts.length }, - { value: 'inventory', label: 'Inventario', count: alerts.filter(a => a.category === 'inventory').length }, - { value: 'production', label: 'Producción', count: alerts.filter(a => a.category === 'production').length }, - { value: 'sales', label: 'Ventas', count: alerts.filter(a => a.category === 'sales').length }, - { value: 'quality', label: 'Calidad', count: alerts.filter(a => a.category === 'quality').length }, - { value: 'equipment', label: 'Equipos', count: alerts.filter(a => a.category === 'equipment').length }, - { value: 'staff', label: 'Personal', count: alerts.filter(a => a.category === 'staff').length } - ]; - - const getAlertIcon = (type: string) => { - const iconProps = { className: "w-5 h-5" }; - switch (type) { - case 'critical': return ; - case 'warning': return ; - case 'success': return ; - default: return ; - } - }; - - const getAlertColor = (type: string) => { - switch (type) { - case 'critical': return 'red'; - case 'warning': return 'yellow'; - case 'success': return 'green'; - default: return 'blue'; - } - }; - - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': return 'bg-red-100 text-red-800'; - case 'medium': return 'bg-yellow-100 text-yellow-800'; - case 'low': return 'bg-green-100 text-green-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - - const filteredAlerts = alerts.filter(alert => { - const matchesFilter = selectedFilter === 'all' || alert.category === selectedFilter; - const matchesSearch = alert.title.toLowerCase().includes(searchTerm.toLowerCase()) || - alert.message.toLowerCase().includes(searchTerm.toLowerCase()); - return matchesFilter && matchesSearch; - }); - - const handleMarkAsRead = (alertId: string) => { - // Handle mark as read logic - console.log('Marking alert as read:', alertId); - }; - - const handleDismissAlert = (alertId: string) => { - // Handle dismiss alert logic - console.log('Dismissing alert:', alertId); - }; - - const formatTimeAgo = (timestamp: string) => { - const now = new Date(); - const alertTime = new Date(timestamp); - const diffInMs = now.getTime() - alertTime.getTime(); - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); - const diffInMins = Math.floor(diffInMs / (1000 * 60)); - - if (diffInHours > 0) { - return `hace ${diffInHours}h`; - } else { - return `hace ${diffInMins}m`; - } - }; - - return ( -
- - - -
- } - /> - - {/* Alert Stats */} -
- -
-
-

Total Alertas

-

{alertStats.total}

-
-
- -
-
-
- - -
-
-

Sin Leer

-

{alertStats.unread}

-
-
- -
-
-
- - -
-
-

Críticas

-

{alertStats.critical}

-
-
- -
-
-
- - -
-
-

Acción Requerida

-

{alertStats.actionRequired}

-
-
- -
-
-
-
- - {/* Filters and Search */} - -
-
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
-
- - -
-
-
- - {/* Alerts List */} -
- {filteredAlerts.map((alert) => ( - setSelectedAlert(selectedAlert === alert.id ? null : alert.id)} - > -
-
-
- {getAlertIcon(alert.type)} -
- -
-
-

{alert.title}

- {!alert.read && ( -
- )} - - {alert.type === 'critical' ? 'Crítica' : - alert.type === 'warning' ? 'Advertencia' : - alert.type === 'success' ? 'Éxito' : 'Info'} - - - Prioridad {alert.priority === 'high' ? 'Alta' : alert.priority === 'medium' ? 'Media' : 'Baja'} - -
- -

{alert.message}

- -
- {formatTimeAgo(alert.timestamp)} - - {alert.source} - {alert.actionRequired && ( - <> - - Acción Requerida - - )} -
-
-
- -
- {!alert.read && ( - - )} - -
-
- - {/* Alert Details - Expandible */} - {selectedAlert === alert.id && ( -
-

Detalles de la Alerta

-
- {Object.entries(alert.details).map(([key, value]) => ( -
-

- {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} -

-

{value}

-
- ))} -
- - {alert.actionRequired && ( -
- - - -
- )} -
- )} -
- ))} -
- - {filteredAlerts.length === 0 && ( - - -

No hay alertas

-

- No se encontraron alertas que coincidan con los filtros seleccionados. -

-
- )} - - ); -}; - -export default AlertsPage; \ No newline at end of file diff --git a/frontend/src/pages/app/communications/alerts/index.ts b/frontend/src/pages/app/communications/alerts/index.ts deleted file mode 100644 index 90ea1649..00000000 --- a/frontend/src/pages/app/communications/alerts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AlertsPage } from './AlertsPage'; \ No newline at end of file diff --git a/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx b/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx deleted file mode 100644 index d5199c20..00000000 --- a/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import React, { useState } from 'react'; -import { Bell, Mail, MessageSquare, Settings, Archive, Trash2, CheckCircle, Filter } from 'lucide-react'; -import { Button, Card, Badge, Input } from '../../../../components/ui'; -import { PageHeader } from '../../../../components/layout'; - -const NotificationsPage: React.FC = () => { - const [selectedTab, setSelectedTab] = useState('all'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedNotifications, setSelectedNotifications] = useState([]); - - const notifications = [ - { - id: '1', - type: 'system', - channel: 'app', - title: 'Actualización del Sistema', - message: 'Nueva versión 2.1.0 disponible con mejoras en el módulo de inventario', - timestamp: '2024-01-26 10:15:00', - read: false, - priority: 'medium', - category: 'update', - sender: 'Sistema', - actions: ['Ver Detalles', 'Instalar Después'] - }, - { - id: '2', - type: 'order', - channel: 'email', - title: 'Nuevo Pedido Recibido', - message: 'Pedido #ORD-456 por €127.50 de Panadería Central', - timestamp: '2024-01-26 09:30:00', - read: false, - priority: 'high', - category: 'sales', - sender: 'Sistema de Ventas', - actions: ['Ver Pedido', 'Procesar'] - }, - { - id: '3', - type: 'inventory', - channel: 'sms', - title: 'Stock Repuesto', - message: 'Se ha repuesto el stock de azúcar. Nivel actual: 50kg', - timestamp: '2024-01-26 08:45:00', - read: true, - priority: 'low', - category: 'inventory', - sender: 'Gestión de Inventario', - actions: ['Ver Inventario'] - }, - { - id: '4', - type: 'reminder', - channel: 'app', - title: 'Recordatorio de Mantenimiento', - message: 'El horno #2 requiere mantenimiento preventivo programado para mañana', - timestamp: '2024-01-26 07:00:00', - read: true, - priority: 'medium', - category: 'maintenance', - sender: 'Sistema de Mantenimiento', - actions: ['Programar', 'Posponer'] - }, - { - id: '5', - type: 'customer', - channel: 'app', - title: 'Reseña de Cliente', - message: 'Nueva reseña de 5 estrellas de María L.: "Excelente calidad y servicio"', - timestamp: '2024-01-25 19:20:00', - read: false, - priority: 'low', - category: 'feedback', - sender: 'Sistema de Reseñas', - actions: ['Ver Reseña', 'Responder'] - }, - { - id: '6', - type: 'promotion', - channel: 'email', - title: 'Campaña de Marketing Completada', - message: 'La campaña "Desayunos Especiales" ha terminado con 340 interacciones', - timestamp: '2024-01-25 16:30:00', - read: true, - priority: 'low', - category: 'marketing', - sender: 'Sistema de Marketing', - actions: ['Ver Resultados'] - } - ]; - - const notificationStats = { - total: notifications.length, - unread: notifications.filter(n => !n.read).length, - high: notifications.filter(n => n.priority === 'high').length, - today: notifications.filter(n => - new Date(n.timestamp).toDateString() === new Date().toDateString() - ).length - }; - - const tabs = [ - { id: 'all', label: 'Todas', count: notifications.length }, - { id: 'unread', label: 'Sin Leer', count: notificationStats.unread }, - { id: 'system', label: 'Sistema', count: notifications.filter(n => n.type === 'system').length }, - { id: 'order', label: 'Pedidos', count: notifications.filter(n => n.type === 'order').length }, - { id: 'inventory', label: 'Inventario', count: notifications.filter(n => n.type === 'inventory').length } - ]; - - const getNotificationIcon = (type: string, channel: string) => { - const iconProps = { className: "w-5 h-5" }; - - if (channel === 'email') return ; - if (channel === 'sms') return ; - - switch (type) { - case 'system': return ; - case 'order': return ; - default: return ; - } - }; - - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': return 'red'; - case 'medium': return 'yellow'; - case 'low': return 'green'; - default: return 'gray'; - } - }; - - const getChannelBadge = (channel: string) => { - const colors = { - app: 'blue', - email: 'purple', - sms: 'green' - }; - return colors[channel as keyof typeof colors] || 'gray'; - }; - - const filteredNotifications = notifications.filter(notification => { - let matchesTab = true; - if (selectedTab === 'unread') { - matchesTab = !notification.read; - } else if (selectedTab !== 'all') { - matchesTab = notification.type === selectedTab; - } - - const matchesSearch = notification.title.toLowerCase().includes(searchTerm.toLowerCase()) || - notification.message.toLowerCase().includes(searchTerm.toLowerCase()); - - return matchesTab && matchesSearch; - }); - - const handleSelectNotification = (notificationId: string) => { - setSelectedNotifications(prev => - prev.includes(notificationId) - ? prev.filter(id => id !== notificationId) - : [...prev, notificationId] - ); - }; - - const handleSelectAll = () => { - setSelectedNotifications( - selectedNotifications.length === filteredNotifications.length - ? [] - : filteredNotifications.map(n => n.id) - ); - }; - - const formatTimeAgo = (timestamp: string) => { - const now = new Date(); - const notificationTime = new Date(timestamp); - const diffInMs = now.getTime() - notificationTime.getTime(); - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); - const diffInDays = Math.floor(diffInHours / 24); - - if (diffInDays > 0) { - return `hace ${diffInDays}d`; - } else if (diffInHours > 0) { - return `hace ${diffInHours}h`; - } else { - return `hace ${Math.floor(diffInMs / (1000 * 60))}m`; - } - }; - - return ( -
- - - -
- } - /> - - {/* Notification Stats */} -
- -
-
-

Total

-

{notificationStats.total}

-
-
- -
-
-
- - -
-
-

Sin Leer

-

{notificationStats.unread}

-
-
- -
-
-
- - -
-
-

Alta Prioridad

-

{notificationStats.high}

-
-
- -
-
-
- - -
-
-

Hoy

-

{notificationStats.today}

-
-
- -
-
-
-
- - {/* Tabs and Search */} - -
- {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Search and Actions */} -
-
- setSearchTerm(e.target.value)} - className="w-full" - /> -
- - {selectedNotifications.length > 0 && ( -
- - - -
- )} - - -
-
-
- - {/* Bulk Actions */} - {filteredNotifications.length > 0 && ( -
- -
- )} - - {/* Notifications List */} -
- {filteredNotifications.map((notification) => ( - -
- handleSelectNotification(notification.id)} - className="rounded border-[var(--border-secondary)] mt-1" - /> - -
- {getNotificationIcon(notification.type, notification.channel)} -
- -
-
-

- {notification.title} -

- {!notification.read && ( -
- )} - - {notification.priority === 'high' ? 'Alta' : - notification.priority === 'medium' ? 'Media' : 'Baja'} - - - {notification.channel.toUpperCase()} - -
- -

{notification.message}

- -
-
- {formatTimeAgo(notification.timestamp)} - - {notification.sender} -
- -
- {notification.actions.map((action, index) => ( - - ))} -
-
-
-
-
- ))} -
- - {filteredNotifications.length === 0 && ( - - -

No hay notificaciones

-

- No se encontraron notificaciones que coincidan con los filtros seleccionados. -

-
- )} - - ); -}; - -export default NotificationsPage; \ No newline at end of file diff --git a/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx.backup b/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx.backup deleted file mode 100644 index 2d1999f0..00000000 --- a/frontend/src/pages/app/communications/notifications/NotificationsPage.tsx.backup +++ /dev/null @@ -1,402 +0,0 @@ -import React, { useState } from 'react'; -import { Bell, Mail, MessageSquare, Settings, Archive, Trash2, MarkAsRead, Filter } from 'lucide-react'; -import { Button, Card, Badge, Input } from '../../../../components/ui'; -import { PageHeader } from '../../../../components/layout'; - -const NotificationsPage: React.FC = () => { - const [selectedTab, setSelectedTab] = useState('all'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedNotifications, setSelectedNotifications] = useState([]); - - const notifications = [ - { - id: '1', - type: 'system', - channel: 'app', - title: 'Actualización del Sistema', - message: 'Nueva versión 2.1.0 disponible con mejoras en el módulo de inventario', - timestamp: '2024-01-26 10:15:00', - read: false, - priority: 'medium', - category: 'update', - sender: 'Sistema', - actions: ['Ver Detalles', 'Instalar Después'] - }, - { - id: '2', - type: 'order', - channel: 'email', - title: 'Nuevo Pedido Recibido', - message: 'Pedido #ORD-456 por €127.50 de Panadería Central', - timestamp: '2024-01-26 09:30:00', - read: false, - priority: 'high', - category: 'sales', - sender: 'Sistema de Ventas', - actions: ['Ver Pedido', 'Procesar'] - }, - { - id: '3', - type: 'inventory', - channel: 'sms', - title: 'Stock Repuesto', - message: 'Se ha repuesto el stock de azúcar. Nivel actual: 50kg', - timestamp: '2024-01-26 08:45:00', - read: true, - priority: 'low', - category: 'inventory', - sender: 'Gestión de Inventario', - actions: ['Ver Inventario'] - }, - { - id: '4', - type: 'reminder', - channel: 'app', - title: 'Recordatorio de Mantenimiento', - message: 'El horno #2 requiere mantenimiento preventivo programado para mañana', - timestamp: '2024-01-26 07:00:00', - read: true, - priority: 'medium', - category: 'maintenance', - sender: 'Sistema de Mantenimiento', - actions: ['Programar', 'Posponer'] - }, - { - id: '5', - type: 'customer', - channel: 'app', - title: 'Reseña de Cliente', - message: 'Nueva reseña de 5 estrellas de María L.: "Excelente calidad y servicio"', - timestamp: '2024-01-25 19:20:00', - read: false, - priority: 'low', - category: 'feedback', - sender: 'Sistema de Reseñas', - actions: ['Ver Reseña', 'Responder'] - }, - { - id: '6', - type: 'promotion', - channel: 'email', - title: 'Campaña de Marketing Completada', - message: 'La campaña "Desayunos Especiales" ha terminado con 340 interacciones', - timestamp: '2024-01-25 16:30:00', - read: true, - priority: 'low', - category: 'marketing', - sender: 'Sistema de Marketing', - actions: ['Ver Resultados'] - } - ]; - - const notificationStats = { - total: notifications.length, - unread: notifications.filter(n => !n.read).length, - high: notifications.filter(n => n.priority === 'high').length, - today: notifications.filter(n => - new Date(n.timestamp).toDateString() === new Date().toDateString() - ).length - }; - - const tabs = [ - { id: 'all', label: 'Todas', count: notifications.length }, - { id: 'unread', label: 'Sin Leer', count: notificationStats.unread }, - { id: 'system', label: 'Sistema', count: notifications.filter(n => n.type === 'system').length }, - { id: 'order', label: 'Pedidos', count: notifications.filter(n => n.type === 'order').length }, - { id: 'inventory', label: 'Inventario', count: notifications.filter(n => n.type === 'inventory').length } - ]; - - const getNotificationIcon = (type: string, channel: string) => { - const iconProps = { className: "w-5 h-5" }; - - if (channel === 'email') return ; - if (channel === 'sms') return ; - - switch (type) { - case 'system': return ; - case 'order': return ; - default: return ; - } - }; - - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': return 'red'; - case 'medium': return 'yellow'; - case 'low': return 'green'; - default: return 'gray'; - } - }; - - const getChannelBadge = (channel: string) => { - const colors = { - app: 'blue', - email: 'purple', - sms: 'green' - }; - return colors[channel as keyof typeof colors] || 'gray'; - }; - - const filteredNotifications = notifications.filter(notification => { - let matchesTab = true; - if (selectedTab === 'unread') { - matchesTab = !notification.read; - } else if (selectedTab !== 'all') { - matchesTab = notification.type === selectedTab; - } - - const matchesSearch = notification.title.toLowerCase().includes(searchTerm.toLowerCase()) || - notification.message.toLowerCase().includes(searchTerm.toLowerCase()); - - return matchesTab && matchesSearch; - }); - - const handleSelectNotification = (notificationId: string) => { - setSelectedNotifications(prev => - prev.includes(notificationId) - ? prev.filter(id => id !== notificationId) - : [...prev, notificationId] - ); - }; - - const handleSelectAll = () => { - setSelectedNotifications( - selectedNotifications.length === filteredNotifications.length - ? [] - : filteredNotifications.map(n => n.id) - ); - }; - - const formatTimeAgo = (timestamp: string) => { - const now = new Date(); - const notificationTime = new Date(timestamp); - const diffInMs = now.getTime() - notificationTime.getTime(); - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); - const diffInDays = Math.floor(diffInHours / 24); - - if (diffInDays > 0) { - return `hace ${diffInDays}d`; - } else if (diffInHours > 0) { - return `hace ${diffInHours}h`; - } else { - return `hace ${Math.floor(diffInMs / (1000 * 60))}m`; - } - }; - - return ( -
- - - -
- } - /> - - {/* Notification Stats */} -
- -
-
-

Total

-

{notificationStats.total}

-
-
- -
-
-
- - -
-
-

Sin Leer

-

{notificationStats.unread}

-
-
- -
-
-
- - -
-
-

Alta Prioridad

-

{notificationStats.high}

-
-
- -
-
-
- - -
-
-

Hoy

-

{notificationStats.today}

-
-
- -
-
-
-
- - {/* Tabs and Search */} - -
- {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Search and Actions */} -
-
- setSearchTerm(e.target.value)} - className="w-full" - /> -
- - {selectedNotifications.length > 0 && ( -
- - - -
- )} - - -
-
-
- - {/* Bulk Actions */} - {filteredNotifications.length > 0 && ( -
- -
- )} - - {/* Notifications List */} -
- {filteredNotifications.map((notification) => ( - -
- handleSelectNotification(notification.id)} - className="rounded border-gray-300 mt-1" - /> - -
- {getNotificationIcon(notification.type, notification.channel)} -
- -
-
-

- {notification.title} -

- {!notification.read && ( -
- )} - - {notification.priority === 'high' ? 'Alta' : - notification.priority === 'medium' ? 'Media' : 'Baja'} - - - {notification.channel.toUpperCase()} - -
- -

{notification.message}

- -
-
- {formatTimeAgo(notification.timestamp)} - - {notification.sender} -
- -
- {notification.actions.map((action, index) => ( - - ))} -
-
-
-
-
- ))} -
- - {filteredNotifications.length === 0 && ( - - -

No hay notificaciones

-

- No se encontraron notificaciones que coincidan con los filtros seleccionados. -

-
- )} - - ); -}; - -export default NotificationsPage; \ No newline at end of file diff --git a/frontend/src/pages/app/communications/notifications/index.ts b/frontend/src/pages/app/communications/notifications/index.ts deleted file mode 100644 index 6af2a51f..00000000 --- a/frontend/src/pages/app/communications/notifications/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NotificationsPage } from './NotificationsPage'; \ No newline at end of file diff --git a/frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx b/frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx index 7fb99bc8..f303d2a3 100644 --- a/frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx +++ b/frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { ChevronRight, ChevronLeft, Check, Store, Users, Settings, Zap, Upload } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { ChevronRight, ChevronLeft, Check, Store, Upload, Brain } from 'lucide-react'; import { Button, Card, Input } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; @@ -14,35 +14,23 @@ const OnboardingSetupPage: React.FC = () => { phone: '+1 234 567 8900', email: 'info@elbuenpan.com' }, - team: { - ownerName: 'María García López', - teamSize: '5-10', - roles: [], - experience: 'experienced' - }, - operations: { - openingHours: { - start: '06:00', - end: '20:00' - }, - daysOpen: 6, - specialties: ['bread', 'pastries', 'cakes'], - dailyProduction: 'medium' - }, upload: { salesData: null, inventoryData: null, - recipeData: null, - useTemplates: true, - skipUpload: false - }, - goals: { - primaryGoals: ['increase-sales', 'reduce-waste', 'improve-efficiency'], - expectedRevenue: '15000-30000', - timeline: '6months' + recipeData: null } }); + const [trainingState, setTrainingState] = useState({ + isTraining: false, + progress: 0, + currentStep: 'Iniciando entrenamiento...', + status: 'pending', // 'pending', 'running', 'completed', 'error' + logs: [] as string[] + }); + + const wsRef = useRef(null); + const steps = [ { id: 1, @@ -53,59 +41,31 @@ const OnboardingSetupPage: React.FC = () => { }, { id: 2, - title: 'Equipo y Personal', - description: 'Información sobre tu equipo de trabajo', - icon: Users, - fields: ['owner', 'teamSize', 'roles', 'experience'] - }, - { - id: 3, - title: 'Operaciones', - description: 'Horarios y especialidades de producción', - icon: Settings, - fields: ['hours', 'specialties', 'production'] - }, - { - id: 4, title: 'Carga de Datos', description: 'Importa tus datos existentes para acelerar la configuración inicial', icon: Upload, fields: ['files', 'templates', 'validation'] }, { - id: 5, - title: 'Objetivos', - description: 'Metas y expectativas para tu panadería', - icon: Zap, - fields: ['goals', 'revenue', 'timeline'] + id: 3, + title: 'Entrenamiento IA', + description: 'Configurando tu modelo de inteligencia artificial personalizado', + icon: Brain, + fields: ['training', 'progress', 'completion'] } ]; const bakeryTypes = [ - { value: 'traditional', label: 'Panadería Tradicional' }, - { value: 'artisan', label: 'Panadería Artesanal' }, - { value: 'cafe', label: 'Panadería-Café' }, - { value: 'industrial', label: 'Producción Industrial' } - ]; - - const specialties = [ - { value: 'bread', label: 'Pan Tradicional' }, - { value: 'pastries', label: 'Bollería' }, - { value: 'cakes', label: 'Tartas y Pasteles' }, - { value: 'cookies', label: 'Galletas' }, - { value: 'savory', label: 'Productos Salados' }, - { value: 'gluten-free', label: 'Sin Gluten' }, - { value: 'vegan', label: 'Vegano' }, - { value: 'organic', label: 'Orgánico' } - ]; - - const businessGoals = [ - { value: 'increase-sales', label: 'Aumentar Ventas' }, - { value: 'reduce-waste', label: 'Reducir Desperdicios' }, - { value: 'improve-efficiency', label: 'Mejorar Eficiencia' }, - { value: 'expand-menu', label: 'Ampliar Menú' }, - { value: 'digital-presence', label: 'Presencia Digital' }, - { value: 'customer-loyalty', label: 'Fidelización de Clientes' } + { + value: 'artisan', + label: 'Panadería Artesanal Local', + description: 'Producción propia y tradicional en el local' + }, + { + value: 'dependent', + label: 'Panadería Dependiente', + description: 'Dependiente de un panadero central' + } ]; const handleInputChange = (section: string, field: string, value: any) => { @@ -153,20 +113,129 @@ const OnboardingSetupPage: React.FC = () => { window.location.href = '/app/dashboard'; }; + const downloadTemplate = (type: 'sales' | 'inventory' | 'recipes') => { + let csvContent = ''; + let fileName = ''; + + switch (type) { + case 'sales': + csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta +2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda +2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online +2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda +2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda +2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`; + fileName = 'plantilla_ventas.csv'; + break; + case 'inventory': + csvContent = `nombre,categoria,unidad_medida,stock_actual,stock_minimo,stock_maximo,precio_compra,proveedor,fecha_vencimiento +Harina de Trigo,Ingrediente,kg,50,20,100,1.20,Molinos del Sur,2024-12-31 +Azúcar Blanca,Ingrediente,kg,25,10,50,0.85,Dulces SA,2024-12-31 +Levadura Fresca,Ingrediente,kg,5,2,10,3.50,Levaduras Pro,2024-03-15 +Mantequilla,Ingrediente,kg,15,5,30,4.20,Lácteos Premium,2024-02-28 +Pan Integral,Producto Final,unidad,20,10,50,0.00,Producción Propia,2024-01-20`; + fileName = 'plantilla_inventario.csv'; + break; + case 'recipes': + csvContent = `nombre_receta,categoria,tiempo_preparacion,tiempo_coccion,porciones,ingrediente,cantidad,unidad +Pan Integral,Panadería,30,45,2,Harina Integral,500,g +Pan Integral,Panadería,30,45,2,Agua,325,ml +Pan Integral,Panadería,30,45,2,Sal,10,g +Pan Integral,Panadería,30,45,2,Levadura,7,g +Croissant,Bollería,120,20,12,Harina,400,g +Croissant,Bollería,120,20,12,Mantequilla,250,g +Croissant,Bollería,120,20,12,Agua,200,ml +Croissant,Bollería,120,20,12,Sal,8,g`; + fileName = 'plantilla_recetas.csv'; + break; + } + + // Create and download the file + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', fileName); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + // WebSocket connection for ML training updates + const connectWebSocket = () => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + return; + } + + const wsUrl = process.env.NODE_ENV === 'production' + ? 'wss://api.bakeryai.com/ws/training' + : 'ws://localhost:8000/ws/training'; + + wsRef.current = new WebSocket(wsUrl); + + wsRef.current.onopen = () => { + console.log('WebSocket connected for ML training'); + setTrainingState(prev => ({ ...prev, status: 'running' })); + }; + + wsRef.current.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + setTrainingState(prev => ({ + ...prev, + progress: data.progress || prev.progress, + currentStep: data.step || prev.currentStep, + status: data.status || prev.status, + logs: data.log ? [...prev.logs, data.log] : prev.logs + })); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + wsRef.current.onclose = () => { + console.log('WebSocket connection closed'); + }; + + wsRef.current.onerror = (error) => { + console.error('WebSocket error:', error); + setTrainingState(prev => ({ ...prev, status: 'error' })); + }; + }; + + const startTraining = () => { + setTrainingState(prev => ({ ...prev, isTraining: true, progress: 0 })); + connectWebSocket(); + + // Send training configuration to server + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + action: 'start_training', + bakery_data: formData.bakery, + upload_data: formData.upload + })); + } + }; + + // Cleanup WebSocket on unmount + useEffect(() => { + return () => { + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, []); + const isStepComplete = (stepId: number) => { // Basic validation logic switch (stepId) { case 1: return formData.bakery.name && formData.bakery.location; case 2: - return formData.team.ownerName; + return true; // Upload step is now optional - users can skip if they want case 3: - return formData.operations.specialties.length > 0; - case 4: - return formData.upload.useTemplates || formData.upload.skipUpload || - (formData.upload.salesData || formData.upload.inventoryData || formData.upload.recipeData); - case 5: - return formData.goals.primaryGoals.length > 0; + return trainingState.status === 'completed'; default: return false; } @@ -198,52 +267,62 @@ const OnboardingSetupPage: React.FC = () => {
-
+
{bakeryTypes.map((type) => (