$name created");
redirect();
}
if (isset($_POST['edit_location'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$row = locationForRequest($_POST['location_id'] ?? 0);
$location_id = intval($row['location_id']);
$set = locationFormSet($_POST, $client_id);
if ($set === null) {
flashAlert('A location needs a name.', 'error');
redirect();
}
mysqli_query($mysqli, "UPDATE locations SET $set WHERE location_id = $location_id");
// Only a form that showed the tags changes them
if (isset($_POST['tags_form'])) {
locationSetTags($location_id, $_POST['tags'] ?? []);
}
if (!empty($_POST['location_primary'])) {
locationMakePrimary($location_id, $client_id);
}
if (!empty($_POST['remove_photo']) && $row['location_photo']) {
$photo = (string) $row['location_photo'];
if (basename($photo) === $photo && is_file("../uploads/clients/$client_id/$photo")) {
unlink("../uploads/clients/$client_id/$photo");
}
mysqli_query($mysqli, "UPDATE locations SET location_photo = NULL WHERE location_id = $location_id");
}
locationSavePhoto($location_id, $client_id, $_FILES['file'] ?? []);
$name = trim($_POST['name']);
logAudit("Location", "Edit", "$session_name edited location " . escapeSql($name), $client_id, $location_id);
flashAlert("Location $name updated");
redirect();
}
if (isset($_GET['make_location_primary'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$row = locationForRequest($_GET['make_location_primary']);
$location_id = intval($row['location_id']);
if ($row['location_archived_at']) {
flashAlert('Restore the location before making it primary.', 'error');
redirect();
}
locationMakePrimary($location_id, $client_id);
logAudit("Location", "Edit", "$session_name made " . escapeSql($row['location_name']) . " the primary location", $client_id, $location_id);
flashAlert("{$row['location_name']} is now the primary location");
redirect();
}
if (isset($_POST['set_location_records'])) {
validateCSRFToken();
$row = locationForRequest($_POST['location_id'] ?? 0);
$kind = (string) ($_POST['kind'] ?? '');
if (!isset(LOCATION_KINDS[$kind])) {
flashAlert('Nothing to move.', 'error');
redirect();
}
enforceUserPermission(LOCATION_KINDS[$kind]['module'], 2);
if ($row['location_archived_at']) {
flashAlert('Restore the location before moving anything to it.', 'error');
redirect();
}
$moved = locationAssign($row, $kind, $_POST['ids'] ?? []);
$k = LOCATION_KINDS[$kind];
if (!$moved) {
flashAlert("Nothing new to move to {$row['location_name']}.", 'warning');
} elseif (count($moved) === 1) {
flashAlert(ucfirst($k['label']) . " {$moved[0]} moved to {$row['location_name']}");
} else {
flashAlert("Moved " . count($moved) . " " . strtolower($k['plural']) . " to {$row['location_name']}");
}
redirect();
}
if (isset($_GET['unset_location_record'])) {
validateCSRFToken();
$row = locationForRequest($_GET['location_id'] ?? 0);
$kind = (string) ($_GET['kind'] ?? '');
if (!isset(LOCATION_KINDS[$kind])) {
redirect();
}
enforceUserPermission(LOCATION_KINDS[$kind]['module'], 2);
$name = locationUnassign($row, $kind, intval($_GET['unset_location_record']));
if ($name !== '') {
flashAlert(ucfirst(LOCATION_KINDS[$kind]['label']) . " $name is no longer at {$row['location_name']}", 'error');
}
redirect();
}
if (isset($_GET['archive_location'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$row = locationForRequest($_GET['archive_location']);
$location_id = intval($row['location_id']);
// The client header and overview show the primary location - another one has to take over first
if (intval($row['location_primary'])) {
flashAlert("{$row['location_name']} is the primary location - make another location primary first.", 'error');
redirect();
}
mysqli_query($mysqli, "UPDATE locations SET location_archived_at = NOW() WHERE location_id = $location_id");
logAudit("Location", "Archive", "$session_name archived location " . escapeSql($row['location_name']), $client_id, $location_id);
flashAlert("Location {$row['location_name']} archived", 'error');
redirect();
}
if (isset($_GET['restore_location'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$row = locationForRequest($_GET['restore_location']);
$location_id = intval($row['location_id']);
mysqli_query($mysqli, "UPDATE locations SET location_archived_at = NULL WHERE location_id = $location_id");
logAudit("Location", "Restore", "$session_name restored location " . escapeSql($row['location_name']), $client_id, $location_id);
flashAlert("Location {$row['location_name']} restored");
redirect();
}
if (isset($_GET['delete_location'])) {
validateCSRFToken();
enforceUserPermission('module_client', 3);
$row = locationForRequest($_GET['delete_location']);
if (intval($row['location_primary'])) {
flashAlert("{$row['location_name']} is the primary location - make another location primary first.", 'error');
redirect();
}
locationDeleteForGood($row);
logAudit("Location", "Delete", "$session_name deleted location " . escapeSql($row['location_name']), $client_id);
flashAlert("Location {$row['location_name']} deleted", 'error');
// From its own page, back to the client's archived locations - that page is gone
if (strpos($_SERVER['HTTP_REFERER'] ?? '', '/location.php') !== false) {
redirect("locations.php?client_id=$client_id&archived=1");
}
redirect();
}
if (isset($_POST['bulk_archive_locations'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$count = 0;
$skipped = [];
foreach (array_unique(array_map('intval', (array) ($_POST['location_ids'] ?? []))) as $location_id) {
$row = locationRow($location_id);
if (!$row) {
continue;
}
$client_id = intval($row['location_client_id']);
enforceClientAccess();
if (intval($row['location_primary'])) {
$skipped[] = $row['location_name'];
continue;
}
mysqli_query($mysqli, "UPDATE locations SET location_archived_at = NOW() WHERE location_id = $location_id");
logAudit("Location", "Archive", "$session_name archived location " . escapeSql($row['location_name']), $client_id, $location_id);
$count++;
}
flashAlert("Archived $count location" . ($count === 1 ? '' : 's') . ($skipped ? ' - kept ' . implode(', ', $skipped) . ', the primary location' : ''), 'error');
redirect();
}
if (isset($_POST['bulk_restore_locations'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$count = 0;
foreach (array_unique(array_map('intval', (array) ($_POST['location_ids'] ?? []))) as $location_id) {
$row = locationRow($location_id);
if (!$row) {
continue;
}
$client_id = intval($row['location_client_id']);
enforceClientAccess();
mysqli_query($mysqli, "UPDATE locations SET location_archived_at = NULL WHERE location_id = $location_id");
logAudit("Location", "Restore", "$session_name restored location " . escapeSql($row['location_name']), $client_id, $location_id);
$count++;
}
flashAlert("Restored $count location" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_delete_locations'])) {
validateCSRFToken();
enforceUserPermission('module_client', 3);
$count = 0;
foreach (array_unique(array_map('intval', (array) ($_POST['location_ids'] ?? []))) as $location_id) {
$row = locationRow($location_id);
if (!$row || intval($row['location_primary'])) {
continue;
}
$client_id = intval($row['location_client_id']);
enforceClientAccess();
locationDeleteForGood($row);
logAudit("Location", "Delete", "$session_name deleted location " . escapeSql($row['location_name']), $client_id);
$count++;
}
flashAlert("Deleted $count location" . ($count === 1 ? '' : 's'), 'error');
redirect();
}
if (isExportRequest('export_locations')) {
validateCSRFToken();
// Exports are reads - see CONTRIBUTING.md
enforceUserPermission('module_client');
$format = resolveExportFormat($_POST['export_locations']);
// Filters inherited from the locations page - mirrors agent/locations.php
$filter_summary = [];
// Archived Filter
$archived = (isset($_POST['archived']) && $_POST['archived'] == 1);
if ($archived) {
$filter_summary['Archived'] = 'Archived only';
}
if (!empty($_POST['client_id'])) {
$client_id = intval($_POST['client_id']);
$client_query = "AND location_client_id = $client_id";
$client_name = getFieldById('clients', $client_id, 'client_name');
$file_name_prepend = "$client_name-";
$filter_summary['Client'] = $client_name;
enforceClientAccess();
$archive_query = $archived ? "location_archived_at IS NOT NULL" : "location_archived_at IS NULL";
} else {
$client_query = '';
$client_id = 0; // for Logging
$file_name_prepend = "$session_company_name-";
// Client Filter
if (!empty($_POST['client'])) {
$filter_client_id = intval($_POST['client']);
$client_query = "AND (location_client_id = $filter_client_id)";
$filter_summary['Client'] = exportClientName($filter_client_id);
}
$archive_query = $archived ? "(client_archived_at IS NOT NULL OR location_archived_at IS NOT NULL)" : "(client_archived_at IS NULL AND location_archived_at IS NULL)";
}
// Tags Filter - a location with any of them
$tag_query = '';
if (isset($_POST['tags']) && is_array($_POST['tags']) && !empty($_POST['tags'])) {
$tag_filter = implode(",", array_map('intval', $_POST['tags']));
$tag_query = "AND locations.location_id IN (SELECT location_id FROM location_tags WHERE tag_id IN ($tag_filter))";
$tag_names = [];
$sql_tags = mysqli_query($mysqli, "SELECT tag_name FROM tags WHERE tag_id IN ($tag_filter) ORDER BY tag_name ASC");
while ($tag_row = mysqli_fetch_assoc($sql_tags)) {
$tag_names[] = $tag_row['tag_name'];
}
$filter_summary['Tags'] = implode(', ', $tag_names);
}
// Search Filter
$q = escapeSql($_POST['q'] ?? '');
if (!empty($q)) {
$filter_summary['Search'] = $_POST['q'];
}
$sql = mysqli_query(
$mysqli,
"SELECT location_name, location_description, location_address, location_city, location_state, location_zip, location_country,
location_phone, location_phone_country_code, location_hours, location_notes, IF(location_primary = 1, 'Yes', '') AS location_primary_text,
client_name, contact_name,
(SELECT COUNT(*) FROM assets WHERE asset_location_id = locations.location_id AND asset_archived_at IS NULL) AS asset_count,
(SELECT COUNT(*) FROM contacts AS people WHERE people.contact_location_id = locations.location_id AND people.contact_archived_at IS NULL) AS contact_count
FROM locations
LEFT JOIN clients ON client_id = location_client_id
LEFT JOIN contacts ON contact_id = location_contact_id
WHERE $archive_query
$tag_query
AND (location_name LIKE '%$q%' OR location_description LIKE '%$q%' OR location_address LIKE '%$q%' OR location_city LIKE '%$q%' OR location_state LIKE '%$q%' OR location_zip LIKE '%$q%' OR location_country LIKE '%$q%' OR location_phone LIKE '%$q%' OR client_name LIKE '%$q%'
OR locations.location_id IN (SELECT location_id FROM location_tags JOIN tags USING (tag_id) WHERE tag_name LIKE '%$q%'))
" . clientScopeSql('location_client_id') . "
$client_query
ORDER BY location_primary DESC, location_name ASC"
);
$num_rows = mysqli_num_rows($sql);
guardExportHasRows($num_rows, 'locations');
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$export = beginExport('locations', $format, $file_name_prepend . 'Locations', 'Locations', summarizeExportFilters($filter_summary));
while ($row = mysqli_fetch_assoc($sql)) {
addExportRow($export, $row);
}
finishExport($export);
}
logAudit("Location", "Export", "$session_name exported $num_rows location(s) to a " . strtoupper($format) . " file", $client_id);
exit;
}
if (isset($_POST["import_locations_csv"])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$client_id = intval($_POST['client_id']);
enforceClientAccess();
if (empty($_FILES["file"]["tmp_name"])) {
flashAlert("Please select a file to upload.", 'error');
redirect();
}
$file_extension = strtolower(pathinfo((string) $_FILES['file']['name'], PATHINFO_EXTENSION));
if ($file_extension !== 'csv') {
flashAlert("Bad file extension", 'error');
redirect();
}
if ($_FILES["file"]["size"] < 1) {
flashAlert("Bad file size (empty?)", 'error');
redirect();
}
$file = fopen($_FILES["file"]["tmp_name"], "r");
$header = fgetcsv($file, 1000, ",");
if (!is_array($header) || count($header) != 8) {
fclose($file);
flashAlert("Bad column count - use the template: Name, Description, Address, City, State, Postal Code, Phone, Hours.", 'error');
redirect();
}
$has_primary = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM locations WHERE location_client_id = $client_id AND location_primary = 1 AND location_archived_at IS NULL"))[0]);
$row_count = 0;
$duplicate_count = 0;
while (($column = fgetcsv($file, 1000, ",")) !== false) {
// Each row starts blank - a short row never inherits the row before it
$column = array_pad($column, 8, '');
$name = trim((string) $column[0]);
if ($name === '') {
continue;
}
$name_sql = escapeSql(mb_substr($name, 0, 200));
if (mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM locations WHERE location_name = '$name_sql' AND location_client_id = $client_id"))) {
$duplicate_count++;
continue;
}
$description = escapeSql((string) $column[1]);
$address = escapeSql(mb_substr((string) $column[2], 0, 200));
$city = escapeSql(mb_substr((string) $column[3], 0, 200));
$state = escapeSql(mb_substr((string) $column[4], 0, 200));
$zip = escapeSql(mb_substr((string) $column[5], 0, 200));
$phone = substr(preg_replace("/[^0-9]/", '', (string) $column[6]), 0, 200);
$hours = escapeSql(mb_substr((string) $column[7], 0, 200));
mysqli_query($mysqli, "INSERT INTO locations SET location_name = '$name_sql', location_description = '$description', location_address = '$address', location_city = '$city', location_state = '$state', location_zip = '$zip', location_phone = '$phone', location_hours = '$hours', location_client_id = $client_id");
// A client with no locations yet gets its first one as primary
if (!$has_primary) {
locationMakePrimary(mysqli_insert_id($mysqli), $client_id);
$has_primary = 1;
}
$row_count++;
}
fclose($file);
logAudit("Location", "Import", "$session_name imported $row_count location(s). $duplicate_count duplicate(s) found and not imported", $client_id);
flashAlert("$row_count Location(s) imported, $duplicate_count duplicate(s) detected and not imported");
redirect();
}
if (isset($_GET['download_locations_csv_template'])) {
$delimiter = ",";
$enclosure = '"';
$escape = '\\';
$filename = "Locations-Template.csv";
//create a file pointer
$f = fopen('php://memory', 'w');
//set column headers
$fields = array('Name', 'Description', 'Address', 'City', 'State', 'Postal Code', 'Phone', 'Hours');
fputcsv($f, $fields, $delimiter, $enclosure, $escape);
//move back to beginning of file
fseek($f, 0);
//set headers to download file rather than displayed
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $filename . '";');
//output all remaining data on a file pointer
fpassthru($f);
exit;
}