Add new API in the frontend
This commit is contained in:
@@ -3,7 +3,11 @@ import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Se
|
||||
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 '../../../../api/services/pos.service';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration, POSProviderConfig } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
import { useTenant, useUpdateTenant } from '../../../../api/hooks/tenant';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
|
||||
interface BakeryConfig {
|
||||
// General Info
|
||||
@@ -32,33 +36,25 @@ 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 { addToast } = useToast();
|
||||
const user = useAuthUser();
|
||||
const tenantId = user?.tenant_id || '';
|
||||
|
||||
const { data: tenant, isLoading: tenantLoading, error: tenantError } = useTenant(tenantId, { enabled: !!tenantId });
|
||||
|
||||
const updateTenantMutation = useUpdateTenant();
|
||||
|
||||
// POS Configuration hooks
|
||||
const posData = usePOSConfigurationData(tenantId);
|
||||
const posManager = usePOSConfigurationManager(tenantId);
|
||||
|
||||
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<POSConfiguration[]>([]);
|
||||
const [posLoading, setPosLoading] = useState(false);
|
||||
const [showAddPosModal, setShowAddPosModal] = useState(false);
|
||||
const [showEditPosModal, setShowEditPosModal] = useState(false);
|
||||
const [selectedPosConfig, setSelectedPosConfig] = useState<POSConfiguration | null>(null);
|
||||
@@ -67,20 +63,41 @@ const BakeryConfigPage: React.FC = () => {
|
||||
const [showCredentials, setShowCredentials] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [config, setConfig] = useState<BakeryConfig>({
|
||||
name: 'Panadería Artesanal San Miguel',
|
||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||
email: 'info@panaderiasanmiguel.com',
|
||||
phone: '+34 912 345 678',
|
||||
website: 'https://panaderiasanmiguel.com',
|
||||
address: 'Calle Mayor 123',
|
||||
city: 'Madrid',
|
||||
postalCode: '28001',
|
||||
country: 'España',
|
||||
taxId: 'B12345678',
|
||||
name: '',
|
||||
description: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
website: '',
|
||||
address: '',
|
||||
city: '',
|
||||
postalCode: '',
|
||||
country: '',
|
||||
taxId: '',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es'
|
||||
});
|
||||
|
||||
// Update config when tenant data is loaded
|
||||
React.useEffect(() => {
|
||||
if (tenant) {
|
||||
setConfig({
|
||||
name: tenant.name || '',
|
||||
description: tenant.description || '',
|
||||
email: tenant.email || '', // Fixed: use email instead of contact_email
|
||||
phone: tenant.phone || '', // Fixed: use phone instead of contact_phone
|
||||
website: tenant.website || '',
|
||||
address: tenant.address || '',
|
||||
city: tenant.city || '',
|
||||
postalCode: tenant.postal_code || '',
|
||||
country: tenant.country || '',
|
||||
taxId: '', // Not supported by backend yet
|
||||
currency: 'EUR', // Default value
|
||||
timezone: 'Europe/Madrid', // Default value
|
||||
language: 'es' // Default value
|
||||
});
|
||||
}
|
||||
}, [tenant]);
|
||||
|
||||
const [businessHours, setBusinessHours] = useState<BusinessHours>({
|
||||
monday: { open: '07:00', close: '20:00', closed: false },
|
||||
@@ -185,27 +202,10 @@ 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);
|
||||
}
|
||||
// Load POS configurations function for refetching after updates
|
||||
const loadPosConfigurations = () => {
|
||||
// This will trigger a refetch of POS configurations
|
||||
posManager.refetch();
|
||||
};
|
||||
|
||||
const validateConfig = (): boolean => {
|
||||
@@ -234,13 +234,26 @@ const BakeryConfigPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!validateConfig()) return;
|
||||
if (!validateConfig() || !tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await updateTenantMutation.mutateAsync({
|
||||
tenantId,
|
||||
updateData: {
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
email: config.email, // Fixed: use email instead of contact_email
|
||||
phone: config.phone, // Fixed: use phone instead of contact_phone
|
||||
website: config.website,
|
||||
address: config.address,
|
||||
city: config.city,
|
||||
postal_code: config.postalCode,
|
||||
country: config.country
|
||||
// Note: tax_id, currency, timezone, language might not be supported by backend
|
||||
}
|
||||
});
|
||||
|
||||
setIsEditing(false);
|
||||
addToast('Configuración actualizada correctamente', 'success');
|
||||
@@ -317,24 +330,23 @@ const BakeryConfigPage: React.FC = () => {
|
||||
|
||||
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');
|
||||
}
|
||||
await posService.updatePOSConfiguration({
|
||||
tenant_id: tenantId,
|
||||
config_id: selectedPosConfig.id,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración actualizada correctamente', 'success');
|
||||
setShowEditPosModal(false);
|
||||
loadPosConfigurations();
|
||||
} 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');
|
||||
}
|
||||
await posService.createPOSConfiguration({
|
||||
tenant_id: tenantId,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración creada correctamente', 'success');
|
||||
setShowAddPosModal(false);
|
||||
loadPosConfigurations();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al guardar la configuración', 'error');
|
||||
@@ -344,12 +356,15 @@ const BakeryConfigPage: React.FC = () => {
|
||||
const handleTestPosConnection = async (configId: string) => {
|
||||
try {
|
||||
setTestingConnection(configId);
|
||||
const response = await posService.testPOSConnection(configId);
|
||||
const response = await posService.testPOSConnection({
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
|
||||
if (response.success && response.data.success) {
|
||||
if (response.success) {
|
||||
addToast('Conexión exitosa', 'success');
|
||||
} else {
|
||||
addToast(`Error en la conexión: ${response.data.message}`, 'error');
|
||||
addToast(`Error en la conexión: ${response.message || 'Error desconocido'}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al probar la conexión', 'error');
|
||||
@@ -364,13 +379,12 @@ const BakeryConfigPage: React.FC = () => {
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
await posService.deletePOSConfiguration({
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
addToast('Configuración eliminada correctamente', 'success');
|
||||
loadPosConfigurations();
|
||||
} catch (error) {
|
||||
addToast('Error al eliminar la configuración', 'error');
|
||||
}
|
||||
@@ -543,6 +557,37 @@ const BakeryConfigPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
if (tenantLoading) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración de Panadería"
|
||||
description="Configurando datos básicos y preferencias de tu panadería"
|
||||
/>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader className="w-8 h-8 animate-spin" />
|
||||
<span className="ml-2">Cargando configuración...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tenantError) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración de Panadería"
|
||||
description="Error al cargar la configuración"
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="text-red-600">
|
||||
Error al cargar la configuración: {tenantError.message}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
@@ -829,11 +874,11 @@ const BakeryConfigPage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{posLoading ? (
|
||||
{posData.isLoading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loader className="w-8 h-8 animate-spin" />
|
||||
</div>
|
||||
) : posConfigurations.length === 0 ? (
|
||||
) : posData.configurations.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" />
|
||||
@@ -848,20 +893,20 @@ const BakeryConfigPage: React.FC = () => {
|
||||
</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);
|
||||
{posData.configurations.map(config => {
|
||||
const provider = posData.supportedSystems.find(p => p.id === config.pos_system);
|
||||
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 className="text-2xl mr-3">📊</div>
|
||||
<div>
|
||||
<h3 className="font-medium">{config.config_name}</h3>
|
||||
<p className="text-sm text-gray-500">{provider?.name || config.provider}</p>
|
||||
<h3 className="font-medium">{config.provider_name}</h3>
|
||||
<p className="text-sm text-gray-500">{provider?.name || config.pos_system}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{config.is_active ? (
|
||||
{config.is_connected ? (
|
||||
<Wifi className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<WifiOff className="w-4 h-4 text-red-500" />
|
||||
|
||||
@@ -2,8 +2,14 @@ import React, { useState } from 'react';
|
||||
import { Settings, Bell, Mail, MessageSquare, Smartphone, Save, RotateCcw } from 'lucide-react';
|
||||
import { Button, Card } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthProfile, useUpdateProfile } from '../../../../api/hooks/auth';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
|
||||
const PreferencesPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const { data: profile, isLoading: profileLoading } = useAuthProfile();
|
||||
const updateProfileMutation = useUpdateProfile();
|
||||
|
||||
const [preferences, setPreferences] = useState({
|
||||
notifications: {
|
||||
inventory: {
|
||||
@@ -50,12 +56,31 @@ const PreferencesPage: React.FC = () => {
|
||||
vibrationEnabled: true
|
||||
},
|
||||
channels: {
|
||||
email: 'panaderia@example.com',
|
||||
phone: '+34 600 123 456',
|
||||
email: profile?.email || '',
|
||||
phone: profile?.phone || '',
|
||||
slack: false,
|
||||
webhook: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Update preferences when profile loads
|
||||
React.useEffect(() => {
|
||||
if (profile) {
|
||||
setPreferences(prev => ({
|
||||
...prev,
|
||||
global: {
|
||||
...prev.global,
|
||||
language: profile.language || 'es',
|
||||
timezone: profile.timezone || 'Europe/Madrid'
|
||||
},
|
||||
channels: {
|
||||
...prev.channels,
|
||||
email: profile.email || '',
|
||||
phone: profile.phone || ''
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
@@ -149,14 +174,49 @@ const PreferencesPage: React.FC = () => {
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Handle save logic
|
||||
console.log('Saving preferences:', preferences);
|
||||
setHasChanges(false);
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// Save notification preferences and contact info
|
||||
await updateProfileMutation.mutateAsync({
|
||||
language: preferences.global.language,
|
||||
timezone: preferences.global.timezone,
|
||||
phone: preferences.channels.phone,
|
||||
notification_preferences: preferences.notifications
|
||||
});
|
||||
|
||||
addToast('Preferencias guardadas correctamente', 'success');
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
addToast('Error al guardar las preferencias', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset to defaults
|
||||
if (profile) {
|
||||
setPreferences({
|
||||
notifications: {
|
||||
inventory: { app: true, email: false, sms: true, frequency: 'immediate' },
|
||||
sales: { app: true, email: true, sms: false, frequency: 'hourly' },
|
||||
production: { app: true, email: false, sms: true, frequency: 'immediate' },
|
||||
system: { app: true, email: true, sms: false, frequency: 'daily' },
|
||||
marketing: { app: false, email: true, sms: false, frequency: 'weekly' }
|
||||
},
|
||||
global: {
|
||||
doNotDisturb: false,
|
||||
quietHours: { enabled: false, start: '22:00', end: '07:00' },
|
||||
language: profile.language || 'es',
|
||||
timezone: profile.timezone || 'Europe/Madrid',
|
||||
soundEnabled: true,
|
||||
vibrationEnabled: true
|
||||
},
|
||||
channels: {
|
||||
email: profile.email || '',
|
||||
phone: profile.phone || '',
|
||||
slack: false,
|
||||
webhook: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
|
||||
interface ProfileFormData {
|
||||
first_name: string;
|
||||
@@ -22,20 +23,38 @@ interface PasswordData {
|
||||
|
||||
const ProfilePage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
const { showToast } = useToast();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
|
||||
const updateProfileMutation = useUpdateProfile();
|
||||
const changePasswordMutation = useChangePassword();
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
|
||||
const [profileData, setProfileData] = useState<ProfileFormData>({
|
||||
first_name: 'María',
|
||||
last_name: 'González Pérez',
|
||||
email: 'admin@bakery.com',
|
||||
phone: '+34 612 345 678',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid'
|
||||
});
|
||||
|
||||
// Update profile data when profile is loaded
|
||||
React.useEffect(() => {
|
||||
if (profile) {
|
||||
setProfileData({
|
||||
first_name: profile.first_name || '',
|
||||
last_name: profile.last_name || '',
|
||||
email: profile.email || '',
|
||||
phone: profile.phone || '',
|
||||
language: profile.language || 'es',
|
||||
timezone: profile.timezone || 'Europe/Madrid'
|
||||
});
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
const [passwordData, setPasswordData] = useState<PasswordData>({
|
||||
currentPassword: '',
|
||||
@@ -105,48 +124,34 @@ const ProfilePage: React.FC = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await updateProfileMutation.mutateAsync(profileData);
|
||||
|
||||
setIsEditing(false);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Perfil actualizado',
|
||||
message: 'Tu información ha sido guardada correctamente'
|
||||
});
|
||||
addToast('Perfil actualizado correctamente', 'success');
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'No se pudo actualizar tu perfil'
|
||||
});
|
||||
addToast('No se pudo actualizar tu perfil', 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
const handleChangePasswordSubmit = async () => {
|
||||
if (!validatePassword()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await changePasswordMutation.mutateAsync({
|
||||
current_password: passwordData.currentPassword,
|
||||
new_password: passwordData.newPassword,
|
||||
confirm_password: passwordData.confirmPassword
|
||||
});
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Contraseña actualizada',
|
||||
message: 'Tu contraseña ha sido cambiada correctamente'
|
||||
});
|
||||
addToast('Contraseña actualizada correctamente', 'success');
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'No se pudo cambiar tu contraseña'
|
||||
});
|
||||
addToast('No se pudo cambiar tu contraseña', 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -182,7 +187,7 @@ const ProfilePage: React.FC = () => {
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src="https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face"
|
||||
src={profile?.avatar_url}
|
||||
name={`${profileData.first_name} ${profileData.last_name}`}
|
||||
size="xl"
|
||||
className="w-20 h-20"
|
||||
@@ -362,7 +367,7 @@ const ProfilePage: React.FC = () => {
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleChangePassword}
|
||||
onClick={handleChangePasswordSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Cambiando..."
|
||||
>
|
||||
|
||||
@@ -37,8 +37,7 @@ import {
|
||||
subscriptionService,
|
||||
type UsageSummary,
|
||||
type AvailablePlans
|
||||
} from '../../../../api/services';
|
||||
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
|
||||
} from '../../../../api';
|
||||
|
||||
interface PlanComparisonProps {
|
||||
plans: AvailablePlans['plans'];
|
||||
@@ -249,7 +248,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const [upgrading, setUpgrading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTenant?.id || user?.tenant_id || isMockMode()) {
|
||||
if (currentTenant?.id || user?.tenant_id) {
|
||||
loadSubscriptionData();
|
||||
}
|
||||
}, [currentTenant, user?.tenant_id]);
|
||||
@@ -257,15 +256,10 @@ const SubscriptionPage: React.FC = () => {
|
||||
const loadSubscriptionData = async () => {
|
||||
let tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
// In mock mode, use the mock tenant ID if no real tenant is available
|
||||
if (isMockMode() && !tenantId) {
|
||||
tenantId = getMockSubscription().tenant_id;
|
||||
console.log('🧪 Mock mode: Using mock tenant ID:', tenantId);
|
||||
if (!tenantId) {
|
||||
toast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 Loading subscription data for tenant:', tenantId, '| Mock mode:', isMockMode());
|
||||
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -292,12 +286,10 @@ const SubscriptionPage: React.FC = () => {
|
||||
const handleUpgradeConfirm = async () => {
|
||||
let tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
// In mock mode, use the mock tenant ID if no real tenant is available
|
||||
if (isMockMode() && !tenantId) {
|
||||
tenantId = getMockSubscription().tenant_id;
|
||||
if (!tenantId || !selectedPlan) {
|
||||
toast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tenantId || !selectedPlan) return;
|
||||
|
||||
try {
|
||||
setUpgrading(true);
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
type UsageSummary,
|
||||
type AvailablePlans
|
||||
} from '../../../../api/services';
|
||||
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
|
||||
|
||||
interface PlanComparisonProps {
|
||||
plans: AvailablePlans['plans'];
|
||||
@@ -297,7 +296,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const [upgrading, setUpgrading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTenant?.id || tenant_id || isMockMode()) {
|
||||
if (currentTenant?.id || tenant_id) {
|
||||
loadSubscriptionData();
|
||||
}
|
||||
}, [currentTenant, tenant_id]);
|
||||
@@ -305,13 +304,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const loadSubscriptionData = async () => {
|
||||
let tenantId = currentTenant?.id || tenant_id;
|
||||
|
||||
// In mock mode, use the mock tenant ID if no real tenant is available
|
||||
if (isMockMode() && !tenantId) {
|
||||
tenantId = getMockSubscription().tenant_id;
|
||||
console.log('🧪 Mock mode: Using mock tenant ID:', tenantId);
|
||||
}
|
||||
|
||||
console.log('📊 Loading subscription data for tenant:', tenantId, '| Mock mode:', isMockMode());
|
||||
console.log('📊 Loading subscription data for tenant:', tenantId);
|
||||
|
||||
if (!tenantId) return;
|
||||
|
||||
@@ -340,11 +333,6 @@ const SubscriptionPage: React.FC = () => {
|
||||
const handleUpgradeConfirm = async () => {
|
||||
let tenantId = currentTenant?.id || tenant_id;
|
||||
|
||||
// In mock mode, use the mock tenant ID if no real tenant is available
|
||||
if (isMockMode() && !tenantId) {
|
||||
tenantId = getMockSubscription().tenant_id;
|
||||
}
|
||||
|
||||
if (!tenantId || !selectedPlan) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,131 +2,31 @@ import React, { useState } from 'react';
|
||||
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useTeamMembers } from '../../../../api/hooks/tenant';
|
||||
import { useAllUsers, useUpdateUser } from '../../../../api/hooks/user';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const user = useAuthUser();
|
||||
const tenantId = user?.tenant_id || '';
|
||||
|
||||
const { data: teamMembers = [], isLoading, error } = useTeamMembers(tenantId, true, { enabled: !!tenantId });
|
||||
const { data: allUsers = [] } = useAllUsers();
|
||||
const updateUserMutation = useUpdateUser();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedRole, setSelectedRole] = useState('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const teamMembers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'María González',
|
||||
email: 'maria.gonzalez@panaderia.com',
|
||||
phone: '+34 600 123 456',
|
||||
role: 'manager',
|
||||
department: 'Administración',
|
||||
status: 'active',
|
||||
joinDate: '2022-03-15',
|
||||
lastLogin: '2024-01-26 09:30:00',
|
||||
permissions: ['inventory', 'sales', 'reports', 'team'],
|
||||
avatar: '/avatars/maria.jpg',
|
||||
schedule: {
|
||||
monday: '07:00-15:00',
|
||||
tuesday: '07:00-15:00',
|
||||
wednesday: '07:00-15:00',
|
||||
thursday: '07:00-15:00',
|
||||
friday: '07:00-15:00',
|
||||
saturday: 'Libre',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Carlos Rodríguez',
|
||||
email: 'carlos.rodriguez@panaderia.com',
|
||||
phone: '+34 600 234 567',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'active',
|
||||
joinDate: '2021-09-20',
|
||||
lastLogin: '2024-01-26 08:45:00',
|
||||
permissions: ['production', 'inventory'],
|
||||
avatar: '/avatars/carlos.jpg',
|
||||
schedule: {
|
||||
monday: '05:00-13:00',
|
||||
tuesday: '05:00-13:00',
|
||||
wednesday: '05:00-13:00',
|
||||
thursday: '05:00-13:00',
|
||||
friday: '05:00-13:00',
|
||||
saturday: '05:00-11:00',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Ana Martínez',
|
||||
email: 'ana.martinez@panaderia.com',
|
||||
phone: '+34 600 345 678',
|
||||
role: 'cashier',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-01-10',
|
||||
lastLogin: '2024-01-26 10:15:00',
|
||||
permissions: ['sales', 'pos'],
|
||||
avatar: '/avatars/ana.jpg',
|
||||
schedule: {
|
||||
monday: '08:00-16:00',
|
||||
tuesday: '08:00-16:00',
|
||||
wednesday: 'Libre',
|
||||
thursday: '08:00-16:00',
|
||||
friday: '08:00-16:00',
|
||||
saturday: '09:00-14:00',
|
||||
sunday: '09:00-14:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Luis Fernández',
|
||||
email: 'luis.fernandez@panaderia.com',
|
||||
phone: '+34 600 456 789',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'inactive',
|
||||
joinDate: '2020-11-05',
|
||||
lastLogin: '2024-01-20 16:30:00',
|
||||
permissions: ['production'],
|
||||
avatar: '/avatars/luis.jpg',
|
||||
schedule: {
|
||||
monday: '13:00-21:00',
|
||||
tuesday: '13:00-21:00',
|
||||
wednesday: '13:00-21:00',
|
||||
thursday: 'Libre',
|
||||
friday: '13:00-21:00',
|
||||
saturday: 'Libre',
|
||||
sunday: '13:00-21:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Isabel Torres',
|
||||
email: 'isabel.torres@panaderia.com',
|
||||
phone: '+34 600 567 890',
|
||||
role: 'assistant',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-06-01',
|
||||
lastLogin: '2024-01-25 18:20:00',
|
||||
permissions: ['sales'],
|
||||
avatar: '/avatars/isabel.jpg',
|
||||
schedule: {
|
||||
monday: 'Libre',
|
||||
tuesday: '16:00-20:00',
|
||||
wednesday: '16:00-20:00',
|
||||
thursday: '16:00-20:00',
|
||||
friday: '16:00-20:00',
|
||||
saturday: '14:00-20:00',
|
||||
sunday: '14:00-20:00'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const roles = [
|
||||
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
|
||||
{ value: 'owner', label: 'Propietario', count: teamMembers.filter(m => m.role === 'owner').length },
|
||||
{ value: 'admin', label: 'Administrador', count: teamMembers.filter(m => m.role === 'admin').length },
|
||||
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
|
||||
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
|
||||
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
|
||||
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
|
||||
{ value: 'employee', label: 'Empleado', count: teamMembers.filter(m => m.role === 'employee').length }
|
||||
];
|
||||
|
||||
const teamStats = {
|
||||
@@ -165,10 +65,28 @@ const TeamPage: React.FC = () => {
|
||||
|
||||
const filteredMembers = teamMembers.filter(member => {
|
||||
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
||||
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesSearch = member.user.first_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
member.user.last_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
member.user.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return matchesRole && matchesSearch;
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Equipo"
|
||||
description="Administra los miembros del equipo, roles y permisos"
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p>Cargando miembros del equipo...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatLastLogin = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -293,7 +211,7 @@ const TeamPage: React.FC = () => {
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.name}</h3>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.user.first_name} {member.user.last_name}</h3>
|
||||
<Badge variant={getStatusColor(member.status)}>
|
||||
{member.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||
</Badge>
|
||||
@@ -302,11 +220,11 @@ const TeamPage: React.FC = () => {
|
||||
<div className="space-y-1 mb-3">
|
||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
{member.email}
|
||||
{member.user.email}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
{member.phone}
|
||||
{member.user.phone || 'No disponible'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -320,35 +238,24 @@ const TeamPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-[var(--text-tertiary)] mb-3">
|
||||
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
|
||||
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
|
||||
<p>Se unió: {new Date(member.joined_at).toLocaleDateString('es-ES')}</p>
|
||||
<p>Estado: {member.is_active ? 'Activo' : 'Inactivo'}</p>
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">Permisos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{member.permissions.map((permission, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full"
|
||||
>
|
||||
{permission}
|
||||
</span>
|
||||
))}
|
||||
<span className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full">
|
||||
{getRoleLabel(member.role)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule Preview */}
|
||||
{/* Member Info */}
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
<p className="font-medium mb-1">Horario esta semana:</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
|
||||
<span key={day}>
|
||||
{day.charAt(0).toUpperCase()}: {hours}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="font-medium mb-1">Información adicional:</p>
|
||||
<p>ID: {member.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user