# Specyfikacja UI - Dodawanie Produktów do Planu Produkcji

## Status Implementacji

**Data aktualizacji:** 2025-11-11  
**Status ogólny:** W trakcie implementacji ⚠️

### ✅ Co już zostało zaimplementowane

#### Backend (Dane i API)
- [x] **Tabela `production.production_plans`** - nagłówek planu produkcji
- [x] **Tabela `production.production_plan_lines`** - linie planowania z sourceType metadata
- [x] **Material Consumption Norms** - normy zużycia materiałów (płyta +20%, obrzeże +2cm)
- [x] **Production Routings** - marszruty z rozgałęzieniami (branches)
- [x] **BOM System** - komponenty z `drilling_required`, automatyczna propagacja
- [x] **Aggregated Demand** - agregacja zapotrzebowania z marketplace orders
- [x] **Available Orders API** - endpoint `/api/production/planning/plans/:id/available-orders`
- [x] **Marketplace-Catalog Linking** - łączenie produktów marketplace z katalogiem

#### Frontend (UI Components)
- [x] **Production Plan Detail Page** - `/production/plans/:id`
- [x] **Available Orders Filters Hook** - `useAvailableOrdersFilters()`
- [x] **Dual-tab System** - zakładki Catalog/Orders (DO USUNIĘCIA)
- [x] **Collapsible Orders** - rozwijane zamówienia z produktami
- [x] **SQL-level Filtering** - search, color, SKU, dimensions, customer, order number, marketplace
- [x] **Marketplace Product Linking Dialog** - łączenie unmapped products
- [x] **Add to Plan Button** - przycisk "Dodaj" dla pojedynczych produktów

### ❌ Co jeszcze trzeba zrobić

---

## 1. Layout - Dual-Panel System ⚠️ WYSOKІ PRIORYTET

### Wireframe nowego układu:

```
┌─────────────────────────────────────────────────────────────┐
│  PLAN HEADER (breadcrumbs, status, dates, actions)          │
├──────────────────────┬──────────────────────────────────────┤
│  AVAILABLE ORDERS    │  PLAN ITEMS (Right Panel)            │
│  (Left Panel)        │  ┌────────────────────────────────┐  │
│  ┌────────────────┐  │  │ 25px Statistics Bar:           │  │
│  │ Filters        │  │  │ Orders: 15 | Products: 23 |    │  │
│  │ - 24px tiles   │  │  │ Lines: 8 | Qty: 45            │  │
│  │ - Color presets│  │  ├────────────────────────────────┤  │
│  │ - Dimensions   │  │  │ Product List:                  │  │
│  ├────────────────┤  │  │ ┌────┬──┬─────┬──────┬───┬───┐ │  │
│  │ Order #1234    │  │  │ │ ID │📷│ Ord │ Date │...│Qty│ │  │
│  │  ├─ Product A  │  │  │ ├────┼──┼─────┼──────┼───┼───┤ │  │
│  │  │  [Dodaj]    │  │  │ │ 1  │🖼│#5001│12-11 │...│10 │ │  │
│  │  └─ Product B  │  │  │ │ 2  │🖼│Zapas│  -   │...│5  │ │  │
│  │     [Dodaj]    │  │  │ └────┴──┴─────┴──────┴───┴───┘ │  │
│  │                │  │  └────────────────────────────────┘  │
│  │ Order #1235    │  │                                      │
│  │  └─ Product C  │  │                                      │
│  └────────────────┘  │                                      │
└──────────────────────┴──────────────────────────────────────┘
```

**Kluczowe zmiany:**
- ❌ **Usunąć zakładki Catalog/Orders** (wszystko w jednym widoku)
- ✅ **Left Panel:** Filtrowalne zamówienia marketplace + możliwość dodania do planu
- ✅ **Right Panel:** 
  - **25px Statistics Bar** (poziomy, kompaktowy)
  - **Szczegółowa lista produktów** dodanych do planu (nie simple table!)

---

## 2. Right Panel - Plan Items ⚠️ KRYTYCZNE

### 2.1. Statistics Bar (25px wysokości)

**Design:** Cienki poziomy pasek z key metrics

