mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Tags everywhere: the same tag pills on every list and detail page, and an inline tag picker that saves as you tick (single records and bulk, with make-a-tag for admins) in place of the Assign Tags modals
This commit is contained in:
139
agent/ajax.php
139
agent/ajax.php
@@ -40,6 +40,145 @@ if (isset($_GET['certificate_fetch_parse_json_details'])) {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Tag picker (js/tag_picker.js) - replaces the "Assign Tags" modals.
|
||||
*
|
||||
* GET lists the tags one record, or a bulk selection of records, can have and
|
||||
* how many of them have each. POST ticks a tag on or off for all of them -
|
||||
* or, for an admin, makes a new tag and ticks it on - and sends back the
|
||||
* redrawn pills for every record, so the page updates in place.
|
||||
*
|
||||
* Same checks the modals' post handlers made: edit rights on the module and
|
||||
* access to each record's client. tagRecordsEditable() does both.
|
||||
*/
|
||||
if (isset($_GET['tag_picker'])) {
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$entity = (string) ($_GET['entity'] ?? '');
|
||||
$config = tagEntity($entity);
|
||||
$records = $config ? tagRecordsEditable($entity, (array) ($_GET['ids'] ?? [])) : false;
|
||||
|
||||
if ($records === false) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'You cannot change the tags on these.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$ids = implode(',', array_keys($records));
|
||||
$response = [
|
||||
'records' => count($records),
|
||||
'type' => $config['type'],
|
||||
'create' => (bool) $session_is_admin,
|
||||
'manage' => $session_is_admin ? "/admin/tags.php?type={$config['type']}" : '',
|
||||
'tags' => []
|
||||
];
|
||||
|
||||
$sql = mysqli_query($mysqli,
|
||||
"SELECT tag_id, tag_name, tag_color, tag_icon,
|
||||
(SELECT COUNT(*) FROM {$config['link_table']}
|
||||
WHERE {$config['link_tag']} = tags.tag_id AND {$config['link_record']} IN ($ids)) AS chosen
|
||||
FROM tags
|
||||
WHERE tag_type = {$config['type']}
|
||||
ORDER BY tag_name ASC"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$response['tags'][] = [
|
||||
'id' => intval($row['tag_id']),
|
||||
'name' => $row['tag_name'],
|
||||
'color' => preg_match('/^(#[0-9a-f]{3,8}|[a-z]{3,24})$/i', trim((string) $row['tag_color'])) ? trim($row['tag_color']) : '',
|
||||
'icon' => preg_replace('/[^a-z0-9-]/', '', strtolower((string) $row['tag_icon'])) ?: 'tag',
|
||||
'chosen' => intval($row['chosen'])
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($_POST['tag_picker_set']) || isset($_POST['tag_picker_create'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$entity = (string) ($_POST['entity'] ?? '');
|
||||
$config = tagEntity($entity);
|
||||
$records = $config ? tagRecordsEditable($entity, (array) ($_POST['ids'] ?? [])) : false;
|
||||
|
||||
if ($records === false) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'You cannot change the tags on these.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$type = $config['type'];
|
||||
$link = $config['link_table'];
|
||||
$link_record = $config['link_record'];
|
||||
$link_tag = $config['link_tag'];
|
||||
|
||||
if (isset($_POST['tag_picker_create'])) {
|
||||
|
||||
// Making tags is an admin job everywhere else (Admin > Tags), so it is here too
|
||||
$name = trim((string) ($_POST['name'] ?? ''));
|
||||
if (!$session_is_admin || $name === '' || mb_strlen($name) > 200) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'That tag cannot be made here.']);
|
||||
exit();
|
||||
}
|
||||
$name_sql = escapeSql($name);
|
||||
|
||||
// Typing the name of a tag that already exists just picks it
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT tag_id FROM tags WHERE tag_type = $type AND tag_name = '$name_sql' LIMIT 1"));
|
||||
if ($row) {
|
||||
$tag_id = intval($row['tag_id']);
|
||||
} else {
|
||||
$color = tagNextColor($type);
|
||||
mysqli_query($mysqli, "INSERT INTO tags SET tag_name = '$name_sql', tag_type = $type, tag_color = '$color', tag_icon = 'tag'");
|
||||
$tag_id = mysqli_insert_id($mysqli);
|
||||
logAudit("Tag", "Create", "$session_name created tag $name_sql", 0, $tag_id);
|
||||
}
|
||||
$on = true;
|
||||
|
||||
} else {
|
||||
$tag_id = intval($_POST['tag_id'] ?? 0);
|
||||
$on = !empty($_POST['on']);
|
||||
}
|
||||
|
||||
// The tag has to be one of this kind of record's tags
|
||||
$tag = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT tag_name FROM tags WHERE tag_id = $tag_id AND tag_type = $type"));
|
||||
if (!$tag) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'That tag no longer exists.']);
|
||||
exit();
|
||||
}
|
||||
$tag_name = escapeSql($tag['tag_name']);
|
||||
|
||||
$changed = 0;
|
||||
foreach ($records as $record_id => $record) {
|
||||
$has = mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM $link WHERE $link_record = $record_id AND $link_tag = $tag_id")) > 0;
|
||||
$record_name = escapeSql($record['name']);
|
||||
if ($on && !$has) {
|
||||
mysqli_query($mysqli, "INSERT INTO $link SET $link_record = $record_id, $link_tag = $tag_id");
|
||||
logAudit($config['log'], "Edit", "$session_name added tag $tag_name to $record_name", $record['client_id'], $record_id);
|
||||
$changed++;
|
||||
} elseif (!$on && $has) {
|
||||
mysqli_query($mysqli, "DELETE FROM $link WHERE $link_record = $record_id AND $link_tag = $tag_id");
|
||||
logAudit($config['log'], "Edit", "$session_name removed tag $tag_name from $record_name", $record['client_id'], $record_id);
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
|
||||
$filter = (string) ($_POST['filter'] ?? '');
|
||||
$pills = [];
|
||||
foreach (tagsForRecords($entity, array_keys($records)) as $record_id => $tags) {
|
||||
$pills[$record_id] = tagPills($entity, $record_id, $tags, $filter);
|
||||
}
|
||||
|
||||
echo json_encode(['tag_id' => $tag_id, 'changed' => $changed, 'pills' => $pills]);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($_POST['client_set_notes'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -135,26 +135,8 @@ if (isset($_GET['asset_id'])) {
|
||||
$document_count = mysqli_num_rows($sql_related_documents);
|
||||
|
||||
// Tags - many to many relationship
|
||||
$asset_tag_name_display_array = array();
|
||||
$asset_tag_id_array = array();
|
||||
$sql_asset_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, tag_id, tag_name FROM asset_tags LEFT JOIN tags ON asset_tag_tag_id = tag_id WHERE asset_tag_asset_id = $asset_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_asset_tags)) {
|
||||
|
||||
$asset_tag_id = intval($row['tag_id']);
|
||||
$asset_tag_name = escapeHtml($row['tag_name']);
|
||||
$asset_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($asset_tag_color)) {
|
||||
$asset_tag_color = "dark";
|
||||
}
|
||||
$asset_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($asset_tag_icon)) {
|
||||
$asset_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$asset_tag_id_array[] = $asset_tag_id;
|
||||
$asset_tag_name_display_array[] = "<a href='client_assets.php?client_id=$client_id&q=$asset_tag_name'><span class='badge text-light p-1 me-1' style='background-color: $asset_tag_color;'><i class='fa fa-fw fa-$asset_tag_icon me-2'></i>$asset_tag_name</span></a>";
|
||||
}
|
||||
$asset_tags_display = implode('', $asset_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
|
||||
|
||||
// Network Interfaces
|
||||
$sql_related_interfaces = mysqli_query($mysqli, "
|
||||
@@ -703,27 +685,6 @@ if (isset($_GET['asset_id'])) {
|
||||
$credential_contact_id = intval($row['credential_contact_id']);
|
||||
$credential_asset_id = intval($row['credential_asset_id']);
|
||||
|
||||
// Tags
|
||||
$credential_tag_name_display_array = array();
|
||||
$credential_tag_id_array = array();
|
||||
$sql_credential_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, credential_tags.tag_id, tag_name FROM credential_tags LEFT JOIN tags ON credential_tags.tag_id = tags.tag_id WHERE credential_id = $credential_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_credential_tags)) {
|
||||
|
||||
$credential_tag_id = intval($row['tag_id']);
|
||||
$credential_tag_name = escapeHtml($row['tag_name']);
|
||||
$credential_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($credential_tag_color)) {
|
||||
$credential_tag_color = "dark";
|
||||
}
|
||||
$credential_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($credential_tag_icon)) {
|
||||
$credential_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$credential_tag_id_array[] = $credential_tag_id;
|
||||
$credential_tag_name_display_array[] = "<a href='credentials.php?client_id=$client_id&tags[]=$credential_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $credential_tag_color;'><i class='fa fa-fw fa-$credential_tag_icon me-2'></i>$credential_tag_name</span></a>";
|
||||
}
|
||||
$credential_tags_display = implode('', $credential_tag_name_display_array);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
|
||||
@@ -400,10 +400,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/asset/asset_bulk_assign_tags.php"
|
||||
data-bulk="true">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Assign Tags
|
||||
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="asset" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Tags
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
@@ -638,26 +636,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$credential_count = mysqli_num_rows($sql_credentials);
|
||||
|
||||
// Tags
|
||||
$asset_tag_name_display_array = array();
|
||||
$asset_tag_id_array = array();
|
||||
$sql_asset_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, tag_id, tag_name FROM asset_tags LEFT JOIN tags ON asset_tag_tag_id = tag_id WHERE asset_tag_asset_id = $asset_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_asset_tags)) {
|
||||
|
||||
$asset_tag_id = intval($row['tag_id']);
|
||||
$asset_tag_name = escapeHtml($row['tag_name']);
|
||||
$asset_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($asset_tag_color)) {
|
||||
$asset_tag_color = "dark";
|
||||
}
|
||||
$asset_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($asset_tag_icon)) {
|
||||
$asset_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$asset_tag_id_array[] = $asset_tag_id;
|
||||
$asset_tag_name_display_array[] = "<a href='assets.php?$client_url tags[]=$asset_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $asset_tag_color;'><i class='fa fa-fw fa-$asset_tag_icon me-1'></i>$asset_tag_name</span></a>";
|
||||
}
|
||||
$asset_tags_display = implode('', $asset_tag_name_display_array);
|
||||
$asset_tags = tagsForRecord('asset', $asset_id);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
@@ -666,20 +645,18 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="asset_ids[]" value="<?= $asset_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark" href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-<?= $device_icon ?> me-3 mt-1"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div>
|
||||
<?= $asset_name ?>
|
||||
|
||||
<?php if ($asset_favorite) { echo "<i class='fas fa-fw fa-star text-warning' title='Favorite'></i>"; } ?></div>
|
||||
<div><small class="text-secondary"><?= $asset_description ?></small></div>
|
||||
<?php if ($asset_tags_display) { echo $asset_tags_display; } ?>
|
||||
</div>
|
||||
<!-- The whole cell opens the asset (stretched-link); the tag pills sit above it -->
|
||||
<td class="position-relative">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-<?= $device_icon ?> me-3 mt-1"></i>
|
||||
<div class="flex-grow-1">
|
||||
<a class="text-dark stretched-link" href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>"><?= $asset_name ?> <?php if ($asset_favorite) { echo "<i class='fas fa-fw fa-star text-warning' title='Favorite'></i>"; } ?></a>
|
||||
<div><small class="text-secondary"><?= $asset_description ?></small></div>
|
||||
<?php if ($asset_tags || lookupUserPermission('module_client') >= 2) { ?>
|
||||
<div class="mt-1"><?= tagPills('asset', $asset_id, $asset_tags, "assets.php?$client_url") ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
@@ -62,7 +62,7 @@ $money_format = function ($amount) use ($currency_format, $client_currency_code,
|
||||
<?php if ($client_type) { ?>
|
||||
<span class="text-body-secondary me-2"><?= $client_type ?></span>
|
||||
<?php } ?>
|
||||
<?php require 'includes/client_tags.php'; ?>
|
||||
<?= tagPills('client', $client_id, $client_tags) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 ms-auto itflow-ov-hero-actions">
|
||||
|
||||
@@ -331,8 +331,8 @@ $total_leads = intval($row['leads']);
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/client/client_bulk_assign_tags.php" data-bulk="true">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Assign Tags
|
||||
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="client" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Tags
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/client/client_bulk_edit_industry.php" data-bulk="true">
|
||||
<i class="fas fa-fw fa-briefcase me-2"></i>Set Industry
|
||||
@@ -516,23 +516,7 @@ $total_leads = intval($row['leads']);
|
||||
<div class="small text-body-secondary"><?= $client_type ?></div>
|
||||
<?php } ?>
|
||||
<?php if (!empty($client_tags[$client_id]) || $can_edit) { ?>
|
||||
<div class="d-flex flex-wrap align-items-center gap-1 mt-1">
|
||||
<?php foreach ($client_tags[$client_id] ?? [] as $tag) {
|
||||
$tag_id = intval($tag['tag_id']);
|
||||
$tag_name = escapeHtml($tag['tag_name']);
|
||||
$tag_color = preg_match('/^#[0-9a-f]{3,8}$/i', (string) $tag['tag_color']) ? $tag['tag_color'] : 'var(--bs-secondary)';
|
||||
$tag_icon = preg_replace('/[^a-z0-9-]/', '', strtolower((string) $tag['tag_icon'])) ?: 'tag';
|
||||
?>
|
||||
<a href="<?= clientsFilterUrl(['tags' => [$tag_id]]) ?>" class="itflow-client-tag" style="--itflow-tag-color: <?= $tag_color ?>" title="Show clients tagged <?= $tag_name ?>">
|
||||
<i class="fas fa-fw fa-<?= $tag_icon ?>" aria-hidden="true"></i><?= $tag_name ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($can_edit) { ?>
|
||||
<a href="#" class="itflow-client-tag itflow-client-tag-add ajax-modal" data-modal-url="modals/client/client_bulk_assign_tags.php?client_ids[]=<?= $client_id ?>" title="Add tags to <?= $client_name ?>">
|
||||
<i class="fas fa-fw fa-plus" aria-hidden="true"></i><?php if (empty($client_tags[$client_id])) { ?>Tag<?php } else { ?><span class="visually-hidden">Add tags</span><?php } ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="mt-1"><?= tagPills('client', $client_id, $client_tags[$client_id] ?? [], $leads_filter ? 'clients.php?leads=1&' : 'clients.php?') ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,26 +134,8 @@ if (isset($_GET['contact_id'])) {
|
||||
|
||||
|
||||
// Tags - many to many relationship
|
||||
$contact_tag_name_display_array = array();
|
||||
$contact_tag_id_array = array();
|
||||
$sql_contact_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, contact_tags.tag_id, tag_name FROM contact_tags LEFT JOIN tags ON contact_tags.tag_id = tags.tag_id WHERE contact_id = $contact_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_contact_tags)) {
|
||||
|
||||
$contact_tag_id = intval($row['tag_id']);
|
||||
$contact_tag_name = escapeHtml($row['tag_name']);
|
||||
$contact_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($contact_tag_color)) {
|
||||
$contact_tag_color = "dark";
|
||||
}
|
||||
$contact_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($contact_tag_icon)) {
|
||||
$contact_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$contact_tag_id_array[] = $contact_tag_id;
|
||||
$contact_tag_name_display_array[] = "<a href='client_contacts.php?client_id=$client_id&q=$contact_tag_name'><span class='badge text-light p-1 me-1' style='background-color: $contact_tag_color;'><i class='fa fa-fw fa-$contact_tag_icon me-2'></i>$contact_tag_name</span></a>";
|
||||
}
|
||||
$contact_tags_display = implode('', $contact_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$contact_tags_display = tagPills('contact', $contact_id, tagsForRecord('contact', $contact_id), "contacts.php?client_id=$client_id&");
|
||||
|
||||
// Notes - 1 to 1 relationship
|
||||
$sql_related_notes = mysqli_query($mysqli, "SELECT contact_note, contact_note_created_at, contact_note_id, contact_note_type, user_name FROM contact_notes LEFT JOIN users ON contact_note_created_by = user_id WHERE contact_note_contact_id = $contact_id AND contact_note_archived_at IS NULL ORDER BY contact_note_created_at DESC");
|
||||
@@ -436,27 +418,8 @@ if (isset($_GET['contact_id'])) {
|
||||
$asset_created_at = escapeHtml($row['asset_created_at']);
|
||||
$device_icon = getAssetIcon($asset_type);
|
||||
|
||||
// Tags
|
||||
$asset_tag_name_display_array = array();
|
||||
$asset_tag_id_array = array();
|
||||
$sql_asset_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, tag_id, tag_name FROM asset_tags LEFT JOIN tags ON asset_tag_tag_id = tag_id WHERE asset_tag_asset_id = $asset_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_asset_tags)) {
|
||||
|
||||
$asset_tag_id = intval($row['tag_id']);
|
||||
$asset_tag_name = escapeHtml($row['tag_name']);
|
||||
$asset_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($asset_tag_color)) {
|
||||
$asset_tag_color = "dark";
|
||||
}
|
||||
$asset_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($asset_tag_icon)) {
|
||||
$asset_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$asset_tag_id_array[] = $asset_tag_id;
|
||||
$asset_tag_name_display_array[] = "<a href='assets.php?$client_url tags[]=$asset_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $asset_tag_color;'><i class='fa fa-fw fa-$asset_tag_icon me-2'></i>$asset_tag_name</span></a>";
|
||||
}
|
||||
$asset_tags_display = implode('', $asset_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
|
||||
|
||||
?>
|
||||
<tr>
|
||||
@@ -590,27 +553,6 @@ if (isset($_GET['contact_id'])) {
|
||||
$credential_contact_id = intval($row['credential_contact_id']);
|
||||
$credential_asset_id = intval($row['credential_asset_id']);
|
||||
|
||||
// Tags
|
||||
$credential_tag_name_display_array = array();
|
||||
$credential_tag_id_array = array();
|
||||
$sql_credential_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, credential_tags.tag_id, tag_name FROM credential_tags LEFT JOIN tags ON credential_tags.tag_id = tags.tag_id WHERE credential_id = $credential_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_credential_tags)) {
|
||||
|
||||
$credential_tag_id = intval($row['tag_id']);
|
||||
$credential_tag_name = escapeHtml($row['tag_name']);
|
||||
$credential_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($credential_tag_color)) {
|
||||
$credential_tag_color = "dark";
|
||||
}
|
||||
$credential_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($credential_tag_icon)) {
|
||||
$credential_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$credential_tag_id_array[] = $credential_tag_id;
|
||||
$credential_tag_name_display_array[] = "<a href='credentials.php?client_id=$client_id&tags[]=$credential_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $credential_tag_color;'><i class='fa fa-fw fa-$credential_tag_icon me-2'></i>$credential_tag_name</span></a>";
|
||||
}
|
||||
$credential_tags_display = implode('', $credential_tag_name_display_array);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
|
||||
@@ -248,10 +248,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<i class="fas fa-fw fa-user-shield me-2"></i>Set Roles
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/contact/contact_bulk_assign_tags.php"
|
||||
data-bulk="true">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Assign Tags
|
||||
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="contact" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Tags
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
@@ -443,26 +441,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
}
|
||||
|
||||
// Tags
|
||||
$contact_tag_name_display_array = array();
|
||||
$contact_tag_id_array = array();
|
||||
$sql_contact_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, contact_tags.tag_id, tag_name FROM contact_tags LEFT JOIN tags ON contact_tags.tag_id = tags.tag_id WHERE contact_id = $contact_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_contact_tags)) {
|
||||
|
||||
$contact_tag_id = intval($row['tag_id']);
|
||||
$contact_tag_name = escapeHtml($row['tag_name']);
|
||||
$contact_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($contact_tag_color)) {
|
||||
$contact_tag_color = "dark";
|
||||
}
|
||||
$contact_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($contact_tag_icon)) {
|
||||
$contact_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$contact_tag_id_array[] = $contact_tag_id;
|
||||
$contact_tag_name_display_array[] = "<a href='contacts.php?$client_url tags[]=$contact_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $contact_tag_color;'><i class='fa fa-fw fa-$contact_tag_icon me-2'></i>$contact_tag_name</span></a>";
|
||||
}
|
||||
$contact_tags_display = implode('', $contact_tag_name_display_array);
|
||||
$contact_tags = tagsForRecord('contact', $contact_id);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
@@ -471,34 +450,29 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="contact_ids[]" value="<?= $contact_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark" href="contact.php?client_id=<?= $client_id ?>&contact_id=<?= $contact_id ?>">
|
||||
<div class="d-flex">
|
||||
<?php if ($contact_photo) { ?>
|
||||
<span class="fa-stack fa-2x me-3 text-center">
|
||||
<img class="img-size-50 rounded-circle" src="<?= "../uploads/clients/$client_id/$contact_photo" ?>">
|
||||
</span>
|
||||
<?php } else { ?>
|
||||
<span class="fa-stack fa-2x me-3">
|
||||
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
|
||||
<span class="fa fa-stack-1x text-white"><?= $contact_initials ?></span>
|
||||
</span>
|
||||
<!-- The whole cell opens the contact (stretched-link); the tag pills sit above it -->
|
||||
<td class="position-relative">
|
||||
<div class="d-flex">
|
||||
<?php if ($contact_photo) { ?>
|
||||
<span class="fa-stack fa-2x me-3 text-center">
|
||||
<img class="img-size-50 rounded-circle" src="<?= "../uploads/clients/$client_id/$contact_photo" ?>">
|
||||
</span>
|
||||
<?php } else { ?>
|
||||
<span class="fa-stack fa-2x me-3">
|
||||
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
|
||||
<span class="fa fa-stack-1x text-white"><?= $contact_initials ?></span>
|
||||
</span>
|
||||
<?php } ?>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<a class="text-dark stretched-link <?php if($contact_important) { echo "text-bold"; } ?>" href="contact.php?client_id=<?= $client_id ?>&contact_id=<?= $contact_id ?>"><?= $contact_name ?> <?= $user_exists_display ?></a>
|
||||
<?= $contact_title_display ?>
|
||||
<div><?= $contact_primary_display ?></div>
|
||||
<?php if ($contact_tags || lookupUserPermission('module_client') >= 2) { ?>
|
||||
<div class="mt-1"><?= tagPills('contact', $contact_id, $contact_tags, "contacts.php?$client_url") ?></div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div class="<?php if($contact_important) { echo "text-bold"; } ?>"><?= $contact_name ?> <?= $user_exists_display ?></div>
|
||||
<?= $contact_title_display ?>
|
||||
<div><?= $contact_primary_display ?></div>
|
||||
<?php
|
||||
if (!empty($contact_tags_display)) { ?>
|
||||
<div class="mt-1">
|
||||
<?= $contact_tags_display ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td><?= $contact_department ?></td>
|
||||
<td><?= $contact_info_display ?></td>
|
||||
|
||||
@@ -252,10 +252,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</button>
|
||||
<?php } else { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/credential/credential_bulk_assign_tags.php"
|
||||
data-bulk="true">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Assign Tags
|
||||
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="credential" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Tags
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger confirm-link"
|
||||
@@ -345,26 +343,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$asset_name = escapeHtml($row['asset_name']);
|
||||
|
||||
// Tags
|
||||
$credential_tag_name_display_array = array();
|
||||
$credential_tag_id_array = array();
|
||||
$sql_credential_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, credential_tags.tag_id, tag_name FROM credential_tags LEFT JOIN tags ON credential_tags.tag_id = tags.tag_id WHERE credential_id = $credential_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_credential_tags)) {
|
||||
|
||||
$credential_tag_id = intval($row['tag_id']);
|
||||
$credential_tag_name = escapeHtml($row['tag_name']);
|
||||
$credential_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($credential_tag_color)) {
|
||||
$credential_tag_color = "dark";
|
||||
}
|
||||
$credential_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($credential_tag_icon)) {
|
||||
$credential_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$credential_tag_id_array[] = $credential_tag_id;
|
||||
$credential_tag_name_display_array[] = "<a href='credentials.php?$client_url tags[]=$credential_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $credential_tag_color;'><i class='fa fa-fw fa-$credential_tag_icon me-2'></i>$credential_tag_name</span></a>";
|
||||
}
|
||||
$credential_tags_display = implode('', $credential_tag_name_display_array);
|
||||
$credential_tags = tagsForRecord('credential', $credential_id);
|
||||
|
||||
if ($credential_contact_id) {
|
||||
$credential_contact_display = "<a href='#' class='me-2 mb-1 badge rounded-pill bg-dark p-2 ajax-modal' title='$contact_name'
|
||||
@@ -421,23 +400,18 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="credential_ids[]" value="<?= $credential_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-key me-3"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div><?= $credential_name ?> <?php if ($credential_favorite) { echo "<i class='fas fa-fw fa-star text-warning' title='Favorite'></i>"; } ?></div>
|
||||
<div><small class="text-secondary"><?= $credential_description ?></small></div>
|
||||
<?php
|
||||
if (!empty($credential_tags_display)) { ?>
|
||||
<div class="mt-1">
|
||||
<?= $credential_tags_display ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<!-- The whole cell opens the credential (stretched-link); the tag pills sit above it -->
|
||||
<td class="position-relative">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-key me-3"></i>
|
||||
<div class="flex-grow-1">
|
||||
<a class="text-dark stretched-link ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>"><?= $credential_name ?> <?php if ($credential_favorite) { echo "<i class='fas fa-fw fa-star text-warning' title='Favorite'></i>"; } ?></a>
|
||||
<div><small class="text-secondary"><?= $credential_description ?></small></div>
|
||||
<?php if ($credential_tags || lookupUserPermission('module_credential') >= 2) { ?>
|
||||
<div class="mt-1"><?= tagPills('credential', $credential_id, $credential_tags, "credentials.php?$client_url") ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-nowrap"><?= $credential_username_display ?></td>
|
||||
<td class="text-nowrap">
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* The client's tag pills, each opening the clients list filtered to that tag,
|
||||
* then a "+" pill that opens Add Tags for this client. Used by the client
|
||||
* header (inc_client_top_head.php) and the client overview. Expects
|
||||
* $client_tags (from inc_all_client.php) and $client_id.
|
||||
*/
|
||||
foreach ($client_tags as $client_tag) {
|
||||
$client_tag_id = intval($client_tag['tag_id']);
|
||||
$client_tag_name = escapeHtml($client_tag['tag_name']);
|
||||
$client_tag_color = preg_match('/^#[0-9a-f]{3,8}$/i', (string) $client_tag['tag_color']) ? $client_tag['tag_color'] : 'var(--bs-secondary)';
|
||||
$client_tag_icon = preg_replace('/[^a-z0-9-]/', '', strtolower((string) $client_tag['tag_icon'])) ?: 'tag';
|
||||
?>
|
||||
<a href="clients.php?tags[]=<?= $client_tag_id ?>" class="itflow-client-tag" style="--itflow-tag-color: <?= $client_tag_color ?>" title="Clients tagged <?= $client_tag_name ?>">
|
||||
<i class="fas fa-fw fa-<?= $client_tag_icon ?>" aria-hidden="true"></i><?= $client_tag_name ?>
|
||||
</a>
|
||||
<?php }
|
||||
if (lookupUserPermission("module_client") >= 2) { ?>
|
||||
<a href="#" class="itflow-client-tag itflow-client-tag-add ajax-modal" data-modal-url="modals/client/client_bulk_assign_tags.php?client_ids[]=<?= $client_id ?>" title="Add tags">
|
||||
<i class="fas fa-fw fa-plus" aria-hidden="true"></i><?= $client_tags ? '<span class="visually-hidden">Add tags</span>' : 'Tag' ?>
|
||||
</a>
|
||||
<?php }
|
||||
@@ -78,7 +78,7 @@ if (isset($_GET['client_id'])) {
|
||||
// Tab Title // No Sanitizing needed
|
||||
$tab_title = $row['client_name'];
|
||||
|
||||
// Client Tags - raw rows, drawn by includes/client_tags.php
|
||||
// Client Tags - raw rows, drawn by tagPills() (functions/tags.php)
|
||||
$client_tags = [];
|
||||
$sql_client_tags = mysqli_query($mysqli, "SELECT tags.tag_id, tag_name, tag_color, tag_icon FROM client_tags JOIN tags ON client_tags.tag_id = tags.tag_id WHERE client_id = $client_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_client_tags)) {
|
||||
|
||||
@@ -44,9 +44,7 @@ if (empty($client_top_head_skip)) {
|
||||
</a>
|
||||
<?php if ($client_is_lead) { ?><span class="badge rounded-pill text-bg-info fw-normal">Lead</span><?php } ?>
|
||||
<?php if ($client_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal">Archived</span><?php } ?>
|
||||
<div class="d-flex flex-wrap align-items-center gap-1">
|
||||
<?php require 'includes/client_tags.php'; ?>
|
||||
</div>
|
||||
<?= tagPills('client', $client_id, $client_tags) ?>
|
||||
|
||||
<?php /* Open tickets and balance due, always in sight */ ?>
|
||||
<?php if ($client_head_glance_tickets || $client_head_glance_due) { ?>
|
||||
|
||||
@@ -183,10 +183,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/location/location_bulk_assign_tags.php"
|
||||
data-bulk="true">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Assign Tags
|
||||
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="location" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-tags me-2"></i>Tags
|
||||
</a>
|
||||
<?php if ($archived) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
@@ -304,27 +302,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
}
|
||||
|
||||
// Tags
|
||||
|
||||
$location_tag_name_display_array = array();
|
||||
$location_tag_id_array = array();
|
||||
$sql_location_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, location_tags.tag_id, tag_name FROM location_tags LEFT JOIN tags ON location_tags.tag_id = tags.tag_id WHERE location_tags.location_id = $location_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_location_tags)) {
|
||||
|
||||
$location_tag_id = intval($row['tag_id']);
|
||||
$location_tag_name = escapeHtml($row['tag_name']);
|
||||
$location_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($location_tag_color)) {
|
||||
$location_tag_color = "dark";
|
||||
}
|
||||
$location_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($location_tag_icon)) {
|
||||
$location_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$location_tag_id_array[] = $location_tag_id;
|
||||
$location_tag_name_display_array[] = "<a href='locations.php?$client_url tags[]=$location_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $location_tag_color;'><i class='fa fa-fw fa-$location_tag_icon me-2'></i>$location_tag_name</span></a>";
|
||||
}
|
||||
$location_tags_display = implode('', $location_tag_name_display_array);
|
||||
$location_tags = tagsForRecord('location', $location_id);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
@@ -333,23 +311,19 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="location_ids[]" value="<?= $location_id ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-dark ajax-modal" href="#" data-modal-url="modals/location/location_edit.php?id=<?= $location_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-map-marker-alt me-2"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div <?php if($location_primary) { echo "class='text-bold'"; } ?>><?= $location_name ?></div>
|
||||
<div><small class="text-secondary"><?= $location_description ?></small></div>
|
||||
<div><?= $location_primary_display ?></div>
|
||||
<?php
|
||||
if (!empty($location_tags_display)) { ?>
|
||||
<div class="mt-1">
|
||||
<?= $location_tags_display ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<!-- The whole cell opens the location (stretched-link); the tag pills sit above it -->
|
||||
<td class="position-relative">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-map-marker-alt me-2"></i>
|
||||
<div class="flex-grow-1">
|
||||
<a class="text-dark stretched-link ajax-modal <?php if($location_primary) { echo "text-bold"; } ?>" href="#" data-modal-url="modals/location/location_edit.php?id=<?= $location_id ?>"><?= $location_name ?></a>
|
||||
<div><small class="text-secondary"><?= $location_description ?></small></div>
|
||||
<div><?= $location_primary_display ?></div>
|
||||
<?php if ($location_tags || lookupUserPermission('module_client') >= 2) { ?>
|
||||
<div class="mt-1"><?= tagPills('location', $location_id, $location_tags, "locations.php?$client_url") ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="//maps.<?= $session_map_source ?>.com?q=<?= "$location_address $location_zip" ?>"
|
||||
|
||||
@@ -88,26 +88,8 @@ if ($location_archived_at) {
|
||||
}
|
||||
|
||||
// Tags - many to many relationship
|
||||
$asset_tag_name_display_array = array();
|
||||
$asset_tag_id_array = array();
|
||||
$sql_asset_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, tag_id, tag_name FROM asset_tags LEFT JOIN tags ON asset_tag_tag_id = tag_id WHERE asset_tag_asset_id = $asset_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_asset_tags)) {
|
||||
|
||||
$asset_tag_id = intval($row['tag_id']);
|
||||
$asset_tag_name = escapeHtml($row['tag_name']);
|
||||
$asset_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($asset_tag_color)) {
|
||||
$asset_tag_color = "dark";
|
||||
}
|
||||
$asset_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($asset_tag_icon)) {
|
||||
$asset_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$asset_tag_id_array[] = $asset_tag_id;
|
||||
$asset_tag_name_display_array[] = "<a href='client_assets.php?client_id=$client_id&q=$asset_tag_name'><span class='badge text-light p-1 me-1' style='background-color: $asset_tag_color;'><i class='fa fa-fw fa-$asset_tag_icon me-2'></i>$asset_tag_name</span></a>";
|
||||
}
|
||||
$asset_tags_display = implode('', $asset_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
|
||||
|
||||
// Network Interfaces
|
||||
$sql_related_interfaces = mysqli_query($mysqli, "
|
||||
@@ -578,27 +560,6 @@ ob_start();
|
||||
$credential_contact_id = intval($row['credential_contact_id']);
|
||||
$credential_asset_id = intval($row['credential_asset_id']);
|
||||
|
||||
// Tags
|
||||
$credential_tag_name_display_array = array();
|
||||
$credential_tag_id_array = array();
|
||||
$sql_credential_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, credential_tags.tag_id, tag_name FROM credential_tags LEFT JOIN tags ON credential_tags.tag_id = tags.tag_id WHERE credential_id = $credential_id ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_credential_tags)) {
|
||||
|
||||
$credential_tag_id = intval($row['tag_id']);
|
||||
$credential_tag_name = escapeHtml($row['tag_name']);
|
||||
$credential_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($credential_tag_color)) {
|
||||
$credential_tag_color = "dark";
|
||||
}
|
||||
$credential_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($credential_tag_icon)) {
|
||||
$credential_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$credential_tag_id_array[] = $credential_tag_id;
|
||||
$credential_tag_name_display_array[] = "<a href='credentials.php?client_id=$client_id&tags[]=$credential_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $credential_tag_color;'><i class='fa fa-fw fa-$credential_tag_icon me-2'></i>$credential_tag_name</span></a>";
|
||||
}
|
||||
$credential_tags_display = implode('', $credential_tag_name_display_array);
|
||||
|
||||
?>
|
||||
<tr>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$asset_ids = array_map('intval', $_GET['asset_ids'] ?? []);
|
||||
|
||||
$count = count($asset_ids);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tags me-2"></i>Assign Tags for <strong><?= $count ?></strong> Assets</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 ($asset_ids as $asset_id) { ?><input type="hidden" name="asset_ids[]" value="<?= $asset_id ?>"><?php } ?>
|
||||
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="remove_tags" value="0">
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="remove_tags" value="1">
|
||||
<label class="form-check-label text-danger">Remove Existing Tags</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tags</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
||||
<select class="form-select select2" name="tags[]" data-placeholder="Add some tags" multiple>
|
||||
<?php
|
||||
|
||||
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 5 ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags_select)) {
|
||||
$tag_id_select = intval($row['tag_id']);
|
||||
$tag_name_select = escapeHtml($row['tag_name']);
|
||||
?>
|
||||
<option value="<?= $tag_id_select ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name_select ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_assign_asset_tags" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Assign Tags</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
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
$client_ids = array_map('intval', $_GET['client_ids'] ?? []);
|
||||
|
||||
$count = count($client_ids);
|
||||
|
||||
// Opened from the + beside one client's tags on the clients list: name the
|
||||
// client, and offer only the tags it doesn't already have
|
||||
$single_client_id = $count === 1 ? $client_ids[0] : 0;
|
||||
$existing_tags_query = '';
|
||||
if ($single_client_id) {
|
||||
enforceClientAccess($single_client_id);
|
||||
$single_client_name = escapeHtml(getFieldById('clients', $single_client_id, 'client_name'));
|
||||
$existing_tags_query = "AND tag_id NOT IN (SELECT tag_id FROM client_tags WHERE client_id = $single_client_id)";
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<?php if ($single_client_id) { ?>
|
||||
<h5 class="modal-title text-truncate"><i class="fa fa-fw fa-tags me-2"></i>Add Tags to <strong><?= $single_client_name ?></strong></h5>
|
||||
<?php } else { ?>
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tags me-2"></i>Assign Tags for <strong><?= $count ?></strong> Clients</h5>
|
||||
<?php } ?>
|
||||
<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 ($client_ids as $client_id) { ?><input type="hidden" name="client_ids[]" value="<?= $client_id ?>"><?php } ?>
|
||||
<input type="hidden" name="bulk_remove_tags" value="0">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php if (!$single_client_id) { ?>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_remove_tags" value="1">
|
||||
<label class="form-check-label text-danger">Remove Existing Tags</label>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tags</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
||||
<select class="form-select select2" name="bulk_tags[]" data-placeholder="Add some tags" multiple>
|
||||
<?php
|
||||
|
||||
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 1 $existing_tags_query ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags_select)) {
|
||||
$tag_id_select = intval($row['tag_id']);
|
||||
$tag_name_select = escapeHtml($row['tag_name']);
|
||||
?>
|
||||
<option value="<?= $tag_id_select ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name_select ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<?php if ($single_client_id && mysqli_num_rows($sql_tags_select) === 0) { ?>
|
||||
<div class="form-text">This client already has every client tag.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_assign_client_tags" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i><?= $single_client_id ? 'Add Tags' : 'Assign' ?></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
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -116,30 +116,8 @@ $sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_ticket_
|
||||
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
|
||||
|
||||
// Tags - many to many relationship
|
||||
$contact_tag_name_display_array = array();
|
||||
$contact_tag_id_array = array();
|
||||
$sql_contact_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, contact_tags.tag_id, tag_name FROM contact_tags
|
||||
LEFT JOIN tags ON contact_tags.tag_id = tags.tag_id
|
||||
WHERE contact_id = $contact_id
|
||||
ORDER BY tag_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_contact_tags)) {
|
||||
|
||||
$contact_tag_id = intval($row['tag_id']);
|
||||
$contact_tag_name = escapeHtml($row['tag_name']);
|
||||
$contact_tag_color = escapeHtml($row['tag_color']);
|
||||
if (empty($contact_tag_color)) {
|
||||
$contact_tag_color = "dark";
|
||||
}
|
||||
$contact_tag_icon = escapeHtml($row['tag_icon']);
|
||||
if (empty($contact_tag_icon)) {
|
||||
$contact_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$contact_tag_id_array[] = $contact_tag_id;
|
||||
$contact_tag_name_display_array[] = "<a href='client_contacts.php?client_id=$client_id&q=$contact_tag_name'><span class='badge text-light p-1 me-1' style='background-color: $contact_tag_color;'><i class='fa fa-fw fa-$contact_tag_icon me-2'></i>$contact_tag_name</span></a>";
|
||||
}
|
||||
$contact_tags_display = implode('', $contact_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$contact_tags_display = tagPills('contact', $contact_id, tagsForRecord('contact', $contact_id), "contacts.php?client_id=$client_id&");
|
||||
|
||||
// Notes - 1 to 1 relationship
|
||||
$sql_related_notes = mysqli_query($mysqli, "SELECT contact_note, contact_note_created_at, contact_note_type, user_name FROM contact_notes
|
||||
@@ -483,24 +461,8 @@ ob_start();
|
||||
|
||||
$device_icon = getAssetIcon($asset_type);
|
||||
|
||||
// Tags
|
||||
$asset_tag_name_display_array = array();
|
||||
$sql_asset_tags = mysqli_query($mysqli, "SELECT tag_color, tag_icon, tag_id, tag_name FROM asset_tags LEFT JOIN tags ON asset_tag_tag_id = tag_id WHERE asset_tag_asset_id = $asset_id ORDER BY tag_name ASC");
|
||||
while ($row2 = mysqli_fetch_assoc($sql_asset_tags)) {
|
||||
$asset_tag_id = intval($row2['tag_id']);
|
||||
$asset_tag_name = escapeHtml($row2['tag_name']);
|
||||
$asset_tag_color = escapeHtml($row2['tag_color']);
|
||||
if (empty($asset_tag_color)) {
|
||||
$asset_tag_color = "dark";
|
||||
}
|
||||
$asset_tag_icon = escapeHtml($row2['tag_icon']);
|
||||
if (empty($asset_tag_icon)) {
|
||||
$asset_tag_icon = "tag";
|
||||
}
|
||||
|
||||
$asset_tag_name_display_array[] = "<a href='assets.php?$client_url tags[]=$asset_tag_id'><span class='badge text-light p-1 me-1' style='background-color: $asset_tag_color;'><i class='fa fa-fw fa-$asset_tag_icon me-2'></i>$asset_tag_name</span></a>";
|
||||
}
|
||||
$asset_tags_display = implode('', $asset_tag_name_display_array);
|
||||
// Tags - functions/tags.php
|
||||
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
|
||||
$asset_favorite = intval($row['asset_favorite']);
|
||||
|
||||
?>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$contact_ids = array_map('intval', $_GET['contact_ids'] ?? []);
|
||||
|
||||
$count = count($contact_ids);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tags me-2"></i>Assign Tags for <strong><?= $count ?></strong> Contacts</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 ($contact_ids as $contact_id) { ?><input type="hidden" name="contact_ids[]" value="<?= $contact_id ?>"><?php } ?>
|
||||
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="bulk_remove_tags" value="0">
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_remove_tags" value="1">
|
||||
<label class="form-check-label text-danger">Remove Existing Tags</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tags</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
||||
<select class="form-select select2" name="bulk_tags[]" data-placeholder="Add some tags" multiple>
|
||||
<?php
|
||||
|
||||
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 3 ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags_select)) {
|
||||
$tag_id_select = intval($row['tag_id']);
|
||||
$tag_name_select = escapeHtml($row['tag_name']);
|
||||
?>
|
||||
<option value="<?= $tag_id_select ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name_select ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_assign_contact_tags" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Assign Tags</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
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$credential_ids = array_map('intval', $_GET['credential_ids'] ?? []);
|
||||
|
||||
$count = count($credential_ids);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tags me-2"></i>Assign Tags to <strong><?= $count ?></strong> Credentials</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 ($credential_ids as $credential_id) { ?><input type="hidden" name="credential_ids[]" value="<?= $credential_id ?>"><?php } ?>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="bulk_remove_tags" value="0">
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_remove_tags" value="1">
|
||||
<label class="form-check-label text-danger">Remove Existing Tags</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tags</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
||||
<select class="form-select select2" name="bulk_tags[]" data-placeholder="Add some tags" multiple>
|
||||
<?php
|
||||
|
||||
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 4 ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags_select)) {
|
||||
$tag_id_select = intval($row['tag_id']);
|
||||
$tag_name_select = escapeHtml($row['tag_name']);
|
||||
?>
|
||||
<option value="<?= $tag_id_select ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name_select ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
<button class="btn btn-secondary ajax-modal" title="New tag" type="button"
|
||||
data-modal-size="sm"
|
||||
data-modal-url="../admin/modals/tag/tag_add.php?type=4">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_assign_credential_tags" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Assign Tags</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
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$location_ids = array_map('intval', $_GET['location_ids'] ?? []);
|
||||
|
||||
$count = count($location_ids);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tags me-2"></i>Assign Tags to <strong><?= $count ?></strong> Locations</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 ($location_ids as $location_id) { ?><input type="hidden" name="location_ids[]" value="<?= $location_id ?>"><?php } ?>
|
||||
<input type="hidden" name="bulk_remove_tags" value="0">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_remove_tags" value="1">
|
||||
<label class="form-check-label text-danger">Remove Existing Tags</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Tags</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
||||
<select class="form-select select2" name="bulk_tags[]" data-placeholder="Add some tags" multiple>
|
||||
<?php
|
||||
|
||||
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 2 ORDER BY tag_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags_select)) {
|
||||
$tag_id_select = intval($row['tag_id']);
|
||||
$tag_name_select = escapeHtml($row['tag_name']);
|
||||
?>
|
||||
<option value="<?= $tag_id_select ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name_select ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_assign_location_tags" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Assign Tags</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
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -359,54 +359,6 @@ if (isset($_GET['delete_asset_note'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_asset_tags'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
if (isset($_POST['asset_ids'])) {
|
||||
|
||||
$count = count($_POST['asset_ids']);
|
||||
|
||||
foreach($_POST['asset_ids'] as $asset_id) {
|
||||
$asset_id = intval($asset_id);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id FROM assets WHERE asset_id = $asset_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_name = escapeSql($row['asset_name']);
|
||||
$client_id = intval($row['asset_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
if($_POST['remove_tags']) {
|
||||
mysqli_query($mysqli, "DELETE FROM asset_tags WHERE asset_tag_asset_id = $asset_id");
|
||||
}
|
||||
|
||||
if (isset($_POST['tags'])) {
|
||||
foreach($_POST['tags'] as $tag) {
|
||||
$tag = intval($tag);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT 1 FROM asset_tags WHERE asset_tag_asset_id = $asset_id AND asset_tag_tag_id = $tag");
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO asset_tags SET asset_tag_asset_id = $asset_id, asset_tag_tag_id = $tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Asset", "Edit", "$session_name added tags to asset $asset_name", $client_id, $asset_id);
|
||||
|
||||
}
|
||||
|
||||
logAudit("Asset", "Bulk Edit", "$session_name added tags for $asset_count assets", $client_id);
|
||||
|
||||
flashAlert("Assigned tags for <strong>$count</strong> assets");
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_asset_location'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -1003,60 +1003,6 @@ if (isset($_POST['bulk_edit_client_net_terms'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_client_tags'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
if (isset($_POST['client_ids'])) {
|
||||
|
||||
// Check every selected client before changing any of them
|
||||
foreach ($_POST['client_ids'] as $client_id) {
|
||||
enforceClientAccess(intval($client_id));
|
||||
}
|
||||
|
||||
$count = count($_POST['client_ids']);
|
||||
|
||||
foreach($_POST['client_ids'] as $client_id) {
|
||||
$client_id = intval($client_id);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT client_name FROM clients WHERE client_id = $client_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$client_name = escapeSql($row['client_name']);
|
||||
|
||||
if ($_POST['bulk_remove_tags']) {
|
||||
mysqli_query($mysqli, "DELETE FROM client_tags WHERE client_id = $client_id");
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_tags'])) {
|
||||
foreach($_POST['bulk_tags'] as $tag) {
|
||||
$tag = intval($tag);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT 1 FROM client_tags WHERE client_id = $client_id AND tag_id = $tag");
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO client_tags SET client_id = $client_id, tag_id = $tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Client", "Edit", "$session_name added tags to $client_name", $client_id, $client_id);
|
||||
|
||||
}
|
||||
|
||||
if ($count === 1) {
|
||||
flashAlert("Tags added to <strong>" . escapeHtml($row['client_name']) . "</strong>");
|
||||
} else {
|
||||
logAudit("Client", "Bulk Edit", "$session_name added tags for $count clients", $client_id);
|
||||
|
||||
flashAlert("Assigned tags for <strong>$count</strong> clients");
|
||||
}
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_send_client_email']) && isset($_POST['client_ids'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -511,59 +511,6 @@ if (isset($_POST['bulk_edit_contact_role'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_contact_tags'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
// Assign Location to Selected Contacts
|
||||
if (isset($_POST['contact_ids'])) {
|
||||
|
||||
// Get Selected Contacts Count
|
||||
$count = count($_POST['contact_ids']);
|
||||
|
||||
foreach($_POST['contact_ids'] as $contact_id) {
|
||||
$contact_id = intval($contact_id);
|
||||
|
||||
// Get Contact Details for Logging
|
||||
$sql = mysqli_query($mysqli,"SELECT contact_name, contact_client_id FROM contacts WHERE contact_id = $contact_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$contact_name = escapeSql($row['contact_name']);
|
||||
$client_id = intval($row['contact_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
if($_POST['bulk_remove_tags']) {
|
||||
// Delete tags if chosed to do so
|
||||
mysqli_query($mysqli, "DELETE FROM contact_tags WHERE contact_id = $contact_id");
|
||||
}
|
||||
|
||||
// Add new tags
|
||||
if (isset($_POST['bulk_tags'])) {
|
||||
foreach($_POST['bulk_tags'] as $tag) {
|
||||
$tag = intval($tag);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT 1 FROM contact_tags WHERE contact_id = $contact_id AND tag_id = $tag");
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO contact_tags SET contact_id = $contact_id, tag_id = $tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Contact", "Edit", "$session_name added tags to $contact_name", $client_id, $contact_id);
|
||||
|
||||
} // End Assign Location Loop
|
||||
|
||||
logAudit("Contact", "Bulk Edit", "$session_name added tags for $contact_count contacts", $client_id);
|
||||
|
||||
flashAlert("You assigned tags for <strong>$count</strong> contacts");
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['send_bulk_mail_now'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -161,60 +161,6 @@ if (isset($_GET['delete_credential'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_credential_tags'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_credential', 2);
|
||||
|
||||
// Assign tags to Selected Credentials
|
||||
if (isset($_POST['credential_ids'])) {
|
||||
|
||||
// Get Selected Credential Count
|
||||
$count = count($_POST['credential_ids']);
|
||||
|
||||
foreach($_POST['credential_ids'] as $credential_id) {
|
||||
$credential_id = intval($credential_id);
|
||||
|
||||
// Get Contact Details for Logging
|
||||
$sql = mysqli_query($mysqli,"SELECT credential_name, credential_client_id FROM credentials WHERE credential_id = $credential_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$credential_name = escapeSql($row['credential_name']);
|
||||
$client_id = intval($row['credential_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
if($_POST['bulk_remove_tags']) {
|
||||
// Delete tags if chosed to do so
|
||||
mysqli_query($mysqli, "DELETE FROM credential_tags WHERE credential_id = $credential_id");
|
||||
}
|
||||
|
||||
// Add new tags
|
||||
if (isset($_POST['bulk_tags'])) {
|
||||
foreach($_POST['bulk_tags'] as $tag) {
|
||||
$tag = intval($tag);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT 1 FROM credential_tags WHERE credential_id = $credential_id AND tag_id = $tag");
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO credential_tags SET credential_id = $credential_id, tag_id = $tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Credential", "Edit", "$session_name added tags to $credential_name", $client_id, $credential_id);
|
||||
|
||||
flashAlert("Assigned tags for <strong>$count</strong> credentials");
|
||||
|
||||
} // End Assign Loop
|
||||
|
||||
logAudit("Credential", "Bulk Edit", "$session_name added tags to $count credentials", $client_id);
|
||||
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_favorite_credentials'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -209,60 +209,6 @@ if(isset($_GET['delete_location'])){
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_assign_location_tags'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
// Assign Tags to Selected
|
||||
if (isset($_POST['location_ids'])) {
|
||||
|
||||
// Get Selected Count
|
||||
$count = count($_POST['location_ids']);
|
||||
|
||||
foreach($_POST['location_ids'] as $location_id) {
|
||||
$location_id = intval($location_id);
|
||||
|
||||
// Get Contact Details for Logging
|
||||
$sql = mysqli_query($mysqli,"SELECT location_name, location_client_id FROM locations WHERE location_id = $location_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$location_name = escapeSql($row['location_name']);
|
||||
$client_id = intval($row['location_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
if($_POST['bulk_remove_tags']) {
|
||||
// Delete tags if chosed to do so
|
||||
mysqli_query($mysqli, "DELETE FROM location_tags WHERE location_id = $location_id");
|
||||
}
|
||||
|
||||
// Add new tags
|
||||
if (isset($_POST['bulk_tags'])) {
|
||||
foreach($_POST['bulk_tags'] as $tag) {
|
||||
$tag = intval($tag);
|
||||
|
||||
$sql = mysqli_query($mysqli,"SELECT 1 FROM location_tags WHERE location_id = $location_id AND tag_id = $tag");
|
||||
if (mysqli_num_rows($sql) == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO location_tags SET location_id = $location_id, tag_id = $tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Location", "Edit", "$session_name assigned tags to location $location_name", $client_id, $location_id);
|
||||
|
||||
} // End Assign Location Loop
|
||||
|
||||
logAudit("Location", "Bulk Edit", "$session_name assigned tags to $count location(s)", $client_id);
|
||||
|
||||
flashAlert("Assigned tags for <strong>$count</strong> locations");
|
||||
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_archive_locations'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -2785,41 +2785,78 @@ a.itflow-dash-rail-item:focus-visible {
|
||||
font-weight: 600;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.itflow-client-tag {
|
||||
/* Tags - functions/tags.php tagPills(), on every list and detail page that has
|
||||
them. A pill opens the list filtered to that tag; the dashed "+" at the end
|
||||
opens the tag picker (js/tag_picker.js). The group sits above stretched-link
|
||||
row and cell links so the pills stay clickable on top of them. */
|
||||
.itflow-tags {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.itflow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .2rem;
|
||||
padding: .05rem .45rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1rem;
|
||||
font-size: .75rem;
|
||||
font-weight: normal;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
background-color: var(--itflow-tag-color, var(--bs-secondary));
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.itflow-client-tag:hover,
|
||||
.itflow-client-tag:focus-visible {
|
||||
.itflow-tag:hover,
|
||||
.itflow-tag:focus-visible {
|
||||
color: #fff;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
/* The whole client cell is a link (stretched-link); the pills stay clickable on top of it */
|
||||
.itflow-clients-client .itflow-client-tag {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
/* "+ Tag" / "+" at the end of the tags - opens Add Tags for that client */
|
||||
.itflow-client-tag-add {
|
||||
.itflow-tag-add {
|
||||
color: var(--bs-secondary-color);
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--bs-border-color);
|
||||
border-color: var(--bs-border-color);
|
||||
border-style: dashed;
|
||||
cursor: pointer;
|
||||
}
|
||||
.itflow-client-tag-add:hover,
|
||||
.itflow-client-tag-add:focus-visible {
|
||||
.itflow-tag-add:hover,
|
||||
.itflow-tag-add:focus-visible,
|
||||
.itflow-tag-add[aria-expanded="true"] {
|
||||
color: var(--itflow-accent, var(--bs-primary));
|
||||
border-color: currentColor;
|
||||
filter: none;
|
||||
}
|
||||
/* The picker - the select picker's panel, plus the bulk note and the dash for
|
||||
a tag only some of the selected records have */
|
||||
.itflow-tag-panel-note {
|
||||
padding: .5rem .75rem;
|
||||
border-bottom: 1px solid var(--bs-border-color-translucent);
|
||||
font-size: .8125rem;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
.itflow-tag-panel .itflow-select-footer {
|
||||
gap: .25rem;
|
||||
padding-top: .375rem;
|
||||
padding-bottom: .375rem;
|
||||
}
|
||||
.itflow-tag-done {
|
||||
--bs-btn-padding-x: .75rem;
|
||||
}
|
||||
/* On a phone the sheet's own header has the Done */
|
||||
.itflow-select-panel.is-sheet .itflow-tag-done {
|
||||
display: none;
|
||||
}
|
||||
.itflow-select-option.is-mixed .itflow-select-checkbox {
|
||||
border-color: var(--itflow-accent, var(--bs-primary));
|
||||
color: var(--itflow-accent, var(--bs-primary));
|
||||
}
|
||||
.itflow-clients-street {
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
@@ -31,3 +31,4 @@ require_once __DIR__ . '/functions/search.php';
|
||||
require_once __DIR__ . '/functions/notifications.php';
|
||||
require_once __DIR__ . '/functions/dashboard.php';
|
||||
require_once __DIR__ . '/functions/client_overview.php';
|
||||
require_once __DIR__ . '/functions/tags.php';
|
||||
|
||||
168
functions/tags.php
Normal file
168
functions/tags.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Tags on clients, locations, contacts, credentials and assets.
|
||||
*
|
||||
* Every list and detail page draws a record's tags with tagPills(): coloured
|
||||
* pills that open the list filtered to that tag, then a "+" pill that opens
|
||||
* the tag picker (js/tag_picker.js). The picker ticks tags on and off in
|
||||
* place through agent/ajax.php and redraws the pills from the HTML this file
|
||||
* returns, so the page never reloads and nothing goes through a modal.
|
||||
*/
|
||||
|
||||
/* What differs between the five kinds of tagged record. */
|
||||
function tagEntity($entity) {
|
||||
$entities = [
|
||||
'client' => [
|
||||
'type' => 1, 'label' => 'client', 'module' => 'module_client',
|
||||
'link_table' => 'client_tags', 'link_record' => 'client_id', 'link_tag' => 'tag_id',
|
||||
'table' => 'clients', 'id' => 'client_id', 'client' => 'client_id', 'name' => 'client_name',
|
||||
'log' => 'Client', 'list' => 'clients.php'
|
||||
],
|
||||
'location' => [
|
||||
'type' => 2, 'label' => 'location', 'module' => 'module_client',
|
||||
'link_table' => 'location_tags', 'link_record' => 'location_id', 'link_tag' => 'tag_id',
|
||||
'table' => 'locations', 'id' => 'location_id', 'client' => 'location_client_id', 'name' => 'location_name',
|
||||
'log' => 'Location', 'list' => 'locations.php'
|
||||
],
|
||||
'contact' => [
|
||||
'type' => 3, 'label' => 'contact', 'module' => 'module_client',
|
||||
'link_table' => 'contact_tags', 'link_record' => 'contact_id', 'link_tag' => 'tag_id',
|
||||
'table' => 'contacts', 'id' => 'contact_id', 'client' => 'contact_client_id', 'name' => 'contact_name',
|
||||
'log' => 'Contact', 'list' => 'contacts.php'
|
||||
],
|
||||
'credential' => [
|
||||
'type' => 4, 'label' => 'credential', 'module' => 'module_credential',
|
||||
'link_table' => 'credential_tags', 'link_record' => 'credential_id', 'link_tag' => 'tag_id',
|
||||
'table' => 'credentials', 'id' => 'credential_id', 'client' => 'credential_client_id', 'name' => 'credential_name',
|
||||
'log' => 'Credential', 'list' => 'credentials.php'
|
||||
],
|
||||
'asset' => [
|
||||
'type' => 5, 'label' => 'asset', 'module' => 'module_client',
|
||||
'link_table' => 'asset_tags', 'link_record' => 'asset_tag_asset_id', 'link_tag' => 'asset_tag_tag_id',
|
||||
'table' => 'assets', 'id' => 'asset_id', 'client' => 'asset_client_id', 'name' => 'asset_name',
|
||||
'log' => 'Asset', 'list' => 'assets.php'
|
||||
],
|
||||
];
|
||||
|
||||
return $entities[$entity] ?? null;
|
||||
}
|
||||
|
||||
/* Tag rows (tag_id, tag_name, tag_color, tag_icon) for each record, in one query. */
|
||||
function tagsForRecords($entity, array $record_ids) {
|
||||
global $mysqli;
|
||||
|
||||
$config = tagEntity($entity);
|
||||
$record_ids = array_values(array_unique(array_filter(array_map('intval', $record_ids))));
|
||||
$tags = array_fill_keys($record_ids, []);
|
||||
if (!$config || !$record_ids) {
|
||||
return $tags;
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli,
|
||||
"SELECT {$config['link_record']} AS record_id, tags.tag_id, tag_name, tag_color, tag_icon
|
||||
FROM {$config['link_table']}
|
||||
JOIN tags ON tags.tag_id = {$config['link_table']}.{$config['link_tag']}
|
||||
WHERE {$config['link_record']} IN (" . implode(',', $record_ids) . ")
|
||||
ORDER BY tag_name ASC"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$tags[intval($row['record_id'])][] = $row;
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/* The tags of one record. */
|
||||
function tagsForRecord($entity, $record_id) {
|
||||
return tagsForRecords($entity, [$record_id])[intval($record_id)] ?? [];
|
||||
}
|
||||
|
||||
/*
|
||||
* The pills for one record, plus the "+" that opens the picker when the
|
||||
* agent may edit this kind of record.
|
||||
*
|
||||
* $filter is where a pill goes, up to the tag: "contacts.php?client_id=4&"
|
||||
* becomes contacts.php?client_id=4&tags[]=9. Empty means the list page for
|
||||
* this kind of record across all clients. Nothing at all comes back for a
|
||||
* record with no tags when the agent cannot add any.
|
||||
*/
|
||||
function tagPills($entity, $record_id, array $tags, $filter = '') {
|
||||
$config = tagEntity($entity);
|
||||
if (!$config) {
|
||||
return '';
|
||||
}
|
||||
$record_id = intval($record_id);
|
||||
$filter = tagFilterBase($entity, $filter);
|
||||
$can_edit = lookupUserPermission($config['module']) >= 2;
|
||||
if (!$tags && !$can_edit) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<span class="itflow-tags" data-tag-entity="' . $entity . '" data-tag-record="' . $record_id . '" data-tag-filter="' . escapeHtml($filter) . '">';
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$tag_id = intval($tag['tag_id']);
|
||||
$name = escapeHtml($tag['tag_name']);
|
||||
$color = preg_match('/^(#[0-9a-f]{3,8}|[a-z]{3,24})$/i', trim((string) $tag['tag_color'])) ? trim($tag['tag_color']) : 'var(--bs-secondary)';
|
||||
$icon = preg_replace('/[^a-z0-9-]/', '', strtolower((string) $tag['tag_icon'])) ?: 'tag';
|
||||
$html .= '<a href="' . escapeHtml($filter) . 'tags[]=' . $tag_id . '" class="itflow-tag" style="--itflow-tag-color: ' . $color . '" title="Show ' . $config['label'] . 's tagged ' . $name . '">'
|
||||
. '<i class="fas fa-fw fa-' . $icon . '" aria-hidden="true"></i>' . $name . '</a>';
|
||||
}
|
||||
|
||||
if ($can_edit) {
|
||||
$html .= '<button type="button" class="itflow-tag itflow-tag-add" data-tag-picker data-tag-entity="' . $entity . '" data-tag-ids="' . $record_id . '" data-csrf="' . escapeHtml($_SESSION['csrf_token'] ?? '') . '" title="' . ($tags ? 'Change tags' : 'Add tags') . '">'
|
||||
. '<i class="fas fa-fw fa-plus" aria-hidden="true"></i>' . ($tags ? '<span class="visually-hidden">Change tags</span>' : 'Tag') . '</button>';
|
||||
}
|
||||
|
||||
return $html . '</span>';
|
||||
}
|
||||
|
||||
/* A pill link base - a page of ours with a query string, or the default list. */
|
||||
function tagFilterBase($entity, $filter) {
|
||||
$config = tagEntity($entity);
|
||||
$filter = (string) $filter;
|
||||
if ($filter === '' || !preg_match('/^[a-z_]+\.php\?([a-z_]+=[0-9a-z_]*&)*$/i', $filter)) {
|
||||
return $config['list'] . '?';
|
||||
}
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check the agent may tag these records: edit rights on the module, and
|
||||
* access to the client each record belongs to. Returns [record_id => name]
|
||||
* for the ones that exist, or false if any is out of bounds.
|
||||
*/
|
||||
function tagRecordsEditable($entity, array $record_ids) {
|
||||
global $mysqli;
|
||||
|
||||
$config = tagEntity($entity);
|
||||
$record_ids = array_values(array_unique(array_filter(array_map('intval', $record_ids))));
|
||||
if (!$config || !$record_ids || lookupUserPermission($config['module']) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$records = [];
|
||||
$sql = mysqli_query($mysqli,
|
||||
"SELECT {$config['id']} AS record_id, {$config['client']} AS client_id, {$config['name']} AS record_name
|
||||
FROM {$config['table']} WHERE {$config['id']} IN (" . implode(',', $record_ids) . ")"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
if (!hasClientAccess(intval($row['client_id']))) {
|
||||
return false;
|
||||
}
|
||||
$records[intval($row['record_id'])] = ['name' => $row['record_name'], 'client_id' => intval($row['client_id'])];
|
||||
}
|
||||
|
||||
return count($records) === count($record_ids) ? $records : false;
|
||||
}
|
||||
|
||||
/* A colour for a tag made from the picker - the next one round the palette. */
|
||||
function tagNextColor($type) {
|
||||
global $mysqli;
|
||||
|
||||
$palette = ['#0d6efd', '#198754', '#6f42c1', '#fd7e14', '#d63384', '#20c997', '#dc3545', '#0dcaf0', '#6610f2', '#ffc107'];
|
||||
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(tag_id) FROM tags WHERE tag_type = " . intval($type)));
|
||||
|
||||
return $palette[intval($row[0]) % count($palette)];
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
<script src="/libs/sweetalert2/js/sweetalert2.min.js"></script>
|
||||
<script src="/js/autocomplete.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
<script src="/js/tag_picker.js"></script>
|
||||
<script src="/js/ajax_modal.js"></script>
|
||||
<script src="/js/confirm_modal.js"></script>
|
||||
<script src="/js/date_filter.js"></script>
|
||||
|
||||
565
js/tag_picker.js
Normal file
565
js/tag_picker.js
Normal file
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* Tag picker - the "+" at the end of a record's tags, and "Assign Tags" in
|
||||
* a list's bulk Action menu. Replaces the Assign Tags modals.
|
||||
*
|
||||
* Opens a panel under the button (a sheet from the bottom on a phone) with
|
||||
* every tag this kind of record can have, ticked if it has it. Ticking or
|
||||
* unticking saves straight away through agent/ajax.php and the pills on the
|
||||
* page redraw from what the server sends back, so the list never reloads and
|
||||
* you can tick several in a row. Admins can type a new tag name and make it
|
||||
* on the spot; "Manage tags" goes to Admin > Tags.
|
||||
*
|
||||
* In bulk, a tag some of the selected records have shows a dash: ticking it
|
||||
* gives it to all of them, unticking takes it off all of them.
|
||||
*
|
||||
* Markup comes from tagPills() in functions/tags.php:
|
||||
* <button data-tag-picker data-tag-entity="contact" data-tag-ids="12" data-csrf="...">
|
||||
* or for bulk, data-tag-bulk instead of data-tag-ids (the ticked .bulk-select
|
||||
* boxes on the page are the records).
|
||||
*
|
||||
* Styled with the select picker's panel classes (css: "Select picker"), so
|
||||
* the two look and behave alike.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.itflowTagPicker) {
|
||||
return; // re-run by an ajax modal - the listeners are already in place
|
||||
}
|
||||
|
||||
var sheetMedia = window.matchMedia('(max-width: 575.98px)');
|
||||
var finePointer = window.matchMedia('(pointer: fine)');
|
||||
var open = null; // the one open picker
|
||||
|
||||
function make(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) {
|
||||
node.className = className;
|
||||
}
|
||||
if (text) {
|
||||
node.textContent = text;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function fold(text) {
|
||||
return String(text || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
function csrfFor(button) {
|
||||
if (button.dataset.csrf) {
|
||||
return button.dataset.csrf;
|
||||
}
|
||||
var input = document.querySelector('input[name="csrf_token"]');
|
||||
return input ? input.value : '';
|
||||
}
|
||||
|
||||
function bulkIds() {
|
||||
return Array.prototype.map.call(document.querySelectorAll('input.bulk-select:checked'), function (box) {
|
||||
return box.value;
|
||||
});
|
||||
}
|
||||
|
||||
/* Swap in the pills the server sent, wherever that record is on the page */
|
||||
function redraw(entity, pills) {
|
||||
Object.keys(pills).forEach(function (id) {
|
||||
document.querySelectorAll('.itflow-tags[data-tag-entity="' + entity + '"][data-tag-record="' + id + '"]').forEach(function (old) {
|
||||
var holder = document.createElement('div');
|
||||
holder.innerHTML = pills[id];
|
||||
var fresh = holder.firstElementChild;
|
||||
if (fresh) {
|
||||
// Keep this spot's own filter links
|
||||
fresh.setAttribute('data-tag-filter', old.getAttribute('data-tag-filter') || fresh.getAttribute('data-tag-filter'));
|
||||
// The open picker hangs off the + in the pills being replaced -
|
||||
// move it to the new +, or the next redraw finds it detached
|
||||
// and closes the panel
|
||||
if (open && old.contains(open.anchor)) {
|
||||
var plus = fresh.querySelector('[data-tag-picker]');
|
||||
if (plus) {
|
||||
plus.setAttribute('aria-expanded', 'true');
|
||||
open.anchor = plus;
|
||||
open.button = plus;
|
||||
}
|
||||
}
|
||||
old.replaceWith(fresh);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterFor(entity, ids) {
|
||||
var spot = document.querySelector('.itflow-tags[data-tag-entity="' + entity + '"][data-tag-record="' + ids[0] + '"]');
|
||||
return spot ? spot.getAttribute('data-tag-filter') || '' : '';
|
||||
}
|
||||
|
||||
function Picker(button) {
|
||||
this.button = button;
|
||||
this.entity = button.dataset.tagEntity;
|
||||
this.bulk = button.hasAttribute('data-tag-bulk');
|
||||
this.ids = this.bulk ? bulkIds() : String(button.dataset.tagIds || '').split(',').filter(Boolean);
|
||||
this.csrf = csrfFor(button);
|
||||
// Bulk opens from a menu item that closes - hang the panel off the Action button
|
||||
this.anchor = this.bulk ? (document.querySelector('#bulkActionButton > button') || button) : button;
|
||||
this.query = '';
|
||||
this.rows = [];
|
||||
this.active = -1;
|
||||
this.busy = 0;
|
||||
}
|
||||
|
||||
Picker.prototype.build = function () {
|
||||
var self = this;
|
||||
var panel = this.panel = make('div', 'itflow-select-panel is-multiple itflow-tag-panel');
|
||||
panel.setAttribute('role', 'dialog');
|
||||
panel.setAttribute('aria-label', 'Tags');
|
||||
|
||||
var head = make('div', 'itflow-select-sheet-head');
|
||||
head.appendChild(make('span', 'itflow-select-sheet-title', 'Tags'));
|
||||
var done = make('button', 'btn btn-sm btn-link itflow-select-done', 'Done');
|
||||
done.type = 'button';
|
||||
done.addEventListener('click', function () {
|
||||
self.close(false);
|
||||
});
|
||||
head.appendChild(done);
|
||||
panel.appendChild(head);
|
||||
|
||||
if (this.bulk) {
|
||||
panel.appendChild(make('div', 'itflow-tag-panel-note', this.ids.length + ' selected - ticked tags go on all of them, unticked come off'));
|
||||
}
|
||||
|
||||
var search = make('div', 'itflow-select-search');
|
||||
search.appendChild(make('i', 'fas fa-search'));
|
||||
var input = this.input = make('input');
|
||||
input.type = 'text';
|
||||
input.autocomplete = 'off';
|
||||
input.spellcheck = false;
|
||||
input.setAttribute('aria-label', 'Search tags');
|
||||
input.addEventListener('input', function () {
|
||||
self.query = input.value;
|
||||
self.draw(false);
|
||||
});
|
||||
input.addEventListener('keydown', function (e) {
|
||||
self.key(e);
|
||||
});
|
||||
search.appendChild(input);
|
||||
panel.appendChild(search);
|
||||
|
||||
var list = this.list = make('div', 'itflow-select-list');
|
||||
list.setAttribute('role', 'listbox');
|
||||
list.setAttribute('aria-multiselectable', 'true');
|
||||
list.setAttribute('aria-label', 'Tags');
|
||||
list.addEventListener('click', function (e) {
|
||||
var node = e.target.closest('[data-row]');
|
||||
if (node) {
|
||||
self.pick(parseInt(node.getAttribute('data-row'), 10));
|
||||
}
|
||||
});
|
||||
list.addEventListener('mousemove', function (e) {
|
||||
var node = e.target.closest('[data-row]');
|
||||
if (node) {
|
||||
self.setActive(parseInt(node.getAttribute('data-row'), 10), false);
|
||||
}
|
||||
});
|
||||
panel.appendChild(list);
|
||||
|
||||
var footer = this.footer = make('div', 'itflow-select-footer');
|
||||
panel.appendChild(footer);
|
||||
|
||||
panel.addEventListener('mousedown', function (e) {
|
||||
if (e.target !== input && !e.target.closest('a, button')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.backdrop = make('div', 'itflow-select-backdrop');
|
||||
};
|
||||
|
||||
Picker.prototype.show = function () {
|
||||
var self = this;
|
||||
if (!this.entity || !this.ids.length) {
|
||||
return;
|
||||
}
|
||||
this.build();
|
||||
open = this;
|
||||
this.sheet = sheetMedia.matches;
|
||||
this.panel.classList.toggle('is-sheet', this.sheet);
|
||||
this.input.placeholder = 'Loading...';
|
||||
this.list.appendChild(make('div', 'itflow-select-empty', 'Loading tags...'));
|
||||
|
||||
var host = this.anchor.closest('.modal, .offcanvas') || document.body;
|
||||
if (this.sheet) {
|
||||
host.appendChild(this.backdrop);
|
||||
}
|
||||
host.appendChild(this.panel);
|
||||
this.anchor.setAttribute('aria-expanded', 'true');
|
||||
this.place();
|
||||
|
||||
itflowGet('ajax.php', { tag_picker: 1, entity: this.entity, ids: this.ids }, function (text) {
|
||||
var data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
if (open !== self) {
|
||||
return;
|
||||
}
|
||||
if (!data || !data.tags) {
|
||||
self.fail('Could not load the tags.');
|
||||
self.close(false);
|
||||
return;
|
||||
}
|
||||
self.data = data;
|
||||
self.input.placeholder = data.create ? 'Search or make a tag' : 'Search tags';
|
||||
self.draw(false);
|
||||
if (finePointer.matches) {
|
||||
self.input.focus({ preventScroll: true });
|
||||
}
|
||||
}, function () {
|
||||
self.fail('Could not load the tags.');
|
||||
self.close(false);
|
||||
});
|
||||
};
|
||||
|
||||
Picker.prototype.close = function (refocus) {
|
||||
if (open !== this) {
|
||||
return;
|
||||
}
|
||||
open = null;
|
||||
this.panel.remove();
|
||||
this.backdrop.remove();
|
||||
this.anchor.setAttribute('aria-expanded', 'false');
|
||||
if (refocus && this.anchor.isConnected) {
|
||||
this.anchor.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
Picker.prototype.fail = function (message) {
|
||||
if (typeof itflowToast === 'function') {
|
||||
itflowToast(message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
Picker.prototype.place = function () {
|
||||
if (this.sheet || open !== this) {
|
||||
return;
|
||||
}
|
||||
var anchor = this.anchor.isConnected ? this.anchor : null;
|
||||
if (!anchor) {
|
||||
this.close(false);
|
||||
return;
|
||||
}
|
||||
var rect = anchor.getBoundingClientRect();
|
||||
var viewWidth = document.documentElement.clientWidth;
|
||||
var viewHeight = window.innerHeight;
|
||||
var width = Math.min(288, viewWidth - 16);
|
||||
var panel = this.panel;
|
||||
panel.style.width = width + 'px';
|
||||
panel.style.left = Math.min(Math.max(8, rect.left), viewWidth - width - 8) + 'px';
|
||||
this.list.style.maxHeight = '';
|
||||
var height = panel.offsetHeight;
|
||||
var below = viewHeight - rect.bottom - 12;
|
||||
var above = rect.top - 12;
|
||||
var up = height > below && above > below;
|
||||
var room = up ? above : below;
|
||||
if (height > room) {
|
||||
this.list.style.maxHeight = Math.max(96, this.list.offsetHeight - (height - room)) + 'px';
|
||||
height = panel.offsetHeight;
|
||||
}
|
||||
panel.style.top = (up ? rect.top - 4 - height : rect.bottom + 4) + 'px';
|
||||
panel.classList.toggle('is-above', up);
|
||||
};
|
||||
|
||||
/* The checklist, filtered by the search, and the "make it" row. */
|
||||
Picker.prototype.draw = function (keep) {
|
||||
var self = this;
|
||||
var data = this.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
var list = this.list;
|
||||
var scroll = list.scrollTop;
|
||||
var activeId = keep && this.rows[this.active] ? this.rows[this.active].id : null;
|
||||
var terms = fold(this.query.trim()).split(/\s+/).filter(Boolean);
|
||||
var exact = false;
|
||||
|
||||
list.textContent = '';
|
||||
this.rows = [];
|
||||
|
||||
data.tags.forEach(function (tag) {
|
||||
var name = fold(tag.name);
|
||||
if (terms.length && !terms.every(function (t) { return name.indexOf(t) !== -1; })) {
|
||||
return;
|
||||
}
|
||||
if (name === fold(self.query.trim())) {
|
||||
exact = true;
|
||||
}
|
||||
var all = tag.chosen >= data.records;
|
||||
var some = tag.chosen > 0 && !all;
|
||||
var index = self.rows.length;
|
||||
var node = make('div', 'itflow-select-option' + (all ? ' is-selected' : '') + (some ? ' is-mixed' : ''));
|
||||
node.setAttribute('role', 'option');
|
||||
node.setAttribute('data-row', index);
|
||||
node.setAttribute('aria-selected', all ? 'true' : 'false');
|
||||
node.id = 'itflow-tag-option-' + tag.id;
|
||||
|
||||
var box = make('span', 'itflow-select-checkbox');
|
||||
box.appendChild(make('i', some ? 'fas fa-minus' : 'fas fa-check'));
|
||||
node.appendChild(box);
|
||||
|
||||
var badge = make('span', 'itflow-select-badge');
|
||||
badge.style.setProperty('--itflow-select-color', tag.color || 'var(--bs-secondary)');
|
||||
badge.appendChild(make('i', 'fas fa-' + tag.icon));
|
||||
badge.setAttribute('aria-hidden', 'true');
|
||||
node.appendChild(badge);
|
||||
|
||||
var text = make('span', 'itflow-select-text');
|
||||
text.appendChild(make('span', 'itflow-select-label', tag.name));
|
||||
if (some) {
|
||||
text.appendChild(make('span', 'itflow-select-description', tag.chosen + ' of ' + data.records));
|
||||
}
|
||||
node.appendChild(text);
|
||||
list.appendChild(node);
|
||||
self.rows.push({ node: node, id: tag.id, tag: tag });
|
||||
});
|
||||
|
||||
var typed = this.query.trim();
|
||||
if (data.create && typed && !exact) {
|
||||
var index = this.rows.length;
|
||||
var create = make('div', 'itflow-select-option itflow-select-create');
|
||||
create.setAttribute('role', 'option');
|
||||
create.setAttribute('data-row', index);
|
||||
create.id = 'itflow-tag-option-new';
|
||||
var plus = make('span', 'itflow-select-icon');
|
||||
plus.appendChild(make('i', 'fas fa-plus'));
|
||||
create.appendChild(plus);
|
||||
var createText = make('span', 'itflow-select-text');
|
||||
createText.appendChild(make('span', 'itflow-select-label', 'Make tag "' + typed + '"'));
|
||||
create.appendChild(createText);
|
||||
list.appendChild(create);
|
||||
this.rows.push({ node: create, create: typed });
|
||||
}
|
||||
|
||||
if (!this.rows.length) {
|
||||
list.appendChild(make('div', 'itflow-select-empty', typed ? 'No tags match' : 'No tags yet' + (data.create ? ' - type a name to make one' : '')));
|
||||
}
|
||||
|
||||
// Footer: how many the record has, and where tags are managed
|
||||
this.footer.textContent = '';
|
||||
var chosen = data.tags.filter(function (tag) { return tag.chosen >= data.records; }).length;
|
||||
this.footer.appendChild(make('span', 'me-auto', chosen ? chosen + ' tagged' : 'No tags'));
|
||||
if (data.manage) {
|
||||
var manage = make('a', 'btn btn-sm btn-link', 'Manage tags');
|
||||
manage.href = data.manage;
|
||||
this.footer.appendChild(manage);
|
||||
}
|
||||
// Saving happens on every tick, so this only closes - no reaching
|
||||
// past the panel to click off it
|
||||
var done = make('button', 'btn btn-sm btn-primary itflow-tag-done', 'Done');
|
||||
done.type = 'button';
|
||||
done.addEventListener('click', function () {
|
||||
self.close(true);
|
||||
});
|
||||
this.footer.appendChild(done);
|
||||
|
||||
var active = -1;
|
||||
if (activeId !== null) {
|
||||
this.rows.forEach(function (row, i) {
|
||||
if (row.id === activeId) {
|
||||
active = i;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (keep) {
|
||||
list.scrollTop = scroll;
|
||||
}
|
||||
this.setActive(active === -1 ? (this.rows.length ? 0 : -1) : active, false);
|
||||
this.place();
|
||||
};
|
||||
|
||||
Picker.prototype.setActive = function (index, scroll) {
|
||||
var old = this.rows[this.active];
|
||||
if (old) {
|
||||
old.node.classList.remove('is-active');
|
||||
}
|
||||
this.active = index;
|
||||
var row = this.rows[index];
|
||||
if (!row) {
|
||||
this.input.removeAttribute('aria-activedescendant');
|
||||
return;
|
||||
}
|
||||
row.node.classList.add('is-active');
|
||||
this.input.setAttribute('aria-activedescendant', row.node.id);
|
||||
if (scroll) {
|
||||
row.node.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
};
|
||||
|
||||
Picker.prototype.key = function (e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.setActive(Math.min(this.active + 1, this.rows.length - 1), true);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.setActive(Math.max(this.active - 1, 0), true);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
this.pick(this.active);
|
||||
break;
|
||||
case 'Escape':
|
||||
// Stop here, or a modal behind the picker closes too
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.close(true);
|
||||
break;
|
||||
case 'Tab':
|
||||
this.close(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/* Tick or untick a tag - or make the typed one - and save it. */
|
||||
Picker.prototype.pick = function (index) {
|
||||
var self = this;
|
||||
var row = this.rows[index];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
var data = this.data;
|
||||
var post = { csrf_token: this.csrf, entity: this.entity, ids: this.ids, filter: filterFor(this.entity, this.ids) };
|
||||
var before = null;
|
||||
|
||||
if (row.create) {
|
||||
post.tag_picker_create = 1;
|
||||
post.name = row.create;
|
||||
} else {
|
||||
// A mixed tag goes on everywhere; a full one comes off everywhere
|
||||
var on = row.tag.chosen < data.records;
|
||||
post.tag_picker_set = 1;
|
||||
post.tag_id = row.id;
|
||||
post.on = on ? 1 : 0;
|
||||
before = row.tag.chosen;
|
||||
row.tag.chosen = on ? data.records : 0;
|
||||
this.draw(true);
|
||||
}
|
||||
|
||||
this.busy++;
|
||||
itflowPost('ajax.php', post, function (text) {
|
||||
self.busy--;
|
||||
var result;
|
||||
try {
|
||||
result = JSON.parse(text);
|
||||
} catch (e) {
|
||||
result = null;
|
||||
}
|
||||
if (!result || !result.pills) {
|
||||
failed();
|
||||
return;
|
||||
}
|
||||
redraw(self.entity, result.pills);
|
||||
if (row.create && open === self) {
|
||||
// Load the list again so the new tag sits in its place, ticked
|
||||
self.query = '';
|
||||
self.input.value = '';
|
||||
itflowGet('ajax.php', { tag_picker: 1, entity: self.entity, ids: self.ids }, function (fresh) {
|
||||
try {
|
||||
self.data = JSON.parse(fresh);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (open === self) {
|
||||
self.draw(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, failed);
|
||||
|
||||
function failed() {
|
||||
self.busy--;
|
||||
if (before !== null) {
|
||||
row.tag.chosen = before;
|
||||
if (open === self) {
|
||||
self.draw(true);
|
||||
}
|
||||
}
|
||||
self.fail('The tag change was not saved - try again.');
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------- wiring ---------- */
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var button = e.target.closest('[data-tag-picker]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var wasOpen = open && open.button === button;
|
||||
if (open) {
|
||||
open.close(false);
|
||||
}
|
||||
if (!wasOpen) {
|
||||
new Picker(button).show();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Clicking off the panel closes it - and that click does nothing else.
|
||||
* Otherwise closing the picker by clicking beside it could land on a
|
||||
* row link, a pill or a button and take you somewhere.
|
||||
*/
|
||||
var swallow = false;
|
||||
function outside(e) {
|
||||
if (open && !open.panel.contains(e.target) && !e.target.closest('[data-tag-picker]')) {
|
||||
open.close(false);
|
||||
swallow = true;
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', outside, true);
|
||||
document.addEventListener('touchstart', outside, { capture: true, passive: true });
|
||||
document.addEventListener('click', function (e) {
|
||||
if (swallow) {
|
||||
swallow = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true);
|
||||
document.addEventListener('pointerdown', function (e) {
|
||||
// A fresh press that did not close a picker clears any leftover flag
|
||||
// (a mousedown whose click never came, e.g. a drag off the page)
|
||||
if (!open && !e.target.closest('[data-tag-picker]')) {
|
||||
swallow = false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
var placing = false;
|
||||
function replace() {
|
||||
if (!open || placing) {
|
||||
return;
|
||||
}
|
||||
placing = true;
|
||||
requestAnimationFrame(function () {
|
||||
placing = false;
|
||||
if (open) {
|
||||
open.place();
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', replace);
|
||||
document.addEventListener('scroll', function (e) {
|
||||
if (open && open.panel.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
replace();
|
||||
}, true);
|
||||
document.addEventListener('hide.bs.modal', function (e) {
|
||||
if (open && e.target.contains(open.anchor)) {
|
||||
open.close(false);
|
||||
}
|
||||
});
|
||||
|
||||
window.itflowTagPicker = true;
|
||||
})();
|
||||
Reference in New Issue
Block a user