Vendors overhaul for daily MSP work: new vendor page built around calling them, with Call support (dials the extension), Email, Support site, a star, and a New menu (case/ticket with them, license from them, asset bought from them, portal login, document, upload, or expense and recurring expense for our own vendors), Support card (phone, hours, SLA, email, site), Account card (account number, PIN shown as dots with show and copy, account manager name, direct line and email), blur-to-save notes, an Also used by list of the agent's other clients with the same vendor by name or template, and tabs for Cases (open first, with their ticket number), Licenses (soonest renewal first), Assets (serial, order ref, warranty status), Domains (registrar, web host, DNS, mail), Services, Logins, Documents, Files and History; our own vendors show spend this year, last year and in all, recent expenses and recurring expenses; new vendor quick look; rebuilt vendors list with starred first, initials tiles coloured by vendor name, support line, hours and SLA, account number and PIN, chips for open cases, licenses, assets, domains, services and logins, an Ours / Clients' switch with an all-clients view in All Client Docs, wider search (account manager, emails, tags), tag filter and bulk Tags/Archive/Restore/Delete; shared tabbed vendor form (Details, Support, Account, Notes) with star, tags and start from template that fills every field; Add a Template to Clients to roll one vendor out to many clients, skipping those that have it; Admin Vendor Templates shows how many clients use each template and has Add to clients; vendor tags (tag type 6) with vendor tags in the Tags starter pack; ticket vendor block shows their ticket number, support line, hours, SLA, account number and PIN with copy buttons; vendor tiles in every vendor picker and quick look links across tickets, licenses, assets, domains, services and expense lists; search opens the vendor page; CSV export adds tags, open cases, account manager and PIN; logins, documents and files can be linked to a vendor and ones made from a vendor's page are linked to it; demo vendors get tags, a star and an account manager; database update 2.8.2 adds vendor_tags and vendor_contact_phone_country_code, vendor_contact_phone, vendor_contact_email (db.sql updated) and clears references to vendors that no longer exist; ticket vendor now validated against the ticket's client and removing a vendor from a ticket is logged as such; deleting a vendor from a template no longer unlinks every other vendor from that template; deleting a vendor clears it from assets, licenses, tickets and domains, and vendors with expenses, recurring expenses or payment provider fees are archived rather than deleted; vendor websites without a scheme open correctly; domain form vendor names escaped; new vendor templates keep the phone country code and pushing a template update with nothing ticked no longer errors; vendors API accepts the account manager fields; new functions/vendor.php (VENDOR_USES, vendorCan/vendorEnforce with client vendors on module_client and ours on module_financial, tile, link, website, PIN, form, tags, counts, delete check, reference clearing, also used by, spend, history), credential|vendor and file|vendor link relations, tag module_ours for our own vendors, handlers add_vendor_template_to_clients, favorite_vendor, unfavorite_vendor and ajax vendor_set_notes, vendor_model.php removed

This commit is contained in:
johnnyq
2026-10-14 22:49:05 -04:00
parent 57b8ced839
commit 9e55d52432
71 changed files with 3201 additions and 1013 deletions

View File

@@ -0,0 +1,36 @@
<?php
/*
* ITFlow - Database update to version 2.8.2 (from 2.8.1)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Vendor tags (tag type 6) - ISP, Telecom, Software, Distributor and so on,
// the same as clients, locations, contacts, credentials and assets have
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `vendor_tags` (
`vendor_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`vendor_id`, `tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `vendor_tags_ibfk_1` FOREIGN KEY (`vendor_id`) REFERENCES `vendors` (`vendor_id`) ON DELETE CASCADE,
CONSTRAINT `vendor_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`tag_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
// The account manager's direct line and email, beside their name
mysqli_query($mysqli, "ALTER TABLE `vendors`
ADD COLUMN `vendor_contact_phone_country_code` varchar(10) DEFAULT NULL AFTER `vendor_contact_name`,
ADD COLUMN `vendor_contact_phone` varchar(200) DEFAULT NULL AFTER `vendor_contact_phone_country_code`,
ADD COLUMN `vendor_contact_email` varchar(200) DEFAULT NULL AFTER `vendor_contact_phone`
");
// Vendors deleted before deletes cleared their references left assets,
// licenses, tickets and domains pointing at an id that no longer exists -
// point those at no vendor
mysqli_query($mysqli, "UPDATE `assets` LEFT JOIN `vendors` ON `vendor_id` = `asset_vendor_id` SET `asset_vendor_id` = 0 WHERE `asset_vendor_id` > 0 AND `vendor_id` IS NULL");
mysqli_query($mysqli, "UPDATE `software` LEFT JOIN `vendors` ON `vendor_id` = `software_vendor_id` SET `software_vendor_id` = 0 WHERE `software_vendor_id` > 0 AND `vendor_id` IS NULL");
mysqli_query($mysqli, "UPDATE `tickets` LEFT JOIN `vendors` ON `vendor_id` = `ticket_vendor_id` SET `ticket_vendor_id` = 0 WHERE `ticket_vendor_id` > 0 AND `vendor_id` IS NULL");
foreach (['domain_registrar', 'domain_webhost', 'domain_dnshost', 'domain_mailhost'] as $domain_vendor_column) {
mysqli_query($mysqli, "UPDATE `domains` LEFT JOIN `vendors` ON `vendor_id` = `$domain_vendor_column` SET `$domain_vendor_column` = 0 WHERE `$domain_vendor_column` > 0 AND `vendor_id` IS NULL");
}

View File

@@ -17,6 +17,8 @@ if (isset($_GET['type'])) {
$type_display = "Credential";
} elseif ($type === 5) {
$type_display = "Asset";
} elseif ($type === 6) {
$type_display = "Vendor";
}
}
ob_start();

View File

@@ -22,6 +22,8 @@ if ($tag_type == 1) {
$tag_type_display = "Credential";
} elseif ( $tag_type == 5) {
$tag_type_display = "Asset";
} elseif ($tag_type == 6) {
$tag_type_display = "Vendor";
} else {
$tag_type_display = "Unknown";
}

View File

@@ -410,6 +410,57 @@ function demoAttachTags($mysqli, $join_table, $join_column, $tag_column, $row_id
}
}
// ------------------------------
// demoVendorTags / demoVendorManager
// What kind of vendor each demo vendor is, as Tags starter pack vendor tags,
// and the account manager who looks after the account - a name, a direct
// line and an email at the vendor's domain, the same every time it is seeded.
// ------------------------------
function demoVendorTags($name, $description, $notes = '', $ours = false) {
if ($ours) {
$map = [
'Distribution' => ['Distributor', 'Hardware'], 'Netgate' => ['Hardware', 'Security'], 'Ubiquiti' => ['Hardware'],
'Synology' => ['Hardware'], 'Proxmox' => ['Software'], 'Nextcloud' => ['Cloud', 'Hosting'], 'Licensing' => ['Cloud', 'Distributor'],
'Colocation' => ['Hosting'], 'Bank' => ['Professional Services'], 'CPA' => ['Professional Services'],
'Insurance' => ['Professional Services'], 'Property' => ['Professional Services'],
];
foreach ($map as $match => $tags) {
if (stripos($name, $match) !== false) {
return $tags;
}
}
return [];
}
$text = "$name $description";
$tags = [];
if (preg_match('/internet|fibre|fiber|circuit|broadband|connect|wireless|cable/i', $text)) {
$tags[] = 'ISP';
}
if (preg_match('/phone|telecom|voice/i', $text)) {
$tags[] = 'Telecom';
}
if (!$tags) {
$tags[] = 'Line of Business';
}
if (stripos($notes, 'authorised list') !== false) {
$tags[] = 'Authorised Contacts Only';
}
return $tags;
}
function demoVendorManager($name, $phone, $domain) {
$people = ['Dana Whitlock', 'Marcus Feld', 'Priya Raman', 'Owen Castell', 'Jolene Avery', 'Theo Marsh', 'Rosa Delgado', 'Ian Kowalski', 'Hannah Pryce', 'Victor Lund'];
$person = $people[crc32($name) % count($people)];
$digits = preg_replace('/[^0-9]/', '', (string) $phone);
$direct = $digits !== '' ? substr($digits, 0, -2) . str_pad((string) ((intval(substr($digits, -2)) + 37) % 100), 2, '0', STR_PAD_LEFT) : '';
return [
'vendor_contact_name' => $person,
'vendor_contact_phone_country_code' => '1',
'vendor_contact_phone' => $direct,
'vendor_contact_email' => strtolower(str_replace(' ', '.', $person)) . '@' . $domain,
];
}
// ------------------------------
// demoLink
// The relationship joins are all two integer columns. Linking assets to the
@@ -475,10 +526,9 @@ function demoEnsureOrgVendors($mysqli) {
$vendors[$vendor[0]] = intval($row['vendor_id']);
continue;
}
$vendors[$vendor[0]] = starterInsert($mysqli, 'vendors', [
$vendors[$vendor[0]] = starterInsert($mysqli, 'vendors', demoVendorManager($vendor[0], $vendor[2], $vendor[3]) + [
'vendor_name' => $vendor[0],
'vendor_description' => $vendor[1],
'vendor_contact_name' => 'Account Manager',
'vendor_phone_country_code' => '1',
'vendor_phone' => $vendor[2],
'vendor_extension' => (string)(200 + (crc32($vendor[0]) % 90)),
@@ -492,6 +542,7 @@ function demoEnsureOrgVendors($mysqli) {
'vendor_client_id' => 0,
'vendor_created_at' => demoMonthDateTime(24, 2, 9, 0),
]);
demoAttachTags($mysqli, 'vendor_tags', 'vendor_id', 'tag_id', $vendors[$vendor[0]], 6, demoVendorTags($vendor[0], $vendor[1], '', true));
}
return $vendors;
@@ -1170,10 +1221,9 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
// The client's own vendors - who they buy their line of business kit from
$vendor_ids = [];
foreach ($profile['vendors'] as $vendor_index => $vendor) {
$vendor_ids[] = starterInsert($mysqli, 'vendors', [
$vendor_id = starterInsert($mysqli, 'vendors', demoVendorManager($vendor[0], $vendor[2], $vendor[3]) + [
'vendor_name' => $vendor[0],
'vendor_description' => $vendor[1],
'vendor_contact_name' => 'Business Support',
'vendor_phone_country_code' => '1',
'vendor_phone' => $vendor[2],
'vendor_email' => 'support@' . $vendor[3],
@@ -1183,9 +1233,12 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
'vendor_code' => demoSerial($profile['abbreviation'] . $vendor[0], 6),
'vendor_account_number' => demoSerial($profile['abbreviation'] . 'acct' . $vendor_index, 8),
'vendor_notes' => $vendor[5],
'vendor_favorite' => $vendor_index === 0 ? 1 : 0,
'vendor_client_id' => $client_id,
'vendor_created_at' => demoMonthDateTime($months, 5, 13, 0),
]);
demoAttachTags($mysqli, 'vendor_tags', 'vendor_id', 'tag_id', $vendor_id, 6, demoVendorTags($vendor[0], $vendor[1], $vendor[5]));
$vendor_ids[] = $vendor_id;
$counts['documentation']++;
}
$primary_vendor_id = $vendor_ids[0] ?? 0;

View File

@@ -30,7 +30,7 @@ function starterContentPacks() {
'tags' => [
'label' => 'Tags',
'icon' => 'fa-tags',
'description' => 'Client, location, contact, credential and asset tags.',
'description' => 'Client, location, contact, credential, asset and vendor tags.',
],
'ticket_templates' => [
'label' => 'Ticket Templates',
@@ -661,6 +661,20 @@ function starterContentTags() {
[5, 'Leased', '#6c757d', 'file-contract'],
[5, 'Spare Stock', '#795548', 'boxes'],
[5, 'Patch Excluded', '#ffc107', 'ban'],
// Vendor
[6, 'ISP', '#007bff', 'wifi'],
[6, 'Telecom', '#28a745', 'phone-alt'],
[6, 'Line of Business', '#6f42c1', 'cubes'],
[6, 'Software', '#20c997', 'window-restore'],
[6, 'Hardware', '#001f3f', 'server'],
[6, 'Cloud', '#0078d4', 'cloud'],
[6, 'Security', '#dc3545', 'shield-alt'],
[6, 'Distributor', '#795548', 'truck'],
[6, 'Printing', '#6c757d', 'print'],
[6, 'Hosting', '#17a2b8', 'globe'],
[6, 'Professional Services', '#343a40', 'briefcase'],
[6, 'Authorised Contacts Only', '#d81b60', 'user-lock'],
];
return $tags;

View File

@@ -25,7 +25,7 @@ if (isset($_POST['add_vendor_template'])) {
$code = escapeSql($_POST['code']);
$notes = escapeSql($_POST['notes']);
mysqli_query($mysqli,"INSERT INTO vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes'");
mysqli_query($mysqli,"INSERT INTO vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone_country_code = '$phone_country_code', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes'");
$vendor_template_id = mysqli_insert_id($mysqli);
@@ -132,7 +132,10 @@ if (isset($_POST['edit_vendor_template'])) {
// Remove the first comma to prevent MySQL error
$sql = preg_replace('/,/', '', $sql, 1);
mysqli_query($mysqli,"UPDATE vendors SET $sql WHERE vendor_template_id = $vendor_template_id");
// Nothing ticked to push - nothing to update, rather than an empty SET
if (trim($sql) !== '') {
mysqli_query($mysqli,"UPDATE vendors SET $sql WHERE vendor_template_id = $vendor_template_id");
}
}
logAudit("Vendor Template", "Edit", "$session_name edited vendor template $name", 0, $vendor_template_id);

View File

@@ -19,6 +19,7 @@ $tag_types = [
3 => ['label' => 'Contact', 'icon' => 'fa-address-book'],
4 => ['label' => 'Credential', 'icon' => 'fa-key'],
5 => ['label' => 'Asset', 'icon' => 'fa-desktop'],
6 => ['label' => 'Vendor', 'icon' => 'fa-building'],
];
// Label for the selected type

View File

@@ -17,6 +17,13 @@ $sql = mysqli_query(
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
// How many clients have a vendor made from each template
$template_use = [];
$sql_template_use = mysqli_query($mysqli, "SELECT vendor_template_id, COUNT(DISTINCT vendor_client_id) FROM vendors WHERE vendor_template_id > 0 AND vendor_client_id > 0 AND vendor_archived_at IS NULL GROUP BY vendor_template_id");
while ($use_row = mysqli_fetch_row($sql_template_use)) {
$template_use[intval($use_row[0])] = intval($use_row[1]);
}
?>
<div class="card">
@@ -59,6 +66,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</a>
</th>
<th>Contact</th>
<th>Used by</th>
<th class="text-center">Action</th>
</tr>
</thead>
@@ -95,7 +103,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<th class="ps-3">
<a class="text-dark ajax-modal" href="#"
data-modal-url="modals/vendor_template/vendor_template_edit.php?id=<?= $vendor_template_id ?>">
<i class="fa fa-fw fa-building text-secondary me-2"></i><?= $vendor_template_name ?>
<span class="d-inline-flex align-items-center gap-2"><?= vendorTile($row['vendor_template_name'], 'sm') ?><?= $vendor_template_name ?></span>
</a>
<?php
if (!empty($vendor_template_account_number)) {
@@ -129,6 +137,14 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php } ?>
</td>
<td class="text-nowrap">
<?php $used_by = $template_use[$vendor_template_id] ?? 0; ?>
<?php if ($used_by) { ?>
<a href="/agent/vendors.php?view=clients&q=<?= urlencode($row['vendor_template_name']) ?>"><?= $used_by ?> client<?= $used_by === 1 ? '' : 's' ?></a>
<?php } else { ?>
<span class="text-secondary">-</span>
<?php } ?>
</td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
@@ -139,6 +155,10 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
data-modal-url="modals/vendor_template/vendor_template_edit.php?id=<?= $vendor_template_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="/agent/modals/vendor/vendor_template_clients.php?template_id=<?= $vendor_template_id ?>">
<i class="fas fa-fw fa-users me-2"></i>Add to clients
</a>
<?php if ($session_user_role == 3) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor_template=<?= $vendor_template_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">

View File

@@ -601,6 +601,29 @@ if (isset($_POST['location_set_notes'])) {
}
/*
* Vendor notes (escalation path, who is authorised to call) - the vendor page saves on blur
*/
if (isset($_POST['vendor_set_notes'])) {
validateCSRFToken();
$row = vendorRow($_POST['vendor_id'] ?? 0);
if (!$row) {
exit;
}
$client_id = intval($row['vendor_client_id']);
vendorEnforce($client_id, 2);
$vendor_id = intval($row['vendor_id']);
$notes = escapeSql($_POST['notes'] ?? '');
mysqli_query($mysqli, "UPDATE vendors SET vendor_notes = '$notes' WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Edit", "$session_name edited the notes on vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
}
/*
* License quick notes - the license page and quick look save on blur
*/
@@ -1085,17 +1108,20 @@ if (isset($_GET['get_client_vendors'])) {
$vendors_sql = mysqli_query(
$mysqli,
"SELECT vendor_id, vendor_name FROM vendors
"SELECT vendor_id, vendor_name, vendor_description FROM vendors
LEFT JOIN clients on vendor_client_id = client_id
WHERE vendors.vendor_archived_at IS NULL AND vendor_client_id = $client_id
" . clientScopeSql('vendor_client_id') . "
ORDER BY vendor_name ASC"
ORDER BY vendor_favorite DESC, vendor_name ASC"
);
// Always return the key, so a client with none gives [] rather than null
$response['vendors'] = [];
while ($row = mysqli_fetch_assoc($vendors_sql)) {
// The vendor's tile in the picker, the same as everywhere else
$row['vendor_initials'] = vendorInitials($row['vendor_name']);
$row['vendor_tone'] = vendorTone($row['vendor_name']);
$response['vendors'][] = $row;
}

View File

@@ -486,7 +486,7 @@ if (mysqli_num_rows($sql) == 0) {
<?php } ?>
<?php if ($vendor_name) { ?>
<dt>Vendor</dt>
<dd><a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $asset_vendor_id ?>"><?= $vendor_name ?></a></dd>
<dd><?= vendorLink($asset_vendor_id, $vendor_name) ?></dd>
<?php } ?>
<?php if ($asset_purchase_reference) { ?>
<dt>Order ref</dt><dd><?= $asset_purchase_reference ?></dd>

View File

@@ -469,7 +469,7 @@ while ($row = mysqli_fetch_assoc($sql)) {
$event_title = json_encode("Vendor : '" . $row['vendor_name'] . "' created");
$event_start = json_encode($row['vendor_created_at']);
echo "{ id: $event_id, title: $event_title, start: $event_start, color: 'brown', url: 'vendors.php?$client_url' },";
echo "{ id: $event_id, title: $event_title, start: $event_start, color: 'brown', url: 'vendor.php?client_id=$client_id&vendor_id=$event_id' },";
}
if (!isset($_GET['client_id'])) {

View File

@@ -302,19 +302,19 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$client_name = escapeHtml($row['client_name']);
// Add - if empty on the table
$domain_registrar_name_display = $domain_registrar_name ? "
<a class='ajax-modal' href='#' data-modal-url='modals/vendor/vendor.php?id=$domain_registrar_id'>
<a class='ajax-modal' href='#' data-modal-size='lg' data-modal-url='modals/vendor/vendor.php?id=$domain_registrar_id'>
$domain_registrar_name
</a>" : "-";
$domain_webhost_name_display = $domain_webhost_name ? "
<a class='ajax-modal' href='#' data-modal-url='modals/vendor/vendor.php?id=$domain_webhost_id'>
<a class='ajax-modal' href='#' data-modal-size='lg' data-modal-url='modals/vendor/vendor.php?id=$domain_webhost_id'>
$domain_webhost_name
</a>" : "-";
$domain_dnshost_name_display = $domain_dnshost_name ? "
<a class='ajax-modal' href='#' data-modal-url='modals/vendor/vendor.php?id=$domain_dnshost_id'>
<a class='ajax-modal' href='#' data-modal-size='lg' data-modal-url='modals/vendor/vendor.php?id=$domain_dnshost_id'>
$domain_dnshost_name
</a>" : "-";
$domain_mailhost_name_display = $domain_mailhost_name ? "
<a class='ajax-modal' href='#' data-modal-url='modals/vendor/vendor.php?id=$domain_mailhost_id'>
<a class='ajax-modal' href='#' data-modal-size='lg' data-modal-url='modals/vendor/vendor.php?id=$domain_mailhost_id'>
$domain_mailhost_name
</a>" : "-";

View File

@@ -299,7 +299,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?= $category_name ?>
<div class="text-secondary"><small><?= truncate($expense_description, 60) ?></small></div>
</td>
<td><?= $vendor_name ?></td>
<td><?= $expense_vendor_id ? vendorLink($expense_vendor_id, $vendor_name, ['page' => true]) : '-' ?></td>
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?></td>
<td><?= $account_name ?></td>
<td><?= $client_name ?></td>

View File

@@ -31,6 +31,9 @@ $num_certificates = $row['num'];
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('software_id') AS num FROM software LEFT JOIN clients ON software_client_id = client_id WHERE software_archived_at IS NULL AND client_archived_at IS NULL " . clientScopeSql('software_client_id') . ""));
$num_software = $row['num'];
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_id') AS num FROM vendors JOIN clients ON vendor_client_id = client_id WHERE vendor_archived_at IS NULL AND client_archived_at IS NULL " . clientScopeSql('vendor_client_id') . ""));
$num_vendors = $row['num'];
?>
<!-- Main Sidebar Container -->
@@ -173,6 +176,19 @@ $num_software = $row['num'];
</a>
</li>
<li class="nav-item">
<a href="vendors.php?view=clients" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "vendors.php" || basename($_SERVER["PHP_SELF"]) == "vendor.php") { echo "active"; } ?>">
<i class="nav-icon fas fa-building"></i>
<p>
Vendors
<?php
if ($num_vendors > 0) { ?>
<span class="right badge text-light"><?= $num_vendors ?></span>
<?php } ?>
</p>
</a>
</li>
<li class="nav-item">
<a href="services.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "services.php") { echo "active"; } ?>">
<i class="nav-icon fas fa-stream"></i>

View File

@@ -84,7 +84,7 @@
</li>
<li class="nav-item">
<a href="/agent/locations.php?client_id=<?= $client_id ?>" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "locations.php") { echo "active"; } ?>">
<a href="/agent/locations.php?client_id=<?= $client_id ?>" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "locations.php" || basename($_SERVER["PHP_SELF"]) == "location.php") { echo "active"; } ?>">
<i class="nav-icon fas fa-map-marker-alt"></i>
<p>
Locations
@@ -97,7 +97,7 @@
</li>
<li class="nav-item">
<a href="/agent/vendors.php?client_id=<?= $client_id ?>" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "vendors.php") { echo "active"; } ?>">
<a href="/agent/vendors.php?client_id=<?= $client_id ?>" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "vendors.php" || basename($_SERVER["PHP_SELF"]) == "vendor.php") { echo "active"; } ?>">
<i class="nav-icon fas fa-building"></i>
<p>
Vendors

View File

@@ -74,7 +74,7 @@
<?php } ?>
<li class="nav-item">
<a href="/agent/vendors.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "vendors.php") { echo "active"; } ?>">
<a href="/agent/vendors.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "vendors.php" || basename($_SERVER["PHP_SELF"]) == "vendor.php") { echo "active"; } ?>">
<i class="nav-icon fas fa-building"></i>
<p>Vendors</p>
</a>

View File

@@ -324,7 +324,15 @@
// Populate dropdown
vendors.forEach(vendor => {
vendorSelectDropdown[vendorSelectDropdown.length] = new Option(vendor.vendor_name, vendor.vendor_id);
const option = new Option(vendor.vendor_name, vendor.vendor_id);
// The vendor's tile and what they are, as on the vendor pages
option.dataset.initials = vendor.vendor_initials || '';
option.dataset.tone = vendor.vendor_tone || 'secondary';
option.dataset.soft = '1';
if (vendor.vendor_description) {
option.dataset.description = vendor.vendor_description;
}
vendorSelectDropdown[vendorSelectDropdown.length] = option;
});
applyPreselection(vendorSelectDropdown);

View File

@@ -265,11 +265,12 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
$vendor_phone = contactPhoneLink($license['vendor_phone'], $license['vendor_phone_country_code'], $license['vendor_extension']);
$vendor_email = escapeHtml($license['vendor_email']);
$vendor_website = escapeHtml($license['vendor_website']);
$vendor_account = escapeHtml($license['vendor_account_number']); ?>
$vendor_account = escapeHtml($license['vendor_account_number']);
$vendor_pin = vendorSecretHtml($license['vendor_code']); ?>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-building text-secondary me-2"></i>Vendor</h3>
<a class="small ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
<span class="small"><?= vendorLink($vendor_id, $license['vendor_name'], ['page' => true, 'client_id' => $client_id]) ?></span>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
@@ -277,6 +278,9 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
<dt>Account</dt>
<dd><span class="font-monospace"><?= $vendor_account ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="<?= $vendor_account ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($vendor_pin) { ?>
<dt>PIN</dt><dd><?= $vendor_pin ?></dd>
<?php } ?>
<?php if ($vendor_phone) { ?>
<dt>Phone</dt><dd><?= $vendor_phone ?></dd>
<?php } ?>
@@ -285,7 +289,7 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
<dd><a href="mailto:<?= $vendor_email ?>"><?= $vendor_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $vendor_email ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($vendor_website) { ?>
<dt>Website</dt><dd><a href="<?= escapeUrl($license['vendor_website']) ?>" target="_blank" rel="noopener"><?= $vendor_website ?><i class="fas fa-external-link-alt ms-1 small"></i></a></dd>
<dt>Website</dt><dd><a href="<?= escapeHtml(vendorWebsiteUrl($license['vendor_website'])) ?>" target="_blank" rel="noopener"><?= escapeHtml(vendorWebsiteLabel($license['vendor_website'])) ?><i class="fas fa-external-link-alt ms-1 small"></i></a></dd>
<?php } ?>
<?php if ($license['vendor_hours']) { ?>
<dt>Hours</dt><dd><?= escapeHtml($license['vendor_hours']) ?></dd>

View File

@@ -21,7 +21,9 @@ $asset_ip = $asset_ipv6 = $asset_nat_ip = $asset_mac = $asset_uri = $asset_uri_2
$asset_status = $asset_purchase_reference = $asset_purchase_date = $asset_install_date = $asset_warranty_expire = '';
$asset_physical_location = $asset_notes = $asset_photo = '';
$asset_favorite = 0;
$asset_vendor_id = $asset_network_id = 0;
$asset_network_id = 0;
// Opened from a vendor's page - bought from them
$asset_vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id) : 0;
// Opened from a location's page - start at that location
$asset_location_id = $client_id ? assetClientOwnedId('location', $_GET['location_id'] ?? 0, $client_id) : 0;
$asset_contact_id = intval($_GET['contact_id'] ?? 0);

View File