```tsx
<div className="h-[25px] bg-secondary/20 px-4 flex items-center gap-6 text-xs border-b">
  <div className="flex items-center gap-1.5">
    <ShoppingCart className="h-3.5 w-3.5 text-muted-foreground" />
    <span className="text-muted-foreground">Zamówienia:</span>
    <span className="font-semibold">{ordersCount}</span>
  </div>
  <Separator orientation="vertical" className="h-4" />
  <div className="flex items-center gap-1.5">
    <Package className="h-3.5 w-3.5 text-muted-foreground" />
    <span className="text-muted-foreground">Produkty:</span>
    <span className="font-semibold">{productsCount}</span>
  </div>
  <Separator orientation="vertical" className="h-4" />
  <div className="flex items-center gap-1.5">
    <FileText className="h-3.5 w-3.5 text-muted-foreground" />
    <span className="text-muted-foreground">Linie:</span>
    <span className="font-semibold">{planLinesCount}</span>
  </div>
  <Separator orientation="vertical" className="h-4" />
  <div className="flex items-center gap-1.5">
    <Hash className="h-3.5 w-3.5 text-muted-foreground" />
    <span className="text-muted-foreground">Ilość:</span>
    <span className="font-semibold">{totalQuantity}</span>
  </div>
</div>
```

**Metryki:**
- **Zamówienia:** Ile unikalnych zamówień w planie
- **Produkty:** Ile różnych produktów (unikalnych SKU)
- **Linie:** Ile linii planu (`production_plan_lines`)
- **Ilość:** Suma wszystkich quantity

---

### 2.2. Product List - Detailed View

**Format:** Table z szerokimi kolumnami, kolorowe badges jak w catalog-products

#### Kolumny tabeli (w kolejności):

| Kolumna | Szerokość | Typ | Opis |
|---------|-----------|-----|------|
| **ID** | 50px | Text | `production_plan_lines.id` |
| **Zdjęcie** | 25px | Image | `catalog.products.primaryImageUrl` (thumbnail 25x25px) |
| **Nr zamówienia** | 100px | Text | `commerce.orders.order_number` lub **"Zapas"** |
| **Data zam.** | 90px | Date | `commerce.orders.order_date` lub "-" |
| **Kupujący** | 150px | Text | `{buyer_first_name} {buyer_last_name}` lub **"Zapas"** |
| **Rodzaj** | 60px | Badge | `catalog.products.productType` - **kolorowe tło** |
| **SKU** | 120px | Text | `catalog.products.sku` |
| **Długość** | 70px | Badge | `catalog.products.length` cm - **kolorowe tło** |
| **Szerokość** | 70px | Badge | `catalog.products.width` cm - **kolorowe tło** |
| **Kolor** | 80px | Badge | `catalog.products.color` - **kolorowe tło** |
| **Opcje koloru** | 150px | Badges | `catalog.products.colorOptions[]` - **kolorowe tła** |
| **Nogi** | 70px | Badge | `catalog.products.legs` - **kolorowe tło** |
| **Drzwi** | 70px | Badge | `catalog.products.doors` - **kolorowe tło** |
| **Cena** | 80px | Text | `catalog.products.basePrice` + currency |
| **Ilość BOM** | 80px | Number | `COUNT(production.bom_components WHERE productId=...)` |
| **Akcje** | 100px | Buttons | Edit, Delete |

---

### 2.3. Kolorowe Badges - Style Guide

**Implementacja identyczna jak w `/catalog-products`:**

```tsx
// Helper functions (już istnieją w catalog-products.tsx):
function getTextColorForBackground(hexColor: string | null | undefined): string {
  if (!hexColor || !hexColor.startsWith('#')) return 'black';
  
  const r = parseInt(hexColor.slice(1, 3), 16);
  const g = parseInt(hexColor.slice(3, 5), 16);
  const b = parseInt(hexColor.slice(5, 7), 16);
  
  const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
  return luminance > 128 ? 'black' : 'white';
}

function needsVisibleBorder(hexColor: string | null | undefined): boolean {
  if (!hexColor || !hexColor.startsWith('#')) return false;
  
  const r = parseInt(hexColor.slice(1, 3), 16);
  const g = parseInt(hexColor.slice(3, 5), 16);
  const b = parseInt(hexColor.slice(5, 7), 16);
  
  const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
  
  return luminance < 30 || luminance > 230;
}
```

