Skip to content

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.