Feature: Designate a client record as your organization currently only provdes a direct link when you click company name in top of the side nav

This commit is contained in:
johnnyq
2026-08-15 20:36:56 -04:00
parent 1514934726
commit 35ddd1a64d
8 changed files with 101 additions and 2 deletions

View File

@@ -0,0 +1,17 @@
<?php
/*
* ITFlow - Database update to version 2.7.1 (from 2.7.0)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Which client record, if any, is the MSP's own company. Most installs create one to
// hold their internal tickets, documentation, assets and credentials, but nothing in
// the app knew that record was special. Admin > Settings > Company Details sets it.
//
// 0 means no client is designated, which is every existing install - nothing changes
// until an administrator chooses one.
mysqli_query($mysqli, "ALTER TABLE `settings`
ADD COLUMN `config_internal_client_id` int(11) NOT NULL DEFAULT 0");

View File

@@ -42,8 +42,18 @@ if (isset($_POST['edit_company'])) {
}
}
// The client record standing in for this company. Verified against the clients table
// rather than trusted from the POST, so a stale or hand-made value cannot leave the
// sidebar pointing at a client that no longer exists. 0 clears the designation.
$internal_client_id = intval($_POST['internal_client_id'] ?? 0);
if ($internal_client_id !== 0 && !mysqli_num_rows(mysqli_query($mysqli, "SELECT client_id FROM clients WHERE client_id = $internal_client_id LIMIT 1"))) {
$internal_client_id = 0;
}
mysqli_query($mysqli,"UPDATE companies SET company_name = '$name', company_address = '$address', company_city = '$city', company_state = '$state', company_zip = '$zip', company_country = '$country', company_phone_country_code = '$phone_country_code', company_phone = '$phone', company_email = '$email', company_website = '$website', company_tax_id = '$tax_id' WHERE company_id = 1");
mysqli_query($mysqli,"UPDATE settings SET config_internal_client_id = $internal_client_id WHERE company_id = 1");
logAudit("Settings", "Edit", "$session_name edited company details");
flashAlert("Company <strong>$name</strong> edited");

View File

@@ -141,6 +141,27 @@ $company_initials = escapeHtml(initials($company_name));
<hr>
<div class="mb-3">
<label>My Company's Client Record</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-house-user"></i></span>
<select class="form-select select2" name="internal_client_id">
<option value="0">None</option>
<?php
$sql_internal_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
while ($row_internal_client = mysqli_fetch_assoc($sql_internal_clients)) {
$internal_client_id_select = intval($row_internal_client['client_id']);
$internal_client_name_select = escapeHtml($row_internal_client['client_name']);
?>
<option <?php if ($config_internal_client_id == $internal_client_id_select) { echo "selected"; } ?> value="<?= $internal_client_id_select ?>"><?= $internal_client_name_select ?></option>
<?php } ?>
</select>
</div>
<small class="text-muted">If you keep a client record for your own company - for internal tickets, documentation, assets and credentials - point it out here. Your company name in the sidebar then links straight to it.</small>
</div>
<hr>
<button type="submit" name="edit_company" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
</div>
</div>

View File

@@ -1,8 +1,21 @@
<!-- Main Sidebar Container -->
<aside class="app-sidebar shadow d-print-none" data-bs-theme="dark">
<?php
/*
* The company name goes to your own company's client record once one is designated in
* Admin > Settings > Company Details, and falls back to the dashboard otherwise. The
* access check keeps a restricted agent from being sent to a client they would only be
* bounced off, and costs no query - the permission arrays are already in the session.
*/
$brand_link = "/agent/dashboard.php";
if (!empty($config_internal_client_id) && lookupUserPermission("module_client") >= 1 && hasClientAccess($config_internal_client_id)) {
$brand_link = "/agent/client_overview.php?client_id=" . intval($config_internal_client_id);
}
?>
<div class="sidebar-brand">
<a class="brand-link" href="/agent/dashboard.php">
<a class="brand-link" href="<?= $brand_link ?>">
<span class="brand-text h4 mb-0"><?= escapeHtml($session_company_name) ?></span>
</a>
</div>

View File

@@ -348,6 +348,9 @@ if (isset($_GET['delete_client'])) {
//Finally Remove the Client
mysqli_query($mysqli, "DELETE FROM clients WHERE client_id = $client_id");
// Don't leave the company's own-client designation pointing at a client that is gone
mysqli_query($mysqli, "UPDATE settings SET config_internal_client_id = 0 WHERE config_internal_client_id = $client_id");
logAudit("Client", "Deleted", "$session_name deleted Client $client_name and all associated data");
flashAlert("Client <strong>$client_name</strong> deleted along with all associated data", 'error');

3
db.sql
View File

@@ -2327,6 +2327,7 @@ CREATE TABLE `settings` (
`config_backup_retention_count` int(11) NOT NULL DEFAULT 5,
`config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full',
`config_update_queued_at` datetime DEFAULT NULL,
`config_internal_client_id` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -3171,4 +3172,4 @@ CREATE TABLE `vendors` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-08-15 19:56:27
-- Dump completed on 2026-08-15 20:35:12

View File

@@ -98,6 +98,36 @@ function clientScopeSql($column) {
return $sql;
}
/*
* Can this user reach this client? Answers yes or no instead of redirecting, for places
* that need to decide whether to render something - enforceClientAccess() below is the
* gate that protects the page itself and still has to be called there.
*
* Reads the permission arrays that includes/load_user_session.php already loaded, so this
* costs no query and can be called from the sidebar on every page. The rules match
* clientScopeSql() above: a deny wins, an allow list restricts to its members, and no
* rules at all means unrestricted.
*/
function hasClientAccess($client_id) {
global $session_is_admin, $client_access_array, $client_deny_array;
$client_id = intval($client_id);
if ($session_is_admin) {
return true;
}
if (in_array($client_id, array_map('intval', $client_deny_array))) {
return false;
}
if (empty($client_access_array)) {
return true;
}
return in_array($client_id, array_map('intval', $client_access_array));
}
function enforceClientAccess($client_id = null) {
global $mysqli, $session_user_id, $session_is_admin, $session_name;

View File

@@ -12,6 +12,7 @@ $sql_settings = mysqli_query($mysqli, "SELECT config_azure_client_id, config_azu
config_invoice_from_email, config_invoice_from_name, config_invoice_late_fee_enable,
config_invoice_late_fee_percent, config_invoice_next_number,
config_invoice_paid_notification_email, config_invoice_prefix, config_invoice_show_tax_id,
config_internal_client_id,
config_log_retention, config_login_key_required, config_login_key_secret,
config_login_message, config_login_remember_me_expire, config_mail_from_email,
config_mail_from_name, config_mail_oauth_access_token,
@@ -136,6 +137,9 @@ $config_module_enable_ticketing = intval($row['config_module_enable_ticketing'])
$config_module_enable_accounting = intval($row['config_module_enable_accounting']);
$config_client_portal_enable = intval($row['config_client_portal_enable']);
// The client record that represents this MSP's own company, or 0 for none
$config_internal_client_id = intval($row['config_internal_client_id']);
// Login
$config_login_message = $row['config_login_message'];
$config_login_key_required = $row['config_login_key_required'];