# Variable Products Implementation

## Overview

This document explains how variable products (products with multiple color/size variants) are handled in the storefront catalog.

## Concept

A **variable product** is a single product (identified by `style_id`) that has multiple variants based on attributes like color and size.

### Example: Product Style `m1s26`

Instead of showing 5 separate cards, we show **ONE card** for style `m1s26`:

**Before (incorrect - 5 cards):**
- Card 1: m1s26 - White / 41
- Card 2: m1s26 - White / 42  
- Card 3: m1s26 - White / 44
- Card 4: m1s26 - Black / 43
- Card 5: m1s26 - Black / 44

**After (correct - 1 card):**
- Card 1: m1s26
  - Colors: White, Black
  - Sizes: 41, 42, 43, 44
  - Price: 399 EGP (or 399-499 EGP if prices vary)
  - Button: "Select option" (takes to detail page)

## Database Structure

### Tables

```
items (products/SKUs)
├── id
├── barcode (unique SKU identifier)
├── name
├── style_id (FK to styles.id) ← KEY FIELD FOR GROUPING
├── color_id (FK to colors.id)
├── size_id (FK to sizes.id)
├── brand_id
├── category_id
└── active

styles
├── id
├── name (e.g., "m1s26")
└── active
```

### Relationship

- **One style** → **Many items** (variants)
- Items with the same `style_id` are variants of the same product
- Each variant has a unique `barcode` and specific `color_id` + `size_id` combination

## Backend Implementation

### 1. Products Listing API

**File:** `app/Http/Controllers/Api/Storefront/StorefrontCatalogController.php`

**Endpoint:** `GET /api/storefront/products`

#### Grouping Logic

```php
// Group by style_id (items without style_id get individual cards)
$groupQuery->select(DB::raw('MIN(items.id) as rep_id'))
    ->groupBy(DB::raw('COALESCE(items.style_id, items.id)'));
```

**Key Points:**
- Uses `COALESCE(items.style_id, items.id)` to group by style when available
- Falls back to individual item ID for products without a style
- Selects one representative item per style (the one with MIN id)
- Pagination counts distinct styles, not individual SKUs

#### Variant Enrichment

After getting representative items, the API:

1. **Fetches all variants** for each style:
```php
$variantsQuery->where(function ($q) use ($styleIds, $noStyleIds) {
    if (!empty($styleIds)) {
        $q->whereIn('style_id', $styleIds);
    }
    if (!empty($noStyleIds)) {
        $q->orWhereIn('id', $noStyleIds);
    }
});
```

2. **Enriches the card** with variant data:
```php
private function enrichCatalogCard(array $card, $variants, array $sourceBranchIds): array
{
    if ($variants->count() <= 1) {
        return $card; // Single variant, no enrichment needed
    }

    // Calculate price range
    $prices = $variants->map(fn($v) => $v->cprices1 ? (float) $v->cprices1->rtp : null)->filter();
    if ($prices->isNotEmpty()) {
        $minP = $prices->min();
        $maxP = $prices->max();
        $card['price'] = $minP;
        if ($maxP > $minP) {
            $card['price_max'] = $maxP; // Shows as "399 - 499 EGP"
        }
    }

    // Aggregate stock across all variants
    $anyStock = false;
    $totalQty = 0;
    foreach ($variants as $v) {
        $q = $this->catalogService->getAvailableStock($v->id, $sourceBranchIds);
        $totalQty += $q;
        if ($q > 0) {
            $anyStock = true;
        }
    }
    $card['in_stock'] = $anyStock;
    $card['available_qty'] = $totalQty;
    $card['requires_variant_selection'] = true;

    // Collect all unique colors
    $uniqueColors = $variants->pluck('color')->filter()->unique('id')->values();
    if ($uniqueColors->count() >= 1) {
        $card['colors_preview'] = $uniqueColors->map(fn($c) => [
            'id'   => $c->id,
            'name' => $c->name,
            'code' => $c->code ?? null,
        ])->all();
        if ($uniqueColors->count() > 1) {
            $card['color'] = null; // Clear single color when multiple exist
        }
    }

    // Collect all unique sizes
    $uniqueSizes = $variants->pluck('size')->filter()->unique('id')->values();
    if ($uniqueSizes->count() >= 1) {
        $card['sizes_preview'] = $uniqueSizes->map(fn($s) => [
            'id'   => $s->id,
            'name' => $s->name,
        ])->all();
        if ($uniqueSizes->count() > 1) {
            $card['size'] = null; // Clear single size when multiple exist
        }
    }

    return $card;
}
```

### 2. Product Detail API

**Endpoint:** `GET /api/storefront/products/{barcode}`

Finds all variants by `style_id`:

