diff --git a/backlog/features.json b/backlog/features.json
index 183a397..9ef59d0 100644
--- a/backlog/features.json
+++ b/backlog/features.json
@@ -4525,13 +4525,15 @@
"Pack values are validated as positive integers",
"Typecheck, tests, verify pass"
],
- "status": "pending",
+ "status": "done",
"created_at": "2026-08-20",
"gates": {
- "reviewer": false,
- "security": false,
- "qa": false
- }
+ "reviewer": true,
+ "security": true,
+ "qa": true,
+ "close": true
+ },
+ "completed_at": "2026-08-21T10:02:15Z"
},
{
"id": "F-103",
diff --git a/project/apps/admin/src/app/(dashboard)/shipping/page.tsx b/project/apps/admin/src/app/(dashboard)/shipping/page.tsx
index 2427e2b..c333b99 100644
--- a/project/apps/admin/src/app/(dashboard)/shipping/page.tsx
+++ b/project/apps/admin/src/app/(dashboard)/shipping/page.tsx
@@ -85,6 +85,12 @@ function MethodRow({ method, onEdit, onDelete }: { method: ShippingMethod; onEdi
{method.zoneName} |
{fmt(method.baseCostCents)} |
{method.freeShippingThresholdCents ? `Gratis desde ${fmt(method.freeShippingThresholdCents)}` : '—'} |
+
+ {method.maxWeightKg !== null ? `Máx ${method.maxWeightKg} kg` : '—'}
+ {method.freeShippingMaxWeightKg !== null && (
+ gratis hasta {method.freeShippingMaxWeightKg} kg
+ )}
+ |
{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" /> |
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" /> |
+
+ 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" />
+ 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" />
+ |
|