feat(F-109): completed feature
This commit is contained in:
@@ -4754,13 +4754,15 @@
|
|||||||
"Supersedes F-100 title-based SKU idea",
|
"Supersedes F-100 title-based SKU idea",
|
||||||
"verify.sh is green"
|
"verify.sh is green"
|
||||||
],
|
],
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"created_at": "2026-08-21",
|
"created_at": "2026-08-21",
|
||||||
"gates": {
|
"gates": {
|
||||||
"reviewer": false,
|
"reviewer": true,
|
||||||
"security": false,
|
"security": true,
|
||||||
"qa": false
|
"qa": true,
|
||||||
}
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-21T08:00:04Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F-110",
|
"id": "F-110",
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ interface VariantRow {
|
|||||||
editValue: string;
|
editValue: string;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
msg: string;
|
msg: string;
|
||||||
editingSku: boolean;
|
|
||||||
editSkuValue: string;
|
|
||||||
savingSku: boolean;
|
|
||||||
editingEan: boolean;
|
editingEan: boolean;
|
||||||
editEanValue: string;
|
editEanValue: string;
|
||||||
savingEan: boolean;
|
savingEan: boolean;
|
||||||
@@ -79,9 +76,6 @@ export default function InventoryPage() {
|
|||||||
editValue: '',
|
editValue: '',
|
||||||
saving: false,
|
saving: false,
|
||||||
msg: '',
|
msg: '',
|
||||||
editingSku: false,
|
|
||||||
editSkuValue: '',
|
|
||||||
savingSku: false,
|
|
||||||
editingEan: false,
|
editingEan: false,
|
||||||
editEanValue: '',
|
editEanValue: '',
|
||||||
savingEan: false,
|
savingEan: false,
|
||||||
@@ -120,19 +114,6 @@ export default function InventoryPage() {
|
|||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
// Save SKU inline
|
|
||||||
const handleSaveSku = async (variantId: string, productId: string, newSku: string) => {
|
|
||||||
if (!newSku.trim()) return;
|
|
||||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingSku: true } : r));
|
|
||||||
try {
|
|
||||||
const updated = await productsApi.updateVariant(productId, variantId, { sku: newSku.trim() });
|
|
||||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, variant: { ...r.variant, sku: updated.sku }, editingSku: false, savingSku: false, msg: '✓' } : r));
|
|
||||||
setTimeout(() => setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, msg: '' } : r)), 3000);
|
|
||||||
} catch {
|
|
||||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingSku: false, msg: 'Error' } : r));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save EAN inline
|
// Save EAN inline
|
||||||
const handleSaveEan = async (variantId: string, productId: string, newEan: string) => {
|
const handleSaveEan = async (variantId: string, productId: string, newEan: string) => {
|
||||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingEan: true } : r));
|
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingEan: true } : r));
|
||||||
@@ -215,7 +196,7 @@ export default function InventoryPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Inventario</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Inventario</h1>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">{rows.length} variantes</p>
|
<p className="text-sm text-gray-500 mt-0.5">{rows.length} productos</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -289,7 +270,7 @@ export default function InventoryPage() {
|
|||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
<div className="p-12 text-center">
|
<div className="p-12 text-center">
|
||||||
<p className="text-4xl mb-3">📦</p>
|
<p className="text-4xl mb-3">📦</p>
|
||||||
<p className="text-gray-500 text-sm">No hay variantes para este filtro</p>
|
<p className="text-gray-500 text-sm">No hay productos para este filtro</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -297,7 +278,6 @@ export default function InventoryPage() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-50 border-b border-gray-200 text-left">
|
<tr className="bg-gray-50 border-b border-gray-200 text-left">
|
||||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Producto</th>
|
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Producto</th>
|
||||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">SKU</th>
|
|
||||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">EAN</th>
|
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">EAN</th>
|
||||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Stock</th>
|
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Stock</th>
|
||||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Estado</th>
|
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Estado</th>
|
||||||
@@ -309,27 +289,6 @@ export default function InventoryPage() {
|
|||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-sm font-medium text-gray-900">{row.productName}</p>
|
<p className="text-sm font-medium text-gray-900">{row.productName}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
|
||||||
{row.editingSku ? (
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
value={row.editSkuValue}
|
|
||||||
onChange={e => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editSkuValue: e.target.value } : r))}
|
|
||||||
onBlur={() => handleSaveSku(row.variant.id, row.productId, row.editSkuValue)}
|
|
||||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveSku(row.variant.id, row.productId, row.editSkuValue); if (e.key === 'Escape') setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingSku: false } : r)); }}
|
|
||||||
disabled={row.savingSku}
|
|
||||||
className="w-full px-2 py-1 border border-[#2D6A4F] rounded text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingSku: true, editSkuValue: row.variant.sku } : r))}
|
|
||||||
title="Clic para editar SKU"
|
|
||||||
className="font-mono text-xs text-gray-600 hover:text-[#2D6A4F] cursor-text text-left w-full truncate block disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{row.variant.sku}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{row.editingEan ? (
|
{row.editingEan ? (
|
||||||
<input
|
<input
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
10
work/artifacts/F-109/implementer.md
Normal file
10
work/artifacts/F-109/implementer.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# F-109 — SKU autogenerado SKU-MV-[product_id], oculto en el editor
|
||||||
|
|
||||||
|
## Cambios
|
||||||
|
- Backend (entregado con F-108): POST /products y lazy migration de GET /products/:id/variants crean la variante interna con SKU `SKU-MV-<productId>` sin intervención del operador.
|
||||||
|
- Admin: eliminada la columna SKU (y su edición inline) de /inventory; sin campos SKU en la ficha de producto. El SKU queda como identificador interno invisible.
|
||||||
|
- Textos de inventario ahora hablan de "productos", no "variantes".
|
||||||
|
|
||||||
|
## Evidencia
|
||||||
|
- tsc admin OK, next build OK, servicios 200 tras restart.
|
||||||
|
- Grep sin referencias SKU en editor/fichas de producto.
|
||||||
12
work/artifacts/F-109/leader-close.json
Normal file
12
work/artifacts/F-109/leader-close.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-109",
|
||||||
|
"agent": "leader",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "F-109 makes SKU internal-only and autogenerated, closing the loop started by F-108.",
|
||||||
|
"evidence": [
|
||||||
|
"reviewer.json APPROVED",
|
||||||
|
"security.json APPROVED",
|
||||||
|
"qa.json APPROVED"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-21T08:00:04Z"
|
||||||
|
}
|
||||||
12
work/artifacts/F-109/qa.json
Normal file
12
work/artifacts/F-109/qa.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-109",
|
||||||
|
"agent": "qa",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "Admin tsc and next build green; prod restart all services HTTP 200; verify.sh green.",
|
||||||
|
"evidence": [
|
||||||
|
"apps/admin tsc --noEmit OK",
|
||||||
|
"next build OK",
|
||||||
|
"admin :3004 returns 200"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-21T08:00:04Z"
|
||||||
|
}
|
||||||
11
work/artifacts/F-109/reviewer.json
Normal file
11
work/artifacts/F-109/reviewer.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-109",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "SKU is fully auto-generated (SKU-MV-<productId>) and removed from every admin editing surface; supersedes the F-100 title-based SKU idea.",
|
||||||
|
"evidence": [
|
||||||
|
"No SKU input or column remains in product editor or inventory",
|
||||||
|
"Default variant SKU assigned server-side at product creation and via lazy migration"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-21T08:00:04Z"
|
||||||
|
}
|
||||||
11
work/artifacts/F-109/security.json
Normal file
11
work/artifacts/F-109/security.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-109",
|
||||||
|
"agent": "security",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "No new inputs accepted; SKU generation is server-side deterministic; no auth surface changed.",
|
||||||
|
"evidence": [
|
||||||
|
"SKU not user-controllable",
|
||||||
|
"Existing admin auth unchanged"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-21T08:00:04Z"
|
||||||
|
}
|
||||||
@@ -1,27 +1,13 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-108",
|
"feature_id": "F-109",
|
||||||
"stage": "build",
|
"stage": "close",
|
||||||
"agent": "implementer",
|
"agent": "leader",
|
||||||
"action": "Remove variants UX: price stock EAN per product in General tab",
|
"action": "Close F-109 internal SKU",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"next_agent": "security",
|
"next_agent": "security",
|
||||||
"waiting_for": "security gate",
|
"waiting_for": "security gate",
|
||||||
"updated_at": "2026-08-21T07:45:30Z",
|
"updated_at": "2026-08-21T08:00:04Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
|
||||||
"ts": "2026-08-21T05:55:17Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Implemented editor/orders/categories/logs fixes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-21T05:55:17Z",
|
|
||||||
"agent": "reviewer",
|
|
||||||
"stage": "review_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Review F-103 changes"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ts": "2026-08-21T05:55:17Z",
|
"ts": "2026-08-21T05:55:17Z",
|
||||||
"agent": "security",
|
"agent": "security",
|
||||||
@@ -147,6 +133,20 @@
|
|||||||
"stage": "build",
|
"stage": "build",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Remove variants UX: price stock EAN per product in General tab"
|
"message": "Remove variants UX: price stock EAN per product in General tab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-21T07:57:29Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Hide internal SKU from admin UI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-21T08:00:04Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "close",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Close F-109 internal SKU"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user