['table' => 'contacts', 'id' => 'contact_id', 'name' => 'contact_name', 'location' => 'contact_location_id', 'client' => 'contact_client_id', 'archived' => 'contact_archived_at', 'module' => 'module_client', 'label' => 'contact', 'plural' => 'Contacts', 'icon' => 'fa-user'],
'asset' => ['table' => 'assets', 'id' => 'asset_id', 'name' => 'asset_name', 'location' => 'asset_location_id', 'client' => 'asset_client_id', 'archived' => 'asset_archived_at', 'module' => 'module_support', 'label' => 'asset', 'plural' => 'Assets', 'icon' => 'fa-desktop'],
'network' => ['table' => 'networks', 'id' => 'network_id', 'name' => 'network_name', 'location' => 'network_location_id', 'client' => 'network_client_id', 'archived' => 'network_archived_at', 'module' => 'module_support', 'label' => 'network', 'plural' => 'Networks', 'icon' => 'fa-network-wired'],
'rack' => ['table' => 'racks', 'id' => 'rack_id', 'name' => 'rack_name', 'location' => 'rack_location_id', 'client' => 'rack_client_id', 'archived' => 'rack_archived_at', 'module' => 'module_support', 'label' => 'rack', 'plural' => 'Racks', 'icon' => 'fa-server'],
];
/** The address on one line - "1140 Ridge Avenue, Pittsburgh, PA 15212". Raw in, raw out. */
function locationAddressText($row, $with_country = false) {
return formatAddress($row['location_address'] ?? '', $row['location_city'] ?? '', $row['location_state'] ?? '', $row['location_zip'] ?? '', $with_country ? ($row['location_country'] ?? '') : '', ', ');
}
/** A link that opens the address in maps - Apple Maps on an Apple device, Google Maps otherwise. '' without an address. */
function locationMapUrl($row) {
global $session_map_source;
$address = locationAddressText($row, true);
if ($address === '') {
return '';
}
return ($session_map_source ?? 'google') === 'apple'
? 'https://maps.apple.com/?q=' . rawurlencode($address)
: 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($address);
}
/** Turn-by-turn directions to the address from wherever the tech is. '' without an address. */
function locationDirectionsUrl($row) {
global $session_map_source;
$address = locationAddressText($row, true);
if ($address === '') {
return '';
}
return ($session_map_source ?? 'google') === 'apple'
? 'https://maps.apple.com/?daddr=' . rawurlencode($address)
: 'https://www.google.com/maps/dir/?api=1&destination=' . rawurlencode($address);
}
/** The location's photo, or a map pin tile. $size '' | 'sm' | 'lg'. */
function locationTile($row, $size = '') {
$size_class = in_array($size, ['sm', 'lg'], true) ? " itflow-asset-icon-$size" : '';
$photo = (string) ($row['location_photo'] ?? '');
if ($photo !== '') {
return '
';
}
$icon = intval($row['location_primary'] ?? 0) ? 'fa-building' : 'fa-map-marker-alt';
return '';
}
/** One location by id with its client and on-site contact - raw, or null. */
function locationRow($location_id) {
global $mysqli;
$location_id = intval($location_id);
if (!$location_id) {
return null;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT locations.*, client_id, client_name,
contact_id, contact_name, contact_title, contact_email, contact_phone, contact_phone_country_code, contact_extension,
contact_mobile, contact_mobile_country_code, contact_photo, contact_archived_at
FROM locations
LEFT JOIN clients ON client_id = location_client_id
LEFT JOIN contacts ON contact_id = location_contact_id AND contact_client_id = location_client_id
WHERE location_id = $location_id
LIMIT 1"));
return $row ?: null;
}
/**
* What is at each of these locations, in one query per kind rather than per
* row: people, assets, networks and racks that are not archived, and tickets
* still open. Returns [location_id => [kind => count]].
*/
function locationCounts($location_ids) {
global $mysqli;
$location_ids = array_values(array_filter(array_map('intval', (array) $location_ids)));
$counts = [];
foreach ($location_ids as $id) {
$counts[$id] = ['contact' => 0, 'asset' => 0, 'network' => 0, 'rack' => 0, 'ticket' => 0];
}
if (!$location_ids) {
return $counts;
}
$in = implode(',', $location_ids);
foreach (LOCATION_KINDS as $kind => $k) {
$sql = mysqli_query($mysqli, "SELECT {$k['location']}, COUNT(*) FROM {$k['table']} WHERE {$k['location']} IN ($in) AND {$k['archived']} IS NULL GROUP BY {$k['location']}");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])][$kind] = intval($row[1]);
}
}
$sql = mysqli_query($mysqli, "SELECT ticket_location_id, COUNT(*) FROM tickets WHERE ticket_location_id IN ($in) AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL GROUP BY ticket_location_id");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])]['ticket'] = intval($row[1]);
}
return $counts;
}
/**
* The location fields a New / Edit form posts, as a SQL SET list - phone
* numbers as digits, only the client's own contact. Null when there is no
* name. Fields missing from the post are blank, never a PHP warning.
*/
function locationFormSet($post, $client_id) {
$text = function ($field, $length = 200) use ($post) {
$value = trim((string) ($post[$field] ?? ''));
return "'" . escapeSql($length ? mb_substr($value, 0, $length) : $value) . "'";
};
$digits = function ($field, $length = 200) use ($post) {
return "'" . substr(preg_replace('/[^0-9]/', '', (string) ($post[$field] ?? '')), 0, $length) . "'";
};
if (trim((string) ($post['name'] ?? '')) === '') {
return null;
}
$fields = [
'location_name' => $text('name'),
'location_description' => $text('description', 0),
'location_address' => $text('address'),
'location_city' => $text('city'),
'location_state' => $text('state'),
'location_zip' => $text('zip'),
'location_country' => $text('country'),
'location_phone' => $digits('phone'),
'location_phone_country_code' => $digits('phone_country_code', 10),
'location_phone_extension' => $digits('extension', 10),
'location_fax' => $digits('fax'),
'location_fax_country_code' => $digits('fax_country_code', 10),
'location_hours' => $text('hours'),
'location_notes' => $text('notes', 0),
'location_contact_id' => assetClientOwnedId('contact', $post['contact'] ?? 0, $client_id),
];
$set = [];
foreach ($fields as $column => $value) {
$set[] = "$column = $value";
}
return implode(', ', $set);
}
/** Replace a location's tags with these - location tags only, and an admin can type a new one in (tagSetForRecord()). */
function locationSetTags($location_id, $tag_ids) {
tagSetForRecord('location', $location_id, (array) $tag_ids);
}
/** Make this the client's one primary location - what the client header and overview show. */
function locationMakePrimary($location_id, $client_id) {
global $mysqli;
$location_id = intval($location_id);
$client_id = intval($client_id);
mysqli_query($mysqli, "UPDATE locations SET location_primary = 0 WHERE location_client_id = $client_id AND location_id != $location_id");
mysqli_query($mysqli, "UPDATE locations SET location_primary = 1 WHERE location_id = $location_id");
}
/**
* Save an uploaded photo for a location, replacing (and removing) the old
* one. Returns true if a photo was saved.
*/
function locationSavePhoto($location_id, $client_id, $file) {
global $mysqli;
if (empty($file['tmp_name']) || !($new_file_name = checkFileUpload($file, ['jpg', 'jpeg', 'gif', 'png', 'webp']))) {
return false;
}
$client_id = intval($client_id);
$location_id = intval($location_id);
$dir = "../uploads/clients/$client_id";
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
if (!move_uploaded_file($file['tmp_name'], "$dir/$new_file_name")) {
return false;
}
$old = (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT location_photo FROM locations WHERE location_id = $location_id"))[0];
if ($old !== '' && basename($old) === $old && is_file("$dir/$old")) {
unlink("$dir/$old");
}
mysqli_query($mysqli, "UPDATE locations SET location_photo = '" . escapeSql($new_file_name) . "' WHERE location_id = $location_id");
return true;
}
/**
* Before a location is deleted for good: everything that pointed at it
* points at no location instead of an id that no longer exists.
*/
function locationClearReferences($location_id) {
global $mysqli;
$location_id = intval($location_id);
foreach (LOCATION_KINDS as $k) {
$none = $k['table'] === 'racks' ? 'NULL' : '0';
mysqli_query($mysqli, "UPDATE {$k['table']} SET {$k['location']} = $none WHERE {$k['location']} = $location_id");
}
mysqli_query($mysqli, "UPDATE tickets SET ticket_location_id = 0 WHERE ticket_location_id = $location_id");
mysqli_query($mysqli, "UPDATE calendar_events SET event_location_id = 0 WHERE event_location_id = $location_id");
mysqli_query($mysqli, "UPDATE trips SET trip_location_id = 0 WHERE trip_location_id = $location_id");
}
/**
* Move records of one kind to a location. Only the location's own client's,
* unarchived records are moved; assets and contacts go through their quick
* pickers' setters, so they are logged (and assets get a history line) the
* same as a move from their own pages. Returns the names moved.
*/
function locationAssign($location, $kind, $ids) {
global $mysqli, $session_name;
$k = LOCATION_KINDS[$kind] ?? null;
if (!$k || !$location) {
return [];
}
$location_id = intval($location['location_id']);
$client_id = intval($location['location_client_id']);
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $ids))));
if (!$ids) {
return [];
}
$sql = mysqli_query($mysqli, "SELECT {$k['id']}, {$k['name']} FROM {$k['table']}
WHERE {$k['id']} IN (" . implode(',', $ids) . ") AND {$k['client']} = $client_id AND {$k['archived']} IS NULL
AND ({$k['location']} IS NULL OR {$k['location']} != $location_id)");
$moved = [];
while ($row = mysqli_fetch_row($sql)) {
[$id, $name] = [intval($row[0]), $row[1]];
if ($kind === 'asset') {
[$ok] = assetQuickSet($id, 'asset_location', $location_id);
} elseif ($kind === 'contact') {
[$ok] = contactQuickSetLocation($id, $location_id);
} else {
mysqli_query($mysqli, "UPDATE {$k['table']} SET {$k['location']} = $location_id WHERE {$k['id']} = $id");
logAudit(ucfirst($kind), "Edit", "$session_name moved {$k['label']} " . escapeSql($name) . " to location " . escapeSql($location['location_name']), $client_id, $id);
$ok = true;
}
if ($ok) {
$moved[] = $name;
}
}
return $moved;
}
/** Take one record off a location - it is then at no location. Returns its name, or '' if it was not there. */
function locationUnassign($location, $kind, $id) {
global $mysqli, $session_name;
$k = LOCATION_KINDS[$kind] ?? null;
if (!$k || !$location) {
return '';
}
$location_id = intval($location['location_id']);
$id = intval($id);
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT {$k['name']} FROM {$k['table']} WHERE {$k['id']} = $id AND {$k['location']} = $location_id"));
if (!$row) {
return '';
}
if ($kind === 'asset') {
assetQuickSet($id, 'asset_location', 0);
} elseif ($kind === 'contact') {
contactQuickSetLocation($id, 0);
} else {
$none = $k['table'] === 'racks' ? 'NULL' : '0';
mysqli_query($mysqli, "UPDATE {$k['table']} SET {$k['location']} = $none WHERE {$k['id']} = $id");
logAudit(ucfirst($kind), "Edit", "$session_name removed {$k['label']} " . escapeSql($row[0]) . " from location " . escapeSql($location['location_name']), intval($location['location_client_id']), $id);
}
return $row[0];
}
/** Who did what to a location - its audit log entries, newest first. */
function locationHistory($location_id, $limit = 50) {
global $mysqli;
$location_id = intval($location_id);
$limit = max(1, intval($limit));
$sql = mysqli_query($mysqli, "SELECT log_action, log_description, log_created_at, user_name FROM logs
LEFT JOIN users ON user_id = log_user_id
WHERE log_type = 'Location' AND log_entity_id = $location_id
ORDER BY log_id DESC LIMIT $limit");
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = $row;
}
return $rows;
}