feat(F-102): completed feature

This commit is contained in:
chattie
2026-08-21 12:02:15 +02:00
parent e46faa869a
commit 027cacd871
31 changed files with 510 additions and 80 deletions

View File

@@ -85,6 +85,12 @@ function MethodRow({ method, onEdit, onDelete }: { method: ShippingMethod; onEdi
<td className="px-6 py-4 text-sm text-gray-600">{method.zoneName}</td>
<td className="px-6 py-4 text-sm font-semibold text-gray-800">{fmt(method.baseCostCents)}</td>
<td className="px-6 py-4 text-sm text-gray-500">{method.freeShippingThresholdCents ? `Gratis desde ${fmt(method.freeShippingThresholdCents)}` : '—'}</td>
<td className="px-6 py-4 text-sm text-gray-500">
{method.maxWeightKg !== null ? `Máx ${method.maxWeightKg} kg` : '—'}
{method.freeShippingMaxWeightKg !== null && (
<span className="block text-xs text-gray-400">gratis hasta {method.freeShippingMaxWeightKg} kg</span>
)}
</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${method.active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}>
{method.active ? 'Activo' : 'Inactivo'}
@@ -103,6 +109,10 @@ function MethodForm({ zones, method, onSave, onCancel }: { zones: ShippingZone[]
const [cost, setCost] = useState(method ? String(method.baseCostCents / 100) : '');
const [threshold, setThreshold] = useState(method?.freeShippingThresholdCents ? String(method.freeShippingThresholdCents / 100) : '');
const [description, setDescription] = useState(method?.description ?? '');
const [maxWeight, setMaxWeight] = useState(method?.maxWeightKg != null ? String(method.maxWeightKg) : '');
const [freeMaxWeight, setFreeMaxWeight] = useState(
method?.freeShippingMaxWeightKg != null ? String(method.freeShippingMaxWeightKg) : '',
);
const [active, setActive] = useState(method?.active ?? true);
const [saving, setSaving] = useState(false);
const [err, setErr] = useState('');
@@ -113,12 +123,16 @@ function MethodForm({ zones, method, onSave, onCancel }: { zones: ShippingZone[]
const baseCostCents = Math.round(parseFloat(cost) * 100);
const freeThreshold = threshold ? Math.round(parseFloat(threshold) * 100) : null;
const descriptionPayload = description.trim() || null;
const maxWeightPayload = maxWeight.trim() ? parseFloat(maxWeight.replace(',', '.')) : null;
const freeMaxWeightPayload = freeMaxWeight.trim() ? parseFloat(freeMaxWeight.replace(',', '.')) : null;
if (method) {
await shippingApi.updateMethod(method.id, {
name,
baseCostCents,
freeShippingThresholdCents: freeThreshold,
description: descriptionPayload,
maxWeightKg: maxWeightPayload,
freeShippingMaxWeightKg: freeMaxWeightPayload,
active,
});
} else {
@@ -128,6 +142,8 @@ function MethodForm({ zones, method, onSave, onCancel }: { zones: ShippingZone[]
baseCostCents,
freeShippingThresholdCents: freeThreshold,
description: descriptionPayload,
maxWeightKg: maxWeightPayload,
freeShippingMaxWeightKg: freeMaxWeightPayload,
active,
});
}
@@ -152,6 +168,12 @@ function MethodForm({ zones, method, onSave, onCancel }: { zones: ShippingZone[]
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" /></td>
<td className="px-4 py-3"><input type="number" step="0.01" value={threshold} onChange={e => setThreshold(e.target.value)} placeholder="Sin gratis"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" /></td>
<td className="px-4 py-3">
<input type="number" step="0.001" min="0" value={maxWeight} onChange={e => setMaxWeight(e.target.value)} placeholder="Sin límite"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-xs focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
<input type="number" step="0.001" min="0" value={freeMaxWeight} onChange={e => setFreeMaxWeight(e.target.value)} placeholder="Gratis hasta kg"
className="w-full px-3 py-2 mt-1.5 border border-gray-300 rounded-lg text-xs focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
</td>
<td className="px-4 py-3">
<select value={String(active)} onChange={e => setActive(e.target.value === 'true')}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none">
@@ -284,6 +306,7 @@ export default function ShippingPage() {
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Zona</th>
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Coste</th>
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Envío gratis</th>
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Peso</th>
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Estado</th>
<th className="text-right px-6 py-3 text-xs font-semibold text-gray-500 uppercase">Acciones</th>
</tr>
@@ -296,7 +319,7 @@ export default function ShippingPage() {
<MethodForm zones={zones} method={editingMethod} onSave={() => { setEditingMethod(null); loadMethods(); }} onCancel={() => setEditingMethod(null)} />
)}
{methods.length === 0 && !showMethodForm ? (
<tr><td colSpan={6} className="px-6 py-12 text-center text-gray-400 text-sm">
<tr><td colSpan={7} className="px-6 py-12 text-center text-gray-400 text-sm">
{zones.length === 0 ? 'Crea primero una zona de envío' : 'Sin métodos de envío'}
</td></tr>
) : methods.map(m => (