0 $clients_sql" . ($agent_id ? " AND documents.document_created_by = $agent_id" : ''); $current = "documents.document_template_id > 0 AND documents.document_archived_at IS NULL $clients_sql AND documents.document_client_id IN (SELECT client_id FROM clients WHERE client_archived_at IS NULL)"; // Every current template, and archived ones used in the period $templates = []; $sql = mysqli_query($mysqli, "SELECT document_template_id, document_template_name, document_template_archived_at, document_template_required, document_template_review_months FROM document_templates WHERE document_template_archived_at IS NULL OR document_template_id IN (SELECT documents.document_template_id FROM documents WHERE $made AND " . reportRangeSql('documents.document_created_at', $period) . ") ORDER BY document_template_name ASC"); while ($row = mysqli_fetch_assoc($sql)) { $templates[intval($row['document_template_id'])] = ['id' => intval($row['document_template_id']), 'template' => $row['document_template_name'], 'archived' => !empty($row['document_template_archived_at']), 'required' => $row['document_template_archived_at'] ? 0 : intval($row['document_template_required']), 'review_months' => intval($row['document_template_review_months']), 'scope' => '', 'made' => 0, 'prev' => 0, 'documents' => 0, 'clients' => 0, 'last' => null, 'should' => null, 'have' => null, 'coverage' => null, 'due' => 0]; } $required_tags = documentTemplateRequiredTags(array_keys($templates)); // Made in the period, and the period before foreach (['made' => false, 'prev' => true] as $key => $previous) { $sql = mysqli_query($mysqli, "SELECT documents.document_template_id, COUNT(*) FROM documents WHERE $made AND " . reportRangeSql('documents.document_created_at', $period, $previous) . " GROUP BY documents.document_template_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($templates[intval($row[0])])) { $templates[intval($row[0])][$key] = intval($row[1]); } } } // The current documents from each, the clients with one, and when the last was made $sql = mysqli_query($mysqli, "SELECT documents.document_template_id, COUNT(*), COUNT(DISTINCT documents.document_client_id), MAX(documents.document_created_at) FROM documents WHERE $current GROUP BY documents.document_template_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($templates[intval($row[0])])) { $templates[intval($row[0])]['documents'] = intval($row[1]); $templates[intval($row[0])]['clients'] = intval($row[2]); $templates[intval($row[0])]['last'] = $row[3]; } } // Who should have each - the clients this user may see (the client picked) - and which have one $has_one = []; $sql = mysqli_query($mysqli, "SELECT DISTINCT documents.document_template_id, documents.document_client_id FROM documents WHERE $current"); while ($row = mysqli_fetch_row($sql)) { $has_one[intval($row[0])][intval($row[1])] = true; } $should_have = []; // client id => [template id => name] foreach ($templates as $id => &$template) { if (!$template['required']) { continue; } $template['scope'] = documentTemplateScopeText($template['required'], $required_tags[$id]); $template['should'] = $template['have'] = 0; $sql = mysqli_query($mysqli, "SELECT client_id FROM clients WHERE " . documentTemplateScopeSql($template['required'], array_keys($required_tags[$id])) . ($client_id ? " AND client_id = $client_id" : '') . clientScopeSql('clients.client_id')); while ($row = mysqli_fetch_row($sql)) { $template['should']++; $template['have'] += isset($has_one[$id][intval($row[0])]) ? 1 : 0; $should_have[intval($row[0])][$id] = $template['template']; } $template['coverage'] = $template['should'] ? reportShare($template['have'], $template['should'], 1) : null; } unset($template); // The documents due a review, as things are now $due = documentsReviewDue("1 = 1 $clients_sql"); foreach ($due as $row) { if (isset($templates[intval($row['document_template_id'])])) { $templates[intval($row['document_template_id'])]['due']++; } } foreach ($templates as &$row) { $row['change'] = reportChange($row['made'], $row['prev']); } unset($row); uasort($templates, function ($a, $b) { return $b['made'] <=> $a['made'] ?: $b['documents'] <=> $a['documents'] ?: strcasecmp($a['template'], $b['template']); }); $templates = array_values($templates); // By client: the ones each should have, has and is missing, its documents from templates and the ones due $by_client = []; $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)) { $by_client[intval($row['client_id'])] = ['client_id' => intval($row['client_id']), 'client' => $row['client_name'], 'abbr' => (string) $row['client_abbreviation'], 'should' => 0, 'have' => 0, 'missing' => [], 'documents' => 0, 'due' => 0, 'last' => null]; } foreach ($should_have as $id => $should) { if (!isset($by_client[$id])) { continue; } $by_client[$id]['should'] = count($should); foreach ($should as $template_id => $name) { if (isset($has_one[$template_id][$id])) { $by_client[$id]['have']++; } else { $by_client[$id]['missing'][] = $name; } } } $sql = mysqli_query($mysqli, "SELECT documents.document_client_id, COUNT(*), MAX(documents.document_created_at) FROM documents WHERE $current GROUP BY documents.document_client_id"); while ($row = mysqli_fetch_row($sql)) { if (isset($by_client[intval($row[0])])) { $by_client[intval($row[0])]['documents'] = intval($row[1]); $by_client[intval($row[0])]['last'] = $row[2]; } } foreach ($due as $row) { if (isset($by_client[intval($row['document_client_id'])])) { $by_client[intval($row['document_client_id'])]['due']++; } } // Only the clients something applies to - most missing first $by_client = array_values(array_filter($by_client, function ($c) { return $c['should'] || $c['documents'] || $c['due']; })); usort($by_client, function ($a, $b) { return count($b['missing']) <=> count($a['missing']) ?: $b['due'] <=> $a['due'] ?: strcasecmp($a['client'], $b['client']); }); $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' => 'Made', 'type' => 'count', 'class' => 'fw-semibold', 'title' => 'Documents 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') . ''; }], 'documents' => ['label' => 'Documents', 'type' => 'count', 'title' => 'Current documents from it'], 'clients' => ['label' => 'Clients', 'type' => 'count', 'title' => 'Clients with one'], '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 one, 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'] . '
'; }], 'due' => ['label' => 'Due a review', 'type' => 'count', 'html' => function ($row) { return $row['due'] ? '' . $row['due'] . '' : ($row['review_months'] ? '0' : '–'); }], 'last' => ['label' => 'Last made', 'type' => 'date', 'html' => function ($row) { return $row['last'] ? '' . timeAgo($row['last']) . '' : 'Never'; }], ]; $client_columns = [ 'client' => ['label' => 'Client', 'html' => function ($row) { return reportClientCell($row['client_id'], $row['client'], $row['abbr'], 'files.php'); }], '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' : ''); }], 'documents' => ['label' => 'Documents', 'type' => 'count', 'title' => 'Current documents from a template'], 'due' => ['label' => 'Due a review', 'type' => 'count', 'html' => function ($row) { return $row['due'] ? '' . $row['due'] . '' : '0'; }], 'last' => ['label' => 'Last made', 'type' => 'date', 'html' => function ($row) { return $row['last'] ? '' . timeAgo($row['last']) . '' : 'Never'; }], ]; $made_total = array_sum(array_column($templates, 'made')); $previous = array_sum(array_column($templates, 'prev')); $should_total = array_sum(array_map('intval', array_column($templates, 'should'))); $have_total = array_sum(array_map('intval', array_column($templates, 'have'))); $totals = ['template' => 'Total', 'made' => $made_total, 'documents' => array_sum(array_column($templates, 'documents')), 'should' => $should_total, 'have' => $have_total, 'due' => count($due)]; $client_totals = ['client' => 'Total', 'should' => array_sum(array_column($by_client, 'should')), 'have' => array_sum(array_column($by_client, 'have')), 'documents' => array_sum(array_column($by_client, 'documents')), 'due' => array_sum(array_column($by_client, 'due'))]; if (reportExporting('clients')) { reportCsv('Document Templates by Client', $client_columns, $by_client, $client_totals); } if (reportExporting()) { reportCsv('Document Templates ' . $period['start'] . ' to ' . $period['end'], $columns, $templates, $totals); } require_once "includes/inc_report_layout.php"; $live = array_filter($templates, function ($t) { return !$t['archived']; }); $used = count(array_filter($live, function ($t) { return $t['made'] > 0; })); $top = array_slice(array_filter($templates, function ($t) { return $t['made'] > 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']; })); $oldest = $due ? reset($due) : null; ?>
$period, 'years' => reportPastYears("SELECT DISTINCT YEAR(document_created_at) AS y FROM documents WHERE document_template_id > 0"), 'client' => $client_id, 'agent' => $agent_id, 'export' => ['' => 'By template', 'clients' => 'By client'], 'print_meta' => $client_id ? reportClientName($client_id) : '', ]); ?> 'Documents made', 'value' => reportCount($made_total), 'sub' => $previous || $made_total ? reportChangeHtml($made_total, $previous, 'up', 'the period before') : ''], ['label' => 'Templates used', 'value' => $used . ' of ' . count($live), 'sub' => count($live) - $used ? (count($live) - $used) . ' not used in this period' : 'Every one'], ['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'], ['label' => 'Due a review', 'value' => reportCount(count($due)), 'tone' => $due ? 'warning' : '', 'sub' => $oldest ? 'the oldest last looked at ' . timeAgo($oldest['reviewed_at']) : ''], ]) ?>
'bar', 'horizontal' => true, 'labels' => array_column($top, 'template'), 'series' => [['label' => 'Documents made', 'tone' => 'accent', 'data' => array_column($top, 'made')]], 'format' => 'count'], '', 'Documents made from each') ?>
'There are no document templates yet.', 'totals' => $totals]) ?> 'No client has a document from a template, or one it should have.', 'totals' => $client_totals]) ?>