Files
itflow/agent/modals/location/location_assign.php
johnnyq 57b8ced839 Locations overhaul for daily MSP work: new location page with header actions (Directions, Call, New ticket/contact/asset/network/rack/document/upload/trip prefilled to the location, Move here, Edit, Make primary, Archive/Restore/Delete), Getting there / Ask for / blur-to-save Access notes panels, and People, Assets, Networks, Racks, Tickets (open first), Documents, Files and History tabs with per-record unassign and remembered tab; new location quick look modal (glance, directions, access notes, People/Assets/Networks/Open tickets tabs); new Move here modal to bulk-move contacts, assets, networks and racks to a location showing where each currently is; rebuilt locations list with primary-first sort, photo/pin tile, maps link and Directions button, phone and hours, on-site contact, On site count chips linking to the location page tabs, wider search (phone, notes, contact, tags) and bulk Tags/Archive/Restore/Delete; shared tabbed location form (Details, Address, Contact, Access) with Primary switch, auto-primary for a client's first location, tags and photo removal; ticket detail Site card with address, phone, hours, access notes and directions; ticket location now validated against the ticket's client; documents and files can be linked to locations via new location_documents and location_files tables (database update 2.8.1, db.sql updated) and the generic link modal; location results in global search; client header primary location, network and rack pages link to the location; CSV export adds Primary, On-site Contact, people/asset counts and Access Notes, CSV import checks the file extension and makes the first location primary; primary locations cannot be archived or deleted and bulk archive skips them; deleting a location (including via API) clears its references on assets, contacts, networks, racks, tickets and calendar events; new functions/location.php helpers (LOCATION_KINDS, address/map/directions URLs, tile, counts, form set, tags, primary, photo, assign/unassign, history), new handlers make_location_primary, set_location_records, unset_location_record and ajax location_set_notes, location_model.php removed, demo data gets an on-site contact and linked runbook for the primary location
2026-10-14 22:06:41 -04:00

153 lines
7.4 KiB
PHP