**Przykład dla Product Type:**

```tsx
{product.productType && (() => {
  const typeDict = productTypes?.find(t => t.code === product.productType);
  const bgColor = typeDict?.color;
  const textColor = getTextColorForBackground(bgColor);
  
  return (
    <Badge 
      variant="outline" 
      className="text-xs h-5 px-1.5 border-transparent" 
      style={{ backgroundColor: bgColor, color: textColor }}
      data-testid={`badge-type-${product.id}`}
    >
      {product.productType}
    </Badge>
  );
})()}
```

**Przykład dla Długość/Szerokość (dimensional badges):**

```tsx
{product.length && (() => {
  // Fetch dimension color from dictionaries or use preset based on range
  const dimensionDict = dimensions?.find(d => 
    product.length >= d.minValue && product.length <= d.maxValue
  );
  const bgColor = dimensionDict?.color || '#e5e7eb'; // default gray
  const textColor = getTextColorForBackground(bgColor);
  const hasBorder = needsVisibleBorder(bgColor);
  
  return (
    <Badge 
      variant="outline" 
      className={`text-xs h-5 px-1.5 ${hasBorder ? '' : 'border-transparent'}`}
      style={{ backgroundColor: bgColor, color: textColor }}
    >
      {product.length} cm
    </Badge>
  );
})()}
```

**Przykład dla Color Options (multiple badges):**

```tsx
{product.colorOptions && product.colorOptions.length > 0 && (
  <div className="flex flex-wrap gap-1">
    {product.colorOptions.map((option, idx) => {
      const colorDict = colors?.find(c => c.code === option);
      const bgColor = colorDict?.color;
      const textColor = getTextColorForBackground(bgColor);
      const hasBorder = needsVisibleBorder(bgColor);
      
      return (
        <Badge 
          key={idx}
          variant="outline" 
          className={`text-xs h-5 px-1.5 ${hasBorder ? '' : 'border-transparent'}`}
          style={{ backgroundColor: bgColor, color: textColor }}
        >
          {option}
        </Badge>
      );
    })}
  </div>
)}
```

---

### 2.4. Data Source - Plan Items

**Query:** Połączenie `production_plan_lines` z danymi produktu i zamówienia

```typescript
interface PlanItem {
  // Production plan line
  id: number;
  planId: number;
  productId: number;
  quantity: number;
  routingId?: number;
  bomId?: number;
  status: string;
  sourceType: "order_demand" | "stock" | "manual";
  metadata?: {
    orderReferences?: Array<{
      orderId: number;
      orderNumber: string;
      marketplace: string;
      quantity: number;
    }>;
  };
  
  // Catalog product data
  product: {
    id: number;
    sku: string;
    title: string;
    productType: string | null;
    length: number | null;
    width: number | null;
    color: string | null;
    colorOptions: string[] | null;
    legs: string | null;
    doors: string | null;
    basePrice: number | null;
    currency: string;
    primaryImageUrl: string | null;
  };
  
  // Marketplace order data (if sourceType === "order_demand")
  order?: {
    orderId: number;
    orderNumber: string;
    orderDate: string;
    marketplace: string;
    buyerFirstName: string;
    buyerLastName: string;
  };
  
  // BOM count
  bomComponentsCount: number;
}
```

**Backend endpoint:** `GET /api/production/planning/plans/:id/items`

```sql
SELECT 
  ppl.id,
  ppl.plan_id,
  ppl.product_id,
  ppl.quantity,
  ppl.routing_id,
  ppl.bom_id,
  ppl.status,
  ppl.source_type,
  ppl.metadata,
  
  -- Catalog product
  cp.sku,
  cp.title,
  cp.product_type,
  cp.length,
  cp.width,
  cp.color,
  cp.color_options,
  cp.legs,
  cp.doors,
  cp.base_price,
  cp.currency,
  cp.primary_image_url,
  
  -- BOM count
  (SELECT COUNT(*) FROM production.bom_components bc WHERE bc.product_id = cp.id) as bom_components_count,
  
  -- Order data (if from order_demand)
  CASE 
    WHEN ppl.source_type = 'order_demand' THEN
      (ppl.metadata->'orderReferences'->0->>'orderId')::int
    ELSE NULL
  END as order_id,
  
  CASE 
    WHEN ppl.source_type = 'order_demand' THEN
      ppl.metadata->'orderReferences'->0->>'orderNumber'
    ELSE NULL
  END as order_number,
  
  -- Join with commerce.orders for buyer info
  co.order_date,
  co.marketplace,
  co.buyer_first_name,
  co.buyer_last_name

FROM production.production_plan_lines ppl
INNER JOIN catalog.products cp ON cp.id = ppl.product_id
LEFT JOIN commerce.orders co ON 
  ppl.source_type = 'order_demand' 
  AND co.order_id = (ppl.metadata->'orderReferences'->0->>'orderId')::int
  
WHERE ppl.plan_id = $1
ORDER BY ppl.sequence, ppl.id;
```

