From 72b4f60cf5e96bb5f0c707f7a3704a2a0b87178d Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 1 Sep 2025 08:58:15 +0200 Subject: [PATCH] Add frontend POS configuration --- .../bakery-config/BakeryConfigPage.tsx | 574 +++++++++++++++++- 1 file changed, 553 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx b/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx index 16a639c0..0e9a72c9 100644 --- a/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx +++ b/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; -import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3 } from 'lucide-react'; -import { Button, Card, Input, Select } from '../../../../components/ui'; +import React, { useState, useEffect } from 'react'; +import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Settings, Trash2, Wifi, WifiOff, AlertCircle, CheckCircle, Loader, Eye, EyeOff, Info } from 'lucide-react'; +import { Button, Card, Input, Select, Modal, Badge } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; import { useToast } from '../../../../hooks/ui/useToast'; +import { posService, POSConfiguration } from '../../../../services/api/pos.service'; interface BakeryConfig { // General Info @@ -31,12 +32,39 @@ interface BusinessHours { }; } +interface POSProviderConfig { + id: string; + name: string; + logo: string; + description: string; + features: string[]; + required_fields: { + field: string; + label: string; + type: 'text' | 'password' | 'url' | 'select'; + placeholder?: string; + required: boolean; + help_text?: string; + options?: { value: string; label: string }[]; + }[]; +} + const BakeryConfigPage: React.FC = () => { - const { showToast } = useToast(); + const { addToast } = useToast(); - const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours'>('general'); + const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours' | 'pos'>('general'); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); + + // POS Configuration State + const [posConfigurations, setPosConfigurations] = useState([]); + const [posLoading, setPosLoading] = useState(false); + const [showAddPosModal, setShowAddPosModal] = useState(false); + const [showEditPosModal, setShowEditPosModal] = useState(false); + const [selectedPosConfig, setSelectedPosConfig] = useState(null); + const [posFormData, setPosFormData] = useState({}); + const [testingConnection, setTestingConnection] = useState(null); + const [showCredentials, setShowCredentials] = useState>({}); const [config, setConfig] = useState({ name: 'Panadería Artesanal San Miguel', @@ -66,11 +94,67 @@ const BakeryConfigPage: React.FC = () => { const [errors, setErrors] = useState>({}); + // POS Providers Configuration + const supportedProviders: POSProviderConfig[] = [ + { + id: 'toast', + name: 'Toast POS', + logo: '🍞', + description: 'Sistema POS líder para restaurantes y panaderías. Muy popular en España.', + features: ['Gestión de pedidos', 'Sincronización de inventario', 'Pagos integrados', 'Reportes en tiempo real'], + required_fields: [ + { field: 'api_key', label: 'API Key', type: 'password', required: true, help_text: 'Obten tu API key desde Toast Dashboard > Settings > Integrations' }, + { field: 'restaurant_guid', label: 'Restaurant GUID', type: 'text', required: true, help_text: 'ID único del restaurante en Toast' }, + { field: 'location_id', label: 'Location ID', type: 'text', required: true, help_text: 'ID de la ubicación específica' }, + { field: 'environment', label: 'Entorno', type: 'select', required: true, options: [ + { value: 'sandbox', label: 'Sandbox (Pruebas)' }, + { value: 'production', label: 'Producción' } + ]}, + ], + }, + { + id: 'square', + name: 'Square POS', + logo: '⬜', + description: 'Solución POS completa con tarifas transparentes. Ampliamente utilizada por pequeñas empresas.', + features: ['Procesamiento de pagos', 'Gestión de inventario', 'Análisis de ventas', 'Integración con e-commerce'], + required_fields: [ + { field: 'application_id', label: 'Application ID', type: 'text', required: true, help_text: 'ID de aplicación de Square Developer Dashboard' }, + { field: 'access_token', label: 'Access Token', type: 'password', required: true, help_text: 'Token de acceso para la API de Square' }, + { field: 'location_id', label: 'Location ID', type: 'text', required: true, help_text: 'ID de la ubicación de Square' }, + { field: 'webhook_signature_key', label: 'Webhook Signature Key', type: 'password', required: false, help_text: 'Clave para verificar webhooks (opcional)' }, + { field: 'environment', label: 'Entorno', type: 'select', required: true, options: [ + { value: 'sandbox', label: 'Sandbox (Pruebas)' }, + { value: 'production', label: 'Producción' } + ]}, + ], + }, + { + id: 'lightspeed', + name: 'Lightspeed POS', + logo: '⚡', + description: 'Sistema POS empresarial con API abierta e integración con múltiples herramientas.', + features: ['API REST completa', 'Gestión multi-ubicación', 'Reportes avanzados', 'Integración con contabilidad'], + required_fields: [ + { field: 'api_key', label: 'API Key', type: 'password', required: true, help_text: 'Clave API de Lightspeed Retail' }, + { field: 'api_secret', label: 'API Secret', type: 'password', required: true, help_text: 'Secreto API de Lightspeed Retail' }, + { field: 'account_id', label: 'Account ID', type: 'text', required: true, help_text: 'ID de cuenta de Lightspeed' }, + { field: 'shop_id', label: 'Shop ID', type: 'text', required: true, help_text: 'ID de la tienda específica' }, + { field: 'server_region', label: 'Región del Servidor', type: 'select', required: true, options: [ + { value: 'eu', label: 'Europa' }, + { value: 'us', label: 'Estados Unidos' }, + { value: 'ca', label: 'Canadá' } + ]}, + ], + }, + ]; + const tabs = [ { id: 'general' as const, label: 'General', icon: Store }, { id: 'location' as const, label: 'Ubicación', icon: MapPin }, { id: 'business' as const, label: 'Empresa', icon: Globe }, - { id: 'hours' as const, label: 'Horarios', icon: Clock } + { id: 'hours' as const, label: 'Horarios', icon: Clock }, + { id: 'pos' as const, label: 'Sistemas POS', icon: Zap } ]; const daysOfWeek = [ @@ -101,6 +185,29 @@ const BakeryConfigPage: React.FC = () => { { value: 'en', label: 'English' } ]; + // Load POS configurations when POS tab is selected + useEffect(() => { + if (activeTab === 'pos') { + loadPosConfigurations(); + } + }, [activeTab]); + + const loadPosConfigurations = async () => { + try { + setPosLoading(true); + const response = await posService.getPOSConfigs(); + if (response.success) { + setPosConfigurations(response.data); + } else { + addToast('Error al cargar configuraciones POS', 'error'); + } + } catch (error) { + addToast('Error al conectar con el servidor', 'error'); + } finally { + setPosLoading(false); + } + }; + const validateConfig = (): boolean => { const newErrors: Record = {}; @@ -136,17 +243,9 @@ const BakeryConfigPage: React.FC = () => { await new Promise(resolve => setTimeout(resolve, 1000)); setIsEditing(false); - showToast({ - type: 'success', - title: 'Configuración actualizada', - message: 'Los datos de la panadería han sido guardados correctamente' - }); + addToast('Configuración actualizada correctamente', 'success'); } catch (error) { - showToast({ - type: 'error', - title: 'Error', - message: 'No se pudo actualizar la configuración' - }); + addToast('No se pudo actualizar la configuración', 'error'); } finally { setIsLoading(false); } @@ -173,6 +272,277 @@ const BakeryConfigPage: React.FC = () => { })); }; + // POS Configuration Handlers + const handleAddPosConfiguration = () => { + setPosFormData({ + provider: '', + config_name: '', + credentials: {}, + sync_settings: { + auto_sync_enabled: true, + sync_interval_minutes: 5, + sync_sales: true, + sync_inventory: true, + sync_customers: true, + }, + }); + setShowAddPosModal(true); + }; + + const handleEditPosConfiguration = (config: POSConfiguration) => { + setSelectedPosConfig(config); + setPosFormData({ + provider: config.provider, + config_name: config.config_name, + credentials: config.credentials, + sync_settings: config.sync_settings, + }); + setShowEditPosModal(true); + }; + + const handleSavePosConfiguration = async () => { + try { + const provider = supportedProviders.find(p => p.id === posFormData.provider); + if (!provider) return; + + // Validate required fields + const missingFields = provider.required_fields + .filter(field => field.required && !posFormData.credentials[field.field]) + .map(field => field.label); + + if (missingFields.length > 0) { + addToast(`Campos requeridos: ${missingFields.join(', ')}`, 'error'); + return; + } + + if (selectedPosConfig) { + // Update existing + const response = await posService.updatePOSConfig(selectedPosConfig.id, posFormData); + if (response.success) { + addToast('Configuración actualizada correctamente', 'success'); + setShowEditPosModal(false); + loadPosConfigurations(); + } else { + addToast('Error al actualizar la configuración', 'error'); + } + } else { + // Create new + const response = await posService.createPOSConfig(posFormData); + if (response.success) { + addToast('Configuración creada correctamente', 'success'); + setShowAddPosModal(false); + loadPosConfigurations(); + } else { + addToast('Error al crear la configuración', 'error'); + } + } + } catch (error) { + addToast('Error al guardar la configuración', 'error'); + } + }; + + const handleTestPosConnection = async (configId: string) => { + try { + setTestingConnection(configId); + const response = await posService.testPOSConnection(configId); + + if (response.success && response.data.success) { + addToast('Conexión exitosa', 'success'); + } else { + addToast(`Error en la conexión: ${response.data.message}`, 'error'); + } + } catch (error) { + addToast('Error al probar la conexión', 'error'); + } finally { + setTestingConnection(null); + } + }; + + const handleDeletePosConfiguration = async (configId: string) => { + if (!window.confirm('¿Estás seguro de que deseas eliminar esta configuración?')) { + return; + } + + try { + const response = await posService.deletePOSConfig(configId); + if (response.success) { + addToast('Configuración eliminada correctamente', 'success'); + loadPosConfigurations(); + } else { + addToast('Error al eliminar la configuración', 'error'); + } + } catch (error) { + addToast('Error al eliminar la configuración', 'error'); + } + }; + + // POS Configuration Form Renderer + const renderPosConfigurationForm = () => { + const selectedProvider = supportedProviders.find(p => p.id === posFormData.provider); + + return ( +
+
+ + setPosFormData(prev => ({ ...prev, config_name: e.target.value }))} + placeholder={`Mi ${selectedProvider.name} ${new Date().getFullYear()}`} + /> +
+ +
+

