= 2; $show_tickets = $config_module_enable_ticketing == 1 && lookupUserPermission("module_support") >= 1; $can_add_ticket = $config_module_enable_ticketing == 1 && lookupUserPermission("module_support") >= 2; // Same gate as the Billing card on a client's own pages (inc_client_top_head.php) $show_billing = $config_module_enable_accounting == 1 && lookupUserPermission("module_financial") >= 1; /** * Builds a link to this page with filter overrides applied. Pass null to drop * a filter. Paging always resets - a page number from the previous filter set * means nothing once the result set changes. */ function clientsFilterUrl($overrides = []) { $params = $_GET; foreach ($overrides as $name => $value) { if ($value === null) { unset($params[$name]); } else { $params[$name] = $value; } } unset($params['page']); return escapeHtml('?' . http_build_query($params)); } // Chips describing what is currently filtering the list, built as each // filter is parsed. Each one renders with an x that drops just that filter. $active_filters = []; // Leads Filter if (isset($_GET['leads']) && $_GET['leads'] == 1) { $leads_filter = 1; $leads_query = "AND client_lead = 1"; } else { $leads_filter = 0; $leads_query = "AND client_lead = 0"; } // Archived - filter_header.php has already turned ?archived into $archive_query if ($archived) { $active_filters[] = ['label' => 'Showing', 'value' => 'Archived', 'drop' => 'archived']; } // Tags Filter if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) { $tag_filter = implode(",", array_map('intval', $_GET['tags'])); $tag_query = "AND clients.client_id IN (SELECT client_tags.client_id FROM client_tags WHERE client_tags.tag_id IN ($tag_filter))"; $tag_names = []; $sql_tag_names = mysqli_query($mysqli, "SELECT tag_name FROM tags WHERE tag_id IN ($tag_filter) ORDER BY tag_name"); while ($row = mysqli_fetch_assoc($sql_tag_names)) { $tag_names[] = escapeHtml($row['tag_name']); } $active_filters[] = ['label' => 'Tags', 'value' => implode(', ', $tag_names), 'drop' => 'tags']; } else { $tag_filter = 0; $tag_query = ''; } // Industry Filter if (!empty($_GET['industry'])) { $industry_query = "AND clients.client_type = '" . escapeSql($_GET['industry']) . "'"; $industry_filter = escapeHtml($_GET['industry']); $active_filters[] = ['label' => 'Industry', 'value' => $industry_filter, 'drop' => 'industry']; } else { $industry_query = ''; $industry_filter = ''; } // Referral Filter if (!empty($_GET['referral'])) { $referral_query = "AND clients.client_referral = '" . escapeSql($_GET['referral']) . "'"; $referral_filter = escapeHtml($_GET['referral']); $active_filters[] = ['label' => 'Referral', 'value' => $referral_filter, 'drop' => 'referral']; } else { $referral_query = ''; $referral_filter = ''; } // Created date $date_filter_active = $dtf !== '1970-01-01' || $dtt !== '2099-12-31'; if ($date_filter_active) { $active_filters[] = ['label' => 'Created', 'value' => escapeHtml("$dtf to $dtt"), 'drop' => ['canned_date', 'dtf', 'dtt']]; } if ($q !== '') { $active_filters[] = ['label' => 'Search', 'value' => stripslashes(escapeHtml($q)), 'drop' => 'q']; } // How many of the collapsed panel's filters are set - shown on the filter button $hidden_filter_count = count(array_filter([$tag_query, $industry_query, $referral_query, $date_filter_active])); /* * Sorting. Only these keys are accepted; anything else falls back to the * page default instead of reaching ORDER BY as an unknown column. Balance and * open tickets are worked out per client, so they sort on an expression. */ $balance_sql = "(SELECT COALESCE(SUM(invoice_amount), 0) FROM invoices WHERE invoice_client_id = clients.client_id AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable')) - (SELECT COALESCE(SUM(payment_amount), 0) FROM payments JOIN invoices ON payment_invoice_id = invoice_id WHERE invoice_client_id = clients.client_id)"; $open_tickets_sql = "(SELECT COUNT(ticket_id) FROM tickets WHERE ticket_client_id = clients.client_id AND ticket_resolved_at IS NULL)"; $sortable = [ 'client_name' => 'client_name', 'contact_name' => 'contact_name', 'location_city' => 'location_city', 'client_accessed_at' => 'client_accessed_at', 'client_created_at' => 'client_created_at', ]; if ($show_billing) { $sortable['balance'] = $balance_sql; } if ($show_tickets) { $sortable['open_tickets'] = $open_tickets_sql; } if (!isset($sortable[$sort])) { $sort = 'client_accessed_at'; } $order_by = $sortable[$sort]; // Sort link for a column header, plus the arrow when it is the active sort function clientsSortLink($column, $label) { global $sort, $disp, $order_icon; $url = clientsFilterUrl(['sort' => $column, 'order' => $sort === $column ? $disp : ($column === 'client_name' || $column === 'contact_name' || $column === 'location_city' ? 'ASC' : 'DESC')]); return '' . $label . ($sort === $column ? " $order_icon" : '') . ''; } $client_scope = clientScopeSql('clients.client_id'); /* * One row per client. The primary contact and location are picked by id * rather than joined on the primary flag, so a client with two "primary" * contacts still lists once - which used to need a GROUP BY over every * column of three tables. */ $sql = mysqli_query( $mysqli, " SELECT SQL_CALC_FOUND_ROWS clients.client_id, client_name, client_type, client_abbreviation, client_rate, client_currency_code, client_lead, client_archived_at, contact_id, contact_name, contact_phone, contact_phone_country_code, contact_extension, contact_mobile, contact_mobile_country_code, contact_email, location_id, location_address, location_city, location_state, location_zip, location_country FROM clients LEFT JOIN contacts ON contact_id = ( SELECT c.contact_id FROM contacts c WHERE c.contact_client_id = clients.client_id AND c.contact_primary = 1 ORDER BY c.contact_id LIMIT 1 ) LEFT JOIN locations ON location_id = ( SELECT l.location_id FROM locations l WHERE l.location_client_id = clients.client_id AND l.location_primary = 1 ORDER BY l.location_id LIMIT 1 ) WHERE (client_name LIKE '%$q%' OR client_abbreviation LIKE '%$q%' OR client_type LIKE '%$q%' OR client_referral LIKE '%$q%' OR contact_email LIKE '%$q%' OR contact_name LIKE '%$q%' OR contact_phone LIKE '%$phone_query%' OR contact_mobile LIKE '%$phone_query%' OR location_address LIKE '%$q%' OR location_city LIKE '%$q%' OR location_state LIKE '%$q%' OR location_zip LIKE '%$q%' OR location_country LIKE '%$q%' OR client_tax_id_number LIKE '%$q%' OR clients.client_id IN (SELECT client_tags.client_id FROM client_tags JOIN tags ON tags.tag_id = client_tags.tag_id WHERE tag_name LIKE '%$q%')) AND client_$archive_query AND DATE(client_created_at) BETWEEN '$dtf' AND '$dtt' $leads_query $client_scope $tag_query $industry_query $referral_query ORDER BY $order_by $order, client_name ASC LIMIT $record_from, $record_to "); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $clients = []; while ($row = mysqli_fetch_assoc($sql)) { $clients[intval($row['client_id'])] = $row; } /* * Everything else a row shows, fetched for the whole page at once - a query * per lookup, not a query per lookup per row. */ $client_tags = []; $client_money = []; $client_tickets = []; if ($clients) { $page_ids = implode(',', array_keys($clients)); $sql_tags = mysqli_query($mysqli, "SELECT client_tags.client_id, tags.tag_id, tag_name, tag_color, tag_icon FROM client_tags JOIN tags ON tags.tag_id = client_tags.tag_id WHERE client_tags.client_id IN ($page_ids) ORDER BY tag_name"); while ($row = mysqli_fetch_assoc($sql_tags)) { $client_tags[intval($row['client_id'])][] = $row; } if ($show_billing) { foreach (array_keys($clients) as $id) { $client_money[$id] = ['invoiced' => 0, 'paid' => 0, 'overdue' => 0, 'recurring' => 0]; } // Balance the way the client's own Billing card works it out: invoiced minus paid $sql_money = mysqli_query($mysqli, "SELECT invoice_client_id AS id, SUM(invoice_amount) AS v FROM invoices WHERE invoice_client_id IN ($page_ids) AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable') GROUP BY invoice_client_id"); while ($row = mysqli_fetch_assoc($sql_money)) { $client_money[intval($row['id'])]['invoiced'] = floatval($row['v']); } $sql_money = mysqli_query($mysqli, "SELECT invoice_client_id AS id, SUM(payment_amount) AS v FROM payments JOIN invoices ON payment_invoice_id = invoice_id WHERE invoice_client_id IN ($page_ids) GROUP BY invoice_client_id"); while ($row = mysqli_fetch_assoc($sql_money)) { $client_money[intval($row['id'])]['paid'] = floatval($row['v']); } // What is still owed on unpaid invoices past their due date $sql_money = mysqli_query($mysqli, "SELECT invoice_client_id AS id, SUM(invoice_amount - COALESCE((SELECT SUM(payment_amount) FROM payments WHERE payment_invoice_id = invoice_id), 0)) AS v FROM invoices WHERE invoice_client_id IN ($page_ids) AND invoice_status IN ('Sent', 'Viewed', 'Partial') AND invoice_due < CURDATE() GROUP BY invoice_client_id"); while ($row = mysqli_fetch_assoc($sql_money)) { $client_money[intval($row['id'])]['overdue'] = floatval($row['v']); } // Monthly recurring - yearly ones count as a twelfth $sql_money = mysqli_query($mysqli, "SELECT recurring_invoice_client_id AS id, SUM(CASE WHEN recurring_invoice_frequency = 'year' THEN recurring_invoice_amount / 12 ELSE recurring_invoice_amount END) AS v FROM recurring_invoices WHERE recurring_invoice_client_id IN ($page_ids) AND recurring_invoice_status = 1 AND recurring_invoice_frequency IN ('month', 'year') GROUP BY recurring_invoice_client_id"); while ($row = mysqli_fetch_assoc($sql_money)) { $client_money[intval($row['id'])]['recurring'] = floatval($row['v']); } } if ($show_tickets) { $sql_tickets = mysqli_query($mysqli, "SELECT ticket_client_id AS id, COUNT(ticket_id) AS open, SUM(ticket_sla_id > 0 AND (ticket_response_sla_alert_stage = 2 OR ticket_resolution_sla_alert_stage = 2 OR ticket_response_sla_met = 0 OR ticket_resolution_sla_met = 0)) AS breached FROM tickets WHERE ticket_client_id IN ($page_ids) AND ticket_resolved_at IS NULL GROUP BY ticket_client_id"); while ($row = mysqli_fetch_assoc($sql_tickets)) { $client_tickets[intval($row['id'])] = ['open' => intval($row['open']), 'breached' => intval($row['breached'])]; } } } // Clients / Leads counts for the header, in the archive state being shown $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COALESCE(SUM(client_lead = 0), 0) AS clients, COALESCE(SUM(client_lead = 1), 0) AS leads FROM clients WHERE client_$archive_query $client_scope")); $total_clients = intval($row['clients']); $total_leads = intval($row['leads']); ?>