" . escapeHtml(trim($_POST['name'])) . " created" . ($linked ? " with $linked linked" : ' - link what it runs on and depends on below'));
redirect(serviceUrl($service_id, $client_id));
}
if (isset($_POST['edit_service'])) {
validateCSRFToken();
$row = serviceForRequest($_POST['service_id'] ?? 0, 2);
$service_id = intval($row['service_id']);
$error = '';
$set = serviceFormSet($_POST, $error, $row);
if ($set === '') {
flashAlert($error, 'error');
redirect();
}
mysqli_query($mysqli, "UPDATE services SET $set WHERE service_id = $service_id");
// Only a form that showed the tags changes them
if (isset($_POST['tags_form'])) {
tagSetForRecord('service', $service_id, (array) ($_POST['tags'] ?? []));
}
$service_name = escapeSql(trim($_POST['name']));
logAudit("Service", "Edit", "$session_name edited service $service_name", $client_id, $service_id);
flashAlert("Service " . escapeHtml(trim($_POST['name'])) . " saved");
redirect();
}
// Notes save from the service page when you click away
if (isset($_POST['service_set_notes'])) {
validateCSRFToken();
$row = serviceForRequest($_POST['service_id'] ?? 0, 2);
$notes = escapeSql((string) ($_POST['notes'] ?? ''));
mysqli_query($mysqli, "UPDATE services SET service_notes = '$notes' WHERE service_id = " . intval($row['service_id']));
logAudit("Service", "Edit", "$session_name updated the notes of service " . escapeSql($row['service_name']), $client_id, intval($row['service_id']));
if (!empty($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
}
flashAlert("Notes saved");
redirect();
}
if (isset($_POST['review_service'])) {
validateCSRFToken();
$row = serviceForRequest($_POST['service_id'] ?? 0, 2);
// A new schedule can be set at the same time
if (isset($_POST['review_interval']) && isset(SERVICE_REVIEW_INTERVALS[intval($_POST['review_interval'])])) {
$row['service_review_interval'] = intval($_POST['review_interval']);
mysqli_query($mysqli, "UPDATE services SET service_review_interval = {$row['service_review_interval']} WHERE service_id = " . intval($row['service_id']));
}
$next = serviceMarkReviewed($row, trim((string) ($_POST['review_due'] ?? '')), (string) ($_POST['note'] ?? ''));
flashAlert("Service " . escapeHtml($row['service_name']) . " marked reviewed" . ($next ? " - next review " . date('M j, Y', strtotime($next)) : ''));
redirect();
}
if (isset($_GET['favorite_service']) || isset($_GET['unfavorite_service'])) {
validateCSRFToken();
$favorite = isset($_GET['favorite_service']) ? 1 : 0;
$row = serviceForRequest($favorite ? $_GET['favorite_service'] : $_GET['unfavorite_service'], 2);
mysqli_query($mysqli, "UPDATE services SET service_favorite = $favorite WHERE service_id = " . intval($row['service_id']));
logAudit("Service", "Edit", "$session_name " . ($favorite ? 'starred' : 'unstarred') . " service " . escapeSql($row['service_name']), $client_id, intval($row['service_id']));
flashAlert("Service " . escapeHtml($row['service_name']) . " " . ($favorite ? 'starred' : 'unstarred'));
redirect();
}
if (isset($_GET['archive_service'])) {
validateCSRFToken();
$row = serviceForRequest($_GET['archive_service'], 2);
serviceArchive($row);
flashAlert("Service " . escapeHtml($row['service_name']) . " archived", 'error');
redirect();
}
if (isset($_GET['restore_service'])) {
validateCSRFToken();
$row = serviceForRequest($_GET['restore_service'], 2);
serviceRestore($row);
flashAlert("Service " . escapeHtml($row['service_name']) . " restored");
redirect();
}
if (isset($_GET['delete_service'])) {
validateCSRFToken();
$row = serviceForRequest($_GET['delete_service'], 3);
serviceDelete($row);
flashAlert("Service " . escapeHtml($row['service_name']) . " deleted", 'error');
// Not back to the page of a service that is gone
$referer = $_SERVER['HTTP_REFERER'] ?? '';
redirect(strpos($referer, 'service.php') !== false ? 'services.php?client_id=' . intval($row['service_client_id']) : null);
}
// Tickets raised about a service - from its page, or from the ticket
if (isset($_POST['link_service_tickets'])) {
validateCSRFToken();
$row = serviceForRequest($_POST['service_id'] ?? 0, 2);
$added = serviceLinkTickets($row['service_id'], $client_id, (array) ($_POST['ticket_ids'] ?? []));
if ($added) {
logAudit("Service", "Link", "$session_name linked $added ticket(s) to service " . escapeSql($row['service_name']), $client_id, intval($row['service_id']));
flashAlert("Linked $added ticket" . ($added === 1 ? '' : 's') . " to " . escapeHtml($row['service_name']) . "");
} else {
flashAlert("Nothing new to link to " . escapeHtml($row['service_name']) . ".", 'warning');
}
redirect();
}
if (isset($_POST['link_ticket_services'])) {
validateCSRFToken();
enforceUserPermission('module_support', 2);
$ticket_id = intval($_POST['ticket_id'] ?? 0);
$ticket = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_client_id FROM tickets WHERE ticket_id = $ticket_id"));
if (!$ticket || !intval($ticket['ticket_client_id'])) {
flashAlert('That ticket no longer exists.', 'error');
redirect();
}
$client_id = intval($ticket['ticket_client_id']);
enforceClientAccess();
$names = [];
foreach (array_unique(array_map('intval', (array) ($_POST['service_ids'] ?? []))) as $service_id) {
$row = serviceRow($service_id);
if (!$row || intval($row['service_client_id']) !== $client_id || $row['service_archived_at']) {
continue;
}
if (serviceLinkTickets($service_id, $client_id, [$ticket_id])) {
$names[] = $row['service_name'];
logAudit("Service", "Link", "$session_name linked ticket " . escapeSql($ticket['ticket_prefix'] . $ticket['ticket_number']) . " to service " . escapeSql($row['service_name']), $client_id, $service_id);
}
}
if ($names) {
flashAlert("Ticket linked to " . escapeHtml(implode(', ', $names)) . "");
} else {
flashAlert('Nothing new to link.', 'warning');
}
redirect();
}
if (isset($_GET['unlink_service_ticket'])) {
validateCSRFToken();
$row = serviceForRequest($_GET['service_id'] ?? 0, 2);
$ticket_id = intval($_GET['unlink_service_ticket']);
mysqli_query($mysqli, "DELETE FROM service_tickets WHERE service_id = " . intval($row['service_id']) . " AND ticket_id = $ticket_id");
if (mysqli_affected_rows($mysqli) > 0) {
$ticket = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_prefix, ticket_number FROM tickets WHERE ticket_id = $ticket_id"));
$reference = $ticket ? $ticket['ticket_prefix'] . $ticket['ticket_number'] : "#$ticket_id";
logAudit("Service", "Unlink", "$session_name unlinked ticket " . escapeSql($reference) . " from service " . escapeSql($row['service_name']), $client_id, intval($row['service_id']));
flashAlert("Ticket " . escapeHtml($reference) . " unlinked from " . escapeHtml($row['service_name']) . "", 'error');
}
redirect();
}
/*
* Bulk actions from the services list
*/
if (isset($_POST['bulk_edit_services'])) {
validateCSRFToken();
$rows = servicesForBulk(2);
// Each field only when one was picked - blank leaves it as it is
$sets = [];
$changes = [];
if (in_array($_POST['importance'] ?? '', SERVICE_IMPORTANCE, true)) {
$sets[] = "service_importance = '{$_POST['importance']}'";
$changes[] = 'importance ' . $_POST['importance'];
}
$category = trim((string) ($_POST['category'] ?? ''));
if ($category !== '') {
$category = mb_substr($category, 0, 50);
$sets[] = "service_category = '" . escapeSql($category) . "'";
$changes[] = 'category ' . $category;
}
$interval = (string) ($_POST['review_interval'] ?? '');
$set_interval = $interval !== '' && isset(SERVICE_REVIEW_INTERVALS[intval($interval)]);
if (!$sets && !$set_interval) {
flashAlert('Nothing picked to change.', 'warning');
redirect();
}
$count = 0;
foreach ($rows as $service_id => $row) {
$row_sets = $sets;
if ($set_interval) {
$months = intval($interval);
$row_sets[] = "service_review_interval = $months";
// A schedule needs a next date: from the last review, never in the past - and none without a schedule
if ($months) {
$due = max(serviceNextReview($months, $row['service_reviewed_at'] ?: null), date('Y-m-d'));
$row_sets[] = "service_review_due = '$due'";
} else {
$row_sets[] = "service_review_due = NULL";
}
}
mysqli_query($mysqli, "UPDATE services SET " . implode(', ', $row_sets) . " WHERE service_id = $service_id");
$what = $changes;
if ($set_interval) {
$what[] = 'reviews ' . strtolower(SERVICE_REVIEW_INTERVALS[intval($interval)]);
}
logAudit("Service", "Edit", "$session_name set " . escapeSql(implode(', ', $what)) . " on service " . escapeSql($row['service_name']), intval($row['service_client_id']), $service_id);
$count++;
}
flashAlert("Updated $count service" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_review_services'])) {
validateCSRFToken();
$count = 0;
foreach (servicesForBulk(2) as $row) {
if ($row['service_archived_at']) {
continue;
}
serviceMarkReviewed($row);
$count++;
}
flashAlert("Marked $count service" . ($count === 1 ? '' : 's') . " reviewed");
redirect();
}
if (isset($_POST['bulk_favorite_services']) || isset($_POST['bulk_unfavorite_services'])) {
validateCSRFToken();
$favorite = isset($_POST['bulk_favorite_services']) ? 1 : 0;
$count = 0;
foreach (servicesForBulk(2) as $service_id => $row) {
mysqli_query($mysqli, "UPDATE services SET service_favorite = $favorite WHERE service_id = $service_id");
$count++;
}
flashAlert(($favorite ? 'Starred' : 'Unstarred') . " $count service" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_archive_services'])) {
validateCSRFToken();
$count = 0;
foreach (servicesForBulk(2) as $row) {
if (!$row['service_archived_at']) {
serviceArchive($row);
$count++;
}
}
flashAlert("Archived $count service" . ($count === 1 ? '' : 's'), 'error');
redirect();
}
if (isset($_POST['bulk_restore_services'])) {
validateCSRFToken();
$count = 0;
foreach (servicesForBulk(2) as $row) {
if ($row['service_archived_at']) {
serviceRestore($row);
$count++;
}
}
flashAlert("Restored $count service" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_delete_services'])) {
validateCSRFToken();
$count = 0;
foreach (servicesForBulk(3) as $row) {
serviceDelete($row);
$count++;
}
flashAlert("Deleted $count service" . ($count === 1 ? '' : 's'), 'error');
redirect();
}
if (isExportRequest('export_services')) {
validateCSRFToken();
enforceUserPermission('module_support');
$format = resolveExportFormat($_POST['export_services']);
$filter_summary = [];
$client_id = intval($_POST['client_id'] ?? 0);
$where = [clientScopeSql('service_client_id')];
if ($client_id) {
enforceClientAccess();
$where[] = "AND service_client_id = $client_id";
$file_name_prepend = getFieldById('clients', $client_id, 'client_name') . '-';
$filter_summary['Client'] = getFieldById('clients', $client_id, 'client_name');
} else {
$file_name_prepend = "$session_company_name-";
$where[] = 'AND clients.client_archived_at IS NULL';
if (intval($_POST['client'] ?? 0)) {
$where[] = 'AND service_client_id = ' . intval($_POST['client']);
$filter_summary['Client'] = exportClientName($_POST['client']);
}
}
$views = serviceViews();
$show = (string) ($_POST['show'] ?? '');
if (!isset($views[$show])) {
$show = '';
}
if ($views[$show]['where'] !== '') {
$where[] = 'AND ' . $views[$show]['where'];
}
$filter_summary['Showing'] = $views[$show]['label'];
if (in_array($_POST['importance'] ?? '', SERVICE_IMPORTANCE, true)) {
$where[] = "AND service_importance = '{$_POST['importance']}'";
$filter_summary['Importance'] = $_POST['importance'];
}
$category = trim((string) ($_POST['category'] ?? ''));
if ($category !== '') {
$where[] = "AND service_category = '" . escapeSql($category) . "'";
$filter_summary['Category'] = $category;
}
$where[] = tagExportFilterSql('service', $_POST['tags'] ?? [], $filter_summary, 'services.service_id');
$q = escapeSql($_POST['q'] ?? '');
if ($q !== '') {
$filter_summary['Search'] = $_POST['q'];
}
$sql = mysqli_query($mysqli, "SELECT services.*, client_name, " . tagNamesSql('service', 'services.service_id') . " AS service_tags FROM services
LEFT JOIN clients ON client_id = service_client_id
WHERE (service_name LIKE '%$q%' OR service_description LIKE '%$q%' OR service_category LIKE '%$q%' OR client_name LIKE '%$q%'
" . tagSearchSql('service', $q, 'services.service_id') . ")
" . implode(' ', $where) . "
ORDER BY client_name ASC, FIELD(service_importance, 'High', 'Medium', 'Low'), service_name ASC");
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[intval($row['service_id'])] = $row;
}
$num_rows = count($rows);
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$ids = array_keys($rows);
$in = implode(',', $ids);
$counts = serviceCounts($ids);
$issues = serviceIssues($ids);
// The names of what each one depends on, for the columns that list them
$names = [];
foreach (['asset', 'vendor', 'contact', 'domain'] as $type) {
$relation = linkRelation('service', $type);
$entity = LINK_ENTITIES[$type];
$sql = mysqli_query($mysqli, "SELECT l.service_id, t.{$entity['name']} FROM {$relation['table']} l
JOIN {$entity['table']} t ON t.{$entity['id']} = l.{$entity['id']} AND t.{$entity['archived']} IS NULL
WHERE l.service_id IN ($in) ORDER BY t.{$entity['name']}");
while ($name_row = mysqli_fetch_row($sql)) {
$names[intval($name_row[0])][$type][] = $name_row[1];
}
}
$export = beginExport('services', $format, $file_name_prepend . 'Services', 'Services', summarizeExportFilters($filter_summary));
foreach ($rows as $service_id => $row) {
$review = serviceReview($row);
$row['service_assets'] = implode(', ', $names[$service_id]['asset'] ?? []);
$row['service_vendors'] = implode(', ', $names[$service_id]['vendor'] ?? []);
$row['service_contacts'] = implode(', ', $names[$service_id]['contact'] ?? []);
$row['service_domains'] = implode(', ', $names[$service_id]['domain'] ?? []);
$row['service_attention'] = implode('; ', array_column($issues[$service_id], 'text'));
$row['service_review'] = $review['date'] ? $review['date'] . ($review['state'] === 'overdue' ? ' (overdue)' : '') : '';
$row['service_reviewed'] = $row['service_reviewed_at'] ? substr($row['service_reviewed_at'], 0, 10) : '';
$row['service_review_every'] = intval($row['service_review_interval']) ? (SERVICE_REVIEW_INTERVALS[intval($row['service_review_interval'])] ?? '') : '';
$row['open_tickets'] = $counts[$service_id]['open_tickets'];
$row['service_state'] = $row['service_archived_at'] ? 'Archived' : 'Active';
$row['service_tags'] = (string) $row['service_tags'];
addExportRow($export, $row);
}
finishExport($export);
logAudit("Service", "Export", "$session_name exported $num_rows service(s) to a " . strtoupper($format) . " file", $client_id);
exit;
}
flashAlert('No services match - nothing to export.', 'error');
redirect();
}