feat(F-094): completed feature
This commit is contained in:
@@ -4230,6 +4230,40 @@
|
||||
"close": true
|
||||
},
|
||||
"completed_at": "2026-08-20T19:57:43Z"
|
||||
},
|
||||
{
|
||||
"id": "F-094",
|
||||
"type": "feature",
|
||||
"title": "Create and explain product variants from Publish tab",
|
||||
"problem": "Pricing and Inventory show a warning that variants are created from the Publish tab, but Publish only contains state controls and no variant UI. Operators do not know what a variant is or how to create the SKU/EAN records required for stock and pricing.",
|
||||
"goal": "Add a clear variant explanation and a create-variant form to Publish with SKU and optional EAN, plus a list of existing variants. Use the existing product variant API and refresh the list after creation.",
|
||||
"scope_in": [
|
||||
"admin ProductEditor Publish tab and variant management"
|
||||
],
|
||||
"scope_out": [
|
||||
"No database or backend API redesign",
|
||||
"no changes to variant uniqueness rules"
|
||||
],
|
||||
"priority": "high",
|
||||
"risk": "low",
|
||||
"description": "Problem: Pricing and Inventory show a warning that variants are created from the Publish tab, but Publish only contains state controls and no variant UI. Operators do not know what a variant is or how to create the SKU/EAN records required for stock and pricing.. Goal: Add a clear variant explanation and a create-variant form to Publish with SKU and optional EAN, plus a list of existing variants. Use the existing product variant API and refresh the list after creation.. Scope IN: admin ProductEditor Publish tab and variant management. Scope OUT: No database or backend API redesign, no changes to variant uniqueness rules. Type: feature. Priority: high. Risk: low.",
|
||||
"acceptance": [
|
||||
"- Publish tab explains that a variant is a sellable presentation of a product with its own SKU/EAN",
|
||||
"- Publish tab lets an operator create a variant with required SKU and optional EAN",
|
||||
"- Successful creation shows the new variant in the list without a page reload",
|
||||
"- Duplicate SKU/EAN errors are shown clearly",
|
||||
"- Pricing and Inventory no longer point to a nonexistent variant workflow",
|
||||
"- Admin typecheck/lint and verify.sh pass"
|
||||
],
|
||||
"status": "done",
|
||||
"created_at": "2026-08-20",
|
||||
"gates": {
|
||||
"reviewer": true,
|
||||
"security": true,
|
||||
"qa": true,
|
||||
"close": true
|
||||
},
|
||||
"completed_at": "2026-08-20T20:01:54Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { productsApi, brandsApi, categoriesApi } from '@/lib/api-client';
|
||||
import { ImagesSection } from './sections/ImagesSection';
|
||||
import { InventorySection } from './sections/InventorySection';
|
||||
import { PricingSection } from './sections/PricingSection';
|
||||
import { VariantManager } from './sections/VariantManager';
|
||||
import LexicalEditor from '@/features/cms/components/LexicalEditor';
|
||||
|
||||
interface ProductEditorProps {
|
||||
@@ -408,6 +409,13 @@ export function ProductEditor({ productId }: ProductEditorProps) {
|
||||
{/* ── PUBLISH ── */}
|
||||
{tab === 'publish' && (
|
||||
<section className="space-y-5">
|
||||
{productId ? (
|
||||
<VariantManager productId={productId} />
|
||||
) : (
|
||||
<div className="p-6 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
|
||||
⚠️ Guarda primero el producto para crear variantes.
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-3">Estado del producto</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
|
||||
@@ -407,7 +407,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
<p className="text-4xl mb-3">📦</p>
|
||||
<p className="text-gray-500 text-sm">Este producto no tiene variantes</p>
|
||||
<p className="text-gray-400 text-xs mt-1">
|
||||
Las variantes se crean desde la pestaña Publicar
|
||||
Ve a Publicar para crear la primera variante
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -114,7 +114,7 @@ export function PricingSection({ productId }: { productId: string }) {
|
||||
if (variants.length === 0) {
|
||||
return (
|
||||
<div className="p-6 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
|
||||
⚠️ Este producto no tiene variantes. Las variantes se crean desde la pestaña Publicar.
|
||||
⚠️ Este producto no tiene variantes. Ve a Publicar para crear la primera variante.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ApiError, type ProductVariant } from '@/types';
|
||||
import { productsApi } from '@/lib/api-client';
|
||||
|
||||
interface VariantManagerProps {
|
||||
productId: string;
|
||||
}
|
||||
|
||||
export function VariantManager({ productId }: VariantManagerProps) {
|
||||
const [variants, setVariants] = useState<ProductVariant[]>([]);
|
||||
const [sku, setSku] = useState('');
|
||||
const [ean, setEan] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await productsApi.getVariants(productId);
|
||||
setVariants(result.items ?? []);
|
||||
} catch {
|
||||
setError('No se pudieron cargar las variantes.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [productId]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const createVariant = async () => {
|
||||
const normalizedSku = sku.trim();
|
||||
const normalizedEan = ean.trim();
|
||||
if (!normalizedSku) {
|
||||
setError('El SKU es obligatorio.');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
const created = await productsApi.createVariant(productId, {
|
||||
sku: normalizedSku,
|
||||
ean: normalizedEan || null,
|
||||
});
|
||||
setVariants((prev) => [...prev, created]);
|
||||
setSku('');
|
||||
setEan('');
|
||||
setMessage('Variante creada correctamente.');
|
||||
} catch (cause) {
|
||||
setError(
|
||||
cause instanceof ApiError && cause.statusCode === 409
|
||||
? 'El SKU o EAN ya existe en otra variante.'
|
||||
: 'No se pudo crear la variante.',
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="p-4 bg-blue-50 border border-blue-100 rounded-xl text-sm text-blue-800">
|
||||
<p className="font-semibold mb-1">¿Qué es una variante?</p>
|
||||
<p>
|
||||
Es una presentación vendible del producto. Cada formato, tamaño o referencia tiene su propio
|
||||
SKU y, opcionalmente, EAN; sobre la variante se gestionan precios, stock e inventario.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-white border border-gray-200 rounded-xl space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Crear variante</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Por ejemplo, un formato o tamaño distinto del mismo producto.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
SKU <span className="text-red-500">*</span>
|
||||
<input
|
||||
value={sku}
|
||||
onChange={(event) => setSku(event.target.value)}
|
||||
placeholder="Ej: PROTEINA-GUISANTE-500G"
|
||||
className="mt-1 w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
EAN <span className="text-gray-400 font-normal">(opcional)</span>
|
||||
<input
|
||||
value={ean}
|
||||
onChange={(event) => setEan(event.target.value)}
|
||||
placeholder="Código de barras"
|
||||
className="mt-1 w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={createVariant}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-[#2D6A4F] text-white text-sm font-semibold rounded-lg hover:bg-[#1B4332] disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Creando...' : 'Crear variante'}
|
||||
</button>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{message && <p className="text-sm text-green-600">{message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-white border border-gray-200 rounded-xl">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Variantes existentes</h3>
|
||||
{loading ? <p className="text-sm text-gray-400">Cargando...</p> : variants.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Todavía no hay variantes. Crea la primera arriba.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant.id} className="flex items-center justify-between px-3 py-2 bg-gray-50 rounded-lg text-sm">
|
||||
<span className="font-mono text-gray-700">{variant.sku}</span>
|
||||
<span className="font-mono text-gray-500">{variant.ean ?? 'Sin EAN'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -75,6 +75,8 @@ export const productsApi = {
|
||||
get: (id: string) => api.get<import('@/types').Product>(`/api/products/${id}`),
|
||||
getVariants: (id: string) =>
|
||||
api.get<{ items: import('@/types').ProductVariant[] }>(`/api/products/${id}/variants`),
|
||||
createVariant: (productId: string, data: { sku: string; ean?: string | null }) =>
|
||||
api.post<import('@/types').ProductVariant>(`/api/products/${productId}/variants`, data),
|
||||
create: (data: unknown) => api.post<import('@/types').Product>('/api/products', data),
|
||||
update: (id: string, data: unknown) =>
|
||||
api.patch<import('@/types').Product>(`/api/products/${id}`, data),
|
||||
|
||||
File diff suppressed because one or more lines are too long
14
work/artifacts/F-094/implementer.md
Normal file
14
work/artifacts/F-094/implementer.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# F-094 — Implementer evidence
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `VariantManager` to the Publish tab with a plain-language explanation of variants, SKU, and EAN.
|
||||
- Added SKU-required/EAN-optional variant creation using the existing `POST /products/:id/variants` API.
|
||||
- Added existing variant list and inline success/conflict feedback.
|
||||
- Added `productsApi.createVariant`.
|
||||
- Updated Pricing and Inventory empty states to point to the now-existing Publish workflow.
|
||||
|
||||
## Validation
|
||||
|
||||
- Admin `npx tsc --noEmit` → exit 0
|
||||
- Admin ESLint on touched files → 0 errors (pre-existing warnings only)
|
||||
14
work/artifacts/F-094/leader-close.json
Normal file
14
work/artifacts/F-094/leader-close.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"feature_id": "F-094",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "F-094 adds an understandable variant creation workflow to Publish and fixes the misleading empty-state guidance.",
|
||||
"evidence": [
|
||||
"reviewer.json verdict=APPROVED",
|
||||
"security.json verdict=APPROVED",
|
||||
"qa.json verdict=APPROVED",
|
||||
"Root tests: 133 passed, 56 skipped",
|
||||
"scripts/verify.sh exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-20T20:01:50Z"
|
||||
}
|
||||
13
work/artifacts/F-094/qa.json
Normal file
13
work/artifacts/F-094/qa.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"feature_id": "F-094",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Publish variant management passes admin checks, the root test suite, and harness verification.",
|
||||
"evidence": [
|
||||
"Admin npx tsc --noEmit exit 0",
|
||||
"Admin ESLint touched files exit 0 with warnings only",
|
||||
"Root tests: 133 passed, 56 skipped",
|
||||
"scripts/verify.sh exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-20T20:01:40Z"
|
||||
}
|
||||
14
work/artifacts/F-094/reviewer.json
Normal file
14
work/artifacts/F-094/reviewer.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"feature_id": "F-094",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Publish now contains a clear variant workflow backed by the existing create/list endpoints, and empty states point to a real action.",
|
||||
"evidence": [
|
||||
"VariantManager explains sellable variants, SKU, and EAN",
|
||||
"Create form uses SKU required and EAN optional",
|
||||
"Successful creation updates the list without reload",
|
||||
"Duplicate API conflicts are shown to the operator",
|
||||
"Pricing and Inventory messages no longer reference a missing UI"
|
||||
],
|
||||
"timestamp": "2026-08-20T20:01:10Z"
|
||||
}
|
||||
13
work/artifacts/F-094/security.json
Normal file
13
work/artifacts/F-094/security.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"feature_id": "F-094",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "The UI uses the existing authenticated admin variant endpoint; server-side SKU/EAN validation and uniqueness remain authoritative.",
|
||||
"evidence": [
|
||||
"No new backend route or authorization bypass",
|
||||
"SKU is required in the UI and backend schema validates all fields",
|
||||
"Duplicate SKU/EAN remains rejected by database constraints",
|
||||
"No secrets, unsafe HTML, or dependencies added"
|
||||
],
|
||||
"timestamp": "2026-08-20T20:01:20Z"
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
# Feature actual
|
||||
|
||||
## Feature activa: F-093 (in_progress) — Inventory search by product name or EAN
|
||||
## Feature activa: F-094 (in_progress) — Create and explain product variants from Publish tab
|
||||
|
||||
Backlog: 161 features (151 done, 9 pending, 1 in_progress).
|
||||
Backlog: 162 features (152 done, 9 pending, 1 in_progress).
|
||||
|
||||
Últimas features cerradas: **F-080**, **F-081**, **F-082**, **F-083**, **F-084**, **F-085**, **F-086**, **F-087**.
|
||||
|
||||
## Incidencia actual (2026-08-20)
|
||||
|
||||
La búsqueda de Inventario envía `q`, pero el listado admin solo filtra por nombre de producto. F-093 añade coincidencias por EAN de variante y actualiza el placeholder a EAN o nombre.
|
||||
Precios e Inventario indican que las variantes se crean desde Publicar, pero Publicar no ofrece creación ni explicación. F-094 añade la gestión de variantes allí.
|
||||
|
||||
## Última incidencia resuelta (2026-08-20)
|
||||
|
||||
F-093 cerrada con todos los gates aprobados. Inventario busca ahora por EAN o nombre de producto.
|
||||
|
||||
## Incidencia anterior (2026-08-20)
|
||||
|
||||
La búsqueda de Inventario enviaba `q`, pero el listado admin solo filtraba por nombre de producto.
|
||||
|
||||
## Última incidencia resuelta (2026-08-20)
|
||||
|
||||
|
||||
@@ -1,62 +1,13 @@
|
||||
{
|
||||
"feature_id": "F-093",
|
||||
"feature_id": "F-094",
|
||||
"stage": "close",
|
||||
"agent": "leader",
|
||||
"action": "Validate F-093 gates and close inventory EAN/name search",
|
||||
"action": "Validate F-094 gates and close variant workflow",
|
||||
"state": "running",
|
||||
"next_agent": "leader",
|
||||
"waiting_for": "verify.sh green",
|
||||
"updated_at": "2026-08-20T19:57:34Z",
|
||||
"updated_at": "2026-08-20T20:01:42Z",
|
||||
"timeline": [
|
||||
{
|
||||
"ts": "2026-08-20T19:44:59Z",
|
||||
"agent": "leader",
|
||||
"stage": "intake",
|
||||
"state": "running",
|
||||
"message": "Triage repeated variant PATCH 409 and opaque SKU/EAN conflict feedback"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:45:13Z",
|
||||
"agent": "implementer",
|
||||
"stage": "build",
|
||||
"state": "running",
|
||||
"message": "Add field-specific SKU/EAN conflict messages and guard duplicate in-flight saves"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:46:54Z",
|
||||
"agent": "reviewer",
|
||||
"stage": "review_gate",
|
||||
"state": "running",
|
||||
"message": "Review duplicate variant conflict mapping and in-flight save guard"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:47:03Z",
|
||||
"agent": "security",
|
||||
"stage": "security_gate",
|
||||
"state": "running",
|
||||
"message": "Check F-091 uniqueness enforcement and error disclosure"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:47:11Z",
|
||||
"agent": "qa",
|
||||
"stage": "qa_gate",
|
||||
"state": "running",
|
||||
"message": "Run F-091 typecheck, tests, lint, and verify"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:47:22Z",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"state": "running",
|
||||
"message": "Validate F-091 gates and close SKU/EAN conflict fix"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:47:40Z",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"state": "done",
|
||||
"message": "F-091 cerrado: conflictos SKU/EAN identificados y doble envío prevenido"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:48:12Z",
|
||||
"agent": "leader",
|
||||
@@ -147,6 +98,55 @@
|
||||
"stage": "close",
|
||||
"state": "running",
|
||||
"message": "Validate F-093 gates and close inventory EAN/name search"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:58:08Z",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"state": "done",
|
||||
"message": "F-093 cerrado: búsqueda de Inventario por EAN o nombre"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:58:26Z",
|
||||
"agent": "leader",
|
||||
"stage": "intake",
|
||||
"state": "running",
|
||||
"message": "Triage missing variant creation UI and explanation in Publish tab"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T19:58:57Z",
|
||||
"agent": "implementer",
|
||||
"stage": "build",
|
||||
"state": "running",
|
||||
"message": "Add Publish variant manager, API create method, and clear empty-state guidance"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T20:00:59Z",
|
||||
"agent": "reviewer",
|
||||
"stage": "review_gate",
|
||||
"state": "running",
|
||||
"message": "Review Publish variant creation UX and API integration"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T20:01:10Z",
|
||||
"agent": "security",
|
||||
"stage": "security_gate",
|
||||
"state": "running",
|
||||
"message": "Check variant creation validation, uniqueness, and authorization"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T20:01:22Z",
|
||||
"agent": "qa",
|
||||
"stage": "qa_gate",
|
||||
"state": "running",
|
||||
"message": "Run variant manager typecheck, tests, lint, and verify"
|
||||
},
|
||||
{
|
||||
"ts": "2026-08-20T20:01:42Z",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"state": "running",
|
||||
"message": "Validate F-094 gates and close variant workflow"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user