---

## 3. Zamówienia Wewnętrzne (Internal Orders) 🆕 KRYTYCZNE

### 3.1. Problem biznesowy

**Aktualnie:** Możemy dodawać do planu tylko produkty z zamówień marketplace (Allegro/Shoper)

**Potrzebujemy:** Produkcja na zapas (bez powiązania z zamówieniem klienta)

**Rozwiązanie:** System zamówień wewnętrznych

---

### 3.2. Nowa tabela: `commerce.internal_orders`

```typescript
export const internalOrders = pgTable("internal_orders", {
  id: serial("id").primaryKey(),
  orderNumber: varchar("order_number", { length: 50 }).notNull().unique(),
  orderType: varchar("order_type", { length: 20 }).notNull().default("stock"), // "stock", "custom", "sample"
  
  // Status
  status: varchar("status", { length: 20 }).notNull().default("draft"),
  // draft, approved, in_production, completed, cancelled
  
  // Dates
  createdDate: timestamp("created_date").notNull().defaultNow(),
  approvedDate: timestamp("approved_date"),
  plannedProductionDate: timestamp("planned_production_date"),
  completedDate: timestamp("completed_date"),
  
  // Creator
  createdBy: integer("created_by").references(() => users.id),
  approvedBy: integer("approved_by").references(() => users.id),
  
  // Priority
  priority: varchar("priority", { length: 20 }).notNull().default("normal"),
  // low, normal, high, urgent
  
  // Description
  title: text("title").notNull(),
  description: text("description"),
  notes: text("notes"),
  
  // Warehouse destination
  targetWarehouseLocation: varchar("target_warehouse_location", { length: 20 }),
  // "MAG-GOT", "MAG-FOR", etc.
  
  // Timestamps
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

export const internalOrderItems = pgTable("internal_order_items", {
  id: serial("id").primaryKey(),
  orderId: integer("order_id").notNull().references(() => internalOrders.id, { onDelete: "cascade" }),
  
  // Product reference
  catalogProductId: integer("catalog_product_id").notNull().references(() => products.id),
  
  // Quantity
  quantity: integer("quantity").notNull().default(1),
  
  // Notes specific to this item
  notes: text("notes"),
  
  // Timestamps
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
}, (table) => ({
  orderIdIdx: index("internal_order_items_order_id_idx").on(table.orderId),
  catalogProductIdIdx: index("internal_order_items_catalog_product_id_idx").on(table.catalogProductId),
}));
```

---

### 3.3. UI - Internal Orders Management

**Nowa strona:** `/production/internal-orders`

**Funkcjonalności:**

1. **Lista zamówień wewnętrznych**
   - Tabela z kolumnami: Nr, Typ, Status, Tytuł, Produkty, Ilość, Data utworzenia
   - Filtry: status, typ, data
   - Przycisk "Nowe zamówienie wewnętrzne"