@@ -53,9 +53,9 @@ if ($client_id) {
while ($row = mysqli_fetch_assoc($sql)) {
$network_options[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name FROM vendors
$sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name, vendor_description FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $asset_vendor_id)
ORDER BY vendor_name ASC");
ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_options[] = $row;
}
@@ -384,7 +384,7 @@ $asset_form_icons = [
<option value="">- Select Vendor -</option>
<?php foreach ($vendor_options as $vendor_option) {
$vendor_option_id = intval($vendor_option['vendor_id']); ?>
<option value="<?= $vendor_option_id ?>" <?php if ($asset_vendor_id === $vendor_option_id) { echo "selected"; } ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<option value="<?= $vendor_option_id ?>" <?php if ($asset_vendor_id === $vendor_option_id) { echo "selected"; } ?><?= selectOptionVendor($vendor_option['vendor_name'], $vendor_option['vendor_description']) ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>

View File

@@ -5,16 +5,20 @@ require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
$contact_id = intval($_GET['contact_id'] ?? 0);
$asset_id = intval($_GET['asset_id'] ?? 0);
// Opened from a vendor's page - their portal login, linked to them
$vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id) : 0;
$vendor_name = $vendor_id ? getFieldById('vendors', $vendor_id, 'vendor_name') : '';
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-key me-2"></i>New Credential</h5>
<h5 class="modal-title"><i class="fa fa-fw fa-key me-2"></i>New Credential<?php if ($vendor_name) { ?> <small class="opacity-75 ms-1">for <?= escapeHtml($vendor_name) ?></small><?php } ?></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'] ?>">
<input type="hidden" name="vendor_id" value="<?= $vendor_id ?>">
<div class="modal-body">

View File

@@ -2,7 +2,7 @@
/*
* New document - in the folder it was opened from, or linked to the asset,
* contact, license or location it was opened from.
* contact, license, location or vendor it was opened from.
*/
require_once '../../../includes/modal_header.php';
@@ -18,7 +18,8 @@ $asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset
$software = linkRecord('software', $_GET['software_id'] ?? 0);
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
$location_id = assetClientOwnedId('location', $_GET['location_id'] ?? 0, $client_id);
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ($location_id ? getFieldById('locations', $location_id, 'location_name') : '')));
$vendor_id = assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id);
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ($location_id ? getFieldById('locations', $location_id, 'location_name') : ($vendor_id ? getFieldById('vendors', $vendor_id, 'vendor_name') : ''))));
$document_form_mode = 'add';
$document_name = $document_description = $document_content = '';
@@ -39,6 +40,7 @@ ob_start();
<input type="hidden" name="asset" value="<?= $asset_id ?>">
<input type="hidden" name="software" value="<?= $software_id ?>">
<input type="hidden" name="location" value="<?= $location_id ?>">
<input type="hidden" name="vendor" value="<?= $vendor_id ?>">
<div class="modal-body">
<?php require 'document_form.php'; ?>
</div>

View File

@@ -85,12 +85,12 @@ ob_start();
<option value="">- Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php } ?>
</select>
</div>
@@ -104,12 +104,12 @@ ob_start();
<option value="">- Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php } ?>
</select>
</div>
@@ -123,12 +123,12 @@ ob_start();
<option value="">- Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php } ?>
</select>
</div>
@@ -142,12 +142,12 @@ ob_start();
<option value="">- Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php } ?>
</select>
</div>

View File

@@ -92,12 +92,12 @@ ob_start();
<select class="form-select select2" name="registrar">
<option value="">- Select Vendor -</option>
<?php
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($vendor_sql)) {
$vendor_id = $row['vendor_id'];
$vendor_name = $row['vendor_name'];
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option <?php if ($domain_registrar == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option <?php if ($domain_registrar == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}
?>
@@ -112,12 +112,12 @@ ob_start();
<select class="form-select select2" name="webhost">
<option value="">- Select Vendor -</option>
<?php
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($vendor_sql)) {
$vendor_id = $row['vendor_id'];
$vendor_name = $row['vendor_name'];
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option <?php if ($domain_webhost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option <?php if ($domain_webhost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}
?>
@@ -132,12 +132,12 @@ ob_start();
<select class="form-select select2" name="dnshost">
<option value="">- Select Vendor -</option>
<?php
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($vendor_sql)) {
$vendor_id = $row['vendor_id'];
$vendor_name = $row['vendor_name'];
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option <?php if ($domain_dnshost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option <?php if ($domain_dnshost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}
?>
@@ -152,12 +152,12 @@ ob_start();
<select class="form-select select2" name="mailhost">
<option value="">- Select Vendor -</option>
<?php
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($vendor_sql)) {
$vendor_id = $row['vendor_id'];
$vendor_name = $row['vendor_name'];
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option <?php if ($domain_mailhost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option <?php if ($domain_mailhost == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}
?>

View File

@@ -3,6 +3,8 @@
require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
// Opened from one of our vendors' pages - that vendor
$prefill_vendor_id = intval($_GET['vendor_id'] ?? 0);
ob_start();
@@ -80,12 +82,12 @@ ob_start();
<option value="">- Select a Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>" <?= $vendor_id === $prefill_vendor_id ? 'selected' : '' ?><?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}

View File

@@ -104,12 +104,12 @@ ob_start();
<select class="form-select select2" name="vendor" required>
<?php
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 ORDER BY vendor_name ASC");
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at IS NULL OR vendor_id = $expense_vendor_id) ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql_vendors)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name_select = escapeHtml($row['vendor_name']);
?>
<option <?php if ($expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"><?= $vendor_name_select ?></option>
<option <?php if ($expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name_select ?></option>
<?php
}

View File

@@ -118,12 +118,12 @@ ob_start();
<select class="form-select select2" name="vendor" required>
<?php
$sql_select = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at > '$expense_created_at' OR vendor_archived_at IS NULL) ORDER BY vendor_name ASC");
$sql_select = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at > '$expense_created_at' OR vendor_archived_at IS NULL OR vendor_id = $expense_vendor_id) ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql_select)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name_select = escapeHtml($row['vendor_name']);
?>
<option <?php if ($expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"><?= $vendor_name_select ?></option>
<option <?php if ($expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name_select ?></option>
<?php
}

View File

@@ -3,8 +3,8 @@
/*
* Upload files into a folder - dropped onto the box, picked with the button,
* or dropped straight onto the files list (agent/js/files.js opens this and
* hands them over). Opened from an asset, a contact, a license or a location,
* the uploads are also linked to it.
* hands them over). Opened from an asset, a contact, a license, a location or
* a vendor, the uploads are also linked to it.
*/
require_once '../../../includes/modal_header.php';
@@ -21,7 +21,8 @@ $asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset
$software = linkRecord('software', $_GET['software_id'] ?? 0);
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
$location_id = assetClientOwnedId('location', $_GET['location_id'] ?? 0, $client_id);
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ($location_id ? getFieldById('locations', $location_id, 'location_name') : '')));
$vendor_id = assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id);
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ($location_id ? getFieldById('locations', $location_id, 'location_name') : ($vendor_id ? getFieldById('vendors', $vendor_id, 'vendor_name') : ''))));
// The extensions agent/post/file.php accepts - offered to the picker so the wrong kind is never chosen
$upload_extensions = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'odt', 'csv', 'xls', 'xlsx', 'ods',
@@ -43,6 +44,7 @@ ob_start();
<input type="hidden" name="asset_id" value="<?= $asset_id ?>">
<input type="hidden" name="software_id" value="<?= $software_id ?>">
<input type="hidden" name="location_id" value="<?= $location_id ?>">
<input type="hidden" name="vendor_id" value="<?= $vendor_id ?>">
<div class="modal-body">
<label class="itflow-upload-drop" data-upload-drop>

View File

@@ -62,7 +62,7 @@ function linkOptionData($to, $row, $client_id) {
case 'location':
return selectOptionData(['icon' => 'fas fa-' . (intval($row['location_primary']) ? 'building' : 'map-marker-alt'), 'description' => implode(' · ', array_filter([intval($row['location_primary']) ? 'Primary' : '', locationAddressText($row)]))]);
case 'vendor':
return selectOptionData(['icon' => 'fas fa-building', 'description' => $row['vendor_description']]);
return selectOptionVendor($row['name'], $row['vendor_description']);
}
return '';
}

View File

@@ -3,6 +3,8 @@
require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
// Opened from one of our vendors' pages - that vendor
$prefill_vendor_id = intval($_GET['vendor_id'] ?? 0);
ob_start();
@@ -118,12 +120,12 @@ ob_start();
<option value="">- Vendor -</option>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND vendor_archived_at IS NULL ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
<option value="<?= $vendor_id ?>" <?= $vendor_id === $prefill_vendor_id ? 'selected' : '' ?><?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?></option>
<?php
}

View File

@@ -156,12 +156,12 @@ ob_start();
<select class="form-select select2" name="vendor" required>
<?php
$sql_select = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at > '$recurring_expense_created_at' OR vendor_archived_at IS NULL) ORDER BY vendor_name ASC");
$sql_select = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at > '$recurring_expense_created_at' OR vendor_archived_at IS NULL OR vendor_id = $recurring_expense_vendor_id) ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql_select)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name_select = escapeHtml($row['vendor_name']);
?>
<option <?php if ($recurring_expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"><?= $vendor_name_select ?></option>
<option <?php if ($recurring_expense_vendor_id == $vendor_id_select) { ?> selected <?php } ?> value="<?= $vendor_id_select ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name_select ?></option>
<?php
}

View File

@@ -227,7 +227,7 @@ ob_start();
while ($row = mysqli_fetch_assoc($sql_vendors)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
echo "<li><a class='ajax-modal' href='#' data-modal-size='lg' data-modal-url='modals/vendor/vendor.php?id=$vendor_id'>$vendor_name</a></li>";
echo "<li class='mb-1'>" . vendorLink($vendor_id, $row['vendor_name']) . "</li>";
}
echo "</ul>";
}

View File

@@ -138,11 +138,11 @@ ob_start();
<label for="vendors">Select related vendors</label>
<select class="form-select select2" id="vendors" name="vendors[]" multiple>
<?php
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id");
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
echo "<option value=\"$vendor_id\">$vendor_name</option>";
echo "<option value=\"$vendor_id\"" . selectOptionVendor($row['vendor_name'], $row['vendor_description']) . ">$vendor_name</option>";
}
?>
</select>

View File

@@ -202,16 +202,16 @@ ob_start();
<?php
$selected_ids = array_column(mysqli_fetch_all($sql_vendors, MYSQLI_ASSOC), "vendor_id");
$sql_all = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE (vendor_archived_at > '$service_created_at' OR vendor_archived_at IS NULL) AND vendor_client_id = $client_id");
$sql_all = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE (vendor_archived_at > '$service_created_at' OR vendor_archived_at IS NULL) AND vendor_client_id = $client_id ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row_all = mysqli_fetch_assoc($sql_all)) {
$vendor_id = intval($row_all['vendor_id']);
$vendor_name = escapeHtml($row_all['vendor_name']);
if (in_array($vendor_id, $selected_ids)) {
echo "<option value=\"$vendor_id\" selected>$vendor_name</option>";
echo "<option value=\"$vendor_id\" selected" . selectOptionVendor($row_all['vendor_name'], $row_all['vendor_description']) . ">$vendor_name</option>";
}
else{
echo "<option value=\"$vendor_id\">$vendor_name</option>";
echo "<option value=\"$vendor_id\"" . selectOptionVendor($row_all['vendor_name'], $row_all['vendor_description']) . ">$vendor_name</option>";
}
}
?>

View File

@@ -129,7 +129,7 @@ ob_start();
<div class="itflow-glance-item" title="Vendor">
<i class="fas fa-fw fa-building"></i>
<span class="min-w-0">
<a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
<?= vendorLink($vendor_id, $license['vendor_name']) ?>
<?php if ($vendor_phone) { ?><span class="text-body-secondary">&middot;</span> <?= $vendor_phone ?><?php } ?>
</span>
</div>

View File

@@ -19,7 +19,8 @@ $software_form_mode = 'add';
$software_id = 0;
$software_name = $software_version = $software_type = $software_description = $software_license_type = '';
$software_seats = $software_key = $software_purchase_reference = $software_purchase = $software_expire = $software_notes = '';
$software_vendor_id = 0;
// Opened from a vendor's page - bought through them
$software_vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id) : 0;
$software_favorite = 0;
// Opened from a device or a user - they get the first seat

View File

@@ -40,7 +40,7 @@ $vendor_options = $asset_options = $contact_options = [];
if ($client_id) {
$sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name, vendor_description FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $software_vendor_id)
ORDER BY vendor_name ASC");
ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_options[] = $row;
}
@@ -180,7 +180,7 @@ $form_id = 'softwareForm' . ($software_form_mode === 'edit' ? $software_id : 'Ne
<option value="">- Select Vendor -</option>
<?php foreach ($vendor_options as $vendor_option) {
$vendor_option_id = intval($vendor_option['vendor_id']); ?>
<option value="<?= $vendor_option_id ?>" <?= $software_vendor_id === $vendor_option_id ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas fa-building', 'description' => $vendor_option['vendor_description']]) ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<option value="<?= $vendor_option_id ?>" <?= $software_vendor_id === $vendor_option_id ? 'selected' : '' ?><?= selectOptionVendor($vendor_option['vendor_name'], $vendor_option['vendor_description']) ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>

View File

@@ -7,6 +7,7 @@ $contact_id = intval($_GET['contact_id'] ?? 0);
$project_id = intval($_GET['project_id'] ?? 0);
$asset_id = intval($_GET['asset_id'] ?? 0);
$location_id = intval($_GET['location_id'] ?? 0);
$vendor_id = intval($_GET['vendor_id'] ?? 0);
// A ticket started from somewhere that knows what it is about (a license's
// Renewal Ticket) - plain text in, escaped here
@@ -247,7 +248,7 @@ ob_start();
<label>Vendor</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
<select class="form-select select2" name="vendor_id" id="vendorSelect">
<select class="form-select select2" name="vendor_id" id="vendorSelect" data-selected="<?= $vendor_id ?>">
</select>
</div>
</div>

View File

@@ -306,12 +306,14 @@ ob_start();
<option value="0">- None -</option>
<?php
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description, vendor_archived_at FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $vendor_id)
ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql_vendors)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name_select = escapeHtml($row['vendor_name']);
$vendor_name_select = escapeHtml($row['vendor_name']) . ($row['vendor_archived_at'] ? ' (archived)' : '');
?>
<option <?php if ($vendor_id == $vendor_id_select) { echo "selected"; } ?> value="<?= $vendor_id_select ?>"><?= $vendor_name_select ?></option>
<option <?php if ($vendor_id == $vendor_id_select) { echo "selected"; } ?> value="<?= $vendor_id_select ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name_select ?></option>
<?php
}

View File