<?php
/*
* Move people, assets, networks or racks to a location (?id=&kind=asset).
* Pills switch between the kinds this agent may move; each lists the client's
* own records that are somewhere else, with where they are now, so a whole
* floor can be moved in one go after an office move.
*/
require_once '../../../includes/modal_header.php';
$location_id = intval($_GET['id'] ?? 0);
$location = locationRow($location_id);
if (!$location) {
echo json_encode(['error' => 'That location no longer exists.']);
exit;
}
$client_id = intval($location['location_client_id']);
enforceClientAccess();
// Only the kinds this agent may move
$kinds = array_filter(LOCATION_KINDS, function ($k) {
return lookupUserPermission($k['module']) >= 2;
});
if (!$kinds) {
echo json_encode(['error' => 'You do not have permission to move things to a location.']);
exit;
}
$kind = isset($kinds[$_GET['kind'] ?? '']) ? $_GET['kind'] : array_key_first($kinds);
/** The client's records of one kind that are not at this location, with where they are now. */
function locationAssignCandidates($kind, $client_id, $location_id) {
global $mysqli;
$k = LOCATION_KINDS[$kind];
$extra = [
'contact' => 'contact_title AS detail, contact_photo, contact_client_id',
'asset' => "asset_type, TRIM(CONCAT(IFNULL(asset_make, ''), ' ', IFNULL(asset_model, ''))) AS detail",
'network' => "CONCAT(network, IF(network_vlan > 0, CONCAT(' · VLAN ', network_vlan), '')) AS detail",
'rack' => "CONCAT(rack_units, 'U') AS detail",
][$kind];
$sql = mysqli_query($mysqli, "SELECT t.{$k['id']} AS id, t.{$k['name']} AS name, $extra, l.location_name AS current_location
FROM {$k['table']} AS t
LEFT JOIN locations AS l ON l.location_id = t.{$k['location']}
WHERE t.{$k['client']} = $client_id AND t.{$k['archived']} IS NULL
AND (t.{$k['location']} IS NULL OR t.{$k['location']} != $location_id)
ORDER BY t.{$k['name']} ASC");
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = $row;
}
return $rows;
}
$location_name = escapeHtml($location['location_name']);
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-dolly me-2"></i>Move to <strong><?= $location_name ?></strong></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off" data-location-assign-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="location_id" value="<?= $location_id ?>">
<input type="hidden" name="kind" value="<?= $kind ?>" data-assign-kind>
<div class="modal-body">
<?php if (count($kinds) > 1) { ?>
<nav class="nav nav-pills itflow-asset-pills itflow-link-types mb-3" role="tablist">
<?php foreach ($kinds as $kind_key => $k) { ?>
<button type="button" class="nav-link <?= $kind_key === $kind ? 'active' : '' ?>" data-bs-toggle="pill" data-bs-target="#locationAssign-<?= $kind_key ?>" data-assign-type="<?= $kind_key ?>" role="tab">
<i class="fas fa-fw <?= $k['icon'] ?> me-1"></i><?= $k['plural'] ?>
</button>
<?php } ?>
</nav>
<?php } ?>
<div class="tab-content">
<?php foreach ($kinds as $kind_key => $k) {
$candidates = locationAssignCandidates($kind_key, $client_id, $location_id); ?>
<div class="tab-pane fade <?= $kind_key === $kind ? 'show active' : '' ?>" id="locationAssign-<?= $kind_key ?>" role="tabpanel">
<?php if ($candidates) { ?>
<label class="form-label">Which <?= strtolower($k['plural']) ?>?</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-fw <?= $k['icon'] ?>"></i></span>
<select class="form-select select2" name="ids[]" multiple data-placeholder="Pick one or more <?= strtolower($k['plural']) ?>" <?= $kind_key === $kind ? '' : 'disabled' ?> data-assign-select="<?= $kind_key ?>">
<?php foreach ($candidates as $row) {
$where = $row['current_location'] ? 'At ' . $row['current_location'] : 'No location';
$description = implode(' · ', array_filter([$row['detail'], $where]));
if ($kind_key === 'contact') {
$data = selectOptionContact($row['contact_client_id'], $row['name'], $row['contact_photo'], $description, $row['id']);
} elseif ($kind_key === 'asset') {
$data = selectOptionAsset($row['asset_type'], $description);
} else {
$data = selectOptionData(['icon' => 'fas ' . $k['icon'], 'description' => $description]);
} ?>
<option value="<?= intval($row['id']) ?>"<?= $data ?>><?= escapeHtml($row['name']) ?></option>
<?php } ?>
</select>
</div>
<div class="form-text"><?= count($candidates) ?> <?= count($candidates) === 1 ? $k['label'] : strtolower($k['plural']) ?> somewhere else or at no location.</div>
<?php } else { ?>
<div class="itflow-asset-empty py-4">
<i class="fas <?= $k['icon'] ?> fa-2x mb-2"></i>
<div>Every <?= $k['label'] ?> this client has is already here.</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="set_location_records" class="btn btn-primary text-bold"><i class="fas fa-dolly me-2"></i>Move here</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
</div>
</form>
<script>
(function () {
// Only the pane on show posts its picks
var form = document.querySelector('form[data-location-assign-form]:not([data-wired])');
if (!form) {
return;
}
form.dataset.wired = '1';
var kind = form.querySelector('[data-assign-kind]');
form.querySelectorAll('[data-assign-type]').forEach(function (pill) {
pill.addEventListener('shown.bs.tab', function () {
kind.value = pill.dataset.assignType;
form.querySelectorAll('[data-assign-select]').forEach(function (select) {
select.disabled = select.dataset.assignSelect !== kind.value;
});
});
});
form.addEventListener('submit', function (e) {
var select = form.querySelector('[data-assign-select="' + kind.value + '"]');
if (!select || !select.selectedOptions.length) {
e.preventDefault();
if (select && select.itflowSelect) {
select.itflowSelect.open();
}
}
});
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';