Add frontend POS configuration
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3 } from 'lucide-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 } from '../../../../components/ui';
|
import { Button, Card, Input, Select, Modal, Badge } from '../../../../components/ui';
|
||||||
import { PageHeader } from '../../../../components/layout';
|
import { PageHeader } from '../../../../components/layout';
|
||||||
import { useToast } from '../../../../hooks/ui/useToast';
|
import { useToast } from '../../../../hooks/ui/useToast';
|
||||||
|
import { posService, POSConfiguration } from '../../../../services/api/pos.service';
|
||||||
|
|
||||||
interface BakeryConfig {
|
interface BakeryConfig {
|
||||||
// General Info
|
// General Info
|
||||||
@@ -31,13 +32,40 @@ interface BusinessHours {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const BakeryConfigPage: React.FC = () => {
|
interface POSProviderConfig {
|
||||||
const { showToast } = useToast();
|
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 [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours'>('general');
|
const BakeryConfigPage: React.FC = () => {
|
||||||
|
const { addToast } = useToast();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours' | 'pos'>('general');
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// POS Configuration State
|
||||||
|
const [posConfigurations, setPosConfigurations] = useState<POSConfiguration[]>([]);
|
||||||
|
const [posLoading, setPosLoading] = useState(false);
|
||||||
|
const [showAddPosModal, setShowAddPosModal] = useState(false);
|
||||||
|
const [showEditPosModal, setShowEditPosModal] = useState(false);
|
||||||
|
const [selectedPosConfig, setSelectedPosConfig] = useState<POSConfiguration | null>(null);
|
||||||
|
const [posFormData, setPosFormData] = useState<any>({});
|
||||||
|
const [testingConnection, setTestingConnection] = useState<string | null>(null);
|
||||||
|
const [showCredentials, setShowCredentials] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const [config, setConfig] = useState<BakeryConfig>({
|
const [config, setConfig] = useState<BakeryConfig>({
|
||||||
name: 'Panadería Artesanal San Miguel',
|
name: 'Panadería Artesanal San Miguel',
|
||||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||||
@@ -66,11 +94,67 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 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 = [
|
const tabs = [
|
||||||
{ id: 'general' as const, label: 'General', icon: Store },
|
{ id: 'general' as const, label: 'General', icon: Store },
|
||||||
{ id: 'location' as const, label: 'Ubicación', icon: MapPin },
|
{ id: 'location' as const, label: 'Ubicación', icon: MapPin },
|
||||||
{ id: 'business' as const, label: 'Empresa', icon: Globe },
|
{ 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 = [
|
const daysOfWeek = [
|
||||||
@@ -101,6 +185,29 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
{ value: 'en', label: 'English' }
|
{ 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 validateConfig = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -136,17 +243,9 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
showToast({
|
addToast('Configuración actualizada correctamente', 'success');
|
||||||
type: 'success',
|
|
||||||
title: 'Configuración actualizada',
|
|
||||||
message: 'Los datos de la panadería han sido guardados correctamente'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast({
|
addToast('No se pudo actualizar la configuración', 'error');
|
||||||
type: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: 'No se pudo actualizar la configuración'
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Sistema POS</label>
|
||||||
|
<Select
|
||||||
|
value={posFormData.provider}
|
||||||
|
onChange={(value) => setPosFormData(prev => ({ ...prev, provider: value as string, credentials: {} }))}
|
||||||
|
placeholder="Selecciona un sistema POS"
|
||||||
|
options={supportedProviders.map(provider => ({
|
||||||
|
value: provider.id,
|
||||||
|
label: `${provider.logo} ${provider.name}`,
|
||||||
|
description: provider.description
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedProvider && (
|
||||||
|
<>
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-900 mb-2">{selectedProvider.name}</h4>
|
||||||
|
<p className="text-sm text-blue-700 mb-3">{selectedProvider.description}</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedProvider.features.map((feature, idx) => (
|
||||||
|
<Badge key={idx} variant="secondary" className="text-xs">
|
||||||
|
{feature}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Nombre de la Configuración</label>
|
||||||
|
<Input
|
||||||
|
value={posFormData.config_name}
|
||||||
|
onChange={(e) => setPosFormData(prev => ({ ...prev, config_name: e.target.value }))}
|
||||||
|
placeholder={`Mi ${selectedProvider.name} ${new Date().getFullYear()}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium text-sm">Credenciales de API</h4>
|
||||||
|
{selectedProvider.required_fields.map(field => (
|
||||||
|
<div key={field.field}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="block text-sm font-medium">
|
||||||
|
{field.label} {field.required && <span className="text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
{field.type === 'password' && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowCredentials(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field.field]: !prev[field.field]
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
{showCredentials[field.field] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{field.type === 'select' ? (
|
||||||
|
<Select
|
||||||
|
value={posFormData.credentials[field.field] || ''}
|
||||||
|
onChange={(value) => setPosFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
credentials: { ...prev.credentials, [field.field]: value }
|
||||||
|
}))}
|
||||||
|
placeholder={`Selecciona ${field.label.toLowerCase()}`}
|
||||||
|
options={field.options?.map(option => ({
|
||||||
|
value: option.value,
|
||||||
|
label: option.label
|
||||||
|
})) || []}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type={field.type === 'password' && !showCredentials[field.field] ? 'password' : 'text'}
|
||||||
|
value={posFormData.credentials[field.field] || ''}
|
||||||
|
onChange={(e) => setPosFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
credentials: { ...prev.credentials, [field.field]: e.target.value }
|
||||||
|
}))}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.help_text && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1 flex items-start">
|
||||||
|
<Info className="w-3 h-3 mt-0.5 mr-1 flex-shrink-0" />
|
||||||
|
{field.help_text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium text-sm">Configuración de Sincronización</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={posFormData.sync_settings?.auto_sync_enabled || false}
|
||||||
|
onChange={(e) => setPosFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
sync_settings: { ...prev.sync_settings, auto_sync_enabled: e.target.checked }
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Sincronización automática</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Intervalo (minutos)</label>
|
||||||
|
<Select
|
||||||
|
value={posFormData.sync_settings?.sync_interval_minutes?.toString() || '5'}
|
||||||
|
onChange={(value) => setPosFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
sync_settings: { ...prev.sync_settings, sync_interval_minutes: parseInt(value as string) }
|
||||||
|
}))}
|
||||||
|
options={[
|
||||||
|
{ value: '5', label: '5 minutos' },
|
||||||
|
{ value: '15', label: '15 minutos' },
|
||||||
|
{ value: '30', label: '30 minutos' },
|
||||||
|
{ value: '60', label: '1 hora' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={posFormData.sync_settings?.sync_sales || false}
|
||||||
|
onChange={(e) => setPosFormData((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
sync_settings: { ...prev.sync_settings, sync_sales: e.target.checked }
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Sincronizar ventas</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={posFormData.sync_settings?.sync_inventory || false}
|
||||||
|
onChange={(e) => setPosFormData((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
sync_settings: { ...prev.sync_settings, sync_inventory: e.target.checked }
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Sincronizar inventario</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -357,7 +727,7 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
label="Moneda"
|
label="Moneda"
|
||||||
options={currencyOptions}
|
options={currencyOptions}
|
||||||
value={config.currency}
|
value={config.currency}
|
||||||
onChange={handleSelectChange('currency')}
|
onChange={(value) => handleSelectChange('currency')(value as string)}
|
||||||
disabled={!isEditing || isLoading}
|
disabled={!isEditing || isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -365,18 +735,16 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
label="Zona Horaria"
|
label="Zona Horaria"
|
||||||
options={timezoneOptions}
|
options={timezoneOptions}
|
||||||
value={config.timezone}
|
value={config.timezone}
|
||||||
onChange={handleSelectChange('timezone')}
|
onChange={(value) => handleSelectChange('timezone')(value as string)}
|
||||||
disabled={!isEditing || isLoading}
|
disabled={!isEditing || isLoading}
|
||||||
leftIcon={<Clock className="w-4 h-4" />}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Idioma"
|
label="Idioma"
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
value={config.language}
|
value={config.language}
|
||||||
onChange={handleSelectChange('language')}
|
onChange={(value) => handleSelectChange('language')(value as string)}
|
||||||
disabled={!isEditing || isLoading}
|
disabled={!isEditing || isLoading}
|
||||||
leftIcon={<Globe className="w-4 h-4" />}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,6 +815,129 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'pos' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary">Sistemas POS</h3>
|
||||||
|
<p className="text-sm text-text-secondary">Configura e integra tus sistemas de punto de venta</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAddPosConfiguration} className="flex items-center">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Agregar Sistema POS
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{posLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<Loader className="w-8 h-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : posConfigurations.length === 0 ? (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Zap className="w-8 h-8 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">No hay sistemas POS configurados</h3>
|
||||||
|
<p className="text-gray-500 mb-4">
|
||||||
|
Configura tu primer sistema POS para comenzar a sincronizar datos de ventas.
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleAddPosConfiguration}>
|
||||||
|
Agregar Sistema POS
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{posConfigurations.map(config => {
|
||||||
|
const provider = supportedProviders.find(p => p.id === config.provider);
|
||||||
|
return (
|
||||||
|
<Card key={config.id} className="p-6">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-2xl mr-3">{provider?.logo || '📊'}</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{config.config_name}</h3>
|
||||||
|
<p className="text-sm text-gray-500">{provider?.name || config.provider}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{config.is_active ? (
|
||||||
|
<Wifi className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="w-4 h-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<Badge variant={config.is_active ? 'success' : 'error'} size="sm">
|
||||||
|
{config.is_active ? 'Activo' : 'Inactivo'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Estado de conexión:</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{config.is_active ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
|
||||||
|
<span className="text-green-600">Conectado</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-500 mr-1" />
|
||||||
|
<span className="text-yellow-600">Desconectado</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.last_sync_at && (
|
||||||
|
<div className="flex justify-between text-sm text-gray-500">
|
||||||
|
<span>Última sincronización:</span>
|
||||||
|
<span>{new Date(config.last_sync_at).toLocaleString('es-ES')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTestPosConnection(config.id)}
|
||||||
|
disabled={testingConnection === config.id}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{testingConnection === config.id ? (
|
||||||
|
<Loader className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wifi className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
Probar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditPosConfiguration(config)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeletePosConfiguration(config.id)}
|
||||||
|
className="text-red-600 hover:text-red-700 border-red-200 hover:border-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save Actions */}
|
{/* Save Actions */}
|
||||||
@@ -474,6 +965,47 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* POS Configuration Modals */}
|
||||||
|
{/* Add Configuration Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showAddPosModal}
|
||||||
|
onClose={() => setShowAddPosModal(false)}
|
||||||
|
title="Agregar Sistema POS"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{renderPosConfigurationForm()}
|
||||||
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||||
|
<Button variant="outline" onClick={() => setShowAddPosModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSavePosConfiguration}>
|
||||||
|
Guardar Configuración
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Edit Configuration Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showEditPosModal}
|
||||||
|
onClose={() => setShowEditPosModal(false)}
|
||||||
|
title="Editar Sistema POS"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{renderPosConfigurationForm()}
|
||||||
|
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||||||
|
<Button variant="outline" onClick={() => setShowEditPosModal(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSavePosConfiguration}>
|
||||||
|
Actualizar Configuración
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user