Skip to content

Product Detail Tabs Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add tabbed navigation to the product admin show page so users can view Stock, Pricelists, and Purchase Orders linked to a product.

Architecture: Each tab is a separate route in ProductController with full page reload. A shared Twig partial renders the tab bar. Related entities are queried by productExternalId using existing repository methods.

Tech Stack: Symfony 8.0, PHP 8.4, Twig, existing admin CSS (inline <style> in base.html.twig)


Task 1: Create the shared tab partial

Files: - Create: templates/admin/products/_tabs.html.twig

Step 1: Create the tab bar partial

Create templates/admin/products/_tabs.html.twig:

{# Expects: entity (Product), active_tab (string: 'details'|'stock'|'pricelists'|'purchase_orders') #}
<div style="display: flex; gap: 0; margin-bottom: 20px; border-bottom: 2px solid var(--admin-border-color);">
    <a href="{{ path('admin_products_show', {id: entity.id}) }}"
       style="padding: 10px 20px; text-decoration: none; font-weight: 500; border-bottom: 2px solid {{ active_tab == 'details' ? 'var(--admin-primary)' : 'transparent' }}; margin-bottom: -2px; color: {{ active_tab == 'details' ? 'var(--admin-primary)' : 'var(--admin-secondary)' }};">
        Details
    </a>
    <a href="{{ path('admin_products_stock', {id: entity.id}) }}"
       style="padding: 10px 20px; text-decoration: none; font-weight: 500; border-bottom: 2px solid {{ active_tab == 'stock' ? 'var(--admin-primary)' : 'transparent' }}; margin-bottom: -2px; color: {{ active_tab == 'stock' ? 'var(--admin-primary)' : 'var(--admin-secondary)' }};">
        Stock
    </a>
    <a href="{{ path('admin_products_pricelists', {id: entity.id}) }}"
       style="padding: 10px 20px; text-decoration: none; font-weight: 500; border-bottom: 2px solid {{ active_tab == 'pricelists' ? 'var(--admin-primary)' : 'transparent' }}; margin-bottom: -2px; color: {{ active_tab == 'pricelists' ? 'var(--admin-primary)' : 'var(--admin-secondary)' }};">
        Pricelists
    </a>
    <a href="{{ path('admin_products_purchase_orders', {id: entity.id}) }}"
       style="padding: 10px 20px; text-decoration: none; font-weight: 500; border-bottom: 2px solid {{ active_tab == 'purchase_orders' ? 'var(--admin-primary)' : 'transparent' }}; margin-bottom: -2px; color: {{ active_tab == 'purchase_orders' ? 'var(--admin-primary)' : 'var(--admin-secondary)' }};">
        Purchase Orders
    </a>
</div>

Step 2: Commit

git add templates/admin/products/_tabs.html.twig
git commit -m "Add product detail tab bar partial"

Task 2: Add tab bar to existing product show page and pass active_tab

Files: - Modify: templates/admin/products/show.html.twig:23-28 (inside admin_content block) - Modify: src/Controller/Admin/ProductController.php:75-81 (show action)

Step 1: Write the failing test

Create tests/Controller/Admin/ProductDetailTabsTest.php:

<?php

declare(strict_types=1);

namespace App\Tests\Controller\Admin;

use App\Entity\Product;
use App\Entity\User;
use App\Enum\ProcessingState;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ProductDetailTabsTest extends WebTestCase
{
    private KernelBrowser $client;
    private EntityManagerInterface $em;

    protected function setUp(): void
    {
        $this->client = static::createClient();
        $this->em = static::getContainer()->get(EntityManagerInterface::class);
        $this->loginAsAdmin();
    }

    public function testShowPageDisplaysTabBar(): void
    {
        $product = $this->createProduct('PROD-001');

        $this->client->request('GET', '/admin/products/'.$product->getId());

        $this->assertResponseIsSuccessful();
        $this->assertSelectorExists('a[href*="/admin/products/'.$product->getId().'/stock"]');
        $this->assertSelectorExists('a[href*="/admin/products/'.$product->getId().'/pricelists"]');
        $this->assertSelectorExists('a[href*="/admin/products/'.$product->getId().'/purchase-orders"]');
    }

    private function loginAsAdmin(): void
    {
        $userRepository = $this->em->getRepository(User::class);
        $admin = $userRepository->findOneBy([]);
        if (!$admin) {
            $admin = new User();
            $admin->setEmail('admin@test.com');
            $admin->setPassword('$2y$13$dummy');
            $admin->setRoles(['ROLE_ADMIN']);
            $this->em->persist($admin);
            $this->em->flush();
        }
        $this->client->loginUser($admin);
    }

    private function createProduct(string $externalId): Product
    {
        $product = new Product();
        $product->setExternalId($externalId);
        $product->setProcessingState(ProcessingState::PENDING);
        $product->setCurrentData(['name' => 'Test Product']);
        $product->setChangedFields([]);
        $this->em->persist($product);
        $this->em->flush();

        return $product;
    }
}

Step 2: Run test to verify it fails

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testShowPageDisplaysTabBar -v

Expected: FAIL — the tab bar links don't exist yet on the show page.

Step 3: Modify the show action to pass active_tab

In src/Controller/Admin/ProductController.php, change the show() method:

#[Route('/{id}', name: 'admin_products_show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(int $id): Response
{
    $entity = $this->findEntityOr404($id);

    return $this->renderShow($entity, ['active_tab' => 'details']);
}

Step 4: Add tab bar include to show template

In templates/admin/products/show.html.twig, insert after {{ flash.render_flash_messages() }} and before the <div class="admin-page-header">:

    {% include 'admin/products/_tabs.html.twig' with {entity: entity, active_tab: active_tab|default('details')} %}

Step 5: Run test to verify it passes

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testShowPageDisplaysTabBar -v

Expected: PASS

Step 6: Commit

git add src/Controller/Admin/ProductController.php templates/admin/products/show.html.twig tests/Controller/Admin/ProductDetailTabsTest.php
git commit -m "Add tab bar to product show page"

Task 3: Add Stock tab route and template

Files: - Modify: src/Controller/Admin/ProductController.php (add stock action) - Create: templates/admin/products/stock.html.twig - Modify: tests/Controller/Admin/ProductDetailTabsTest.php (add stock tests)

Step 1: Write the failing tests

Add to tests/Controller/Admin/ProductDetailTabsTest.php:

public function testStockTabRequiresAuthentication(): void
{
    $client = static::createClient();
    $client->request('GET', '/admin/products/1/stock');

    $this->assertResponseRedirects();
    $this->assertStringContainsString('/login', $client->getResponse()->headers->get('Location'));
}

public function testStockTabReturns404ForMissingProduct(): void
{
    $this->client->request('GET', '/admin/products/99999/stock');

    $this->assertResponseStatusCodeSame(404);
}

public function testStockTabDisplaysStockRecords(): void
{
    $product = $this->createProduct('PROD-STOCK-001');
    $this->createStock($product->getExternalId(), 'SUPPLIER-001', '100.0000');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/stock');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'SUPPLIER-001');
    $this->assertSelectorTextContains('body', '100.0000');
}

public function testStockTabShowsEmptyState(): void
{
    $product = $this->createProduct('PROD-STOCK-EMPTY');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/stock');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'No stock records');
}

Add the helper method:

private function createStock(string $productExternalId, string $supplierExternalId, string $quantity): \App\Entity\Stock
{
    $stock = new \App\Entity\Stock();
    $stock->setProductExternalId($productExternalId);
    $stock->setSupplierExternalId($supplierExternalId);
    $stock->setQuantity($quantity);
    $this->em->persist($stock);
    $this->em->flush();

    return $stock;
}

Step 2: Run tests to verify they fail

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testStockTab -v

Expected: FAIL — route not defined.

Step 3: Add stock action to ProductController

Add to src/Controller/Admin/ProductController.php (before the edit method):

use App\Repository\StockRepository;
#[Route('/{id}/stock', name: 'admin_products_stock', requirements: ['id' => '\d+'], methods: ['GET'])]
public function stock(int $id, StockRepository $stockRepository): Response
{
    $entity = $this->findEntityOr404($id);
    $stocks = $stockRepository->findByProduct($entity->getExternalId());

    return $this->render('admin/products/stock.html.twig', [
        'entity' => $entity,
        'entity_label' => $this->getEntityLabel(),
        'route_prefix' => $this->getRoutePrefix(),
        'stocks' => $stocks,
        'active_tab' => 'stock',
    ]);
}

Step 4: Create stock template

Create templates/admin/products/stock.html.twig:

{% extends 'admin/base.html.twig' %}

{% block admin_title %}Product #{{ entity.id }} - Stock{% endblock %}

{% block admin_breadcrumb %}
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_index') }}">Products</a>
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_show', {id: entity.id}) }}">#{{ entity.id }}</a>
    <span class="admin-breadcrumb-separator">/</span>
    <span>Stock</span>
{% endblock %}

{% block admin_actions %}
    <a href="{{ path('admin_products_edit', {id: entity.id}) }}" class="admin-btn admin-btn-primary">
        Edit Product
    </a>
    <a href="{{ path('admin_products_index') }}" class="admin-btn admin-btn-secondary">
        Back to List
    </a>
{% endblock %}

{% block admin_content %}
    <div class="admin-page-header">
        <h1 class="admin-page-title">Product #{{ entity.id }}</h1>
    </div>

    {% include 'admin/products/_tabs.html.twig' with {entity: entity, active_tab: active_tab} %}

    <div class="admin-card">
        <div class="admin-card-header">
            <h3 class="admin-card-title">Stock ({{ stocks|length }})</h3>
        </div>
        <div class="admin-card-body" style="padding: 0; overflow-x: auto;">
            {% if stocks|length == 0 %}
                <div class="admin-empty-state" style="padding: 40px;">
                    <div class="admin-empty-state-icon">&#128230;</div>
                    <div class="admin-empty-state-title">No stock records</div>
                    <p>No stock records found for this product.</p>
                </div>
            {% else %}
                <table class="admin-table">
                    <thead>
                        <tr>
                            <th>Supplier External ID</th>
                            <th>Supplier Product ID</th>
                            <th>Quantity</th>
                            <th>Previous Quantity</th>
                            <th>Qty Changed</th>
                            <th>Purchase Price</th>
                            <th>Suggested Sale Price</th>
                            <th>Created At</th>
                            <th>Updated At</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for stock in stocks %}
                            <tr>
                                <td>{{ stock.supplierExternalId }}</td>
                                <td>{{ stock.supplierProductId ?? '-' }}</td>
                                <td>{{ stock.quantity|number_format(4, '.', ',') }}</td>
                                <td>{{ stock.previousQuantity is not null ? stock.previousQuantity|number_format(4, '.', ',') : '-' }}</td>
                                <td>
                                    {% if stock.quantityChanged %}
                                        <span class="admin-badge admin-badge-warning">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>{{ stock.purchasePrice is not null ? stock.purchasePrice|number_format(4, '.', ',') : '-' }}</td>
                                <td>{{ stock.suggestedSalePrice is not null ? stock.suggestedSalePrice|number_format(4, '.', ',') : '-' }}</td>
                                <td>{{ stock.createdAt ? stock.createdAt|date('Y-m-d H:i:s') : '-' }}</td>
                                <td>{{ stock.updatedAt ? stock.updatedAt|date('Y-m-d H:i:s') : '-' }}</td>
                            </tr>
                        {% endfor %}
                    </tbody>
                </table>
            {% endif %}
        </div>
    </div>
{% endblock %}

