Improve teh securty of teh DB
This commit is contained in:
@@ -8,144 +8,7 @@ import { PageHeader } from '../../../../components/layout';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { Equipment } from '../../../../api/types/equipment';
|
||||
import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal';
|
||||
|
||||
const MOCK_EQUIPMENT: Equipment[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Horno Principal #1',
|
||||
type: 'oven',
|
||||
model: 'Miwe Condo CO 4.1212',
|
||||
serialNumber: 'MCO-2021-001',
|
||||
location: 'Área de Horneado - Zona A',
|
||||
status: 'operational',
|
||||
installDate: '2021-03-15',
|
||||
lastMaintenance: '2024-01-15',
|
||||
nextMaintenance: '2024-04-15',
|
||||
maintenanceInterval: 90,
|
||||
temperature: 220,
|
||||
targetTemperature: 220,
|
||||
efficiency: 92,
|
||||
uptime: 98.5,
|
||||
energyUsage: 45.2,
|
||||
utilizationToday: 87,
|
||||
alerts: [],
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-01-15',
|
||||
type: 'preventive',
|
||||
description: 'Limpieza general y calibración de termostatos',
|
||||
technician: 'Juan Pérez',
|
||||
cost: 150,
|
||||
downtime: 2,
|
||||
partsUsed: ['Filtros de aire', 'Sellos de puerta']
|
||||
}
|
||||
],
|
||||
specifications: {
|
||||
power: 45,
|
||||
capacity: 24,
|
||||
dimensions: { width: 200, height: 180, depth: 120 },
|
||||
weight: 850
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Batidora Industrial #2',
|
||||
type: 'mixer',
|
||||
model: 'Hobart HL800',
|
||||
serialNumber: 'HHL-2020-002',
|
||||
location: 'Área de Preparación - Zona B',
|
||||
status: 'warning',
|
||||
installDate: '2020-08-10',
|
||||
lastMaintenance: '2024-01-20',
|
||||
nextMaintenance: '2024-02-20',
|
||||
maintenanceInterval: 30,
|
||||
efficiency: 88,
|
||||
uptime: 94.2,
|
||||
energyUsage: 12.8,
|
||||
utilizationToday: 76,
|
||||
alerts: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'warning',
|
||||
message: 'Vibración inusual detectada en el motor',
|
||||
timestamp: '2024-01-23T10:30:00Z',
|
||||
acknowledged: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'info',
|
||||
message: 'Mantenimiento programado en 5 días',
|
||||
timestamp: '2024-01-23T08:00:00Z',
|
||||
acknowledged: true
|
||||
}
|
||||
],
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-01-20',
|
||||
type: 'corrective',
|
||||
description: 'Reemplazo de correas de transmisión',
|
||||
technician: 'María González',
|
||||
cost: 85,
|
||||
downtime: 4,
|
||||
partsUsed: ['Correa tipo V', 'Rodamientos']
|
||||
}
|
||||
],
|
||||
specifications: {
|
||||
power: 15,
|
||||
capacity: 80,
|
||||
dimensions: { width: 120, height: 150, depth: 80 },
|
||||
weight: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Cámara de Fermentación #1',
|
||||
type: 'proofer',
|
||||
model: 'Bongard EUROPA 16.18',
|
||||
serialNumber: 'BEU-2022-001',
|
||||
location: 'Área de Fermentación',
|
||||
status: 'maintenance',
|
||||
installDate: '2022-06-20',
|
||||
lastMaintenance: '2024-01-23',
|
||||
nextMaintenance: '2024-01-24',
|
||||
maintenanceInterval: 60,
|
||||
temperature: 32,
|
||||
targetTemperature: 35,
|
||||
efficiency: 0,
|
||||
uptime: 85.1,
|
||||
energyUsage: 0,
|
||||
utilizationToday: 0,
|
||||
alerts: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'info',
|
||||
message: 'En mantenimiento programado',
|
||||
timestamp: '2024-01-23T06:00:00Z',
|
||||
acknowledged: true
|
||||
}
|
||||
],
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-01-23',
|
||||
type: 'preventive',
|
||||
description: 'Mantenimiento programado - sistema de humidificación',
|
||||
technician: 'Carlos Rodríguez',
|
||||
cost: 200,
|
||||
downtime: 8,
|
||||
partsUsed: ['Sensor de humedad', 'Válvulas']
|
||||
}
|
||||
],
|
||||
specifications: {
|
||||
power: 8,
|
||||
capacity: 16,
|
||||
dimensions: { width: 180, height: 200, depth: 100 },
|
||||
weight: 450
|
||||
}
|
||||
}
|
||||
];
|
||||
import { useEquipment, useCreateEquipment, useUpdateEquipment } from '../../../../api/hooks/equipment';
|
||||
|
||||
const MaquinariaPage: React.FC = () => {
|
||||
const { t } = useTranslation(['equipment', 'common']);
|
||||
@@ -157,11 +20,19 @@ const MaquinariaPage: React.FC = () => {
|
||||
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
|
||||
const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create');
|
||||
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
|
||||
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
// Mock functions for equipment actions - these would be replaced with actual API calls
|
||||
// Fetch equipment data from API
|
||||
const { data: equipment = [], isLoading, error } = useEquipment(tenantId, {
|
||||
is_active: true
|
||||
});
|
||||
|
||||
// Mutations for create and update
|
||||
const createEquipmentMutation = useCreateEquipment(tenantId);
|
||||
const updateEquipmentMutation = useUpdateEquipment(tenantId);
|
||||
|
||||
const handleCreateEquipment = () => {
|
||||
setSelectedEquipment({
|
||||
id: '',
|
||||
@@ -193,8 +64,8 @@ const MaquinariaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleEditEquipment = (equipmentId: string) => {
|
||||
// Find the equipment to edit
|
||||
const equipmentToEdit = MOCK_EQUIPMENT.find(eq => eq.id === equipmentId);
|
||||
// Find the equipment to edit from real data
|
||||
const equipmentToEdit = equipment.find(eq => eq.id === equipmentId);
|
||||
if (equipmentToEdit) {
|
||||
setSelectedEquipment(equipmentToEdit);
|
||||
setEquipmentModalMode('edit');
|
||||
@@ -217,16 +88,26 @@ const MaquinariaPage: React.FC = () => {
|
||||
// Implementation would go here
|
||||
};
|
||||
|
||||
const handleSaveEquipment = (equipment: Equipment) => {
|
||||
console.log('Saving equipment:', equipment);
|
||||
// In a real implementation, you would save to the API
|
||||
// For now, just close the modal
|
||||
setShowEquipmentModal(false);
|
||||
// Refresh equipment list if needed
|
||||
const handleSaveEquipment = async (equipmentData: Equipment) => {
|
||||
try {
|
||||
if (equipmentModalMode === 'create') {
|
||||
await createEquipmentMutation.mutateAsync(equipmentData);
|
||||
} else if (equipmentModalMode === 'edit' && equipmentData.id) {
|
||||
await updateEquipmentMutation.mutateAsync({
|
||||
equipmentId: equipmentData.id,
|
||||
equipmentData: equipmentData
|
||||
});
|
||||
}
|
||||
setShowEquipmentModal(false);
|
||||
setSelectedEquipment(null);
|
||||
} catch (error) {
|
||||
console.error('Error saving equipment:', error);
|
||||
// Error is already handled by mutation with toast
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEquipment = useMemo(() => {
|
||||
return MOCK_EQUIPMENT.filter(eq => {
|
||||
return equipment.filter(eq => {
|
||||
const matchesSearch = !searchTerm ||
|
||||
eq.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
eq.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -237,15 +118,15 @@ const MaquinariaPage: React.FC = () => {
|
||||
|
||||
return matchesSearch && matchesStatus && matchesType;
|
||||
});
|
||||
}, [MOCK_EQUIPMENT, searchTerm, statusFilter, typeFilter]);
|
||||
}, [equipment, searchTerm, statusFilter, typeFilter]);
|
||||
|
||||
const equipmentStats = useMemo(() => {
|
||||
const total = MOCK_EQUIPMENT.length;
|
||||
const operational = MOCK_EQUIPMENT.filter(e => e.status === 'operational').length;
|
||||
const warning = MOCK_EQUIPMENT.filter(e => e.status === 'warning').length;
|
||||
const maintenance = MOCK_EQUIPMENT.filter(e => e.status === 'maintenance').length;
|
||||
const down = MOCK_EQUIPMENT.filter(e => e.status === 'down').length;
|
||||
const totalAlerts = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
|
||||
const total = equipment.length;
|
||||
const operational = equipment.filter(e => e.status === 'operational').length;
|
||||
const warning = equipment.filter(e => e.status === 'warning').length;
|
||||
const maintenance = equipment.filter(e => e.status === 'maintenance').length;
|
||||
const down = equipment.filter(e => e.status === 'down').length;
|
||||
const totalAlerts = equipment.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
|
||||
|
||||
return {
|
||||
total,
|
||||
@@ -255,7 +136,7 @@ const MaquinariaPage: React.FC = () => {
|
||||
down,
|
||||
totalAlerts
|
||||
};
|
||||
}, [MOCK_EQUIPMENT]);
|
||||
}, [equipment]);
|
||||
|
||||
const getStatusConfig = (status: Equipment['status']) => {
|
||||
const configs = {
|
||||
@@ -320,6 +201,28 @@ const MaquinariaPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-64">
|
||||
<LoadingSpinner text={t('common:loading')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-64">
|
||||
<AlertTriangle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
{t('common:errors.load_error')}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{t('common:errors.try_again')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
||||
import { SubscriptionPricingCards } from '../../../../components/subscription/SubscriptionPricingCards';
|
||||
|
||||
const SubscriptionPage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
@@ -576,144 +577,18 @@ const SubscriptionPage: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* Available Plans */}
|
||||
<Card className="p-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)] flex items-center">
|
||||
<Crown className="w-5 h-5 mr-2 text-yellow-500" />
|
||||
Planes Disponibles
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{Object.entries(availablePlans.plans).map(([planKey, plan]) => {
|
||||
const isCurrentPlan = usageSummary.plan === planKey;
|
||||
const getPlanColor = () => {
|
||||
switch (planKey) {
|
||||
case 'starter': return 'border-blue-500/30 bg-blue-500/5';
|
||||
case 'professional': return 'border-purple-500/30 bg-purple-500/5';
|
||||
case 'enterprise': return 'border-amber-500/30 bg-amber-500/5';
|
||||
default: return 'border-[var(--border-primary)] bg-[var(--bg-secondary)]';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={planKey}
|
||||
className={`relative p-6 ${getPlanColor()} ${
|
||||
isCurrentPlan ? 'ring-2 ring-[var(--color-primary)]' : ''
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="primary" className="px-3 py-1">
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
Más Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-6">
|
||||
<h4 className="text-xl font-bold text-[var(--text-primary)] mb-2">{plan.name}</h4>
|
||||
<div className="text-3xl font-bold text-[var(--color-primary)] mb-1">
|
||||
{subscriptionService.formatPrice(plan.monthly_price)}
|
||||
<span className="text-lg text-[var(--text-secondary)]">/mes</span>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Users className="w-4 h-4 text-[var(--color-primary)]" />
|
||||
<span>{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuarios`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MapPin className="w-4 h-4 text-[var(--color-primary)]" />
|
||||
<span>{plan.max_locations === -1 ? 'Ubicaciones ilimitadas' : `${plan.max_locations} ubicación${plan.max_locations > 1 ? 'es' : ''}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Package className="w-4 h-4 text-[var(--color-primary)]" />
|
||||
<span>{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} productos`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="border-t border-[var(--border-color)] pt-4 mb-6">
|
||||
<h5 className="text-sm font-semibold text-[var(--text-primary)] mb-3 flex items-center">
|
||||
<TrendingUp className="w-4 h-4 mr-2 text-[var(--color-primary)]" />
|
||||
Funcionalidades Incluidas
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
{(() => {
|
||||
const getPlanFeatures = (planKey: string) => {
|
||||
switch (planKey) {
|
||||
case 'starter':
|
||||
return [
|
||||
'✓ Panel de Control Básico',
|
||||
'✓ Gestión de Inventario',
|
||||
'✓ Gestión de Pedidos',
|
||||
'✓ Gestión de Proveedores',
|
||||
'✓ Punto de Venta Básico',
|
||||
'✗ Analytics Avanzados',
|
||||
'✗ Pronósticos IA',
|
||||
'✗ Insights Predictivos'
|
||||
];
|
||||
case 'professional':
|
||||
return [
|
||||
'✓ Panel de Control Avanzado',
|
||||
'✓ Gestión de Inventario Completa',
|
||||
'✓ Analytics de Ventas',
|
||||
'✓ Pronósticos con IA (92% precisión)',
|
||||
'✓ Análisis de Rendimiento',
|
||||
'✓ Optimización de Producción',
|
||||
'✓ Integración POS',
|
||||
'✗ Insights Predictivos Avanzados'
|
||||
];
|
||||
case 'enterprise':
|
||||
return [
|
||||
'✓ Todas las funcionalidades Professional',
|
||||
'✓ Insights Predictivos con IA',
|
||||
'✓ Analytics Multi-ubicación',
|
||||
'✓ Integración ERP',
|
||||
'✓ API Personalizada',
|
||||
'✓ Gestor de Cuenta Dedicado',
|
||||
'✓ Soporte 24/7 Prioritario',
|
||||
'✓ Demo Personalizada'
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return getPlanFeatures(planKey).map((feature, index) => (
|
||||
<div key={index} className={`text-xs flex items-center gap-2 ${
|
||||
feature.startsWith('✓')
|
||||
? 'text-green-600'
|
||||
: 'text-[var(--text-secondary)] opacity-60'
|
||||
}`}>
|
||||
<span>{feature}</span>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCurrentPlan ? (
|
||||
<Badge variant="success" className="w-full justify-center py-2">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Plan Actual
|
||||
</Badge>
|
||||
) : (
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
onClick={() => handleUpgradeClick(planKey)}
|
||||
>
|
||||
{plan.contact_sales ? 'Contactar Ventas' : 'Cambiar Plan'}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
<SubscriptionPricingCards
|
||||
mode="selection"
|
||||
selectedPlan={usageSummary.plan}
|
||||
onPlanSelect={handleUpgradeClick}
|
||||
showPilotBanner={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Invoices Section */}
|
||||
<Card className="p-6">
|
||||
|
||||
Reference in New Issue
Block a user