feat(admin): implement variant management — list, create, edit, preview, activate/deactivate, delete (Plan 05)

- Extend addVariant with dimension fields and SKU uniqueness check; expand updateVariant to full field set; update getByIdForAdmin to return all variants (active + inactive)
- Add generateSku utility to @repo/utils; auto-generates SKU from brand, product name, attributes, and weight with manual-override support
- Move ProductSearchSection to components/shared and fix nav link /variants → /variant
- Variants page: product search, loading skeleton, variants table, toolbar with create button
- VariantsTable: 8 columns, activate/deactivate toggle, delete with AlertDialog confirmation
- VariantPreviewDialog: read-only full variant details with sections for pricing, inventory, shipping, attributes
- VariantForm: zod schema with superRefine for dimension and on-sale validation, auto-SKU generation
- CreateVariantDialog and EditVariantDialog wiring dollarsToCents on submit
- Install sonner and add Toaster to root layout; install ShadCN Switch component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 09:32:32 +03:00
parent 1ea527ca1f
commit 8e4309892c
17 changed files with 1818 additions and 8 deletions

View File

@@ -166,6 +166,44 @@ export function getTotalPages(total: number, limit: number): number {
return Math.ceil(total / limit);
}
// ─── SKU Generation ───────────────────────────────────────────────────────────
/**
* Generate a SKU from product metadata.
* e.g. Royal Canin, Adult Dog Food, flavor: Chicken, 5kg → "ROY-CANI-ADUL-DOG-CHIC-5KG"
*/
export function generateSku(
brand: string,
productName: string,
attributes?: {
size?: string;
flavor?: string;
color?: string;
},
weight?: number,
weightUnit?: string
): string {
const clean = (str: string) =>
str
.toUpperCase()
.trim()
.replace(/[^A-Z0-9\s]/g, "")
.split(/\s+/)
.map((w) => w.slice(0, 4))
.join("-");
const parts = [
brand ? clean(brand) : null,
productName ? clean(productName) : null,
attributes?.flavor ? clean(attributes.flavor) : null,
attributes?.size ? clean(attributes.size) : null,
attributes?.color ? clean(attributes.color) : null,
weight && weightUnit ? `${weight}${weightUnit.toUpperCase()}` : null,
].filter(Boolean);
return parts.join("-");
}
// ─── Misc ─────────────────────────────────────────────────────────────────────
/**