mirror of
https://github.com/itflow-org/itflow
synced 2026-11-13 07:57:25 +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:
@@ -1,177 +1,222 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Admin > App Logs - what ITFlow itself does and runs into: cron runs, mail
|
||||
* going out and coming in, payment providers, the API, errors. Listed by
|
||||
* level - errors, warnings, info and debug, each counted for the filters
|
||||
* picked - searched, and filtered by category and date. Each entry opens a
|
||||
* quick look with all of it and what happened around then
|
||||
* (modals/app_log/app_log.php). What is shown can be exported
|
||||
* (export_app_logs) or cleared (clear_app_logs). The work is in
|
||||
* functions/app_log.php.
|
||||
*/
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "app_log_id";
|
||||
$order = "DESC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// Log Type Filter
|
||||
if (isset($_GET['type']) & !empty($_GET['type'])) {
|
||||
$log_type_query = "AND (app_log_type = '" . escapeSql($_GET['type']) . "')";
|
||||
$type_filter = escapeHtml($_GET['type']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_type_query = '';
|
||||
$type_filter = '';
|
||||
// Tab title - the file is named for the table, the page for what it holds
|
||||
$page_title = 'App Logs';
|
||||
|
||||
// Only these sort - an unknown column would take the page down
|
||||
$sortable_columns = [
|
||||
'app_log_id' => "app_log_id $order",
|
||||
'app_log_created_at' => "app_log_created_at $order, app_log_id $order",
|
||||
'app_log_type' => "FIELD(app_log_type, 'error', 'warning', 'info', 'debug') " . ($order === 'ASC' ? 'DESC' : 'ASC') . ", app_log_id DESC",
|
||||
'app_log_category' => "app_log_category IS NULL, app_log_category $order, app_log_id DESC",
|
||||
];
|
||||
if (!isset($sortable_columns[$sort])) {
|
||||
$sort = 'app_log_id';
|
||||
}
|
||||
|
||||
// Log Category Filter
|
||||
if (isset($_GET['category']) & !empty($_GET['catergory'])) {
|
||||
$log_category_query = "AND (app_log_category = '" . escapeSql($_GET['category']) . "')";
|
||||
$category_filter = escapeHtml($_GET['category']);
|
||||
} else {
|
||||
// Default - any
|
||||
$log_category_query = '';
|
||||
$category_filter = '';
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS app_log_category, app_log_created_at, app_log_details, app_log_id, app_log_type FROM app_logs
|
||||
WHERE (app_log_type LIKE '%$q%' OR app_log_category LIKE '%$q%' OR app_log_details LIKE '%$q%')
|
||||
AND DATE(app_log_created_at) BETWEEN '$dtf' AND '$dtt'
|
||||
$log_type_query
|
||||
$log_category_query
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
$views = appLogViews();
|
||||
$view = isset($_GET['view']) && is_string($_GET['view']) && isset($views[$_GET['view']]) ? $_GET['view'] : '';
|
||||
$category_filter = is_string($_GET['category'] ?? null) ? trim($_GET['category']) : '';
|
||||
$filter_src = array_merge($_GET, ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view, 'category' => $category_filter]);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS app_log_id, app_log_category, app_log_type, app_log_details, app_log_created_at
|
||||
FROM app_logs
|
||||
WHERE " . appLogFiltersSql($filter_src) . "
|
||||
ORDER BY {$sortable_columns[$sort]}
|
||||
LIMIT $record_from, $record_to");
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
// Each level counted for the rest of the filters
|
||||
$count_sql = [];
|
||||
foreach ($views as $definition) {
|
||||
$count_sql[] = 'COALESCE(SUM(' . ($definition['where'] === '' ? '1' : $definition['where']) . '), 0)';
|
||||
}
|
||||
$counts = array_combine(array_keys($views), array_map('intval', mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . "
|
||||
FROM app_logs WHERE " . appLogFiltersSql(array_merge($filter_src, ['view' => '']))))));
|
||||
// Nothing logs at debug as it comes - its view shows once something does
|
||||
if (!$counts['debug'] && $view !== 'debug') {
|
||||
unset($views['debug']);
|
||||
}
|
||||
|
||||
$categories = appLogCategories();
|
||||
$any_filter = $q !== '' || $category_filter !== '' || $dtf !== '1970-01-01' || $dtt !== '2099-12-31';
|
||||
$retention = appLogRetention();
|
||||
|
||||
$sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) {
|
||||
return '<a class="text-reset text-decoration-none" href="?' . escapeHtml($url_query_strings_sort) . '&sort=' . $column . '&order=' . $disp . '">'
|
||||
. $label . ($sort === $column ? " $order_icon" : '') . '</a>';
|
||||
};
|
||||
// A view keeps the filters, and starts from its first page
|
||||
$view_url = function ($key) {
|
||||
$query = $_GET;
|
||||
unset($query['page'], $query['open']);
|
||||
$query['view'] = $key;
|
||||
return '?' . http_build_query(array_filter($query, function ($value) {
|
||||
return $value !== '' && $value !== null && $value !== [];
|
||||
}));
|
||||
};
|
||||
// The page with just this category - from a category in the list
|
||||
$category_url = function ($category) {
|
||||
$query = $_GET;
|
||||
unset($query['page'], $query['open']);
|
||||
$query['category'] = $category;
|
||||
return '?' . http_build_query($query);
|
||||
};
|
||||
$filters_url = function ($modal) use ($dtf, $dtt, $view, $category_filter) {
|
||||
return buildExportModalUrl($modal, ['q'], ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view, 'category' => $category_filter]);
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-history me-2"></i>App Logs</h3>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-sm-4">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search app logs">
|
||||
<button class="btn btn-secondary" title="More filters" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div>
|
||||
<select class="form-select select2" name="type" onchange="this.form.submit()">
|
||||
<option value="">- All Types -</option>
|
||||
|
||||
<?php
|
||||
$sql_types_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_type FROM app_logs ORDER BY app_log_type ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_types_filter)) {
|
||||
$log_type = escapeHtml($row['app_log_type']);
|
||||
?>
|
||||
<option <?php if ($type_filter == $log_type) { echo "selected"; } ?>><?= $log_type ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div>
|
||||
<select class="form-select select2" name="category" onchange="this.form.submit()">
|
||||
<option value="">- All Categories -</option>
|
||||
|
||||
<?php
|
||||
$sql_categories_filter = mysqli_query($mysqli, "SELECT DISTINCT app_log_category FROM app_logs ORDER BY app_log_category ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_categories_filter)) {
|
||||
$log_category = escapeHtml($row['app_log_category']);
|
||||
?>
|
||||
<option <?php if ($category_filter == $log_category) { echo "selected"; } ?>><?= $log_category ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-clipboard-list me-2"></i>App Logs</h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="<?= $filters_url('modals/app_log/app_log_export.php') ?>">
|
||||
<i class="fas fa-fw fa-download"></i><span class="d-none d-lg-inline ms-2">Export</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More"></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item text-danger ajax-modal" href="#" data-modal-url="<?= $filters_url('modals/app_log/app_log_clear.php') ?>" data-app-log-clear>
|
||||
<i class="fas fa-fw fa-trash-alt me-2"></i>Clear
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="settings_logs.php">
|
||||
<i class="fas fa-fw fa-cog text-secondary me-2"></i>Keeping and forwarding
|
||||
</a>
|
||||
<a class="dropdown-item" href="audit_logs.php">
|
||||
<i class="fas fa-fw fa-history text-secondary me-2"></i>Audit Logs
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo "show"; } ?>" id="advancedFilter">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div>
|
||||
<label class="form-label">Date range</label>
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_created_at&order=<?= $disp ?>">
|
||||
Timestamp <?php if ($sort == 'app_log_created_at') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_type&order=<?= $disp ?>">
|
||||
Type <?php if ($sort == 'app_log_type') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_category&order=<?= $disp ?>">
|
||||
Category <?php if ($sort == 'app_log_category') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=app_log_details&order=<?= $disp ?>">
|
||||
Details <?php if ($sort == 'app_log_details') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$log_id = intval($row['app_log_id']);
|
||||
$log_type = escapeHtml($row['app_log_type']);
|
||||
$log_category = escapeHtml($row['app_log_category']);
|
||||
$log_details = escapeHtml($row['app_log_details']);
|
||||
$log_created_at = escapeHtml($row['app_log_created_at']);
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td class="font-monospace ps-3"><?= $log_created_at ?></td>
|
||||
<td><?= $log_type ?></td>
|
||||
<td><?= $log_category ?></td>
|
||||
<td><?= $log_details ?></td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
$empty_state = [
|
||||
'thing' => 'app logs',
|
||||
'icon' => 'fa-history',
|
||||
'text' => 'What ITFlow itself does - cron runs, mail, errors - shows here.',
|
||||
];
|
||||
?>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($view !== '') { ?><input type="hidden" name="view" value="<?= escapeHtml($view) ?>"><?php } ?>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-lg-5 col-md-12">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search the app logs" title="The category or what it says">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off" title="When" aria-label="When">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml(is_string($_GET['canned_date'] ?? null) ? $_GET['canned_date'] : '') ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<select class="form-select select2" name="category" onchange="this.form.submit()" aria-label="Category">
|
||||
<option value="">- Any category -</option>
|
||||
<?php foreach ($categories as $category => $count) { ?>
|
||||
<option value="<?= escapeHtml($category) ?>" <?= $category_filter === (string) $category ? 'selected' : '' ?><?= selectOptionData(['icon' => appLogCategoryIcon($category), 'description' => number_format($count) . ' entr' . ($count === 1 ? 'y' : 'ies')]) ?>><?= escapeHtml($category) ?></option>
|
||||
<?php } ?>
|
||||
<?php if ($category_filter !== '' && !isset($categories[$category_filter])) { ?>
|
||||
<option value="<?= escapeHtml($category_filter) ?>" selected><?= escapeHtml($category_filter) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<nav class="nav nav-pills itflow-asset-tabs flex-nowrap overflow-auto mt-3">
|
||||
<?php foreach ($views as $key => $definition) { ?>
|
||||
<a class="nav-link text-nowrap <?= $view === $key ? 'active' : '' ?>" href="<?= escapeHtml($view_url($key)) ?>" title="<?= escapeHtml($definition['title']) ?>" data-app-log-view="<?= $key === '' ? 'all' : $key ?>">
|
||||
<i class="fas fa-fw <?= $definition['icon'] ?> me-1 <?= $definition['tone'] !== '' && $view !== $key ? 'text-' . $definition['tone'] : '' ?>"></i><?= $definition['label'] ?>
|
||||
<span class="badge rounded-pill ms-1"><?= number_format($counts[$key]) ?></span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-nowrap <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3"><?= $sort_link('app_log_created_at', 'When') ?></th>
|
||||
<th><?= $sort_link('app_log_type', 'Level') ?></th>
|
||||
<th><?= $sort_link('app_log_category', 'Category') ?></th>
|
||||
<th>Details</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$app_log_id = intval($row['app_log_id']);
|
||||
$category = (string) $row['app_log_category'];
|
||||
$details = (string) $row['app_log_details'];
|
||||
$short = mb_strlen($details) > 240 ? rtrim(mb_substr($details, 0, 240)) . '…' : $details;
|
||||
?>
|
||||
<tr data-app-log-id="<?= $app_log_id ?>">
|
||||
<td class="ps-3 text-nowrap small" title="<?= escapeHtml($row['app_log_created_at']) ?>">
|
||||
<div><?= escapeHtml(date('M j, Y g:i A', strtotime($row['app_log_created_at']))) ?></div>
|
||||
<div class="text-body-secondary"><?= escapeHtml(timeAgo($row['app_log_created_at'])) ?></div>
|
||||
</td>
|
||||
<td><?= appLogLevelPill($row['app_log_type']) ?></td>
|
||||
<td class="small text-nowrap">
|
||||
<?php if ($category !== '') { ?>
|
||||
<a class="text-reset" href="<?= escapeHtml($category_url($category)) ?>" title="Just <?= escapeHtml($category) ?>"><i class="<?= appLogCategoryIcon($category) ?> fa-fw text-secondary me-1"></i><?= escapeHtml($category) ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="small" style="min-width: 18rem;">
|
||||
<div class="text-break" data-app-log-details><?= escapeHtml($short) ?></div>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<button type="button" class="btn btn-sm btn-light ajax-modal" data-modal-size="lg" data-modal-url="modals/app_log/app_log.php?id=<?= $app_log_id ?>" title="All of it" aria-label="All of it"><i class="fas fa-fw fa-search-plus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
$empty_state = [
|
||||
'thing' => 'app log entries',
|
||||
'icon' => 'fa-clipboard-list',
|
||||
'filtered' => $any_filter,
|
||||
'views' => ['view'],
|
||||
'view' => $view !== '' ? ['icon' => $views[$view]['icon'], 'title' => 'No ' . ($view === 'info' ? 'info entries' : strtolower($views[$view]['label'])) . ' yet', 'text' => escapeHtml($views[$view]['title']) . ' shows here.'] : null,
|
||||
'text' => 'What ITFlow itself does - cron runs, mail, payment providers, the API - and the errors it runs into show here. ' . ($retention['app'] ? 'They are kept for ' . appLogRetentionText($retention['app']) . '.' : 'They are kept until they are cleared.'),
|
||||
];
|
||||
require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($_GET['open'])) { ?>
|
||||
<script>
|
||||
// Opened from somewhere that names one entry - straight to its quick look
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var open = document.createElement('a');
|
||||
open.className = 'ajax-modal';
|
||||
open.href = '#';
|
||||
open.dataset.modalSize = 'lg';
|
||||
open.dataset.modalUrl = 'modals/app_log/app_log.php?id=<?= intval(is_scalar($_GET['open']) ? $_GET['open'] : 0) ?>';
|
||||
document.body.appendChild(open);
|
||||
open.click();
|
||||
open.remove();
|
||||
});
|
||||
</script>
|
||||
<?php } ?>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
|
||||
@@ -100,9 +100,23 @@ $view_empty = [
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-history me-2"></i>Audit Logs</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="<?= buildExportModalUrl('modals/audit_log/audit_log_export.php', ['client', 'user', 'type', 'action', 'entity', 'q'], ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view]) ?>">
|
||||
<i class="fas fa-fw fa-download"></i><span class="d-none d-lg-inline ms-2">Export</span>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="<?= buildExportModalUrl('modals/audit_log/audit_log_export.php', ['client', 'user', 'type', 'action', 'entity', 'q'], ['dtf' => $dtf, 'dtt' => $dtt, 'view' => $view]) ?>">
|
||||
<i class="fas fa-fw fa-download"></i><span class="d-none d-lg-inline ms-2">Export</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More"></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href="/agent/reports/audit_activity.php">
|
||||
<i class="fas fa-fw fa-chart-bar text-secondary me-2"></i>Activity report
|
||||
</a>
|
||||
<a class="dropdown-item" href="settings_logs.php">
|
||||
<i class="fas fa-fw fa-cog text-secondary me-2"></i>Keeping and forwarding
|
||||
</a>
|
||||
<a class="dropdown-item" href="app_logs.php">
|
||||
<i class="fas fa-fw fa-clipboard-list text-secondary me-2"></i>App Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
|
||||
56
admin/database_updates/3.3.3.php
Normal file
56
admin/database_updates/3.3.3.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.3.3 (from 3.3.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// App logs, retention and forwarding (functions/app_log.php, functions/log_forward.php).
|
||||
//
|
||||
// Each step checks first, so it can run again.
|
||||
$itflow_has_column = function ($column) use ($mysqli) {
|
||||
$result = mysqli_query($mysqli, "SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'settings' AND COLUMN_NAME = '" . mysqli_real_escape_string($mysqli, $column) . "' LIMIT 1");
|
||||
return $result && mysqli_num_rows($result) > 0;
|
||||
};
|
||||
|
||||
// 1. The app logs are kept for their own number of days, starting from
|
||||
// what the audit log keeps, as both were kept for that until now. For
|
||||
// both, 0 is now keep forever.
|
||||
if (!$itflow_has_column('config_app_log_retention')) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD COLUMN `config_app_log_retention` int(11) NOT NULL DEFAULT 90");
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_app_log_retention` = `config_log_retention`");
|
||||
}
|
||||
|
||||
// 2. Sending log entries on to a syslog server or SIEM as they are
|
||||
// written: on or off, where to (host, port, and UDP, TCP or TLS), how
|
||||
// (JSON, CEF or plain text), what (the audit log, and the app logs from
|
||||
// a level up - '' for none) and, over TLS, whether the server's
|
||||
// certificate is checked.
|
||||
$itflow_columns = [
|
||||
'config_log_forward_enabled' => "tinyint(1) NOT NULL DEFAULT 0",
|
||||
'config_log_forward_host' => "varchar(200) DEFAULT NULL",
|
||||
'config_log_forward_port' => "int(11) NOT NULL DEFAULT 514",
|
||||
'config_log_forward_protocol' => "varchar(10) NOT NULL DEFAULT 'udp'",
|
||||
'config_log_forward_format' => "varchar(10) NOT NULL DEFAULT 'json'",
|
||||
'config_log_forward_audit' => "tinyint(1) NOT NULL DEFAULT 1",
|
||||
'config_log_forward_app_level' => "varchar(10) NOT NULL DEFAULT 'warning'",
|
||||
'config_log_forward_tls_verify' => "tinyint(1) NOT NULL DEFAULT 1",
|
||||
];
|
||||
foreach ($itflow_columns as $itflow_column => $itflow_definition) {
|
||||
if (!$itflow_has_column($itflow_column)) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD COLUMN `$itflow_column` $itflow_definition");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. The app logs are listed and cleared by date, and nightly deletes the
|
||||
// old ones by date - a key on it.
|
||||
$itflow_index = mysqli_query($mysqli, "SELECT 1 FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_logs' AND INDEX_NAME = 'app_log_created_at' LIMIT 1");
|
||||
if (!$itflow_index || mysqli_num_rows($itflow_index) === 0) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `app_logs` ADD KEY `app_log_created_at` (`app_log_created_at`)");
|
||||
}
|
||||
|
||||
unset($itflow_has_column, $itflow_columns, $itflow_column, $itflow_definition, $itflow_index);
|
||||
37
admin/database_updates/3.3.4.php
Normal file
37
admin/database_updates/3.3.4.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.3.4 (from 3.3.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Vendor templates (functions/vendor_template.php).
|
||||
//
|
||||
// 1. Who made a vendor template and who last changed it. Checked first, so
|
||||
// the step can run again.
|
||||
$itflow_column = mysqli_query($mysqli, "SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vendor_templates' AND COLUMN_NAME = 'vendor_template_created_by' LIMIT 1");
|
||||
if (!$itflow_column || mysqli_num_rows($itflow_column) === 0) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `vendor_templates`
|
||||
ADD `vendor_template_created_by` int(11) NOT NULL DEFAULT 0 AFTER `vendor_template_archived_at`,
|
||||
ADD `vendor_template_updated_by` int(11) NOT NULL DEFAULT 0 AFTER `vendor_template_created_by`");
|
||||
}
|
||||
|
||||
// 2. ...taken from the audit log where it has a record of it - the first
|
||||
// Create on the template and the latest Edit. Not counted as a change.
|
||||
mysqli_query($mysqli, "UPDATE `vendor_templates` JOIN (
|
||||
SELECT `log_entity_id`, SUBSTRING_INDEX(GROUP_CONCAT(`log_user_id` ORDER BY `log_id` ASC), ',', 1) AS `user_id`
|
||||
FROM `logs` WHERE `log_type` = 'Vendor Template' AND `log_action` = 'Create' AND `log_entity_id` > 0 AND `log_user_id` > 0 GROUP BY `log_entity_id`
|
||||
) AS `made` ON `made`.`log_entity_id` = `vendor_templates`.`vendor_template_id`
|
||||
SET `vendor_template_created_by` = `made`.`user_id`, `vendor_template_updated_at` = `vendor_template_updated_at`
|
||||
WHERE `vendor_template_created_by` = 0");
|
||||
mysqli_query($mysqli, "UPDATE `vendor_templates` JOIN (
|
||||
SELECT `log_entity_id`, SUBSTRING_INDEX(GROUP_CONCAT(`log_user_id` ORDER BY `log_id` DESC), ',', 1) AS `user_id`
|
||||
FROM `logs` WHERE `log_type` = 'Vendor Template' AND `log_action` = 'Edit' AND `log_entity_id` > 0 AND `log_user_id` > 0 GROUP BY `log_entity_id`
|
||||
) AS `changed` ON `changed`.`log_entity_id` = `vendor_templates`.`vendor_template_id`
|
||||
SET `vendor_template_updated_by` = `changed`.`user_id`, `vendor_template_updated_at` = `vendor_template_updated_at`
|
||||
WHERE `vendor_template_updated_by` = 0 AND `vendor_template_updated_at` IS NOT NULL");
|
||||
|
||||
unset($itflow_column);
|
||||
@@ -150,7 +150,7 @@
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item">
|
||||
<a href="/admin/vendor_templates.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'vendor_templates.php' ? 'active' : '') ?>">
|
||||
<a href="/admin/vendor_templates.php" class="nav-link <?= (in_array(basename($_SERVER['PHP_SELF']), ['vendor_templates.php', 'vendor_template.php']) ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-building"></i>
|
||||
<p>Vendor Templates</p>
|
||||
</a>
|
||||
@@ -221,7 +221,7 @@
|
||||
</li>
|
||||
|
||||
<!-- SETTINGS Section -->
|
||||
<li class="nav-item mt-2 <?= (in_array(basename($_SERVER['PHP_SELF']), ['settings_company.php', 'settings_localization.php', 'settings_theme.php', 'settings_security.php', 'settings_mail.php', 'settings_notification.php', 'settings_default.php', 'settings_invoice.php', 'settings_quote.php', 'settings_online_payment.php', 'settings_online_payment_clients.php', 'settings_project.php', 'settings_ticket.php', 'settings_ai.php', 'identity_providers.php', 'settings_telemetry.php', 'settings_module.php', 'settings_client_portal.php']) ? 'menu-open' : '') ?>">
|
||||
<li class="nav-item mt-2 <?= (in_array(basename($_SERVER['PHP_SELF']), ['settings_company.php', 'settings_localization.php', 'settings_theme.php', 'settings_security.php', 'settings_logs.php', 'settings_mail.php', 'settings_notification.php', 'settings_default.php', 'settings_invoice.php', 'settings_quote.php', 'settings_online_payment.php', 'settings_online_payment_clients.php', 'settings_project.php', 'settings_ticket.php', 'settings_ai.php', 'identity_providers.php', 'settings_telemetry.php', 'settings_module.php', 'settings_client_portal.php']) ? 'menu-open' : '') ?>">
|
||||
<a href="#" class="nav-link">
|
||||
<p>
|
||||
SETTINGS
|
||||
@@ -253,6 +253,12 @@
|
||||
<p>Security</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_logs.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_logs.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon fas fa-scroll"></i>
|
||||
<p>Logs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings_mail.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'settings_mail.php' ? 'active' : '') ?>">
|
||||
<i class="nav-icon far fa-envelope"></i>
|
||||
|
||||
155
admin/modals/app_log/app_log.php
Normal file
155
admin/modals/app_log/app_log.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* An app log entry's quick look - all of it: when, the level and category,
|
||||
* the whole of what it says (to copy, for a bug report), then the other
|
||||
* entries in the five minutes either side and what was done in the audit log
|
||||
* then - so a payment error sits beside the payment, a mail error beside the
|
||||
* ticket reply. Opened from Admin > App Logs.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$app_log_id = intval($_GET['id'] ?? 0);
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM app_logs WHERE app_log_id = $app_log_id"));
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That app log entry no longer exists - it may have been cleared.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$level = appLogLevel($row['app_log_type']);
|
||||
$style = appLogLevels()[$level];
|
||||
$category = (string) $row['app_log_category'];
|
||||
$created = strtotime($row['app_log_created_at']);
|
||||
$at = escapeSql($row['app_log_created_at']);
|
||||
$window = "BETWEEN '$at' - INTERVAL 5 MINUTE AND '$at' + INTERVAL 5 MINUTE";
|
||||
|
||||
// The other app log entries in the five minutes either side
|
||||
$around_where = "app_log_created_at $window AND app_log_id <> $app_log_id";
|
||||
$around_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM app_logs WHERE $around_where"))[0]);
|
||||
$around = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT app_log_id, app_log_category, app_log_type, app_log_details, app_log_created_at FROM app_logs WHERE $around_where ORDER BY app_log_created_at ASC, app_log_id ASC LIMIT 8");
|
||||
while ($around_row = mysqli_fetch_assoc($sql)) {
|
||||
$around[] = $around_row;
|
||||
}
|
||||
|
||||
// What was done in the audit log then
|
||||
$audit_where = "log_created_at $window";
|
||||
$audit_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE $audit_where"))[0]);
|
||||
$audit = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT log_id, log_type, log_action, log_description, log_created_at FROM logs WHERE $audit_where ORDER BY log_created_at ASC, log_id ASC LIMIT 8");
|
||||
while ($audit_row = mysqli_fetch_assoc($sql)) {
|
||||
$audit[] = $audit_row;
|
||||
}
|
||||
|
||||
$category_count = $category !== '' ? intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM app_logs WHERE app_log_category = '" . escapeSql($category) . "'"))[0]) : 0;
|
||||
$day = date('Y-m-d', $created);
|
||||
$day_query = 'canned_date=custom&dtf=' . $day . '&dtt=' . $day;
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark" data-bs-theme="dark">
|
||||
<div class="d-flex align-items-center gap-3 min-w-0 flex-grow-1">
|
||||
<span class="itflow-asset-icon itflow-asset-icon-lg bg-<?= $style['tone'] ?>-subtle text-<?= $style['tone'] ?>-emphasis" aria-hidden="true"><i class="<?= appLogCategoryIcon($category) ?>"></i></span>
|
||||
<div class="min-w-0 flex-grow-1">
|
||||
<h5 class="modal-title fw-semibold text-break"><?= escapeHtml($category !== '' ? $category : 'No category') ?> · <?= $style['one'] ?></h5>
|
||||
<div class="small text-body-secondary mt-1"><?= escapeHtml(date('D, M j, Y \a\t g:i:s A', $created)) ?> · <?= escapeHtml(timeAgo($row['app_log_created_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white ms-3" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="p-3 rounded border bg-body-tertiary mb-3 position-relative">
|
||||
<button type="button" class="btn btn-sm btn-light position-absolute top-0 end-0 m-2 clipboardjs" title="Copy" aria-label="Copy" data-clipboard-text="<?= escapeHtml($row['app_log_details']) ?>"><i class="far fa-fw fa-copy"></i></button>
|
||||
<div class="text-break font-monospace small pe-5" style="white-space: pre-wrap;" data-app-log-full-details><?= escapeHtml((string) $row['app_log_details']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 itflow-asset-glance">
|
||||
<div class="col-sm-6">
|
||||
<div class="itflow-glance-item" title="Level">
|
||||
<i class="fas fa-fw fa-signal"></i>
|
||||
<span><?= appLogLevelPill($level) ?> <span class="small text-body-secondary ms-1"><?= escapeHtml($style['title']) ?></span></span>
|
||||
</div>
|
||||
<div class="itflow-glance-item" title="Category">
|
||||
<i class="fas fa-fw fa-folder"></i>
|
||||
<?php if ($category !== '') { ?>
|
||||
<span><?= escapeHtml($category) ?> <a class="small ms-1" href="/admin/app_logs.php?category=<?= urlencode($category) ?>">All <?= number_format($category_count) ?> from it</a></span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">None</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="itflow-glance-item" title="Entry">
|
||||
<i class="fas fa-fw fa-hashtag"></i>
|
||||
<span class="text-body-secondary">Entry <?= $app_log_id ?></span>
|
||||
</div>
|
||||
<div class="itflow-glance-item" title="When">
|
||||
<i class="fas fa-fw fa-clock"></i>
|
||||
<span class="font-monospace small"><?= escapeHtml($row['app_log_created_at']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" data-app-log-around>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="small fw-semibold text-uppercase text-body-secondary">The app logs around then</div>
|
||||
<?php if ($around_count > 0) { ?>
|
||||
<a class="small ms-auto" href="/admin/app_logs.php?<?= $day_query ?>">All of that day</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($around) { ?>
|
||||
<div class="list-group">
|
||||
<?php foreach ($around as $entry) { ?>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-start gap-2 small ajax-modal" data-modal-size="lg" data-modal-url="/admin/modals/app_log/app_log.php?id=<?= intval($entry['app_log_id']) ?>">
|
||||
<span class="text-body-secondary text-nowrap flex-shrink-0" style="min-width: 4.5rem;" title="<?= escapeHtml($entry['app_log_created_at']) ?>"><?= escapeHtml(date('g:i:s A', strtotime($entry['app_log_created_at']))) ?></span>
|
||||
<span class="flex-shrink-0"><?= appLogLevelPill($entry['app_log_type']) ?></span>
|
||||
<span class="text-break"><span class="fw-semibold"><?= escapeHtml((string) $entry['app_log_category']) ?></span> <?= escapeHtml(mb_strimwidth((string) $entry['app_log_details'], 0, 160, '…')) ?></span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($around_count > count($around)) { ?>
|
||||
<div class="small text-body-secondary mt-1">and <?= $around_count - count($around) ?> more</div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<div class="small text-body-secondary">Nothing else in the five minutes either side.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" data-app-log-audit>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="small fw-semibold text-uppercase text-body-secondary">The audit log then</div>
|
||||
<?php if ($audit_count > 0) { ?>
|
||||
<a class="small ms-auto" href="/admin/audit_logs.php?<?= $day_query ?>">All of that day</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($audit) { ?>
|
||||
<div class="list-group">
|
||||
<?php foreach ($audit as $entry) { ?>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-start gap-2 small ajax-modal" data-modal-size="lg" data-modal-url="/admin/modals/audit_log/audit_log.php?id=<?= intval($entry['log_id']) ?>">
|
||||
<span class="text-body-secondary text-nowrap flex-shrink-0" style="min-width: 4.5rem;" title="<?= escapeHtml($entry['log_created_at']) ?>"><?= escapeHtml(date('g:i:s A', strtotime($entry['log_created_at']))) ?></span>
|
||||
<span class="flex-shrink-0"><?= auditLogActionPill($entry['log_action']) ?></span>
|
||||
<span class="text-break"><?= escapeHtml($entry['log_description']) ?></span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($audit_count > count($audit)) { ?>
|
||||
<div class="small text-body-secondary mt-1">and <?= $audit_count - count($audit) ?> more</div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<div class="small text-body-secondary">Nothing was done in the five minutes either side.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
77
admin/modals/app_log/app_log_clear.php
Normal file
77
admin/modals/app_log/app_log_clear.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Clear the app logs - the entries the list is showing, with its search,
|
||||
* level, category and dates, or every one. Each clear is in the audit log,
|
||||
* saying what went (clear_app_logs, admin/post/app_log.php). The audit log
|
||||
* itself can't be cleared - it is only ever trimmed by its retention.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$views = appLogViews();
|
||||
$src = [
|
||||
'q' => is_string($_GET['q'] ?? null) ? $_GET['q'] : '',
|
||||
'view' => is_string($_GET['view'] ?? null) && isset($views[$_GET['view']]) ? $_GET['view'] : '',
|
||||
'category' => is_string($_GET['category'] ?? null) ? $_GET['category'] : '',
|
||||
'dtf' => is_string($_GET['dtf'] ?? null) ? $_GET['dtf'] : '',
|
||||
'dtt' => is_string($_GET['dtt'] ?? null) ? $_GET['dtt'] : '',
|
||||
];
|
||||
$summary = [];
|
||||
$where = appLogFiltersSql($src, $summary);
|
||||
$matching = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM app_logs WHERE $where"))[0]);
|
||||
$total = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM app_logs"))[0]);
|
||||
$filtered = !empty($summary);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-trash-alt me-2"></i>Clear App Logs</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<?php foreach ($src as $name => $value) { if ($value !== '') { ?>
|
||||
<input type="hidden" name="<?= $name ?>" value="<?= escapeHtml($value) ?>">
|
||||
<?php } } ?>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php if (!$total) { ?>
|
||||
<div class="text-center text-secondary py-4">There is nothing in the app logs to clear.</div>
|
||||
<?php } else { ?>
|
||||
|
||||
<p class="mb-3">The entries are deleted for good. Clearing is in the audit log, with what was cleared.</p>
|
||||
|
||||
<?php if ($filtered) { ?>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="scope" value="shown" id="appLogClearShown" <?= $matching ? 'checked' : 'disabled' ?>>
|
||||
<label class="form-check-label" for="appLogClearShown">
|
||||
<span class="fw-semibold" data-app-log-clear-matching="<?= $matching ?>"><?= number_format($matching) ?> entr<?= $matching === 1 ? 'y' : 'ies' ?></span> - the ones shown:
|
||||
<span class="text-body-secondary" data-app-log-clear-filters><?= escapeHtml(appLogFiltersText($summary)) ?></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scope" value="all" id="appLogClearAll" <?= $filtered && $matching ? '' : 'checked' ?>>
|
||||
<label class="form-check-label" for="appLogClearAll">
|
||||
<span class="fw-semibold" data-app-log-clear-total="<?= $total ?>">Every entry - all <?= number_format($total) ?></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?php if ($total) { ?>
|
||||
<button type="submit" name="clear_app_logs" class="btn btn-danger text-bold"><i class="fas fa-trash-alt me-2"></i>Clear</button>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
47
admin/modals/app_log/app_log_export.php
Normal file
47
admin/modals/app_log/app_log_export.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Export the app logs to CSV or PDF - pre-filled from the list's filters and
|
||||
* level, every one editable: the export runs what this form posts, not what
|
||||
* the page happened to show (export_app_logs, admin/post/app_log.php).
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$views = appLogViews();
|
||||
$q_filter = is_string($_GET['q'] ?? null) ? $_GET['q'] : '';
|
||||
$view_filter = is_string($_GET['view'] ?? null) && isset($views[$_GET['view']]) ? $_GET['view'] : '';
|
||||
$category_filter = is_string($_GET['category'] ?? null) ? $_GET['category'] : '';
|
||||
$date_from_filter = (is_string($_GET['dtf'] ?? null) && appLogDate($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') ? $_GET['dtf'] : '';
|
||||
$date_to_filter = (is_string($_GET['dtt'] ?? null) && appLogDate($_GET['dtt']) && $_GET['dtt'] !== '2099-12-31') ? $_GET['dtt'] : '';
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export App Logs</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<?php exportTabsNav(); ?>
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php exportTabsFiltersOpen(); ?>
|
||||
|
||||
<?php require 'app_log_filters.php'; ?>
|
||||
|
||||
<?php exportTabsColumns('app_logs'); ?>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?php renderExportButtons('export_app_logs'); ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
62
admin/modals/app_log/app_log_filters.php
Normal file
62
admin/modals/app_log/app_log_filters.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The app log filters, for the export and clear windows - search, level,
|
||||
* category and dates, filled in from what the list shows. Expects
|
||||
* $q_filter, $view_filter, $category_filter, $date_from_filter and
|
||||
* $date_to_filter, and $views (appLogViews()).
|
||||
*/
|
||||
|
||||
isset($mysqli, $views) || die("Direct file access is not allowed");
|
||||
|
||||
$filter_categories = appLogCategories();
|
||||
|
||||
?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="<?= escapeHtml($q_filter) ?>" placeholder="The category or what it says">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Level</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-signal"></i></span>
|
||||
<select class="form-select select2" name="view">
|
||||
<?php foreach ($views as $key => $definition) { ?>
|
||||
<option value="<?= $key ?>" <?= $view_filter === $key ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . $definition['icon'], 'description' => $definition['title']]) ?>><?= $key === '' ? 'Every level' : escapeHtml($definition['label']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-folder"></i></span>
|
||||
<select class="form-select select2" name="category">
|
||||
<option value="">- Any category -</option>
|
||||
<?php foreach ($filter_categories as $category => $count) { ?>
|
||||
<option value="<?= escapeHtml($category) ?>" <?= $category_filter === (string) $category ? 'selected' : '' ?><?= selectOptionData(['icon' => appLogCategoryIcon($category), 'description' => number_format($count) . ' entr' . ($count === 1 ? 'y' : 'ies')]) ?>><?= escapeHtml($category) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">From</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="dtf" value="<?= $date_from_filter ?>" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">To</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="dtt" value="<?= $date_to_filter ?>" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,147 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* New vendor template - the fields are vendor_template_form.php. With
|
||||
* from_vendor (a vendor's id) it starts from that vendor - not its account
|
||||
* number, PIN or account manager, which are its client's own - and the
|
||||
* vendor is linked to the new template.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$vt_id = 0;
|
||||
$vt = [];
|
||||
$from_vendor = null;
|
||||
$from_vendor_id = intval($_GET['from_vendor'] ?? 0);
|
||||
if ($from_vendor_id) {
|
||||
$from_vendor = vendorRow($from_vendor_id);
|
||||
if ($from_vendor) {
|
||||
$vt = [
|
||||
'name' => $from_vendor['vendor_name'],
|
||||
'description' => $from_vendor['vendor_description'],
|
||||
'phone_country_code' => $from_vendor['vendor_phone_country_code'],
|
||||
'phone' => $from_vendor['vendor_phone'],
|
||||
'extension' => $from_vendor['vendor_extension'],
|
||||
'hours' => $from_vendor['vendor_hours'],
|
||||
'sla' => $from_vendor['vendor_sla'],
|
||||
'email' => $from_vendor['vendor_email'],
|
||||
'website' => $from_vendor['vendor_website'],
|
||||
'notes' => $from_vendor['vendor_notes'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>New Vendor Template</h5>
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-building me-2"></i>New Vendor Template</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<form action="/admin/post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<ul class="nav nav-pills nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details">Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-support">Support</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes">Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade show active" id="pills-details">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Vendor Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Account Number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
|
||||
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Account Manager</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<input type="text" class="form-control" name="contact_name" placeholder="Account manager's name" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($from_vendor) { ?>
|
||||
<input type="hidden" name="from_vendor" value="<?= intval($from_vendor['vendor_id']) ?>">
|
||||
<div class="alert alert-info small py-2" data-from-vendor>
|
||||
<i class="fas fa-fw fa-info-circle me-1"></i>Starting from <strong><?= escapeHtml($from_vendor['vendor_name']) ?></strong><?= $from_vendor['client_name'] !== null ? ' for ' . escapeHtml($from_vendor['client_name']) : '' ?> - its account number, PIN and account manager stay its own. It is linked to the new template.
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-support">
|
||||
|
||||
<label>Support Phone / <span class="text-secondary">Extension</span></label>
|
||||
<div class="row g-2">
|
||||
<div class="col-9">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
|
||||
<input type="hidden" name="phone_country_code">
|
||||
<input type="tel" class="form-control" name="phone" placeholder="Phone Number" maxlength="200" data-itflow-phone="phone_country_code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" name="extension" placeholder="ext." maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Hours</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="text" class="form-control" name="hours" placeholder="Support Hours" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Email</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
|
||||
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Website URL</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
|
||||
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Pin/Code</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
|
||||
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-notes">
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
<?php require __DIR__ . '/vendor_template_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_vendor_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create Template</button>
|
||||
<button type="submit" name="add_vendor_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create Vendor Template</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,217 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Edit a vendor template - the fields are vendor_template_form.php, with
|
||||
* the fields ticked changed on the vendors made from it too.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$vendor_template_id = intval($_GET['id']);
|
||||
$template = vendorTemplateInfo($_GET['id'] ?? 0);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT vendor_template_account_number, vendor_template_code, vendor_template_contact_name,
|
||||
vendor_template_description, vendor_template_email, vendor_template_extension,
|
||||
vendor_template_hours, vendor_template_name, vendor_template_notes, vendor_template_phone,
|
||||
vendor_template_phone_country_code, vendor_template_sla, vendor_template_website FROM vendor_templates WHERE vendor_template_id = $vendor_template_id LIMIT 1");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$vendor_name = escapeHtml($row['vendor_template_name']);
|
||||
$vendor_description = escapeHtml($row['vendor_template_description']);
|
||||
$vendor_account_number = escapeHtml($row['vendor_template_account_number']);
|
||||
$vendor_contact_name = escapeHtml($row['vendor_template_contact_name']);
|
||||
$vendor_phone_country_code = intval($row['vendor_template_phone_country_code']);
|
||||
$vendor_phone = formatPhoneNumber($row['vendor_template_phone'], $vendor_phone_country_code, false);
|
||||
$vendor_extension = escapeHtml($row['vendor_template_extension']);
|
||||
$vendor_email = escapeHtml($row['vendor_template_email']);
|
||||
$vendor_website = escapeHtml($row['vendor_template_website']);
|
||||
$vendor_hours = escapeHtml($row['vendor_template_hours']);
|
||||
$vendor_sla = escapeHtml($row['vendor_template_sla']);
|
||||
$vendor_code = escapeHtml($row['vendor_template_code']);
|
||||
$vendor_notes = escapeHtml($row['vendor_template_notes']);
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
if (!$template) { ?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-building me-2"></i>Vendor template</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center text-secondary py-5">That vendor template is gone - it may have been deleted.</div>
|
||||
<?php } else {
|
||||
$vt_id = intval($template['vendor_template_id']);
|
||||
$vt = vendorTemplateRowFields($template);
|
||||
$vt_vendors = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM vendors WHERE vendor_template_id = $vt_id"))[0]);
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>Editing vendor template: <strong><?= $vendor_name ?></strong></h5>
|
||||
<h5 class="modal-title text-truncate"><i class="fa fa-fw fa-building me-2"></i>Vendor template: <strong><?= escapeHtml($template['vendor_template_name']) ?></strong></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<form action="/admin/post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="vendor_template_id" value="<?= $vendor_template_id ?>">
|
||||
<input type="hidden" name="vendor_template_id" value="<?= $vt_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<ul class="nav nav-pills nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details<?= $vendor_template_id ?>">Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-support<?= $vendor_template_id ?>">Support</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes<?= $vendor_template_id ?>">Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="alert alert-info">Check the fields you would like to update globally</div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade show active" id="pills-details<?= $vendor_template_id ?>">
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Vendor Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" value="<?= "$vendor_name" ?>" required>
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_name" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200" value="<?= $vendor_description ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_description" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Account Number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
|
||||
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200" value="<?= $vendor_account_number ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_account_number" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Account Manager</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<input type="text" class="form-control" name="contact_name" maxlength="200" value="<?= $vendor_contact_name ?>" placeholder="Vendor contact name">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_contact_name" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="updateVendorsCheckbox<?= $vendor_template_id ?>" name="update_base_vendors" value="1" >
|
||||
<label class="form-check-label" for="updateVendorsCheckbox<?= $vendor_template_id ?>">Update All Base Vendors</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-support<?= $vendor_template_id ?>">
|
||||
|
||||
<label>Support Phone</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-8">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
|
||||
<input type="hidden" name="phone_country_code" value="<?= $vendor_phone_country_code ?>">
|
||||
<input type="tel" class="form-control" name="phone" maxlength="200" value="<?= $vendor_phone ?>" data-itflow-phone="phone_country_code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="extension" placeholder="Prompts" maxlength="200" value="<?= $vendor_extension ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_phone" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Hours</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="text" class="form-control" name="hours" placeholder="Support Hours" maxlength="200" value="<?= $vendor_hours ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_hours" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Email</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
|
||||
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200" value="<?= $vendor_email ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_email" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Support Website URL</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
|
||||
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200" value="<?= $vendor_website ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_website" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
|
||||
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200" value="<?= $vendor_sla ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_sla" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Pin/Code</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200" value="<?= $vendor_code ?>">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_code" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-notes<?= $vendor_template_id ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" rows="8" placeholder="Enter some notes" name="notes"><?= $vendor_notes ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Update Notes Globally?</label>
|
||||
<input class="form-check-input" type="checkbox" name="global_update_vendor_notes" value="1">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/vendor_template_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary text-bold" name="edit_vendor_template"><i class="fa fa-check me-2"></i>Update Template</button>
|
||||
<button type="submit" name="edit_vendor_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
<?php }
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
186
admin/modals/vendor_template/vendor_template_form.php
Normal file
186
admin/modals/vendor_template/vendor_template_form.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* A vendor template's fields - for vendor_template_add.php and
|
||||
* vendor_template_edit.php, which set $vt (vendorTemplateRowFields() of the
|
||||
* template, or blanks when new), $vt_id (0 when new) and, editing, $vt_vendors
|
||||
* (how many vendors were made from it). The same tabs as a vendor's own
|
||||
* window - Details, Support, Account and Notes - as a vendor made from it
|
||||
* gets every one of them. Editing, the fields can be changed on the vendors
|
||||
* made from it too. Posts to admin/post.php (admin/post/vendor_template.php).
|
||||
*/
|
||||
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
|
||||
$form_id = 'vendorTemplateForm' . ($vt_id ?: 'New');
|
||||
$value = function ($key) use ($vt) {
|
||||
return escapeHtml((string) ($vt[$key] ?? ''));
|
||||
};
|
||||
$phone_value = ($vt['phone'] ?? '') !== '' ? escapeHtml(formatPhoneNumber($vt['phone'], $vt['phone_country_code'] ?? '', false)) : '';
|
||||
|
||||
?>
|
||||
|
||||
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#<?= $form_id ?>Details"><i class="fas fa-fw fa-building me-2 d-none d-lg-inline"></i>Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Support"><i class="fas fa-fw fa-headset me-2 d-none d-lg-inline"></i>Support</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Account"><i class="fas fa-fw fa-id-card me-2 d-none d-lg-inline"></i>Account</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Notes"><i class="fas fa-fw fa-sticky-note me-2 d-none d-lg-inline"></i>Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="<?= $form_id ?>">
|
||||
|
||||
<!-- Who they are -->
|
||||
<div class="tab-pane fade show active" id="<?= $form_id ?>Details">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-building"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Comcast Business, Dell, Microsoft" maxlength="200" value="<?= $value('name') ?>" required <?= $vt_id ? '' : 'autofocus' ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">What they are</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Business internet and phone, servers and workstations" maxlength="200" value="<?= $value('description') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-3">A vendor added to a client from it starts with everything here - their own account number and PIN go on after.</div>
|
||||
</div>
|
||||
|
||||
<!-- How to reach support -->
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>Support">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Support phone</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
|
||||
<input type="hidden" name="phone_country_code" value="<?= $value('phone_country_code') ?>">
|
||||
<input type="tel" class="form-control" name="phone" value="<?= $phone_value ?>" placeholder="Phone number" maxlength="200" data-itflow-phone="phone_country_code">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Extension / option</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-hashtag"></i></span>
|
||||
<input type="text" class="form-control" name="extension" value="<?= $value('extension') ?>" placeholder="ext." maxlength="200" inputmode="numeric">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Support hours</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
|
||||
<input type="text" class="form-control" name="hours" placeholder="24/7, Mon-Fri 8-6 ET" maxlength="200" value="<?= $value('hours') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">SLA</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
|
||||
<input type="text" class="form-control" name="sla" placeholder="4 hour response, next business day" maxlength="200" value="<?= $value('sla') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Support email</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
|
||||
<input type="email" class="form-control" name="email" placeholder="support@vendor.com" maxlength="200" value="<?= $value('email') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Support site or portal</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
|
||||
<input type="text" class="form-control" name="website" placeholder="support.vendor.com" maxlength="200" value="<?= $value('website') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What they ask for, and who looks after us -->
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>Account">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<label class="form-label">Account number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
|
||||
<input type="text" class="form-control" name="account_number" placeholder="Usually each client's own - leave blank" maxlength="200" value="<?= $value('account_number') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">PIN / passcode</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control" name="code" placeholder="Usually each client's own" maxlength="200" value="<?= $value('code') ?>" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text mt-0">Fill these in only when every client uses the same one - your own partner or reseller account, say.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Account manager</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-tie"></i></span>
|
||||
<input type="text" class="form-control" name="contact_name" placeholder="Your rep there, when every client has the same one" maxlength="200" value="<?= $value('contact_name') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Everything else -->
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>Notes">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea class="form-control" rows="10" name="notes" placeholder="Escalation path, what they need before they will help, menu options, how to open a case online - what every client's vendor should say"><?= $value('notes') ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($vt_id && !empty($vt_vendors)) { ?>
|
||||
<!-- The vendors made from it, changed too - the fields ticked -->
|
||||
<div class="border rounded p-3 mt-3 bg-body-tertiary" data-vendor-template-push>
|
||||
<div class="fw-semibold mb-1"><i class="fas fa-fw fa-sync-alt text-secondary me-1"></i>Change them on the <?= $vt_vendors ?> vendor<?= $vt_vendors === 1 ? '' : 's' ?> made from it too</div>
|
||||
<div class="small text-body-secondary mb-2">Ticked fields are set to what is here on each one - what a client's vendor says now is replaced.</div>
|
||||
<div class="row row-cols-2 row-cols-md-3 g-1 small">
|
||||
<?php foreach (vendorTemplateFieldLabels() as $key => $field) { ?>
|
||||
<div class="col">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="push[]" value="<?= $key ?>" id="<?= $form_id ?>Push<?= $key ?>">
|
||||
<label class="form-check-label" for="<?= $form_id ?>Push<?= $key ?>"><i class="fas fa-fw <?= $field['icon'] ?> text-secondary me-1"></i><?= $field['label'] ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<script>
|
||||
itflowReady(function () {
|
||||
var form = document.getElementById(<?= json_encode($form_id) ?>).closest('form');
|
||||
|
||||
// A required field on a tab that isn't showing: show that tab and point at it
|
||||
var reported = false;
|
||||
form.addEventListener('invalid', function (e) {
|
||||
var pane = e.target.closest('.tab-pane');
|
||||
var tab = pane && form.querySelector('[data-bs-toggle="pill"][href="#' + pane.id + '"]');
|
||||
if (!tab || pane.classList.contains('active') || reported) {
|
||||
return;
|
||||
}
|
||||
reported = true;
|
||||
tab.addEventListener('shown.bs.tab', function () {
|
||||
reported = false;
|
||||
e.target.reportValidity();
|
||||
}, { once: true });
|
||||
bootstrap.Tab.getOrCreateInstance(tab).show();
|
||||
}, true);
|
||||
});
|
||||
</script>
|
||||
79
admin/post/app_log.php
Normal file
79
admin/post/app_log.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - POST handler for the app logs: exporting them to CSV or PDF, and
|
||||
* clearing them, with the filters the export and clear windows post - the
|
||||
* same ones the list uses (appLogFiltersSql(), functions/app_log.php). Both
|
||||
* are in the audit log.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isExportRequest('export_app_logs')) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceAdminPermission();
|
||||
|
||||
$format = resolveExportFormat($_POST['export_app_logs']);
|
||||
|
||||
$filter_summary = [];
|
||||
$where = appLogFiltersSql($_POST, $filter_summary);
|
||||
|
||||
$num_rows = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM app_logs WHERE $where"))[0]);
|
||||
|
||||
if ($num_rows > 0) {
|
||||
|
||||
guardExportPdfRowCount($format, $num_rows);
|
||||
|
||||
$export = beginExport('app_logs', $format, "$session_company_name-App-Logs", 'App Logs', summarizeExportFilters($filter_summary));
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT app_log_id, app_log_category, app_log_type, app_log_details, app_log_created_at FROM app_logs
|
||||
WHERE $where
|
||||
ORDER BY app_log_created_at DESC, app_log_id DESC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$row['app_log_type'] = appLogLevels()[appLogLevel($row['app_log_type'])]['one'];
|
||||
addExportRow($export, $row);
|
||||
}
|
||||
|
||||
finishExport($export);
|
||||
|
||||
logAudit("App Log", "Export", "$session_name exported $num_rows app log " . ($num_rows === 1 ? 'entry' : 'entries') . " to a " . strtoupper($format) . " file");
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
flashAlert('No app log entries match - nothing to export.', 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['clear_app_logs'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceAdminPermission();
|
||||
|
||||
// The ones shown, with the list's filters - or every one
|
||||
$all = ($_POST['scope'] ?? '') !== 'shown';
|
||||
$summary = [];
|
||||
$where = $all ? '1 = 1' : appLogFiltersSql($_POST, $summary);
|
||||
if (!$all && !$summary) {
|
||||
$all = true;
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM app_logs WHERE $where");
|
||||
$cleared = mysqli_affected_rows($mysqli);
|
||||
|
||||
if ($cleared > 0) {
|
||||
$what = lcfirst(appLogFiltersText($summary));
|
||||
logAudit("App Log", "Delete", "$session_name cleared $cleared app log " . ($cleared === 1 ? 'entry' : 'entries') . ($all ? ' - all of them' : ' - ' . escapeSql($what)));
|
||||
flashAlert("Cleared $cleared app log " . ($cleared === 1 ? 'entry' : 'entries'));
|
||||
} else {
|
||||
flashAlert('There was nothing to clear.', 'error');
|
||||
}
|
||||
|
||||
redirect('app_logs.php');
|
||||
|
||||
}
|
||||
119
admin/post/settings_logs.php
Normal file
119
admin/post/settings_logs.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - POST handler for Admin > Settings > Logs: how long each log is
|
||||
* kept, and sending entries on to a syslog server or SIEM. Save and send a
|
||||
* test saves first, then sends one test message with what was saved
|
||||
* (logForwardTest(), functions/log_forward.php). What changed is in the audit
|
||||
* log.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['edit_log_settings']) || isset($_POST['test_log_forward'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
// Days, or 0 for keep forever
|
||||
$days = function ($name, $forever) {
|
||||
if (!empty($_POST[$forever])) {
|
||||
return 0;
|
||||
}
|
||||
return min(36500, max(1, intval($_POST[$name] ?? 90)));
|
||||
};
|
||||
$audit_days = $days('config_log_retention', 'audit_forever');
|
||||
$app_days = $days('config_app_log_retention', 'app_forever');
|
||||
|
||||
$was_retention = appLogRetention();
|
||||
$was = logForwardSettings();
|
||||
$posted_host = is_string($_POST['config_log_forward_host'] ?? null) ? trim($_POST['config_log_forward_host']) : '';
|
||||
$forward = logForwardTidy([
|
||||
'enabled' => $_POST['config_log_forward_enabled'] ?? 0,
|
||||
'host' => $posted_host,
|
||||
'port' => $_POST['config_log_forward_port'] ?? 514,
|
||||
'protocol' => $_POST['config_log_forward_protocol'] ?? 'udp',
|
||||
'format' => $_POST['config_log_forward_format'] ?? 'json',
|
||||
'audit' => $_POST['config_log_forward_audit'] ?? 0,
|
||||
'app_level' => $_POST['config_log_forward_app_level'] ?? '',
|
||||
// Only asked over TLS - kept as it was otherwise
|
||||
'tls_verify' => ($_POST['config_log_forward_protocol'] ?? '') === 'tls' ? ($_POST['config_log_forward_tls_verify'] ?? 0) : $was['tls_verify'],
|
||||
]);
|
||||
|
||||
// Nowhere to send to - saved, but off
|
||||
$warning = '';
|
||||
if ($posted_host !== '' && $forward['host'] === '') {
|
||||
$warning = "That isn't a server name or address, so sending stays off.";
|
||||
$forward['enabled'] = 0;
|
||||
} elseif ($forward['enabled'] && $forward['host'] === '') {
|
||||
$warning = 'Sending stays off until there is a server to send to.';
|
||||
$forward['enabled'] = 0;
|
||||
}
|
||||
|
||||
$host_sql = $forward['host'] !== '' ? "'" . escapeSql($forward['host']) . "'" : 'NULL';
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_log_retention = $audit_days, config_app_log_retention = $app_days,
|
||||
config_log_forward_enabled = {$forward['enabled']}, config_log_forward_host = $host_sql, config_log_forward_port = {$forward['port']},
|
||||
config_log_forward_protocol = '{$forward['protocol']}', config_log_forward_format = '{$forward['format']}', config_log_forward_audit = {$forward['audit']},
|
||||
config_log_forward_app_level = '{$forward['app_level']}', config_log_forward_tls_verify = {$forward['tls_verify']}
|
||||
WHERE company_id = 1");
|
||||
|
||||
// What changed, in words
|
||||
$changes = [];
|
||||
if ($audit_days !== $was_retention['audit']) {
|
||||
$changes[] = 'the audit log kept ' . strtolower(appLogRetentionText($audit_days)) . ' (was ' . strtolower(appLogRetentionText($was_retention['audit'])) . ')';
|
||||
}
|
||||
if ($app_days !== $was_retention['app']) {
|
||||
$changes[] = 'app logs kept ' . strtolower(appLogRetentionText($app_days)) . ' (was ' . strtolower(appLogRetentionText($was_retention['app'])) . ')';
|
||||
}
|
||||
$target = function ($settings) {
|
||||
return logForwardTarget($settings) . ($settings['protocol'] === 'tls' && !$settings['tls_verify'] ? ' (certificate not checked)' : '');
|
||||
};
|
||||
$what = function ($settings) {
|
||||
return implode(' and ', array_filter([$settings['audit'] ? 'the audit log' : '', $settings['app_level'] !== '' ? 'app ' . strtolower(logForwardAppLevels()[$settings['app_level']]) : ''])) ?: 'nothing';
|
||||
};
|
||||
$on = logForwardOn($forward);
|
||||
$was_on = logForwardOn($was);
|
||||
if ($on && !$was_on) {
|
||||
$changes[] = 'sending logs to ' . $target($forward) . ' as ' . LOG_FORWARD_FORMATS[$forward['format']] . ' - ' . $what($forward);
|
||||
} elseif (!$on && $was_on) {
|
||||
$changes[] = 'stopped sending logs (was to ' . $target($was) . ')';
|
||||
} elseif ($on) {
|
||||
if ($target($forward) !== $target($was)) {
|
||||
$changes[] = 'sending logs to ' . $target($forward) . ' (was ' . $target($was) . ')';
|
||||
}
|
||||
if ($forward['format'] !== $was['format']) {
|
||||
$changes[] = 'sent as ' . LOG_FORWARD_FORMATS[$forward['format']] . ' (was ' . LOG_FORWARD_FORMATS[$was['format']] . ')';
|
||||
}
|
||||
if ($what($forward) !== $what($was)) {
|
||||
$changes[] = 'sending ' . $what($forward) . ' (was ' . $what($was) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Read again, so what is logged from here on goes where it now should
|
||||
logForwardSettings(true);
|
||||
|
||||
if ($changes) {
|
||||
logAudit("Settings", "Edit", "$session_name edited log settings - " . escapeSql(implode('; ', $changes)));
|
||||
}
|
||||
|
||||
if (isset($_POST['test_log_forward'])) {
|
||||
if ($forward['host'] === '') {
|
||||
flashAlert($warning !== '' ? $warning : 'Give the server to send to first.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$test = logForwardTest($forward, stripslashes($session_name));
|
||||
$target = logForwardTarget($forward);
|
||||
if ($test['ok']) {
|
||||
flashAlert(($forward['protocol'] === 'udp'
|
||||
? "Saved, and a test message went to $target. UDP doesn't say whether it arrived - check the server for it."
|
||||
: "Saved, and $target took the test message.") . ($forward['enabled'] ? '' : ' Sending is still off - switch it on to send entries.'));
|
||||
} else {
|
||||
flashAlert("Saved, but the test message couldn't be sent to $target - " . $test['error'] . '.', 'error');
|
||||
}
|
||||
redirect();
|
||||
}
|
||||
|
||||
flashAlert($warning !== '' ? "Log settings saved. $warning" : 'Log settings saved', $warning !== '' ? 'warning' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
@@ -10,7 +10,6 @@ if (isset($_POST['edit_security_settings'])) {
|
||||
$config_login_key_required = intval($_POST['config_login_key_required'] ?? 0);
|
||||
$config_login_key_secret = escapeSql($_POST['config_login_key_secret']);
|
||||
$config_login_remember_me_expire = intval($_POST['config_login_remember_me_expire']);
|
||||
$config_log_retention = intval($_POST['config_log_retention']);
|
||||
|
||||
// Shared links (functions/share.php) - only the lengths the share window offers
|
||||
$config_share_max_hours = intval($_POST['config_share_max_hours'] ?? 720);
|
||||
@@ -30,7 +29,7 @@ if (isset($_POST['edit_security_settings'])) {
|
||||
$config_login_key_required = 0;
|
||||
}
|
||||
|
||||
mysqli_query($mysqli,"UPDATE settings SET config_login_message = '$config_login_message', config_login_key_required = '$config_login_key_required', config_login_key_secret = '$config_login_key_secret', config_login_remember_me_expire = $config_login_remember_me_expire, config_log_retention = $config_log_retention, config_share_max_hours = $config_share_max_hours, config_share_require_protection = $config_share_require_protection, config_login_alert_failures = $config_login_alert_failures WHERE company_id = 1");
|
||||
mysqli_query($mysqli,"UPDATE settings SET config_login_message = '$config_login_message', config_login_key_required = '$config_login_key_required', config_login_key_secret = '$config_login_key_secret', config_login_remember_me_expire = $config_login_remember_me_expire, config_share_max_hours = $config_share_max_hours, config_share_require_protection = $config_share_require_protection, config_login_alert_failures = $config_login_alert_failures WHERE company_id = 1");
|
||||
|
||||
logAudit("Settings", "Edit", "$session_name edited security settings");
|
||||
|
||||
|
||||
@@ -1,146 +1,108 @@
|
||||
<?php
|
||||
|
||||
// Vendor Templates
|
||||
/*
|
||||
* Vendor templates (Admin > Vendor Templates) - making, changing (and the
|
||||
* fields ticked on the vendors made from it too), copying, archiving,
|
||||
* restoring and deleting them. The work is in functions/vendor_template.php;
|
||||
* a vendor is made from one by add_vendor, add_vendor_from_template and
|
||||
* add_vendor_template_to_clients (agent/post/vendor.php).
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
// Import shared code from user-side vendor management as we reuse functions
|
||||
require_once '../agent/post/vendor.php';
|
||||
/* The template a request is for - or back to the list, saying it is gone. */
|
||||
function vendorTemplateForRequest($vendor_template_id) {
|
||||
$row = vendorTemplateInfo($vendor_template_id);
|
||||
if (!$row) {
|
||||
flashAlert('That vendor template no longer exists.', 'error');
|
||||
redirect('vendor_templates.php');
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
if (isset($_POST['add_vendor_template'])) {
|
||||
if (isset($_POST['add_vendor_template']) || isset($_POST['edit_vendor_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$name = escapeSql($_POST['name']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$account_number = escapeSql($_POST['account_number']);
|
||||
$contact_name = escapeSql($_POST['contact_name']);
|
||||
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['phone_country_code']);
|
||||
$phone = preg_replace("/[^0-9]/", '', $_POST['phone']);
|
||||
$extension = preg_replace("/[^0-9]/", '', $_POST['extension']);
|
||||
$email = escapeSql($_POST['email']);
|
||||
$website = preg_replace("(^https?://)", "", escapeSql($_POST['website']));
|
||||
$hours = escapeSql($_POST['hours']);
|
||||
$sla = escapeSql($_POST['sla']);
|
||||
$code = escapeSql($_POST['code']);
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$editing = isset($_POST['edit_vendor_template']);
|
||||
$before = $editing ? vendorTemplateForRequest($_POST['vendor_template_id'] ?? 0) : null;
|
||||
$vendor_template_id = $before ? intval($before['vendor_template_id']) : 0;
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone_country_code = '$phone_country_code', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes'");
|
||||
$fields = vendorTemplateFields($_POST, $error);
|
||||
if (!$fields) {
|
||||
flashAlert(escapeHtml($error), 'error');
|
||||
redirect();
|
||||
}
|
||||
$vendor_template_id = vendorTemplateSave($vendor_template_id, $fields);
|
||||
$name = $fields['name'];
|
||||
|
||||
$vendor_template_id = mysqli_insert_id($mysqli);
|
||||
if ($editing) {
|
||||
$changed = vendorTemplateChanged(vendorTemplateRowFields($before), $fields);
|
||||
$renamed = $before['vendor_template_name'] !== $name ? ' (was ' . $before['vendor_template_name'] . ')' : '';
|
||||
logAudit("Vendor Template", "Edit", "$session_name edited vendor template " . escapeSql($name . $renamed) . ($changed ? ' - ' . escapeSql(mb_strtolower(implode(', ', $changed))) : ''), 0, $vendor_template_id);
|
||||
|
||||
logAudit("Vendor Template", "Create", "$session_name created vendor template $name", 0, $vendor_template_id);
|
||||
// The fields ticked, on every vendor made from it too
|
||||
$labels = array_map(function ($field) {
|
||||
return $field['label'];
|
||||
}, vendorTemplateFieldLabels());
|
||||
$push = array_values(array_intersect(array_keys($labels), array_map('strval', array_filter((array) ($_POST['push'] ?? []), 'is_scalar'))));
|
||||
$pushed = $push ? vendorTemplatePush($vendor_template_id, $push) : [];
|
||||
$what = mb_strtolower(implode(', ', array_intersect_key($labels, array_flip($push))));
|
||||
foreach ($pushed as $vendor_id => $vendor) {
|
||||
logAudit("Vendor", "Edit", "$session_name updated vendor " . escapeSql($vendor['name']) . " from template " . escapeSql($name) . " - " . escapeSql($what), $vendor['client_id'], $vendor_id);
|
||||
}
|
||||
|
||||
flashAlert("Vendor template <strong>" . stripslashes($name) . "</strong> created");
|
||||
flashAlert("Vendor template <strong>" . escapeHtml($name) . "</strong> saved" . ($pushed ? ", and " . escapeHtml($what) . " changed on " . count($pushed) . " vendor" . (count($pushed) === 1 ? '' : 's') . " made from it" : ''));
|
||||
|
||||
redirect();
|
||||
redirect();
|
||||
}
|
||||
|
||||
// Made from one of a client's vendors - that vendor is now made from it
|
||||
$from_vendor = vendorRow($_POST['from_vendor'] ?? 0);
|
||||
if ($from_vendor) {
|
||||
mysqli_query($mysqli, "UPDATE vendors SET vendor_template_id = $vendor_template_id, vendor_updated_at = vendor_updated_at WHERE vendor_id = " . intval($from_vendor['vendor_id']));
|
||||
}
|
||||
|
||||
logAudit("Vendor Template", "Create", "$session_name created vendor template " . escapeSql($name) . ($from_vendor ? " from vendor " . escapeSql($from_vendor['vendor_name']) . ($from_vendor['client_name'] !== null ? " of " . escapeSql($from_vendor['client_name']) : '') : ''), 0, $vendor_template_id);
|
||||
|
||||
flashAlert("Vendor template <strong>" . escapeHtml($name) . "</strong> created");
|
||||
|
||||
// A new one opens on its page
|
||||
redirect("vendor_template.php?vendor_template_id=$vendor_template_id");
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_vendor_template'])) {
|
||||
if (isset($_GET['copy_vendor_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$vendor_template_id = intval($_POST['vendor_template_id']);
|
||||
$name = escapeSql($_POST['name']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$account_number = escapeSql($_POST['account_number']);
|
||||
$contact_name = escapeSql($_POST['contact_name']);
|
||||
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['phone_country_code']);
|
||||
$phone = preg_replace("/[^0-9]/", '', $_POST['phone']);
|
||||
$extension = preg_replace("/[^0-9]/", '', $_POST['extension']);
|
||||
$email = escapeSql($_POST['email']);
|
||||
$website = preg_replace("(^https?://)", "", escapeSql($_POST['website']));
|
||||
$hours = escapeSql($_POST['hours']);
|
||||
$sla = escapeSql($_POST['sla']);
|
||||
$code = escapeSql($_POST['code']);
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$row = vendorTemplateForRequest($_GET['copy_vendor_template']);
|
||||
$new_id = vendorTemplateCopy($row['vendor_template_id']);
|
||||
$new_name = (string) vendorTemplateInfo($new_id)['vendor_template_name'];
|
||||
|
||||
if ($_POST['global_update_vendor_name'] == 1 ?? 0) {
|
||||
$sql_global_update_vendor_name = ", vendor_name = '$name'";
|
||||
} else {
|
||||
$sql_global_update_vendor_name = "";
|
||||
}
|
||||
logAudit("Vendor Template", "Create", "$session_name copied vendor template " . escapeSql($row['vendor_template_name']) . " as " . escapeSql($new_name), 0, $new_id);
|
||||
|
||||
if ($_POST['global_update_vendor_description'] == 1) {
|
||||
$sql_global_update_vendor_description = ", vendor_description = '$description'";
|
||||
} else {
|
||||
$sql_global_update_vendor_description = "";
|
||||
}
|
||||
flashAlert("Vendor template <strong>" . escapeHtml($row['vendor_template_name']) . "</strong> copied as <strong>" . escapeHtml($new_name) . "</strong>");
|
||||
|
||||
if ($_POST['global_update_vendor_account_number'] == 1) {
|
||||
$sql_global_update_vendor_account_number = ", vendor_account_number = '$account_number'";
|
||||
} else {
|
||||
$sql_global_update_vendor_account_number = "";
|
||||
}
|
||||
redirect("vendor_template.php?vendor_template_id=$new_id");
|
||||
|
||||
if ($_POST['global_update_vendor_contact_name'] == 1) {
|
||||
$sql_global_update_vendor_contact_name = ", vendor_contact_name = '$contact_name'";
|
||||
} else {
|
||||
$sql_global_update_vendor_contact_name = "";
|
||||
}
|
||||
}
|
||||
|
||||
if ($_POST['global_update_vendor_phone'] == 1) {
|
||||
$sql_global_update_vendor_phone = ", vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension'";
|
||||
} else {
|
||||
$sql_global_update_vendor_phone = "";
|
||||
}
|
||||
if (isset($_GET['archive_vendor_template']) || isset($_GET['restore_vendor_template'])) {
|
||||
|
||||
if ($_POST['global_update_vendor_hours'] == 1) {
|
||||
$sql_global_update_vendor_hours = ", vendor_hours = '$hours'";
|
||||
} else {
|
||||
$sql_global_update_vendor_hours = "";
|
||||
}
|
||||
validateCSRFToken();
|
||||
|
||||
if ($_POST['global_update_vendor_email'] == 1) {
|
||||
$sql_global_update_vendor_email = ", vendor_email = '$email'";
|
||||
} else {
|
||||
$sql_global_update_vendor_email = "";
|
||||
}
|
||||
$archive = isset($_GET['archive_vendor_template']);
|
||||
$row = vendorTemplateForRequest($archive ? $_GET['archive_vendor_template'] : $_GET['restore_vendor_template']);
|
||||
$vendor_template_id = intval($row['vendor_template_id']);
|
||||
|
||||
if ($_POST['global_update_vendor_website'] == 1) {
|
||||
$sql_global_update_vendor_website = ", vendor_website = '$website'";
|
||||
} else {
|
||||
$sql_global_update_vendor_website = "";
|
||||
}
|
||||
// Archived, it isn't offered for new vendors; the ones made from it stay
|
||||
mysqli_query($mysqli, "UPDATE vendor_templates SET vendor_template_archived_at = " . ($archive ? 'NOW()' : 'NULL') . ", vendor_template_updated_at = vendor_template_updated_at
|
||||
WHERE vendor_template_id = $vendor_template_id");
|
||||
|
||||
if ($_POST['global_update_vendor_sla'] == 1) {
|
||||
$sql_global_update_vendor_sla = ", vendor_sla = '$sla'";
|
||||
} else {
|
||||
$sql_global_update_vendor_sla = "";
|
||||
}
|
||||
logAudit("Vendor Template", $archive ? "Archive" : "Restore", "$session_name " . ($archive ? 'archived' : 'restored') . " vendor template " . escapeSql($row['vendor_template_name']), 0, $vendor_template_id);
|
||||
|
||||
if ($_POST['global_update_vendor_code'] == 1) {
|
||||
$sql_global_update_vendor_code = ", vendor_code = '$code'";
|
||||
} else {
|
||||
$sql_global_update_vendor_code = "";
|
||||
}
|
||||
|
||||
if ($_POST['global_update_vendor_notes'] == 1) {
|
||||
$sql_global_update_vendor_notes = ", vendor_notes = '$notes'";
|
||||
} else {
|
||||
$sql_global_update_vendor_notes = "";
|
||||
}
|
||||
|
||||
// Update just the template
|
||||
mysqli_query($mysqli,"UPDATE vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone_country_code = '$phone_country_code', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes' WHERE vendor_template_id = $vendor_template_id");
|
||||
|
||||
if ($_POST['update_base_vendors'] == 1) {
|
||||
// Update client related vendors if anything is checked
|
||||
$sql = "$sql_global_update_vendor_name $sql_global_update_vendor_description $sql_global_update_vendor_account_number $sql_global_update_vendor_contact_name $sql_global_update_vendor_phone $sql_global_update_vendor_hours $sql_global_update_vendor_email $sql_global_update_vendor_website $sql_global_update_vendor_sla $sql_global_update_vendor_code $sql_global_update_vendor_notes";
|
||||
|
||||
// Remove the first comma to prevent MySQL error
|
||||
$sql = preg_replace('/,/', '', $sql, 1);
|
||||
|
||||
// Nothing ticked to push - nothing to update, rather than an empty SET
|
||||
if (trim($sql) !== '') {
|
||||
mysqli_query($mysqli,"UPDATE vendors SET $sql WHERE vendor_template_id = $vendor_template_id");
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Vendor Template", "Edit", "$session_name edited vendor template $name", 0, $vendor_template_id);
|
||||
|
||||
flashAlert("Vendor template <strong>" . stripslashes($name) . "</strong> saved");
|
||||
flashAlert("Vendor template <strong>" . escapeHtml($row['vendor_template_name']) . "</strong> " . ($archive ? 'archived' : 'restored'), $archive ? 'error' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -150,22 +112,16 @@ if (isset($_GET['delete_vendor_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$vendor_template_id = intval($_GET['delete_vendor_template']);
|
||||
$row = vendorTemplateForRequest($_GET['delete_vendor_template']);
|
||||
|
||||
$vendor_template_name = escapeSql(getFieldById('vendor_templates', $vendor_template_id, 'vendor_template_name'));
|
||||
// The vendors made from it stay, no longer linked to it
|
||||
vendorTemplateDelete($row['vendor_template_id']);
|
||||
|
||||
// If its a template reset all vendors based off this template to no template base
|
||||
mysqli_query($mysqli,"UPDATE vendors SET vendor_template_id = 0 WHERE vendor_template_id = $vendor_template_id");
|
||||
logAudit("Vendor Template", "Delete", "$session_name deleted vendor template " . escapeSql($row['vendor_template_name']), 0, intval($row['vendor_template_id']));
|
||||
|
||||
// License templates that took their vendor from it no longer have one
|
||||
mysqli_query($mysqli, "UPDATE software_templates SET software_template_vendor_template_id = 0, software_template_updated_at = software_template_updated_at WHERE software_template_vendor_template_id = $vendor_template_id");
|
||||
flashAlert("Vendor template <strong>" . escapeHtml($row['vendor_template_name']) . "</strong> deleted", 'error');
|
||||
|
||||
mysqli_query($mysqli,"DELETE FROM vendor_templates WHERE vendor_template_id = $vendor_template_id");
|
||||
|
||||
logAudit("Vendor Template", "Delete", "$session_name deleted vendor template $vendor_template_name");
|
||||
|
||||
flashAlert("Vendor template <strong>" . stripslashes($vendor_template_name) . "</strong> deleted", 'error');
|
||||
|
||||
redirect();
|
||||
// From its own page, back to the list
|
||||
redirect(!empty($_GET['from_page']) ? 'vendor_templates.php' : null);
|
||||
|
||||
}
|
||||
|
||||
239
admin/settings_logs.php
Normal file
239
admin/settings_logs.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Admin > Settings > Logs - how long the audit log (with API key use) and
|
||||
* the app logs are kept, each its own number of days or forever, and sending
|
||||
* their entries on to a syslog server or SIEM as they are written
|
||||
* (functions/log_forward.php). Saved by edit_log_settings, or
|
||||
* test_log_forward, which saves and sends a test message
|
||||
* (admin/post/settings_logs.php).
|
||||
*/
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// Read here rather than with every page's settings, so the page still opens before the update that adds them
|
||||
$retention = appLogRetention();
|
||||
$forward = logForwardSettings();
|
||||
$audit_size = appLogSize('logs');
|
||||
$app_size = appLogSize('app_logs');
|
||||
|
||||
// The last time sending failed, in the last day
|
||||
$forward_failure = null;
|
||||
if ($forward['enabled']) {
|
||||
$forward_failure = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT app_log_id, app_log_details, app_log_created_at FROM app_logs
|
||||
WHERE app_log_category = '" . escapeSql(LOG_FORWARD_CATEGORY) . "' AND app_log_type = 'error' AND app_log_created_at > NOW() - INTERVAL 1 DAY
|
||||
ORDER BY app_log_id DESC LIMIT 1"));
|
||||
}
|
||||
|
||||
// What an entry looks like as it is sent - the latest audit log entry, in the format picked
|
||||
$sample_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT log_id, log_type, log_action, log_description, log_ip, log_user_agent, log_created_at, log_client_id, log_user_id, log_entity_id,
|
||||
user_name, user_email, user_type, client_name
|
||||
FROM logs LEFT JOIN users ON user_id = log_user_id AND log_user_id > 0 LEFT JOIN clients ON client_id = log_client_id AND log_client_id > 0
|
||||
ORDER BY log_id DESC LIMIT 1"));
|
||||
$samples = [];
|
||||
if ($sample_row) {
|
||||
foreach (array_keys(LOG_FORWARD_FORMATS) as $format) {
|
||||
$samples[$format] = logForwardAuditMessage($sample_row, $format);
|
||||
}
|
||||
}
|
||||
|
||||
$size_line = function ($size, $url) {
|
||||
if (!$size['count']) {
|
||||
return 'Nothing in it yet.';
|
||||
}
|
||||
return '<a href="' . $url . '">' . number_format($size['count']) . ' entr' . ($size['count'] === 1 ? 'y' : 'ies') . '</a>, the oldest from ' . escapeHtml(date('M j, Y', strtotime($size['oldest']))) . '.';
|
||||
};
|
||||
|
||||
// The days field and its keep forever switch, for one log
|
||||
$retention_field = function ($name, $forever_name, $days, $label, $help) {
|
||||
$id = preg_replace('/[^a-z_]/', '', $name);
|
||||
ob_start(); ?>
|
||||
<div class="mb-3" data-log-retention="<?= $id ?>">
|
||||
<label class="form-label" for="<?= $id ?>"><?= $label ?></label>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-sm-6 col-lg-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-alt"></i></span>
|
||||
<input type="number" class="form-control" id="<?= $id ?>" name="<?= $name ?>" min="1" max="36500" placeholder="Days" value="<?= $days > 0 ? $days : '' ?>" <?= $days > 0 ? 'required' : 'disabled' ?>>
|
||||
<span class="input-group-text">days</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input type="checkbox" class="form-check-input" name="<?= $forever_name ?>" value="1" id="<?= $id ?>_forever" data-log-forever="<?= $id ?>" <?= $days > 0 ? '' : 'checked' ?>>
|
||||
<label class="form-check-label" for="<?= $id ?>_forever">Keep forever</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text"><?= $help ?></div>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-3">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-scroll me-2"></i>Logs</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="post.php" method="post" autocomplete="off" id="logSettingsForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<h4>Keeping logs</h4>
|
||||
<p class="text-secondary small">Each night, entries older than this are deleted for good. Keep the audit log as long as your clients or insurers ask you to be able to say who did what.</p>
|
||||
|
||||
<?= $retention_field('config_log_retention', 'audit_forever', $retention['audit'], 'Audit log', 'Who did what, signed in, looked at a password or exported something. How each API key is used is kept as long. ' . $size_line($audit_size, 'audit_logs.php')) ?>
|
||||
|
||||
<?= $retention_field('config_app_log_retention', 'app_forever', $retention['app'], 'App logs', 'What ITFlow itself does - cron runs, mail, payment providers - and the errors it runs into. They can also be cleared by hand. ' . $size_line($app_size, 'app_logs.php')) ?>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4 id="forwarding">Sending logs on</h4>
|
||||
<p class="text-secondary small">Each entry can also go to a syslog server or SIEM - Graylog, Splunk, Wazuh, Microsoft Sentinel and the like - as it is written, as a syslog message (RFC 5424, facility local0). A server that can't be reached doesn't slow ITFlow down: it is tried again a minute later, and the entries in between stay in ITFlow's own logs.</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="config_log_forward_enabled" value="1" id="logForwardEnabled" <?= $forward['enabled'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="logForwardEnabled">Send log entries to a syslog server or SIEM</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($forward_failure) { ?>
|
||||
<div class="alert alert-warning small" data-log-forward-failure>
|
||||
<i class="fas fa-fw fa-exclamation-triangle me-1"></i>Sending failed <?= escapeHtml(timeAgo($forward_failure['app_log_created_at'])) ?>: <?= escapeHtml($forward_failure['app_log_details']) ?>
|
||||
<a class="ms-1" href="app_logs.php?category=<?= urlencode(LOG_FORWARD_CATEGORY) ?>&open=<?= intval($forward_failure['app_log_id']) ?>">See it</a>
|
||||
</div>
|
||||
<?php } elseif (logForwardOn($forward)) { ?>
|
||||
<div class="alert alert-info small" data-log-forward-status>
|
||||
<i class="fas fa-fw fa-share-square me-1"></i>Sending to <?= escapeHtml(logForwardTarget($forward)) ?> as <?= escapeHtml(LOG_FORWARD_FORMATS[$forward['format']]) ?>.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div data-log-forward-fields>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Server</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
<input type="text" class="form-control" name="config_log_forward_host" maxlength="200" placeholder="logs.example.com or 10.0.0.20" value="<?= escapeHtml($forward['host']) ?>" pattern="\[?[A-Za-z0-9.:_\-]{1,200}\]?" title="A name or an IP address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<label class="form-label">Port</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
|
||||
<input type="number" class="form-control" name="config_log_forward_port" min="1" max="65535" value="<?= intval($forward['port']) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-8">
|
||||
<label class="form-label">Over</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-exchange-alt"></i></span>
|
||||
<select class="form-select select2" name="config_log_forward_protocol" id="logForwardProtocol">
|
||||
<?php foreach (LOG_FORWARD_PROTOCOLS as $key => $label) { ?>
|
||||
<option value="<?= $key ?>" <?= $forward['protocol'] === $key ? 'selected' : '' ?><?= selectOptionData(['description' => logForwardProtocolHelp()[$key]]) ?>><?= $label ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 <?= $forward['protocol'] === 'tls' ? '' : 'd-none' ?>" data-log-forward-tls>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="config_log_forward_tls_verify" value="1" id="logForwardVerify" <?= $forward['tls_verify'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="logForwardVerify">Check the server's certificate</label>
|
||||
</div>
|
||||
<div class="form-text">Turn off only for a server on your own network with a certificate it made itself - anyone in between could otherwise read the entries.</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">As</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-code"></i></span>
|
||||
<select class="form-select select2" name="config_log_forward_format" id="logForwardFormat">
|
||||
<?php foreach (LOG_FORWARD_FORMATS as $key => $label) { ?>
|
||||
<option value="<?= $key ?>" <?= $forward['format'] === $key ? 'selected' : '' ?><?= selectOptionData(['description' => logForwardFormatHelp()[$key]]) ?>><?= $label ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">App logs</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-clipboard-list"></i></span>
|
||||
<select class="form-select select2" name="config_log_forward_app_level">
|
||||
<?php foreach (logForwardAppLevels() as $key => $label) { ?>
|
||||
<option value="<?= $key ?>" <?= $forward['app_level'] === $key ? 'selected' : '' ?>><?= $label ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="config_log_forward_audit" value="1" id="logForwardAudit" <?= $forward['audit'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="logForwardAudit">The audit log - every entry</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($samples) { ?>
|
||||
<details class="mb-3" data-log-forward-sample>
|
||||
<summary class="small">What an entry looks like</summary>
|
||||
<div class="small text-body-secondary mt-2">The latest audit log entry, as it would be sent:</div>
|
||||
<?php foreach ($samples as $format => $sample) { ?>
|
||||
<pre class="small bg-body-tertiary border rounded p-2 mt-1 mb-0 text-wrap text-break <?= $format === $forward['format'] ? '' : 'd-none' ?>" data-log-forward-sample-format="<?= $format ?>"><?= escapeHtml($sample) ?></pre>
|
||||
<?php } ?>
|
||||
</details>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<button type="submit" name="edit_log_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
|
||||
<button type="submit" name="test_log_forward" class="btn btn-outline-secondary ms-2" title="Saves, then sends one test message to the server"><i class="fas fa-paper-plane me-2"></i>Save and send a test</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('logSettingsForm');
|
||||
|
||||
// Keep forever leaves the days alone, greyed out
|
||||
form.querySelectorAll('[data-log-forever]').forEach(function (toggle) {
|
||||
var days = document.getElementById(toggle.dataset.logForever);
|
||||
var sync = function () {
|
||||
days.disabled = toggle.checked;
|
||||
days.required = !toggle.checked;
|
||||
if (!toggle.checked && days.value === '') {
|
||||
days.value = 90;
|
||||
}
|
||||
};
|
||||
toggle.addEventListener('change', sync);
|
||||
});
|
||||
|
||||
// The certificate switch is for TLS, and the sample follows the format
|
||||
var protocol = document.getElementById('logForwardProtocol');
|
||||
var format = document.getElementById('logForwardFormat');
|
||||
var tls = form.querySelector('[data-log-forward-tls]');
|
||||
var follow = function () {
|
||||
tls.classList.toggle('d-none', protocol.value !== 'tls');
|
||||
form.querySelectorAll('[data-log-forward-sample-format]').forEach(function (pre) {
|
||||
pre.classList.toggle('d-none', pre.dataset.logForwardSampleFormat !== format.value);
|
||||
});
|
||||
};
|
||||
[protocol, format].forEach(function (select) {
|
||||
select.addEventListener('change', follow);
|
||||
if (window.jQuery) {
|
||||
window.jQuery(select).on('change', follow);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
@@ -42,14 +42,6 @@ $config_login_alert_failures = signInAlertLimit();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Log retention <small class="text-secondary">(The amount of days before app and audit logs, and API key use, are deleted during nightly cron)</small></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
|
||||
<input type="number" class="form-control" name="config_log_retention" placeholder="Enter days to retain" value="<?= intval($config_log_retention) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tell administrators about failed sign-ins <small class="text-secondary">(In <?= SIGN_IN_WINDOW_MINUTES ?> minutes, from one IP address or against one account - each admin gets a notification and an email. An address is turned away after <?= SIGN_IN_LOCKOUT ?>.)</small></label>
|
||||
<div class="input-group">
|
||||
|
||||
255
admin/vendor_template.php
Normal file
255
admin/vendor_template.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* One vendor template - what a vendor made from it starts with (its name,
|
||||
* what they are, the support line, hours, SLA, support email and site, any
|
||||
* account number, PIN and account manager every client shares, and its
|
||||
* notes), who made it and last changed it, and its use: the clients with a
|
||||
* vendor made from it - each with its own account number and PIN, or not yet
|
||||
* - and the license templates that take their vendor from it. It can be
|
||||
* added to clients from here. The work is in functions/vendor_template.php.
|
||||
*/
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$template = vendorTemplateInfo($_GET['vendor_template_id'] ?? 0);
|
||||
if (!$template) {
|
||||
echo emptyStateHtml(['thing' => 'vendor templates', 'title' => 'That vendor template is gone', 'text' => 'It may have been deleted.', 'icon' => 'fa-building', 'filtered' => false,
|
||||
'actions' => [['label' => 'Vendor Templates', 'href' => 'vendor_templates.php']]]);
|
||||
require_once "../includes/footer.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
$vendor_template_id = intval($template['vendor_template_id']);
|
||||
$name = escapeHtml($template['vendor_template_name']);
|
||||
// Tab title - used raw, the footer encodes it
|
||||
$page_title = $template['vendor_template_name'];
|
||||
$archived = !empty($template['vendor_template_archived_at']);
|
||||
$usage = vendorTemplateUsage([$vendor_template_id])[$vendor_template_id] ?? ['clients' => 0, 'vendors' => 0, 'ours' => 0, 'last' => null];
|
||||
$csrf = $_SESSION['csrf_token'];
|
||||
|
||||
// Its vendors - the current ones for current clients listed, the rest counted
|
||||
$all_vendors = vendorTemplateVendors($vendor_template_id);
|
||||
$vendors = array_filter($all_vendors, function ($vendor) {
|
||||
return !$vendor['archived'] && !$vendor['client_archived'];
|
||||
});
|
||||
$set_up = count(array_filter($vendors, function ($vendor) {
|
||||
return $vendor['client_id'] && $vendor['account_number'] !== '';
|
||||
}));
|
||||
$client_vendors = count(array_filter($vendors, function ($vendor) {
|
||||
return $vendor['client_id'] > 0;
|
||||
}));
|
||||
$others = count($all_vendors) - count($vendors);
|
||||
$license_templates = vendorTemplateLicenseTemplates($vendor_template_id);
|
||||
|
||||
$edit_url = "modals/vendor_template/vendor_template_edit.php?id=$vendor_template_id";
|
||||
$clients_url = "/agent/modals/vendor/vendor_template_clients.php?template_id=$vendor_template_id";
|
||||
|
||||
// What a vendor from it starts with - the ones it has
|
||||
$t = $template;
|
||||
$website = vendorWebsiteLabel($t['vendor_template_website']);
|
||||
$facts = array_filter([
|
||||
'What they are' => escapeHtml((string) $t['vendor_template_description']),
|
||||
'Support line' => vendorTemplatePhoneHtml($t['vendor_template_phone'], $t['vendor_template_phone_country_code'], $t['vendor_template_extension']),
|
||||
'Support hours' => escapeHtml((string) $t['vendor_template_hours']),
|
||||
'SLA' => escapeHtml((string) $t['vendor_template_sla']),
|
||||
'Support email' => trim((string) $t['vendor_template_email']) !== '' ? '<a href="mailto:' . escapeHtml($t['vendor_template_email']) . '">' . escapeHtml($t['vendor_template_email']) . '</a>' : '',
|
||||
'Support site' => $website !== '' ? '<a href="' . escapeHtml(vendorWebsiteUrl($t['vendor_template_website'])) . '" target="_blank" rel="noopener noreferrer">' . escapeHtml($website) . '<i class="fas fa-external-link-alt small ms-1"></i></a>' : '',
|
||||
'Account number' => escapeHtml(trim((string) $t['vendor_template_account_number'])),
|
||||
'PIN' => vendorSecretHtml($t['vendor_template_code']),
|
||||
'Account manager' => escapeHtml(trim((string) $t['vendor_template_contact_name'])),
|
||||
], function ($value) {
|
||||
return trim($value) !== '';
|
||||
});
|
||||
$icons = [];
|
||||
foreach (vendorTemplateFieldLabels() as $field) {
|
||||
$icons[$field['label']] = $field['icon'];
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item"><a href="../index.php">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="users.php">Admin</a></li>
|
||||
<li class="breadcrumb-item"><a href="vendor_templates.php">Vendor Templates</a></li>
|
||||
<li class="breadcrumb-item active"><?= $name ?></li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-3">
|
||||
<?= vendorTemplateTile($template['vendor_template_name'], 'lg') ?>
|
||||
<div class="min-w-0 flex-grow-1">
|
||||
<h1 class="h4 mb-0"><?= $name ?></h1>
|
||||
<?php if (trim((string) $template['vendor_template_description']) !== '') { ?>
|
||||
<div class="text-secondary"><?= escapeHtml($template['vendor_template_description']) ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($archived) { ?>
|
||||
<div class="small text-secondary"><i class="fas fa-fw fa-archive me-1"></i>Archived <?= date('M j, Y', strtotime($template['vendor_template_archived_at'])) ?> - not offered for new vendors</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 d-print-none">
|
||||
<?php if (!$archived) { ?>
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="<?= $clients_url ?>"><i class="fas fa-fw fa-users me-2"></i>Add to clients</button>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="<?= $edit_url ?>"><i class="fas fa-fw fa-edit me-2"></i>Edit</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-h"></i></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href="post.php?copy_vendor_template=<?= $vendor_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-copy me-2"></i>Copy</a>
|
||||
<?php if ($archived) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_vendor_template=<?= $vendor_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-redo me-2"></i>Restore</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item" href="post.php?archive_vendor_template=<?= $vendor_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-archive me-2"></i>Archive</a>
|
||||
<?php } ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_vendor_template=<?= $vendor_template_id ?>&from_page=1&csrf_token=<?= $csrf ?>"
|
||||
data-confirm-title="Delete <?= $name ?>?" data-confirm-text="The vendors made from it stay as they are." data-confirm-button="Delete"><i class="fas fa-fw fa-trash me-2"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-building me-2"></i>What a vendor starts with</h3>
|
||||
<button type="button" class="btn btn-sm btn-link ms-auto ajax-modal" data-modal-size="lg" data-modal-url="<?= $edit_url ?>"><i class="fas fa-edit me-1"></i>Edit</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0" data-vendor-template-facts>
|
||||
<dt class="col-sm-3 fw-normal text-secondary"><i class="fas fa-fw fa-building me-1"></i>Name</dt>
|
||||
<dd class="col-sm-9"><?= $name ?></dd>
|
||||
<?php foreach ($facts as $label => $value) { ?>
|
||||
<dt class="col-sm-3 fw-normal text-secondary"><i class="fas fa-fw <?= $icons[$label] ?? 'fa-circle' ?> me-1"></i><?= $label ?></dt>
|
||||
<dd class="col-sm-9"><?= $value ?></dd>
|
||||
<?php } ?>
|
||||
<dt class="col-sm-3 fw-normal text-secondary"><i class="fas fa-fw fa-sticky-note me-1"></i>Notes</dt>
|
||||
<dd class="col-sm-9 mb-0">
|
||||
<?php if (trim((string) $template['vendor_template_notes']) !== '') { ?>
|
||||
<div style="white-space: pre-wrap;"><?= escapeHtml($template['vendor_template_notes']) ?></div>
|
||||
<?php } else { ?>
|
||||
<span class="text-secondary">None - what every client's vendor should say: the escalation path, what they ask for before they help, how to open a case online.</span>
|
||||
<?php } ?>
|
||||
</dd>
|
||||
</dl>
|
||||
<?php if (!isset($facts['Support line']) && !isset($facts['Support site']) && !isset($facts['Support email'])) { ?>
|
||||
<div class="small text-warning-emphasis mt-3"><i class="fas fa-fw fa-exclamation-triangle me-1"></i>No way to reach their support yet - give it their support line, email or site, so every client's vendor has it.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3" id="vendorTemplateVendors">
|
||||
<div class="card-header d-flex flex-wrap align-items-center gap-2">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-users me-2"></i>Clients with it</h3>
|
||||
<?php if (!$archived) { ?>
|
||||
<button type="button" class="btn btn-sm btn-link ms-auto ajax-modal" data-modal-url="<?= $clients_url ?>"><i class="fas fa-plus me-1"></i>Add to clients</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($vendors) { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-nowrap">
|
||||
<tr><th class="ps-3">Client</th><th>Their vendor</th><th class="d-none d-sm-table-cell">Account</th><th class="d-none d-md-table-cell">Added</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($vendors as $vendor_id => $vendor) {
|
||||
$client_id = $vendor['client_id'];
|
||||
$vendor_url = '/agent/vendor.php?' . ($client_id ? "client_id=$client_id&" : '') . "vendor_id=$vendor_id";
|
||||
?>
|
||||
<tr data-vendor-id="<?= $vendor_id ?>">
|
||||
<td class="ps-3">
|
||||
<?php if ($client_id) { ?>
|
||||
<a class="fw-semibold text-body" href="/agent/vendors.php?client_id=<?= $client_id ?>"><?= escapeHtml($vendor['client_name']) ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary"><i class="fas fa-fw fa-briefcase me-1"></i>Ours</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><a class="text-reset" href="<?= $vendor_url ?>"><?= escapeHtml($vendor['vendor_name']) ?></a></td>
|
||||
<td class="d-none d-sm-table-cell small">
|
||||
<?php if ($vendor['account_number'] !== '') { ?>
|
||||
<span class="font-monospace"><?= escapeHtml($vendor['account_number']) ?></span>
|
||||
<?php if ($vendor['code'] !== '') { ?><span class="text-body-secondary ms-1" title="Has a PIN"><i class="fas fa-key"></i></span><?php } ?>
|
||||
<?php } elseif ($client_id) { ?>
|
||||
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle fw-normal" title="Add the client's account number on their vendor, so support can find them" data-no-account>No account number</span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell small text-body-secondary text-nowrap" title="<?= escapeHtml($vendor['created_at']) ?>"><?= date('M j, Y', strtotime($vendor['created_at'])) ?></td>
|
||||
<td class="text-end pe-3"><a class="btn btn-sm btn-light" href="<?= $vendor_url ?>" title="Open their vendor"><i class="fas fa-fw fa-arrow-right"></i></a></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($others) { ?>
|
||||
<div class="card-footer small text-secondary">And <?= $others ?> archived, or for an archived client.</div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<div class="card-body small text-secondary">No client has a vendor made from it yet<?= $others ? " - $others archived, or for an archived client" : '' ?>. Agents add one from <strong>New Vendor</strong> on a client's Vendors<?= $archived ? '' : ', or add it to several clients from here' ?>.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 d-print-none">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title mb-0"><i class="fas fa-fw fa-chart-line me-2"></i>Used</h3></div>
|
||||
<div class="card-body" data-vendor-template-used>
|
||||
<?php if ($usage['clients'] || $usage['ours']) { ?>
|
||||
<div class="mb-2">
|
||||
<?php if ($usage['clients']) { ?><strong><?= $usage['clients'] ?></strong> client<?= $usage['clients'] === 1 ? ' has' : 's have' ?> a vendor from it<?php } ?>
|
||||
<?php if ($usage['ours']) { ?><?= $usage['clients'] ? ', and it is one of ours' : 'It is one of our own vendors' ?><?php } ?>
|
||||
<span class="small text-secondary">- last added <?= timeAgo($usage['last']) ?></span>
|
||||
</div>
|
||||
<?php if ($client_vendors) { ?>
|
||||
<div class="small <?= $set_up < $client_vendors ? 'text-warning-emphasis' : 'text-success' ?>"><i class="fas fa-fw fa-fingerprint me-1"></i><?= $set_up < $client_vendors ? ($client_vendors - $set_up) . ' without the client\'s account number' : 'Every one has the client\'s account number' ?></div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<div class="small text-secondary">No vendors made from it yet.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($license_templates) { ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title mb-0"><i class="fas fa-fw fa-cube me-2"></i>License templates</h3></div>
|
||||
<div class="list-group list-group-flush small" data-vendor-template-licenses>
|
||||
<?php foreach ($license_templates as $software_template_id => $license_template) { ?>
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2" href="software_template.php?software_template_id=<?= $software_template_id ?>">
|
||||
<i class="fas fa-fw fa-cube text-secondary"></i>
|
||||
<span class="min-w-0 text-truncate <?= $license_template['archived'] ? 'text-body-secondary' : '' ?>"><?= escapeHtml($license_template['name']) ?></span>
|
||||
<?php if ($license_template['archived']) { ?><span class="badge text-bg-light border fw-normal ms-auto">Archived</span><?php } ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="card-footer small text-secondary">Licenses made from these get the client's vendor from this template.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title mb-0"><i class="fas fa-fw fa-info-circle me-2"></i>Details</h3></div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 small">
|
||||
<dt class="col-5 fw-normal text-secondary">Made</dt>
|
||||
<dd class="col-7 mb-2" title="<?= escapeHtml($template['vendor_template_created_at']) ?>"><?= date('M j, Y', strtotime($template['vendor_template_created_at'])) ?><?= $template['created_by_name'] !== null ? '<div class="text-secondary">' . escapeHtml($template['created_by_name']) . '</div>' : '' ?></dd>
|
||||
<?php if ($template['vendor_template_updated_at']) { ?>
|
||||
<dt class="col-5 fw-normal text-secondary">Last changed</dt>
|
||||
<dd class="col-7 mb-0" title="<?= escapeHtml($template['vendor_template_updated_at']) ?>"><?= timeAgo($template['vendor_template_updated_at']) ?><?= $template['updated_by_name'] !== null ? '<div class="text-secondary">' . escapeHtml($template['updated_by_name']) . '</div>' : '' ?></dd>
|
||||
<?php } ?>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History, from the audit log -->
|
||||
<?= auditLogHistoryCard(auditLogHistory('Vendor Template', $vendor_template_id, 10)) ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
@@ -1,181 +1,235 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Admin > Vendor Templates - the vendors an MSP's clients use again and
|
||||
* again, set up once: the ISPs, the hardware makers, the distributors, the
|
||||
* line-of-business software. Each row shows what it is, its support line,
|
||||
* hours and SLA, how many clients have a vendor made from it, when one was
|
||||
* last added and when it last changed, and by whom. Each opens its page
|
||||
* (vendor_template.php). A template can be added to several clients at once,
|
||||
* changed, copied as the start of another, archived - not offered for new
|
||||
* vendors, while the vendors made from it stay - or deleted. The work is in
|
||||
* functions/vendor_template.php; the handlers are
|
||||
* admin/post/vendor_template.php.
|
||||
*/
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "vendor_template_name";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS vendor_template_account_number, vendor_template_code, vendor_template_contact_name,
|
||||
vendor_template_description, vendor_template_email, vendor_template_extension,
|
||||
vendor_template_hours, vendor_template_id, vendor_template_name, vendor_template_notes,
|
||||
vendor_template_phone, vendor_template_sla, vendor_template_website FROM vendor_templates
|
||||
WHERE vendor_template_name LIKE '%$q%' OR vendor_template_description LIKE '%$q%' OR vendor_template_account_number LIKE '%$q%' OR vendor_template_website LIKE '%$q%' OR vendor_template_contact_name LIKE '%$q%' OR vendor_template_email LIKE '%$q%' OR vendor_template_phone LIKE '%$phone_query%' ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
// '' current, 'archived'
|
||||
$show_archived = ($_GET['show'] ?? '') === 'archived' || ($_GET['archived'] ?? '') === '1';
|
||||
$show_where = $show_archived ? 'vendor_template_archived_at IS NOT NULL' : 'vendor_template_archived_at IS NULL';
|
||||
|
||||
// Tab title - the file is named for the table, the page for what it holds
|
||||
$page_title = 'Vendor Templates';
|
||||
|
||||
// Only these sort - an unknown column would take the page down
|
||||
$sortable_columns = [
|
||||
'vendor_template_name' => "vendor_template_name $order",
|
||||
'used_count' => "used_count $order, vendor_template_name ASC",
|
||||
'last_used' => "last_used IS NULL, last_used $order, vendor_template_name ASC",
|
||||
'changed_at' => "changed_at $order, vendor_template_name ASC",
|
||||
];
|
||||
if (!isset($sortable_columns[$sort])) {
|
||||
$sort = 'vendor_template_name';
|
||||
}
|
||||
|
||||
$phone_search = preg_replace('/[^0-9]/', '', (string) ($_GET['q'] ?? ''));
|
||||
$search = "(vendor_template_name LIKE '%$q%' OR vendor_template_description LIKE '%$q%' OR vendor_template_website LIKE '%$q%' OR vendor_template_email LIKE '%$q%'
|
||||
OR vendor_template_hours LIKE '%$q%' OR vendor_template_sla LIKE '%$q%' OR vendor_template_contact_name LIKE '%$q%' OR vendor_template_account_number LIKE '%$q%'
|
||||
OR vendor_template_notes LIKE '%$q%'" . ($phone_search !== '' ? " OR vendor_template_phone LIKE '%$phone_search%'" : '') . ")";
|
||||
|
||||
// Its use is the current vendors made from it for current clients - as vendorTemplateUsage() counts them
|
||||
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS vendor_templates.vendor_template_id, vendor_template_name, vendor_template_description, vendor_template_phone_country_code,
|
||||
vendor_template_phone, vendor_template_extension, vendor_template_email, vendor_template_website, vendor_template_hours, vendor_template_sla,
|
||||
vendor_template_created_at, vendor_template_updated_at, vendor_template_archived_at,
|
||||
COALESCE(vendor_template_updated_at, vendor_template_created_at) AS changed_at, COALESCE(updater.user_name, creator.user_name) AS changed_by,
|
||||
COALESCE(used.used_count, 0) AS used_count, used.last_used
|
||||
FROM vendor_templates
|
||||
LEFT JOIN users creator ON creator.user_id = vendor_template_created_by AND vendor_template_created_by > 0
|
||||
LEFT JOIN users updater ON updater.user_id = vendor_template_updated_by AND vendor_template_updated_by > 0
|
||||
LEFT JOIN (
|
||||
SELECT vendor_template_id AS used_template_id, COUNT(DISTINCT vendor_client_id) AS used_count, MAX(vendor_created_at) AS last_used
|
||||
FROM vendors JOIN clients ON client_id = vendor_client_id AND client_archived_at IS NULL
|
||||
WHERE vendor_template_id > 0 AND vendor_archived_at IS NULL GROUP BY vendor_template_id
|
||||
) used ON used.used_template_id = vendor_templates.vendor_template_id
|
||||
WHERE $search AND $show_where
|
||||
ORDER BY {$sortable_columns[$sort]}, vendor_templates.vendor_template_id ASC
|
||||
LIMIT $record_from, $record_to");
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
// How many clients have a vendor made from each template
|
||||
$template_use = [];
|
||||
$sql_template_use = mysqli_query($mysqli, "SELECT vendor_template_id, COUNT(DISTINCT vendor_client_id) FROM vendors WHERE vendor_template_id > 0 AND vendor_client_id > 0 AND vendor_archived_at IS NULL GROUP BY vendor_template_id");
|
||||
while ($use_row = mysqli_fetch_row($sql_template_use)) {
|
||||
$template_use[intval($use_row[0])] = intval($use_row[1]);
|
||||
$rows = [];
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$rows[intval($row['vendor_template_id'])] = $row;
|
||||
}
|
||||
$usage = vendorTemplateUsage(array_keys($rows));
|
||||
|
||||
$counts = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COALESCE(SUM(vendor_template_archived_at IS NULL), 0), COALESCE(SUM(vendor_template_archived_at IS NOT NULL), 0)
|
||||
FROM vendor_templates WHERE $search"));
|
||||
|
||||
$sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) {
|
||||
return '<a class="text-reset text-decoration-none" href="?' . escapeHtml($url_query_strings_sort) . '&sort=' . $column . '&order=' . $disp . '">'
|
||||
. $label . ($sort === $column ? " $order_icon" : '') . '</a>';
|
||||
};
|
||||
$pill_url = function ($show) {
|
||||
return '?' . http_build_query(array_filter(['q' => $_GET['q'] ?? '', 'show' => $show]));
|
||||
};
|
||||
|
||||
$add_url = 'modals/vendor_template/vendor_template_add.php';
|
||||
$clients_url = '/agent/modals/vendor/vendor_template_clients.php';
|
||||
$csrf = $_SESSION['csrf_token'];
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2">
|
||||
<i class="fas fa-fw fa-building me-2"></i>Vendor Templates
|
||||
</h3>
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-building me-2"></i>Vendor Templates</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/vendor_template/vendor_template_add.php">
|
||||
<i class="fas fa-plus me-2"></i>New Vendor Template
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $add_url ?>"><i class="fas fa-plus"></i><span class="d-none d-lg-inline ms-2">New Vendor Template</span></button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More"></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $clients_url ?>">
|
||||
<i class="fas fa-fw fa-users text-secondary me-2"></i>Add one to clients
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="starter_content.php">
|
||||
<i class="fas fa-fw fa-seedling text-secondary me-2"></i>Starter vendor templates
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($show_archived) { ?><input type="hidden" name="show" value="archived"><?php } ?>
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-lg-5 col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Vendors Templates">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search vendor templates..." title="Name, what they are, support line, email, site, hours, SLA, account manager or notes">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<nav class="nav nav-pills itflow-asset-tabs mt-2">
|
||||
<a class="nav-link <?= !$show_archived ? 'active' : '' ?>" href="<?= escapeHtml($pill_url('')) ?>"><i class="fas fa-fw fa-building me-1"></i>Active <span class="badge rounded-pill ms-1"><?= intval($counts[0]) ?></span></a>
|
||||
<a class="nav-link <?= $show_archived ? 'active' : '' ?>" href="<?= escapeHtml($pill_url('archived')) ?>" title="Not offered for new vendors - the vendors made from them stay"><i class="fas fa-fw fa-archive me-1"></i>Archived <span class="badge rounded-pill ms-1"><?= intval($counts[1]) ?></span></a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-nowrap <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_template_name&order=<?= $disp ?>">
|
||||
Vendor <?php if ($sort == 'vendor_template_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_template_description&order=<?= $disp ?>">
|
||||
Description <?php if ($sort == 'vendor_template_description') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>Contact</th>
|
||||
<th>Used by</th>
|
||||
<th class="text-center">Action</th>
|
||||
<th class="ps-3"><?= $sort_link('vendor_template_name', 'Template') ?></th>
|
||||
<th class="d-none d-lg-table-cell">Support</th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('used_count', 'Used') ?></th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('last_used', 'Last used') ?></th>
|
||||
<th class="d-none d-lg-table-cell"><?= $sort_link('changed_at', 'Changed') ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$vendor_template_id = intval($row['vendor_template_id']);
|
||||
$vendor_template_name = escapeHtml($row['vendor_template_name']);
|
||||
$vendor_template_description = escapeHtml($row['vendor_template_description']);
|
||||
if (empty($vendor_template_description)) {
|
||||
$vendor_template_description_display = "-";
|
||||
} else {
|
||||
$vendor_template_description_display = $vendor_template_description;
|
||||
<?php foreach ($rows as $template_id => $row) {
|
||||
$name = escapeHtml($row['vendor_template_name']);
|
||||
$use = $usage[$template_id] ?? null;
|
||||
$used_clients = $use['clients'] ?? 0;
|
||||
$used_text = $used_clients ? $used_clients . ' client' . ($used_clients === 1 ? '' : 's') : '';
|
||||
if ($use && $use['ours']) {
|
||||
$used_text = trim($used_text . ($used_text !== '' ? ' and ours' : 'Ours'));
|
||||
}
|
||||
$vendor_template_account_number = escapeHtml($row['vendor_template_account_number']);
|
||||
$vendor_template_contact_name = escapeHtml($row['vendor_template_contact_name']);
|
||||
if (empty($vendor_template_contact_name)) {
|
||||
$vendor_template_contact_name_display = "-";
|
||||
} else {
|
||||
$vendor_template_contact_name_display = $vendor_template_contact_name;
|
||||
}
|
||||
$vendor_template_phone = formatPhoneNumber($row['vendor_template_phone']);
|
||||
$vendor_template_extension = escapeHtml($row['vendor_template_extension']);
|
||||
$vendor_template_email = escapeHtml($row['vendor_template_email']);
|
||||
$vendor_template_website = escapeHtml($row['vendor_template_website']);
|
||||
$vendor_template_hours = escapeHtml($row['vendor_template_hours']);
|
||||
$vendor_template_sla = escapeHtml($row['vendor_template_sla']);
|
||||
$vendor_template_code = escapeHtml($row['vendor_template_code']);
|
||||
$vendor_template_notes = escapeHtml($row['vendor_template_notes']);
|
||||
|
||||
$summary = trim((string) $row['vendor_template_description']);
|
||||
$page_url = "vendor_template.php?vendor_template_id=$template_id";
|
||||
$phone = vendorTemplatePhoneHtml($row['vendor_template_phone'], $row['vendor_template_phone_country_code'], $row['vendor_template_extension']);
|
||||
$website = vendorWebsiteLabel($row['vendor_template_website']);
|
||||
// How to reach their support
|
||||
$support = array_filter([
|
||||
$phone !== '' ? '<i class="fas fa-fw fa-phone text-secondary me-1"></i>' . $phone : '',
|
||||
trim((string) $row['vendor_template_hours']) !== '' ? '<i class="fas fa-fw fa-clock text-secondary me-1"></i>' . escapeHtml($row['vendor_template_hours']) : '',
|
||||
trim((string) $row['vendor_template_sla']) !== '' ? '<i class="fas fa-fw fa-handshake text-secondary me-1"></i>' . escapeHtml($row['vendor_template_sla']) : '',
|
||||
]);
|
||||
?>
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/vendor_template/vendor_template_edit.php?id=<?= $vendor_template_id ?>">
|
||||
<span class="d-inline-flex align-items-center gap-2"><?= vendorTile($row['vendor_template_name'], 'sm') ?><?= $vendor_template_name ?></span>
|
||||
</a>
|
||||
<?php
|
||||
if (!empty($vendor_template_account_number)) {
|
||||
?>
|
||||
<br>
|
||||
<small class="text-secondary"><?= $vendor_template_account_number ?></small>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</th>
|
||||
<td><?= $vendor_template_description_display ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if (!empty($vendor_template_contact_name)) {
|
||||
?>
|
||||
<i class="fa fa-fw fa-user text-secondary me-2 mb-2"></i><?= $vendor_template_contact_name_display ?>
|
||||
<br>
|
||||
<?php
|
||||
} else {
|
||||
echo $vendor_template_contact_name_display;
|
||||
}
|
||||
|
||||
if (!empty($vendor_template_phone)) { ?>
|
||||
<i class="fa fa-fw fa-phone text-secondary me-2 mb-2"></i><?= $vendor_template_phone ?>
|
||||
<br>
|
||||
<?php }
|
||||
|
||||
if (!empty($vendor_template_email)) { ?>
|
||||
<i class="fa fa-fw fa-envelope text-secondary me-2 mb-2"></i><?= $vendor_template_email ?>
|
||||
<br>
|
||||
<?php } ?>
|
||||
|
||||
<tr data-vendor-template-id="<?= $template_id ?>">
|
||||
<td class="ps-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="d-none d-sm-inline-flex"><?= vendorTemplateTile($row['vendor_template_name']) ?></span>
|
||||
<div class="min-w-0">
|
||||
<a class="fw-semibold text-body" href="<?= $page_url ?>"><?= $name ?></a>
|
||||
<?php if ($summary !== '') { ?>
|
||||
<div class="small text-body-secondary"><?= escapeHtml($summary) ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($website !== '' || trim((string) $row['vendor_template_email']) !== '') { ?>
|
||||
<div class="small text-body-secondary d-flex flex-wrap column-gap-3">
|
||||
<?php if ($website !== '') { ?><a class="text-reset text-nowrap" href="<?= escapeHtml(vendorWebsiteUrl($row['vendor_template_website'])) ?>" target="_blank" rel="noopener noreferrer"><i class="fas fa-fw fa-globe me-1"></i><?= escapeHtml($website) ?></a><?php } ?>
|
||||
<?php if (trim((string) $row['vendor_template_email']) !== '') { ?><span class="text-nowrap"><i class="fas fa-fw fa-envelope me-1"></i><?= escapeHtml($row['vendor_template_email']) ?></span><?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($row['vendor_template_archived_at']) { ?>
|
||||
<div class="small text-body-secondary"><i class="fas fa-fw fa-archive me-1"></i>Archived <?= date('M j, Y', strtotime($row['vendor_template_archived_at'])) ?></div>
|
||||
<?php } ?>
|
||||
<!-- On smaller screens its support line and use come along under the name -->
|
||||
<?php if ($support) { ?><div class="d-lg-none small text-body-secondary mt-1"><?= reset($support) ?></div><?php } ?>
|
||||
<div class="d-md-none small text-body-secondary"><?= $used_text !== '' ? escapeHtml($used_text) . ' · last ' . timeAgo($use['last']) : 'Not used yet' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php $used_by = $template_use[$vendor_template_id] ?? 0; ?>
|
||||
<?php if ($used_by) { ?>
|
||||
<a href="/agent/vendors.php?view=clients&q=<?= urlencode($row['vendor_template_name']) ?>"><?= $used_by ?> client<?= $used_by === 1 ? '' : 's' ?></a>
|
||||
<td class="d-none d-lg-table-cell small">
|
||||
<?php if ($support) { ?>
|
||||
<?php foreach ($support as $line) { ?><div class="text-nowrap"><?= $line ?></div><?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-secondary">-</span>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<td class="d-none d-md-table-cell small text-nowrap" data-used="<?= $used_clients ?>">
|
||||
<?php if ($used_text !== '') { ?>
|
||||
<a class="text-reset" href="<?= $page_url ?>#vendorTemplateVendors"><?= escapeHtml($used_text) ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">Not yet</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell small text-body-secondary text-nowrap"<?= $row['last_used'] ? ' title="' . escapeHtml($row['last_used']) . '"' : '' ?>><?= $row['last_used'] ? timeAgo($row['last_used']) : '-' ?></td>
|
||||
<td class="d-none d-lg-table-cell small text-body-secondary text-nowrap" title="<?= escapeHtml($row['changed_at']) ?>">
|
||||
<?= timeAgo($row['changed_at']) ?>
|
||||
<?php if ($row['changed_by'] !== null) { ?><div><?= escapeHtml($row['changed_by']) ?></div><?php } ?>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/vendor_template/vendor_template_edit.php?id=<?= $vendor_template_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/vendor_template/vendor_template_edit.php?id=<?= $template_id ?>">
|
||||
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="/agent/modals/vendor/vendor_template_clients.php?template_id=<?= $vendor_template_id ?>">
|
||||
<i class="fas fa-fw fa-users me-2"></i>Add to clients
|
||||
</a>
|
||||
<?php if ($session_user_role == 3) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor_template=<?= $vendor_template_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
<?php if (!$row['vendor_template_archived_at']) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $clients_url ?>?template_id=<?= $template_id ?>">
|
||||
<i class="fas fa-fw fa-users text-secondary me-2"></i>Add to clients
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item" href="post.php?copy_vendor_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>" title="A new template starting from this one">
|
||||
<i class="fas fa-fw fa-copy text-secondary me-2"></i>Copy
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if ($row['vendor_template_archived_at']) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_vendor_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>">
|
||||
<i class="fas fa-fw fa-box-open text-secondary me-2"></i>Restore
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item text-danger" href="post.php?archive_vendor_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>" title="Not offered for new vendors - the vendors made from it stay">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>"
|
||||
data-confirm-title="Delete <?= $name ?>?" data-confirm-text="The vendors made from it stay as they are." data-confirm-button="Delete">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -183,11 +237,14 @@ while ($use_row = mysqli_fetch_row($sql_template_use)) {
|
||||
$empty_state = [
|
||||
'thing' => 'vendor templates',
|
||||
'icon' => 'fa-building',
|
||||
'text' => 'A vendor set up once - its support numbers and site - to add to any client.',
|
||||
'actions' => [['label' => 'New Vendor Template', 'modal' => 'modals/vendor_template/vendor_template_add.php']],
|
||||
'filtered' => $q !== '',
|
||||
'archived' => $show_archived,
|
||||
'views' => ['show'],
|
||||
'text' => 'A vendor your clients use again and again - an ISP, a hardware maker, the software they run - set up once with its support line, hours and site, ready to add to any client. Agents start one from <strong>New Vendor</strong> on a client\'s Vendors.',
|
||||
'actions' => [['label' => 'New Vendor Template', 'modal' => $add_url, 'size' => 'lg'], ['label' => 'Starter vendor templates', 'href' => 'starter_content.php', 'icon' => 'fa-seedling']],
|
||||
];
|
||||
require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user