= 1; $sees_cases = reportFeatureOn('ticketing') && lookupUserPermission('module_support') >= 1; // What we spend with a vendor is the company's, not a client's - left out when one client is picked $sees_spend = reportFeatureOn('accounting') && lookupUserPermission('module_financial') >= 1 && !$client_id; // The current clients this user may see (the client picked) $client_rows = []; $sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL" . ($client_id ? " AND client_id = $client_id" : '') . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC"); while ($row = mysqli_fetch_assoc($sql)) { $client_rows[intval($row['client_id'])] = $row; } $client_ids = array_keys($client_rows) ?: [0]; $in_clients = function ($column) use ($client_ids) { return "$column IN (" . implode(',', $client_ids) . ")"; }; // Every current template, and archived ones a vendor was made from in the period $made = "vendors.vendor_template_id > 0 AND " . $in_clients('vendors.vendor_client_id'); $templates = []; $template_rows = []; $sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_archived_at IS NULL OR vendor_template_id IN (SELECT vendors.vendor_template_id FROM vendors WHERE $made AND " . reportRangeSql('vendors.vendor_created_at', $period) . ") ORDER BY vendor_template_name ASC"); while ($row = mysqli_fetch_assoc($sql)) { $id = intval($row['vendor_template_id']); $template_rows[$id] = $row; $templates[$id] = ['id' => $id, 'template' => $row['vendor_template_name'], 'archived' => !empty($row['vendor_template_archived_at']), 'required' => $row['vendor_template_archived_at'] ? 0 : intval($row['vendor_template_required']), 'scope' => '', 'made' => 0, 'prev' => 0, 'clients' => 0, 'vendors' => 0, 'in_step' => 0, 'out' => 0, 'no_account' => 0, 'ours' => false, 'should' => null, 'have' => null, 'coverage' => null, 'licenses' => 0, 'assets' => 0, 'cases' => 0, 'open' => 0, 'spend' => null]; } $required_tags = vendorTemplateRequiredTags(array_keys($templates)); // Made in the period, and the period before foreach (['made' => false, 'prev' => true] as $key => $previous) { $sql = mysqli_query($mysqli, "SELECT vendors.vendor_template_id, COUNT(*) FROM vendors WHERE $made AND " . reportRangeSql('vendors.vendor_created_at', $period, $previous) . " GROUP BY vendors.vendor_template_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($templates[intval($row[0])])) { $templates[intval($row[0])][$key] = intval($row[1]); } } } // Each one's current vendors, for the clients counted - in step with it or not, with an account number or not $by_client = []; foreach ($client_rows as $id => $row) { $by_client[$id] = ['client_id' => $id, 'client' => $row['client_name'], 'abbr' => (string) $row['client_abbreviation'], 'vendors' => 0, 'should' => 0, 'have' => 0, 'missing' => [], 'out' => 0, 'no_account' => 0, 'open' => 0]; } $has_one = []; $clients_of = []; $sql = mysqli_query($mysqli, "SELECT vendors.* FROM vendors WHERE vendors.vendor_template_id > 0 AND vendor_archived_at IS NULL AND " . $in_clients('vendor_client_id')); while ($vendor = mysqli_fetch_assoc($sql)) { $id = intval($vendor['vendor_template_id']); $cid = intval($vendor['vendor_client_id']); if (!isset($templates[$id])) { continue; } $out = (bool) vendorTemplateDifferences($template_rows[$id], $vendor); $no_account = trim((string) $vendor['vendor_account_number']) === ''; $templates[$id]['vendors']++; $templates[$id][$out ? 'out' : 'in_step']++; $templates[$id]['no_account'] += $no_account ? 1 : 0; $clients_of[$id][$cid] = true; $has_one[$id][$cid] = true; $by_client[$cid]['vendors']++; $by_client[$cid]['out'] += $out ? 1 : 0; $by_client[$cid]['no_account'] += $no_account ? 1 : 0; } foreach ($templates as $id => &$template) { $template['clients'] = count($clients_of[$id] ?? []); } unset($template); // Our own vendor from each, when every client is counted if (!$client_id) { $sql = mysqli_query($mysqli, "SELECT DISTINCT vendor_template_id FROM vendors WHERE vendor_template_id > 0 AND vendor_client_id = 0 AND vendor_archived_at IS NULL"); while ($row = mysqli_fetch_row($sql)) { if (isset($templates[intval($row[0])])) { $templates[intval($row[0])]['ours'] = true; } } } // What names their vendors - current licenses and assets, cases logged in the period and the ones open now $from_template = function ($table, $vendor_column, $client_column, $where) use ($in_clients) { global $mysqli; $counts = []; $sql = mysqli_query($mysqli, "SELECT vendors.vendor_template_id, COUNT(*) FROM $table JOIN vendors ON vendors.vendor_id = $table.$vendor_column WHERE vendors.vendor_template_id > 0 AND $where AND " . $in_clients("$table.$client_column") . " GROUP BY vendors.vendor_template_id"); while ($row = mysqli_fetch_row($sql)) { $counts[intval($row[0])] = intval($row[1]); } return $counts; }; if ($sees_docs) { foreach ($from_template('software', 'software_vendor_id', 'software_client_id', 'software_archived_at IS NULL') as $id => $count) { if (isset($templates[$id])) { $templates[$id]['licenses'] = $count; } } foreach ($from_template('assets', 'asset_vendor_id', 'asset_client_id', 'asset_archived_at IS NULL') as $id => $count) { if (isset($templates[$id])) { $templates[$id]['assets'] = $count; } } } if ($sees_cases) { foreach (['cases' => reportRangeSql('tickets.ticket_created_at', $period), 'open' => 'tickets.ticket_resolved_at IS NULL AND tickets.ticket_closed_at IS NULL'] as $key => $where) { foreach ($from_template('tickets', 'ticket_vendor_id', 'ticket_client_id', "tickets.ticket_archived_at IS NULL AND $where") as $id => $count) { if (isset($templates[$id])) { $templates[$id][$key] = $count; } } } $sql = mysqli_query($mysqli, "SELECT tickets.ticket_client_id, COUNT(*) FROM tickets JOIN vendors ON vendors.vendor_id = tickets.ticket_vendor_id WHERE vendors.vendor_template_id > 0 AND tickets.ticket_archived_at IS NULL AND tickets.ticket_resolved_at IS NULL AND tickets.ticket_closed_at IS NULL AND " . $in_clients('tickets.ticket_client_id') . " GROUP BY tickets.ticket_client_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($by_client[intval($row[0])])) { $by_client[intval($row[0])]['open'] = intval($row[1]); } } } // What we spent with our own vendor from each in the period - as Spend by Vendor counts it if ($sees_spend) { foreach ($templates as $id => &$template) { $template['spend'] = $template['ours'] ? 0.0 : null; } unset($template); $sql = mysqli_query($mysqli, "SELECT vendors.vendor_template_id, SUM(expense_amount) FROM expenses JOIN vendors ON vendors.vendor_id = expense_vendor_id WHERE vendors.vendor_template_id > 0 AND vendors.vendor_client_id = 0 AND " . reportRangeSql('expense_date', $period) . " GROUP BY vendors.vendor_template_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($templates[intval($row[0])])) { $templates[intval($row[0])]['spend'] = round(floatval($row[1]), 2); } } } // Who should have each - and which have one foreach ($templates as $id => &$template) { if (!$template['required']) { continue; } $template['scope'] = vendorTemplateScopeText($template['required'], $required_tags[$id]); $template['should'] = $template['have'] = 0; $sql = mysqli_query($mysqli, "SELECT client_id FROM clients WHERE " . vendorTemplateScopeSql($template['required'], array_keys($required_tags[$id])) . ($client_id ? " AND client_id = $client_id" : '') . clientScopeSql('clients.client_id')); while ($row = mysqli_fetch_row($sql)) { $cid = intval($row[0]); $template['should']++; if (isset($has_one[$id][$cid])) { $template['have']++; $by_client[$cid]['have']++; } else { $by_client[$cid]['missing'][] = $template['template']; } $by_client[$cid]['should']++; } $template['coverage'] = $template['should'] ? reportShare($template['have'], $template['should'], 1) : null; } unset($template); foreach ($templates as &$row) { $row['change'] = reportChange($row['made'], $row['prev']); } unset($row); uasort($templates, function ($a, $b) { return $b['clients'] <=> $a['clients'] ?: $b['made'] <=> $a['made'] ?: strcasecmp($a['template'], $b['template']); }); $templates = array_values($templates); // Only the clients something applies to - most missing first $by_client = array_values(array_filter($by_client, function ($c) { return $c['should'] || $c['vendors']; })); usort($by_client, function ($a, $b) { return count($b['missing']) <=> count($a['missing']) ?: $b['out'] <=> $a['out'] ?: $b['no_account'] <=> $a['no_account'] ?: strcasecmp($a['client'], $b['client']); }); $zero = function ($value, $tone = '') { return $value ? '' . reportCount($value) . '' : '0'; }; $columns = [ 'template' => ['label' => 'Template', 'html' => function ($row) use ($admin) { $name = escapeHtml($row['template']); return ($admin ? '' . $name . '' : $name) . ($row['archived'] ? ' Archived' : ''); }], 'scope' => ['label' => 'Required of', 'html' => function ($row) { return $row['scope'] !== '' ? '' . escapeHtml($row['scope']) . '' : '–'; }], 'made' => ['label' => 'Added', 'type' => 'count', 'title' => 'Client vendors made from it in the period'], 'change' => ['label' => 'Change', 'type' => 'percent', 'csv' => false, 'html' => function ($row) { return $row['change'] === null ? '–' : '' . reportChangeHtml($row['made'], $row['prev'], 'up') . ''; }], 'clients' => ['label' => 'Clients', 'type' => 'count', 'class' => 'fw-semibold', 'title' => 'Clients with a current vendor from it'], 'vendors' => ['label' => 'Vendors', 'type' => 'count', 'screen' => false], 'out' => ['label' => 'Out of step', 'type' => 'count', 'title' => 'Their vendors that say something other than the template does', 'html' => function ($row) use ($admin, $zero) { if ($row['out'] && $admin) { return '' . reportCount($row['out']) . ''; } return $zero($row['out'], 'warning'); }], 'no_account' => ['label' => 'No account number', 'type' => 'count', 'title' => "Their vendors without the client's account number", 'html' => function ($row) use ($zero) { return $zero($row['no_account'], 'warning'); }], 'should' => ['label' => 'Should have one', 'type' => 'count', 'screen' => false], 'have' => ['label' => 'Have one', 'type' => 'count', 'screen' => false], 'coverage' => ['label' => 'Coverage', 'type' => 'percent', 'title' => 'Of the clients that should have it, how many do', 'html' => function ($row) { if ($row['coverage'] === null) { return '–'; } $tone = $row['coverage'] >= 100 ? 'success' : ($row['coverage'] >= 75 ? 'warning' : 'danger'); return '