2. **Tworzenie zamówienia wewnętrznego**
   - Dialog z formularzem:
     ```tsx
     <Dialog>
       <DialogTitle>Nowe zamówienie wewnętrzne</DialogTitle>
       <DialogContent>
         <Form>
           <FormField name="orderType">
             <Select>
               <SelectItem value="stock">Na zapas</SelectItem>
               <SelectItem value="custom">Niestandardowe</SelectItem>
               <SelectItem value="sample">Próbki</SelectItem>
             </Select>
           </FormField>
           
           <FormField name="title">
             <Input placeholder="Tytuł zamówienia" />
           </FormField>
           
           <FormField name="description">
             <Textarea placeholder="Opis" />
           </FormField>
           
           <FormField name="priority">
             <Select>
               <SelectItem value="low">Niski</SelectItem>
               <SelectItem value="normal">Normalny</SelectItem>
               <SelectItem value="high">Wysoki</SelectItem>
               <SelectItem value="urgent">Pilny</SelectItem>
             </Select>
           </FormField>
           
           <FormField name="targetWarehouseLocation">
             <Select>
               <SelectItem value="MAG-GOT">Magazyn gotowych</SelectItem>
               <SelectItem value="MAG-FOR">Magazyn formatek</SelectItem>
             </Select>
           </FormField>
           
           {/* Product selection */}
           <div>
             <Label>Produkty</Label>
             <ProductSelector 
               onSelect={(product, quantity) => addItem(product, quantity)}
             />
             
             {/* Selected items list */}
             <Table>
               <TableBody>
                 {items.map(item => (
                   <TableRow key={item.productId}>
                     <TableCell>{item.sku}</TableCell>
                     <TableCell>{item.title}</TableCell>
                     <TableCell>
                       <Input 
                         type="number" 
                         value={item.quantity}
                         onChange={(e) => updateQuantity(item.id, e.target.value)}
                       />
                     </TableCell>
                     <TableCell>
                       <Button onClick={() => removeItem(item.id)}>
                         <Trash2 />
                       </Button>
                     </TableCell>
                   </TableRow>
                 ))}
               </TableBody>
             </Table>
           </div>
           
           <Button type="submit">Utwórz zamówienie</Button>
         </Form>
       </DialogContent>
     </Dialog>
     ```

3. **Szczegóły zamówienia wewnętrznego**
   - Strona `/production/internal-orders/:id`
   - Wyświetla: nagłówek, status, produkty, historia zmian
   - Akcje: Zatwierdź, Dodaj do planu produkcji, Anuluj

---

### 3.4. Integracja z Production Planning

**Modyfikacja:** `GET /api/production/planning/plans/:id/available-orders`

**Zamiast:** Tylko zamówienia marketplace  
**Teraz:** Zamówienia marketplace + zamówienia wewnętrzne

**SQL rozszerzony:**

```sql
-- Marketplace orders (existing)
SELECT 
  'marketplace' as source_type,
  o.order_id,
  o.order_number,
  o.marketplace,
  o.order_date,
  o.buyer_first_name,
  o.buyer_last_name,
  ...
FROM commerce.orders o
WHERE ...

UNION ALL

-- Internal orders (NEW)
SELECT 
  'internal' as source_type,
  io.id as order_id,
  io.order_number,
  'INTERNAL' as marketplace,
  io.created_date as order_date,
  'Zapas' as buyer_first_name,
  '' as buyer_last_name,
  ...
FROM commerce.internal_orders io
WHERE io.status IN ('approved', 'draft')
  AND io.id NOT IN (
    -- Exclude already planned internal orders
    SELECT DISTINCT (ppl.metadata->'internalOrderId')::int
    FROM production.production_plan_lines ppl
    WHERE ppl.source_type = 'stock'
  )
ORDER BY order_date DESC;
```

**UI Rozróżnienie:**

```tsx
// Left panel - Order card
<Card className={order.source_type === 'internal' ? 'border-blue-500' : ''}>
  <CardHeader>
    <div className="flex items-center gap-2">
      {order.source_type === 'internal' ? (
        <Package className="h-4 w-4 text-blue-500" />
      ) : (
        <ShoppingCart className="h-4 w-4" />
      )}
      <span className="font-semibold">{order.order_number}</span>
      {order.source_type === 'internal' && (
        <Badge variant="secondary" className="text-xs">Zapas</Badge>
      )}
    </div>
  </CardHeader>
  <CardContent>
    {/* Buyer info */}
    <p className="text-sm text-muted-foreground">
      {order.source_type === 'internal' ? (
        <span className="font-medium text-blue-600">Zamówienie wewnętrzne</span>
      ) : (
        `${order.buyer_first_name} ${order.buyer_last_name}`
      )}
    </p>
    
    {/* Products */}
    {order.items.map(item => (
      <div key={item.item_id} className="mt-2">
        <span>{item.name}</span>
        <Button 
          size="sm" 
          onClick={() => addToPlan(item)}
        >
          Dodaj
        </Button>
      </div>
    ))}
  </CardContent>
</Card>
```