Step 5: Run tests to verify they pass

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testStockTab -v

Expected: All stock tests PASS.

Step 6: Commit

git add src/Controller/Admin/ProductController.php templates/admin/products/stock.html.twig tests/Controller/Admin/ProductDetailTabsTest.php
git commit -m "Add Stock tab to product detail view"

Task 4: Add Pricelists tab route and template

Files: - Modify: src/Controller/Admin/ProductController.php (add pricelists action) - Create: templates/admin/products/pricelists.html.twig - Modify: tests/Controller/Admin/ProductDetailTabsTest.php (add pricelist tests)

Step 1: Write the failing tests

Add to tests/Controller/Admin/ProductDetailTabsTest.php:

public function testPricelistsTabRequiresAuthentication(): void
{
    $client = static::createClient();
    $client->request('GET', '/admin/products/1/pricelists');

    $this->assertResponseRedirects();
    $this->assertStringContainsString('/login', $client->getResponse()->headers->get('Location'));
}

public function testPricelistsTabReturns404ForMissingProduct(): void
{
    $this->client->request('GET', '/admin/products/99999/pricelists');

    $this->assertResponseStatusCodeSame(404);
}

public function testPricelistsTabDisplaysPricelistItems(): void
{
    $product = $this->createProduct('PROD-PL-001');
    $this->createPricelistItem($product->getExternalId(), 'PL-001', '49.9900');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/pricelists');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'PL-001');
    $this->assertSelectorTextContains('body', '49.9900');
}

