Product Detail Tabs Design
Summary
Add tabbed navigation to the product admin show page, allowing users to view Stock, Pricelists, and Purchase Orders linked to a product. Each tab is a separate route with full page reload, sharing a common tab bar.
Routes
| Route | Name | Purpose |
|---|---|---|
/admin/products/{id} |
admin_products_show |
Details tab (existing page, add tab bar) |
/admin/products/{id}/stock |
admin_products_stock |
Stock records for this product |
/admin/products/{id}/pricelists |
admin_products_pricelists |
Pricelist items for this product |
/admin/products/{id}/purchase-orders |
admin_products_purchase_orders |
Purchase order lines for this product |
Controller
Add 3 new actions to ProductController, each injecting the relevant repository:
#[Route('/{id}/stock', name: 'admin_products_stock', requirements: ['id' => '\d+'], methods: ['GET'])]
public function stock(int $id, StockRepository $stockRepository): Response
#[Route('/{id}/pricelists', name: 'admin_products_pricelists', requirements: ['id' => '\d+'], methods: ['GET'])]
public function pricelists(int $id, PricelistItemRepository $pricelistItemRepository): Response
#[Route('/{id}/purchase-orders', name: 'admin_products_purchase_orders', requirements: ['id' => '\d+'], methods: ['GET'])]
public function purchaseOrders(int $id, PurchaseOrderLineRepository $purchaseOrderLineRepository): Response
Each action: finds the product via findEntityOr404($id), queries related entities by $product->getExternalId(), renders its template with entity, the related collection, and active_tab.
Templates
Shared tab partial: templates/admin/products/_tabs.html.twig
Horizontal tab bar with 4 tabs: Details, Stock, Pricelists, Purchase Orders. Highlights the active tab based on an active_tab variable. Each tab links to its route.
Stock tab: templates/admin/products/stock.html.twig
Table columns: Supplier External ID, Supplier Product ID, Quantity, Previous Quantity, Qty Changed (badge), Purchase Price, Suggested Sale Price, Created At, Updated At. Empty state if no records.
Pricelists tab: templates/admin/products/pricelists.html.twig
Table columns: Pricelist Name, Pricelist External ID, Price, Previous Price, Price Changed (badge), Is New (badge), Start Date, End Date, Is Promo (badge), Is Action (badge), Until ATX Stock Is 0 (badge), Processing State (badge), Updated At. Empty state if no records.
Purchase Orders tab: templates/admin/products/purchase-orders.html.twig
Table columns: PO Name (link to PO show page), Vendor, Product Name, Quantity, Qty Received, Unit Price, Date Planned, PO State (badge). Empty state if no records.
Modify existing: templates/admin/products/show.html.twig
Include the tab partial at the top of admin_content block, before the existing product details content. Pass active_tab: 'details'.
Data Flow
ProductController::stock()
-> findEntityOr404($id) -> Product
-> stockRepo->findByProduct( -> Stock[]
$product->getExternalId())
-> render('stock.html.twig', [entity, stocks, active_tab: 'stock'])
Same pattern for pricelists (-> PricelistItem[]) and purchase orders (-> PurchaseOrderLine[]).
Existing Repository Methods Used
StockRepository::findByProduct(string $productExternalId): Stock[]PricelistItemRepository::findByProduct(string $productExternalId): PricelistItem[]PurchaseOrderLineRepository::findByProductExternalId(string $productExternalId): PurchaseOrderLine[]
No new repository methods needed.