feat(F-048): completed feature
This commit is contained in:
@@ -1,8 +1,114 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { customersApi } from '@/lib/api-client';
|
||||
import type { Customer } from '@/types';
|
||||
import type { Customer, Address } from '@/types';
|
||||
|
||||
// ── FIX-18: edición de datos personales (no credenciales) + CRUD de direcciones ──
|
||||
|
||||
interface AddressFormState {
|
||||
label: string;
|
||||
recipientName: string;
|
||||
street: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_ADDRESS: AddressFormState = {
|
||||
label: '',
|
||||
recipientName: '',
|
||||
street: '',
|
||||
city: '',
|
||||
postalCode: '',
|
||||
country: '',
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
function AddressForm({
|
||||
initial,
|
||||
onSave,
|
||||
onCancel,
|
||||
saving,
|
||||
}: {
|
||||
initial?: Address;
|
||||
onSave: (data: AddressFormState) => void;
|
||||
onCancel: () => void;
|
||||
saving: boolean;
|
||||
}) {
|
||||
const [f, setF] = useState<AddressFormState>(
|
||||
initial
|
||||
? {
|
||||
label: initial.label ?? '',
|
||||
recipientName: initial.recipientName,
|
||||
street: initial.street,
|
||||
city: initial.city,
|
||||
postalCode: initial.postalCode,
|
||||
country: initial.country,
|
||||
isDefault: initial.isDefault ?? false,
|
||||
}
|
||||
: EMPTY_ADDRESS,
|
||||
);
|
||||
|
||||
const set = (k: keyof AddressFormState) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setF((prev) => ({ ...prev, [k]: e.target.value }));
|
||||
|
||||
const valid = f.recipientName && f.street && f.city && f.postalCode && f.country;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-xl p-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Etiqueta</label>
|
||||
<input value={f.label} onChange={set('label')} placeholder="Casa, Oficina…"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Destinatario *</label>
|
||||
<input value={f.recipientName} onChange={set('recipientName')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Dirección *</label>
|
||||
<input value={f.street} onChange={set('street')} placeholder="Calle, número, piso"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Ciudad *</label>
|
||||
<input value={f.city} onChange={set('city')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Código postal *</label>
|
||||
<input value={f.postalCode} onChange={set('postalCode')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">País *</label>
|
||||
<input value={f.country} onChange={set('country')} placeholder="España"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
|
||||
<input type="checkbox" checked={f.isDefault} onChange={(e) => setF((p) => ({ ...p, isDefault: e.target.checked }))}
|
||||
className="rounded text-[#2D6A4F] focus:ring-[#2D6A4F]" />
|
||||
Predeterminada
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => onSave(f)} disabled={!valid || saving}
|
||||
className="px-4 py-2 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-lg">
|
||||
{saving ? 'Guardando…' : 'Guardar dirección'}
|
||||
</button>
|
||||
<button onClick={onCancel} className="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-lg hover:bg-white">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CustomerDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -10,29 +116,98 @@ export default function CustomerDetailPage() {
|
||||
const [customer, setCustomer] = useState<Customer | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
customersApi.get(id).then(setCustomer).catch(() => setError('No se encontró el cliente')).finally(() => setLoading(false));
|
||||
// Personal data (no credenciales)
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [savingProfile, setSavingProfile] = useState(false);
|
||||
|
||||
// Addresses
|
||||
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||
const [showAddressForm, setShowAddressForm] = useState(false);
|
||||
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||||
const [savingAddress, setSavingAddress] = useState(false);
|
||||
|
||||
const loadAddresses = useCallback(async () => {
|
||||
try {
|
||||
const d = await customersApi.listAddresses(id);
|
||||
setAddresses(d.items ?? []);
|
||||
} catch {
|
||||
/* silencioso */
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!customer) return;
|
||||
setSaving(true);
|
||||
setMsg('');
|
||||
useEffect(() => {
|
||||
customersApi
|
||||
.get(id)
|
||||
.then((c) => {
|
||||
setCustomer(c);
|
||||
setDisplayName(c.displayName ?? '');
|
||||
setPhone(c.phone ?? '');
|
||||
})
|
||||
.catch(() => setError('No se encontró el cliente'))
|
||||
.finally(() => setLoading(false));
|
||||
loadAddresses();
|
||||
}, [id, loadAddresses]);
|
||||
|
||||
const flash = (text: string) => {
|
||||
setMsg(text);
|
||||
setTimeout(() => setMsg(''), 3000);
|
||||
};
|
||||
|
||||
const saveProfile = async () => {
|
||||
setSavingProfile(true);
|
||||
try {
|
||||
// PATCH /users/:id for profile fields (displayName, phone — role changes require separate process)
|
||||
await fetch(`/api/customers/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: customer.role }),
|
||||
const updated = await customersApi.update(id, {
|
||||
displayName: displayName || undefined,
|
||||
phone: phone || undefined,
|
||||
});
|
||||
setMsg('Cliente actualizado');
|
||||
setCustomer(updated);
|
||||
flash('Datos personales guardados');
|
||||
} catch {
|
||||
setMsg('Error al guardar');
|
||||
flash('Error al guardar');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setSavingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAddress = async (data: AddressFormState) => {
|
||||
setSavingAddress(true);
|
||||
try {
|
||||
const payload = {
|
||||
label: data.label || null,
|
||||
recipientName: data.recipientName,
|
||||
street: data.street,
|
||||
city: data.city,
|
||||
postalCode: data.postalCode,
|
||||
country: data.country,
|
||||
isDefault: data.isDefault,
|
||||
};
|
||||
if (editingAddress) {
|
||||
await customersApi.updateAddress(id, editingAddress.id, payload);
|
||||
flash('Dirección actualizada');
|
||||
} else {
|
||||
await customersApi.createAddress(id, payload);
|
||||
flash('Dirección creada');
|
||||
}
|
||||
setShowAddressForm(false);
|
||||
setEditingAddress(null);
|
||||
loadAddresses();
|
||||
} catch {
|
||||
flash('Error al guardar dirección');
|
||||
} finally {
|
||||
setSavingAddress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAddress = async (addressId: string) => {
|
||||
if (!confirm('¿Eliminar esta dirección?')) return;
|
||||
try {
|
||||
await customersApi.deleteAddress(id, addressId);
|
||||
loadAddresses();
|
||||
} catch {
|
||||
flash('Error al eliminar dirección');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,49 +215,108 @@ export default function CustomerDetailPage() {
|
||||
if (error || !customer) return <div className="p-8 text-red-600">{error || 'No encontrado'}</div>;
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-2xl space-y-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => router.push('/customers')} className="text-sm text-gray-500 hover:text-gray-700">← Clientes</button>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{customer.email}</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 space-y-5">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Email</p>
|
||||
<p className="text-sm text-gray-900">{customer.email}</p>
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm ${msg.startsWith('Error') ? 'bg-red-50 text-red-700' : 'bg-green-50 text-green-700'}`}>{msg}</div>
|
||||
)}
|
||||
|
||||
{/* Datos personales */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||
<h2 className="text-base font-semibold text-gray-800">Datos personales</h2>
|
||||
<p className="text-xs text-gray-400 mt-0.5">El email y las credenciales de acceso no se pueden modificar desde aquí.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Rol</p>
|
||||
<select
|
||||
value={customer.role}
|
||||
onChange={(e) => setCustomer({ ...customer, role: e.target.value as 'customer' | 'admin' })}
|
||||
className="px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white"
|
||||
>
|
||||
<option value="customer">Cliente</option>
|
||||
<option value="admin">Administrador</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Registrado</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
{customer.createdAt ? new Date(customer.createdAt).toLocaleString('es-ES') : '—'}
|
||||
</p>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email (solo lectura)</label>
|
||||
<input value={customer.email} disabled
|
||||
className="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50 text-gray-400" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Nombre / Razón social</label>
|
||||
<input value={displayName} onChange={(e) => setDisplayName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Teléfono</label>
|
||||
<input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="+34 600 000 000"
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-400">
|
||||
Registrado: {customer.createdAt ? new Date(customer.createdAt).toLocaleString('es-ES') : '—'}
|
||||
</p>
|
||||
<button onClick={saveProfile} disabled={savingProfile}
|
||||
className="px-5 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl">
|
||||
{savingProfile ? 'Guardando…' : 'Guardar datos personales'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm ${msg.startsWith('Error') ? 'bg-red-50 text-red-700' : 'bg-green-50 text-green-700'}`}>
|
||||
{msg}
|
||||
{/* Direcciones */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-gray-800">Direcciones ({addresses.length})</h2>
|
||||
{!showAddressForm && (
|
||||
<button onClick={() => { setEditingAddress(null); setShowAddressForm(true); }}
|
||||
className="px-3 py-1.5 bg-[#2D6A4F] hover:bg-[#1B4332] text-white text-sm font-semibold rounded-lg">
|
||||
+ Añadir dirección
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6 space-y-4">
|
||||
{showAddressForm && (
|
||||
<AddressForm
|
||||
initial={editingAddress ?? undefined}
|
||||
onSave={handleSaveAddress}
|
||||
onCancel={() => { setShowAddressForm(false); setEditingAddress(null); }}
|
||||
saving={savingAddress}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-6 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors"
|
||||
>
|
||||
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||
</button>
|
||||
{addresses.length === 0 && !showAddressForm ? (
|
||||
<p className="text-sm text-gray-400 text-center py-6">Este cliente no tiene direcciones.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{addresses.map((a) => (
|
||||
<div key={a.id} className="border border-gray-200 rounded-xl p-4 flex items-start justify-between gap-4">
|
||||
<div className="text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-gray-900">{a.recipientName}</p>
|
||||
{a.label && <span className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded-full">{a.label}</span>}
|
||||
{a.isDefault && <span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">Predeterminada</span>}
|
||||
</div>
|
||||
<p className="text-gray-600 mt-1">{a.street}</p>
|
||||
<p className="text-gray-500">{a.postalCode} {a.city}, {a.country}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button onClick={() => { setEditingAddress(a); setShowAddressForm(true); }} title="Editar" aria-label="Editar"
|
||||
className="p-1.5 text-gray-400 hover:text-[#2D6A4F] hover:bg-green-50 rounded-lg transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => handleDeleteAddress(a.id)} title="Eliminar" aria-label="Eliminar"
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { Customer } from '@/types';
|
||||
import { customersApi } from '@/lib/api-client';
|
||||
import { RowActions } from '@/components/ui/RowActions';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -84,69 +86,9 @@ function CreateForm({ onClose, onCreated }: { onClose: () => void; onCreated: ()
|
||||
);
|
||||
}
|
||||
|
||||
// ── Formulario editar cliente ────────────────────────────────────────────────
|
||||
function EditForm({ customer, onClose, onSaved }: { customer: Customer; onClose: () => void; onSaved: () => void }) {
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(customer.displayName || '');
|
||||
setPhone(customer.phone || '');
|
||||
}, [customer]);
|
||||
|
||||
const handle = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSaving(true); setError('');
|
||||
try {
|
||||
await customersApi.update(customer.id, {
|
||||
displayName: displayName || undefined,
|
||||
phone: phone || undefined,
|
||||
});
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al guardar');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handle} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<input type="email" value={customer.email} disabled
|
||||
className="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50 text-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Nombre / Razón social</label>
|
||||
<input type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Teléfono</label>
|
||||
<input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="+34 600 000 000"
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600 bg-red-50 rounded-xl px-4 py-2">{error}</p>}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="submit" disabled={saving}
|
||||
className="flex-1 px-5 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors">
|
||||
{saving ? 'Guardando...' : 'Guardar cambios'}
|
||||
</button>
|
||||
<button type="button" onClick={onClose}
|
||||
className="px-5 py-2.5 border border-gray-300 text-gray-600 text-sm rounded-xl hover:bg-gray-50 transition-colors">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Página principal ───────────────────────────────────────────────────────────
|
||||
export default function CustomersPage() {
|
||||
const router = useRouter();
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
@@ -155,7 +97,6 @@ export default function CustomersPage() {
|
||||
const [page, setPage] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [editing, setEditing] = useState<Customer | null>(null);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -181,7 +122,6 @@ export default function CustomersPage() {
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const handleCreated = () => { setMsg('Cliente creado correctamente'); setTimeout(() => setMsg(''), 3000); load(); };
|
||||
const handleSaved = () => { setMsg('Cliente actualizado'); setTimeout(() => setMsg(''), 3000); load(); };
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
@@ -255,8 +195,7 @@ export default function CustomersPage() {
|
||||
{c.createdAt ? new Date(c.createdAt).toLocaleDateString('es-ES') : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<button onClick={() => setEditing(c)}
|
||||
className="text-xs text-[#2D6A4F] hover:underline">Editar</button>
|
||||
<RowActions onEdit={() => router.push(`/customers/${c.id}`)} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -287,12 +226,6 @@ export default function CustomersPage() {
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Modal editar */}
|
||||
{editing && (
|
||||
<Modal title={`Editar: ${editing.email}`} onClose={() => setEditing(null)}>
|
||||
<EditForm customer={editing} onClose={() => setEditing(null)} onSaved={handleSaved} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user