public function testPricelistsTabShowsEmptyState(): void
{
    $product = $this->createProduct('PROD-PL-EMPTY');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/pricelists');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'No pricelist items');
}

Add the helper method:

private function createPricelistItem(string $productExternalId, string $pricelistExternalId, string $price): \App\Entity\PricelistItem
{
    $item = new \App\Entity\PricelistItem();
    $item->setProductExternalId($productExternalId);
    $item->setPricelistExternalId($pricelistExternalId);
    $item->setPrice($price);
    $this->em->persist($item);
    $this->em->flush();

    return $item;
}

Step 2: Run tests to verify they fail

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testPricelists -v

Expected: FAIL — route not defined.

Step 3: Add pricelists action to ProductController

Add import:

use App\Repository\PricelistItemRepository;

Add action:

#[Route('/{id}/pricelists', name: 'admin_products_pricelists', requirements: ['id' => '\d+'], methods: ['GET'])]
public function pricelists(int $id, PricelistItemRepository $pricelistItemRepository): Response
{
    $entity = $this->findEntityOr404($id);
    $pricelistItems = $pricelistItemRepository->findByProduct($entity->getExternalId());

    return $this->render('admin/products/pricelists.html.twig', [
        'entity' => $entity,
        'entity_label' => $this->getEntityLabel(),
        'route_prefix' => $this->getRoutePrefix(),
        'pricelist_items' => $pricelistItems,
        'active_tab' => 'pricelists',
    ]);
}

