Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -177,21 +177,13 @@ const DashboardPage: React.FC = () => {
navigate('/app/operations/procurement');
};
// Build stats from real API data
// Build stats from real API data (Sales analytics removed - Professional/Enterprise tier only)
const criticalStats = React.useMemo(() => {
if (!dashboardStats) {
// Return loading/empty state
return [];
}
// Format currency values
const formatCurrency = (value: number): string => {
return `${dashboardStats.salesCurrency}${value.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})}`;
};
// Determine trend direction
const getTrendDirection = (value: number): 'up' | 'down' | 'neutral' => {
if (value > 0) return 'up';
@@ -199,33 +191,7 @@ const DashboardPage: React.FC = () => {
return 'neutral';
};
// Build subtitle for sales
const salesChange = dashboardStats.salesToday * (dashboardStats.salesTrend / 100);
const salesSubtitle = salesChange > 0
? `+${formatCurrency(salesChange)} ${t('dashboard:messages.more_than_yesterday', 'more than yesterday')}`
: salesChange < 0
? `${formatCurrency(Math.abs(salesChange))} ${t('dashboard:messages.less_than_yesterday', 'less than yesterday')}`
: t('dashboard:messages.same_as_yesterday', 'Same as yesterday');
// Build subtitle for products
const productsChange = Math.round(dashboardStats.productsSoldToday * (dashboardStats.productsSoldTrend / 100));
const productsSubtitle = productsChange !== 0
? `${productsChange > 0 ? '+' : ''}${productsChange} ${t('dashboard:messages.more_units', 'units')}`
: t('dashboard:messages.same_as_yesterday', 'Same as yesterday');
return [
{
title: t('dashboard:stats.sales_today', 'Sales Today'),
value: formatCurrency(dashboardStats.salesToday),
icon: Euro,
variant: 'success' as const,
trend: {
value: Math.abs(dashboardStats.salesTrend),
direction: getTrendDirection(dashboardStats.salesTrend),
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
subtitle: salesSubtitle
},
{
title: t('dashboard:stats.pending_orders', 'Pending Orders'),
value: dashboardStats.pendingOrders.toString(),
@@ -240,18 +206,6 @@ const DashboardPage: React.FC = () => {
? t('dashboard:messages.require_attention', 'Require attention')
: t('dashboard:messages.all_caught_up', 'All caught up!')
},
{
title: t('dashboard:stats.products_sold', 'Products Sold'),
value: dashboardStats.productsSoldToday.toString(),
icon: Package,
variant: 'info' as const,
trend: dashboardStats.productsSoldTrend !== 0 ? {
value: Math.abs(dashboardStats.productsSoldTrend),
direction: getTrendDirection(dashboardStats.productsSoldTrend),
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
} : undefined,
subtitle: productsSubtitle
},
{
title: t('dashboard:stats.stock_alerts', 'Critical Stock'),
value: dashboardStats.criticalStock.toString(),
@@ -406,8 +360,8 @@ const DashboardPage: React.FC = () => {
{/* Critical Metrics using StatsGrid */}
<div data-tour="dashboard-stats">
{isLoadingStats ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 mb-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="h-32 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg animate-pulse"
@@ -423,7 +377,7 @@ const DashboardPage: React.FC = () => {
) : (
<StatsGrid
stats={criticalStats}
columns={6}
columns={4}
gap="lg"
className="mb-6"
/>

View File

@@ -13,7 +13,7 @@ import {
useModelPerformance,
useTenantTrainingStatistics
} from '../../../../api/hooks/training';
import { ModelDetailsModal } from '../../../../components/domain/forecasting';
import { ModelDetailsModal, RetrainModelModal } from '../../../../components/domain/forecasting';
import type { IngredientResponse } from '../../../../api/types/inventory';
import type { TrainedModelResponse, SingleProductTrainingRequest } from '../../../../api/types/training';
@@ -47,6 +47,7 @@ const ModelsConfigPage: React.FC = () => {
const [selectedIngredient, setSelectedIngredient] = useState<IngredientResponse | null>(null);
const [selectedModel, setSelectedModel] = useState<TrainedModelResponse | null>(null);
const [showTrainingModal, setShowTrainingModal] = useState(false);
const [showRetrainModal, setShowRetrainModal] = useState(false);
const [showModelDetailsModal, setShowModelDetailsModal] = useState(false);
const [trainingSettings, setTrainingSettings] = useState<Partial<SingleProductTrainingRequest>>({
seasonality_mode: 'additive',
@@ -183,9 +184,38 @@ const ModelsConfigPage: React.FC = () => {
setShowTrainingModal(true);
};
const handleStartRetraining = (ingredient: IngredientResponse) => {
setSelectedIngredient(ingredient);
// Find and set the model for this ingredient
const model = modelStatuses.find(status => status.ingredient.id === ingredient.id)?.model;
if (model) {
setSelectedModel(model);
}
setShowRetrainModal(true);
};
const handleRetrain = async (settings: SingleProductTrainingRequest) => {
if (!selectedIngredient) return;
try {
await trainMutation.mutateAsync({
tenantId,
inventoryProductId: selectedIngredient.id,
request: settings
});
addToast(`Reentrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
setShowRetrainModal(false);
setSelectedIngredient(null);
setSelectedModel(null);
} catch (error) {
addToast('Error al reentrenar el modelo', { type: 'error' });
}
};
if (ingredientsLoading || modelsLoading) {
return (
@@ -238,7 +268,7 @@ const ModelsConfigPage: React.FC = () => {
},
{
title: 'Precisión Promedio',
value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${Number(statistics.average_accuracy).toFixed(1)}%` : 'N/A'),
value: statsError ? 'N/A' : (statistics?.models?.average_accuracy !== undefined && statistics?.models?.average_accuracy !== null ? `${Number(statistics.models.average_accuracy).toFixed(1)}%` : 'N/A'),
icon: TrendingUp,
variant: 'success',
},
@@ -354,7 +384,7 @@ const ModelsConfigPage: React.FC = () => {
...(status.hasModel ? [{
label: 'Reentrenar',
icon: RotateCcw,
onClick: () => handleStartTraining(status.ingredient),
onClick: () => handleStartRetraining(status.ingredient),
priority: 'secondary' as const
}] : [])
]}
@@ -451,6 +481,22 @@ const ModelsConfigPage: React.FC = () => {
model={selectedModel}
/>
)}
{/* Retrain Model Modal */}
{selectedIngredient && (
<RetrainModelModal
isOpen={showRetrainModal}
onClose={() => {
setShowRetrainModal(false);
setSelectedIngredient(null);
setSelectedModel(null);
}}
ingredient={selectedIngredient}
currentModel={selectedModel}
onRetrain={handleRetrain}
isLoading={trainMutation.isPending}
/>
)}
</div>
);
};

View File

@@ -11,11 +11,10 @@ import {
Calendar,
Download,
FileText,
Info,
HelpCircle
Info
} from 'lucide-react';
import { PageHeader } from '../../../../components/layout';
import { StatsGrid, Button, Card, Tooltip } from '../../../../components/ui';
import { StatsGrid, Button, Card } from '../../../../components/ui';
import { LoadingSpinner } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { useSustainabilityMetrics } from '../../../../api/hooks/sustainability';
@@ -146,6 +145,76 @@ const SustainabilityPage: React.FC = () => {
);
}
// Check if we have insufficient data
if (metrics.data_sufficient === false) {
return (
<div className="space-y-6 p-4 sm:p-6">
<PageHeader
title={t('sustainability:page.title', 'Sostenibilidad')}
description={t('sustainability:page.description', 'Seguimiento de impacto ambiental y cumplimiento SDG 12.3')}
/>
<Card className="p-8">
<div className="text-center py-12 max-w-2xl mx-auto">
<div className="mb-6 inline-flex items-center justify-center w-20 h-20 bg-blue-500/10 rounded-full">
<Info className="w-10 h-10 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
{t('sustainability:insufficient_data.title', 'Collecting Sustainability Data')}
</h3>
<p className="text-base text-[var(--text-secondary)] mb-6">
{t('sustainability:insufficient_data.description',
'Start producing batches to see your sustainability metrics and SDG compliance status.'
)}
</p>
<div className="bg-[var(--bg-secondary)] rounded-lg p-6 mb-6">
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('sustainability:insufficient_data.requirements_title', 'Minimum Requirements')}
</h4>
<ul className="text-sm text-[var(--text-secondary)] space-y-2 text-left max-w-md mx-auto">
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_production',
'At least 50kg of production over the analysis period'
)}
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_baseline',
'90 days of production history for accurate baseline calculation'
)}
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5"></span>
<span>
{t('sustainability:insufficient_data.req_tracking',
'Production batches with waste tracking enabled'
)}
</span>
</li>
</ul>
</div>
<div className="flex items-center justify-center gap-2 text-sm text-[var(--text-tertiary)]">
<Calendar className="w-4 h-4" />
<span>
{t('sustainability:insufficient_data.current_production',
'Current production: {{production}}kg of {{required}}kg minimum',
{
production: metrics.current_production_kg?.toFixed(1) || '0.0',
required: metrics.minimum_production_required_kg || 50
}
)}
</span>
</div>
</div>
</Card>
</div>
);
}
return (
<div className="space-y-6 p-4 sm:p-6">
{/* Page Header */}
@@ -180,14 +249,9 @@ const SustainabilityPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.waste_analytics', 'Análisis de Residuos')}
</h3>
<Tooltip content={t('sustainability:tooltips.waste_analytics', 'Información detallada sobre los residuos generados en la producción')}>
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
</Tooltip>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.waste_analytics', 'Análisis de Residuos')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.waste_subtitle', 'Desglose de residuos por tipo')}
</p>
@@ -254,14 +318,9 @@ const SustainabilityPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.environmental_impact', 'Impacto Ambiental')}
</h3>
<Tooltip content={t('sustainability:tooltips.environmental_impact', 'Métricas de huella ambiental y su equivalencia en términos cotidianos')}>
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
</Tooltip>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.environmental_impact', 'Impacto Ambiental')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.environmental_subtitle', 'Métricas de huella ambiental')}
</p>
@@ -334,14 +393,9 @@ const SustainabilityPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.sdg_compliance', 'Cumplimiento SDG 12.3')}
</h3>
<Tooltip content={t('sustainability:tooltips.sdg_compliance', 'Progreso hacia el objetivo de desarrollo sostenible de la ONU para reducir residuos alimentarios')}>
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
</Tooltip>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.sdg_compliance', 'Cumplimiento SDG 12.3')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.sdg_subtitle', 'Progreso hacia objetivo ONU')}
</p>
@@ -413,14 +467,9 @@ const SustainabilityPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.grant_readiness', 'Subvenciones Disponibles')}
</h3>
<Tooltip content={t('sustainability:tooltips.grant_readiness', 'Programas de financiación disponibles para empresas españolas según la Ley 1/2025 de prevención de residuos')}>
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
</Tooltip>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.grant_readiness', 'Subvenciones Disponibles')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.grant_subtitle', 'Programas de financiación elegibles')}
</p>
@@ -508,14 +557,9 @@ const SustainabilityPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.financial_impact', 'Impacto Financiero')}
</h3>
<Tooltip content={t('sustainability:tooltips.financial_impact', 'Costes asociados a residuos y ahorros potenciales mediante la reducción de desperdicio')}>
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
</Tooltip>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('sustainability:sections.financial_impact', 'Impacto Financiero')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('sustainability:sections.financial_subtitle', 'Costes y ahorros de sostenibilidad')}
</p>

