Improve the frontend 2
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user