---

## 4. Filter Presets - 24px Compact Tiles ⚠️ WYSOKІ PRIORYTET

**Lokalizacja:** Left Panel, nad listą zamówień

```tsx
<div className="space-y-3 p-4 border-b">
  {/* Kolory */}
  <div>
    <Label className="text-xs text-muted-foreground mb-1.5 block">Kolory</Label>
    <div className="flex flex-wrap gap-2">
      {colorPresets.map(color => {
        const isActive = orderFilters.color === color.code;
        const textColor = getTextColorForBackground(color.hex);
        const hasBorder = needsVisibleBorder(color.hex);
        
        return (
          <button
            key={color.code}
            className={`h-6 px-3 rounded text-xs font-medium transition-all
              ${isActive ? 'ring-2 ring-primary ring-offset-2' : ''}
              ${hasBorder ? 'border border-border' : 'border border-transparent'}
            `}
            style={{ 
              backgroundColor: color.hex, 
              color: textColor 
            }}
            onClick={() => updateFilter('color', isActive ? '' : color.code)}
            data-testid={`preset-color-${color.code}`}
          >
            {color.name}
          </button>
        );
      })}
    </div>
  </div>
  
  {/* Długości */}
  <div>
    <Label className="text-xs text-muted-foreground mb-1.5 block">Długość (cm)</Label>
    <div className="flex flex-wrap gap-2">
      {dimensionPresetsLength.map(preset => {
        const isActive = 
          orderFilters.minLength === preset.min && 
          orderFilters.maxLength === preset.max;
        
        return (
          <button
            key={preset.id}
            className={`h-6 px-3 rounded text-xs font-medium transition-all
              bg-secondary text-secondary-foreground
              ${isActive ? 'ring-2 ring-primary ring-offset-2' : ''}
            `}
            onClick={() => {
              if (isActive) {
                updateFilter('minLength', '');
                updateFilter('maxLength', '');
              } else {
                updateFilter('minLength', preset.min);
                updateFilter('maxLength', preset.max);
              }
            }}
            data-testid={`preset-length-${preset.id}`}
          >
            {preset.label}
          </button>
        );
      })}
    </div>
  </div>
  
  {/* Szerokości */}
  <div>
    <Label className="text-xs text-muted-foreground mb-1.5 block">Szerokość (cm)</Label>
    <div className="flex flex-wrap gap-2">
      {dimensionPresetsWidth.map(preset => {
        const isActive = 
          orderFilters.minWidth === preset.min && 
          orderFilters.maxWidth === preset.max;
        
        return (
          <button
            key={preset.id}
            className={`h-6 px-3 rounded text-xs font-medium transition-all
              bg-secondary text-secondary-foreground
              ${isActive ? 'ring-2 ring-primary ring-offset-2' : ''}
            `}
            onClick={() => {
              if (isActive) {
                updateFilter('minWidth', '');
                updateFilter('maxWidth', '');
              } else {
                updateFilter('minWidth', preset.min);
                updateFilter('maxWidth', preset.max);
              }
            }}
            data-testid={`preset-width-${preset.id}`}
          >
            {preset.label}
          </button>
        );
      })}
    </div>
  </div>
</div>
```

**Dane presetów:**

```typescript
// Color presets (z dictionaries)
const colorPresets = colors?.filter(c => c.isActive).map(c => ({
  code: c.code,
  name: c.name,
  hex: c.color || '#e5e7eb',
})) || [];

// Length presets
const dimensionPresetsLength = [
  { id: 'l1', label: '50-70', min: '50', max: '70' },
  { id: 'l2', label: '70-90', min: '70', max: '90' },
  { id: 'l3', label: '90-110', min: '90', max: '110' },
  { id: 'l4', label: '110-130', min: '110', max: '130' },
  { id: 'l5', label: '130+', min: '130', max: '' },
];

// Width presets
const dimensionPresetsWidth = [
  { id: 'w1', label: '30-40', min: '30', max: '40' },
  { id: 'w2', label: '40-50', min: '40', max: '50' },
  { id: 'w3', label: '50-60', min: '50', max: '60' },
  { id: 'w4', label: '60-80', min: '60', max: '80' },
  { id: 'w5', label: '80+', min: '80', max: '' },
];
```