View File

@@ -8,19 +8,26 @@ import { PageHeader } from '../../../../components/layout';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { Equipment } from '../../../../api/types/equipment';
import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment } from '../../../../api/hooks/equipment';
import { DeleteEquipmentModal } from '../../../../components/domain/equipment/DeleteEquipmentModal';
import { MaintenanceHistoryModal } from '../../../../components/domain/equipment/MaintenanceHistoryModal';
import { ScheduleMaintenanceModal, type MaintenanceScheduleData } from '../../../../components/domain/equipment/ScheduleMaintenanceModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment, useDeleteEquipment, useHardDeleteEquipment } from '../../../../api/hooks/equipment';
const MaquinariaPage: React.FC = () => {
const { t } = useTranslation(['equipment', 'common']);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [selectedItem, setSelectedItem] = useState<Equipment | null>(null);
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create');
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
// New modal states
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [equipmentForAction, setEquipmentForAction] = useState<Equipment | null>(null);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
@@ -29,9 +36,11 @@ const MaquinariaPage: React.FC = () => {
is_active: true
});
// Mutations for create and update
// Mutations for create, update, and delete
const createEquipmentMutation = useCreateEquipment(tenantId);
const updateEquipmentMutation = useUpdateEquipment(tenantId);
const deleteEquipmentMutation = useDeleteEquipment(tenantId);
const hardDeleteEquipmentMutation = useHardDeleteEquipment(tenantId);
const handleCreateEquipment = () => {
setSelectedEquipment({
@@ -73,19 +82,58 @@ const MaquinariaPage: React.FC = () => {
}
};
const handleScheduleMaintenance = (equipmentId: string) => {
console.log('Schedule maintenance for equipment:', equipmentId);
// Implementation would go here
const handleScheduleMaintenance = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowScheduleModal(true);
};
const handleAcknowledgeAlert = (equipmentId: string, alertId: string) => {
console.log('Acknowledge alert:', alertId, 'for equipment:', equipmentId);
// Implementation would go here
const handleScheduleMaintenanceSubmit = async (equipmentId: string, maintenanceData: MaintenanceScheduleData) => {
try {
// Update next maintenance date based on scheduled date
await updateEquipmentMutation.mutateAsync({
equipmentId: equipmentId,
equipmentData: {
nextMaintenance: maintenanceData.scheduledDate
} as Partial<Equipment>
});
setShowScheduleModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error scheduling maintenance:', error);
throw error;
}
};
const handleViewMaintenanceHistory = (equipmentId: string) => {
console.log('View maintenance history for equipment:', equipmentId);
// Implementation would go here
const handleViewMaintenanceHistory = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowHistoryModal(true);
};
const handleDeleteEquipment = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowDeleteModal(true);
};
const handleSoftDelete = async (equipmentId: string) => {
try {
await deleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error deleting equipment:', error);
throw error;
}
};
const handleHardDelete = async (equipmentId: string) => {
try {
await hardDeleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error hard deleting equipment:', error);
throw error;
}
};
const handleSaveEquipment = async (equipmentData: Equipment) => {
@@ -200,13 +248,9 @@ const MaquinariaPage: React.FC = () => {
];
const handleShowMaintenanceDetails = (equipment: Equipment) => {
setSelectedItem(equipment);
setShowMaintenanceModal(true);
};
const handleCloseMaintenanceModal = () => {
setShowMaintenanceModal(false);
setSelectedItem(null);
setSelectedEquipment(equipment);
setEquipmentModalMode('view');
setShowEquipmentModal(true);
};
// Loading state
@@ -336,23 +380,25 @@ const MaquinariaPage: React.FC = () => {
priority: 'primary',
onClick: () => handleShowMaintenanceDetails(equipment)
},
{
label: t('actions.edit'),
icon: Edit,
priority: 'secondary',
onClick: () => handleEditEquipment(equipment.id)
},
{
label: t('actions.view_history'),
icon: History,
priority: 'secondary',
onClick: () => handleViewMaintenanceHistory(equipment.id)
onClick: () => handleViewMaintenanceHistory(equipment)
},
{
label: t('actions.schedule_maintenance'),
icon: Wrench,
priority: 'secondary',
onClick: () => handleScheduleMaintenance(equipment.id)
highlighted: true,
onClick: () => handleScheduleMaintenance(equipment)
},
{
label: t('actions.delete'),
icon: Trash2,
priority: 'secondary',
destructive: true,
onClick: () => handleDeleteEquipment(equipment)
}
]}
/>
@@ -372,183 +418,7 @@ const MaquinariaPage: React.FC = () => {
/>
)}
{/* Maintenance Details Modal */}
{selectedItem && showMaintenanceModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div className="bg-[var(--bg-primary)] rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto my-8">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-between mb-4 sm:mb-6">
<div>
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
{selectedItem.name}
</h2>
<p className="text-[var(--text-secondary)] text-sm">
{selectedItem.model} - {selectedItem.serialNumber}
</p>
</div>
<button
onClick={handleCloseMaintenanceModal}
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] p-1"
>
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4 sm:space-y-6">
{/* Equipment Status */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.status')}</h3>
<div className="flex items-center space-x-2">
<div
className="w-2 h-2 sm:w-3 sm:h-3 rounded-full"
style={{ backgroundColor: getStatusConfig(selectedItem.status).color }}
/>
<span className="text-[var(--text-primary)] text-sm sm:text-base">
{t(`equipment_status.${selectedItem.status}`)}
</span>
</div>
</div>
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.efficiency')}</h3>
<div className="text-lg sm:text-2xl font-bold text-[var(--text-primary)]">
{selectedItem.efficiency}%
</div>
</div>
</div>
{/* Maintenance Information */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.title')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.last')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{new Date(selectedItem.lastMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.next')}</p>
<p className={`font-medium ${(new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime()) ? 'text-red-500' : 'text-[var(--text-primary)]'} text-sm sm:text-base`}>
{new Date(selectedItem.nextMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.interval')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{selectedItem.maintenanceInterval} {t('common:units.days')}
</p>
</div>
</div>
{new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime() && (
<div className="mt-3 p-2 bg-red-50 dark:bg-red-900/20 rounded border-l-2 border-red-500">
<div className="flex items-center space-x-2">
<AlertTriangle className="w-4 h-4 text-red-500" />
<span className="text-xs sm:text-sm font-medium text-red-700 dark:text-red-300">
{t('maintenance.overdue')}
</span>
</div>
</div>
)}
</div>
{/* Active Alerts */}
{selectedItem.alerts.filter(a => !a.acknowledged).length > 0 && (
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('alerts.title')}</h3>
<div className="space-y-2 sm:space-y-3">
{selectedItem.alerts.filter(a => !a.acknowledged).map((alert) => (
<div
key={alert.id}
className={`p-2 sm:p-3 rounded border-l-2 ${
alert.type === 'critical' ? 'bg-red-50 border-red-500 dark:bg-red-900/20' :
alert.type === 'warning' ? 'bg-orange-50 border-orange-500 dark:bg-orange-900/20' :
'bg-blue-50 border-blue-500 dark:bg-blue-900/20'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<AlertTriangle className={`w-3 h-3 sm:w-4 sm:h-4 ${
alert.type === 'critical' ? 'text-red-500' :
alert.type === 'warning' ? 'text-orange-500' : 'text-blue-500'
}`} />
<span className="font-medium text-[var(--text-primary)] text-xs sm:text-sm">
{alert.message}
</span>
</div>
<span className="text-xs text-[var(--text-secondary)] hidden sm:block">
{new Date(alert.timestamp).toLocaleString('es-ES')}
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Maintenance History */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.history')}</h3>
<div className="space-y-3 sm:space-y-4">
{selectedItem.maintenanceHistory.map((history) => (
<div key={history.id} className="border-b border-[var(--border-primary)] pb-2 sm:pb-3 last:border-0 last:pb-0">
<div className="flex justify-between items-start">
<div>
<h4 className="font-medium text-[var(--text-primary)] text-sm">{history.description}</h4>
<p className="text-xs text-[var(--text-secondary)]">
{new Date(history.date).toLocaleDateString('es-ES')} - {history.technician}
</p>
</div>
<span className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{t(`maintenance.type.${history.type}`)}
</span>
</div>
<div className="mt-1 sm:mt-2 flex flex-wrap gap-2">
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('common:actions.cost')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.cost}</span>
</span>
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('fields.uptime')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.downtime}h</span>
</span>
</div>
{history.partsUsed.length > 0 && (
<div className="mt-1 sm:mt-2">
<span className="text-xs text-[var(--text-secondary)]">{t('fields.parts')}:</span>
<div className="flex flex-wrap gap-1 mt-1">
{history.partsUsed.map((part, index) => (
<span key={index} className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{part}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end space-x-2 sm:space-x-3 mt-4 sm:mt-6">
<Button variant="outline" size="sm" onClick={handleCloseMaintenanceModal}>
{t('common:actions.close')}
</Button>
<Button variant="primary" size="sm" onClick={() => selectedItem && handleScheduleMaintenance(selectedItem.id)}>
{t('actions.schedule_maintenance')}
</Button>
</div>
</div>
</div>
</div>
)}
{/* Equipment Modal */}
{/* Equipment Modal - Used for View Details, Edit, and Create */}
{showEquipmentModal && (
<EquipmentModal
isOpen={showEquipmentModal}
@@ -561,6 +431,47 @@ const MaquinariaPage: React.FC = () => {
mode={equipmentModalMode}
/>
)}
{/* Delete Equipment Modal */}
{showDeleteModal && equipmentForAction && (
<DeleteEquipmentModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSoftDelete={handleSoftDelete}
onHardDelete={handleHardDelete}
isLoading={deleteEquipmentMutation.isPending || hardDeleteEquipmentMutation.isPending}
/>
)}
{/* Maintenance History Modal */}
{showHistoryModal && equipmentForAction && (
<MaintenanceHistoryModal
isOpen={showHistoryModal}
onClose={() => {
setShowHistoryModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
/>
)}
{/* Schedule Maintenance Modal */}
{showScheduleModal && equipmentForAction && (
<ScheduleMaintenanceModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSchedule={handleScheduleMaintenanceSubmit}
isLoading={updateEquipmentMutation.isPending}
/>
)}
</div>
);
};

View File

@@ -187,7 +187,7 @@ const ProcurementPage: React.FC = () => {
const handleTriggerScheduler = async () => {
try {
await triggerSchedulerMutation.mutateAsync({ tenantId });
await triggerSchedulerMutation.mutateAsync(tenantId);
toast.success('Scheduler ejecutado exitosamente');
refetchPOs();
} catch (error) {

View File

@@ -1 +1 @@
export { default as ProcurementPage } from './ProcurementPage';
export { default as ProcurementPage } from './ProcurementPage';

View File

@@ -1,15 +1,16 @@
import React, { useState } from 'react';
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader, Trash2 } from 'lucide-react';
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, AddModal, SearchAndFilter, DialogModal, type FilterConfig, EmptyState } from '../../../../components/ui';
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader, Trash2, DollarSign, Package } from 'lucide-react';
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, AddModal, SearchAndFilter, DialogModal, Modal, ModalHeader, ModalBody, type FilterConfig, EmptyState } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { SupplierStatus, SupplierType, PaymentTerms } from '../../../../api/types/suppliers';
import { useSuppliers, useSupplierStatistics, useCreateSupplier, useUpdateSupplier, useApproveSupplier, useDeleteSupplier, useHardDeleteSupplier } from '../../../../api/hooks/suppliers';
import { useSuppliers, useSupplierStatistics, useCreateSupplier, useUpdateSupplier, useApproveSupplier, useDeleteSupplier, useHardDeleteSupplier, useSupplierPriceLists, useCreateSupplierPriceList, useUpdateSupplierPriceList, useDeleteSupplierPriceList } from '../../../../api/hooks/suppliers';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
import { statusColors } from '../../../../styles/colors';
import { DeleteSupplierModal } from '../../../../components/domain/suppliers';
import { DeleteSupplierModal, SupplierPriceListViewModal, PriceListModal } from '../../../../components/domain/suppliers';
import { useQueryClient } from '@tanstack/react-query';
const SuppliersPage: React.FC = () => {
const [activeTab] = useState('all');
@@ -23,6 +24,9 @@ const SuppliersPage: React.FC = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [supplierToApprove, setSupplierToApprove] = useState<any>(null);
const [showPriceListView, setShowPriceListView] = useState(false);
const [showAddPrice, setShowAddPrice] = useState(false);
const [priceListSupplier, setPriceListSupplier] = useState<any>(null);
// Get tenant ID from tenant store (preferred) or auth user (fallback)
const currentTenant = useCurrentTenant();
@@ -48,6 +52,7 @@ const SuppliersPage: React.FC = () => {
const suppliers = suppliersData || [];
const { t } = useTranslation(['suppliers', 'common']);
const queryClient = useQueryClient();
// Mutation hooks
const createSupplierMutation = useCreateSupplier();
@@ -56,6 +61,21 @@ const SuppliersPage: React.FC = () => {
const softDeleteMutation = useDeleteSupplier();
const hardDeleteMutation = useHardDeleteSupplier();
// Price list hooks
const {
data: priceListsData,
isLoading: priceListsLoading,
isRefetching: isRefetchingPriceLists
} = useSupplierPriceLists(
tenantId,
priceListSupplier?.id || '',
!!priceListSupplier?.id && showPriceListView
);
const createPriceListMutation = useCreateSupplierPriceList();
const updatePriceListMutation = useUpdateSupplierPriceList();
const deletePriceListMutation = useDeleteSupplierPriceList();
// Delete handlers
const handleSoftDelete = async (supplierId: string) => {
await softDeleteMutation.mutateAsync({ tenantId, supplierId });
@@ -65,6 +85,27 @@ const SuppliersPage: React.FC = () => {
return await hardDeleteMutation.mutateAsync({ tenantId, supplierId });
};
// Price list handlers
const handlePriceListSaveComplete = async () => {
if (!tenantId || !priceListSupplier?.id) return;
await queryClient.invalidateQueries({
queryKey: ['supplier-price-lists', tenantId, priceListSupplier.id]
});
};
const handleAddPriceSubmit = async (priceListData: any) => {
if (!priceListSupplier) return;
await createPriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListData
});
// Close the add modal
setShowAddPrice(false);
};
const getSupplierStatusConfig = (status: SupplierStatus) => {
const statusConfig = {
[SupplierStatus.ACTIVE]: { text: t(`suppliers:status.${status.toLowerCase()}`), icon: CheckCircle },
@@ -274,6 +315,18 @@ const SuppliersPage: React.FC = () => {
setShowForm(true);
}
},
// Manage products action
{
label: t('suppliers:actions.manage_products'),
icon: Package,
variant: 'outline',
priority: 'secondary',
highlighted: true,
onClick: () => {
setPriceListSupplier(supplier);
setShowPriceListView(true);
}
},
// Approval action - Only show for pending suppliers + admin/super_admin
...(supplier.status === SupplierStatus.PENDING_APPROVAL &&
(user?.role === 'admin' || user?.role === 'super_admin')
@@ -769,7 +822,7 @@ const SuppliersPage: React.FC = () => {
placeholder: t('suppliers:placeholders.notes')
}
]
}] : [])
}] : []),
];
return (
@@ -942,6 +995,55 @@ const SuppliersPage: React.FC = () => {
}}
loading={approveSupplierMutation.isPending}
/>
{/* Price List View Modal */}
{priceListSupplier && (
<SupplierPriceListViewModal
isOpen={showPriceListView}
onClose={() => {
setShowPriceListView(false);
setPriceListSupplier(null);
}}
supplier={priceListSupplier}
priceLists={priceListsData || []}
loading={priceListsLoading}
tenantId={tenantId}
onAddPrice={() => setShowAddPrice(true)}
onEditPrice={async (priceId, updateData) => {
await updatePriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListId: priceId,
priceListData: updateData
});
}}
onDeletePrice={async (priceId) => {
await deletePriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListId: priceId
});
}}
waitForRefetch={true}
isRefetching={isRefetchingPriceLists}
onSaveComplete={handlePriceListSaveComplete}
/>
)}
{/* Add Price Modal */}
{priceListSupplier && (
<PriceListModal
isOpen={showAddPrice}
onClose={() => setShowAddPrice(false)}
onSave={handleAddPriceSubmit}
mode="create"
loading={createPriceListMutation.isPending}
excludeProductIds={priceListsData?.map(p => p.inventory_product_id) || []}
waitForRefetch={true}
isRefetching={isRefetchingPriceLists}
onSaveComplete={handlePriceListSaveComplete}
/>
)}
</div>
);
};

View File

@@ -1,17 +1,20 @@
import React, { useState } from 'react';
import { Crown, Users, MapPin, Package, TrendingUp, RefreshCw, AlertCircle, CheckCircle, ArrowRight, Star, ExternalLink, Download, CreditCard, X, Activity, Database, Zap, HardDrive, ShoppingCart, ChefHat } from 'lucide-react';
import { Button, Card, Badge, Modal } from '../../../../components/ui';
import { DialogModal } from '../../../../components/ui/DialogModal/DialogModal';
import { PageHeader } from '../../../../components/layout';
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 { useSubscriptionEvents } from '../../../../contexts/SubscriptionEventsContext';
import { SubscriptionPricingCards } from '../../../../components/subscription/SubscriptionPricingCards';
const SubscriptionPage: React.FC = () => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { addToast } = useToast();
const { notifySubscriptionChanged } = useSubscriptionEvents();
const [usageSummary, setUsageSummary] = useState<UsageSummary | null>(null);
const [availablePlans, setAvailablePlans] = useState<AvailablePlans | null>(null);
@@ -154,6 +157,9 @@ const SubscriptionPage: React.FC = () => {
if (result.success) {
addToast(result.message, { type: 'success' });
// Broadcast subscription change event to refresh sidebar and other components
notifySubscriptionChanged();
await loadSubscriptionData();
setUpgradeDialogOpen(false);
setSelectedPlan('');
@@ -325,7 +331,7 @@ const SubscriptionPage: React.FC = () => {
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Usuarios</span>
<span className="font-medium text-[var(--text-primary)]">
{usageSummary.usage.users.current}/{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}
{usageSummary.usage.users.current}/{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit ?? 0}
</span>
</div>
</div>
@@ -333,7 +339,7 @@ const SubscriptionPage: React.FC = () => {
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Ubicaciones</span>
<span className="font-medium text-[var(--text-primary)]">
{usageSummary.usage.locations.current}/{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}
{usageSummary.usage.locations.current}/{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit ?? 0}
</span>
</div>
</div>
@@ -377,7 +383,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.users.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
@@ -398,7 +404,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.locations.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
@@ -425,7 +431,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.products.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.products.unlimited ? '∞' : usageSummary.usage.products.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.products.unlimited ? '∞' : usageSummary.usage.products.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
@@ -446,7 +452,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.recipes.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.recipes.unlimited ? '∞' : usageSummary.usage.recipes.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.recipes.unlimited ? '∞' : usageSummary.usage.recipes.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.recipes.usage_percentage} />
@@ -467,7 +473,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.suppliers.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.suppliers.unlimited ? '∞' : usageSummary.usage.suppliers.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.suppliers.unlimited ? '∞' : usageSummary.usage.suppliers.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.suppliers.usage_percentage} />
@@ -494,7 +500,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.training_jobs_today.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.training_jobs_today.unlimited ? '∞' : usageSummary.usage.training_jobs_today.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.training_jobs_today.unlimited ? '∞' : usageSummary.usage.training_jobs_today.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.training_jobs_today.usage_percentage} />
@@ -515,7 +521,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.forecasts_today.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.forecasts_today.unlimited ? '∞' : usageSummary.usage.forecasts_today.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.forecasts_today.unlimited ? '∞' : usageSummary.usage.forecasts_today.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.forecasts_today.usage_percentage} />
@@ -542,7 +548,7 @@ const SubscriptionPage: React.FC = () => {
</div>
<span className="text-sm font-bold text-[var(--text-primary)]">
{usageSummary.usage.api_calls_this_hour.current}<span className="text-[var(--text-tertiary)]">/</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.api_calls_this_hour.unlimited ? '∞' : usageSummary.usage.api_calls_this_hour.limit}</span>
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.api_calls_this_hour.unlimited ? '∞' : usageSummary.usage.api_calls_this_hour.limit ?? 0}</span>
</span>
</div>
<ProgressBar value={usageSummary.usage.api_calls_this_hour.usage_percentage} />
@@ -704,89 +710,61 @@ const SubscriptionPage: React.FC = () => {
</>
)}
{/* Upgrade Modal */}
{/* Upgrade Dialog */}
{upgradeDialogOpen && selectedPlan && availablePlans && (
<Modal
<DialogModal
isOpen={upgradeDialogOpen}
onClose={() => setUpgradeDialogOpen(false)}
title="Confirmar Cambio de Plan"
>
<div className="space-y-4">
<p className="text-[var(--text-secondary)]">
¿Estás seguro de que quieres cambiar tu plan de suscripción?
</p>
{availablePlans.plans[selectedPlan] && usageSummary && (
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg space-y-2">
<div className="flex justify-between">
<span>Plan actual:</span>
<span>{usageSummary.plan}</span>
message={
<div className="space-y-3">
<p>¿Estás seguro de que quieres cambiar tu plan de suscripción?</p>
{availablePlans.plans[selectedPlan as keyof typeof availablePlans.plans] && usageSummary && (
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg space-y-2">
<div className="flex justify-between">
<span>Plan actual:</span>
<span>{usageSummary.plan}</span>
</div>
<div className="flex justify-between">
<span>Nuevo plan:</span>
<span>{availablePlans.plans[selectedPlan as keyof typeof availablePlans.plans].name}</span>
</div>
<div className="flex justify-between font-medium">
<span>Nuevo precio:</span>
<span>{subscriptionService.formatPrice(availablePlans.plans[selectedPlan as keyof typeof availablePlans.plans].monthly_price)}/mes</span>
</div>
</div>
<div className="flex justify-between">
<span>Nuevo plan:</span>
<span>{availablePlans.plans[selectedPlan].name}</span>
</div>
<div className="flex justify-between font-medium">
<span>Nuevo precio:</span>
<span>{subscriptionService.formatPrice(availablePlans.plans[selectedPlan].monthly_price)}/mes</span>
</div>
</div>
)}
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={() => setUpgradeDialogOpen(false)}
className="flex-1"
>
Cancelar
</Button>
<Button
variant="primary"
onClick={handleUpgradeConfirm}
disabled={upgrading}
className="flex-1"
>
{upgrading ? 'Procesando...' : 'Confirmar Cambio'}
</Button>
)}
</div>
</div>
</Modal>
}
type="confirm"
onConfirm={handleUpgradeConfirm}
onCancel={() => setUpgradeDialogOpen(false)}
confirmLabel="Confirmar Cambio"
cancelLabel="Cancelar"
loading={upgrading}
/>
)}
{/* Cancellation Modal */}
{/* Cancellation Dialog */}
{cancellationDialogOpen && (
<Modal
<DialogModal
isOpen={cancellationDialogOpen}
onClose={() => setCancellationDialogOpen(false)}
title="Cancelar Suscripción"
>
<div className="space-y-4">
<p className="text-[var(--text-secondary)]">
¿Estás seguro de que deseas cancelar tu suscripción? Esta acción no se puede deshacer.
</p>
<p className="text-[var(--text-secondary)]">
Perderás acceso a las funcionalidades premium al final del período de facturación actual.
</p>
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={() => setCancellationDialogOpen(false)}
className="flex-1"
>
Volver
</Button>
<Button
variant="danger"
onClick={handleCancelSubscription}
disabled={cancelling}
className="flex-1"
>
{cancelling ? 'Cancelando...' : 'Confirmar Cancelación'}
</Button>
message={
<div className="space-y-3">
<p>¿Estás seguro de que deseas cancelar tu suscripción? Esta acción no se puede deshacer.</p>
<p>Perderás acceso a las funcionalidades premium al final del período de facturación actual.</p>
</div>
</div>
</Modal>
}
type="warning"
onConfirm={handleCancelSubscription}
onCancel={() => setCancellationDialogOpen(false)}
confirmLabel="Confirmar Cancelación"
cancelLabel="Volver"
loading={cancelling}
/>
)}
</div>
);