Step 4: Create pricelists template

Create templates/admin/products/pricelists.html.twig:

{% extends 'admin/base.html.twig' %}

{% block admin_title %}Product #{{ entity.id }} - Pricelists{% endblock %}

{% block admin_breadcrumb %}
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_index') }}">Products</a>
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_show', {id: entity.id}) }}">#{{ entity.id }}</a>
    <span class="admin-breadcrumb-separator">/</span>
    <span>Pricelists</span>
{% endblock %}

{% block admin_actions %}
    <a href="{{ path('admin_products_edit', {id: entity.id}) }}" class="admin-btn admin-btn-primary">
        Edit Product
    </a>
    <a href="{{ path('admin_products_index') }}" class="admin-btn admin-btn-secondary">
        Back to List
    </a>
{% endblock %}

{% block admin_content %}
    {% set state_colors = {
        'pending': 'warning',
        'processing': 'info',
        'processed': 'success',
        'failed': 'danger',
        'awaiting_dependency': 'secondary'
    } %}

    <div class="admin-page-header">
        <h1 class="admin-page-title">Product #{{ entity.id }}</h1>
    </div>

    {% include 'admin/products/_tabs.html.twig' with {entity: entity, active_tab: active_tab} %}

    <div class="admin-card">
        <div class="admin-card-header">
            <h3 class="admin-card-title">Pricelist Items ({{ pricelist_items|length }})</h3>
        </div>
        <div class="admin-card-body" style="padding: 0; overflow-x: auto;">
            {% if pricelist_items|length == 0 %}
                <div class="admin-empty-state" style="padding: 40px;">
                    <div class="admin-empty-state-icon">&#128181;</div>
                    <div class="admin-empty-state-title">No pricelist items</div>
                    <p>No pricelist items found for this product.</p>
                </div>
            {% else %}
                <table class="admin-table">
                    <thead>
                        <tr>
                            <th>Pricelist Name</th>
                            <th>Pricelist External ID</th>
                            <th>Price</th>
                            <th>Previous Price</th>
                            <th>Price Changed</th>
                            <th>Is New</th>
                            <th>Start Date</th>
                            <th>End Date</th>
                            <th>Promo</th>
                            <th>Action</th>
                            <th>Until Stock 0</th>
                            <th>Processing State</th>
                            <th>Updated At</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for item in pricelist_items %}
                            <tr>
                                <td>{{ item.pricelistName ?? '-' }}</td>
                                <td>{{ item.pricelistExternalId }}</td>
                                <td>{{ item.price is not null ? item.price|number_format(4, '.', ',') : '-' }}</td>
                                <td>{{ item.previousPrice is not null ? item.previousPrice|number_format(4, '.', ',') : '-' }}</td>
                                <td>
                                    {% if item.priceChanged %}
                                        <span class="admin-badge admin-badge-warning">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>
                                    {% if item.new %}
                                        <span class="admin-badge admin-badge-info">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>{{ item.startDate ? item.startDate|date('Y-m-d') : '-' }}</td>
                                <td>{{ item.endDate ? item.endDate|date('Y-m-d') : '-' }}</td>
                                <td>
                                    {% if item.promoPrice %}
                                        <span class="admin-badge admin-badge-success">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>
                                    {% if item.action %}
                                        <span class="admin-badge admin-badge-success">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>
                                    {% if item.untilAtxStockIs0 %}
                                        <span class="admin-badge admin-badge-info">Yes</span>
                                    {% else %}
                                        <span class="admin-badge admin-badge-secondary">No</span>
                                    {% endif %}
                                </td>
                                <td>
                                    <span class="admin-badge admin-badge-{{ state_colors[item.processingState.value]|default('secondary') }}">
                                        {{ item.processingState.value }}
                                    </span>
                                </td>
                                <td>{{ item.updatedAt ? item.updatedAt|date('Y-m-d H:i:s') : '-' }}</td>
                            </tr>
                        {% endfor %}
                    </tbody>
                </table>
            {% endif %}
        </div>
    </div>
{% endblock %}

