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
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):
#[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">📦</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:
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">💵</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:
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">🛒</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