From d2addbc1f6e08373819aab7030c4468fa41bda0b Mon Sep 17 00:00:00 2001 From: Torch2196 Date: Sat, 31 Jan 2026 23:47:46 -0600 Subject: [PATCH] Add edit/delete functionality for clients - ClienteForm: Now supports both create and edit modes with logo preview - ClienteDetail: Added edit and delete buttons with confirmation modal - Added delete report functionality with confirmation Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/forms/ClienteForm.tsx | 98 ++++++++-- frontend/src/pages/Clientes/ClienteDetail.tsx | 177 +++++++++++++++--- 2 files changed, 235 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/forms/ClienteForm.tsx b/frontend/src/components/forms/ClienteForm.tsx index e9692bc..5891a82 100644 --- a/frontend/src/components/forms/ClienteForm.tsx +++ b/frontend/src/components/forms/ClienteForm.tsx @@ -1,14 +1,15 @@ import { useState, useEffect } from 'react'; import { clientesApi, girosApi } from '../../services/api'; -import { Giro } from '../../types'; +import { Giro, Cliente } from '../../types'; import toast from 'react-hot-toast'; interface Props { + cliente?: Cliente | null; onSuccess: () => void; onCancel: () => void; } -export default function ClienteForm({ onSuccess, onCancel }: Props) { +export default function ClienteForm({ cliente, onSuccess, onCancel }: Props) { const [giros, setGiros] = useState([]); const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ @@ -17,13 +18,51 @@ export default function ClienteForm({ onSuccess, onCancel }: Props) { moneda: 'MXN', }); const [logo, setLogo] = useState(null); + const [logoPreview, setLogoPreview] = useState(null); + + const isEditing = !!cliente; useEffect(() => { girosApi.list().then(setGiros).catch(() => toast.error('Error al cargar giros')); }, []); + useEffect(() => { + if (cliente) { + setFormData({ + nombre_empresa: cliente.nombre_empresa, + giro_id: cliente.giro_id?.toString() || '', + moneda: cliente.moneda, + }); + if (cliente.logo) { + setLogoPreview(`/storage/${cliente.logo}`); + } + } + }, [cliente]); + + const handleLogoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] || null; + setLogo(file); + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setLogoPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + if (!formData.nombre_empresa.trim()) { + toast.error('El nombre de la empresa es requerido'); + return; + } + if (!formData.giro_id) { + toast.error('Seleccione un giro'); + return; + } + setLoading(true); try { @@ -35,10 +74,19 @@ export default function ClienteForm({ onSuccess, onCancel }: Props) { data.append('logo', logo); } - await clientesApi.create(data); + if (isEditing && cliente) { + data.append('_method', 'PUT'); + await clientesApi.update(cliente.id, data); + toast.success('Cliente actualizado'); + } else { + await clientesApi.create(data); + toast.success('Cliente creado'); + } + onSuccess(); - } catch { - toast.error('Error al crear cliente'); + } catch (error: any) { + const message = error.response?.data?.message || `Error al ${isEditing ? 'actualizar' : 'crear'} cliente`; + toast.error(message); } finally { setLoading(false); } @@ -46,24 +94,34 @@ export default function ClienteForm({ onSuccess, onCancel }: Props) { return (
+ {/* Logo preview */} + {logoPreview && ( +
+ Logo preview +
+ )} +
- + setFormData({ ...formData, nombre_empresa: e.target.value })} className="input" - required + placeholder="Empresa S.A. de C.V." />
- + setLogo(e.target.files?.[0] || null)} + onChange={handleLogoChange} className="input" /> +

+ Formatos: JPG, PNG, GIF. Máximo 2MB. +