---

## 5. Active Filters Summary Panel

**Lokalizacja:** Left Panel, nad presetami (lub zaraz pod nimi)

```tsx
{getActiveFilters().length > 0 && (
  <div className="p-3 bg-secondary/20 rounded-md border">
    <div className="flex items-center justify-between mb-2">
      <span className="text-xs font-medium text-muted-foreground">
        Aktywne filtry ({getActiveFilters().length})
      </span>
      <Button 
        variant="ghost" 
        size="sm" 
        className="h-6 text-xs"
        onClick={resetFilters}
        data-testid="button-clear-all-filters"
      >
        Wyczyść wszystkie
      </Button>
    </div>
    
    <div className="flex flex-wrap gap-1.5">
      {getActiveFilters().map(filter => (
        <Badge 
          key={filter.key} 
          variant="secondary" 
          className="gap-1.5 pr-1 text-xs h-6"
          data-testid={`active-filter-${filter.key}`}
        >
          <span className="text-muted-foreground">{filter.label}:</span>
          <span className="font-medium">{filter.value}</span>
          <button
            onClick={() => clearFilter(filter.key)}
            className="ml-0.5 hover:bg-background rounded-sm p-0.5"
            data-testid={`clear-filter-${filter.key}`}
          >
            <X className="h-3 w-3" />
          </button>
        </Badge>
      ))}
    </div>
  </div>
)}
```

---

## 6. Grupowanie Formatek (Batches) ⚠️ KRYTYCZNE

**Status:** Backend TODO, UI TODO

**Logika:** Zobacz `docs/production-workflow-specification.md` sekcja "Grupowanie Formatek"

**Endpoint:** `POST /api/production/planning/lines/:id/generate-batches`

**UI:** Accordion pod szczegółami linii planu w prawym panelu

---

## 7. Podsumowanie - Plan Implementacji

### Sprint 1: Dual-Panel Layout + Statistics Bar (1 dzień)
- [ ] CSS Grid layout: `grid-cols-1 lg:grid-cols-2`
- [ ] Usunąć zakładki Catalog/Orders
- [ ] Left panel: orders list (istniejąca)
- [ ] Right panel: 25px statistics bar
- [ ] Right panel: detailed product list (tabela)

### Sprint 2: Right Panel - Product List Columns (2 dni)
- [ ] Backend: `GET /api/production/planning/plans/:id/items` (JOIN z orders, catalog, BOM count)
- [ ] Frontend: Tabela z wszystkimi kolumnami (ID, zdjęcie, order, date, buyer, type, SKU, dims, colors, price, BOM)
- [ ] Kolorowe badges (identyczne jak catalog-products)
- [ ] Helper functions: `getTextColorForBackground`, `needsVisibleBorder`

### Sprint 3: Internal Orders System (3 dni) ⚠️ BLOKUJĄCE
- [ ] Tabele: `commerce.internal_orders`, `commerce.internal_order_items`
- [ ] Backend CRUD: `/api/production/internal-orders/*`
- [ ] UI: `/production/internal-orders` (lista + tworzenie)
- [ ] UI: `/production/internal-orders/:id` (szczegóły)
- [ ] Integracja: rozszerzenie `available-orders` endpoint o internal orders
- [ ] UI: Rozróżnienie marketplace vs. internal w left panel

### Sprint 4: Filter Presets (1 dzień)
- [ ] 24px color tiles (z dictionaries)
- [ ] 24px dimension tiles (length/width presets)
- [ ] Active filters summary panel
- [ ] Clear individual filter
- [ ] Clear all filters

### Sprint 5: Batch Generation (3 dni)
- [ ] Backend: algorytm grupowania formatek
- [ ] Endpoint: `POST /api/production/planning/lines/:id/generate-batches`
- [ ] Programowe wyliczanie materiałów
- [ ] UI: accordion view batchy w right panel

---

**Dokument zaktualizowany:** 2025-11-11  
**Wersja:** 2.0  
**Następne kroki:** Sprint 1 - Dual-Panel Layout