Credenciales de API

+ {selectedProvider.required_fields.map(field => ( +
+
+ + {field.type === 'password' && ( + + )} +
+ + {field.type === 'select' ? ( + setPosFormData(prev => ({ + ...prev, + credentials: { ...prev.credentials, [field.field]: e.target.value } + }))} + placeholder={field.placeholder} + /> + )} + + {field.help_text && ( +

+ + {field.help_text} +

+ )} +
+ ))} +
+ +
+

Configuración de Sincronización

+
+
+ +
+
+ + setPosFormData((prev: any) => ({ + ...prev, + sync_settings: { ...prev.sync_settings, sync_sales: e.target.checked } + }))} + /> + Sincronizar ventas + +
+
+ +
+
+
+ + )} +
+ ); + }; + return (
{ label="Moneda" options={currencyOptions} value={config.currency} - onChange={handleSelectChange('currency')} + onChange={(value) => handleSelectChange('currency')(value as string)} disabled={!isEditing || isLoading} /> @@ -365,18 +735,16 @@ const BakeryConfigPage: React.FC = () => { label="Zona Horaria" options={timezoneOptions} value={config.timezone} - onChange={handleSelectChange('timezone')} + onChange={(value) => handleSelectChange('timezone')(value as string)} disabled={!isEditing || isLoading} - leftIcon={} />