diff --git a/app/[locale]/admin/sellers/[sellerId]/page.module.css b/app/[locale]/admin/sellers/[sellerId]/page.module.css new file mode 100644 index 00000000..04a62cec --- /dev/null +++ b/app/[locale]/admin/sellers/[sellerId]/page.module.css @@ -0,0 +1,114 @@ +.container { + max-width: 760px; + margin: 0 auto; + padding: var(--spacing-xl); +} + +.header { + margin-bottom: var(--spacing-lg); +} + +.backLink { + display: inline-flex; + margin-bottom: var(--spacing-sm); + color: var(--color-green-dark); + font-weight: var(--font-weight-medium); +} + +.backLink:hover { + text-decoration: underline; +} + +.kicker { + margin: 0 0 var(--spacing-xs); + color: var(--color-lila); + font-size: 0.9rem; +} + +.title { + margin: 0; + font-size: 1.75rem; + color: var(--color-green-dark); +} + +.summary { + display: grid; + gap: var(--spacing-md); + margin-bottom: var(--spacing-xl); + padding: var(--spacing-lg); + border-radius: 12px; + background: var(--color-white); + box-shadow: 0 1px 4px rgb(0 0 0 / 0.08); +} + +.summaryRow { + display: grid; + gap: var(--spacing-xs); +} + +.summaryLabel { + font-size: 0.8rem; + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-lila); +} + +.summaryValue { + margin: 0; + font-size: 1rem; + color: #222; +} + +.form { + display: grid; + gap: var(--spacing-md); + padding: var(--spacing-lg); + border-radius: 12px; + background: var(--color-white); + box-shadow: 0 1px 4px rgb(0 0 0 / 0.08); +} + +.sectionTitle { + margin: 0; + font-size: 1.2rem; + color: var(--color-green-dark); +} + +.field { + display: grid; + gap: var(--spacing-xs); +} + +.label { + font-size: 0.9rem; + font-weight: var(--font-weight-medium); + color: #222; +} + +.input, +.textarea { + width: 100%; + padding: 0.8rem 0.95rem; + border: 1px solid var(--color-lila); + border-radius: 8px; + background: var(--color-white); + color: #222; +} + +.textarea { + resize: vertical; + min-height: 120px; +} + +.successMessage { + padding: 0.75rem 1rem; + border-radius: 8px; + background: rgb(193 224 140 / 0.25); + color: var(--color-green-dark); +} + +.formActions { + display: flex; + justify-content: flex-start; +} diff --git a/app/[locale]/admin/sellers/[sellerId]/page.tsx b/app/[locale]/admin/sellers/[sellerId]/page.tsx new file mode 100644 index 00000000..45af7beb --- /dev/null +++ b/app/[locale]/admin/sellers/[sellerId]/page.tsx @@ -0,0 +1,80 @@ +import { redirect, notFound } from 'next/navigation'; +import Link from 'next/link'; +import { container } from '@/composition-root/container'; +import { assertRole } from '@/shared/authorization/authorization'; +import { getDictionary } from '@/shared/i18n/get-dictionary'; +import { NotFoundError } from '@/shared/kernel/app-error'; +import { GetSellerUseCase } from '@/modules/sellers/application/use-cases/get-seller-use-case'; +import styles from './page.module.css'; +import { SellerDetailForm } from './seller-detail-form'; + +export default async function AdminSellerDetailPage({ + params, +}: { + params: Promise<{ locale: string; sellerId: string }>; +}) { + const { locale, sellerId } = await params; + + try { + await assertRole('ADMIN'); + } catch { + redirect(`/${locale}`); + } + + const dict = await getDictionary(locale as 'es' | 'cat'); + const sellerRepository = container.getSellerRepository(); + const getSeller = new GetSellerUseCase(sellerRepository); + + let seller; + try { + seller = await getSeller.execute({ sellerId }); + } catch (error) { + if (error instanceof NotFoundError) { + notFound(); + } + + throw error; + } + + return ( +
+
+
+ + {dict.admin.backToSellers} + +

{dict.admin.sellerDetail.editTitle}

+
+
+ +
+
+ + {dict.admin.sellerDetail.nameLabel} + + {seller.name} +
+
+ + {dict.admin.sellerDetail.descriptionLabel} + +

{seller.description ?? '—'}

+
+
+ + +
+ ); +} diff --git a/app/[locale]/admin/sellers/[sellerId]/products/page.module.css b/app/[locale]/admin/sellers/[sellerId]/products/page.module.css index 321bd221..9cf0ce1e 100644 --- a/app/[locale]/admin/sellers/[sellerId]/products/page.module.css +++ b/app/[locale]/admin/sellers/[sellerId]/products/page.module.css @@ -171,6 +171,34 @@ pointer-events: none; } +.statusBadge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: var(--font-weight-medium); +} + +.statusBadge[data-status='active'] { + background: var(--color-green-light); + color: var(--color-green-dark); +} + +.statusBadge[data-status='draft'] { + background: var(--color-lila); + color: var(--color-white); +} + +.statusBadge[data-status='archived'] { + background: var(--color-coral); + color: var(--color-white); +} + +.statusBadge[data-status='eliminated'] { + background: var(--color-gray); + color: var(--color-white); +} + @media (max-width: 720px) { .header { flex-direction: column; diff --git a/app/[locale]/admin/sellers/[sellerId]/products/page.tsx b/app/[locale]/admin/sellers/[sellerId]/products/page.tsx index d9ffa3c3..2b379115 100644 --- a/app/[locale]/admin/sellers/[sellerId]/products/page.tsx +++ b/app/[locale]/admin/sellers/[sellerId]/products/page.tsx @@ -8,6 +8,7 @@ import { getDictionary } from '@/shared/i18n/get-dictionary'; import { assertRole } from '@/shared/authorization/authorization'; import { LocalizedDate } from '@/shared/kernel/domain/value-objects/localized-date'; import { PaginationDefaults } from '@/shared/kernel/domain/value-objects/pagination'; +import { ProductActions } from '@/app/[locale]/seller/products/product-actions'; import styles from './page.module.css'; function buildPageUrl( @@ -69,9 +70,24 @@ export default async function AdminSellerProductsPage({ const productRepository = container.getProductRepository(); const useCase = new ProductListQueryUseCase(productRepository); const result = await useCase.execute(filter); - const { items: products, totalPages, total } = result; + const { items: products, totalPages } = result; let page = result.page; + function getStatusLabel(status: string): string { + switch (status) { + case 'DRAFT': + return dict.admin.status_draft; + case 'ACTIVE': + return dict.admin.status_active; + case 'ARCHIVED': + return dict.admin.status_archived; + case 'ELIMINATED': + return dict.admin.status_eliminated; + default: + return status; + } + } + if (totalPages > 0 && page > totalPages) { page = totalPages; } @@ -113,16 +129,6 @@ export default async function AdminSellerProductsPage({ {hasProducts ? ( <> -
- - {dict.admin.pageXofY - .replace('{current}', String(page)) - .replace('{total}', String(totalPages))} - - - {dict.admin.productCount.replace('{total}', String(total))} - -
@@ -131,6 +137,7 @@ export default async function AdminSellerProductsPage({ + @@ -141,7 +148,14 @@ export default async function AdminSellerProductsPage({ (translation) => translation.locale === locale, )?.name ?? dict.admin.untranslatedProduct} - + + ))}
{dict.admin.productStatus} {dict.admin.productPrice} {dict.admin.productUpdated}{dict.admin.actions}
{product.status} + + {getStatusLabel(product.status)} + + {product.basePrice.format()} {LocalizedDate.create( @@ -149,46 +163,50 @@ export default async function AdminSellerProductsPage({ locale, ).toString()} + +
- {totalPages > 1 ? ( - - ) : null} + ) : (

{dict.admin.noProducts}

diff --git a/app/[locale]/admin/sellers/[sellerId]/seller-detail-form.tsx b/app/[locale]/admin/sellers/[sellerId]/seller-detail-form.tsx new file mode 100644 index 00000000..e206cffc --- /dev/null +++ b/app/[locale]/admin/sellers/[sellerId]/seller-detail-form.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/modules/presentation/components/button'; +import { ErrorMessage } from '@/modules/presentation/components/error-message'; +import styles from './page.module.css'; + +interface SellerDetailFormProps { + sellerId: string; + nameLabel: string; + descriptionLabel: string; + saveLabel: string; + savedLabel: string; + errorLabel: string; + initialName: string; + initialDescription: string; +} + +export function SellerDetailForm({ + sellerId, + nameLabel, + descriptionLabel, + saveLabel, + savedLabel, + errorLabel, + initialName, + initialDescription, +}: SellerDetailFormProps) { + const [name, setName] = useState(initialName); + const [description, setDescription] = useState(initialDescription); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [saved, setSaved] = useState(null); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLoading(true); + setError(null); + setSaved(null); + + try { + const response = await fetch(`/api/sellers/${sellerId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name.trim(), + description: description.trim(), + }), + }); + + const responseText = await response.text().catch(() => ''); + + if (!response.ok) { + let errorMessage = ''; + + if (responseText.trim()) { + try { + const data = JSON.parse(responseText) as { + error?: string; + message?: string; + }; + errorMessage = data.error ?? data.message ?? responseText.trim(); + } catch { + errorMessage = responseText.trim(); + } + } + + throw new Error(errorMessage || errorLabel); + } + + setSaved(savedLabel); + } catch (submitError: unknown) { + setError(submitError instanceof Error ? submitError.message : errorLabel); + } finally { + setLoading(false); + } + }; + + return ( +
+

{saveLabel}

+ + {error ? : null} + {saved ? ( +
+ {saved} +
+ ) : null} + + + +