= 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 '
' . '' . $row['have'] . ' of ' . $row['should'] . '
'; }], ]; if ($sees_docs) { $columns['licenses'] = ['label' => 'Licenses', 'type' => 'count', 'title' => 'Current licenses from their vendors']; $columns['assets'] = ['label' => 'Assets', 'type' => 'count', 'title' => 'Current assets bought from their vendors']; } if ($sees_cases) { $columns['cases'] = ['label' => 'Cases', 'type' => 'count', 'title' => 'Tickets with their vendors on them, opened in the period']; $columns['open'] = ['label' => 'Open cases', 'type' => 'count', 'title' => 'Tickets with their vendors on them, open now', 'html' => function ($row) use ($zero) { return $zero($row['open'], 'warning'); }]; } if ($sees_spend) { $columns['spend'] = ['label' => 'Spent', 'type' => 'money', 'title' => 'Expenses with our own vendor from it in the period', 'html' => function ($row) { return $row['spend'] === null ? '–' : ($row['spend'] ? reportMoney($row['spend']) : '' . reportMoney(0) . ''); }]; } $client_columns = [ 'client' => ['label' => 'Client', 'html' => function ($row) { return reportClientCell($row['client_id'], $row['client'], $row['abbr'], 'vendors.php'); }], 'vendors' => ['label' => 'Vendors', 'type' => 'count', 'title' => 'Current vendors made from a template'], 'should' => ['label' => 'Should have', 'type' => 'count'], 'have' => ['label' => 'Have', 'type' => 'count'], 'missing' => ['label' => 'Missing', 'sort' => false, 'class' => 'itflow-report-text', 'csv' => function ($row) { return implode(', ', $row['missing']); }, 'html' => function ($row) { return $row['missing'] ? '' . escapeHtml(implode(', ', $row['missing'])) . '' : ($row['should'] ? 'None' : ''); }], 'out' => ['label' => 'Out of step', 'type' => 'count', 'title' => 'Its vendors that say something other than their template', 'html' => function ($row) use ($zero) { return $zero($row['out'], 'warning'); }], 'no_account' => ['label' => 'No account number', 'type' => 'count', 'html' => function ($row) use ($zero) { return $zero($row['no_account'], 'warning'); }], ]; if ($sees_cases) { $client_columns['open'] = ['label' => 'Open cases', 'type' => 'count', 'title' => 'Its tickets open now with a vendor from a template on them']; } $sum = function ($key) use ($templates) { return array_sum(array_map('floatval', array_column($templates, $key))); }; $made_total = intval($sum('made')); $previous = intval($sum('prev')); $vendor_total = intval($sum('vendors')); $out_total = intval($sum('out')); $should_total = intval($sum('should')); $have_total = intval($sum('have')); $totals = ['template' => 'Total', 'made' => $made_total, 'vendors' => $vendor_total, 'out' => $out_total, 'no_account' => intval($sum('no_account')), 'should' => $should_total, 'have' => $have_total, 'clients' => count(array_filter($by_client, function ($c) { return $c['vendors'] > 0; })), 'licenses' => intval($sum('licenses')), 'assets' => intval($sum('assets')), 'cases' => intval($sum('cases')), 'open' => intval($sum('open'))]; if ($sees_spend) { $totals['spend'] = round($sum('spend'), 2); } $client_totals = ['client' => 'Total', 'vendors' => array_sum(array_column($by_client, 'vendors')), 'should' => array_sum(array_column($by_client, 'should')), 'have' => array_sum(array_column($by_client, 'have')), 'out' => array_sum(array_column($by_client, 'out')), 'no_account' => array_sum(array_column($by_client, 'no_account')), 'open' => array_sum(array_column($by_client, 'open'))]; if (reportExporting('clients')) { reportCsv('Vendor Templates by Client', $client_columns, $by_client, $client_totals); } if (reportExporting()) { reportCsv('Vendor Templates ' . $period['start'] . ' to ' . $period['end'], $columns, $templates, $totals); } require_once "includes/inc_report_layout.php"; $top = array_slice(array_filter($templates, function ($t) { return $t['clients'] > 0; }), 0, 12); $fully = count(array_filter($by_client, function ($c) { return $c['should'] && !$c['missing']; })); $with_required = count(array_filter($by_client, function ($c) { return $c['should']; })); $stats = [ ['label' => 'Vendors added', 'value' => reportCount($made_total), 'sub' => $previous || $made_total ? reportChangeHtml($made_total, $previous, 'up', 'the period before') : 'from a template'], ['label' => 'In step', 'value_html' => $vendor_total ? reportRateHtml(reportShare($vendor_total - $out_total, $vendor_total, 1), 95, 80) : '-', 'sub' => $vendor_total ? ($out_total ? '' . reportCount($out_total) . ' out of step of ' . reportCount($vendor_total) . ' vendors' : 'Every one of the ' . reportCount($vendor_total) . ' vendors') : 'No vendors from a template yet'], ['label' => 'Have what they should', 'value_html' => $should_total ? reportRateHtml(reportShare($have_total, $should_total, 1), 100, 75) : '-', 'sub' => $should_total ? $fully . ' of ' . $with_required . ' client' . ($with_required === 1 ? '' : 's') . ' have every one' : 'No template is required of anyone'], ]; if ($sees_spend) { $stats[] = ['label' => 'Spent with them', 'value' => reportMoney($totals['spend']), 'sub' => 'our own vendors from a template']; } elseif ($sees_cases) { $stats[] = ['label' => 'Open cases', 'value' => reportCount($totals['open']), 'tone' => $totals['open'] ? 'warning' : '', 'sub' => reportCount($totals['cases']) . ' opened in the period']; } ?>
$period, 'years' => reportPastYears("SELECT DISTINCT YEAR(vendor_created_at) AS y FROM vendors WHERE vendor_template_id > 0"), 'client' => $client_id, 'export' => ['' => 'By template', 'clients' => 'By client'], 'print_meta' => $client_id ? reportClientName($client_id) : '', ]); ?>
'bar', 'horizontal' => true, 'labels' => array_column($top, 'template'), 'series' => [['label' => 'In step', 'tone' => 'accent', 'data' => array_column($top, 'in_step')], ['label' => 'Out of step', 'tone' => 'warning', 'data' => array_column($top, 'out')]], 'stacked' => true, 'format' => 'count'], '', 'Vendors from each, in step and out of step') ?>
'There are no vendor templates yet.', 'totals' => $totals]) ?> 'No client has a vendor from a template, or one it should have.', 'totals' => $client_totals]) ?>