= 2; $can_delete = lookupUserPermission('module_sales') >= 3; $mail_ready = !empty($config_smtp_provider); // Client Filter (all clients) $client = ''; if (!$client_url && !empty($_GET['client'])) { $client = intval($_GET['client']); $client_query = "AND quote_client_id = $client"; } // Category Filter $category_filter = intval($_GET['category'] ?? 0); $category_query = $category_filter ? "AND quote_category_id = $category_filter" : ''; $unanswered_sql = "quote_status IN ('Draft', 'Sent', 'Viewed')"; $statuses = [ '' => ['label' => 'All', 'where' => ''], 'Draft' => ['label' => 'Draft', 'icon' => 'fa-pencil-ruler', 'where' => "quote_status = 'Draft'", 'title' => 'Not sent yet - the client can\'t see them'], 'Awaiting' => ['label' => 'Awaiting answer', 'icon' => 'fa-hourglass-half', 'where' => "quote_status IN ('Sent', 'Viewed') AND quote_expire >= CURDATE()", 'title' => 'Sent, not answered, and not run out'], 'Expired' => ['label' => 'Expired', 'icon' => 'fa-hourglass-end', 'where' => "$unanswered_sql AND quote_expire < CURDATE()", 'title' => 'Ran out without an answer'], 'Accepted' => ['label' => 'Accepted', 'icon' => 'fa-thumbs-up', 'where' => "quote_status = 'Accepted'", 'title' => 'Accepted and not invoiced yet'], 'Invoiced' => ['label' => 'Invoiced', 'icon' => 'fa-file-invoice-dollar', 'where' => "quote_status = 'Invoiced'"], 'Declined' => ['label' => 'Declined', 'icon' => 'fa-thumbs-down', 'where' => "quote_status = 'Declined'"], ]; $status = isset($statuses[$_GET['status'] ?? '']) ? ($_GET['status'] ?? '') : ''; $status_query = $statuses[$status]['where'] ? 'AND ' . $statuses[$status]['where'] : ''; $sortable_columns = ['quote_number' => 'quote_number', 'quote_scope' => 'quote_scope', 'client_name' => 'client_name', 'quote_date' => 'quote_date', 'quote_expire' => 'quote_expire', 'quote_amount' => 'quote_amount', 'quote_status' => 'quote_status', 'category_name' => 'category_name']; if (!isset($sortable_columns[$sort])) { $sort = 'quote_number'; } $scope_query = clientScopeSql('quote_client_id'); $search_query = "AND (CONCAT(quote_prefix, quote_number) LIKE '%$q%' OR quote_scope LIKE '%$q%' OR category_name LIKE '%$q%' OR quote_status LIKE '%$q%' OR quote_amount LIKE '%$q%' OR client_name LIKE '%$q%')"; $date_query = "AND DATE(quote_date) BETWEEN '$dtf' AND '$dtt'"; $filters = "$scope_query $client_query $category_query $date_query $search_query"; $joins = "LEFT JOIN clients ON quote_client_id = client_id LEFT JOIN categories ON quote_category_id = category_id"; $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS quotes.*, client_name, category_name FROM quotes $joins WHERE 1 $filters $status_query ORDER BY {$sortable_columns[$sort]} $order, quote_id DESC LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $quotes = []; while ($row = mysqli_fetch_assoc($sql)) { $quotes[] = $row; } $count_sql = []; foreach ($statuses as $key => $s) { $count_sql[] = $key === '' ? 'COUNT(*)' : "COALESCE(SUM({$s['where']}), 0)"; } $row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . " FROM quotes $joins WHERE 1 $filters")); $counts = []; foreach (array_keys($statuses) as $i => $key) { $counts[$key] = intval($row[$i]); } // The pipeline along the top - everything in this scope $pipeline = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COALESCE(SUM(CASE WHEN {$statuses['Awaiting']['where']} THEN quote_amount END), 0) AS awaiting, COALESCE(SUM(CASE WHEN {$statuses['Awaiting']['where']} THEN quote_monthly_amount END), 0) AS awaiting_monthly, COALESCE(SUM(CASE WHEN quote_status = 'Accepted' THEN quote_monthly_amount END), 0) AS accepted_monthly, COALESCE(SUM(CASE WHEN quote_status IN ('Accepted', 'Invoiced') AND quote_date >= CURDATE() - INTERVAL 90 DAY THEN quote_monthly_amount END), 0) AS won_monthly, COALESCE(SUM(CASE WHEN {$statuses['Awaiting']['where']} THEN 1 END), 0) AS awaiting_count, COALESCE(SUM(CASE WHEN quote_status = 'Accepted' THEN quote_amount END), 0) AS accepted, COALESCE(SUM(CASE WHEN quote_status = 'Accepted' THEN 1 END), 0) AS accepted_count, COALESCE(SUM(CASE WHEN quote_status IN ('Accepted', 'Invoiced') AND quote_date >= CURDATE() - INTERVAL 90 DAY THEN quote_amount END), 0) AS won, COALESCE(SUM(CASE WHEN quote_status IN ('Accepted', 'Invoiced') AND quote_date >= CURDATE() - INTERVAL 90 DAY THEN 1 END), 0) AS won_count, COALESCE(SUM(CASE WHEN quote_status IN ('Accepted', 'Invoiced', 'Declined') AND quote_date >= CURDATE() - INTERVAL 90 DAY THEN 1 END), 0) AS answered_count FROM quotes WHERE 1 $scope_query $client_query")); $win_rate = intval($pipeline['answered_count']) ? round(intval($pipeline['won_count']) / intval($pipeline['answered_count']) * 100) : null; $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; })); }; $stat_money = function ($amount) use ($session_company_currency) { return escapeHtml(billingMoney($amount, $session_company_currency)); }; // "+ $2,400.00/mo" under a figure, when there is recurring in it $stat_monthly = function ($amount) use ($stat_money) { return floatval($amount) != 0 ? '+ ' . $stat_money($amount) . '/mo' : ''; }; ?>
Awaiting answer
out with clients
Accepted, to invoice
ready to bill
Won, 90 days
quote accepted
Win rate, 90 days
answered

Quotes

0) { ?>
Quote Client Date Valid until Total Status
+ /mo
'quotes', 'icon' => 'fa-comment-dollar', 'filtered' => $q || $dtf !== '1970-01-01', 'views' => ['status'], 'view' => emptyStatePill($statuses, $status, '', ['Expired']), 'text' => 'Price up work for a client, and turn it into an invoice once they accept.', 'actions' => $can_edit ? [['label' => 'New Quote', 'modal' => "modals/quote/quote_add.php?$client_url"]] : [], ]; require_once "../includes/filter_footer.php"; ?>