feat(F-170): completed feature
This commit is contained in:
@@ -6902,13 +6902,15 @@
|
|||||||
"description": "Categories admin delete action does not work; restore guarded deletion and clear error feedback.",
|
"description": "Categories admin delete action does not work; restore guarded deletion and clear error feedback.",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"risk": "med",
|
"risk": "med",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"created_at": "2026-08-22",
|
"created_at": "2026-08-22",
|
||||||
"gates": {
|
"gates": {
|
||||||
"reviewer": false,
|
"reviewer": true,
|
||||||
"security": false,
|
"security": true,
|
||||||
"qa": false
|
"qa": true,
|
||||||
}
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-22T17:13:31Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F-171",
|
"id": "F-171",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function slugify(text: string): string {
|
|||||||
.replace(/^-+|-+$/g, '');
|
.replace(/^-+|-+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryRow({ cat, onEdit, onDelete }: { cat: Category; onEdit: (c: Category) => void; onDelete: (id: string) => void }) {
|
function CategoryRow({ cat, onEdit, onDelete, deleting }: { cat: Category; onEdit: (c: Category) => void; onDelete: (category: Category) => void; deleting: boolean }) {
|
||||||
return (
|
return (
|
||||||
<tr className="hover:bg-gray-50 transition-colors">
|
<tr className="hover:bg-gray-50 transition-colors">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
@@ -45,7 +45,7 @@ function CategoryRow({ cat, onEdit, onDelete }: { cat: Category; onEdit: (c: Cat
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => onDelete(cat.id)} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Eliminar">
|
<button disabled={deleting} onClick={() => onDelete(cat)} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors disabled:cursor-wait disabled:opacity-40" title="Eliminar">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -117,6 +117,7 @@ export default function CategoriesPage() {
|
|||||||
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -239,13 +240,22 @@ export default function CategoriesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (category: Category) => {
|
||||||
if (!confirm('¿Eliminar esta categoría?')) return;
|
const childCount = category.children?.length ?? 0;
|
||||||
|
const warning = childCount > 0
|
||||||
|
? `¿Eliminar “${category.name}” y sus ${childCount} subcategorías? También se retirarán sus asociaciones con productos.`
|
||||||
|
: `¿Eliminar “${category.name}”? También se retirarán sus asociaciones con productos.`;
|
||||||
|
if (!confirm(warning)) return;
|
||||||
|
setDeletingId(category.id);
|
||||||
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
await categoriesApi.delete(id);
|
await categoriesApi.delete(category.id);
|
||||||
load();
|
setMsg(`Categoría “${category.name}” eliminada`);
|
||||||
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e instanceof Error ? e.message : 'Error al eliminar');
|
setMsg(`Error al eliminar: ${e instanceof Error ? e.message : 'Error desconocido'}`);
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -507,7 +517,7 @@ export default function CategoriesPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{flat(tree).map((c) => (
|
{flat(tree).map((c) => (
|
||||||
<CategoryRow key={c.id} cat={c} onEdit={openEdit} onDelete={handleDelete} />
|
<CategoryRow key={c.id} cat={c} onEdit={openEdit} onDelete={handleDelete} deleting={deletingId === c.id} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (res.status === 204) return undefined as T;
|
||||||
return res.json() as Promise<T>;
|
return res.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
work/artifacts/F-170/architect.md
Normal file
3
work/artifacts/F-170/architect.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# F-170
|
||||||
|
|
||||||
|
Handle no-content centrally; category deletion retains server RBAC/cascade and explicit UI confirmation.
|
||||||
3
work/artifacts/F-170/documenter.md
Normal file
3
work/artifacts/F-170/documenter.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# F-170
|
||||||
|
|
||||||
|
Categorías se eliminan con confirmación y mensaje visible.
|
||||||
3
work/artifacts/F-170/implementer.md
Normal file
3
work/artifacts/F-170/implementer.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# F-170
|
||||||
|
|
||||||
|
Admin API client now accepts 204 without JSON parsing. Category delete warns about cascading children/product associations, prevents duplicate click, reloads and gives inline feedback. Runtime create 201/delete 204 empty body. Admin build/typecheck pass.
|
||||||
1
work/artifacts/F-170/leader-close.json
Normal file
1
work/artifacts/F-170/leader-close.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-170","agent":"leader","stage":"close","verdict":"APPROVED","checks":[{"item":"all gates/runtime/verify","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-170/qa.json
Normal file
1
work/artifacts/F-170/qa.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-170","agent":"qa","stage":"qa_gate","verdict":"APPROVED","checks":[{"item":"create/delete runtime","ok":true},{"item":"build/typecheck","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-170/reviewer.json
Normal file
1
work/artifacts/F-170/reviewer.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-170","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","checks":[{"item":"204 handling","ok":true},{"item":"guarded UI feedback","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-170/security.json
Normal file
1
work/artifacts/F-170/security.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-170","agent":"security","stage":"security_gate","verdict":"APPROVED","checks":[{"item":"server admin RBAC retained","ok":true}],"issues":[]}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# F-183 — Repair Almagro and storefront LAN development
|
# F-170 — Delete categories
|
||||||
|
|
||||||
Recompile Almagro with fontTools to regenerate valid cmap/table directories and WOFF2 container. Permit the configured LAN host as a Next development origin so chunks and HMR websocket are accepted.
|
Admin client must treat HTTP 204 as success instead of attempting JSON parsing. Category UI confirms cascading impact, disables duplicate deletion, reloads tree and shows persistent success/error feedback.
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-183",
|
"feature_id": "F-170",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"action": "close",
|
"action": "close",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||||
"updated_at": "2026-08-22T17:11:58Z",
|
"updated_at": "2026-08-22T17:13:31Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:10:44Z",
|
"ts": "2026-08-22T17:12:20Z",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"stage": "intake",
|
"stage": "intake",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Repair font tables and LAN dev origin"
|
"message": "Fix category delete 204 handling"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:10:44Z",
|
"ts": "2026-08-22T17:12:20Z",
|
||||||
"agent": "architect",
|
"agent": "architect",
|
||||||
"stage": "design",
|
"stage": "design",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "design"
|
"message": "design"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:10:44Z",
|
"ts": "2026-08-22T17:12:20Z",
|
||||||
"agent": "implementer",
|
"agent": "implementer",
|
||||||
"stage": "build",
|
"stage": "build",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Recompile font and allow LAN dev origin"
|
"message": "Implement reliable category deletion"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:11:58Z",
|
"ts": "2026-08-22T17:13:31Z",
|
||||||
"agent": "reviewer",
|
"agent": "reviewer",
|
||||||
"stage": "review_gate",
|
"stage": "review_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "review"
|
"message": "review"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:11:58Z",
|
"ts": "2026-08-22T17:13:31Z",
|
||||||
"agent": "security",
|
"agent": "security",
|
||||||
"stage": "security_gate",
|
"stage": "security_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "security"
|
"message": "security"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:11:58Z",
|
"ts": "2026-08-22T17:13:31Z",
|
||||||
"agent": "qa",
|
"agent": "qa",
|
||||||
"stage": "qa_gate",
|
"stage": "qa_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "qa"
|
"message": "qa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:11:58Z",
|
"ts": "2026-08-22T17:13:31Z",
|
||||||
"agent": "documenter",
|
"agent": "documenter",
|
||||||
"stage": "document",
|
"stage": "document",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "document"
|
"message": "document"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T17:11:58Z",
|
"ts": "2026-08-22T17:13:31Z",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
|
|||||||
Reference in New Issue
Block a user