Step 5: Run tests to verify they pass

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testPricelists -v

Expected: All pricelist tests PASS.

Step 6: Commit

git add src/Controller/Admin/ProductController.php templates/admin/products/pricelists.html.twig tests/Controller/Admin/ProductDetailTabsTest.php
git commit -m "Add Pricelists tab to product detail view"

Task 5: Add Purchase Orders tab route and template

Files: - Modify: src/Controller/Admin/ProductController.php (add purchaseOrders action) - Create: templates/admin/products/purchase-orders.html.twig - Modify: tests/Controller/Admin/ProductDetailTabsTest.php (add PO tests)

Step 1: Write the failing tests

Add to tests/Controller/Admin/ProductDetailTabsTest.php:

public function testPurchaseOrdersTabRequiresAuthentication(): void
{
    $client = static::createClient();
    $client->request('GET', '/admin/products/1/purchase-orders');

    $this->assertResponseRedirects();
    $this->assertStringContainsString('/login', $client->getResponse()->headers->get('Location'));
}

public function testPurchaseOrdersTabReturns404ForMissingProduct(): void
{
    $this->client->request('GET', '/admin/products/99999/purchase-orders');

    $this->assertResponseStatusCodeSame(404);
}

public function testPurchaseOrdersTabDisplaysOrderLines(): void
{
    $product = $this->createProduct('PROD-PO-001');
    $this->createPurchaseOrderWithLine($product->getExternalId(), 'PO-001', 'Vendor A');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/purchase-orders');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'PO-001');
    $this->assertSelectorTextContains('body', 'Vendor A');
}

