= 2; $can_delete = lookupUserPermission('module_sales') >= 3 && !empty($config_destructive_deletes_enable); // Tabs $views = [ 'all' => ['label' => 'All', 'icon' => 'fa-th-list', 'sql' => ''], 'service' => ['label' => 'Services', 'icon' => 'fa-wrench', 'sql' => "AND product_type = 'service'"], 'product' => ['label' => 'Products', 'icon' => 'fa-box-open', 'sql' => "AND product_type = 'product'"], 'hardware' => ['label' => 'Hardware', 'icon' => 'fa-laptop', 'sql' => "AND " . productHardwareSql(), 'title' => 'Becomes a client asset when it is sold'], 'recurring' => ['label' => 'Recurring', 'icon' => 'fa-redo-alt', 'sql' => "AND product_frequency IS NOT NULL", 'title' => 'Billed each month, quarter or year by default'], 'stocked' => ['label' => 'Stocked', 'icon' => 'fa-warehouse', 'sql' => "AND product_track_stock = 1", 'title' => 'Stock tracked'], 'reorder' => ['label' => 'Reorder', 'icon' => 'fa-shopping-basket', 'sql' => "AND " . productReorderSql(), 'title' => 'Stock at or under its reorder level, or out'], ]; $view = (string) ($_GET['type'] ?? 'all'); if (!isset($views[$view])) { $view = 'all'; } // Category Filter $category_filter = intval($_GET['category'] ?? 0); $category_query = $category_filter ? "AND product_category_id = $category_filter" : ''; // Vendor Filter $vendor_filter = intval($_GET['vendor'] ?? 0); $vendor_query = $vendor_filter ? "AND product_vendor_id = $vendor_filter" : ''; $search_query = "AND (product_name LIKE '%$q%' OR product_description LIKE '%$q%' OR product_code LIKE '%$q%' OR product_vendor_part LIKE '%$q%' OR product_location LIKE '%$q%' OR category_name LIKE '%$q%' OR vendor_name LIKE '%$q%' OR product_price LIKE '%$q%')"; $from = "FROM products LEFT JOIN categories ON product_category_id = category_id LEFT JOIN taxes ON product_tax_id = tax_id LEFT JOIN vendors ON product_vendor_id = vendor_id"; $where = "WHERE product_$archive_query $search_query $category_query $vendor_query"; // Only these sort - an unknown column takes the page down $sortable_columns = ['product_name', 'product_code', 'category_name', 'vendor_name', 'product_price', 'product_cost', 'margin', 'product_stock', 'product_frequency', 'product_created_at']; if (!in_array($sort, $sortable_columns, true)) { $sort = 'product_name'; } $sort_sql = $sort === 'margin' ? "IF(product_cost > 0 AND product_price > 0, (product_price - product_cost) / product_price, NULL)" : $sort; $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS products.*, category_name, category_color, tax_name, tax_percent, vendor_name, " . productStockSql() . " AS product_stock $from $where {$views[$view]['sql']} ORDER BY $sort_sql $order, product_name ASC LIMIT $record_from, $record_to" ); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $products = []; while ($row = mysqli_fetch_assoc($sql)) { $products[] = $row; } // Each tab's count, with the same search and filters $counts = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS `all`, COALESCE(SUM(product_type = 'service'), 0) AS service, COALESCE(SUM(product_type = 'product'), 0) AS product, COALESCE(SUM(product_frequency IS NOT NULL), 0) AS recurring, COALESCE(SUM(product_track_stock = 1), 0) AS stocked, COALESCE(SUM(" . productReorderSql() . "), 0) AS reorder, COALESCE(SUM(" . productHardwareSql() . "), 0) AS hardware $from $where")); // The catalog at work - recurring, sold, on the shelf $scope_invoice = clientScopeSql('invoice_client_id'); $scope_recurring = clientScopeSql('recurring_invoice_client_id'); $recurring_stat = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COALESCE(SUM(" . recurringInvoiceMonthlySql('item_subtotal', 'recurring_invoice_frequency') . "), 0) AS monthly, COUNT(DISTINCT item_product_id) AS products, COUNT(DISTINCT recurring_invoice_client_id) AS clients FROM recurring_invoice_items JOIN recurring_invoices ON recurring_invoice_id = item_recurring_invoice_id WHERE item_product_id > 0 AND recurring_invoice_status = 1 AND recurring_invoice_archived_at IS NULL $scope_recurring")); $sold_stat = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COALESCE(SUM(item_subtotal), 0) AS amount, COUNT(DISTINCT item_invoice_id) AS invoices FROM invoice_items JOIN invoices ON invoice_id = item_invoice_id WHERE item_product_id > 0 AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable') AND invoice_date >= CURDATE() - INTERVAL 90 DAY $scope_invoice")); $top_seller = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT product_id, product_name, SUM(item_subtotal) AS amount FROM invoice_items JOIN invoices ON invoice_id = item_invoice_id JOIN products ON product_id = item_product_id WHERE invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable') AND invoice_date >= CURDATE() - INTERVAL 90 DAY $scope_invoice GROUP BY product_id ORDER BY amount DESC LIMIT 1")); $stock_stat = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COALESCE(SUM(GREATEST(s.on_hand, 0) * product_cost), 0) AS value, COALESCE(SUM(GREATEST(s.on_hand, 0)), 0) AS units, COUNT(*) AS tracked, COALESCE(SUM(s.on_hand <= 0), 0) AS out_of_stock, COALESCE(SUM(product_cost <= 0 AND s.on_hand > 0), 0) AS no_cost FROM products JOIN (SELECT stock_product_id, SUM(stock_qty) AS on_hand FROM product_stock GROUP BY stock_product_id) s ON s.stock_product_id = product_id WHERE product_track_stock = 1 AND product_archived_at IS NULL")); $tracked_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM products WHERE product_track_stock = 1 AND product_archived_at IS NULL"))[0]); $reorder_count = productReorderCount(); // Lines on recurring invoices, drafts and quotes still at an old price $reprice = productRepriceCount(); $catalog_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM products"))[0]); $filter_url = function ($changes) { $params = array_merge($_GET, $changes); unset($params['page']); return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null && $v !== 0; })); }; $money = function ($amount, $currency = null) use ($session_company_currency) { return escapeHtml(billingMoney($amount, $currency ?: $session_company_currency)); }; $add_type = in_array($view, ['product', 'hardware', 'stocked', 'reorder'], true) ? 'product' : 'service'; ?>