feat(F-086): completed feature

This commit is contained in:
chattie
2026-08-20 06:13:37 +02:00
parent 7ece045e13
commit 822bc7546c
16 changed files with 201 additions and 27 deletions

View File

@@ -186,6 +186,9 @@ export default function ProductsPage() {
<th className="text-left text-xs font-semibold text-gray-500 uppercase tracking-wide px-4 py-3">
Marca
</th>
<th className="text-left text-xs font-semibold text-gray-500 uppercase tracking-wide px-4 py-3">
Caducidad
</th>
<th className="text-left text-xs font-semibold text-gray-500 uppercase tracking-wide px-4 py-3">
Estado
</th>
@@ -232,6 +235,27 @@ export default function ProductsPage() {
<td className="px-4 py-3">
<p className="text-sm text-gray-600">{p.brand?.name ?? '—'}</p>
</td>
<td className="px-4 py-3">
{p.expirationDate ? (
(() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const exp = new Date(p.expirationDate);
const expired = exp < today;
return (
<span
className={`text-sm ${expired ? 'text-red-600 font-semibold' : 'text-gray-600'}`}
title={expired ? 'Producto caducado' : 'Caduca el'}
>
{expired ? '⚠ ' : ''}
{exp.toLocaleDateString('es-ES')}
</span>
);
})()
) : (
<span className="text-sm text-gray-400"></span>
)}
</td>
<td className="px-4 py-3">
<StateBadge state={p.state} />
</td>

View File

@@ -64,6 +64,7 @@ export function ProductEditor({ productId }: ProductEditorProps) {
const [seoTitleManual, setSeoTitleManual] = useState(false);
const [seoDesc, setSeoDesc] = useState('');
const [seoDescManual, setSeoDescManual] = useState(false);
const [expirationDate, setExpirationDate] = useState('');
const [brands, setBrands] = useState<Brand[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
@@ -88,8 +89,8 @@ export function ProductEditor({ productId }: ProductEditorProps) {
* on every render — a fetch loop that broke checkbox selection.
*/
const getSnap = useCallback(() => JSON.stringify({
name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc,
}), [name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc]);
name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc, expirationDate,
}), [name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc, expirationDate]);
// Keep a ref to the latest getSnap so the load effect (which uses an empty
// deps list to avoid the loop) can still compute the initial snapshot.
@@ -109,6 +110,7 @@ export function ProductEditor({ productId }: ProductEditorProps) {
setState(p.state);
setSeoTitle((p as any).seoTitle ?? ''); setSeoTitleManual(true);
setSeoDesc((p as any).seoDescription ?? ''); setSeoDescManual(true);
setExpirationDate((p as any).expirationDate ?? '');
snapRef.current = getSnapRef.current();
setLoading(false);
}).catch(() => {
@@ -122,7 +124,7 @@ export function ProductEditor({ productId }: ProductEditorProps) {
useEffect(() => {
if (loading) return;
dirtyRef.current = getSnap() !== snapRef.current;
}, [name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc, loading, getSnap]);
}, [name, slug, desc, brandId, categoryIds, channels, featured, attributes, state, seoTitle, seoDesc, expirationDate, loading, getSnap]);
useEffect(() => {
const h = (e: BeforeUnloadEvent) => { if (dirtyRef.current) { e.preventDefault(); e.returnValue = ''; } };
@@ -151,6 +153,7 @@ export function ProductEditor({ productId }: ProductEditorProps) {
state,
seoTitle: seoTitle || undefined,
seoDescription: seoDesc || undefined,
expirationDate: expirationDate || undefined,
};
let saved: Product;
if (isCreate) saved = await productsApi.create(payload);
@@ -385,6 +388,21 @@ export function ProductEditor({ productId }: ProductEditorProps) {
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none resize-none" />
<div className="mt-1 text-xs text-gray-400">{seoDesc.length}/160</div>
</div>
{/* Fecha de caducidad */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-sm font-semibold text-gray-900">Fecha de caducidad</label>
<span className="text-xs text-gray-400">opcional</span>
</div>
<input
type="date"
value={expirationDate}
onChange={(e) => setExpirationDate(e.target.value)}
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none"
/>
<p className="mt-1 text-xs text-gray-400">Se mostrará en el listado de productos y en la tienda.</p>
</div>
</section>
)}

View File

@@ -34,6 +34,7 @@ export interface Product {
categoryIds?: string[];
brand?: { id: string; name: string; slug: string };
imageUrl?: string;
expirationDate?: string | null;
createdAt?: string;
updatedAt?: string;
}