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

@@ -1,13 +1,10 @@
import React, { useState, useEffect } from 'react';
import { Trash2, AlertTriangle, Info } from 'lucide-react';
import { Modal, Button } from '../../ui';
import { RecipeResponse, RecipeDeletionSummary } from '../../../api/types/recipes';
import React from 'react';
import { BaseDeleteModal } from '../../ui';
import { RecipeResponse } from '../../../api/types/recipes';
import { useTranslation } from 'react-i18next';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useRecipeDeletionSummary } from '../../../api/hooks/recipes';
type DeleteMode = 'soft' | 'hard';
interface DeleteRecipeModalProps {
isOpen: boolean;
onClose: () => void;
@@ -32,345 +29,121 @@ export const DeleteRecipeModal: React.FC<DeleteRecipeModalProps> = ({
}) => {
const { t } = useTranslation(['recipes', 'common']);
const currentTenant = useCurrentTenant();
const [selectedMode, setSelectedMode] = useState<DeleteMode>('soft');
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmText, setConfirmText] = useState('');
const [deletionComplete, setDeletionComplete] = useState(false);
// Fetch deletion summary when modal opens for hard delete
// Fetch deletion summary for dependency checking
const { data: deletionSummary, isLoading: summaryLoading } = useRecipeDeletionSummary(
currentTenant?.id || '',
recipe?.id || '',
{
enabled: isOpen && !!recipe && selectedMode === 'hard' && showConfirmation,
enabled: isOpen && !!recipe,
}
);
useEffect(() => {
if (!isOpen) {
// Reset state when modal closes
setShowConfirmation(false);
setSelectedMode('soft');
setConfirmText('');
setDeletionComplete(false);
}
}, [isOpen]);
if (!recipe) return null;
const handleDeleteModeSelect = (mode: DeleteMode) => {
setSelectedMode(mode);
setShowConfirmation(true);
setConfirmText('');
};
const handleConfirmDelete = async () => {
try {
if (selectedMode === 'hard') {
await onHardDelete(recipe.id);
} else {
await onSoftDelete(recipe.id);
}
setDeletionComplete(true);
// Auto-close after 1.5 seconds
setTimeout(() => {
handleClose();
}, 1500);
} catch (error) {
console.error('Error deleting recipe:', error);
// Error handling is done by parent component
// Build dependency check warnings
const dependencyWarnings: string[] = [];
if (deletionSummary) {
if (deletionSummary.production_batches_count > 0) {
dependencyWarnings.push(
t('recipes:delete.batches_affected',
{ count: deletionSummary.production_batches_count },
`${deletionSummary.production_batches_count} lotes de producción afectados`
)
);
}
if (deletionSummary.affected_orders_count > 0) {
dependencyWarnings.push(
t('recipes:delete.orders_affected',
{ count: deletionSummary.affected_orders_count },
`${deletionSummary.affected_orders_count} pedidos afectados`
)
);
}
if (deletionSummary.warnings && deletionSummary.warnings.length > 0) {
dependencyWarnings.push(...deletionSummary.warnings);
}
};
const handleClose = () => {
setShowConfirmation(false);
setSelectedMode('soft');
setConfirmText('');
setDeletionComplete(false);
onClose();
};
const isConfirmDisabled =
selectedMode === 'hard' && confirmText.toUpperCase() !== 'ELIMINAR';
// Show deletion success
if (deletionComplete) {
return (
<Modal isOpen={isOpen} onClose={handleClose} size="md">
<div className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{selectedMode === 'hard'
? t('recipes:delete.success_hard_title', 'Receta Eliminada')
: t('recipes:delete.success_soft_title', 'Receta Archivada')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{selectedMode === 'hard'
? t('recipes:delete.recipe_deleted', { name: recipe.name })
: t('recipes:delete.recipe_archived', { name: recipe.name })}
</p>
</div>
</div>
</div>
</Modal>
);
}
// Show confirmation step
if (showConfirmation) {
const isHardDelete = selectedMode === 'hard';
const canDelete = !isHardDelete || (deletionSummary?.can_delete !== false);
// Build hard delete warning items
const hardDeleteItems = [
t('recipes:delete.hard_warning_1', 'La receta y toda su información'),
t('recipes:delete.hard_warning_2', 'Todos los ingredientes asociados'),
];
return (
<Modal isOpen={isOpen} onClose={handleClose} size="md">
<div className="p-6">
<div className="flex items-start gap-4 mb-6">
<div className="flex-shrink-0">
{isHardDelete ? (
<AlertTriangle className="w-8 h-8 text-red-500" />
) : (
<Info className="w-8 h-8 text-orange-500" />
)}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{isHardDelete
? t('recipes:delete.confirm_hard_title', 'Confirmación de Eliminación Permanente')
: t('recipes:delete.confirm_soft_title', 'Confirmación de Archivo')}
</h3>
<div className="mb-4">
<div className="bg-[var(--background-secondary)] p-3 rounded-lg mb-4">
<p className="font-medium text-[var(--text-primary)]">{recipe.name}</p>
<p className="text-sm text-[var(--text-secondary)]">
{t('recipes:delete.recipe_code', 'Código')}: {recipe.recipe_code || 'N/A'} {t('recipes:delete.recipe_category', 'Categoría')}: {recipe.category}
</p>
</div>
{isHardDelete ? (
<>
{summaryLoading ? (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--text-primary)]"></div>
<p className="mt-2 text-sm text-[var(--text-secondary)]">
{t('recipes:delete.checking_dependencies', 'Verificando dependencias...')}
</p>
</div>
) : deletionSummary && !canDelete ? (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4">
<p className="font-medium text-red-700 dark:text-red-400 mb-2">
{t('recipes:delete.cannot_delete', '⚠️ No se puede eliminar esta receta')}
</p>
<ul className="text-sm space-y-1 text-red-600 dark:text-red-300">
{deletionSummary.warnings.map((warning, idx) => (
<li key={idx}> {warning}</li>
))}
</ul>
</div>
) : (
<div className="text-red-600 dark:text-red-400 mb-4">
<p className="font-medium mb-2">
{t('recipes:delete.hard_warning_title', '⚠️ Esta acción eliminará permanentemente:')}
</p>
<ul className="text-sm space-y-1 ml-4">
<li>{t('recipes:delete.hard_warning_1', '• La receta y toda su información')}</li>
<li>{t('recipes:delete.hard_warning_2', '• Todos los ingredientes asociados')}</li>
{deletionSummary && (
<>
{deletionSummary.production_batches_count > 0 && (
<li>{t('recipes:delete.batches_affected', { count: deletionSummary.production_batches_count }, `${deletionSummary.production_batches_count} lotes de producción`)}</li>
)}
{deletionSummary.affected_orders_count > 0 && (
<li>{t('recipes:delete.orders_affected', { count: deletionSummary.affected_orders_count }, `${deletionSummary.affected_orders_count} pedidos afectados`)}</li>
)}
</>
)}
</ul>
<p className="font-bold mt-3 text-red-700 dark:text-red-300">
{t('recipes:delete.irreversible', 'Esta acción NO se puede deshacer')}
</p>
</div>
)}
</>
) : (
<div className="text-orange-600 dark:text-orange-400 mb-4">
<p className="font-medium mb-2">
{t('recipes:delete.soft_info_title', ' Esta acción archivará la receta:')}
</p>
<ul className="text-sm space-y-1 ml-4">
<li>{t('recipes:delete.soft_info_1', '• La receta cambiará a estado ARCHIVADO')}</li>
<li>{t('recipes:delete.soft_info_2', '• No aparecerá en listas activas')}</li>
<li>{t('recipes:delete.soft_info_3', '• Se conserva todo el historial y datos')}</li>
<li>{t('recipes:delete.soft_info_4', '• Se puede reactivar posteriormente')}</li>
</ul>
</div>
)}
</div>
{isHardDelete && canDelete && (
<div className="mb-6">
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
{t('recipes:delete.type_to_confirm_label', 'Para confirmar, escriba')} <span className="font-mono bg-[var(--background-secondary)] px-1 rounded text-[var(--text-primary)]">ELIMINAR</span>:
</label>
<input
type="text"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 placeholder:text-[var(--text-tertiary)]"
placeholder={t('recipes:delete.type_placeholder', 'Escriba ELIMINAR')}
autoComplete="off"
/>
</div>
)}
</div>
</div>
<div className="flex gap-3 justify-end">
<Button
variant="outline"
onClick={() => setShowConfirmation(false)}
disabled={isLoading}
>
{t('common:back', 'Volver')}
</Button>
{(!isHardDelete || canDelete) && (
<Button
variant={isHardDelete ? 'danger' : 'warning'}
onClick={handleConfirmDelete}
disabled={isConfirmDisabled || isLoading || summaryLoading}
isLoading={isLoading}
>
{isHardDelete
? t('recipes:delete.confirm_hard', 'Eliminar Permanentemente')
: t('recipes:delete.confirm_soft', 'Archivar Receta')}
</Button>
)}
</div>
</div>
</Modal>
);
if (deletionSummary) {
if (deletionSummary.production_batches_count > 0) {
hardDeleteItems.push(
t('recipes:delete.batches_affected',
{ count: deletionSummary.production_batches_count },
`${deletionSummary.production_batches_count} lotes de producción`
)
);
}
if (deletionSummary.affected_orders_count > 0) {
hardDeleteItems.push(
t('recipes:delete.orders_affected',
{ count: deletionSummary.affected_orders_count },
`${deletionSummary.affected_orders_count} pedidos afectados`
)
);
}
}
// Initial mode selection
return (
<Modal isOpen={isOpen} onClose={handleClose} size="lg">
<div className="p-6">
<div className="mb-6">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{t('recipes:delete.title', 'Eliminar Receta')}
</h2>
</div>
<div className="mb-6">
<div className="bg-[var(--background-secondary)] p-4 rounded-lg">
<p className="font-medium text-[var(--text-primary)]">{recipe.name}</p>
<p className="text-sm text-[var(--text-secondary)]">
{t('recipes:delete.recipe_code', 'Código')}: {recipe.recipe_code || 'N/A'} {t('recipes:delete.recipe_category', 'Categoría')}: {recipe.category}
</p>
</div>
</div>
<div className="mb-6">
<p className="text-[var(--text-primary)] mb-4">
{t('recipes:delete.choose_type', 'Elija el tipo de eliminación que desea realizar:')}
</p>
<div className="space-y-4">
{/* Soft Delete Option */}
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
selectedMode === 'soft'
? 'border-orange-500 bg-orange-50 dark:bg-orange-900/10'
: 'border-[var(--border-color)] hover:border-orange-300'
}`}
onClick={() => setSelectedMode('soft')}
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-1">
<div className={`w-4 h-4 rounded-full border-2 ${
selectedMode === 'soft'
? 'border-orange-500 bg-orange-500'
: 'border-[var(--border-color)]'
}`}>
{selectedMode === 'soft' && (
<div className="w-2 h-2 bg-white rounded-full m-0.5" />
)}
</div>
</div>
<div>
<h3 className="font-medium text-[var(--text-primary)] mb-1">
{t('recipes:delete.soft_delete', 'Archivar (Recomendado)')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('recipes:delete.soft_explanation', 'La receta se marca como archivada pero conserva todo su historial. Ideal para recetas fuera de uso temporal.')}
</p>
<div className="mt-2 text-xs text-orange-600 dark:text-orange-400">
{t('recipes:delete.soft_benefits', '✓ Reversible • ✓ Conserva historial • ✓ Conserva datos')}
</div>
</div>
</div>
</div>
{/* Hard Delete Option */}
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
selectedMode === 'hard'
? 'border-red-500 bg-red-50 dark:bg-red-900/10'
: 'border-[var(--border-color)] hover:border-red-300'
}`}
onClick={() => setSelectedMode('hard')}
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-1">
<div className={`w-4 h-4 rounded-full border-2 ${
selectedMode === 'hard'
? 'border-red-500 bg-red-500'
: 'border-[var(--border-color)]'
}`}>
{selectedMode === 'hard' && (
<div className="w-2 h-2 bg-white rounded-full m-0.5" />
)}
</div>
</div>
<div>
<h3 className="font-medium text-[var(--text-primary)] mb-1 flex items-center gap-2">
{t('recipes:delete.hard_delete', 'Eliminar Permanentemente')}
<AlertTriangle className="w-4 h-4 text-red-500" />
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('recipes:delete.hard_explanation', 'Elimina completamente la receta y todos sus datos asociados. Use solo para datos erróneos o pruebas.')}
</p>
<div className="mt-2 text-xs text-red-600 dark:text-red-400">
{t('recipes:delete.hard_risks', '⚠️ No reversible • ⚠️ Elimina historial • ⚠️ Elimina todos los datos')}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex gap-3 justify-end">
<Button variant="outline" onClick={handleClose}>
{t('common:cancel', 'Cancelar')}
</Button>
<Button
variant={selectedMode === 'hard' ? 'danger' : 'warning'}
onClick={() => handleDeleteModeSelect(selectedMode)}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('common:continue', 'Continuar')}
</Button>
</div>
</div>
</Modal>
<BaseDeleteModal
isOpen={isOpen}
onClose={onClose}
entity={recipe}
onSoftDelete={onSoftDelete}
onHardDelete={onHardDelete}
isLoading={isLoading}
title={t('recipes:delete.title', 'Eliminar Receta')}
getEntityId={(rec) => rec.id}
getEntityDisplay={(rec) => ({
primaryText: rec.name,
secondaryText: `${t('recipes:delete.recipe_code', 'Código')}: ${rec.recipe_code || 'N/A'}${t('recipes:delete.recipe_category', 'Categoría')}: ${rec.category}`,
})}
softDeleteOption={{
title: t('recipes:delete.soft_delete', 'Archivar (Recomendado)'),
description: t('recipes:delete.soft_explanation', 'La receta se marca como archivada pero conserva todo su historial. Ideal para recetas fuera de uso temporal.'),
benefits: t('recipes:delete.soft_benefits', '✓ Reversible • ✓ Conserva historial • ✓ Conserva datos'),
}}
hardDeleteOption={{
title: t('recipes:delete.hard_delete', 'Eliminar Permanentemente'),
description: t('recipes:delete.hard_explanation', 'Elimina completamente la receta y todos sus datos asociados. Use solo para datos erróneos o pruebas.'),
benefits: t('recipes:delete.hard_risks', '⚠️ No reversible • ⚠️ Elimina historial • ⚠️ Elimina todos los datos'),
enabled: true,
}}
softDeleteWarning={{
title: t('recipes:delete.soft_info_title', ' Esta acción archivará la receta:'),
items: [
t('recipes:delete.soft_info_1', 'La receta cambiará a estado ARCHIVADO'),
t('recipes:delete.soft_info_2', 'No aparecerá en listas activas'),
t('recipes:delete.soft_info_3', 'Se conserva todo el historial y datos'),
t('recipes:delete.soft_info_4', 'Se puede reactivar posteriormente'),
],
}}
hardDeleteWarning={{
title: t('recipes:delete.hard_warning_title', '⚠️ Esta acción eliminará permanentemente:'),
items: hardDeleteItems,
footer: t('recipes:delete.irreversible', 'Esta acción NO se puede deshacer'),
}}
dependencyCheck={{
isLoading: summaryLoading,
canDelete: deletionSummary?.can_delete !== false,
warnings: dependencyWarnings,
}}
requireConfirmText={true}
confirmText="ELIMINAR"
showSuccessScreen={true}
successTitle={t('recipes:delete.success_soft_title', 'Receta Archivada')}
getSuccessMessage={(rec, mode) =>
mode === 'hard'
? t('recipes:delete.recipe_deleted', { name: rec.name })
: t('recipes:delete.recipe_archived', { name: rec.name })
}
autoCloseDelay={1500}
/>
);
};