public function testPurchaseOrdersTabShowsEmptyState(): void
{
    $product = $this->createProduct('PROD-PO-EMPTY');

    $this->client->request('GET', '/admin/products/'.$product->getId().'/purchase-orders');

    $this->assertResponseIsSuccessful();
    $this->assertSelectorTextContains('body', 'No purchase order lines');
}

Add the helper method:

private function createPurchaseOrderWithLine(string $productExternalId, string $poName, string $vendor): \App\Entity\PurchaseOrderLine
{
    $po = new \App\Entity\PurchaseOrder();
    $po->setExternalId('PO-EXT-'.uniqid());
    $po->setName($poName);
    $po->setVendor($vendor);
    $po->setState('purchase');
    $po->setProcessingState(\App\Enum\ProcessingState::PENDING);
    $po->setCurrentData([]);
    $po->setChangedFields([]);
    $this->em->persist($po);

    $line = new \App\Entity\PurchaseOrderLine();
    $line->setPurchaseOrder($po);
    $line->setProductExternalId($productExternalId);
    $line->setQuantity('10.0000');
    $line->setQtyReceived('0.0000');
    $this->em->persist($line);

    $this->em->flush();

    return $line;
}

Step 2: Run tests to verify they fail

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testPurchaseOrders -v

Expected: FAIL — route not defined.

Step 3: Add purchaseOrders action to ProductController

Add import:

use App\Repository\PurchaseOrderLineRepository;

Add action:

#[Route('/{id}/purchase-orders', name: 'admin_products_purchase_orders', requirements: ['id' => '\d+'], methods: ['GET'])]
public function purchaseOrders(int $id, PurchaseOrderLineRepository $purchaseOrderLineRepository): Response
{
    $entity = $this->findEntityOr404($id);
    $lines = $purchaseOrderLineRepository->findByProductExternalId($entity->getExternalId());

    return $this->render('admin/products/purchase-orders.html.twig', [
        'entity' => $entity,
        'entity_label' => $this->getEntityLabel(),
        'route_prefix' => $this->getRoutePrefix(),
        'lines' => $lines,
        'active_tab' => 'purchase_orders',
    ]);
}

Step 4: Create purchase orders template

Create templates/admin/products/purchase-orders.html.twig:

{% extends 'admin/base.html.twig' %}

{% block admin_title %}Product #{{ entity.id }} - Purchase Orders{% endblock %}

{% block admin_breadcrumb %}
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_index') }}">Products</a>
    <span class="admin-breadcrumb-separator">/</span>
    <a href="{{ path('admin_products_show', {id: entity.id}) }}">#{{ entity.id }}</a>
    <span class="admin-breadcrumb-separator">/</span>
    <span>Purchase Orders</span>
{% endblock %}

{% block admin_actions %}
    <a href="{{ path('admin_products_edit', {id: entity.id}) }}" class="admin-btn admin-btn-primary">
        Edit Product
    </a>
    <a href="{{ path('admin_products_index') }}" class="admin-btn admin-btn-secondary">
        Back to List
    </a>
{% endblock %}