- -
diff --git a/frontend/src/pages/Clientes/ClienteDetail.tsx b/frontend/src/pages/Clientes/ClienteDetail.tsx index 730e7df..1095cda 100644 --- a/frontend/src/pages/Clientes/ClienteDetail.tsx +++ b/frontend/src/pages/Clientes/ClienteDetail.tsx @@ -2,19 +2,27 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { clientesApi, balanzasApi, reportesApi } from '../../services/api'; import { Cliente, Balanza, Reporte } from '../../types'; +import { useAuth } from '../../context/AuthContext'; import toast from 'react-hot-toast'; import UploadBalanza from '../../components/forms/UploadBalanza'; import GenerarReporte from '../../components/forms/GenerarReporte'; +import ClienteForm from '../../components/forms/ClienteForm'; export default function ClienteDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { isAdmin, isAnalista } = useAuth(); const [cliente, setCliente] = useState(null); const [balanzas, setBalanzas] = useState([]); const [reportes, setReportes] = useState([]); const [loading, setLoading] = useState(true); const [showUpload, setShowUpload] = useState(false); const [showGenerarReporte, setShowGenerarReporte] = useState(false); + const [showEditCliente, setShowEditCliente] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); + + const canManage = isAdmin || isAnalista; useEffect(() => { if (id) { @@ -52,6 +60,27 @@ export default function ClienteDetail() { toast.success('Reporte generado exitosamente'); }; + const handleClienteUpdated = () => { + setShowEditCliente(false); + if (id) loadData(parseInt(id)); + }; + + const handleDeleteCliente = async () => { + if (!cliente) return; + + setDeleting(true); + try { + await clientesApi.delete(cliente.id); + toast.success('Cliente eliminado'); + navigate('/clientes'); + } catch { + toast.error('Error al eliminar cliente'); + } finally { + setDeleting(false); + setShowDeleteConfirm(false); + } + }; + const handleDownloadPdf = async (reporteId: number, nombre: string) => { try { const blob = await reportesApi.downloadPdf(reporteId); @@ -66,6 +95,18 @@ export default function ClienteDetail() { } }; + const handleDeleteReporte = async (reporteId: number) => { + if (!confirm('¿Estás seguro de eliminar este reporte?')) return; + + try { + await reportesApi.delete(reporteId); + toast.success('Reporte eliminado'); + if (id) loadData(parseInt(id)); + } catch { + toast.error('Error al eliminar reporte'); + } + }; + if (loading) { return (
@@ -81,30 +122,50 @@ export default function ClienteDetail() { return (
{/* Header */} -
- +
- {cliente.logo ? ( - {cliente.nombre_empresa} - ) : ( -
- 🏢 + +
+ {cliente.logo ? ( + {cliente.nombre_empresa} + ) : ( +
+ 🏢 +
+ )} +
+

{cliente.nombre_empresa}

+

{cliente.giro?.nombre} • {cliente.moneda}

- )} -
-

{cliente.nombre_empresa}

-

{cliente.giro?.nombre} • {cliente.moneda}

+ + {/* Acciones del cliente */} + {canManage && ( +
+ + +
+ )}
@@ -112,9 +173,11 @@ export default function ClienteDetail() {

Balanzas de Comprobación

- + {canManage && ( + + )}
{balanzas.length === 0 ? ( @@ -160,7 +223,7 @@ export default function ClienteDetail() {

Reportes

- {balanzasCompletadas.length >= 1 && ( + {canManage && balanzasCompletadas.length >= 1 && ( )} + {canManage && ( + + )}
- {/* Modales */} + {/* Modal: Subir Balanza */} {showUpload && cliente && (
@@ -247,6 +318,7 @@ export default function ClienteDetail() {
)} + {/* Modal: Generar Reporte */} {showGenerarReporte && cliente && (
@@ -270,6 +342,59 @@ export default function ClienteDetail() {
)} + + {/* Modal: Editar Cliente */} + {showEditCliente && cliente && ( +
+
+
+
+

Editar Cliente

+ +
+ setShowEditCliente(false)} + /> +
+
+
+ )} + + {/* Modal: Confirmar Eliminar */} + {showDeleteConfirm && ( +
+
+

Eliminar Cliente

+

+ ¿Estás seguro de eliminar a {cliente.nombre_empresa}? + Esta acción eliminará todas las balanzas y reportes asociados. +

+
+ + +
+
+
+ )}
); }