mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Vendor templates stage 1: Admin > Vendor Templates rebuilt on the standard list with Active and Archived tabs, support line, hours and SLA, how many clients have a vendor from each, last used and changed by, sortable and searchable including by support line; new template page with what a vendor starts with, the clients with it and their account numbers, the license templates that use it, made and changed by, and history; shared New/Edit window on the vendor window's tabs with per-field push to the vendors made from it, each logged on the vendor; copy, archive, restore and delete with vendors and license templates unlinked on delete; Make a template from a client's vendor, linking the vendor to it, and the vendor page linking its template for admins; Delete offered to every admin role, not only the built-in Administrator; the admin handler no longer includes the agent vendor handler; database 3.3.4 adds vendor_template_created_by and vendor_template_updated_by, backfilled from the audit log
This commit is contained in:
412
agent/reports/audit_activity.php
Normal file
412
agent/reports/audit_activity.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Audit Activity - what was done in ITFlow in the period, from the audit log:
|
||||
* changes made, records deleted, passwords looked at and data exported, and
|
||||
* for an admin the sign-ins and the ones that failed - against the period
|
||||
* before, over time, and by the hour of the week, so work at odd hours shows.
|
||||
* Then by person, by client and by kind of record, each linking to those
|
||||
* entries in the audit log, and for an admin the security events. Anyone
|
||||
* else sees only the kinds of entry their role covers, for the clients they
|
||||
* can open (auditLogRoleSql(), functions/audit_log.php) - as on a client's
|
||||
* Audit Log. One client, or one agent, when picked.
|
||||
*/
|
||||
|
||||
require_once "includes/inc_report.php";
|
||||
reportEnforce('audit_activity');
|
||||
|
||||
$period = reportPeriod('last_30');
|
||||
$client_id = reportClientId();
|
||||
$agent_id = reportAgentId();
|
||||
$admin = !empty($session_is_admin);
|
||||
|
||||
// The entries counted: those this user may see, for the client and agent picked
|
||||
$scope = auditLogRoleSql() . reportClientSql('log_client_id', $client_id) . ($agent_id ? " AND log_user_id = $agent_id" : '');
|
||||
$in_period = reportRangeSql('log_created_at', $period);
|
||||
$views = auditLogViews();
|
||||
$signins = "log_type IN ('" . implode("', '", SIGN_IN_TYPES) . "')";
|
||||
$parts = [
|
||||
'changes' => $views['changes']['where'],
|
||||
'deletions' => $views['deletions']['where'],
|
||||
'data' => $views['data']['where'],
|
||||
'passwords' => "log_type = 'Credential' AND log_action IN ('View', 'View TOTP')",
|
||||
'exports' => "log_action IN ('Export', 'Download')",
|
||||
'signins' => "$signins AND log_action = 'Success'",
|
||||
'failed' => "$signins AND log_action IN ('" . implode("', '", SIGN_IN_FAILED) . "')",
|
||||
];
|
||||
$sums = function ($alias = '') use ($parts) {
|
||||
$out = [];
|
||||
foreach ($parts as $key => $where) {
|
||||
$out[] = "COALESCE(SUM($where), 0) AS {$alias}$key";
|
||||
}
|
||||
return implode(', ', $out);
|
||||
};
|
||||
|
||||
// Across the period, and the one before
|
||||
$overall = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS entries, COUNT(DISTINCT NULLIF(log_user_id, 0)) AS people, " . $sums() . "
|
||||
FROM logs WHERE $scope AND $in_period"));
|
||||
$before = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS entries, " . $sums() . "
|
||||
FROM logs WHERE $scope AND " . reportRangeSql('log_created_at', $period, true)));
|
||||
$overall = array_map('intval', $overall);
|
||||
$before = array_map('intval', $before);
|
||||
|
||||
// Over time - changes, data out and, for an admin, sign-ins and failures
|
||||
$buckets = reportBuckets($period);
|
||||
$series = array_fill_keys(['changes', 'data', 'signins', 'failed'], array_fill_keys($buckets['keys'], 0));
|
||||
$sql = mysqli_query($mysqli, "SELECT " . reportBucketSql('log_created_at', $buckets['unit']) . " AS b, " . $sums() . "
|
||||
FROM logs WHERE $scope AND $in_period GROUP BY b");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
foreach (array_keys($series) as $key) {
|
||||
if (isset($series[$key][$row['b']])) {
|
||||
$series[$key][$row['b']] = intval($row[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When people were at it - the hour of the week, ITFlow itself left out
|
||||
$heat = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT WEEKDAY(log_created_at) + 1 AS d, HOUR(log_created_at) AS h, COUNT(*) AS n
|
||||
FROM logs WHERE $scope AND $in_period AND log_user_id > 0 GROUP BY d, h");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$heat[intval($row['d'])][intval($row['h'])] = intval($row['n']);
|
||||
}
|
||||
// Out of hours - outside the business days and hours set for SLAs (Admin > SLA), else 9 to 5 on weekdays
|
||||
$sla = getSlaSettings();
|
||||
$work_days = $sla['business_days'] ?: [1, 2, 3, 4, 5];
|
||||
$work_start = preg_match('/^\d{2}:\d{2}(:\d{2})?$/', (string) $sla['business_hours_start']) ? substr($sla['business_hours_start'] . ':00', 0, 8) : '09:00:00';
|
||||
$work_end = preg_match('/^\d{2}:\d{2}(:\d{2})?$/', (string) $sla['business_hours_end']) ? substr($sla['business_hours_end'] . ':00', 0, 8) : '17:00:00';
|
||||
if ($work_end <= $work_start) {
|
||||
[$work_start, $work_end] = ['09:00:00', '17:00:00'];
|
||||
}
|
||||
$out_of_hours = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE $scope AND $in_period AND log_user_id > 0
|
||||
AND (WEEKDAY(log_created_at) + 1 NOT IN (" . implode(',', array_map('intval', $work_days)) . ") OR TIME(log_created_at) < '$work_start' OR TIME(log_created_at) >= '$work_end')"))[0]);
|
||||
$by_people = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE $scope AND $in_period AND log_user_id > 0"))[0]);
|
||||
|
||||
// The audit log, for these entries - an admin's, or the client's when one is picked
|
||||
$period_query = ['canned_date' => 'custom', 'dtf' => $period['start'], 'dtt' => $period['end']];
|
||||
$log_url = function (array $filters) use ($admin, $client_id, $period_query) {
|
||||
if ($admin) {
|
||||
return '/admin/audit_logs.php?' . http_build_query(array_merge($filters, $client_id && !isset($filters['client']) ? ['client' => $client_id] : [], $period_query));
|
||||
}
|
||||
$client = intval($filters['client'] ?? $client_id);
|
||||
if (!$client) {
|
||||
return '';
|
||||
}
|
||||
unset($filters['client']);
|
||||
return '/agent/audit_log.php?' . http_build_query(array_merge(['client_id' => $client], $filters, $period_query));
|
||||
};
|
||||
|
||||
// By person - ITFlow itself last
|
||||
$people = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT log_user_id AS id, user_name, user_avatar, user_type, COUNT(*) AS entries, " . $sums() . ",
|
||||
COUNT(DISTINCT NULLIF(log_ip, '')) AS ips, COUNT(DISTINCT NULLIF(log_client_id, 0)) AS clients, MAX(log_created_at) AS last
|
||||
FROM logs LEFT JOIN users ON user_id = log_user_id AND log_user_id > 0
|
||||
WHERE $scope AND $in_period GROUP BY log_user_id");
|
||||
// People since deleted are one row, -1, before ITFlow itself
|
||||
$gone_ids = [];
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$id = intval($row['id']);
|
||||
if ($id > 0 && $row['user_name'] === null) {
|
||||
$gone_ids[$id] = true;
|
||||
$id = -1;
|
||||
}
|
||||
if (!isset($people[$id])) {
|
||||
$people[$id] = ['id' => $id, 'person' => $id > 0 ? $row['user_name'] : ($id === 0 ? 'ITFlow itself' : 'People since deleted'), 'avatar' => (string) $row['user_avatar'],
|
||||
'kind' => $id === 0 ? 'system' : ($id < 0 ? 'gone' : (intval($row['user_type']) === 2 ? 'portal' : 'agent')), 'entries' => 0, 'prev' => 0,
|
||||
'ips' => 0, 'clients' => 0, 'last' => null, 'count' => 0] + array_fill_keys(array_keys($parts), 0);
|
||||
}
|
||||
$people[$id]['count']++;
|
||||
foreach (array_merge(['entries', 'ips', 'clients'], array_keys($parts)) as $key) {
|
||||
$people[$id][$key] += intval($row[$key]);
|
||||
}
|
||||
$people[$id]['last'] = max((string) $people[$id]['last'], (string) $row['last']);
|
||||
}
|
||||
if (isset($people[-1])) {
|
||||
$people[-1]['person'] = $people[-1]['count'] . ' ' . ($people[-1]['count'] === 1 ? 'person' : 'people') . ' since deleted';
|
||||
}
|
||||
if ($people) {
|
||||
$sql = mysqli_query($mysqli, "SELECT log_user_id AS id, COUNT(*) AS n FROM logs WHERE $scope AND " . reportRangeSql('log_created_at', $period, true) . " GROUP BY log_user_id");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$id = isset($gone_ids[intval($row['id'])]) ? -1 : intval($row['id']);
|
||||
if (isset($people[$id])) {
|
||||
$people[$id]['prev'] += intval($row['n']);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($people as &$row) {
|
||||
$row['change'] = reportChange($row['entries'], $row['prev']);
|
||||
}
|
||||
unset($row);
|
||||
uasort($people, function ($a, $b) {
|
||||
$order = ['agent' => 0, 'portal' => 0, 'gone' => 1, 'system' => 2];
|
||||
return $order[$a['kind']] <=> $order[$b['kind']] ?: $b['entries'] <=> $a['entries'] ?: strcasecmp($a['person'], $b['person']);
|
||||
});
|
||||
$people = array_values($people);
|
||||
|
||||
// By client
|
||||
$clients = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT log_client_id AS id, client_name, client_abbreviation, COUNT(*) AS entries, " . $sums() . ",
|
||||
COUNT(DISTINCT NULLIF(log_user_id, 0)) AS people, MAX(log_created_at) AS last
|
||||
FROM logs LEFT JOIN clients ON client_id = log_client_id
|
||||
WHERE $scope AND $in_period AND log_client_id > 0 GROUP BY log_client_id ORDER BY entries DESC, client_name ASC");
|
||||
// Clients since deleted are one row, last
|
||||
$gone_clients = null;
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$client = ['id' => intval($row['id']), 'client' => (string) $row['client_name'], 'abbr' => (string) $row['client_abbreviation'], 'gone' => $row['client_name'] === null,
|
||||
'entries' => intval($row['entries']), 'changes' => intval($row['changes']), 'deletions' => intval($row['deletions']), 'data' => intval($row['data']),
|
||||
'people' => intval($row['people']), 'last' => $row['last'], 'count' => 1];
|
||||
if (!$client['gone']) {
|
||||
$clients[] = $client;
|
||||
} elseif ($gone_clients === null) {
|
||||
$gone_clients = $client;
|
||||
} else {
|
||||
foreach (['entries', 'changes', 'deletions', 'data', 'people', 'count'] as $key) {
|
||||
$gone_clients[$key] += $client[$key];
|
||||
}
|
||||
$gone_clients['last'] = max($gone_clients['last'], $client['last']);
|
||||
}
|
||||
}
|
||||
if ($gone_clients) {
|
||||
$gone_clients['client'] = $gone_clients['count'] . ' client' . ($gone_clients['count'] === 1 ? '' : 's') . ' since deleted';
|
||||
$clients[] = $gone_clients;
|
||||
}
|
||||
|
||||
// By kind of record
|
||||
$kinds = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT log_type AS type, COUNT(*) AS entries, SUM(log_action IN ('Create', 'Bulk Create', 'Import', 'Upload')) AS created,
|
||||
SUM(log_action IN ('Edit', 'Bulk Edit', 'Update', 'Modify', 'Rename', 'Move', 'Merge')) AS edited, " . $sums() . ",
|
||||
COUNT(DISTINCT NULLIF(log_entity_id, 0)) AS records, MAX(log_created_at) AS last
|
||||
FROM logs WHERE $scope AND $in_period GROUP BY log_type ORDER BY entries DESC, log_type ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$kinds[] = ['type' => (string) $row['type'], 'entries' => intval($row['entries']), 'created' => intval($row['created']), 'edited' => intval($row['edited']),
|
||||
'deletions' => intval($row['deletions']), 'data' => intval($row['data']), 'records' => intval($row['records']), 'last' => $row['last']];
|
||||
}
|
||||
|
||||
// The security events - for an admin
|
||||
$security = [];
|
||||
if ($admin) {
|
||||
$sql = mysqli_query($mysqli, "SELECT log_type AS type, log_action AS action, COUNT(*) AS n, COUNT(DISTINCT NULLIF(log_ip, '')) AS ips, MAX(log_created_at) AS last
|
||||
FROM logs WHERE $scope AND $in_period AND {$views['security']['where']} GROUP BY log_type, log_action ORDER BY n DESC, log_type ASC, log_action ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$security[] = ['type' => (string) $row['type'], 'action' => (string) $row['action'], 'n' => intval($row['n']), 'ips' => intval($row['ips']), 'last' => $row['last']];
|
||||
}
|
||||
}
|
||||
|
||||
$zero = function ($value, $tone = '') {
|
||||
return $value ? '<span class="' . ($tone !== '' ? "fw-semibold text-$tone-emphasis" : '') . '">' . reportCount($value) . '</span>' : '<span class="itflow-report-zero">0</span>';
|
||||
};
|
||||
$last_cell = function ($row) {
|
||||
return $row['last'] ? '<span class="small text-nowrap" title="' . escapeHtml($row['last']) . '">' . escapeHtml(timeAgo($row['last'])) . '</span>' : '';
|
||||
};
|
||||
|
||||
$people_columns = [
|
||||
'person' => ['label' => 'Who', 'html' => function ($row) use ($log_url) {
|
||||
if ($row['kind'] === 'system') {
|
||||
$cell = '<span class="small text-nowrap"><i class="fas fa-fw fa-cog text-body-tertiary me-1"></i>ITFlow itself</span><span class="d-block small text-body-secondary text-nowrap">cron, the API, the email parser</span>';
|
||||
} elseif ($row['kind'] === 'gone') {
|
||||
return '<span class="small text-body-secondary"><i class="fas fa-fw fa-user-slash text-body-tertiary me-1"></i>' . escapeHtml($row['person']) . '</span>';
|
||||
} else {
|
||||
$cell = reportAgentCell($row['id'], $row['person'], $row['avatar'], 'Someone since deleted')
|
||||
. ($row['kind'] === 'portal' ? ' <span class="badge text-bg-light border fw-normal">Portal</span>' : '');
|
||||
}
|
||||
$url = $log_url(['user' => $row['id'] ?: 'system']);
|
||||
return $url !== '' ? '<a class="text-decoration-none" href="' . escapeHtml($url) . '" title="Their entries in the audit log">' . $cell . '</a>' : $cell;
|
||||
}, 'csv' => function ($row) {
|
||||
return $row['person'] . ($row['kind'] === 'portal' ? ' (portal)' : '');
|
||||
}],
|
||||
'entries' => ['label' => 'Entries', 'type' => 'count', 'class' => 'fw-semibold'],
|
||||
'change' => ['label' => 'Change', 'type' => 'percent', 'csv' => false, 'html' => function ($row) {
|
||||
return $row['change'] === null ? '<span class="itflow-report-zero">–</span>' : '<span class="small">' . reportChangeHtml($row['entries'], $row['prev'], 'up') . '</span>';
|
||||
}],
|
||||
'changes' => ['label' => 'Changes', 'type' => 'count', 'title' => 'Records made, changed, archived, restored and deleted'],
|
||||
'deletions' => ['label' => 'Deleted', 'type' => 'count', 'title' => 'Records deleted for good', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['deletions'], 'danger');
|
||||
}],
|
||||
'passwords' => ['label' => 'Passwords seen', 'type' => 'count', 'title' => 'Passwords and 2FA codes looked at', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['passwords'], 'warning');
|
||||
}],
|
||||
'exports' => ['label' => 'Exports', 'type' => 'count', 'title' => 'Exports and downloads', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['exports'], 'warning');
|
||||
}],
|
||||
'clients' => ['label' => 'Clients', 'type' => 'count', 'title' => 'Clients they did something for'],
|
||||
];
|
||||
if ($admin) {
|
||||
$people_columns['signins'] = ['label' => 'Sign-ins', 'type' => 'count'];
|
||||
$people_columns['failed'] = ['label' => 'Failed', 'type' => 'count', 'title' => 'Failed sign-ins against the account - not always them', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['failed'], 'danger');
|
||||
}];
|
||||
$people_columns['ips'] = ['label' => 'IP addresses', 'type' => 'count', 'title' => 'Places it was done from'];
|
||||
}
|
||||
$people_columns['last'] = ['label' => 'Last', 'type' => 'date', 'html' => $last_cell];
|
||||
|
||||
$client_columns = [
|
||||
'client' => ['label' => 'Client', 'html' => function ($row) use ($log_url) {
|
||||
if ($row['gone']) {
|
||||
return '<span class="small text-body-secondary"><i class="fas fa-fw fa-ghost text-body-tertiary me-1"></i>' . escapeHtml($row['client']) . '</span>';
|
||||
}
|
||||
$url = $log_url(['client' => $row['id']]);
|
||||
return reportClientCell($row['id'], $row['client'], $row['abbr'], 'client_overview.php')
|
||||
. ($url !== '' ? ' <a class="small ms-1" href="' . escapeHtml($url) . '" title="Its audit log for the period"><i class="fas fa-history"></i></a>' : '');
|
||||
}],
|
||||
'entries' => ['label' => 'Entries', 'type' => 'count', 'class' => 'fw-semibold'],
|
||||
'changes' => ['label' => 'Changes', 'type' => 'count'],
|
||||
'deletions' => ['label' => 'Deleted', 'type' => 'count', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['deletions'], 'danger');
|
||||
}],
|
||||
'data' => ['label' => 'Data out', 'type' => 'count', 'title' => 'Passwords looked at, exports, downloads and shared links', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['data'], 'warning');
|
||||
}],
|
||||
'people' => ['label' => 'People', 'type' => 'count', 'title' => 'Agents and portal contacts'],
|
||||
'last' => ['label' => 'Last', 'type' => 'date', 'html' => $last_cell],
|
||||
];
|
||||
|
||||
$kind_columns = [
|
||||
'type' => ['label' => 'What', 'html' => function ($row) use ($log_url) {
|
||||
$url = $log_url(['type' => $row['type']]);
|
||||
$label = '<i class="fas fa-fw ' . auditLogTypeIcon($row['type']) . ' text-body-tertiary me-1"></i>' . escapeHtml($row['type']);
|
||||
return $url !== '' ? '<a class="text-body" href="' . escapeHtml($url) . '">' . $label . '</a>' : $label;
|
||||
}],
|
||||
'entries' => ['label' => 'Entries', 'type' => 'count', 'class' => 'fw-semibold'],
|
||||
'records' => ['label' => 'Records', 'type' => 'count', 'title' => 'Different records'],
|
||||
'created' => ['label' => 'Made', 'type' => 'count'],
|
||||
'edited' => ['label' => 'Changed', 'type' => 'count'],
|
||||
'deletions' => ['label' => 'Deleted', 'type' => 'count', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['deletions'], 'danger');
|
||||
}],
|
||||
'data' => ['label' => 'Data out', 'type' => 'count', 'html' => function ($row) use ($zero) {
|
||||
return $zero($row['data'], 'warning');
|
||||
}],
|
||||
'last' => ['label' => 'Last', 'type' => 'date', 'html' => $last_cell],
|
||||
];
|
||||
|
||||
$security_columns = [
|
||||
'what' => ['label' => 'What', 'html' => function ($row) use ($log_url) {
|
||||
$url = $log_url(['view' => 'security', 'type' => $row['type'], 'action' => $row['action']]);
|
||||
return '<a class="text-body text-decoration-none" href="' . escapeHtml($url) . '"><i class="fas fa-fw ' . auditLogTypeIcon($row['type']) . ' text-body-tertiary me-1"></i>' . escapeHtml($row['type']) . '</a> ' . auditLogActionPill($row['action']);
|
||||
}, 'csv' => function ($row) {
|
||||
return $row['type'] . ' ' . $row['action'];
|
||||
}],
|
||||
'n' => ['label' => 'Times', 'type' => 'count', 'class' => 'fw-semibold'],
|
||||
'ips' => ['label' => 'IP addresses', 'type' => 'count'],
|
||||
'last' => ['label' => 'Last', 'type' => 'date', 'html' => $last_cell],
|
||||
];
|
||||
|
||||
$sum = function (array $rows, $key) {
|
||||
return array_sum(array_column($rows, $key));
|
||||
};
|
||||
$people_totals = ['person' => 'Total', 'entries' => $overall['entries']] + array_intersect_key($overall, $parts);
|
||||
$client_totals = ['client' => 'Total', 'entries' => $sum($clients, 'entries'), 'changes' => $sum($clients, 'changes'), 'deletions' => $sum($clients, 'deletions'), 'data' => $sum($clients, 'data')];
|
||||
$kind_totals = ['type' => 'Total', 'entries' => $overall['entries'], 'created' => $sum($kinds, 'created'), 'edited' => $sum($kinds, 'edited'), 'deletions' => $overall['deletions'], 'data' => $overall['data']];
|
||||
|
||||
$name = $period['start'] . ' to ' . $period['end'];
|
||||
if (reportExporting('clients')) {
|
||||
reportCsv("Audit Activity by Client $name", $client_columns, $clients, $client_totals);
|
||||
}
|
||||
if (reportExporting('kinds')) {
|
||||
reportCsv("Audit Activity by Kind $name", $kind_columns, $kinds, $kind_totals);
|
||||
}
|
||||
if ($admin && reportExporting('security')) {
|
||||
reportCsv("Audit Activity Security $name", $security_columns, $security, ['what' => 'Total', 'n' => $sum($security, 'n')]);
|
||||
}
|
||||
if (reportExporting()) {
|
||||
reportCsv("Audit Activity by Person $name", $people_columns, $people, $people_totals);
|
||||
}
|
||||
|
||||
require_once "includes/inc_report_layout.php";
|
||||
|
||||
// Years with entries, back to the oldest kept
|
||||
$past_years = [];
|
||||
$oldest = mysqli_fetch_row(mysqli_query($mysqli, "SELECT YEAR(MIN(log_created_at)) FROM logs"))[0] ?? null;
|
||||
for ($y = intval(date('Y')) - 2; $oldest && $y >= intval($oldest); $y--) {
|
||||
$past_years[] = $y;
|
||||
}
|
||||
$retention = appLogRetention();
|
||||
|
||||
$exports = ['' => 'By person', 'clients' => 'By client', 'kinds' => 'By kind'] + ($admin ? ['security' => 'Security events'] : []);
|
||||
$stats = [
|
||||
['label' => 'Changes', 'value' => reportCount($overall['changes']),
|
||||
'sub' => ($before['changes'] || $overall['changes'] ? reportChangeHtml($overall['changes'], $before['changes'], 'up', 'the period before') . ' · ' : '')
|
||||
. $overall['people'] . ' <span class="text-body-tertiary">' . ($overall['people'] === 1 ? 'person' : 'people') . '</span>'],
|
||||
['label' => 'Deleted', 'value' => reportCount($overall['deletions']), 'tone' => $overall['deletions'] ? 'danger' : '',
|
||||
'sub' => $before['deletions'] || $overall['deletions'] ? reportChangeHtml($overall['deletions'], $before['deletions'], 'down', 'the period before') : '<span class="text-body-tertiary">Nothing deleted</span>'],
|
||||
['label' => 'Data out', 'value' => reportCount($overall['data']), 'tone' => $overall['data'] ? 'warning' : '',
|
||||
'sub' => reportCount($overall['passwords']) . ' <span class="text-body-tertiary">password' . ($overall['passwords'] === 1 ? '' : 's') . ' seen</span> · '
|
||||
. reportCount($overall['exports']) . ' <span class="text-body-tertiary">export' . ($overall['exports'] === 1 ? '' : 's') . '</span>'],
|
||||
];
|
||||
if ($admin) {
|
||||
$stats[] = ['label' => 'Sign-ins', 'value' => reportCount($overall['signins']), 'tone' => $overall['failed'] > $overall['signins'] ? 'danger' : '',
|
||||
'sub' => $overall['failed'] ? '<span class="text-danger-emphasis">' . reportCount($overall['failed']) . ' failed</span>' : '<span class="text-body-tertiary">None failed</span>'];
|
||||
} else {
|
||||
$stats[] = ['label' => 'Out of hours', 'value' => $by_people ? reportPercent(reportShare($out_of_hours, $by_people)) : '-',
|
||||
'sub' => reportCount($out_of_hours) . ' <span class="text-body-tertiary">of ' . reportCount($by_people) . ' entries</span>'];
|
||||
}
|
||||
|
||||
$chart_series = [
|
||||
['label' => 'Changes', 'tone' => 'accent', 'data' => array_values($series['changes']), 'stack' => 'a'],
|
||||
['label' => 'Data out', 'tone' => 'warning', 'data' => array_values($series['data']), 'stack' => 'a'],
|
||||
];
|
||||
if ($admin) {
|
||||
$chart_series[] = ['label' => 'Sign-ins', 'tone' => 'info', 'data' => array_values($series['signins']), 'stack' => 'a'];
|
||||
$chart_series[] = ['label' => 'Failed sign-ins', 'tone' => 'danger', 'data' => array_values($series['failed']), 'stack' => 'a'];
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="itflow-report">
|
||||
|
||||
<?php reportHeader('audit_activity', [
|
||||
'period' => $period,
|
||||
'years' => $past_years,
|
||||
'client' => $client_id,
|
||||
'agent' => $agent_id,
|
||||
'export' => $exports,
|
||||
'print_meta' => implode(' · ', array_filter([$client_id ? reportClientName($client_id) : '', $agent_id ? getFieldById('users', $agent_id, 'user_name') : ''])),
|
||||
'note' => escapeHtml(reportFind('audit_activity')['description']) . ($admin ? '' : ' You see the kinds of entry your role covers.')
|
||||
. ($retention['audit'] ? ' The audit log keeps ' . escapeHtml(strtolower(appLogRetentionText($retention['audit']))) . ', so a period before that is empty.' : ''),
|
||||
]); ?>
|
||||
|
||||
<?= reportStats($stats) ?>
|
||||
|
||||
<?php if (!$overall['entries']) { ?>
|
||||
<div class="card"><div class="card-body"><?= reportEmpty('Nothing was done in this period that you can see.', 'fa-history', 'secondary') ?></div></div>
|
||||
<?php } else { ?>
|
||||
|
||||
<?= reportSectionOpen('Over time', escapeHtml($period['range'])) ?>
|
||||
<div class="card-body">
|
||||
<?= reportChart(['type' => 'bar', 'labels' => $buckets['labels'], 'series' => $chart_series, 'stacked' => true, 'format' => 'count'], '', 'Entries by ' . $buckets['unit']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= reportSectionOpen('When', reportCount($out_of_hours) . ' of ' . reportCount($by_people) . ' entries by people out of hours - before ' . escapeHtml(date('g:ia', strtotime($work_start))) . ', after ' . escapeHtml(date('g:ia', strtotime($work_end))) . ' or on a day off') ?>
|
||||
<div class="card-body">
|
||||
<?= reportHeatmap($heat, 'entry') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= reportSectionOpen('By person', 'Most first, then ITFlow itself') ?>
|
||||
<?= reportTable($people_columns, $people, ['empty' => 'Nobody did anything in this period.', 'totals' => $people_totals]) ?>
|
||||
<div class="card-footer small text-body-secondary">
|
||||
Changes are records made, changed, archived, restored and deleted. Failed sign-ins are against the account, so they may be someone else trying it. Each person opens their entries in the audit log<?= $admin ? '' : ' when a client is picked' ?>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($security) { ?>
|
||||
<?= reportSectionOpen('Security events', 'Failed and turned away sign-ins, alerts, access denied, and changes to users, roles, keys and settings') ?>
|
||||
<?= reportTable($security_columns, $security, ['empty' => '']) ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?= reportSectionOpen('By client', 'Most first', 'itflow-report-long') ?>
|
||||
<?= reportTable($client_columns, $clients, ['empty' => 'Nothing was done for a client in this period.', 'totals' => $client_totals]) ?>
|
||||
</div>
|
||||
|
||||
<?= reportSectionOpen('By kind', 'What it was done to', 'itflow-report-long') ?>
|
||||
<?= reportTable($kind_columns, $kinds, ['empty' => 'Nothing in this period.', 'totals' => $kind_totals]) ?>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once "../../includes/footer.php"; ?>
|
||||
<script src="/js/reports.js"></script>
|
||||
@@ -249,7 +249,13 @@ if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($clie
|
||||
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
|
||||
<?php if ($vendor_description) { ?><span><?= $vendor_description ?></span><?php } ?>
|
||||
<?php if ($vendor_hours) { ?><span><i class="fas fa-fw fa-clock me-1"></i><?= $vendor_hours ?></span><?php } ?>
|
||||
<?php if ($vendor['vendor_template_name']) { ?><span title="Made from this template"><i class="fas fa-fw fa-puzzle-piece me-1"></i><?= escapeHtml($vendor['vendor_template_name']) ?></span><?php } ?>
|
||||
<?php if ($vendor['vendor_template_name']) { ?>
|
||||
<?php if (!empty($session_is_admin)) { ?>
|
||||
<a class="text-reset" href="/admin/vendor_template.php?vendor_template_id=<?= intval($vendor['vendor_template_id']) ?>" title="Made from this template - open it" data-vendor-template-link><i class="fas fa-fw fa-puzzle-piece me-1"></i><?= escapeHtml($vendor['vendor_template_name']) ?></a>
|
||||
<?php } else { ?>
|
||||
<span title="Made from this template"><i class="fas fa-fw fa-puzzle-piece me-1"></i><?= escapeHtml($vendor['vendor_template_name']) ?></span>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($vendor_tags_display) { ?>
|
||||
<div class="mt-2"><?= $vendor_tags_display ?></div>
|
||||
@@ -316,6 +322,12 @@ if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($clie
|
||||
<i class="fas fa-fw fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<?php if (!empty($session_is_admin) && !$vendor['vendor_template_name'] && !$vendor_archived_at) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="/admin/modals/vendor_template/vendor_template_add.php?from_vendor=<?= $vendor_id ?>" title="A vendor template starting from this vendor, so other clients can have it">
|
||||
<i class="fas fa-fw fa-puzzle-piece text-secondary me-2"></i>Make a template from it
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
<?php if ($vendor_archived_at) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
|
||||
|
||||
Reference in New Issue
Block a user