```php
$variantsQuery = Item::with(['color', 'size', 'gender', 'cprices1'])
    ->where('active', 1);

if ($item->style_id) {
    $variantsQuery->where('style_id', $item->style_id);
} else {
    $variantsQuery->where('name', $item->name); // Fallback
}

$variants = $variantsQuery->get();
```

Returns all variants with their specific:
- `barcode` (for adding to cart)
- `color`, `size`, `gender`
- `price`
- `available_qty`, `in_stock`

### 3. Response Structure

#### Catalog Card Response

```json
{
  "id": 123,
  "barcode": "1234567890",
  "name": "Nike Air Force 1",
  "style": {
    "id": 5,
    "name": "m1s26"
  },
  "price": 399,
  "price_max": 499,
  "available_qty": 45,
  "in_stock": true,
  "requires_variant_selection": true,
  "colors_preview": [
    { "id": 1, "name": "White", "code": "#FFFFFF" },
    { "id": 2, "name": "Black", "code": "#000000" }
  ],
  "sizes_preview": [
    { "id": 10, "name": "41" },
    { "id": 11, "name": "42" },
    { "id": 12, "name": "43" },
    { "id": 13, "name": "44" }
  ],
  "image_url": "/api/storefront/media/...",
  "category": { "id": 3, "name": "Shoes" },
  "brand": { "id": 7, "name": "Nike" }
}
```

## Frontend Implementation

### TypeScript Types

**File:** `shopzaki/types/index.ts`

```typescript
export interface Style {
  id: number;
  name: string;
}

export interface Color {
  id: number;
  name: string;
  hex?: string | null;
  code?: string | null;
}

export interface Size {
  id: number;
  name: string;
}

export interface Product {
  id: number;
  barcode: string;
  name: string;
  style?: Style | null;
  price: number;
  price_max?: number; // When variants have different prices
  available_qty: number;
  in_stock: boolean;
  
  // Single variant fields (null when multiple variants)
  color?: Color | null;
  size?: Size | null;
  
  // Multiple variant fields
  colors_preview?: Color[];
  sizes_preview?: Size[];
  requires_variant_selection?: boolean;
  
  // Other fields...
  image_url?: string | null;
  category?: Category | null;
  brand?: Brand | null;
  variants?: ProductVariant[];
}
```

### UI Components

**File:** `shopzaki/components/product/ProductCard.tsx`

#### 1. Price Display Component

```tsx
function CatalogPrice({ product, currency, className, style, variant }) {
  const max = product.price_max;
  if (max != null && max > product.price) {
    return (
      <p className={className} style={style}>
        {formatPrice(product.price, currency)}
        <span className="text-gray-500 font-normal mx-1">–</span>
        {formatPrice(max, currency)}
      </p>
    );
  }
  return (
    <p className={className} style={style}>
      {formatPrice(product.price, currency)}
    </p>
  );
}
```

**Output:** "399 EGP – 499 EGP" or "399 EGP"

#### 2. Variant Options Display

```tsx
function CatalogVariantRow({ product }) {
  // Prefer colors_preview, fallback to single color
  const colors =
    product.colors_preview && product.colors_preview.length > 0
      ? product.colors_preview
      : product.color
        ? [product.color]
        : [];
  
  // Prefer sizes_preview, fallback to single size
  const sizes =
    product.sizes_preview && product.sizes_preview.length > 0
      ? product.sizes_preview
      : product.size
        ? [product.size]
        : [];

  return (
    <div className="flex flex-wrap items-center gap-2">
      {/* Show up to 6 color swatches */}
      {colors.slice(0, 6).map((c) => (
        <ColorSwatch
          key={c.id}
          name={c.name}
          hex={c.hex ?? c.code}
          size="sm"
        />
      ))}
      {colors.length > 6 && (
        <span className="text-xs text-gray-500">+{colors.length - 6}</span>
      )}
      
      {/* Show up to 5 size chips */}
      {sizes.slice(0, 5).map((s) => (
        <span
          key={s.id}
          className="text-xs text-gray-600 border border-gray-200 rounded-md px-2 py-0.5 font-medium"
        >
          {s.name}
        </span>
      ))}
      {sizes.length > 5 && (
        <span className="text-xs text-gray-500">+{sizes.length - 5}</span>
      )}
    </div>
  );
}
```

**Visual Output:**
```
[White swatch] [Black swatch] [Red swatch] [41] [42] [43] [44] [45] +2
```

#### 3. Action Button

```tsx
function CatalogPrimaryAction({ product, tone = "light" }) {
  if (!product.in_stock) return null;
  
  if (product.requires_variant_selection) {
    return (
      <span className="...">
        Select option
      </span>
    );
  }
  
  return <AddToCartButton product={product} />;
}
```

**Behavior:**
- Variable products → "Select option" button → Navigate to detail page
- Single products → "Add to Cart" button → Add directly to cart