{% block admin_content %}
    {% set state_colors = {
        'draft': 'secondary',
        'sent': 'info',
        'to approve': 'warning',
        'purchase': 'primary',
        'done': 'success',
        'cancel': 'danger'
    } %}

    <div class="admin-page-header">
        <h1 class="admin-page-title">Product #{{ entity.id }}</h1>
    </div>

    {% include 'admin/products/_tabs.html.twig' with {entity: entity, active_tab: active_tab} %}

    <div class="admin-card">
        <div class="admin-card-header">
            <h3 class="admin-card-title">Purchase Order Lines ({{ lines|length }})</h3>
        </div>
        <div class="admin-card-body" style="padding: 0; overflow-x: auto;">
            {% if lines|length == 0 %}
                <div class="admin-empty-state" style="padding: 40px;">
                    <div class="admin-empty-state-icon">&#128722;</div>
                    <div class="admin-empty-state-title">No purchase order lines</div>
                    <p>No purchase order lines found for this product.</p>
                </div>
            {% else %}
                <table class="admin-table">
                    <thead>
                        <tr>
                            <th>PO Name</th>
                            <th>Vendor</th>
                            <th>Product Name</th>
                            <th>Quantity</th>
                            <th>Qty Received</th>
                            <th>Unit Price</th>
                            <th>Date Planned</th>
                            <th>PO State</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for line in lines %}
                            <tr>
                                <td>
                                    <a href="{{ path('admin_purchase_orders_show', {id: line.purchaseOrder.id}) }}">
                                        {{ line.purchaseOrder.name ?? '#' ~ line.purchaseOrder.id }}
                                    </a>
                                </td>
                                <td>{{ line.purchaseOrder.vendor ?? '-' }}</td>
                                <td>{{ line.name ? line.name|slice(0, 50) ~ (line.name|length > 50 ? '...' : '') : '-' }}</td>
                                <td>{{ line.quantity|number_format(4, '.', ',') }}</td>
                                <td>{{ line.qtyReceived|number_format(4, '.', ',') }}</td>
                                <td>{{ line.priceUnit is not null ? line.priceUnit|number_format(4, '.', ',') : '-' }}</td>
                                <td>{{ line.datePlanned ? line.datePlanned|date('Y-m-d') : '-' }}</td>
                                <td>
                                    {% if line.purchaseOrder.state %}
                                        <span class="admin-badge admin-badge-{{ state_colors[line.purchaseOrder.state|lower]|default('secondary') }}">
                                            {{ line.purchaseOrder.state }}
                                        </span>
                                    {% else %}
                                        -
                                    {% endif %}
                                </td>
                            </tr>
                        {% endfor %}
                    </tbody>
                </table>
            {% endif %}
        </div>
    </div>
{% endblock %}

Step 5: Run tests to verify they pass

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php --filter testPurchaseOrders -v

Expected: All purchase order tests PASS.

Step 6: Commit

git add src/Controller/Admin/ProductController.php templates/admin/products/purchase-orders.html.twig tests/Controller/Admin/ProductDetailTabsTest.php
git commit -m "Add Purchase Orders tab to product detail view"

Task 6: Run full test suite and static analysis

Step 1: Run all product detail tab tests

Run: bin/phpunit tests/Controller/Admin/ProductDetailTabsTest.php -v

Expected: All tests PASS.

Step 2: Run PHPStan

Run: vendor/bin/phpstan analyse

Expected: No new errors.

Step 3: Run PHP-CS-Fixer

Run: vendor/bin/php-cs-fixer fix --dry-run --diff

Fix any style issues if needed: vendor/bin/php-cs-fixer fix

Step 4: Final commit if any fixes were needed

git add -A
git commit -m "Fix code style for product detail tabs"