@@ -40,12 +40,15 @@ ob_start();
<option value="0">No vendor</option>
<?php
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
// The client's vendors, starred first - and the one already set, even if archived since
$sql_vendors = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description, vendor_archived_at FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $vendor_id)
ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql_vendors)) {
$vendor_id_select = intval($row['vendor_id']);
$vendor_name = escapeHtml($row['vendor_name']);
?>
<option <?php if ($vendor_id == $vendor_id_select) { echo "selected"; } ?> value="<?= $vendor_id_select ?>"><?= $vendor_name ?></option>
<option <?php if ($vendor_id == $vendor_id_select) { echo "selected"; } ?> value="<?= $vendor_id_select ?>"<?= selectOptionVendor($row['vendor_name'], $row['vendor_description']) ?>><?= $vendor_name ?><?= $row['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<?php
}

View File

@@ -1,123 +1,307 @@
<?php
/*
* Vendor quick look - the vendor page in a modal, opened from the vendors
* list, tickets, licenses, assets, domains and search. Everything for the
* call to them (support line, hours, SLA, the account number and PIN they ask
* for, the account manager), the notes, then what is open with them and what
* came from them, in tabs - only those with something in them.
*/
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_client');
$vendor_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT vendor_account_number, vendor_client_id, vendor_code, vendor_contact_name,
vendor_description, vendor_email, vendor_extension, vendor_hours, vendor_name,
vendor_notes, vendor_phone, vendor_sla, vendor_website FROM vendors WHERE vendor_id = $vendor_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$name = escapeHtml($row['vendor_name']);
$description = escapeHtml($row['vendor_description']);
$account_number = escapeHtml($row['vendor_account_number']);
$contact_name = escapeHtml($row['vendor_contact_name']);
$phone = preg_replace("/[^0-9]/", '', (string) $row['vendor_phone']);
$extension = preg_replace("/[^0-9]/", '', (string) $row['vendor_extension']);
$email = escapeHtml($row['vendor_email']);
$website = escapeHtml($row['vendor_website']);
$hours = escapeHtml($row['vendor_hours']);
$sla = escapeHtml($row['vendor_sla']);
$code = escapeHtml($row['vendor_code']);
$notes = escapeHtml($row['vendor_notes']);
$client_id = intval($row['vendor_client_id']);
$vendor_id = intval($_GET['id'] ?? 0);
$vendor = vendorRow($vendor_id);
if (!$vendor) {
echo json_encode(['error' => 'That vendor no longer exists.']);
exit;
}
$client_id = intval($vendor['vendor_client_id']);
if ($client_id) {
enforceUserPermission('module_client');
enforceClientAccess();
}
// Licenses bought through this vendor - soonest renewal first
$vendor_licenses = [];
if (lookupUserPermission('module_support') >= 1) {
$sql_vendor_licenses = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type,
$vendor_name = escapeHtml($vendor['vendor_name']);
$vendor_archived = !empty($vendor['vendor_archived_at']);
$vendor_phone = contactPhoneLink($vendor['vendor_phone'], $vendor['vendor_phone_country_code'], $vendor['vendor_extension']);
$vendor_dial = vendorDialNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']);
$vendor_email = escapeHtml($vendor['vendor_email']);
$vendor_website_url = vendorWebsiteUrl($vendor['vendor_website']);
$vendor_account = escapeHtml($vendor['vendor_account_number']);
$vendor_pin = vendorSecretHtml($vendor['vendor_code']);
$vendor_notes = escapeHtml($vendor['vendor_notes']);
$manager_phone = contactPhoneLink($vendor['vendor_contact_phone'], $vendor['vendor_contact_phone_country_code']);
$manager_email = escapeHtml($vendor['vendor_contact_email']);
$can_edit = vendorCan($client_id, 2) && !$vendor_archived;
$can_support = lookupUserPermission('module_support') >= 1;
$can_support_edit = lookupUserPermission('module_support') >= 2;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$can_financial = lookupUserPermission('module_financial') >= 1;
$cases = $licenses = $assets = $logins = [];
if ($can_support) {
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_vendor_ticket_number, ticket_status_name, ticket_status_color
FROM tickets
LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status
WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL
ORDER BY ticket_number DESC");
while ($row = mysqli_fetch_assoc($sql)) {
$cases[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type,
software_seats, software_expire, " . softwareSeatsUsedSql('software.software_id') . " AS seats_used
FROM software
WHERE software_vendor_id = $vendor_id AND software_archived_at IS NULL " . clientScopeSql('software_client_id') . "
WHERE software_client_id = $client_id AND software_vendor_id = $vendor_id AND software_archived_at IS NULL
ORDER BY software_expire IS NULL, software_expire ASC, software_name ASC");
while ($vendor_license = mysqli_fetch_assoc($sql_vendor_licenses)) {
$vendor_licenses[] = $vendor_license;
while ($row = mysqli_fetch_assoc($sql)) {
$licenses[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT asset_id AS id, asset_name AS name, asset_type, asset_make, asset_model, asset_warranty_expire
FROM assets
WHERE asset_client_id = $client_id AND asset_vendor_id = $vendor_id AND asset_archived_at IS NULL
ORDER BY asset_type ASC, asset_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$assets[] = $row;
}
}
if ($can_credentials && $client_id) {
$logins = linkedRecords('vendor', $vendor_id, 'credential');
}
$spend = !$client_id && $can_financial ? vendorSpend($vendor_id) : null;
$quick_tabs = array_filter([
'cases' => ['label' => 'Open cases', 'count' => count($cases)],
'licenses' => ['label' => 'Licenses', 'count' => count($licenses)],
'assets' => ['label' => 'Assets', 'count' => count($assets)],
'logins' => ['label' => 'Logins', 'count' => count($logins)],
], function ($tab) {
return $tab['count'] > 0;
});
$first_tab = array_key_first($quick_tabs);
$quick_id = "vendor-quick-$vendor_id";
$page_url = 'vendor.php?' . ($client_id ? "client_id=$client_id&" : '') . "vendor_id=$vendor_id";
ob_start();
?>
<div class="modal-header bg-dark text-white">
<div class="d-flex align-items-center">
<i class="fas fa-fw fa-building fa-2x me-3"></i>
<div>
<h5 class="modal-title mb-0"><?= $name ?></h5>
<div class="text-muted"><?= $description ?: '-'; ?></div>
<div class="modal-header bg-dark" data-bs-theme="dark">
<div class="d-flex align-items-center gap-3 min-w-0">
<?= vendorTile($vendor['vendor_name']) ?>
<div class="min-w-0">
<h5 class="modal-title d-flex flex-wrap align-items-center column-gap-2 row-gap-1">
<span class="fw-semibold text-break"><?= $vendor_name ?></span>
<?php if (intval($vendor['vendor_favorite'])) { ?><i class="fas fa-star text-warning small" title="Starred"></i><?php } ?>
<?php if ($vendor_archived) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
</h5>
<div class="small text-body-secondary d-flex flex-wrap column-gap-3 mt-1">
<span><?= $client_id ? escapeHtml($vendor['client_name']) : 'Our vendor' ?></span>
<?php if ($vendor['vendor_description']) { ?><span><?= escapeHtml($vendor['vendor_description']) ?></span><?php } ?>
</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light">
<div class="modal-body">
<!-- Vendor Info Card -->
<div class="card mb-3 shadow-sm rounded">
<div class="card-body">
<h6 class="text-secondary"><i class="fas fa-info-circle me-2"></i>Vendor Details</h6>
<div class="row">
<div class="col-sm-6">
<div><strong>Account Number:</strong> <?= $account_number ?: '-'; ?></div>
<div><strong>Hours:</strong> <?= $hours ?: '-'; ?></div>
<div><strong>SLA:</strong> <?= $sla ?: '-'; ?></div>
</div>
<div class="col-sm-6">
<div><strong>Code:</strong> <?= $code ?: '-'; ?></div>
<div><strong>Website:</strong> <?= !empty($website) ? '<a href="https://' . $website . '" target="_blank" class="text-primary">' . $website . '</a>' : '<span class="text-muted">Not Available</span>' ?></div>
</div>
<!-- Calling them - the icons say what each line is -->
<div class="row g-2 itflow-asset-glance">
<div class="col-sm-6">
<div class="itflow-glance-item" title="Support line">
<i class="fas fa-fw fa-headset"></i>
<span><?= $vendor_phone ?: '<span class="text-body-secondary">No support line</span>' ?></span>
</div>
<?php if ($vendor['vendor_hours'] || $vendor['vendor_sla']) { ?>
<div class="itflow-glance-item" title="Hours and SLA">
<i class="fas fa-fw fa-clock"></i>
<span><?= escapeHtml(implode(' · ', array_filter([$vendor['vendor_hours'], $vendor['vendor_sla']]))) ?></span>
</div>
<?php } ?>
<?php if ($vendor_email) { ?>
<div class="itflow-glance-item" title="Support email">
<i class="fas fa-fw fa-envelope"></i>
<span class="min-w-0 text-truncate"><a href="mailto:<?= $vendor_email ?>"><?= $vendor_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $vendor_email ?>"><i class="far fa-copy"></i></button></span>
</div>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<div class="itflow-glance-item" title="Support site">
<i class="fas fa-fw fa-globe"></i>
<span class="min-w-0 text-truncate"><a href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><?= escapeHtml(vendorWebsiteLabel($vendor['vendor_website'])) ?></a></span>
</div>
<?php } ?>
</div>
<div class="col-sm-6">
<div class="itflow-glance-item" title="Account number">
<i class="fas fa-fw fa-fingerprint"></i>
<span><?= $vendor_account ? '<span class="font-monospace">' . $vendor_account . '</span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="' . $vendor_account . '"><i class="far fa-copy"></i></button>' : '<span class="text-body-secondary">No account number</span>' ?></span>
</div>
<?php if ($vendor_pin) { ?>
<div class="itflow-glance-item" title="PIN / passcode">
<i class="fas fa-fw fa-key"></i>
<span><?= $vendor_pin ?></span>
</div>
<?php } ?>
<?php if ($vendor['vendor_contact_name'] || $manager_phone || $manager_email) { ?>
<div class="itflow-glance-item" title="Account manager">
<i class="fas fa-fw fa-user-tie"></i>
<span class="min-w-0">
<?= escapeHtml($vendor['vendor_contact_name'] ?: 'Account manager') ?>
<?php if ($manager_phone) { ?><span class="text-body-secondary">&middot;</span> <?= $manager_phone ?><?php } ?>
<?php if ($manager_email) { ?><div class="text-truncate"><a href="mailto:<?= $manager_email ?>"><?= $manager_email ?></a></div><?php } ?>
</span>
</div>
<?php } ?>
<?php if ($spend) { ?>
<div class="itflow-glance-item" title="Spent with them">
<i class="fas fa-fw fa-receipt"></i>
<span><?= numfmt_format_currency($currency_format, $spend['year'], $session_company_currency) ?> this year<?php if ($spend['last']) { ?> <span class="text-body-secondary">&middot; last <?= escapeHtml($spend['last']) ?></span><?php } ?></span>
</div>
<?php } ?>
</div>
</div>
<!-- Contact Info Card -->
<div class="card mb-3 shadow-sm rounded">
<div class="card-body">
<h6 class="text-secondary"><i class="fas fa-user me-2"></i>Contact Information</h6>
<div class="row">
<div class="col-sm-6">
<div><strong>Contact Name:</strong> <?= $contact_name ?: '-'; ?></div>
<div><strong>Phone:</strong> <?= $phone ?: '-'; ?></div>
</div>
<div class="col-sm-6">
<div><strong>Email:</strong> <?= !empty($email) ? '<a href="mailto:' . $email . '" class="text-primary">' . $email . '</a>' : '<span class="text-muted">Not Available</span>' ?></div>
</div>
</div>
<?php if ($vendor_dial || $vendor_email || $vendor_website_url || ($client_id && $can_support_edit && !$vendor_archived)) { ?>
<div class="d-flex flex-wrap gap-2 mt-3">
<?php if ($vendor_dial) { ?>
<a class="btn btn-sm btn-outline-primary" href="tel:<?= escapeHtml($vendor_dial) ?><?= $vendor['vendor_extension'] ? ',' . escapeHtml($vendor['vendor_extension']) : '' ?>"><i class="fas fa-phone me-1"></i>Call support</a>
<?php } ?>
<?php if ($vendor_email) { ?>
<a class="btn btn-sm btn-outline-primary" href="mailto:<?= $vendor_email ?>"><i class="fas fa-envelope me-1"></i>Email</a>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<a class="btn btn-sm btn-outline-primary" href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><i class="fas fa-external-link-alt me-1"></i>Support site</a>
<?php } ?>
<?php if ($client_id && $can_support_edit && !$vendor_archived) { ?>
<a class="btn btn-sm btn-outline-secondary ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/ticket/ticket_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>"><i class="fas fa-life-ring me-1"></i>New case</a>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if ($vendor_licenses) { ?>
<!-- Licenses from this vendor -->
<div class="card mb-3 shadow-sm rounded">
<div class="card-body">
<h6 class="text-secondary"><i class="fas fa-cube me-2"></i>Licenses <span class="badge rounded-pill text-bg-secondary ms-1"><?= count($vendor_licenses) ?></span></h6>
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($vendor_licenses as $vendor_license) { ?>
<li class="itflow-link-row"><?= linkRowHtml('software', $vendor_license, $client_id) ?></li>
<?php } ?>
</ul>
<?php if ($can_edit || $vendor_notes) { ?>
<label class="form-label small text-body-secondary mt-3 mb-1"><i class="fas fa-sticky-note me-1"></i>Notes</label>
<textarea class="form-control form-control-sm itflow-vendor-notes" rows="3" placeholder="Escalation path, who may call them, circuit IDs - saves when you click away" aria-label="Notes"
data-vendor-id="<?= $vendor_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token'] ?? '') ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $vendor_notes ?></textarea>
<?php } ?>
<?php if ($quick_tabs) { ?>
<div class="itflow-asset-related itflow-asset-quick-related mt-3" data-vendor-quick="<?= $vendor_id ?>">
<ul class="nav nav-pills itflow-asset-pills mb-2" role="tablist">
<?php foreach ($quick_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab_key === $first_tab ? 'active' : '' ?>" data-bs-toggle="pill" data-bs-target="#<?= $quick_id ?>-<?= $tab_key ?>" data-vendor-quick-tab="<?= $tab_key ?>" type="button" role="tab">
<?= $tab['label'] ?> <span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
</button>
</li>
<?php } ?>
</ul>
<div class="tab-content">
<?php if ($cases) { ?>
<div class="tab-pane fade <?= $first_tab === 'cases' ? 'show active' : '' ?>" id="<?= $quick_id ?>-cases" role="tabpanel">
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<tbody>
<?php foreach ($cases as $case) {
$ticket_id = intval($case['ticket_id']);
$their_ref = escapeHtml($case['ticket_vendor_ticket_number']); ?>
<tr>
<td class="text-nowrap"><a href="ticket.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>"><?= escapeHtml($case['ticket_prefix']) . intval($case['ticket_number']) ?></a></td>
<td class="w-100"><a class="text-body" href="ticket.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>"><?= escapeHtml($case['ticket_subject']) ?></a></td>
<td class="text-nowrap small"><?php if ($their_ref) { ?><span class="text-body-secondary">Theirs</span> <span class="font-monospace"><?= $their_ref ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy their reference" data-clipboard-text="<?= $their_ref ?>"><i class="far fa-copy"></i></button><?php } ?></td>
<td class="text-nowrap"><?= ticketStatusHtml($case['ticket_status_name'], $case['ticket_status_color'], 'detail') ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php } ?>
<?php foreach (['licenses' => ['software', $licenses], 'logins' => ['credential', $logins]] as $pane => [$to, $rows]) {
if (!$rows) {
continue;
} ?>
<div class="tab-pane fade <?= $first_tab === $pane ? 'show active' : '' ?>" id="<?= $quick_id ?>-<?= $pane ?>" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($rows as $row) { ?>
<li class="itflow-link-row"><?= linkRowHtml($to, $row, $client_id) ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($assets) { ?>
<div class="tab-pane fade <?= $first_tab === 'assets' ? 'show active' : '' ?>" id="<?= $quick_id ?>-assets" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($assets as $asset) {
$warranty = $asset['asset_warranty_expire'] ? strtotime($asset['asset_warranty_expire']) : 0; ?>
<li class="itflow-link-row">
<?= linkRowHtml('asset', $asset, $client_id) ?>
<?php if ($warranty) { ?>
<span class="small text-nowrap <?= $warranty < time() ? 'text-danger' : 'text-body-secondary' ?>" title="Warranty"><?= $warranty < time() ? 'Out of warranty' : 'Warranty to ' . escapeHtml($asset['asset_warranty_expire']) ?></span>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
<!-- Notes Card -->
<div class="card mb-3 shadow-sm rounded">
<div class="card-body">
<h6 class="text-secondary"><i class="fas fa-sticky-note me-2"></i>Notes</h6>
<div>
<?= $notes ?: '-'; ?>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a href="<?= $page_url ?>" class="btn btn-primary text-bold"><i class="fas fa-arrow-right me-2"></i>Open vendor</a>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
<?php } ?>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
</div>
<script>
(function () {
// Notes save when you click away - this modal's own textarea
document.querySelectorAll('.modal .itflow-vendor-notes:not([data-wired])').forEach(function (notes) {
notes.dataset.wired = '1';
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
vendor_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
vendor_id: notes.dataset.vendorId,
notes: notes.value
});
});
});
// Open on the tab used last time, if this vendor has one
var key = 'itflow.vendorQuickTab';
document.querySelectorAll('[data-vendor-quick]:not([data-wired])').forEach(function (box) {
box.dataset.wired = '1';
var remembered = null;
try {
remembered = localStorage.getItem(key);
} catch (e) {}
var tab = remembered && box.querySelector('[data-vendor-quick-tab="' + remembered + '"]');
if (tab && !tab.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(tab).show();
}
box.addEventListener('shown.bs.tab', function (e) {
try {
localStorage.setItem(key, e.target.dataset.vendorQuickTab);
} catch (err) {}
});
});
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -1,149 +1,66 @@
<?php
/*
* New vendor - the client's it was opened from (?client_id=), one of ours
* (no client), or for a client picked here (?view=clients, the all-clients
* list). ?template_id= starts it from that template; ?from_template=1 (New
* from Template) opens on the template picker.
*/
require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
$vendor_pick_client = !$client_id && ($_GET['view'] ?? '') === 'clients';
if ($vendor_pick_client) {
enforceUserPermission('module_client', 2);
} else {
vendorEnforce($client_id, 2);
}
if ($client_id) {
enforceClientAccess();
$vendor_form_mode = 'add';
$vendor_id = 0;
$vendor_name = $vendor_description = $vendor_account_number = $vendor_code = '';
$vendor_contact_name = $vendor_contact_phone_country_code = $vendor_contact_phone = $vendor_contact_email = '';
$vendor_phone_country_code = $vendor_phone = $vendor_extension = $vendor_email = $vendor_website = '';
$vendor_hours = $vendor_sla = $vendor_notes = '';
$vendor_favorite = 0;
$vendor_template_id = 0;
$vendor_tag_ids = [];
$vendor_template_focus = !empty($_GET['from_template']);
// Started from a template - its fields, escaped like the rest
$template = vendorTemplateRow($_GET['template_id'] ?? 0);
if ($template) {
foreach (vendorTemplateFormFields($template) as $field => $value) {
if ($field === 'vendor_template_id') {
$vendor_template_id = intval($value);
} else {
${"vendor_$field"} = escapeHtml($value);
}
}
}
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>New Vendor</h5>
<h5 class="modal-title"><i class="fa fa-fw fa-building me-2"></i>New <?= $client_id || $vendor_pick_client ? '' : 'Company ' ?>Vendor</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'] ?>">
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php if (!$vendor_pick_client) { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } ?>
<div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#pills-support">Support</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes">Notes</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details">
<div class="mb-3">
<label>Vendor Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" required autofocus>
</div>
</div>
<div class="mb-3">
<label>Description</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>Account Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>Account Manager</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="contact_name" placeholder="Account manager's name" maxlength="200">
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-support">
<label>Support Phone / <span class="text-secondary">Extension</span></label>
<div class="row g-2">
<div class="col-9">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
<input type="hidden" name="phone_country_code">
<input type="tel" class="form-control" name="phone" placeholder="Phone Number" maxlength="200" data-itflow-phone="phone_country_code">
</div>
</div>
</div>
<div class="col-3">
<div class="mb-3">
<input type="text" class="form-control" name="extension" placeholder="ext." maxlength="200">
</div>
</div>
</div>
<div class="mb-3">
<label>Support Hours</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="text" class="form-control" name="hours" placeholder="Support Hours" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>Support Email</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>Support Website URL</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>Pin/Code</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200">
</div>
</div>
<div class="mb-3">
<label>SLA</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200">
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-notes">
<div class="mb-3">
<textarea class="form-control" rows="12" placeholder="Enter some notes" name="notes"></textarea>
</div>
</div>
</div>
<?php require 'vendor_form.php'; ?>
</div>
<div class="modal-footer">
<button type="submit" name="add_vendor" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Create</button>
<button type="submit" name="add_vendor" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</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';

View File

@@ -1,50 +1,10 @@
<?php
require_once '../../../includes/modal_header.php';
/*
* Kept so existing links keep working. New from Template is the New Vendor
* form opened on its template picker.
*/
$client_id = intval($_GET['client_id'] ?? 0);
$_GET['from_template'] = 1;
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>New Vendor from Template</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'] ?>">
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<div class="modal-body">
<label>Template</label>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
<select class="form-select" name="vendor_template_id" required>
<option value="">- Select Template -</option>
<?php
$sql_vendor_templates = mysqli_query($mysqli, "SELECT vendor_template_id, vendor_template_name FROM vendor_templates WHERE vendor_template_archived_at IS NULL ORDER BY vendor_template_name ASC");
while ($row = mysqli_fetch_assoc($sql_vendor_templates)) {
$vendor_template_id = intval($row['vendor_template_id']);
$vendor_template_name = escapeHtml($row['vendor_template_name']);
?>
<option value="<?= $vendor_template_id ?>"><?= $vendor_template_name ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_vendor_from_template" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Create Vendor</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';
require __DIR__ . '/vendor_add.php';

View File

@@ -2,201 +2,58 @@
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_client', 2);
$vendor_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT vendor_account_number, vendor_client_id, vendor_code, vendor_contact_name,
vendor_description, vendor_email, vendor_extension, vendor_hours, vendor_name,
vendor_notes, vendor_phone, vendor_phone_country_code, vendor_sla, vendor_template_id,
vendor_website FROM vendors WHERE vendor_id = $vendor_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeHtml($row['vendor_name']);
$vendor_description = escapeHtml($row['vendor_description']);
$vendor_account_number = escapeHtml($row['vendor_account_number']);
$vendor_contact_name = escapeHtml($row['vendor_contact_name']);
$vendor_phone_country_code = escapeHtml($row['vendor_phone_country_code']);
$vendor_phone = escapeHtml(formatPhoneNumber($row['vendor_phone'], $vendor_phone_country_code, false));
$vendor_extension = escapeHtml($row['vendor_extension']);
$vendor_email = escapeHtml($row['vendor_email']);
$vendor_website = escapeHtml($row['vendor_website']);
$vendor_hours = escapeHtml($row['vendor_hours']);
$vendor_sla = escapeHtml($row['vendor_sla']);
$vendor_code = escapeHtml($row['vendor_code']);
$vendor_notes = escapeHtml($row['vendor_notes']);
$vendor_template_id = intval($row['vendor_template_id']);
$client_id = intval($row['vendor_client_id']);
if ($client_id) {
enforceClientAccess();
$vendor_id = intval($_GET['id'] ?? 0);
$vendor = vendorRow($vendor_id);
if (!$vendor) {
echo json_encode(['error' => 'That vendor no longer exists.']);
exit;
}
$client_id = intval($vendor['vendor_client_id']);
vendorEnforce($client_id, 2);
$vendor_form_mode = 'edit';
$vendor_pick_client = false;
$vendor_name = escapeHtml($vendor['vendor_name']);
$vendor_description = escapeHtml($vendor['vendor_description']);
$vendor_account_number = escapeHtml($vendor['vendor_account_number']);
$vendor_code = escapeHtml($vendor['vendor_code']);
$vendor_contact_name = escapeHtml($vendor['vendor_contact_name']);
$vendor_contact_phone_country_code = escapeHtml($vendor['vendor_contact_phone_country_code']);
$vendor_contact_phone = escapeHtml(formatPhoneNumber($vendor['vendor_contact_phone'], $vendor['vendor_contact_phone_country_code'], false));
$vendor_contact_email = escapeHtml($vendor['vendor_contact_email']);
$vendor_phone_country_code = escapeHtml($vendor['vendor_phone_country_code']);
$vendor_phone = escapeHtml(formatPhoneNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code'], false));
$vendor_extension = escapeHtml($vendor['vendor_extension']);
$vendor_email = escapeHtml($vendor['vendor_email']);
$vendor_website = escapeHtml($vendor['vendor_website']);
$vendor_hours = escapeHtml($vendor['vendor_hours']);
$vendor_sla = escapeHtml($vendor['vendor_sla']);
$vendor_notes = escapeHtml($vendor['vendor_notes']);
$vendor_favorite = intval($vendor['vendor_favorite']);
$vendor_template_id = intval($vendor['vendor_template_id']);
$vendor_tag_ids = array_map(function ($tag) {
return intval($tag['tag_id']);
}, tagsForRecord('vendor', $vendor_id));
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>Editing vendor: <strong><?= $vendor_name ?></strong></h5>
<h5 class="modal-title text-truncate"><i class="fa fa-fw fa-building me-2"></i>Editing vendor: <strong><?= $vendor_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">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="vendor_id" value="<?= $vendor_id ?>">
<div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details<?= $vendor_id ?>">Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#pills-support<?= $vendor_id ?>">Support</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes<?= $vendor_id ?>">Notes</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-details<?= $vendor_id ?>">
<div class="mb-3">
<label>Vendor Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
<input type="text" class="form-control" name="name" placeholder="Vendor Name" maxlength="200" value="<?= "$vendor_name" ?>" required>
</div>
</div>
<div class="mb-3">
<label>Description</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
<input type="text" class="form-control" name="description" placeholder="Description" maxlength="200" value="<?= $vendor_description ?>">
</div>
</div>
<div class="mb-3">
<label>Account Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
<input type="text" class="form-control" name="account_number" placeholder="Account number" maxlength="200" value="<?= $vendor_account_number ?>">
</div>
</div>
<div class="mb-3">
<label>Account Manager</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="contact_name" maxlength="200" value="<?= $vendor_contact_name ?>" placeholder="Vendor contact name">
</div>
</div>
<div class="mb-3">
<label>Template Base</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
<select class="form-select select2" name="vendor_template_id">
<option value="0">- None -</option>
<?php
$sql_vendor_templates = mysqli_query($mysqli, "SELECT vendor_template_id, vendor_template_name FROM vendor_templates WHERE vendor_template_archived_at IS NULL ORDER BY vendor_template_name ASC");
while ($row = mysqli_fetch_assoc($sql_vendor_templates)) {
$vendor_template_id_select = $row['vendor_template_id'];
$vendor_template_name_select = escapeHtml($row['vendor_template_name']); ?>
<option <?php if ($vendor_template_id == $vendor_template_id_select) { echo "selected"; } ?> value="<?= $vendor_template_id_select ?>"><?= $vendor_template_name_select ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-support<?= $vendor_id ?>">
<label>Support Phone / <span class="text-secondary">Extension</span></label>
<div class="row g-2">
<div class="col-9">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
<input type="hidden" name="phone_country_code" value="<?= $vendor_phone_country_code ?>">
<input type="tel" class="form-control" name="phone" value="<?= $vendor_phone ?>" placeholder="Phone Number" maxlength="200" data-itflow-phone="phone_country_code">
</div>
</div>
</div>
<div class="col-3">
<div class="mb-3">
<input type="text" class="form-control" name="extension" value="<?= $vendor_extension ?>" placeholder="ext." maxlength="200">
</div>
</div>
</div>
<div class="mb-3">
<label>Support Hours</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="text" class="form-control" name="hours" placeholder="Support Hours" maxlength="200" value="<?= $vendor_hours ?>">
</div>
</div>
<div class="mb-3">
<label>Support Email</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
<input type="email" class="form-control" name="email" placeholder="Support Email" maxlength="200" value="<?= $vendor_email ?>">
</div>
</div>
<div class="mb-3">
<label>Support Website URL</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
<input type="text" class="form-control" name="website" placeholder="Do not include http(s)://" maxlength="200" value="<?= $vendor_website ?>">
</div>
</div>
<div class="mb-3">
<label>SLA</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
<input type="text" class="form-control" name="sla" placeholder="SLA Response Time" maxlength="200" value="<?= $vendor_sla ?>">
</div>
</div>
<div class="mb-3">
<label>Pin/Code</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="text" class="form-control" name="code" placeholder="Access Code or Pin" maxlength="200" value="<?= $vendor_code ?>">
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-notes<?= $vendor_id ?>">
<div class="mb-3">
<textarea class="form-control" rows="12" placeholder="Enter some notes" name="notes"><?= $vendor_notes ?></textarea>
</div>
<p class="text-muted text-end">Vendor ID: <?= $vendor_id ?></p>
</div>
</div>
<?php require 'vendor_form.php'; ?>
</div>
<div class="modal-footer">
<button type="submit" name="edit_vendor" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
<button type="submit" name="edit_vendor" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</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';

View File

@@ -2,18 +2,29 @@
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_client');
// Pre-filled from the vendors page's current filters. Everything here is editable -
// the export runs whatever this form posts, not whatever the page happened to show.
$client_id = intval($_GET['client_id'] ?? 0);
if ($client_id) {
enforceUserPermission('module_client');
enforceClientAccess();
}
// Ours, or every client's (?view=clients)
$view = !$client_id && ($_GET['view'] ?? '') === 'clients' ? 'clients' : '';
if ($view) {
enforceUserPermission('module_client');
}
// Search Filter
$q_filter = $_GET['q'] ?? '';
// Client Filter
$client_filter = intval($_GET['client'] ?? 0);
// Tags Filter
$tag_filter = (isset($_GET['tags']) && is_array($_GET['tags'])) ? array_map('intval', $_GET['tags']) : [];
// Archived Filter
$archived_filter = (isset($_GET['archived']) && $_GET['archived'] == 1) ? 1 : 0;
@@ -22,7 +33,7 @@ ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export Vendors</h5>
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export <?= $view ? 'Client Vendors' : 'Vendors' ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
@@ -31,6 +42,7 @@ ob_start();
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<input type="hidden" name="view" value="<?= $view ?>">
<div class="modal-body">
@@ -39,15 +51,50 @@ ob_start();
<div class="mb-3">
<label>Search</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Name, contact, account number">
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Name, account number, contact, phone">
</div>
</div>
<?php if ($view) { ?>
<div class="mb-3">
<label>Client</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<select class="form-select select2" name="client">
<option value="">- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients
WHERE EXISTS (SELECT 1 FROM vendors WHERE vendor_client_id = client_id) " . clientScopeSql('client_id') . "
ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$filter_client_id = intval($row['client_id']); ?>
<option <?php if ($client_filter == $filter_client_id) { echo "selected"; } ?> value="<?= $filter_client_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
<?php } ?>
</select>
</div>
</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="tags[]" data-placeholder="- All Tags -" multiple>
<?php
$sql_tags_filter = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 6 ORDER BY tag_name ASC");
while ($row = mysqli_fetch_assoc($sql_tags_filter)) {
$filter_tag_id = intval($row['tag_id']); ?>
<option <?php if (in_array($filter_tag_id, $tag_filter, true)) { echo "selected"; } ?> value="<?= $filter_tag_id ?>"<?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= escapeHtml($row['tag_name']) ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="mb-3">
<label>Archived</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-archive"></i></span>
<span class="input-group-text"><i class="fa fa-fw fa-archive"></i></span>
<select class="form-select select2" name="archived">
<option <?php if (!$archived_filter) { echo "selected"; } ?> value="0">Active only</option>
<option <?php if ($archived_filter) { echo "selected"; } ?> value="1">Archived only</option>

318
agent/modals/vendor/vendor_form.php vendored Normal file
View File

@@ -0,0 +1,318 @@
<?php
/*
* Vendor form fields - shared by vendor_add.php and vendor_edit.php, which set
* $vendor_form_mode ('add' | 'edit'), $client_id (the vendor's client, 0 for
* one of ours), $vendor_pick_client (pick the client here - the all-clients
* list), $vendor_id and the $vendor_* values (escaped, empty for a new vendor)
* plus $vendor_tag_ids, before including this. Posts what vendorFormSet()
* (functions/vendor.php) reads.
*
* Tabs: Details (name, star, what they are, tags, template), Support (the
* number to ring, hours, SLA, support email and site), Account (the account
* number and PIN they ask for, the account manager) and Notes. A new vendor
* can start from a template, which fills every field in.
*
* Should not be accessed directly.
*/
isset($mysqli) || die("Direct file access is not allowed");
$client_options = [];
if (!empty($vendor_pick_client)) {
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$client_options[] = $row;
}
}
// Templates, and for a new vendor what each one fills in
$template_options = [];
$template_fields = [];
$sql = mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_archived_at IS NULL OR vendor_template_id = " . intval($vendor_template_id) . " ORDER BY vendor_template_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$template_options[] = $row;
if ($vendor_form_mode === 'add') {
$template_fields[intval($row['vendor_template_id'])] = vendorTemplateFormFields($row);
}
}
$sql_tags_select = mysqli_query($mysqli, "SELECT tag_id, tag_name, tag_color, tag_icon FROM tags WHERE tag_type = 6 ORDER BY tag_name ASC");
$form_id = 'vendorForm' . ($vendor_form_mode === 'edit' ? $vendor_id : 'New');
?>
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#<?= $form_id ?>Details"><i class="fas fa-fw fa-building me-2 d-none d-lg-inline"></i>Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Support"><i class="fas fa-fw fa-headset me-2 d-none d-lg-inline"></i>Support</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Account"><i class="fas fa-fw fa-id-card me-2 d-none d-lg-inline"></i>Account</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Notes"><i class="fas fa-fw fa-sticky-note me-2 d-none d-lg-inline"></i>Notes</a>
</li>
</ul>
<div class="tab-content" id="<?= $form_id ?>">
<!-- Who they are -->
<div class="tab-pane fade show active" id="<?= $form_id ?>Details">
<div class="row g-3">
<?php if ($vendor_form_mode === 'add' && $template_options) { ?>
<div class="col-12">
<label class="form-label">Start from a template</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
<select class="form-select select2" name="vendor_template_id" data-vendor-template <?= !empty($vendor_template_focus) ? 'autofocus' : '' ?>>
<option value="0">- Blank vendor -</option>
<?php foreach ($template_options as $template_option) {
$template_option_id = intval($template_option['vendor_template_id']); ?>
<option value="<?= $template_option_id ?>" <?= intval($vendor_template_id) === $template_option_id ? 'selected' : '' ?><?= selectOptionVendor($template_option['vendor_template_name'], $template_option['vendor_template_description']) ?>><?= escapeHtml($template_option['vendor_template_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="form-text">Fills in their support line, hours and site - then add this client's account number and PIN.</div>
</div>
<?php } ?>
<?php if (!empty($vendor_pick_client)) { ?>
<div class="col-12">
<label class="form-label">Client <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<select class="form-select select2" name="client_id" required>
<option value="">- Select Client -</option>
<?php foreach ($client_options as $client_option) { ?>
<option value="<?= intval($client_option['client_id']) ?>"<?= selectOptionClient($client_option['client_id'], $client_option['client_name'], $client_option['client_abbreviation'] ?? '') ?>><?= escapeHtml($client_option['client_name']) ?></option>
<?php } ?>
</select>
</div>
</div>
<?php } ?>
<div class="col-md-8">
<label class="form-label">Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-fw fa-building"></i></span>
<input type="text" class="form-control" name="name" placeholder="Comcast Business, Dell, Meridian Dental" maxlength="200" value="<?= $vendor_name ?>" required <?= $vendor_form_mode === 'edit' || !empty($vendor_template_focus) ? '' : 'autofocus' ?>>
</div>
</div>
<div class="col-md-4">
<label class="form-label d-none d-md-block">&nbsp;</label>
<div class="form-check form-switch mt-md-2">
<input class="form-check-input" type="checkbox" role="switch" name="favorite" value="1" id="<?= $form_id ?>Favorite" <?= $vendor_favorite ? 'checked' : '' ?>>
<label class="form-check-label" for="<?= $form_id ?>Favorite"><i class="fas fa-star text-warning me-1"></i>Starred</label>
</div>
</div>
<div class="col-12">
<label class="form-label">What they are</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
<input type="text" class="form-control" name="description" placeholder="Primary internet circuit, practice management software" maxlength="200" value="<?= $vendor_description ?>">
</div>
</div>
<div class="col-12">
<label class="form-label">Tags</label>
<input type="hidden" name="tags_form" value="1">
<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="ISP, Telecom, Software, Hardware" multiple>
<?php while ($row = mysqli_fetch_assoc($sql_tags_select)) {
$tag_id_select = intval($row['tag_id']); ?>
<option value="<?= $tag_id_select ?>" <?= in_array($tag_id_select, $vendor_tag_ids, true) ? 'selected' : '' ?><?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= escapeHtml($row['tag_name']) ?></option>
<?php } ?>
</select>
<?php if (!empty($session_is_admin)) { ?>
<button class="btn btn-secondary ajax-modal" title="New tag" type="button" data-modal-url="../admin/modals/tag/tag_add.php?type=6">
<i class="fas fa-plus"></i>
</button>
<?php } ?>
</div>
</div>
<?php if ($vendor_form_mode === 'edit' && $template_options) { ?>
<div class="col-12">
<label class="form-label">Based on template</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
<select class="form-select select2" name="vendor_template_id">
<option value="0">- None -</option>
<?php foreach ($template_options as $template_option) {
$template_option_id = intval($template_option['vendor_template_id']); ?>
<option value="<?= $template_option_id ?>" <?= intval($vendor_template_id) === $template_option_id ? 'selected' : '' ?><?= selectOptionVendor($template_option['vendor_template_name'], $template_option['vendor_template_description']) ?>><?= escapeHtml($template_option['vendor_template_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="form-text">Template changes can be pushed to every vendor based on it, from Admin &rsaquo; Vendor Templates.</div>
</div>
<?php } elseif ($vendor_form_mode === 'edit') { ?>
<input type="hidden" name="vendor_template_id" value="<?= intval($vendor_template_id) ?>">
<?php } ?>
</div>
<?php if ($vendor_form_mode === 'edit') { ?>
<p class="text-body-secondary small text-end mt-3 mb-0">Vendor ID <span class="font-monospace"><?= $vendor_id ?></span></p>
<?php } ?>
</div>
<!-- How to reach support -->
<div class="tab-pane fade" id="<?= $form_id ?>Support">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Support phone</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
<input type="hidden" name="phone_country_code" value="<?= $vendor_phone_country_code ?>">
<input type="tel" class="form-control" name="phone" value="<?= $vendor_phone ?>" placeholder="Phone number" maxlength="200" data-itflow-phone="phone_country_code">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Extension / option</label>
<input type="text" class="form-control" name="extension" value="<?= $vendor_extension ?>" placeholder="ext." maxlength="200" inputmode="numeric">
</div>
<div class="col-md-6">
<label class="form-label">Support hours</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
<input type="text" class="form-control" name="hours" placeholder="24/7, Mon-Fri 8-6 ET" maxlength="200" value="<?= $vendor_hours ?>">
</div>
</div>
<div class="col-md-6">
<label class="form-label">SLA</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-handshake"></i></span>
<input type="text" class="form-control" name="sla" placeholder="4 hour response, next business day" maxlength="200" value="<?= $vendor_sla ?>">
</div>
</div>
<div class="col-12">
<label class="form-label">Support email</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
<input type="email" class="form-control" name="email" placeholder="support@vendor.com" maxlength="200" value="<?= $vendor_email ?>">
</div>
</div>
<div class="col-12">
<label class="form-label">Support site or portal</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
<input type="text" class="form-control" name="website" placeholder="support.vendor.com" maxlength="200" value="<?= $vendor_website ?>">
</div>
<div class="form-text">Link the portal login itself from the vendor's Logins tab.</div>
</div>
</div>
</div>
<!-- What they ask for, and who looks after us -->
<div class="tab-pane fade" id="<?= $form_id ?>Account">
<div class="row g-3">
<div class="col-md-7">
<label class="form-label">Account number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-fingerprint"></i></span>
<input type="text" class="form-control" name="account_number" placeholder="Account or customer number" maxlength="200" value="<?= $vendor_account_number ?>">
</div>
</div>
<div class="col-md-5">
<label class="form-label">PIN / passcode</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="text" class="form-control" name="code" placeholder="Verbal PIN or code" maxlength="200" value="<?= $vendor_code ?>" autocomplete="off">
</div>
</div>
<div class="col-12">
<div class="form-text mt-0">What support asks for before they will talk - shown beside the support line, the PIN as dots until it is asked for.</div>
</div>
<div class="col-12">
<label class="form-label">Account manager</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user-tie"></i></span>
<input type="text" class="form-control" name="contact_name" placeholder="Name" maxlength="200" value="<?= $vendor_contact_name ?>">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Their direct line</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-phone"></i></span>
<input type="hidden" name="contact_phone_country_code" value="<?= $vendor_contact_phone_country_code ?>">
<input type="tel" class="form-control" name="contact_phone" value="<?= $vendor_contact_phone ?>" placeholder="Phone number" maxlength="200" data-itflow-phone="contact_phone_country_code">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Their email</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
<input type="email" class="form-control" name="contact_email" placeholder="name@vendor.com" maxlength="200" value="<?= $vendor_contact_email ?>">
</div>
</div>
</div>
</div>
<!-- Everything else -->
<div class="tab-pane fade" id="<?= $form_id ?>Notes">
<label class="form-label">Notes</label>
<textarea class="form-control" rows="10" name="notes" placeholder="Escalation path, who is authorised to call them, circuit IDs, contract renewal, anything the next tech should know before they ring"><?= $vendor_notes ?></textarea>
<div class="form-text">Shown on the vendor's page and its quick look.</div>
</div>
</div>
<script>
itflowReady(function () {
var form = document.getElementById(<?= json_encode($form_id) ?>).closest('form');
// A required field on a tab that isn't showing: show that tab and point at it
var reported = false;
form.addEventListener('invalid', function (e) {
var pane = e.target.closest('.tab-pane');
var tab = pane && form.querySelector('[data-bs-toggle="pill"][href="#' + pane.id + '"]');
if (!tab || pane.classList.contains('active') || reported) {
return;
}
reported = true;
tab.addEventListener('shown.bs.tab', function () {
reported = false;
e.target.reportValidity();
}, { once: true });
bootstrap.Tab.getOrCreateInstance(tab).show();
}, true);
// Starting from a template fills in everything it has
var templates = <?= json_encode((object) $template_fields, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var picker = form.querySelector('[data-vendor-template]');
if (!picker) {
return;
}
function setField(name, value) {
var field = form.querySelector('[name="' + name + '"]');
if (field) {
field.value = value || '';
}
}
function setPhone(name, code, number) {
var input = form.querySelector('input[name="' + name + '"]');
var hidden = input ? form.querySelector('input[name="' + input.dataset.itflowPhone + '"]') : null;
if (!input) {
return;
}
var iti = window.intlTelInput && window.intlTelInput.getInstance ? window.intlTelInput.getInstance(input) : null;
var iso = code && typeof isoForDialCode === 'function' ? isoForDialCode(String(code), '') : '';
if (iti && iso) {
iti.setSelectedCountry(iso.toLowerCase());
}
if (hidden && code) {
hidden.value = String(code).replace(/[^0-9]/g, '');
}
input.value = number || '';
}
picker.addEventListener('change', function () {
var fields = templates[picker.value];
if (!fields) {
return;
}
['name', 'description', 'account_number', 'code', 'contact_name', 'email', 'website', 'hours', 'sla', 'notes', 'extension'].forEach(function (name) {
setField(name, fields[name]);
});
setPhone('phone', fields.phone_country_code, fields.phone);
});
});
</script>

View File

@@ -0,0 +1,114 @@
<?php
/*
* Add one vendor template to several clients at once - a standard vendor
* rolled out everywhere. Opened from the vendors lists and from Admin >
* Vendor Templates, so it posts to the agent handler by its full path. A client that already has a vendor made from the
* template is shown as having it and is left alone.
*/
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_client', 2);
$templates = [];
$sql = mysqli_query($mysqli, "SELECT vendor_template_id, vendor_template_name, vendor_template_description FROM vendor_templates WHERE vendor_template_archived_at IS NULL ORDER BY vendor_template_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$templates[] = $row;
}
if (!$templates) {
echo json_encode(['error' => 'There are no vendor templates yet - an admin adds them under Admin > Vendor Templates.']);
exit;
}
$template_id = intval($_GET['template_id'] ?? 0);
$template_ids = array_map(function ($t) { return intval($t['vendor_template_id']); }, $templates);
if (!in_array($template_id, $template_ids, true)) {
$template_id = $template_ids[0];
}
// Which clients already have a vendor from each template
$has = [];
$sql = mysqli_query($mysqli, "SELECT DISTINCT vendor_template_id, vendor_client_id FROM vendors WHERE vendor_template_id > 0 AND vendor_client_id > 0");
while ($row = mysqli_fetch_row($sql)) {
$has[intval($row[0])][intval($row[1])] = true;
}
$clients = [];
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$clients[] = $row;
}
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-puzzle-piece me-2"></i>Add a Vendor to Clients</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="/agent/post.php" method="post" autocomplete="off" data-vendor-template-clients>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Template</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
<select class="form-select select2" name="vendor_template_id" data-template-pick>
<?php foreach ($templates as $template) {
$option_id = intval($template['vendor_template_id']); ?>
<option value="<?= $option_id ?>" <?= $option_id === $template_id ? 'selected' : '' ?><?= selectOptionVendor($template['vendor_template_name'], $template['vendor_template_description']) ?>><?= escapeHtml($template['vendor_template_name']) ?></option>
<?php } ?>
</select>
</div>
</div>
<?php foreach ($templates as $template) {
$option_id = intval($template['vendor_template_id']);
$already = count(array_filter($clients, function ($client) use ($has, $option_id) {
return isset($has[$option_id][intval($client['client_id'])]);
})); ?>
<div data-template-clients="<?= $option_id ?>" <?= $option_id === $template_id ? '' : 'hidden' ?>>
<label class="form-label">Clients</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-users"></i></span>
<select class="form-select select2" name="client_ids[]" multiple data-placeholder="Pick the clients that use them" <?= $option_id === $template_id ? '' : 'disabled' ?>>
<?php foreach ($clients as $client) {
$client_option_id = intval($client['client_id']);
$client_has = isset($has[$option_id][$client_option_id]); ?>
<option value="<?= $client_option_id ?>" <?= $client_has ? 'disabled' : '' ?><?= selectOptionClient($client_option_id, $client['client_name'], $client['client_abbreviation'] ?? '') ?><?= $client_has ? selectOptionData(['description' => 'Already has it']) : '' ?>><?= escapeHtml($client['client_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="form-text"><?= $already ? "$already client" . ($already === 1 ? ' has' : 's have') . ' it already. ' : '' ?>Each gets its own copy - add their account number and PIN after.</div>
</div>
<?php } ?>
</div>
<div class="modal-footer">
<button type="submit" name="add_vendor_template_to_clients" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Add to clients</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>
itflowReady(function () {
var form = document.querySelector('form[data-vendor-template-clients]:not([data-wired])');
if (!form) {
return;
}
form.dataset.wired = '1';
var pick = form.querySelector('[data-template-pick]');
// Only the chosen template's client list posts
pick.addEventListener('change', function () {
form.querySelectorAll('[data-template-clients]').forEach(function (pane) {
var on = pane.dataset.templateClients === pick.value;
pane.hidden = !on;
pane.querySelector('select').disabled = !on;
});
});
});
</script>
<?php
require_once '../../../includes/modal_footer.php';

View File

@@ -22,6 +22,12 @@ if (isset($_POST['add_credential'])) {
$credential_id = mysqli_insert_id($mysqli);
// Made from a vendor's page - linked to that vendor, if it is the same client's
$vendor_id = assetClientOwnedId('vendor', $_POST['vendor_id'] ?? 0, $client_id);
if ($vendor_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO vendor_credentials SET vendor_id = $vendor_id, credential_id = $credential_id");
}
// Add Tags
if (isset($_POST['tags'])) {
foreach($_POST['tags'] as $tag) {

View File

@@ -52,7 +52,7 @@ if (isset($_POST['add_document'])) {
enforceClientAccess();
// Only the client's own folder, contact, asset, license and location
// Only the client's own folder, contact, asset, license, location and vendor
$folder = folderOwnedId($folder, $client_id);
$contact_id = assetClientOwnedId('contact', $_POST['contact'] ?? 0, $client_id);
$asset = linkRecord('asset', $_POST['asset'] ?? 0);
@@ -60,6 +60,7 @@ if (isset($_POST['add_document'])) {
$software = linkRecord('software', $_POST['software'] ?? 0);
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
$location_id = assetClientOwnedId('location', $_POST['location'] ?? 0, $client_id);
$vendor_id = assetClientOwnedId('vendor', $_POST['vendor'] ?? 0, $client_id);
$client_visible = intval($_POST['client_visible'] ?? 0) ? 1 : 0;
if ($name === '') {
@@ -93,13 +94,16 @@ if (isset($_POST['add_document'])) {
if ($location_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO location_documents SET location_id = $location_id, document_id = $document_id");
}
if ($vendor_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO vendor_documents SET vendor_id = $vendor_id, document_id = $document_id");
}
logAudit("Document", "Create", "$session_name created document $name", $client_id, $document_id);
flashAlert("Document <strong>" . trim($_POST['name']) . "</strong> created");
// From an asset, a contact, a license or a location, back to it; otherwise to the new document
redirect(($contact_id || $asset_id || $software_id || $location_id) ? null : "document.php?client_id=$client_id&document_id=$document_id");
// From an asset, a contact, a license, a location or a vendor, back to it; otherwise to the new document
redirect(($contact_id || $asset_id || $software_id || $location_id || $vendor_id) ? null : "document.php?client_id=$client_id&document_id=$document_id");
}

View File

@@ -28,7 +28,7 @@ if (isset($_POST['upload_files'])) {
enforceClientAccess();
// Only the client's own folder, contact, asset, license and location
// Only the client's own folder, contact, asset, license, location and vendor
$folder_id = folderOwnedId($_POST['folder_id'] ?? 0, $client_id);
$contact_id = assetClientOwnedId('contact', $_POST['contact_id'] ?? 0, $client_id);
$asset = linkRecord('asset', $_POST['asset_id'] ?? 0);
@@ -36,6 +36,7 @@ if (isset($_POST['upload_files'])) {
$software = linkRecord('software', $_POST['software_id'] ?? 0);
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
$location_id = assetClientOwnedId('location', $_POST['location_id'] ?? 0, $client_id);
$vendor_id = assetClientOwnedId('vendor', $_POST['vendor_id'] ?? 0, $client_id);
if (!is_dir($client_dir)) {
mkdir($client_dir, 0755, true);
@@ -106,6 +107,9 @@ if (isset($_POST['upload_files'])) {
if ($location_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO location_files SET location_id = $location_id, file_id = $file_id");
}
if ($vendor_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO vendor_files SET vendor_id = $vendor_id, file_id = $file_id");
}
logAudit("File", "Upload", "$session_name uploaded file $file_name", $client_id, $file_id);

View File

@@ -74,8 +74,9 @@ if (isset($_POST['add_ticket'])) {
//Generate a unique URL key for clients to access
$url_key = randomString(32);
// Only one of the ticket's own client's locations
// Only the ticket's own client's location and vendor
$location_id = assetClientOwnedId('location', $location_id, $client_id);
$vendor_id = assetClientOwnedId('vendor', $vendor_id, $client_id);
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Agent', ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = '$billable', ticket_status = '$ticket_status', ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_location_id = $location_id, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_due_at = $due, ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $project_id");
@@ -258,8 +259,9 @@ if (isset($_POST['edit_ticket'])) {
* priority and assign handlers record - so read the originals first and
* record the same changes when they come through here instead
*/
// Only one of the ticket's own client's locations
// Only the ticket's own client's location and vendor
$location_id = assetClientOwnedId('location', $location_id, $client_id);
$vendor_id = assetClientOwnedId('vendor', $vendor_id, $client_id);
$original_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_priority, ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id"));
$original_priority = escapeSql($original_row['ticket_priority']);
@@ -775,6 +777,9 @@ if (isset($_POST['edit_ticket_vendor'])) {
enforceClientAccess();
}
// Only the ticket's own client's vendors
$vendor_id = assetClientOwnedId('vendor', $vendor_id, $client_id);
mysqli_query($mysqli, "UPDATE tickets SET ticket_vendor_id = $vendor_id WHERE ticket_id = $ticket_id");
// The vendor modal now carries the vendor's own ticket number too. Only touched when the
@@ -785,22 +790,21 @@ if (isset($_POST['edit_ticket_vendor'])) {
}
// Get ticket / vendor details for logging
$sql = mysqli_query($mysqli, "SELECT vendor_name, ticket_prefix, ticket_number, ticket_status_name, ticket_client_id FROM vendors
LEFT JOIN tickets ON ticket_vendor_id = $vendor_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE ticket_id = $ticket_id"
);
$row = mysqli_fetch_assoc($sql);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_prefix, ticket_number, vendor_name FROM tickets
LEFT JOIN vendors ON vendor_id = ticket_vendor_id
WHERE ticket_id = $ticket_id"));
$ticket_prefix = escapeSql($row['ticket_prefix']);
$ticket_number = intval($row['ticket_number']);
$ticket_status_name = escapeSql($row['ticket_status_name']);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['ticket_client_id']);
$vendor_name = (string) $row['vendor_name'];
logAudit("Ticket", "Edit", "$session_name set vendor to $vendor_name for ticket $ticket_prefix$ticket_number", $client_id, $ticket_id);
flashAlert("Set vendor to <strong>$vendor_name</strong> for ticket <strong>$ticket_prefix$ticket_number</strong>");
if ($vendor_id) {
logAudit("Ticket", "Edit", "$session_name set vendor to " . escapeSql($vendor_name) . " for ticket $ticket_prefix$ticket_number", $client_id, $ticket_id);
flashAlert("Set vendor to <strong>$vendor_name</strong> for ticket <strong>$ticket_prefix$ticket_number</strong>");
} else {
logAudit("Ticket", "Edit", "$session_name removed the vendor from ticket $ticket_prefix$ticket_number", $client_id, $ticket_id);
flashAlert("Removed the vendor from ticket <strong>$ticket_prefix$ticket_number</strong>");
}
redirect();

View File

@@ -2,87 +2,70 @@
/*
* ITFlow - GET/POST request handler for vendors
*
* A client's vendors are the client module's; ours (no client) are the
* financial module's - vendorEnforce() (functions/vendor.php) checks the right
* one, and the client access, for the vendor a request names rather than for
* whatever the form says. Documents, files and portal logins are linked to a
* vendor through post/link.php; the notes save through ajax.php.
*/
// Todo: 2026-03-02 JQ - Need Permssions reworked as we have client vendors and Global Vendors basically check the referral url if it has client_id then Perm check client else perm check financial.
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_vendor_from_template'])) {
validateCSRFToken();
// GET POST Data
$client_id = intval($_POST['client_id']); //Used if this vendor is under a contact otherwise its 0 for under company and or template
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
/** The vendor a request names, once the agent may do this to it - or back with an error if it is gone. */
function vendorForRequest($vendor_id, $level) {
global $client_id;
$row = vendorRow($vendor_id);
if (!$row) {
flashAlert('That vendor no longer exists.', 'error');
redirect();
}
$vendor_template_id = intval($_POST['vendor_template_id']);
//GET Vendor Info
$sql_vendor_templates = mysqli_query($mysqli,"SELECT vendor_template_account_number, vendor_template_code, vendor_template_contact_name,
vendor_template_description, vendor_template_email, vendor_template_extension,
vendor_template_hours, vendor_template_name, vendor_template_notes, vendor_template_phone,
vendor_template_phone_country_code, vendor_template_sla, vendor_template_website FROM vendor_templates WHERE vendor_template_id = $vendor_template_id");
$row = mysqli_fetch_assoc($sql_vendor_templates);
$name = escapeSql($row['vendor_template_name']);
$description = escapeSql($row['vendor_template_description']);
$account_number = escapeSql($row['vendor_template_account_number']);
$contact_name = escapeSql($row['vendor_template_contact_name']);
$phone_country_code = preg_replace("/[^0-9]/", '',$row['vendor_template_phone_country_code']);
$phone = preg_replace("/[^0-9]/", '',$row['vendor_template_phone']);
$extension = preg_replace("/[^0-9]/", '',$row['vendor_template_extension']);
$email = escapeSql($row['vendor_template_email']);
$website = escapeSql($row['vendor_template_website']);
$hours = escapeSql($row['vendor_template_hours']);
$sla = escapeSql($row['vendor_template_sla']);
$code = escapeSql($row['vendor_template_code']);
$notes = escapeSql($row['vendor_template_notes']);
// Vendor add query
mysqli_query($mysqli,"INSERT INTO vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_client_id = $client_id, vendor_template_id = $vendor_template_id");
$vendor_id = mysqli_insert_id($mysqli);
logAudit("Vendor", "Create", "$session_name created vendor $name using a template", $client_id, $vendor_id);
flashAlert("Vendor <strong>$name</strong> created from template");
redirect();
$client_id = intval($row['vendor_client_id']);
vendorEnforce($client_id, $level);
return $row;
}
// Vendors
/** Back to the list a vendor was on - its own page is gone once it is deleted. */
function vendorRedirectAfterDelete($client_id) {
if (strpos($_SERVER['HTTP_REFERER'] ?? '', '/vendor.php') !== false) {
redirect($client_id ? "vendors.php?client_id=$client_id&archived=1" : 'vendors.php?archived=1');
}
redirect();
}
/** Insert a vendor from a form's fields for this client - the new id, or 0 without a name. */
function vendorCreate($fields, $client_id) {
global $mysqli;
$set = vendorFormSet($fields);
if ($set === null) {
return 0;
}
mysqli_query($mysqli, "INSERT INTO vendors SET $set, vendor_client_id = " . intval($client_id));
return mysqli_insert_id($mysqli);
}
if (isset($_POST['add_vendor'])) {
validateCSRFToken();
require_once 'vendor_model.php';
$client_id = intval($_POST['client_id']); // Used if this vendor is under a contact otherwise its 0 for under company
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
$client_id = intval($_POST['client_id'] ?? 0);
vendorEnforce($client_id, 2);
if ($client_id && !getFieldById('clients', $client_id, 'client_name')) {
flashAlert('That client no longer exists.', 'error');
redirect();
}
mysqli_query($mysqli,"INSERT INTO vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_client_id = $client_id");
$vendor_id = vendorCreate($_POST, $client_id);
if (!$vendor_id) {
flashAlert('A vendor needs a name.', 'error');
redirect();
}
$vendor_id = mysqli_insert_id($mysqli);
vendorSetTags($vendor_id, $_POST['tags'] ?? []);
logAudit("Vendor", "Create", "$session_name created vendor $name", $client_id, $vendor_id);
$name = trim($_POST['name']);
$template_note = intval($_POST['vendor_template_id'] ?? 0) ? ' from a template' : '';
logAudit("Vendor", "Create", "$session_name created vendor " . escapeSql($name) . $template_note, $client_id, $vendor_id);
flashAlert("Vendor <strong>$name</strong> created");
@@ -90,31 +73,109 @@ if (isset($_POST['add_vendor'])) {
}
// Kept for anything still posting the old New from Template form - the vendor form now does both
if (isset($_POST['add_vendor_from_template'])) {
validateCSRFToken();
$client_id = intval($_POST['client_id'] ?? 0);
vendorEnforce($client_id, 2);
$template = vendorTemplateRow($_POST['vendor_template_id'] ?? 0);
if (!$template) {
flashAlert('That template no longer exists.', 'error');
redirect();
}
$vendor_id = vendorCreate(vendorTemplateFormFields($template), $client_id);
logAudit("Vendor", "Create", "$session_name created vendor " . escapeSql($template['vendor_template_name']) . " from a template", $client_id, $vendor_id);
flashAlert("Vendor <strong>{$template['vendor_template_name']}</strong> created from template");
redirect();
}
/*
* One template, added as a vendor to several clients at once - a new standard
* vendor rolled out everywhere. A client that already has a vendor made from
* this template is left alone.
*/
if (isset($_POST['add_vendor_template_to_clients'])) {
validateCSRFToken();
enforceUserPermission('module_client', 2);
$template = vendorTemplateRow($_POST['vendor_template_id'] ?? 0);
if (!$template) {
flashAlert('That template no longer exists.', 'error');
redirect();
}
$template_id = intval($template['vendor_template_id']);
$fields = vendorTemplateFormFields($template);
$added = $already = 0;
foreach (array_unique(array_filter(array_map('intval', (array) ($_POST['client_ids'] ?? [])))) as $target_client_id) {
if (!hasClientAccess($target_client_id) || !getFieldById('clients', $target_client_id, 'client_name')) {
continue;
}
if (mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM vendors WHERE vendor_client_id = $target_client_id AND vendor_template_id = $template_id LIMIT 1"))) {
$already++;
continue;
}
$vendor_id = vendorCreate($fields, $target_client_id);
logAudit("Vendor", "Create", "$session_name created vendor " . escapeSql($template['vendor_template_name']) . " from a template", $target_client_id, $vendor_id);
$added++;
}
flashAlert("Added <strong>{$template['vendor_template_name']}</strong> to <strong>$added</strong> client" . ($added === 1 ? '' : 's') . ($already ? " - $already already had it" : ''));
redirect();
}
if (isset($_POST['edit_vendor'])) {
validateCSRFToken();
require_once 'vendor_model.php';
$row = vendorForRequest($_POST['vendor_id'] ?? 0, 2);
$vendor_id = intval($row['vendor_id']);
$vendor_id = intval($_POST['vendor_id']);
$vendor_template_id = intval($_POST['vendor_template_id']);
// Get Client ID
$client_id = intval(getFieldById('vendors', $vendor_id, 'vendor_client_id'));
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
$set = vendorFormSet($_POST);
if ($set === null) {
flashAlert('A vendor needs a name.', 'error');
redirect();
}
mysqli_query($mysqli,"UPDATE vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code',vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_template_id = $vendor_template_id WHERE vendor_id = $vendor_id");
mysqli_query($mysqli, "UPDATE vendors SET $set WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Edit", "$session_name edited vendor $name", $client_id, $vendor_id);
// Only a form that showed the tags changes them
if (isset($_POST['tags_form'])) {
vendorSetTags($vendor_id, $_POST['tags'] ?? []);
}
flashAlert("Vendor <strong>$name</strong> edited");
$name = trim($_POST['name']);
logAudit("Vendor", "Edit", "$session_name edited vendor " . escapeSql($name), $client_id, $vendor_id);
flashAlert("Vendor <strong>$name</strong> updated");
redirect();
}
if (isset($_GET['favorite_vendor']) || isset($_GET['unfavorite_vendor'])) {
validateCSRFToken();
$favorite = isset($_GET['favorite_vendor']) ? 1 : 0;
$row = vendorForRequest($favorite ? $_GET['favorite_vendor'] : $_GET['unfavorite_vendor'], 2);
$vendor_id = intval($row['vendor_id']);
mysqli_query($mysqli, "UPDATE vendors SET vendor_favorite = $favorite WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Edit", "$session_name " . ($favorite ? 'starred' : 'unstarred') . " vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
redirect();
@@ -124,57 +185,31 @@ if (isset($_GET['archive_vendor'])) {
validateCSRFToken();
$vendor_id = intval($_GET['archive_vendor']);
$row = vendorForRequest($_GET['archive_vendor'], 2);
$vendor_id = intval($row['vendor_id']);
//Get Vendor Name
$sql = mysqli_query($mysqli,"SELECT vendor_client_id, vendor_name FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NOW() WHERE vendor_id = $vendor_id");
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
}
logAudit("Vendor", "Archive", "$session_name archived vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
mysqli_query($mysqli,"UPDATE vendors SET vendor_archived_at = NOW() WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Archive", "$session_name archived vendor $vendor_name", $client_id, $vendor_id);
flashAlert("Vendor <strong>$vendor_name</strong> archived", 'error');
flashAlert("Vendor <strong>{$row['vendor_name']}</strong> archived", 'error');
redirect();
}
if(isset($_GET['restore_vendor'])){
if (isset($_GET['restore_vendor'])) {
validateCSRFToken();
$vendor_id = intval($_GET['restore_vendor']);
$row = vendorForRequest($_GET['restore_vendor'], 2);
$vendor_id = intval($row['vendor_id']);
// Get Name and Client ID for logging and alert message
$sql = mysqli_query($mysqli,"SELECT vendor_name, vendor_client_id FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NULL WHERE vendor_id = $vendor_id");
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
}
logAudit("Vendor", "Restore", "$session_name restored vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
mysqli_query($mysqli,"UPDATE vendors SET vendor_archived_at = NULL WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Restore", "$session_name restored vendor $vendor_name", $client_id, $vendor_id);
flashAlert("Vendor <strong>$vendor_name</strong> restored");
flashAlert("Vendor <strong>{$row['vendor_name']}</strong> restored");
redirect();
@@ -184,35 +219,24 @@ if (isset($_GET['delete_vendor'])) {
validateCSRFToken();
$vendor_id = intval($_GET['delete_vendor']);
$row = vendorForRequest($_GET['delete_vendor'], 3);
$vendor_id = intval($row['vendor_id']);
//Get Vendor Name
$sql = mysqli_query($mysqli,"SELECT vendor_client_id, vendor_name, vendor_template_id FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
$vendor_template_id = intval($row['vendor_template_id']);
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 3);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 3);
$blocker = vendorDeleteBlocker($vendor_id);
if ($blocker) {
flashAlert("<strong>{$row['vendor_name']}</strong> was not deleted - $blocker.", 'error');
redirect();
}
// If its a template reset all vendors based off this template to no template base
if ($vendor_template_id > 0) {
mysqli_query($mysqli,"UPDATE vendors SET vendor_template_id = 0 WHERE vendor_template_id = $vendor_template_id");
}
// What came from them stays, from no vendor; tags and links go with it
vendorClearReferences($vendor_id);
mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_id = $vendor_id");
mysqli_query($mysqli,"DELETE FROM vendors WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Delete", "$session_name deleted vendor " . escapeSql($row['vendor_name']), $client_id);
logAudit("Vendor", "Delete", "$session_name deleted vendor $vendor_name", $client_id);
flashAlert("Vendor <strong>{$row['vendor_name']}</strong> deleted", 'error');
flashAlert("Vendor <strong>$vendor_name</strong> deleted", 'error');
redirect();
vendorRedirectAfterDelete($client_id);
}
@@ -220,41 +244,21 @@ if (isset($_POST['bulk_archive_vendors'])) {
validateCSRFToken();
if (isset($_POST['vendor_ids'])) {
// Get Selected Count
$count = count($_POST['vendor_ids']);
// Cycle through array and archive each record
foreach ($_POST['vendor_ids'] as $vendor_id) {
$vendor_id = intval($vendor_id);
// Get Name and Client ID for logging and alert message
$sql = mysqli_query($mysqli,"SELECT vendor_name, vendor_client_id FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
}
mysqli_query($mysqli,"UPDATE vendors SET vendor_archived_at = NOW() WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Archive", "$session_name archived vendor $vendor_name", $client_id, $vendor_id);
$count = 0;
foreach (array_unique(array_map('intval', (array) ($_POST['vendor_ids'] ?? []))) as $vendor_id) {
$row = vendorRow($vendor_id);
if (!$row || $row['vendor_archived_at']) {
continue;
}
logAudit("Vendor", "Bulk Archive", "$session_name archived $count vendor(s)");
flashAlert("Archived <strong>$count</strong> vendor(s)", 'error');
$client_id = intval($row['vendor_client_id']);
vendorEnforce($client_id, 2);
mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NOW() WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Archive", "$session_name archived vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
$count++;
}
flashAlert("Archived <strong>$count</strong> vendor" . ($count === 1 ? '' : 's'), 'error');
redirect();
}
@@ -263,42 +267,21 @@ if (isset($_POST['bulk_restore_vendors'])) {
validateCSRFToken();
if (isset($_POST['vendor_ids'])) {
// Get Selected Count
$count = count($_POST['vendor_ids']);
// Cycle through array and unarchive each record
foreach ($_POST['vendor_ids'] as $vendor_id) {
$vendor_id = intval($vendor_id);
// Get Name and Client ID for logging and alert message
$sql = mysqli_query($mysqli,"SELECT vendor_name, vendor_client_id FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 2);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 2);
}
mysqli_query($mysqli,"UPDATE vendors SET vendor_archived_at = NULL WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Restore", "$session_name restored vendor $vendor_name", $client_id, $vendor_id);
$count = 0;
foreach (array_unique(array_map('intval', (array) ($_POST['vendor_ids'] ?? []))) as $vendor_id) {
$row = vendorRow($vendor_id);
if (!$row || !$row['vendor_archived_at']) {
continue;
}
logAudit("Vendor", "Bulk Restore", "$session_name restored $count vendor(s)");
flashAlert("Restored <strong>$count</strong> vendor(s)");
$client_id = intval($row['vendor_client_id']);
vendorEnforce($client_id, 2);
mysqli_query($mysqli, "UPDATE vendors SET vendor_archived_at = NULL WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Restore", "$session_name restored vendor " . escapeSql($row['vendor_name']), $client_id, $vendor_id);
$count++;
}
flashAlert("Restored <strong>$count</strong> vendor" . ($count === 1 ? '' : 's'));
redirect();
}
@@ -307,48 +290,27 @@ if (isset($_POST['bulk_delete_vendors'])) {
validateCSRFToken();
if (isset($_POST['vendor_ids'])) {
// Get Selected Count
$count = count($_POST['vendor_ids']);
// Cycle through array and delete each record
foreach ($_POST['vendor_ids'] as $vendor_id) {
$vendor_id = intval($vendor_id);
// Get Name and Client ID for logging and alert message
$sql = mysqli_query($mysqli,"SELECT vendor_name, vendor_client_id, vendor_template_id FROM vendors WHERE vendor_id = $vendor_id");
$row = mysqli_fetch_assoc($sql);
$vendor_name = escapeSql($row['vendor_name']);
$client_id = intval($row['vendor_client_id']);
$vendor_template_id = intval($row['vendor_template_id']);
// Permission check
if ($client_id) {
enforceUserPermission('module_client', 3);
enforceClientAccess();
} else {
enforceUserPermission('module_financial', 3);
}
// If its a template reset all vendors based off this template to no template base
if ($vendor_template_id > 0) {
mysqli_query($mysqli,"UPDATE vendors SET vendor_template_id = 0 WHERE vendor_template_id = $vendor_template_id");
}
mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id");
logAudit("Vendor", "Delete", "$session_name deleted vendor $vendor_name", $client_id);
$count = 0;
$kept = [];
foreach (array_unique(array_map('intval', (array) ($_POST['vendor_ids'] ?? []))) as $vendor_id) {
$row = vendorRow($vendor_id);
if (!$row) {
continue;
}
logAudit("Vendor", "Bulk Delete", "$session_name deleted $count vendor(s)");
flashAlert("Deleted <strong>$count</strong> vendor(s)", 'error');
$client_id = intval($row['vendor_client_id']);
vendorEnforce($client_id, 3);
if (vendorDeleteBlocker($vendor_id)) {
$kept[] = $row['vendor_name'];
continue;
}
vendorClearReferences($vendor_id);
mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_id = $vendor_id");
logAudit("Vendor", "Delete", "$session_name deleted vendor " . escapeSql($row['vendor_name']), $client_id);
$count++;
}
flashAlert("Deleted <strong>$count</strong> vendor" . ($count === 1 ? '' : 's') . ($kept ? ' - kept <strong>' . implode(', ', $kept) . '</strong>, which have expenses booked to them' : ''), 'error');
redirect();
}
@@ -357,9 +319,6 @@ if (isExportRequest('export_vendors')) {
validateCSRFToken();
// Exports are reads - see CONTRIBUTING.md
enforceUserPermission('module_client');
$format = resolveExportFormat($_POST['export_vendors']);
// Filters inherited from the vendors page - mirrors agent/vendors.php
@@ -370,23 +329,50 @@ if (isExportRequest('export_vendors')) {
if ($archived) {
$filter_summary['Archived'] = 'Archived only';
}
$archive_query = $archived ? "vendor_archived_at IS NOT NULL" : "vendor_archived_at IS NULL";
if (!empty($_POST['client_id'])) {
// One client's vendors
$client_id = intval($_POST['client_id']);
enforceUserPermission('module_client');
enforceClientAccess();
$client_query = "AND vendor_client_id = $client_id";
$client_name = getFieldById('clients', $client_id, 'client_name');
$file_name_prepend = "$client_name-";
$filter_summary['Client'] = $client_name;
enforceClientAccess();
} else {
// Global vendors only, same as the vendors page
$client_query = "AND vendor_client_id = 0";
} elseif (($_POST['view'] ?? '') === 'clients') {
// Every client's vendors this agent may see
enforceUserPermission('module_client');
$client_id = 0; // for Logging
$client_query = "AND vendor_client_id > 0";
$file_name_prepend = "$session_company_name-Client-";
$filter_summary['Vendors'] = 'Client vendors';
if (!empty($_POST['client'])) {
$filter_client_id = intval($_POST['client']);
$client_query = "AND vendor_client_id = $filter_client_id";
$filter_summary['Client'] = getFieldById('clients', $filter_client_id, 'client_name');
}
$archive_query = $archived ? "(client_archived_at IS NOT NULL OR vendor_archived_at IS NOT NULL)" : "(client_archived_at IS NULL AND vendor_archived_at IS NULL)";
} else {
// Ours, same as the vendors page
$client_id = 0; // for Logging
$client_query = "AND vendor_client_id = 0";
$file_name_prepend = "$session_company_name-";
}
$archive_query = $archived ? "vendor_archived_at IS NOT NULL" : "vendor_archived_at IS NULL";
// Tags Filter - a vendor 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 vendors.vendor_id IN (SELECT vendor_id FROM vendor_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'] ?? '');
@@ -396,13 +382,18 @@ if (isExportRequest('export_vendors')) {
$sql = mysqli_query(
$mysqli,
"SELECT * FROM vendors
"SELECT vendors.*, client_name,
(SELECT GROUP_CONCAT(tag_name ORDER BY tag_name SEPARATOR ', ') FROM vendor_tags JOIN tags ON tags.tag_id = vendor_tags.tag_id WHERE vendor_tags.vendor_id = vendors.vendor_id) AS vendor_tags,
(SELECT COUNT(*) FROM tickets WHERE ticket_client_id = vendor_client_id AND ticket_vendor_id = vendors.vendor_id AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) AS open_case_count
FROM vendors
LEFT JOIN clients ON client_id = vendor_client_id
WHERE $archive_query
AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%' OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_phone LIKE '%$q%')
AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%'
OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_contact_email LIKE '%$q%' OR vendor_phone LIKE '%$q%' OR vendor_notes LIKE '%$q%')
$client_query
$tag_query
" . clientScopeSql('vendor_client_id') . "
ORDER BY vendor_name ASC"
ORDER BY client_name ASC, vendor_name ASC"
);
$num_rows = mysqli_num_rows($sql);

View File

@@ -1,16 +0,0 @@
<?php
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
$name = escapeSql($_POST['name']);
$description = escapeSql($_POST['description']);
$account_number = escapeSql($_POST['account_number']);
$contact_name = escapeSql($_POST['contact_name']);
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['phone_country_code']);
$phone = preg_replace("/[^0-9]/", '', $_POST['phone']);
$extension = preg_replace("/[^0-9]/", '', $_POST['extension']);
$email = escapeSql($_POST['email']);
$website = preg_replace("(^https?://)", "", escapeSql($_POST['website']));
$hours = escapeSql($_POST['hours']);
$sla = escapeSql($_POST['sla']);
$code = escapeSql($_POST['code']);
$notes = escapeSql($_POST['notes']);

View File

@@ -175,7 +175,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?= $category_name ?>
<div class="text-secondary"><small><?= truncate($recurring_expense_description, 60) ?></small></div>
</td>
<td><?= $vendor_name ?></td>
<td><?= $recurring_expense_vendor_id ? vendorLink($recurring_expense_vendor_id, $vendor_name, ['page' => true]) : '-' ?></td>
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $recurring_expense_amount, $recurring_expense_currency_code) ?></td>
<td><?= $recurring_expense_frequency_display ?></td>
<td><?= $recurring_expense_last_sent_display ?></td>

View File

@@ -463,7 +463,7 @@ while ($license_type_row = mysqli_fetch_row($sql_license_types)) {
<?php } ?>
<td>
<?php if ($vendor_name) { ?>
<a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
<?= vendorLink($vendor_id, $row['vendor_name']) ?>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>

View File

@@ -1452,28 +1452,38 @@ if (isset($_GET['ticket_id'])) {
<?php } ?>
<?php if ($vendor_id) {
// Stored websites may or may not carry a scheme - only add one when missing
$vendor_website_href = preg_match('#^https?://#i', $ticket['vendor_website']) ? $vendor_website : "https://$vendor_website";
// Everything for the call to them: the line, their reference, and what they will ask for
$vendor_phone_link = contactPhoneLink($ticket['vendor_phone'], $ticket['vendor_phone_country_code'], $ticket['vendor_extension']);
$vendor_account = escapeHtml($ticket['vendor_account_number']);
$vendor_pin = vendorSecretHtml($ticket['vendor_code']);
$vendor_website_url = vendorWebsiteUrl($ticket['vendor_website']);
$vendor_when = escapeHtml(implode(' · ', array_filter([$ticket['vendor_hours'], $ticket['vendor_sla']])));
?>
<div class="ticket-link-heading <?= $asset_id ? 'mt-3 pt-2 border-top' : '' ?>">Vendor</div>
<div class="mt-1 text-truncate">
<i class="fa fa-fw fa-building text-secondary me-2"></i><?= $vendor_name ?>
<div class="mt-1 d-flex min-w-0">
<?= vendorLink($vendor_id, $ticket['vendor_name'], ['archived' => !empty($ticket['vendor_archived_at'])]) ?>
</div>
<?php if ($ticket_vendor_ticket_number) { ?>
<!-- Labelled - a bare reference beside a tag icon read as nothing in particular -->
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-hashtag text-secondary me-2"></i><span class="text-muted">Their ticket</span> <?= $ticket_vendor_ticket_number ?></div>
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-hashtag text-secondary me-2"></i><span class="text-muted">Their ticket</span> <span class="font-monospace"><?= $ticket_vendor_ticket_number ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy their ticket number" data-clipboard-text="<?= $ticket_vendor_ticket_number ?>"><i class="far fa-copy"></i></button></div>
<?php } ?>
<?php if ($vendor_contact_name) { ?>
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-user text-secondary me-2"></i><?= $vendor_contact_name ?></div>
<?php if ($vendor_phone_link) { ?>
<div class="mt-1"><i class="fa fa-fw fa-phone text-secondary me-2"></i><?= $vendor_phone_link ?></div>
<?php } ?>
<?php if ($vendor_phone) { ?>
<div class="mt-1"><i class="fa fa-fw fa-phone text-secondary me-2"></i><a href="tel:<?= $vendor_phone ?>"><?= $vendor_phone ?></a></div>
<?php if ($vendor_when) { ?>
<div class="mt-1 small text-muted text-truncate" title="<?= $vendor_when ?>"><i class="fa fa-fw fa-clock text-secondary me-2"></i><?= $vendor_when ?></div>
<?php } ?>
<?php if ($vendor_account || $vendor_pin) { ?>
<div class="mt-1 small" title="What they will ask for">
<i class="fa fa-fw fa-fingerprint text-secondary me-2"></i><?php if ($vendor_account) { ?><span class="font-monospace"><?= $vendor_account ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="<?= $vendor_account ?>"><i class="far fa-copy"></i></button><?php } ?>
<?php if ($vendor_pin) { ?><span class="text-muted ms-1">PIN</span> <?= $vendor_pin ?><?php } ?>
</div>
<?php } ?>
<?php if ($vendor_email) { ?>
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-envelope text-secondary me-2"></i><a href="mailto:<?= $vendor_email ?>" title="<?= $vendor_email ?>"><?= $vendor_email ?></a></div>
<?php } ?>
<?php if ($vendor_website) { ?>
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-globe text-secondary me-2"></i><a href="<?= $vendor_website_href ?>" target="_blank" rel="noopener noreferrer" title="<?= $vendor_website ?>"><?= $vendor_website ?></a></div>
<?php if ($vendor_website_url) { ?>
<div class="mt-1 text-truncate"><i class="fa fa-fw fa-globe text-secondary me-2"></i><a href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener noreferrer" title="<?= $vendor_website ?>"><?= escapeHtml(vendorWebsiteLabel($ticket['vendor_website'])) ?></a></div>
<?php } ?>
<?php } ?>

822
agent/vendor.php Normal file
View File

@@ -0,0 +1,822 @@
<?php
/*
* Vendor - one of a client's vendors, or one of ours.
*
* Built for the call to them: the support line, hours and SLA, the account
* number and PIN they will ask for (the PIN as dots until it is wanted), the
* account manager, and the notes - escalation path, who may call - always in
* view. Then, for a client's vendor, what is open with them and what came
* from them in tabs with counts: cases (tickets with their reference, open
* first), licenses, assets bought from them, domains they host, services,
* portal logins, documents, files and history. For one of ours: what we spend
* with them and the recurring expenses. "Also used by" lists the agent's
* other clients with the same vendor - who else an outage hits.
*
* The open tab follows the URL hash (#cases) and is remembered between visits.
*/
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
} else {
require_once "includes/inc_all.php";
}
$vendor_id = intval($_GET['vendor_id'] ?? 0);
$vendor = vendorRow($vendor_id);
$vendor_client_id = $vendor ? intval($vendor['vendor_client_id']) : 0;
if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($client_id)) || !vendorCan($vendor_client_id, 1)) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
} else {
$client_id = $vendor_client_id;
if ($client_id) {
enforceUserPermission('module_client');
enforceClientAccess();
}
$is_ours = !$client_id;
$vendor_name = escapeHtml($vendor['vendor_name']);
$vendor_description = escapeHtml($vendor['vendor_description']);
$vendor_archived_at = escapeHtml($vendor['vendor_archived_at']);
$vendor_created_at = escapeHtml($vendor['vendor_created_at']);
$vendor_favorite = intval($vendor['vendor_favorite']);
$vendor_phone = contactPhoneLink($vendor['vendor_phone'], $vendor['vendor_phone_country_code'], $vendor['vendor_extension']);
$vendor_phone_text = $vendor['vendor_phone'] ? formatPhoneNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']) : '';
$vendor_dial = vendorDialNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']);
$vendor_email = escapeHtml($vendor['vendor_email']);
$vendor_website_url = vendorWebsiteUrl($vendor['vendor_website']);
$vendor_website_label = escapeHtml(vendorWebsiteLabel($vendor['vendor_website']));
$vendor_hours = escapeHtml($vendor['vendor_hours']);
$vendor_sla = escapeHtml($vendor['vendor_sla']);
$vendor_account = escapeHtml($vendor['vendor_account_number']);
$vendor_pin = vendorSecretHtml($vendor['vendor_code']);
$vendor_notes = escapeHtml($vendor['vendor_notes']);
$manager_name = escapeHtml($vendor['vendor_contact_name']);
$manager_phone = contactPhoneLink($vendor['vendor_contact_phone'], $vendor['vendor_contact_phone_country_code']);
$manager_email = escapeHtml($vendor['vendor_contact_email']);
$can_edit = vendorCan($client_id, 2);
$can_delete = vendorCan($client_id, 3) && !empty($config_destructive_deletes_enable);
$can_support = lookupUserPermission('module_support') >= 1;
$can_support_edit = lookupUserPermission('module_support') >= 2;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$can_credentials_edit = lookupUserPermission('module_credential') >= 2;
$can_financial = lookupUserPermission('module_financial') >= 1;
$can_financial_edit = lookupUserPermission('module_financial') >= 2;
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
$page_title = $vendor['vendor_name'];
$list_url = $is_ours ? 'vendors.php' : "vendors.php?client_id=$client_id";
$vendor_tags_display = tagPills('vendor', $vendor_id, tagsForRecord('vendor', $vendor_id), ($is_ours ? 'vendors.php?' : "vendors.php?client_id=$client_id&"), $can_edit);
// Cases - every open one, then the most recent closed ones
$cases = [];
$case_count = $case_open_count = 0;
$licenses = $assets = $domains = $services = $logins = $documents = $files = [];
if ($can_support) {
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(ticket_resolved_at IS NULL AND ticket_closed_at IS NULL), 0) FROM tickets WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id"));
[$case_count, $case_open_count] = [intval($row[0]), intval($row[1])];
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_vendor_ticket_number, ticket_created_at, ticket_updated_at,
ticket_resolved_at, ticket_closed_at, ticket_assigned_to, user_name, user_avatar, ticket_status_name, ticket_status_color
FROM tickets
LEFT JOIN users ON ticket_assigned_to = user_id
LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status
WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id
ORDER BY (ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) DESC, ticket_number DESC
LIMIT " . max(25, $case_open_count));
while ($row = mysqli_fetch_assoc($sql)) {
$cases[] = $row;
}
if (!$is_ours) {
$sql = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type,
software_seats, software_expire, software_purchase_reference, " . softwareSeatsUsedSql('software.software_id') . " AS seats_used
FROM software
WHERE software_client_id = $client_id AND software_vendor_id = $vendor_id AND software_archived_at IS NULL
ORDER BY software_expire IS NULL, software_expire ASC, software_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$licenses[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model, asset_serial, asset_purchase_date, asset_purchase_reference, asset_warranty_expire
FROM assets
WHERE asset_client_id = $client_id AND asset_vendor_id = $vendor_id AND asset_archived_at IS NULL
ORDER BY asset_warranty_expire IS NULL, asset_warranty_expire ASC, asset_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$assets[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT domain_id, domain_name, domain_description, domain_expire, domain_registrar, domain_webhost, domain_dnshost, domain_mailhost
FROM domains
WHERE domain_client_id = $client_id AND domain_archived_at IS NULL
AND $vendor_id IN (domain_registrar, domain_webhost, domain_dnshost, domain_mailhost)
ORDER BY domain_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$domains[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT services.service_id, service_name, service_description, service_category, service_importance
FROM service_vendors JOIN services ON services.service_id = service_vendors.service_id
WHERE service_vendors.vendor_id = $vendor_id AND service_client_id = $client_id
ORDER BY service_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$services[] = $row;
}
$documents = linkedRecords('vendor', $vendor_id, 'document');
$files = linkedRecords('vendor', $vendor_id, 'file');
}
}
if ($can_credentials && !$is_ours) {
$logins = linkedRecords('vendor', $vendor_id, 'credential');
}
// What we spend with one of ours
$expenses = $recurring = [];
$spend = null;
if ($is_ours && $can_financial) {
$spend = vendorSpend($vendor_id);
$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_currency_code, expense_description, expense_reference, category_name, account_name
FROM expenses
LEFT JOIN categories ON category_id = expense_category_id
LEFT JOIN accounts ON account_id = expense_account_id
WHERE expense_vendor_id = $vendor_id AND expense_archived_at IS NULL
ORDER BY expense_date DESC, expense_id DESC
LIMIT 25");
while ($row = mysqli_fetch_assoc($sql)) {
$expenses[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT recurring_expense_id, recurring_expense_frequency, recurring_expense_next_date, recurring_expense_status, recurring_expense_amount,
recurring_expense_currency_code, recurring_expense_description, category_name
FROM recurring_expenses
LEFT JOIN categories ON category_id = recurring_expense_category_id
WHERE recurring_expense_vendor_id = $vendor_id AND recurring_expense_archived_at IS NULL
ORDER BY recurring_expense_status DESC, recurring_expense_next_date ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$recurring[] = $row;
}
}
$elsewhere = lookupUserPermission('module_client') >= 1 ? vendorElsewhere($vendor) : [];
$history = vendorHistory($vendor_id, 100);
if ($is_ours) {
$vendor_tabs = [
'expenses' => ['label' => 'Expenses', 'icon' => 'fa-receipt', 'count' => $spend ? $spend['count'] : 0],
'recurring' => ['label' => 'Recurring', 'icon' => 'fa-redo-alt', 'count' => count($recurring)],
'cases' => ['label' => 'Cases', 'icon' => 'fa-life-ring', 'count' => $case_count, 'badge' => $case_open_count],
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => count($history)],
];
if (!$can_financial) {
unset($vendor_tabs['expenses'], $vendor_tabs['recurring']);
}
if (!$case_count) {
unset($vendor_tabs['cases']);
}
} else {
$vendor_tabs = [
'cases' => ['label' => 'Cases', 'icon' => 'fa-life-ring', 'count' => $case_count, 'badge' => $case_open_count],
'licenses' => ['label' => 'Licenses', 'icon' => 'fa-cube', 'count' => count($licenses)],
'assets' => ['label' => 'Assets', 'icon' => 'fa-desktop', 'count' => count($assets)],
'domains' => ['label' => 'Domains', 'icon' => 'fa-globe', 'count' => count($domains)],
'services' => ['label' => 'Services', 'icon' => 'fa-stream', 'count' => count($services)],
'logins' => ['label' => 'Logins', 'icon' => 'fa-key', 'count' => count($logins)],
'documents' => ['label' => 'Documents', 'icon' => 'fa-file-alt', 'count' => count($documents)],
'files' => ['label' => 'Files', 'icon' => 'fa-paperclip', 'count' => count($files)],
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => count($history)],
];
if (!$can_support) {
unset($vendor_tabs['cases'], $vendor_tabs['licenses'], $vendor_tabs['assets'], $vendor_tabs['domains'], $vendor_tabs['services'], $vendor_tabs['documents'], $vendor_tabs['files']);
}
if (!$can_credentials) {
unset($vendor_tabs['logins']);
}
}
$link_url = "modals/link/link.php?from=vendor&id=$vendor_id&to=";
$unlink_url = function ($to, $other_id) use ($vendor_id) {
return "post.php?unlink_item&from=vendor&id=$vendor_id&to=$to&other=" . intval($other_id) . "&csrf_token=" . $_SESSION['csrf_token'];
};
$new_case_url = "modals/ticket/ticket_add.php?client_id=$client_id&vendor_id=$vendor_id";
$can_new = !$vendor_archived_at && (($is_ours && $can_financial_edit) || (!$is_ours && ($can_support_edit || $can_credentials_edit)));
?>
<ol class="breadcrumb d-print-none">
<?php if ($is_ours) { ?>
<li class="breadcrumb-item"><a href="vendors.php">Vendors</a></li>
<?php } else { ?>
<li class="breadcrumb-item"><a href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($vendor['client_name']) ?></a></li>
<li class="breadcrumb-item"><a href="vendors.php?client_id=<?= $client_id ?>">Vendors</a></li>
<?php } ?>
<li class="breadcrumb-item active"><?= $vendor_name ?></li>
</ol>
<!-- Who they are and how to reach them -->
<div class="card mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?= vendorTile($vendor['vendor_name'], 'lg') ?>
<div class="flex-grow-1 min-w-0">
<h1 class="h4 fw-semibold mb-1 d-flex flex-wrap align-items-center gap-2">
<span><?= $vendor_name ?></span>
<?php if ($can_edit && !$vendor_archived_at) { ?>
<a class="text-decoration-none fs-6" href="post.php?<?= $vendor_favorite ? 'unfavorite_vendor' : 'favorite_vendor' ?>=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" title="<?= $vendor_favorite ? 'Unstar' : 'Star - starred vendors come first' ?>">
<i class="<?= $vendor_favorite ? 'fas text-warning' : 'far text-body-secondary' ?> fa-star"></i>
</a>
<?php } elseif ($vendor_favorite) { ?>
<i class="fas fa-star text-warning fs-6" title="Starred"></i>
<?php } ?>
<?php if ($is_ours) { ?><span class="badge rounded-pill text-bg-dark fw-normal fs-6">Our vendor</span><?php } ?>
<?php if ($vendor_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
</h1>
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
<?php if ($vendor_description) { ?><span><?= $vendor_description ?></span><?php } ?>
<?php if ($vendor_hours) { ?><span><i class="fas fa-fw fa-clock me-1"></i><?= $vendor_hours ?></span><?php } ?>
<?php if ($vendor['vendor_template_name']) { ?><span title="Made from this template"><i class="fas fa-fw fa-puzzle-piece me-1"></i><?= escapeHtml($vendor['vendor_template_name']) ?></span><?php } ?>
</div>
<?php if ($vendor_tags_display) { ?>
<div class="mt-2"><?= $vendor_tags_display ?></div>
<?php } ?>
</div>
<div class="d-flex flex-wrap gap-2 d-print-none">
<?php if ($vendor_dial) { ?>
<a class="btn btn-outline-primary" href="tel:<?= escapeHtml($vendor_dial) ?><?= $vendor['vendor_extension'] ? ',' . escapeHtml($vendor['vendor_extension']) : '' ?>" title="<?= escapeHtml($vendor_phone_text) ?>"><i class="fas fa-phone me-2"></i>Call support</a>
<?php } ?>
<?php if ($vendor_email) { ?>
<a class="btn btn-outline-primary" href="mailto:<?= $vendor_email ?>"><i class="fas fa-envelope me-2"></i>Email</a>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<a class="btn btn-outline-primary" href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><i class="fas fa-external-link-alt me-2"></i>Support site</a>
<?php } ?>
<?php if ($can_new) { ?>
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-plus me-2"></i>New</button>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($is_ours) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/expense/expense_add.php?vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-receipt text-secondary me-2"></i>Expense
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_add.php?vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-redo-alt text-secondary me-2"></i>Recurring expense
</a>
<?php } else { ?>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= $new_case_url ?>">
<i class="fa fa-fw fa-life-ring text-secondary me-2"></i>Case with them
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License from them
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-desktop text-secondary me-2"></i>Asset bought from them
</a>
<?php } ?>
<?php if ($can_credentials_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/credential/credential_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Portal login
</a>
<?php } ?>
<?php if ($can_support_edit) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="xl" data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-file-alt text-secondary me-2"></i>Document
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/file/file_upload.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
</a>
<?php } ?>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>">
<i class="fas fa-edit me-2"></i>Edit
</button>
<div class="dropdown dropstart">
<button class="btn btn-outline-secondary" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<?php if ($vendor_archived_at) { ?>
<a class="dropdown-item" href="post.php?restore_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= $vendor_name ?> goes for good. Its cases, licenses, assets and domains stay, from no vendor.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
<div class="row g-3">
<!-- Calling them, what they will ask for, who looks after us -->
<div class="col-xl-4 col-lg-5">
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-headset text-secondary me-2"></i>Support</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Phone</dt>
<dd>
<?php if ($vendor_phone) { ?>
<?= $vendor_phone ?><button type="button" class="itflow-copy clipboardjs" title="Copy phone number" data-clipboard-text="<?= escapeHtml($vendor_phone_text) ?>"><i class="far fa-copy"></i></button>
<?php } else { ?>
<span class="text-body-secondary">Not recorded</span>
<?php } ?>
</dd>
<dt>Hours</dt>
<dd><?= $vendor_hours ?: '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<?php if ($vendor_sla) { ?>
<dt>SLA</dt><dd><?= $vendor_sla ?></dd>
<?php } ?>
<?php if ($vendor_email) { ?>
<dt>Email</dt>
<dd class="text-break"><a href="mailto:<?= $vendor_email ?>"><?= $vendor_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $vendor_email ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<dt>Site</dt>
<dd class="text-break"><a href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><?= $vendor_website_label ?><i class="fas fa-external-link-alt ms-1 small"></i></a></dd>
<?php } ?>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-id-card text-secondary me-2"></i>Account</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Account</dt>
<dd><?= $vendor_account ? '<span class="font-monospace">' . $vendor_account . '</span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="' . $vendor_account . '"><i class="far fa-copy"></i></button>' : '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<dt>PIN</dt>
<dd><?= $vendor_pin ?: '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<dt>Manager</dt>
<dd>
<?php if ($manager_name || $manager_phone || $manager_email) { ?>
<?php if ($manager_name) { ?><div class="fw-semibold"><?= $manager_name ?></div><?php } ?>
<?php if ($manager_phone) { ?><div class="d-flex align-items-baseline"><i class="fas fa-fw fa-phone text-body-secondary me-1 flex-shrink-0"></i><span><?= $manager_phone ?></span></div><?php } ?>
<?php if ($manager_email) { ?><div class="d-flex align-items-baseline min-w-0"><i class="fas fa-fw fa-envelope text-body-secondary me-1 flex-shrink-0"></i><a class="text-truncate" href="mailto:<?= $manager_email ?>" title="<?= $manager_email ?>"><?= $manager_email ?></a><button type="button" class="itflow-copy clipboardjs flex-shrink-0" title="Copy email" data-clipboard-text="<?= $manager_email ?>"><i class="far fa-copy"></i></button></div><?php } ?>
<?php } else { ?>
<span class="text-body-secondary">Not recorded</span>
<?php } ?>
</dd>
<dt>Added</dt><dd title="<?= $vendor_created_at ?>"><?= timeAgo($vendor_created_at) ?></dd>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-sticky-note text-secondary me-2"></i>Notes</h3>
</div>
<textarea class="form-control border-0 rounded-0 rounded-bottom itflow-vendor-notes" rows="7" placeholder="Escalation path, who is authorised to call them, circuit IDs, contract renewal - saves when you click away"
data-vendor-id="<?= $vendor_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token']) ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $vendor_notes ?></textarea>
</div>
<?php if ($elsewhere) { ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-users text-secondary me-2"></i>Also used by <span class="badge rounded-pill text-bg-secondary ms-1"><?= count($elsewhere) ?></span></h3>
</div>
<ul class="list-group list-group-flush">
<?php foreach ($elsewhere as $other) {
$other_client_id = intval($other['vendor_client_id']); ?>
<li class="list-group-item d-flex align-items-center gap-2 py-2">
<a class="text-truncate flex-grow-1" href="vendor.php?client_id=<?= $other_client_id ?>&vendor_id=<?= intval($other['vendor_id']) ?>"><?= escapeHtml($other['client_name']) ?></a>
<?php if ($other['vendor_account_number']) { ?><span class="small text-body-secondary font-monospace text-nowrap" title="Their account number"><?= escapeHtml($other['vendor_account_number']) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<div class="card-footer small text-body-secondary">Clients with a vendor of the same name or template - who else an outage hits.</div>
</div>
<?php } ?>
</div>
<!-- What is open with them and what came from them -->
<div class="col-xl-8 col-lg-7">
<div class="card mb-3 itflow-asset-related">
<div class="card-header py-2">
<ul class="nav nav-pills itflow-asset-pills" role="tablist">
<?php foreach ($vendor_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab['count'] ? '' : 'is-empty' ?>" id="vendor-tab-<?= $tab_key ?>" data-bs-toggle="pill" data-bs-target="#vendor-pane-<?= $tab_key ?>" data-vendor-tab="<?= $tab_key ?>" type="button" role="tab">
<i class="fas fa-fw <?= $tab['icon'] ?> me-1 d-none d-xxl-inline"></i><?= $tab['label'] ?>
<span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
<?php if (!empty($tab['badge'])) { ?>
<span class="badge rounded-pill text-bg-danger" title="Open"><?= intval($tab['badge']) ?> open</span>
<?php } ?>
</button>
</li>
<?php } ?>
</ul>
</div>
<div class="tab-content">
<?php if (isset($vendor_tabs['cases'])) { ?>
<!-- Cases - tickets logged with this vendor, open first -->
<div class="tab-pane fade" id="vendor-pane-cases" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Tickets waiting on or worked with them, open first - with their reference.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $new_case_url ?>"><i class="fas fa-plus me-2"></i>New Case</button>
<?php } ?>
</div>
<?php if ($cases) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Ticket</th><th>Subject</th><th>Their ref</th><th>Status</th><th>Assigned</th><th>Updated</th></tr></thead>
<tbody>
<?php foreach ($cases as $case) {
$ticket_id = intval($case['ticket_id']);
$ticket_is_open = empty($case['ticket_resolved_at']) && empty($case['ticket_closed_at']);
$ticket_last = $case['ticket_updated_at'] ?: $case['ticket_created_at'];
$their_ref = escapeHtml($case['ticket_vendor_ticket_number']);
$ticket_url = "ticket.php?" . ($client_id ? "client_id=$client_id&" : '') . "ticket_id=$ticket_id"; ?>
<tr class="<?= $ticket_is_open ? '' : 'text-body-secondary' ?>">
<td class="text-nowrap"><a class="font-monospace" href="<?= $ticket_url ?>"><?= escapeHtml($case['ticket_prefix']) . intval($case['ticket_number']) ?></a></td>
<td><a class="<?= $ticket_is_open ? 'text-body fw-semibold' : 'text-body-secondary' ?>" href="<?= $ticket_url ?>"><?= escapeHtml($case['ticket_subject']) ?></a></td>
<td class="text-nowrap"><?php if ($their_ref) { ?><span class="font-monospace"><?= $their_ref ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy their reference" data-clipboard-text="<?= $their_ref ?>"><i class="far fa-copy"></i></button><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
<td class="text-nowrap"><?= ticketStatusHtml($case['ticket_status_name'], $case['ticket_status_color'], 'detail') ?></td>
<td class="text-nowrap"><?= formatAssignee($case['ticket_assigned_to'], $case['user_name'], $case['user_avatar'], $ticket_is_open) ?></td>
<td class="text-nowrap small" title="<?= escapeHtml($ticket_last) ?>"><?= timeAgo($ticket_last) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php if ($case_count > count($cases)) { ?>
<div class="small text-body-secondary px-3 py-2 border-top">Showing every open case and the <?= count($cases) - $case_open_count ?> most recent closed of <?= $case_count ?>.</div>
<?php } ?>
<?php } else { ?>
<div class="itflow-asset-empty">No cases with them. Set the vendor on a ticket, with their reference, when you log a call with them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['licenses'])) { ?>
<!-- Licenses bought through them - soonest renewal first -->
<div class="tab-pane fade" id="vendor-pane-licenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Licenses and subscriptions bought through them, soonest renewal first.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New License</button>
<?php } ?>
</div>
<?php if ($licenses) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($licenses as $license) {
$renewal = softwareRenewal($license['software_expire']); ?>
<li class="itflow-link-row">
<?= linkRowHtml('software', $license, $client_id) ?>
<?php if ($renewal) { ?><span class="text-nowrap"><?= softwareRenewalPill($renewal) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">No licenses from them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['assets'])) { ?>
<!-- Assets bought from them - for warranty claims -->
<div class="tab-pane fade" id="vendor-pane-assets" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Bought from them, warranty ending soonest first - what to have to hand for a claim.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Asset</button>
<?php } ?>
</div>
<?php if ($assets) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Asset</th><th>Serial</th><th>Order ref</th><th>Purchased</th><th>Warranty</th></tr></thead>
<tbody>
<?php foreach ($assets as $asset) {
$asset_id = intval($asset['asset_id']);
$serial = escapeHtml($asset['asset_serial']);
$order_ref = escapeHtml($asset['asset_purchase_reference']);
$warranty = softwareRenewal($asset['asset_warranty_expire'], 60); ?>
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-<?= assetTypeTone($asset['asset_type']) ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= getAssetIcon($asset['asset_type']) ?>"></i></span>
<div class="min-w-0">
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= $asset_id ?>"><?= escapeHtml($asset['asset_name']) ?></a>
<div class="small text-body-secondary"><?= escapeHtml(implode(' · ', array_filter([$asset['asset_type'], trim($asset['asset_make'] . ' ' . $asset['asset_model'])]))) ?></div>
</div>
</div>
</td>
<td class="text-nowrap"><?php if ($serial) { ?><span class="font-monospace small"><?= $serial ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy serial" data-clipboard-text="<?= $serial ?>"><i class="far fa-copy"></i></button><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
<td class="text-nowrap small"><?= $order_ref ?: '<span class="text-body-secondary">-</span>' ?></td>
<td class="text-nowrap small"><?= escapeHtml($asset['asset_purchase_date']) ?: '<span class="text-body-secondary">-</span>' ?></td>
<td class="text-nowrap">
<?php if ($warranty) {
$tone = $warranty['tone']; ?>
<span class="badge rounded-pill bg-<?= $tone ?>-subtle text-<?= $tone ?>-emphasis border border-<?= $tone ?>-subtle fw-normal" title="<?= escapeHtml($warranty['date']) ?>">
<?= $warranty['state'] === 'expired' ? 'Expired' : ($warranty['state'] === 'soon' ? 'Ends in ' . intval($warranty['days']) . ' days' : 'To ' . escapeHtml($warranty['date'])) ?>
</span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No assets bought from them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['domains'])) { ?>
<!-- Domains they register or host -->
<div class="tab-pane fade" id="vendor-pane-domains" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Domains they register or host, and for what.</span>
</div>
<?php if ($domains) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Domain</th><th>They are</th><th>Expires</th></tr></thead>
<tbody>
<?php foreach ($domains as $domain) {
$roles = [];
foreach (VENDOR_DOMAIN_ROLES as $column => $role) {
if (intval($domain[$column]) === $vendor_id) {
$roles[] = $role;
}
}
$expiry = softwareRenewal($domain['domain_expire']); ?>
<tr>
<td>
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/domain/domain_edit.php?client_id=<?= $client_id ?>&id=<?= intval($domain['domain_id']) ?>"><?= escapeHtml($domain['domain_name']) ?></a>
<?php if ($domain['domain_description']) { ?><div class="small text-body-secondary"><?= escapeHtml($domain['domain_description']) ?></div><?php } ?>
</td>
<td><?php foreach ($roles as $role) { ?><span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal me-1"><?= $role ?></span><?php } ?></td>
<td class="text-nowrap"><?= $expiry ? softwareRenewalPill($expiry) : '<span class="text-body-secondary">-</span>' ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No domains with them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['services'])) { ?>
<!-- Services they are part of -->
<div class="tab-pane fade" id="vendor-pane-services" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">The client's services that depend on them.</span>
</div>
<?php if ($services) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($services as $service) { ?>
<li class="itflow-link-row">
<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-stream"></i></span>
<div class="min-w-0 flex-grow-1">
<div class="d-flex"><a class="ajax-modal text-truncate" href="#" data-modal-size="lg" data-modal-url="modals/service/service.php?id=<?= intval($service['service_id']) ?>"><?= escapeHtml($service['service_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$service['service_category'], $service['service_description']]))) ?></div>
</div>
<?php if ($service['service_importance']) { ?><span class="badge rounded-pill bg-<?= $service['service_importance'] === 'High' ? 'danger' : ($service['service_importance'] === 'Medium' ? 'warning' : 'secondary') ?>-subtle text-body fw-normal"><?= escapeHtml($service['service_importance']) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">No services depend on them.</div>
<?php } ?>
</div>
<?php } ?>
<!-- Portal logins, documents and files -->
<?php foreach (['logins' => ['credential', $logins, 'Their portal and support logins - linked from the client\'s credentials.', 'No logins linked.', $can_credentials_edit],
'documents' => ['document', $documents, 'The contract, the escalation sheet, how to raise a case with them.', 'No documents linked.', $can_support_edit],
'files' => ['file', $files, 'Quotes, invoices, the signed agreement.', 'No files linked.', $can_support_edit]] as $pane => [$to, $linked, $hint, $empty, $can_link]) {
if (!isset($vendor_tabs[$pane])) {
continue;
} ?>
<div class="tab-pane fade" id="vendor-pane-<?= $pane ?>" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small"><?= $hint ?></span>
<?php if ($can_link && !$vendor_archived_at) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="<?= $link_url . $to ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<?php if ($to === 'credential') { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/credential/credential_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-plus me-2"></i>New Login
</button>
<?php } elseif ($to === 'document') { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="xl" data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-plus me-2"></i>New Document
</button>
<?php } else { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/file/file_upload.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-upload me-2"></i>Upload
</button>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($linked) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($linked as $linked_row) { ?>
<li class="itflow-link-row">
<?= linkRowHtml($to, $linked_row, $client_id) ?>
<?php if ($can_link) { ?>
<a class="itflow-link-remove" href="<?= $unlink_url($to, $linked_row['id']) ?>" title="Unlink <?= $to ?>"><i class="fas fa-unlink"></i></a>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty"><?= $empty ?></div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['expenses'])) { ?>
<!-- What we spend with them -->
<div class="tab-pane fade" id="vendor-pane-expenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small d-flex flex-wrap column-gap-3">
<span><strong class="text-body"><?= numfmt_format_currency($currency_format, $spend['year'], $session_company_currency) ?></strong> this year</span>
<span><?= numfmt_format_currency($currency_format, $spend['last_year'], $session_company_currency) ?> last year</span>
<span><?= numfmt_format_currency($currency_format, $spend['total'], $session_company_currency) ?> in all</span>
</span>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-secondary" href="expenses.php?vendor=<?= $vendor_id ?>"><i class="fas fa-list me-2"></i>All</a>
<?php if ($can_financial_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/expense/expense_add.php?vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Expense</button>
<?php } ?>
</div>
</div>
<?php if ($expenses) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Date</th><th>What for</th><th>Account</th><th class="text-end">Amount</th></tr></thead>
<tbody>
<?php foreach ($expenses as $expense) { ?>
<tr>
<td class="text-nowrap"><a class="ajax-modal" href="#" data-modal-url="modals/expense/expense_edit.php?id=<?= intval($expense['expense_id']) ?>"><?= escapeHtml($expense['expense_date']) ?></a></td>
<td class="w-100">
<div><?= escapeHtml(truncate((string) $expense['expense_description'], 90)) ?: '<span class="text-body-secondary">-</span>' ?></div>
<div class="small text-body-secondary"><?= escapeHtml(implode(' · ', array_filter([$expense['category_name'], $expense['expense_reference']]))) ?></div>
</td>
<td class="text-nowrap small"><?= escapeHtml($expense['account_name']) ?></td>
<td class="text-end text-nowrap font-monospace"><?= numfmt_format_currency($currency_format, floatval($expense['expense_amount']), $expense['expense_currency_code']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php if ($spend['count'] > count($expenses)) { ?>
<div class="small text-body-secondary px-3 py-2 border-top">The <?= count($expenses) ?> most recent of <?= $spend['count'] ?> - <a href="expenses.php?vendor=<?= $vendor_id ?>">see them all</a>.</div>
<?php } ?>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing spent with them yet.</div>
<?php } ?>
</div>
<!-- What is booked to them every month or year -->
<div class="tab-pane fade" id="vendor-pane-recurring" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Subscriptions and contracts booked automatically.</span>
<?php if ($can_financial_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_add.php?vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Recurring</button>
<?php } ?>
</div>
<?php if ($recurring) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Next</th><th>Every</th><th>What for</th><th class="text-end">Amount</th></tr></thead>
<tbody>
<?php foreach ($recurring as $recurring_expense) {
$active = intval($recurring_expense['recurring_expense_status']) === 1; ?>
<tr class="<?= $active ? '' : 'text-body-secondary' ?>">
<td class="text-nowrap"><a class="ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_edit.php?id=<?= intval($recurring_expense['recurring_expense_id']) ?>"><?= escapeHtml($recurring_expense['recurring_expense_next_date']) ?></a><?php if (!$active) { ?> <span class="badge rounded-pill text-bg-secondary fw-normal">Paused</span><?php } ?></td>
<td class="text-nowrap"><?= intval($recurring_expense['recurring_expense_frequency']) === 1 ? 'Month' : 'Year' ?></td>
<td class="w-100">
<div><?= escapeHtml(truncate((string) $recurring_expense['recurring_expense_description'], 90)) ?: '<span class="text-body-secondary">-</span>' ?></div>
<div class="small text-body-secondary"><?= escapeHtml($recurring_expense['category_name']) ?></div>
</td>
<td class="text-end text-nowrap font-monospace"><?= numfmt_format_currency($currency_format, floatval($recurring_expense['recurring_expense_amount']), $recurring_expense['recurring_expense_currency_code']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing recurring with them.</div>
<?php } ?>
</div>
<?php } ?>
<!-- History, from the audit log -->
<div class="tab-pane fade" id="vendor-pane-history" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Edits, links and notes - newest first.</span>
</div>
<?php if ($history) {
$history_icons = ['Create' => 'fa-plus', 'Edit' => 'fa-edit', 'Archive' => 'fa-archive', 'Restore' => 'fa-redo', 'Link' => 'fa-link', 'Unlink' => 'fa-unlink']; ?>
<ul class="list-group list-group-flush">
<?php foreach ($history as $entry) { ?>
<li class="list-group-item d-flex gap-3 py-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-other" title="<?= escapeHtml($entry['log_action']) ?>"><i class="fas fa-fw <?= $history_icons[$entry['log_action']] ?? 'fa-circle' ?>"></i></span>
<div class="flex-grow-1 min-w-0">
<div><?= escapeHtml($entry['log_description']) ?></div>
<div class="small text-body-secondary"><?= escapeHtml($entry['user_name'] ?: 'System') ?> &middot; <span title="<?= escapeHtml($entry['log_created_at']) ?>"><?= timeAgo($entry['log_created_at']) ?></span></div>
</div>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing logged yet.</div>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<script>
// Notes save when you click away
document.querySelectorAll('.itflow-vendor-notes').forEach(function (notes) {
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
vendor_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
vendor_id: notes.dataset.vendorId,
notes: notes.value
});
});
});
/*
* Which tab is open: the URL hash wins (#licenses), then the tab last
* used on any vendor, then the first. Choosing a tab updates the hash
* without a history entry, so the redirect back from a save lands on
* the same tab. Bootstrap loads in the footer, so this waits for it.
*/
document.addEventListener('DOMContentLoaded', function () {
var key = 'itflow.vendorTab';
var tabs = document.querySelectorAll('[data-vendor-tab]');
if (!tabs.length) {
return;
}
function byName(name) {
return name ? document.querySelector('[data-vendor-tab="' + name + '"]') : null;
}
var remembered = '';
try { remembered = localStorage.getItem(key) || ''; } catch (e) {}
var start = byName(location.hash.replace('#', '')) || byName(remembered) || tabs[0];
bootstrap.Tab.getOrCreateInstance(start).show();
tabs.forEach(function (tab) {
tab.addEventListener('shown.bs.tab', function () {
var name = tab.getAttribute('data-vendor-tab');
try { localStorage.setItem(key, name); } catch (e) {}
history.replaceState(null, '', location.pathname + location.search + '#' + name);
});
});
});
</script>
<?php
}
require_once "../includes/footer.php";

View File

@@ -1,269 +1,436 @@
<?php
/*
* Vendors - one client's (their ISP, phone provider, software maker), ours
* (our suppliers - what expenses are booked against), or every client's from
* All Client Docs (?view=clients) - which is how an outage at one ISP is
* checked against every client that uses it.
*
* Starred vendors come first. Each row is what a tech needs to make the call -
* the support line and hours, the account number and PIN - and what the
* vendor is to the client: open cases, licenses, assets, domains, services
* and logins, each opening that tab on the vendor's page.
*/
// Default Column Sortby Filter
$sort = "vendor_name";
$order = "ASC";
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
// One client's vendors
require_once "includes/inc_all_client.php";
$view = 'client';
$client_query = "AND vendor_client_id = $client_id";
$client_url = "client_id=$client_id&";
} elseif (($_GET['view'] ?? '') === 'clients') {
// Every client's vendors
require_once "includes/inc_client_overview_all.php";
$view = 'clients';
$client_query = "AND vendor_client_id > 0";
$client_url = "view=clients&";
} else {
// Ours
require_once "includes/inc_all.php";
$view = 'ours';
$client_query = "AND vendor_client_id = 0";
$client_url = '';
}
// Overide Filter Header Archived
$archived = isset($_GET['archived']) && $_GET['archived'] == 1 ? 1 : 0;
if ($view === 'clients') {
$archive_query = $archived ? "(client_archived_at IS NOT NULL OR vendor_archived_at IS NOT NULL)" : "(client_archived_at IS NULL AND vendor_archived_at IS NULL)";
} else {
$archive_query = $archived ? "vendor_archived_at IS NOT NULL" : "vendor_archived_at IS NULL";
}
// Perms - a client's vendors are the client module's; anyone may look up ours, the financial module changes them
if ($view !== 'ours') {
enforceUserPermission('module_client');
}
$can_edit = $view === 'ours' ? lookupUserPermission('module_financial') >= 2 : lookupUserPermission('module_client') >= 2;
$can_delete = ($view === 'ours' ? lookupUserPermission('module_financial') >= 3 : lookupUserPermission('module_client') >= 3) && !empty($config_destructive_deletes_enable);
$can_support = lookupUserPermission('module_support') >= 1;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$can_financial = lookupUserPermission('module_financial') >= 1;
$client = '';
if ($view === 'clients' && !empty($_GET['client'])) {
// Client Filter
$client = intval($_GET['client']);
$client_query = "AND vendor_client_id = $client";
}
// Tags Filter - a vendor with any of them
if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) {
$tag_filter = implode(",", array_map('intval', $_GET['tags']));
$tag_query = "AND vendors.vendor_id IN (SELECT vendor_id FROM vendor_tags WHERE tag_id IN ($tag_filter))";
} else {
$tag_filter = 0;
$tag_query = '';
}
// Only these sort - filter_header.php will hand through any lowercase word, and
// an unknown column takes the page down with a mysqli exception
$sortable_columns = ['vendor_name', 'vendor_description', 'vendor_account_number', 'vendor_phone', 'vendor_contact_name', 'vendor_hours', 'client_name', 'vendor_created_at'];
if (!in_array($sort, $sortable_columns, true)) {
$sort = 'vendor_name';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS vendor_account_number, vendor_archived_at, vendor_code, vendor_contact_name,
vendor_created_at, vendor_description, vendor_email, vendor_extension, vendor_hours,
vendor_id, vendor_name, vendor_notes, vendor_phone, vendor_phone_country_code, vendor_sla,
vendor_templates.vendor_template_id, vendor_template_name, vendor_website FROM vendors
"SELECT SQL_CALC_FOUND_ROWS vendors.vendor_id, vendor_name, vendor_description, vendor_account_number, vendor_code, vendor_contact_name,
vendor_contact_phone, vendor_contact_phone_country_code, vendor_contact_email, vendor_phone, vendor_phone_country_code, vendor_extension,
vendor_email, vendor_website, vendor_hours, vendor_sla, vendor_favorite, vendor_archived_at, vendor_client_id, vendor_template_name,
client_id, client_name
FROM vendors
LEFT JOIN clients ON client_id = vendor_client_id
LEFT JOIN vendor_templates ON vendors.vendor_template_id = vendor_templates.vendor_template_id
WHERE vendor_$archive_query
AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%' OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_phone LIKE '%$phone_query%')
LEFT JOIN vendor_templates ON vendor_templates.vendor_template_id = vendors.vendor_template_id
WHERE $archive_query
$tag_query
AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%'
OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_contact_email LIKE '%$q%' OR vendor_notes LIKE '%$q%'
OR vendor_phone LIKE '%$phone_query%' OR vendor_contact_phone LIKE '%$phone_query%'" . ($view === 'clients' ? " OR client_name LIKE '%$q%'" : '') . "
OR vendors.vendor_id IN (SELECT vendor_id FROM vendor_tags JOIN tags USING (tag_id) WHERE tag_name LIKE '%$q%'))
$client_query
" . clientScopeSql('vendor_client_id') . "
ORDER BY $sort $order LIMIT $record_from, $record_to"
ORDER BY vendor_favorite DESC, $sort $order, vendor_name ASC LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$vendors = [];
while ($row = mysqli_fetch_assoc($sql)) {
$vendors[intval($row['vendor_id'])] = $row;
}
// Tags and what each vendor is used for, for the whole page at once, not per row
$vendors_tags = tagsForRecords('vendor', array_keys($vendors));
$vendors_counts = vendorCounts($vendors);
$has_templates = (bool) mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM vendor_templates WHERE vendor_template_archived_at IS NULL LIMIT 1"));
$add_url = "modals/vendor/vendor_add.php?" . ($view === 'client' ? "client_id=$client_id" : ($view === 'clients' ? 'view=clients' : ''));
?>
<div class="card">
<div class="card-header bg-dark py-2">
<h3 class="card-title mt-2">
<i class="fas fa-fw fa-building me-2"></i>Vendors
</h3>
<div class="card-tools">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-building me-2"></i><?= $view === 'clients' ? 'Client Vendors' : 'Vendors' ?></h3>
<div class="card-tools d-flex align-items-center gap-2">
<?php if ($view !== 'client' && lookupUserPermission('module_client') >= 1) { ?>
<!-- Ours, or every client's -->
<nav class="nav nav-pills itflow-vendor-scope d-none d-md-flex">
<a class="nav-link py-1 <?= $view === 'ours' ? 'active' : 'text-light' ?>" href="vendors.php">Ours</a>
<a class="nav-link py-1 <?= $view === 'clients' ? 'active' : 'text-light' ?>" href="vendors.php?view=clients">Clients'</a>
</nav>
<?php } ?>
<?php if ($can_edit || $num_rows[0]) { ?>
<div class="btn-group">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/vendor/vendor_add.php?<?= $client_url ?>">
<i class="fas fa-plus me-2"></i>New Vendor
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu">
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="modals/vendor/vendor_add_from_template.php?<?= $client_url ?>">
<i class="fa fa-fw fa-puzzle-piece me-2"></i>Create from Template
</a>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $add_url ?>">
<i class="fas fa-plus me-2"></i>New Vendor
</button>
<?php } ?>
<button type="button" class="btn btn-primary dropdown-toggle <?= $can_edit ? 'dropdown-toggle-split' : '' ?>" data-bs-toggle="dropdown"><?= $can_edit ? '' : '<i class="fas fa-ellipsis-h"></i>' ?></button>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($can_edit && $has_templates) { ?>
<a class="dropdown-item text-dark ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= $add_url ?>&from_template=1">
<i class="fa fa-fw fa-puzzle-piece me-2"></i>New from Template
</a>
<?php if ($view !== 'ours') { ?>
<a class="dropdown-item text-dark ajax-modal" href="#" data-modal-url="modals/vendor/vendor_template_clients.php">
<i class="fa fa-fw fa-users me-2"></i>Add a Template to Clients
</a>
<?php } ?>
<?php } ?>
<?php if ($num_rows[0] > 0) { ?>
<div class="dropdown-divider"></div>
<?php if ($can_edit && $has_templates) { ?><div class="dropdown-divider"></div><?php } ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="<?= buildExportModalUrl('modals/vendor/vendor_export.php', ['client_id', 'archived', 'q']) ?>">
data-modal-url="<?= buildExportModalUrl('modals/vendor/vendor_export.php', ['client_id', 'view', 'client', 'tags', 'archived', 'q']) ?>">
<i class="fa fa-fw fa-download me-2"></i>Export
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
<div class="card-header py-3">
<form autocomplete="off">
<?php if ($client_url) { ?>
<?php if ($view === 'client') { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } elseif ($view === 'clients') { ?>
<input type="hidden" name="view" value="clients">
<?php } ?>
<input type="hidden" name="archived" value="<?= $archived ?>">
<div class="row g-2 align-items-center">
<div class="col-md-4">
<div class="col-lg-4 col-md-6">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Vendors">
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search name, account, phone, contact...">
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-md-8">
<div class="btn-group float-end">
<a href="?<?= "$client_url" ?>archived=<?php if($archived == 1){ echo 0; } else { echo 1; } ?>"
class="btn btn-<?php if($archived == 1){ echo "primary"; } else { echo "default"; } ?>">
<i class="fa fa-fw fa-archive me-2"></i>Archived
<?php if ($view === 'clients') { ?>
<div class="col-lg-3 col-md-6">
<select class="form-select select2" name="client" onchange="this.form.submit()">
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "
SELECT DISTINCT client_id, client_name, client_abbreviation
FROM clients
JOIN vendors ON vendor_client_id = client_id
WHERE $archive_query
" . clientScopeSql('clients.client_id') . "
ORDER BY client_name ASC
");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id_filter = intval($row['client_id']); ?>
<option <?php if ($client == $client_id_filter) { echo "selected"; } ?> value="<?= $client_id_filter ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
<?php } ?>
</select>
</div>
<?php } ?>
<div class="col-lg-3 col-md-6">
<select onchange="this.form.submit()" class="form-select select2" name="tags[]" data-placeholder="- Tags -" multiple>
<?php
// Tags in use on vendors in this scope, plus any already picked
$sql_tags_filter = mysqli_query($mysqli, "
SELECT tag_id, tag_name, tag_color, tag_icon FROM tags
WHERE tag_type = 6
AND (
EXISTS (SELECT 1 FROM vendor_tags JOIN vendors ON vendors.vendor_id = vendor_tags.vendor_id
WHERE vendor_tags.tag_id = tags.tag_id " . clientScopeSql('vendor_client_id') . " $client_query)
OR tag_id IN ($tag_filter)
)
ORDER BY tag_name ASC
");
while ($row = mysqli_fetch_assoc($sql_tags_filter)) {
$tag_id = intval($row['tag_id']); ?>
<option value="<?= $tag_id ?>" <?php if (isset($_GET['tags']) && is_array($_GET['tags']) && in_array($tag_id, $_GET['tags'])) { echo 'selected'; } ?><?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= escapeHtml($row['tag_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="<?= $view === 'clients' ? 'col-lg-2' : 'col-lg-5' ?> col-md-6">
<div class="d-flex justify-content-end gap-2">
<?php if ($view !== 'client' && lookupUserPermission('module_client') >= 1) { ?>
<nav class="nav nav-pills d-md-none me-auto">
<a class="nav-link py-1 <?= $view === 'ours' ? 'active' : '' ?>" href="vendors.php">Ours</a>
<a class="nav-link py-1 <?= $view === 'clients' ? 'active' : '' ?>" href="vendors.php?view=clients">Clients'</a>
</nav>
<?php } ?>
<a href="?<?= $client_url ?>archived=<?= $archived ? 0 : 1 ?>"
class="btn <?= $archived ? 'btn-dark' : 'btn-outline-dark' ?>" title="<?= $archived ? 'Back to active vendors' : 'Show archived vendors' ?>">
<i class="fa fa-fw fa-archive"></i><span class="d-lg-none ms-2">Archived</span>
</a>
<div class="dropdown ms-2" id="bulkActionButton" hidden>
<?php if ($can_edit) { ?>
<div class="dropdown" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<?php if ($archived) { ?>
<button class="dropdown-item text-info"
type="submit" form="bulkActions" name="bulk_restore_vendors">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="vendor" 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 text-bold"
type="submit" form="bulkActions" name="bulk_delete_vendors">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php if ($archived) { ?>
<button class="dropdown-item text-info" type="submit" form="bulkActions" name="bulk_restore_vendors">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</button>
<?php if ($can_delete) { ?>
<button class="dropdown-item text-danger text-bold confirm-link" type="submit" form="bulkActions" name="bulk_delete_vendors"
data-confirm-text="The vendors go for good. Their cases, licenses, assets and domains stay, from no vendor. Vendors with expenses are kept.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php } ?>
<?php } else { ?>
<button class="dropdown-item text-danger confirm-link"
type="submit" form="bulkActions" name="bulk_archive_vendors">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</button>
<button class="dropdown-item text-danger confirm-link" type="submit" form="bulkActions" name="bulk_archive_vendors">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</button>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
</form>
</div>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover mb-0">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<table class="table table-hover align-middle mb-0 itflow-asset-table">
<thead class="<?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap table-light">
<tr>
<td class="checkbox-column border-end">
<?php if ($can_edit) { ?>
<th class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
</div>
</td>
</th>
<?php } ?>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_name&order=<?= $disp ?>">
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=vendor_name&order=<?= $disp ?>">
Vendor <?php if ($sort == 'vendor_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_description&order=<?= $disp ?>">
Description <?php if ($sort == 'vendor_description') { echo $order_icon; } ?>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=vendor_phone&order=<?= $disp ?>">
Support <?php if ($sort == 'vendor_phone') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_contact_name&order=<?= $disp ?>">
Contact <?php if ($sort == 'vendor_contact_name') { echo $order_icon; } ?>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=vendor_account_number&order=<?= $disp ?>">
Account <?php if ($sort == 'vendor_account_number') { echo $order_icon; } ?>
</a>
</th>
<th><?= $view === 'ours' ? 'Spend' : 'With them' ?></th>
<?php if ($view === 'clients') { ?>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_website&order=<?= $disp ?>">
Website <?php if ($sort == 'vendor_website') { echo $order_icon; } ?>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th></th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_id = intval($row['vendor_id']);
foreach ($vendors as $vendor_id => $row) {
$row_client_id = intval($row['vendor_client_id']);
$vendor_name = escapeHtml($row['vendor_name']);
$vendor_description = escapeHtml($row['vendor_description']);
if ($vendor_description) {
$vendor_description_display = $vendor_description;
} else {
$vendor_description_display = "-";
}
$vendor_account_number = escapeHtml($row['vendor_account_number']);
if ($vendor_account_number) {
$vendor_account_number_display = "<div class='text-secondary'>Account #: <span class='font-monospace'>$vendor_account_number</span></div>";
} else {
$vendor_account_number_display = '';
}
$vendor_contact_name = escapeHtml($row['vendor_contact_name']);
if ($vendor_contact_name) {
$vendor_contact_name_display = $vendor_contact_name;
} else {
$vendor_contact_name_display = "-";
}
$vendor_phone_country_code = escapeHtml($row['vendor_phone_country_code']);
$vendor_phone = escapeHtml(formatPhoneNumber($row['vendor_phone'], $vendor_phone_country_code));
$vendor_extension = escapeHtml($row['vendor_extension']);
$vendor_phone = contactPhoneLink($row['vendor_phone'], $row['vendor_phone_country_code'], $row['vendor_extension']);
$vendor_phone_text = $row['vendor_phone'] ? escapeHtml(formatPhoneNumber($row['vendor_phone'], $row['vendor_phone_country_code'])) : '';
$vendor_email = escapeHtml($row['vendor_email']);
$vendor_website = escapeHtml($row['vendor_website']);
if ($vendor_website) {
$vendor_website_display = "<a href='https://$vendor_website' target='_blank'>$vendor_website <i class='fa fa-external-link-alt'></i></a><button type='button' class='itflow-copy clipboardjs' title='Copy website' data-clipboard-text='$vendor_website'><i class='far fa-copy'></i></button>";
} else {
$vendor_website_display = "-";
}
$vendor_hours = escapeHtml($row['vendor_hours']);
$vendor_sla = escapeHtml($row['vendor_sla']);
$vendor_code = escapeHtml($row['vendor_code']);
$vendor_notes = escapeHtml($row['vendor_notes']);
$vendor_created_at = escapeHtml($row['vendor_created_at']);
$vendor_website_url = vendorWebsiteUrl($row['vendor_website']);
$vendor_support_when = escapeHtml(implode(' · ', array_filter([$row['vendor_hours'], $row['vendor_sla']])));
$vendor_account = escapeHtml($row['vendor_account_number']);
$vendor_pin = vendorSecretHtml($row['vendor_code']);
$vendor_favorite = intval($row['vendor_favorite']);
$vendor_archived_at = escapeHtml($row['vendor_archived_at']);
$vendor_template_id = intval($row['vendor_template_id']);
$vendor_template_name = escapeHtml($row['vendor_template_name']);
if ($vendor_template_id) {
$vendor_template_display = "<div class='text-secondary' title='Base Vendor Template'><i class='fas fa-puzzle-piece me-1'></i>$vendor_template_name</div>";
} else {
$vendor_template_display = "";
}
$vendor_tags = $vendors_tags[$vendor_id] ?? [];
$counts = $vendors_counts[$vendor_id];
$vendor_url = 'vendor.php?' . ($row_client_id ? "client_id=$row_client_id&" : '') . "vendor_id=$vendor_id";
$row_can_edit = $can_edit;
$tag_base = $view === 'client' ? "vendors.php?client_id=$row_client_id&" : ($view === 'clients' ? 'vendors.php?view=clients&' : 'vendors.php?');
?>
<tr>
<?php if ($can_edit) { ?>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="vendor_ids[]" value="<?= $vendor_id ?>">
<input class="form-check-input bulk-select" type="checkbox" name="vendor_ids[]" value="<?= $vendor_id ?>" aria-label="Select <?= $vendor_name ?>">
</div>
</td>
<td>
<a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>">
<div class="d-flex">
<i class="fas fa-fw fa-2x fa-building text-dark me-2"></i>
<div class="flex-grow-1">
<div><?= $vendor_name ?></div>
<div><small><?= $vendor_account_number_display ?><small></div>
</div>
<?php } ?>
<!-- The whole cell opens the vendor; the tag pills sit above it -->
<td class="position-relative itflow-asset-name-cell">
<div class="d-flex align-items-center gap-3">
<?= vendorTile($row['vendor_name']) ?>
<div class="min-w-0">
<a class="fw-semibold text-body stretched-link" href="<?= $vendor_url ?>"><?= $vendor_name ?></a>
<?php if ($vendor_favorite) { ?><i class="fas fa-star text-warning small ms-1" title="Starred"></i><?php } ?>
<?php if ($row['vendor_template_name']) { ?><i class="fas fa-puzzle-piece text-body-secondary small ms-1" title="From template <?= escapeHtml($row['vendor_template_name']) ?>"></i><?php } ?>
<?php if ($vendor_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal ms-1">Archived</span><?php } ?>
<?php if ($vendor_description) { ?>
<div class="small text-body-secondary text-truncate itflow-asset-desc" title="<?= $vendor_description ?>"><?= $vendor_description ?></div>
<?php } ?>
<?php if ($vendor_tags || $row_can_edit) { ?>
<div class="mt-1 itflow-above-stretch"><?= tagPills('vendor', $vendor_id, $vendor_tags, $tag_base, $row_can_edit) ?></div>
<?php } ?>
</div>
</a>
</div>
</td>
<td>
<?= $vendor_description_display ?>
</td>
<td>
<?php
if (!empty($vendor_contact_name)) { ?>
<i class="fa fa-fw fa-user text-secondary me-2 mb-2"></i><?= $vendor_contact_name_display ?>
<br>
<?php } else {
echo $vendor_contact_name_display;
}
if (!empty($vendor_phone)) { ?>
<i class="fa fa-fw fa-phone text-secondary me-2 mb-2"></i><?= $vendor_phone ?>
<br>
<?php }
if (!empty($vendor_email)) { ?>
<i class="fa fa-fw fa-envelope text-secondary me-2 mb-2"></i><?= $vendor_email ?>
<br>
<td class="itflow-vendor-support-cell">
<?php if ($vendor_phone) { ?>
<div class="text-nowrap"><?= $vendor_phone ?><button type="button" class="itflow-copy clipboardjs" title="Copy phone number" data-clipboard-text="<?= $vendor_phone_text ?>"><i class="far fa-copy"></i></button></div>
<?php } ?>
<?php if ($vendor_support_when) { ?><div class="small text-body-secondary"><i class="far fa-clock me-1"></i><?= $vendor_support_when ?></div><?php } ?>
<?php if ($vendor_email || $vendor_website_url) { ?>
<div class="small d-flex align-items-center gap-2 mt-1">
<?php if ($vendor_email) { ?><span class="text-truncate itflow-vendor-email"><a href="mailto:<?= $vendor_email ?>"><?= $vendor_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $vendor_email ?>"><i class="far fa-copy"></i></button></span><?php } ?>
<?php if ($vendor_website_url) { ?><a class="text-body-secondary" href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener" title="<?= escapeHtml(vendorWebsiteLabel($row['vendor_website'])) ?>"><i class="fas fa-globe"></i></a><?php } ?>
</div>
<?php } ?>
<?php if (!$vendor_phone && !$vendor_support_when && !$vendor_email && !$vendor_website_url) { ?><span class="text-body-secondary">-</span><?php } ?>
</td>
<td><?= $vendor_website_display ?></td>
<td><?= $vendor_template_display ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<?php if ($vendor_account) { ?>
<div class="text-nowrap"><span class="font-monospace"><?= $vendor_account ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="<?= $vendor_account ?>"><i class="far fa-copy"></i></button></div>
<?php } ?>
<?php if ($vendor_pin) { ?><div class="small text-nowrap"><span class="text-body-secondary me-1">PIN</span><?= $vendor_pin ?></div><?php } ?>
<?php if ($row['vendor_contact_name']) { ?><div class="small text-body-secondary text-truncate"><i class="fas fa-user-tie me-1"></i><?= escapeHtml($row['vendor_contact_name']) ?></div><?php } ?>
<?php if (!$vendor_account && !$vendor_pin && !$row['vendor_contact_name']) { ?><span class="text-body-secondary">-</span><?php } ?>
</td>
<td>
<div class="d-flex flex-wrap gap-1">
<?php if ($can_support && $counts['open']) { ?>
<a class="itflow-contact-chip is-alert" href="<?= $vendor_url ?>#cases" title="<?= $counts['open'] ?> open case<?= $counts['open'] === 1 ? '' : 's' ?> with them"><i class="fas fa-life-ring"></i><?= $counts['open'] ?></a>
<?php } ?>
<?php if ($view === 'ours') { ?>
<?php if ($can_financial && $counts['expense']) { ?>
<span class="w-100 small text-nowrap"><strong><?= numfmt_format_currency($currency_format, $counts['spend_year'], $session_company_currency) ?></strong> <span class="text-body-secondary">this year</span></span>
<?php } ?>
<?php if ($can_financial && $counts['expense']) { ?>
<a class="itflow-contact-chip" href="<?= $vendor_url ?>#expenses" title="<?= $counts['expense'] ?> expense<?= $counts['expense'] === 1 ? '' : 's' ?>"><i class="fas fa-receipt"></i><?= $counts['expense'] ?></a>
<?php } ?>
<?php if ($can_financial && $counts['recurring']) { ?>
<a class="itflow-contact-chip" href="<?= $vendor_url ?>#recurring" title="<?= $counts['recurring'] ?> recurring expense<?= $counts['recurring'] === 1 ? '' : 's' ?>"><i class="fas fa-redo-alt"></i><?= $counts['recurring'] ?></a>
<?php } ?>
<?php } else { ?>
<?php foreach (['software' => ['licenses', 'fa-cube', 'license'], 'asset' => ['assets', 'fa-desktop', 'asset'], 'domain' => ['domains', 'fa-globe', 'domain'], 'service' => ['services', 'fa-stream', 'service']] as $kind => [$tab, $icon, $label]) {
if ($can_support && $counts[$kind]) { ?>
<a class="itflow-contact-chip" href="<?= $vendor_url ?>#<?= $tab ?>" title="<?= $counts[$kind] ?> <?= $label ?><?= $counts[$kind] === 1 ? '' : 's' ?>"><i class="fas <?= $icon ?>"></i><?= $counts[$kind] ?></a>
<?php }
} ?>
<?php if ($can_credentials && $counts['credential']) { ?>
<a class="itflow-contact-chip" href="<?= $vendor_url ?>#logins" title="<?= $counts['credential'] ?> login<?= $counts['credential'] === 1 ? '' : 's' ?>"><i class="fas fa-key"></i><?= $counts['credential'] ?></a>
<?php } ?>
<?php } ?>
<?php
$shown = $view === 'ours'
? ($can_support && $counts['open']) || ($can_financial && ($counts['expense'] || $counts['recurring']))
: ($can_support && ($counts['open'] || $counts['software'] || $counts['asset'] || $counts['domain'] || $counts['service'])) || ($can_credentials && $counts['credential']);
if (!$shown) { ?><span class="text-body-secondary small">Nothing yet</span><?php } ?>
</div>
</td>
<?php if ($view === 'clients') { ?>
<td><a href="vendors.php?client_id=<?= $row_client_id ?>"><?= escapeHtml($row['client_name']) ?></a></td>
<?php } ?>
<td class="text-end">
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>">
<i class="fas fa-fw fa-eye text-secondary me-2"></i>Quick look
</a>
<?php if ($session_user_role == 3) { ?>
<?php if ($vendor_archived_at) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-info confirm-link" href="post.php?restore_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</a>
<?php if ($config_destructive_deletes_enable) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
<?php if ($row_can_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<?php if (!$vendor_archived_at) { ?>
<a class="dropdown-item" href="post.php?<?= $vendor_favorite ? 'unfavorite_vendor' : 'favorite_vendor' ?>=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="<?= $vendor_favorite ? 'far' : 'fas' ?> fa-fw fa-star text-secondary me-2"></i><?= $vendor_favorite ? 'Unstar' : 'Star' ?>
</a>
<?php } ?>
<?php } else { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php if ($vendor_archived_at) { ?>
<a class="dropdown-item" href="post.php?restore_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= $vendor_name ?> goes for good. Its cases, licenses, assets and domains stay, from no vendor.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
<?php } ?>
</div>
@@ -271,17 +438,19 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</td>
</tr>
<?php
}
?>
<?php } ?>
</tbody>
</table>
</div>
</form>
<?php require_once "../includes/filter_footer.php";
?>
<?php if (!$num_rows[0]) { ?>
<div class="itflow-asset-empty">
<i class="fas fa-building fa-2x mb-2 opacity-50"></i>
<div><?= $q || $tag_query ? 'No vendors match.' : ($view === 'ours' ? 'None of our own vendors yet - the suppliers expenses are booked against.' : 'No vendors yet - their ISP, phone provider and software makers.') ?></div>
</div>
<?php } ?>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
<script src="../js/bulk_actions.js"></script>

View File

@@ -13,7 +13,7 @@ $insert_id = false;
if (!empty($name)) {
// Insert vendor
$insert_sql = mysqli_query($mysqli, "INSERT INTO vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_client_id = $client_id");
$insert_sql = mysqli_query($mysqli, "INSERT INTO vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_contact_phone_country_code = '$contact_phone_country_code', vendor_contact_phone = '$contact_phone', vendor_contact_email = '$contact_email', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes', vendor_client_id = $client_id");
// Check insert & get insert ID
if ($insert_sql) {

View File

@@ -18,7 +18,7 @@ if (!empty($vendor_id)) {
require_once 'vendor_model.php';
if ($vendor_row) {
$update_sql = mysqli_query($mysqli, "UPDATE vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes' WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL LIMIT 1");
$update_sql = mysqli_query($mysqli, "UPDATE vendors SET vendor_name = '$name', vendor_description = '$description', vendor_contact_name = '$contact_name', vendor_contact_phone_country_code = '$contact_phone_country_code', vendor_contact_phone = '$contact_phone', vendor_contact_email = '$contact_email', vendor_phone_country_code = '$phone_country_code', vendor_phone = '$phone', vendor_extension = '$extension', vendor_email = '$email', vendor_website = '$website', vendor_hours = '$hours', vendor_sla = '$sla', vendor_code = '$code', vendor_account_number = '$account_number', vendor_notes = '$notes' WHERE vendor_id = $vendor_id AND vendor_client_id = $client_id AND vendor_archived_at IS NULL LIMIT 1");
// Check update & get affected rows
if ($update_sql) {

View File

@@ -34,6 +34,30 @@ if (isset($_POST['vendor_contact_name'])) {
$contact_name = '';
}
if (isset($_POST['vendor_contact_phone_country_code'])) {
$contact_phone_country_code = preg_replace("/[^0-9]/", '', $_POST['vendor_contact_phone_country_code']);
} elseif ($vendor_row) {
$contact_phone_country_code = mysqli_real_escape_string($mysqli, (string) $vendor_row['vendor_contact_phone_country_code']);
} else {
$contact_phone_country_code = '';
}
if (isset($_POST['vendor_contact_phone'])) {
$contact_phone = preg_replace("/[^0-9]/", '', $_POST['vendor_contact_phone']);
} elseif ($vendor_row) {
$contact_phone = mysqli_real_escape_string($mysqli, (string) $vendor_row['vendor_contact_phone']);
} else {
$contact_phone = '';
}
if (isset($_POST['vendor_contact_email'])) {
$contact_email = escapeSql($_POST['vendor_contact_email']);
} elseif ($vendor_row) {
$contact_email = mysqli_real_escape_string($mysqli, (string) $vendor_row['vendor_contact_email']);
} else {
$contact_email = '';
}
if (isset($_POST['vendor_phone_country_code'])) {
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['vendor_phone_country_code']);
} elseif ($vendor_row) {

View File

@@ -4550,3 +4550,55 @@ img.itflow-contact-avatar {
max-height: 7.5rem;
overflow-y: auto;
}
/* Vendors - agent/vendors.php, agent/vendor.php, agent/modals/vendor/
A vendor's tile is its initials on a colour picked by its name
(functions/vendor.php vendorTile()), square like the asset and license
tiles, so one vendor is the same colour at every client. */
.itflow-vendor-tile {
font-weight: 600;
font-size: .85rem;
letter-spacing: .02em;
}
.itflow-vendor-tile-lg {
font-size: 1.2rem;
}
.itflow-vendor-tile-sm {
font-size: .65rem;
letter-spacing: 0;
}
.itflow-vendor-tile-xs {
width: 1.25rem;
height: 1.25rem;
border-radius: .3rem;
font-size: .5rem;
letter-spacing: 0;
}
/* A PIN or code shown as dots until it is asked for */
.itflow-secret {
font-family: var(--bs-font-monospace);
}
.itflow-secret:not(.is-shown) .itflow-secret-value {
display: none;
}
.itflow-secret.is-shown .itflow-secret-mask {
display: none;
}
.itflow-secret-toggle {
border: 0;
background: none;
padding: 0 .25rem;
color: var(--bs-secondary-color);
font-size: .8em;
}
.itflow-secret-toggle:hover {
color: var(--bs-emphasis-color);
}
/* The vendor list's support and account cells */
.itflow-vendor-support-cell {
min-width: 13rem;
}
.itflow-vendor-email {
max-width: 16rem;
}

20
db.sql
View File

@@ -3220,6 +3220,23 @@ CREATE TABLE `vendor_files` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `vendor_tags`
--
DROP TABLE IF EXISTS `vendor_tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `vendor_tags` (
`vendor_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`vendor_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `vendor_tags_ibfk_1` FOREIGN KEY (`vendor_id`) REFERENCES `vendors` (`vendor_id`) ON DELETE CASCADE,
CONSTRAINT `vendor_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`tag_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `vendor_templates`
--
@@ -3261,6 +3278,9 @@ CREATE TABLE `vendors` (
`vendor_name` varchar(200) NOT NULL,
`vendor_description` varchar(200) DEFAULT NULL,
`vendor_contact_name` varchar(200) DEFAULT NULL,
`vendor_contact_phone_country_code` varchar(10) DEFAULT NULL,
`vendor_contact_phone` varchar(200) DEFAULT NULL,
`vendor_contact_email` varchar(200) DEFAULT NULL,
`vendor_phone_country_code` varchar(10) DEFAULT NULL,
`vendor_phone` varchar(200) DEFAULT NULL,
`vendor_extension` varchar(200) DEFAULT NULL,

View File

@@ -23,6 +23,7 @@ require_once __DIR__ . '/functions/asset.php';
require_once __DIR__ . '/functions/contact.php';
require_once __DIR__ . '/functions/software.php';
require_once __DIR__ . '/functions/location.php';
require_once __DIR__ . '/functions/vendor.php';
require_once __DIR__ . '/functions/links.php';
require_once __DIR__ . '/functions/auth.php';
require_once __DIR__ . '/functions/logging.php';

View File

@@ -102,11 +102,21 @@ function getExportColumns($export_type) {
'vendors' => [
'vendor_name' => ['label' => 'Name', 'weight' => 2],
'vendor_description' => ['label' => 'Description', 'weight' => 3],
'vendor_contact_name' => ['label' => 'Contact Name'],
'vendor_phone' => ['label' => 'Phone', 'format' => 'phone'],
'vendor_tags' => ['label' => 'Tags', 'default' => false],
'vendor_phone' => ['label' => 'Support Phone', 'format' => 'phone'],
'vendor_extension' => ['label' => 'Extension', 'default' => false],
'vendor_email' => ['label' => 'Support Email', 'weight' => 2],
'vendor_website' => ['label' => 'Website', 'weight' => 2],
'vendor_hours' => ['label' => 'Hours'],
'vendor_sla' => ['label' => 'SLA', 'default' => false],
'vendor_account_number' => ['label' => 'Account Number'],
'vendor_code' => ['label' => 'PIN / Code', 'default' => false],
'vendor_contact_name' => ['label' => 'Account Manager'],
'vendor_contact_phone' => ['label' => 'Account Manager Phone', 'format' => 'phone', 'default' => false],
'vendor_contact_email' => ['label' => 'Account Manager Email', 'default' => false],
'open_case_count' => ['label' => 'Open Cases', 'format' => 'number', 'default' => false],
'vendor_notes' => ['label' => 'Notes', 'weight' => 3],
'client_name' => ['label' => 'Client', 'default' => false],
],
'assets' => [

View File

@@ -5,8 +5,9 @@
* contacts, licenses and vendors a document covers, the files attached to a
* document, the assets and contacts a file belongs to - and the seats on a
* license (the devices and users on it) and the credentials and files that
* go with it, and the documents and files about a location (a floor plan, a
* site runbook).
* go with it, the documents and files about a location (a floor plan, a
* site runbook), and a vendor's portal logins, documents and files (the
* contract, the escalation sheet).
*
* Every link is a row in a two-column table (asset_documents, document_files,
* contact_files...) and they all work the same way, so they are described once
@@ -55,6 +56,8 @@ const LINK_RELATIONS = [
'file|software' => ['table' => 'software_files', 'module' => 'module_support'],
'document|location' => ['table' => 'location_documents', 'module' => 'module_support'],
'file|location' => ['table' => 'location_files', 'module' => 'module_support'],
'credential|vendor' => ['table' => 'vendor_credentials', 'module' => 'module_credential'],
'file|vendor' => ['table' => 'vendor_files', 'module' => 'module_support'],
];
/** The relation between two types, or null when they cannot be linked. */
@@ -69,9 +72,12 @@ function linkRelation($a, $b) {
* a license's seats first - leaving out types this agent may not see.
*/
function linkTypesFor($from) {
$order = $from === 'software'
? ['asset', 'contact', 'credential', 'document', 'file']
: ['file', 'document', 'asset', 'contact', 'location', 'software', 'credential', 'vendor'];
$orders = [
'software' => ['asset', 'contact', 'credential', 'document', 'file'],
// A vendor's portal logins first - what is wanted on the phone to them
'vendor' => ['credential', 'document', 'file'],
];
$order = $orders[$from] ?? ['file', 'document', 'asset', 'contact', 'location', 'software', 'credential', 'vendor'];
return array_values(array_filter($order, function ($to) use ($from) {
$view = LINK_ENTITIES[$to]['view'] ?? null;
return $to !== $from && linkRelation($from, $to) && (!$view || lookupUserPermission($view) >= 1);
@@ -107,7 +113,7 @@ function linkDisplayColumns($type) {
'file' => 'file_ext, file_size, file_mime_type, file_description',
'software' => 'software_type, software_version, software_license_type, software_seats, software_expire, ' . softwareSeatsUsedSql('t.software_id') . ' AS seats_used',
'credential' => 'credential_description, credential_uri, credential_username, credential_otp_secret, credential_favorite',
'vendor' => 'vendor_description',
'vendor' => 'vendor_description, vendor_account_number, vendor_phone, vendor_phone_country_code, vendor_extension',
'location' => 'location_address, location_city, location_state, location_zip, location_primary, location_photo, location_client_id',
][$type] ?? '';
}
@@ -325,9 +331,9 @@ function linkRowHtml($to, $row, $client_id) {
$detail = locationAddressText($row);
break;
case 'vendor':
$lead = '<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-building"></i></span>';
$link = "<a class=\"ajax-modal text-truncate\" href=\"#\" data-modal-url=\"modals/vendor/vendor.php?id=$id\">$name</a>";
$detail = $row['vendor_description'];
$lead = vendorTile($row['name'], 'sm');
$link = "<a class=\"ajax-modal text-truncate\" href=\"#\" data-modal-size=\"lg\" data-modal-url=\"modals/vendor/vendor.php?id=$id\">$name</a>";
$detail = implode(' · ', array_filter([$row['vendor_description'], $row['vendor_phone'] ? formatPhoneNumber($row['vendor_phone'], $row['vendor_phone_country_code']) : '']));
break;
default:
return '';

View File

@@ -399,19 +399,20 @@ function globalSearchGroups($terms, $deep) {
$vendor_phone_match = $phone ? "OR REGEXP_REPLACE(vendor_phone, '[^0-9]', '') LIKE '%$phone%'" : '';
$groups[] = [
'key' => 'vendors', 'label' => 'Vendors', 'icon' => 'building', 'module' => 'module_client',
'select' => 'vendor_id, vendor_name, vendor_description, vendor_client_id, client_name',
'select' => 'vendor_id, vendor_name, vendor_description, vendor_phone, vendor_phone_country_code, vendor_client_id, client_name',
'from' => 'vendors LEFT JOIN clients ON vendor_client_id = client_id',
'where' => "vendor_archived_at IS NULL
AND (vendor_name LIKE '%$like%' OR vendor_description LIKE '%$like%' OR vendor_account_number LIKE '%$like%' $vendor_phone_match)",
AND (vendor_name LIKE '%$like%' OR vendor_description LIKE '%$like%' OR vendor_account_number LIKE '%$like%'
OR vendor_contact_name LIKE '%$like%' OR vendor_email LIKE '%$like%' OR vendor_contact_email LIKE '%$like%' $vendor_phone_match)",
'scope' => 'vendor_client_id',
'order' => "(vendor_name = '$eq') DESC, (vendor_name LIKE '$like%') DESC, vendor_name",
'order' => "(vendor_name = '$eq') DESC, (vendor_name LIKE '$like%') DESC, vendor_favorite DESC, vendor_name",
'list_url' => '/agent/vendors.php?' . http_build_query(['q' => $q]),
'item' => function ($row) {
return [
'title' => $row['vendor_name'],
'subtitle' => $row['vendor_description'],
'context' => $row['client_name'],
'url' => globalSearchClientUrl('vendors.php', $row['vendor_client_id'], ['q' => $row['vendor_name']]),
'subtitle' => implode(' · ', array_filter([$row['vendor_description'], $row['vendor_phone'] ? formatPhoneNumber($row['vendor_phone'], $row['vendor_phone_country_code']) : ''])),
'context' => intval($row['vendor_client_id']) ? $row['client_name'] : 'Our vendor',
'url' => globalSearchClientUrl('vendor.php', $row['vendor_client_id'], ['vendor_id' => intval($row['vendor_id'])]),
];
},
];

View File

@@ -218,7 +218,7 @@ function softwareRow($software_id) {
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software.*, client_id, client_name,
vendor_id, vendor_name, vendor_description, vendor_account_number, vendor_phone, vendor_phone_country_code, vendor_extension,
vendor_email, vendor_website, vendor_hours, vendor_sla, vendor_archived_at,
vendor_email, vendor_website, vendor_hours, vendor_sla, vendor_code, vendor_archived_at,
" . softwareSeatsUsedSql('software.software_id') . " AS seats_used
FROM software
LEFT JOIN clients ON client_id = software_client_id

View File

@@ -1,7 +1,7 @@
<?php
/*
* Tags on clients, locations, contacts, credentials and assets.
* Tags on clients, locations, contacts, credentials, assets and vendors.
*
* 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
@@ -10,7 +10,7 @@
* returns, so the page never reloads and nothing goes through a modal.
*/
/* What differs between the five kinds of tagged record. */
/* What differs between the six kinds of tagged record. */
function tagEntity($entity) {
$entities = [
'client' => [
@@ -43,6 +43,13 @@ function tagEntity($entity) {
'table' => 'assets', 'id' => 'asset_id', 'client' => 'asset_client_id', 'name' => 'asset_name',
'log' => 'Asset', 'list' => 'assets.php'
],
// A vendor of no client is one of ours - a supplier, which is the financial module's
'vendor' => [
'type' => 6, 'label' => 'vendor', 'module' => 'module_client', 'module_ours' => 'module_financial',
'link_table' => 'vendor_tags', 'link_record' => 'vendor_id', 'link_tag' => 'tag_id',
'table' => 'vendors', 'id' => 'vendor_id', 'client' => 'vendor_client_id', 'name' => 'vendor_name',
'log' => 'Vendor', 'list' => 'vendors.php'
],
];
return $entities[$entity] ?? null;
@@ -87,14 +94,15 @@ function tagsForRecord($entity, $record_id) {
* 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 = '') {
function tagPills($entity, $record_id, array $tags, $filter = '', $can_edit = null) {
$config = tagEntity($entity);
if (!$config) {
return '';
}
$record_id = intval($record_id);
$filter = tagFilterBase($entity, $filter);
$can_edit = lookupUserPermission($config['module']) >= 2;
// The caller can say who may edit when the module alone does not (one of our own vendors)
$can_edit = $can_edit === null ? lookupUserPermission($config['module']) >= 2 : (bool) $can_edit;
if (!$tags && !$can_edit) {
return '';
}
@@ -138,7 +146,12 @@ function tagRecordsEditable($entity, array $record_ids) {
$config = tagEntity($entity);
$record_ids = array_values(array_unique(array_filter(array_map('intval', $record_ids))));
if (!$config || !$record_ids || lookupUserPermission($config['module']) < 2) {
if (!$config || !$record_ids) {
return false;
}
$can_module = lookupUserPermission($config['module']) >= 2;
$can_ours = isset($config['module_ours']) && lookupUserPermission($config['module_ours']) >= 2;
if (!$can_module && !$can_ours) {
return false;
}
@@ -148,7 +161,11 @@ function tagRecordsEditable($entity, array $record_ids) {
FROM {$config['table']} WHERE {$config['id']} IN (" . implode(',', $record_ids) . ")"
);
while ($row = mysqli_fetch_assoc($sql)) {
if (!hasClientAccess(intval($row['client_id']))) {
if (intval($row['client_id']) === 0 && isset($config['module_ours'])) {
if (!$can_ours) {
return false;
}
} elseif (!$can_module || !hasClientAccess(intval($row['client_id']))) {
return false;
}
$records[intval($row['record_id'])] = ['name' => $row['record_name'], 'client_id' => intval($row['client_id'])];

443
functions/vendor.php Normal file
View File

@@ -0,0 +1,443 @@
<?php
/*
* Vendors - who a client (or we) buy from and call for support.
*
* A vendor with a client is that client's: their ISP, phone provider, copier
* company, line-of-business software maker. A tech calls them on the client's
* behalf, so the vendor is built around making that call (support line, hours,
* SLA, the account number and PIN they will ask for), the cases open with them
* (tickets with their own reference), and what came from them - licenses,
* assets, domains, services and the portal logins.
*
* A vendor with no client is one of ours - a distributor, the colo, the
* accountant - what expenses and recurring expenses are booked against. Ours
* are the financial module's to change; a client's are the client module's.
*/
/**
* What can point at a vendor, by a column on its own table. 'module' is the
* permission it takes to see them. Domains point at a vendor in four roles.
*/
const VENDOR_USES = [
'ticket' => ['table' => 'tickets', 'id' => 'ticket_id', 'columns' => ['ticket_vendor_id'], 'client' => 'ticket_client_id', 'archived' => 'ticket_archived_at', 'module' => 'module_support', 'label' => 'case', 'plural' => 'Cases', 'icon' => 'fa-life-ring'],
'software' => ['table' => 'software', 'id' => 'software_id', 'columns' => ['software_vendor_id'], 'client' => 'software_client_id', 'archived' => 'software_archived_at', 'module' => 'module_support', 'label' => 'license', 'plural' => 'Licenses', 'icon' => 'fa-cube'],
'asset' => ['table' => 'assets', 'id' => 'asset_id', 'columns' => ['asset_vendor_id'], 'client' => 'asset_client_id', 'archived' => 'asset_archived_at', 'module' => 'module_support', 'label' => 'asset', 'plural' => 'Assets', 'icon' => 'fa-desktop'],
'domain' => ['table' => 'domains', 'id' => 'domain_id', 'columns' => ['domain_registrar', 'domain_webhost', 'domain_dnshost', 'domain_mailhost'], 'client' => 'domain_client_id', 'archived' => 'domain_archived_at', 'module' => 'module_support', 'label' => 'domain', 'plural' => 'Domains', 'icon' => 'fa-globe'],
];
/** What each domain column makes the vendor - shown beside the domain. */
const VENDOR_DOMAIN_ROLES = ['domain_registrar' => 'Registrar', 'domain_webhost' => 'Web host', 'domain_dnshost' => 'DNS', 'domain_mailhost' => 'Mail'];
/** The module a vendor belongs to - a client's are the client module's, ours the financial module's. */
function vendorModule($client_id) {
return intval($client_id) ? 'module_client' : 'module_financial';
}
/**
* May this agent do this to a vendor of this client? 1 is to see it, 2 to
* change it, 3 to delete it. Anyone may look up one of our own vendors -
* their number is no secret - as it has always been.
*/
function vendorCan($client_id, $level) {
$client_id = intval($client_id);
if (!$client_id && $level <= 1) {
return true;
}
return lookupUserPermission(vendorModule($client_id)) >= $level && (!$client_id || hasClientAccess($client_id));
}
/** Stop here unless this agent may do this to a vendor of this client. */
function vendorEnforce($client_id, $level) {
$client_id = intval($client_id);
if ($client_id) {
enforceUserPermission('module_client', $level);
enforceClientAccess($client_id);
} elseif ($level >= 2) {
enforceUserPermission('module_financial', $level);
}
}
/** Up to two letters for a vendor's tile - "Keystone Business Fiber" is KB, "Netgate" is NE. */
function vendorInitials($name) {
$name = trim(html_entity_decode((string) $name, ENT_QUOTES, 'UTF-8'));
$words = array_values(array_filter(preg_split('/[\s\-\/&,.]+/u', $name, -1, PREG_SPLIT_NO_EMPTY), function ($word) {
return !in_array(mb_strtolower($word), ['the', 'and', 'of', 'inc', 'llc', 'ltd'], true) && preg_match('/^[\p{L}\p{N}]/u', $word);
}));
if (!$words) {
return $name === '' ? '?' : mb_strtoupper(mb_substr($name, 0, 1));
}
if (count($words) === 1) {
return mb_strtoupper(mb_substr($words[0], 0, 2));
}
return mb_strtoupper(mb_substr($words[0], 0, 1) . mb_substr($words[1], 0, 1));
}
/** A vendor's colour, by name - so a vendor many clients use is the same colour for all of them. */
function vendorTone($name) {
$tones = ['primary', 'success', 'info', 'warning', 'secondary'];
$key = mb_strtolower(trim(html_entity_decode((string) $name, ENT_QUOTES, 'UTF-8')));
return $tones[crc32($key) % count($tones)];
}
/** The vendor's initials on its colour, in the same rounded square as the asset and license tiles. $size 'xs' | 'sm' | '' | 'lg'. */
function vendorTile($name, $size = '') {
$size_class = in_array($size, ['xs', 'sm', 'lg'], true) ? " itflow-asset-icon-$size itflow-vendor-tile-$size" : '';
$tone = vendorTone($name);
return '<span class="itflow-asset-icon itflow-vendor-tile' . $size_class . ' bg-' . $tone . '-subtle text-' . $tone . '-emphasis" aria-hidden="true">' . escapeHtml(vendorInitials($name)) . '</span>';
}
/** A vendor in a picker - its tile, and a quieter line under it. */
function selectOptionVendor($name, $description = '') {
return selectOptionData(['initials' => vendorInitials($name), 'tone' => vendorTone($name), 'soft' => '1', 'description' => $description]);
}
/**
* A vendor wherever a page mentions one: its tile, then its name, opening
* its quick look - or its page with 'page' => true (then 'client_id' too).
* 'archived' strikes the name through; 'class' goes on the link.
* Takes the name raw or already escaped.
*/
function vendorLink($vendor_id, $name, $options = []) {
$vendor_id = intval($vendor_id);
$name = html_entity_decode((string) $name, ENT_QUOTES, 'UTF-8');
$label = escapeHtml($name !== '' ? $name : 'Unnamed vendor');
$archived = !empty($options['archived']);
$class = trim('text-truncate ' . ($options['class'] ?? '') . ($archived ? ' text-decoration-line-through text-body-secondary' : ''));
$title = $archived ? ' title="Archived vendor"' : '';
if (!empty($options['page'])) {
$client_id = intval($options['client_id'] ?? 0);
$href = 'vendor.php?' . ($client_id ? "client_id=$client_id&amp;" : '') . "vendor_id=$vendor_id";
$link = "<a class=\"$class\" href=\"$href\"$title>$label</a>";
} else {
$link = "<a class=\"ajax-modal $class\" href=\"#\" data-modal-size=\"lg\" data-modal-url=\"modals/vendor/vendor.php?id=$vendor_id\"$title>$label</a>";
}
return '<span class="itflow-contact-ref">' . vendorTile($name, 'xs') . $link . '</span>';
}
/** The vendor's website as a link - with https:// when it was saved without a scheme. Raw in, raw out; '' without one. */
function vendorWebsiteUrl($website) {
$website = trim(html_entity_decode((string) $website, ENT_QUOTES, 'UTF-8'));
if ($website === '') {
return '';
}
return preg_match('#^[a-z][a-z0-9+.-]*://#i', $website) ? $website : "https://$website";
}
/** The website without its scheme or a trailing slash - how it reads on a page. Raw in, raw out. */
function vendorWebsiteLabel($website) {
$website = trim(html_entity_decode((string) $website, ENT_QUOTES, 'UTF-8'));
return rtrim(preg_replace('#^https?://#i', '', $website), '/');
}
/** The support line to dial, with the country code - for a Call button. '' without one. */
function vendorDialNumber($phone, $country_code) {
$digits = preg_replace('/[^0-9]/', '', (string) $phone);
if ($digits === '') {
return '';
}
$country = preg_replace('/[^0-9]/', '', (string) $country_code);
return ($country !== '' ? "+$country" : '') . $digits;
}
/**
* A PIN or code as dots, with an eye to show it and a copy button that works
* either way - so it is not read off a shared screen. '' when there is none.
* Takes the value raw or already escaped.
*/
function vendorSecretHtml($value, $what = 'PIN') {
$value = escapeHtml(trim(html_entity_decode((string) $value, ENT_QUOTES, 'UTF-8')));
if ($value === '') {
return '';
}
return '<span class="itflow-secret"><span class="itflow-secret-mask" aria-hidden="true">&bull;&bull;&bull;&bull;&bull;&bull;</span><span class="itflow-secret-value">' . $value . '</span>'
. '<button type="button" class="itflow-secret-toggle" title="Show" aria-label="Show ' . $what . '" aria-pressed="false"><i class="fas fa-eye"></i></button>'
. '<button type="button" class="itflow-copy clipboardjs" title="Copy ' . $what . '" data-clipboard-text="' . $value . '"><i class="far fa-copy"></i></button></span>';
}
/** One vendor by id with its client and template names - raw, or null. */
function vendorRow($vendor_id) {
global $mysqli;
$vendor_id = intval($vendor_id);
if (!$vendor_id) {
return null;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT vendors.*, client_name, vendor_template_name
FROM vendors
LEFT JOIN clients ON client_id = vendor_client_id
LEFT JOIN vendor_templates ON vendor_templates.vendor_template_id = vendors.vendor_template_id
WHERE vendor_id = $vendor_id
LIMIT 1"));
return $row ?: null;
}
/**
* The vendor fields a New / Edit form posts, as a SQL SET list - phone
* numbers as digits, the website without its scheme, only a template that
* exists. Null when there is no name. Fields missing from the post are blank.
*/
function vendorFormSet($post) {
global $mysqli;
$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;
}
$template_id = intval($post['vendor_template_id'] ?? 0);
if ($template_id && !mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM vendor_templates WHERE vendor_template_id = $template_id"))) {
$template_id = 0;
}
$website = mb_substr(preg_replace('#^https?://#i', '', trim((string) ($post['website'] ?? ''))), 0, 200);
$fields = [
'vendor_name' => $text('name'),
'vendor_description' => $text('description'),
'vendor_account_number' => $text('account_number'),
'vendor_code' => $text('code'),
'vendor_contact_name' => $text('contact_name'),
'vendor_contact_phone_country_code' => $digits('contact_phone_country_code', 10),
'vendor_contact_phone' => $digits('contact_phone'),
'vendor_contact_email' => $text('contact_email'),
'vendor_phone_country_code' => $digits('phone_country_code', 10),
'vendor_phone' => $digits('phone'),
'vendor_extension' => $digits('extension'),
'vendor_email' => $text('email'),
'vendor_website' => "'" . escapeSql($website) . "'",
'vendor_hours' => $text('hours'),
'vendor_sla' => $text('sla'),
'vendor_notes' => $text('notes', 0),
'vendor_favorite' => intval($post['favorite'] ?? 0) ? 1 : 0,
'vendor_template_id' => $template_id,
];
$set = [];
foreach ($fields as $column => $value) {
$set[] = "$column = $value";
}
return implode(', ', $set);
}
/** Replace a vendor's tags with these - vendor tags (type 6) only. */
function vendorSetTags($vendor_id, $tag_ids) {
global $mysqli;
$vendor_id = intval($vendor_id);
mysqli_query($mysqli, "DELETE FROM vendor_tags WHERE vendor_id = $vendor_id");
$tag_ids = array_values(array_unique(array_filter(array_map('intval', (array) $tag_ids))));
if (!$tag_ids) {
return;
}
mysqli_query($mysqli, "INSERT IGNORE INTO vendor_tags (vendor_id, tag_id)
SELECT $vendor_id, tag_id FROM tags WHERE tag_type = 6 AND tag_id IN (" . implode(',', $tag_ids) . ")");
}
/** A template's fields as they go on a vendor - for New from template. Raw, or null. */
function vendorTemplateRow($template_id) {
global $mysqli;
$template_id = intval($template_id);
if (!$template_id) {
return null;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM vendor_templates WHERE vendor_template_id = $template_id AND vendor_template_archived_at IS NULL LIMIT 1"));
return $row ?: null;
}
/** A template as the fields of the vendor form - what New from template fills in. */
function vendorTemplateFormFields($template) {
return [
'name' => $template['vendor_template_name'] ?? '',
'description' => $template['vendor_template_description'] ?? '',
'account_number' => $template['vendor_template_account_number'] ?? '',
'code' => $template['vendor_template_code'] ?? '',
'contact_name' => $template['vendor_template_contact_name'] ?? '',
'phone_country_code' => $template['vendor_template_phone_country_code'] ?? '',
'phone' => $template['vendor_template_phone'] ?? '',
'extension' => $template['vendor_template_extension'] ?? '',
'email' => $template['vendor_template_email'] ?? '',
'website' => $template['vendor_template_website'] ?? '',
'hours' => $template['vendor_template_hours'] ?? '',
'sla' => $template['vendor_template_sla'] ?? '',
'notes' => $template['vendor_template_notes'] ?? '',
'vendor_template_id' => intval($template['vendor_template_id'] ?? 0),
];
}
/**
* How much each vendor is used, for the list's chips and the page's tabs:
* open cases and cases, licenses, assets, domains, services, logins,
* documents and files, and - for our own vendors - expenses, what was spent
* with them this year, and recurring expenses. Only what belongs to the vendor's own client counts, which is
* also what lets these use the client indexes.
*/
function vendorCounts($vendors) {
global $mysqli;
$counts = [];
$clients = [];
foreach ((array) $vendors as $vendor) {
$id = intval($vendor['vendor_id']);
if (!$id) {
continue;
}
$counts[$id] = ['open' => 0, 'ticket' => 0, 'software' => 0, 'asset' => 0, 'domain' => 0, 'service' => 0, 'credential' => 0, 'document' => 0, 'file' => 0, 'expense' => 0, 'recurring' => 0, 'spend_year' => 0.0];
$clients[intval($vendor['vendor_client_id'])] = true;
}
if (!$counts) {
return $counts;
}
$in = implode(',', array_keys($counts));
$client_in = implode(',', array_keys($clients));
$sql = mysqli_query($mysqli, "SELECT ticket_vendor_id, COUNT(*), COALESCE(SUM(ticket_resolved_at IS NULL AND ticket_closed_at IS NULL), 0)
FROM tickets WHERE ticket_client_id IN ($client_in) AND ticket_vendor_id IN ($in) GROUP BY ticket_vendor_id");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])]['ticket'] = intval($row[1]);
$counts[intval($row[0])]['open'] = intval($row[2]);
}
foreach (['software', 'asset'] as $kind) {
$use = VENDOR_USES[$kind];
$column = $use['columns'][0];
$sql = mysqli_query($mysqli, "SELECT $column, COUNT(*) FROM {$use['table']}
WHERE {$use['client']} IN ($client_in) AND $column IN ($in) AND {$use['archived']} IS NULL GROUP BY $column");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])][$kind] = intval($row[1]);
}
}
// A domain counts once for a vendor however many of its roles the vendor has
$sql = mysqli_query($mysqli, "SELECT domain_registrar, domain_webhost, domain_dnshost, domain_mailhost FROM domains
WHERE domain_client_id IN ($client_in) AND domain_archived_at IS NULL
AND (domain_registrar IN ($in) OR domain_webhost IN ($in) OR domain_dnshost IN ($in) OR domain_mailhost IN ($in))");
while ($row = mysqli_fetch_row($sql)) {
foreach (array_unique(array_map('intval', $row)) as $vendor_id) {
if (isset($counts[$vendor_id])) {
$counts[$vendor_id]['domain']++;
}
}
}
$joins = [
'service' => ['service_vendors', 'services', 'service_id', 'service_client_id IN (' . $client_in . ')'],
'credential' => ['vendor_credentials', 'credentials', 'credential_id', 'credential_archived_at IS NULL'],
'document' => ['vendor_documents', 'documents', 'document_id', 'document_archived_at IS NULL'],
'file' => ['vendor_files', 'files', 'file_id', 'file_archived_at IS NULL'],
];
foreach ($joins as $kind => [$link_table, $table, $id, $where]) {
$sql = mysqli_query($mysqli, "SELECT l.vendor_id, COUNT(*) FROM $link_table l JOIN $table t ON t.$id = l.$id
WHERE l.vendor_id IN ($in) AND $where GROUP BY l.vendor_id");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])][$kind] = intval($row[1]);
}
}
if (isset($clients[0])) {
$year = intval(date('Y'));
$sql = mysqli_query($mysqli, "SELECT expense_vendor_id, COUNT(*), COALESCE(SUM(IF(YEAR(expense_date) = $year, expense_amount, 0)), 0)
FROM expenses WHERE expense_vendor_id IN ($in) AND expense_archived_at IS NULL GROUP BY expense_vendor_id");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])]['expense'] = intval($row[1]);
$counts[intval($row[0])]['spend_year'] = floatval($row[2]);
}
$sql = mysqli_query($mysqli, "SELECT recurring_expense_vendor_id, COUNT(*) FROM recurring_expenses WHERE recurring_expense_vendor_id IN ($in) AND recurring_expense_archived_at IS NULL GROUP BY recurring_expense_vendor_id");
while ($row = mysqli_fetch_row($sql)) {
$counts[intval($row[0])]['recurring'] = intval($row[1]);
}
}
return $counts;
}
/**
* Why this vendor cannot be deleted, or '' if it can. Expenses, recurring
* expenses and a payment provider's fees are the books - those vendors are
* archived instead, so the books keep saying who was paid.
*/
function vendorDeleteBlocker($vendor_id) {
global $mysqli;
$vendor_id = intval($vendor_id);
$reasons = [];
$expenses = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM expenses WHERE expense_vendor_id = $vendor_id"))[0]);
if ($expenses) {
$reasons[] = $expenses === 1 ? '1 expense' : "$expenses expenses";
}
$recurring = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM recurring_expenses WHERE recurring_expense_vendor_id = $vendor_id"))[0]);
if ($recurring) {
$reasons[] = $recurring === 1 ? '1 recurring expense' : "$recurring recurring expenses";
}
$providers = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM payment_providers WHERE payment_provider_expense_vendor = $vendor_id"))[0]);
if ($providers) {
$reasons[] = 'a payment provider\'s fees';
}
return $reasons ? 'it has ' . implode(' and ', $reasons) . ' booked to it - archive it instead' : '';
}
/**
* Before a vendor is deleted for good: everything that pointed at it points
* at no vendor instead of an id that no longer exists. Its tags, links to
* documents, files, logins and services go with it (the link tables cascade).
*/
function vendorClearReferences($vendor_id) {
global $mysqli;
$vendor_id = intval($vendor_id);
foreach (VENDOR_USES as $use) {
foreach ($use['columns'] as $column) {
mysqli_query($mysqli, "UPDATE {$use['table']} SET $column = 0 WHERE $column = $vendor_id");
}
}
}
/**
* The same vendor at the agent's other clients - by name, or made from the
* same template - so an outage at an ISP can be checked against every
* client that uses it. Archived vendors and clients are left out.
*/
function vendorElsewhere($vendor, $limit = 50) {
global $mysqli;
$vendor_id = intval($vendor['vendor_id']);
$name = escapeSql(mb_strtolower(trim((string) $vendor['vendor_name'])));
$template_id = intval($vendor['vendor_template_id'] ?? 0);
$match = "LOWER(TRIM(vendor_name)) = '$name'" . ($template_id ? " OR vendor_template_id = $template_id" : '');
$sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_account_number, vendor_client_id, client_name
FROM vendors
JOIN clients ON client_id = vendor_client_id
WHERE vendor_id != $vendor_id AND vendor_client_id > 0 AND vendor_archived_at IS NULL AND client_archived_at IS NULL
AND ($match) " . clientScopeSql('vendor_client_id') . "
ORDER BY client_name ASC
LIMIT " . max(1, intval($limit)));
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = $row;
}
return $rows;
}
/** What we have spent with one of our vendors: this year, last year and in all, from unarchived expenses. */
function vendorSpend($vendor_id) {
global $mysqli;
$vendor_id = intval($vendor_id);
$year = intval(date('Y'));
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT
COALESCE(SUM(IF(YEAR(expense_date) = $year, expense_amount, 0)), 0),
COALESCE(SUM(IF(YEAR(expense_date) = " . ($year - 1) . ", expense_amount, 0)), 0),
COALESCE(SUM(expense_amount), 0),
COUNT(*),
MAX(expense_date)
FROM expenses WHERE expense_vendor_id = $vendor_id AND expense_archived_at IS NULL"));
return ['year' => floatval($row[0]), 'last_year' => floatval($row[1]), 'total' => floatval($row[2]), 'count' => intval($row[3]), 'last' => $row[4]];
}
/** Who did what to a vendor - its audit log entries, newest first. */
function vendorHistory($vendor_id, $limit = 50) {
global $mysqli;
$vendor_id = intval($vendor_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 = 'Vendor' AND log_entity_id = $vendor_id
ORDER BY log_id DESC LIMIT $limit");
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = $row;
}
return $rows;
}

View File

@@ -774,6 +774,23 @@ itflowBindOnce('itflowTinyMceFocus', 'focusin', '.tox-tinymce-aux', function (e)
e.stopImmediatePropagation();
});
// A PIN or code shown as dots (.itflow-secret) - the eye beside it shows it, and again hides it
itflowBindOnce('itflowSecret', 'click', '.itflow-secret-toggle', function (e) {
e.preventDefault();
const secret = this.closest('.itflow-secret');
if (!secret) {
return;
}
const shown = secret.classList.toggle('is-shown');
const icon = this.querySelector('i');
if (icon) {
icon.classList.toggle('fa-eye', !shown);
icon.classList.toggle('fa-eye-slash', shown);
}
this.setAttribute('title', shown ? 'Hide' : 'Show');
this.setAttribute('aria-pressed', shown ? 'true' : 'false');
});
itflowBindOnce('itflowAllDay', 'change', '.event-all-day-toggle', function () {
const allDay = this.checked;