### Product Card Layout

All card styles (rounded, sharp, overlay, minimal, magazine) use these components:

```tsx
<ProductCard product={product} currency="EGP" />
  ├── Product Image
  ├── Product Name
  ├── <CatalogVariantRow /> ← Shows colors & sizes
  ├── <CatalogPrice /> ← Shows price or range
  └── <CatalogPrimaryAction /> ← "Select option" or "Add to Cart"
```

## User Flow

### 1. Catalog Page (`/products`)

User sees:
- **One card per style** (e.g., "Nike Air Force 1 - m1s26")
- All available colors as swatches
- All available sizes as chips
- Price range if applicable
- "Select option" button

### 2. Click on Card

User navigates to: `/products/{barcode}` (barcode of representative variant)

### 3. Product Detail Page

User sees:
- Product images
- Full description
- **Variant selector:**
  - Color buttons (clickable)
  - Size buttons (clickable)
  - Each combination maps to a specific barcode
- Price updates based on selected variant
- Stock status for selected variant
- "Add to Cart" button (adds specific barcode to cart)

### 4. Add to Cart

Cart stores:
- Specific `barcode` (e.g., "SKU-WHITE-42")
- Specific `color` and `size`
- Quantity

## Sorting & Filtering

### Sorting

When sorting by price:
- **Price Low → High:** Uses `MIN(price)` across all variants
- **Price High → Low:** Uses `MAX(price)` across all variants
- **Newest:** Uses `MAX(created_at)` across all variants
- **Name A-Z:** Uses `MIN(name)` across all variants

### Filtering

Filters apply to variants:
- **Color filter:** Shows styles that have at least one variant with that color
- **Size filter:** Shows styles that have at least one variant with that size
- **Price filter:** Shows styles with at least one variant in that price range

Example:
- User filters by "Black" color
- System shows all styles that have a Black variant
- Card still displays all colors for that style (including Black)

## Edge Cases

### Products Without style_id

Products without a `style_id` are treated as standalone items:
- Each gets its own card
- No variant grouping
- Grouped by their individual `id`

### Single Variant Styles

If a style has only one variant:
- Still shows as one card
- Shows the single color and size
- No "Select option" button (can add directly to cart)
- `requires_variant_selection` is `false`

### Out of Stock Variants

- If all variants are out of stock: card shows "Out of Stock"
- If some variants are in stock: card shows "In Stock"
- Stock is aggregated across all variants
- Detail page shows per-variant stock status

## Performance Considerations

### Database Queries

1. **Grouping query:** One query to get representative items per style
2. **Variants query:** One query to fetch all variants for displayed styles
3. **In-memory grouping:** Variants grouped by style_id in PHP

### Optimization

- Uses `MIN(items.id)` for representative selection (indexed)
- `COALESCE(style_id, id)` for efficient grouping
- Eager loading of relationships (`with(['color', 'size', 'style', ...])`)
- Pagination on grouped results (not raw items)

### Caching Strategy

Consider caching:
- Style → variants mapping
- Aggregated stock counts
- Price ranges per style

## Testing Checklist

- [ ] Products with same `style_id` show as one card
- [ ] Card displays all unique colors from variants
- [ ] Card displays all unique sizes from variants
- [ ] Price range shows when variants have different prices
- [ ] Stock aggregates correctly across variants
- [ ] "Select option" button appears for variable products
- [ ] Clicking card navigates to detail page
- [ ] Detail page shows all variants
- [ ] Filters work correctly (color, size, price)
- [ ] Sorting works correctly (price, newest, name)
- [ ] Products without `style_id` still display correctly
- [ ] Out of stock variants handled properly
- [ ] Related products use style grouping

## Migration Notes

If you have existing products that should be grouped:

1. **Create styles:**
   ```sql
   INSERT INTO styles (name, active) VALUES ('m1s26', 1);
   ```

2. **Assign style_id to items:**
   ```sql
   UPDATE items 
   SET style_id = (SELECT id FROM styles WHERE name = 'm1s26')
   WHERE name LIKE '%Air Force%';
   ```

3. **Verify grouping:**
   ```sql
   SELECT style_id, COUNT(*) as variant_count
   FROM items
   WHERE style_id IS NOT NULL
   GROUP BY style_id;
   ```

## Summary

This implementation provides a clean, scalable solution for variable products:

✅ **One card per product style** (not per SKU)  
✅ **All colors and sizes visible** in catalog  
✅ **Price ranges** when applicable  
✅ **Aggregated stock** across variants  
✅ **Clean user flow** from catalog → detail → cart  
✅ **Efficient queries** with proper grouping  
✅ **Backward compatible** with non-variable products  

The system uses `style_id` as the primary grouping key, making it easy to manage variable products while maintaining flexibility for standalone items.
