Files
itflow/agent/contact.php

892 lines
59 KiB
PHP

<?php
/*
* Contact - one person at a client.
*
* Built like the asset page (agent/asset.php): who they are in the header,
* with a click to call or email them; how to reach them and the details a
* tech needs on a call (location, PIN, portal access) down the side; and
* everything linked to them - assets, credentials, tickets, licenses,
* documents, files, services, notes - and their history in tabs with counts. The open tab
* follows the URL hash (#tickets) 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";
$client_query = "AND contact_client_id = $client_id";
$client_url = "client_id=$client_id&";
} else {
require_once "includes/inc_client_overview_all.php";
$client_query = '';
$client_url = '';
}
// Perms
enforceUserPermission('module_client');
$contact_id = intval($_GET['contact_id'] ?? 0);
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, contact_archived_at, contact_billing, contact_client_id, contact_created_at,
contact_department, contact_email, contact_extension, contact_id, contact_important, contact_location_id, contact_mobile,
contact_mobile_country_code, contact_name, contact_notes, contact_phone, contact_phone_country_code, contact_photo, contact_pin,
contact_primary, contact_technical, contact_title, contact_updated_at, contact_user_id,
contact_ticket_emails, contact_company_ticket_emails, location_archived_at, location_city, location_name, location_state, user_auth_method, user_token
FROM contacts
LEFT JOIN clients ON client_id = contact_client_id
LEFT JOIN locations ON location_id = contact_location_id
LEFT JOIN users ON user_id = contact_user_id
WHERE contact_id = $contact_id
" . clientScopeSql('contact_client_id') . "
$client_query
LIMIT 1
");
if (mysqli_num_rows($sql) == 0) {
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>";
require_once "../includes/footer.php";
exit();
}
$contact_row = mysqli_fetch_assoc($sql); // raw, for the helpers - $row gets reused below
$row = $contact_row;
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
$contact_name = escapeHtml($row['contact_name']);
$contact_title = escapeHtml($row['contact_title']);
$contact_department = escapeHtml($row['contact_department']);
$contact_email = escapeHtml($row['contact_email']);
$contact_pin = escapeHtml($row['contact_pin']);
$contact_notes = escapeHtml($row['contact_notes']);
$contact_primary = intval($row['contact_primary']);
$contact_archived_at = $row['contact_archived_at'];
$contact_created_at = escapeHtml($row['contact_created_at']);
$contact_phone_link = contactPhoneLink($row['contact_phone'], $row['contact_phone_country_code'], $row['contact_extension']);
$contact_mobile_link = contactPhoneLink($row['contact_mobile'], $row['contact_mobile_country_code']);
$contact_phone_copy = escapeHtml(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
$contact_mobile_copy = escapeHtml(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
$contact_dial = $row['contact_phone'] ? 'phone' : ($row['contact_mobile'] ? 'mobile' : '');
$contact_portal = contactPortalAccess($row['contact_user_id'], $row['user_auth_method']);
$contact_portal_mfa = $contact_portal === 'Password' && trim((string) $row['user_token']) !== '';
$contact_portal_last = $contact_portal ? portalContactLastSignIn($row['contact_user_id']) : null;
// How they joined, when it was by an invite or signing up (functions/portal_invite.php)
$contact_portal_joined = $contact_portal ? portalContactJoined($contact_id) : null;
// View as contact - read access to clients and support is checked again when it starts (functions/portal.php)
$can_view_as = $config_client_portal_enable && !$row['contact_archived_at'] && lookupUserPermission('module_support') >= 1;
$contact_role_line = implode(' &middot; ', array_filter([$contact_title, $contact_department]));
$can_edit = lookupUserPermission('module_client') >= 2;
$can_delete = lookupUserPermission('module_client') >= 3;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$can_credentials_edit = lookupUserPermission('module_credential') >= 2;
$can_support = lookupUserPermission('module_support') >= 1;
$can_support_edit = lookupUserPermission('module_support') >= 2;
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
$page_title = $row['contact_name'];
// Tags - functions/tags.php
$contact_tags_display = tagPills('contact', $contact_id, tagsForRecord('contact', $contact_id), "contacts.php?client_id=$client_id&");
// Assets assigned to them
$sql_related_assets = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_description, asset_type, asset_make, asset_model, asset_serial,
asset_status, asset_favorite, interface_ip
FROM assets
LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1
WHERE asset_contact_id = $contact_id
AND asset_archived_at IS NULL
ORDER BY asset_name ASC"
);
$asset_count = mysqli_num_rows($sql_related_assets);
// Credentials - only for agents who may see them
$related_credentials = $can_credentials ? credentialRows("credential_contact_id = $contact_id AND credential_client_id = $client_id") : [];
$credential_count = count($related_credentials);
// Tickets they raised or are the contact on - open ones first
$related_tickets = [];
$ticket_open_count = 0;
if ($can_support) {
$related_tickets = ticketRows("tickets.ticket_contact_id = $contact_id");
$ticket_open_count = count(array_filter(array_column($related_tickets, 'is_open')));
}
$ticket_count = count($related_tickets);
// Recurring tickets
$recurring_ticket_count = 0;
if ($can_support) {
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_ticket_id, recurring_ticket_subject, recurring_ticket_priority,
recurring_ticket_frequency, recurring_ticket_next_run, recurring_ticket_paused_at
FROM recurring_tickets
WHERE recurring_ticket_contact_id = $contact_id
ORDER BY recurring_ticket_next_run ASC"
);
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
}
// Licenses they hold a seat on, with each one's seats in use and the key they were given
$held_software = $can_support ? softwareHeldBy('contact', $contact_id) : [];
$software_count = count($held_software);
// Documents
$sql_linked_documents = mysqli_query($mysqli, "SELECT documents.document_id, document_name, document_description, document_created_at,
document_updated_at, user_name
FROM contact_documents
JOIN documents ON documents.document_id = contact_documents.document_id
LEFT JOIN users ON document_created_by = user_id
WHERE contact_documents.contact_id = $contact_id
AND document_archived_at IS NULL
ORDER BY document_name ASC"
);
$document_count = mysqli_num_rows($sql_linked_documents);
// Files
$sql_linked_files = mysqli_query($mysqli, "SELECT files.file_id, file_name, file_description, file_ext, file_size, file_created_at
FROM contact_files
JOIN files ON files.file_id = contact_files.file_id
WHERE contact_files.contact_id = $contact_id
AND file_archived_at IS NULL
ORDER BY file_name ASC"
);
$file_count = mysqli_num_rows($sql_linked_files);
// Services
// The services they are a contact for - most important first
$linked_services = servicesFor('contact', $contact_id);
$service_count = count($linked_services);
// Notes
$sql_related_notes = mysqli_query($mysqli, "SELECT contact_note, contact_note_created_at, contact_note_id, contact_note_type, user_name
FROM contact_notes
LEFT JOIN users ON contact_note_created_by = user_id
WHERE contact_note_contact_id = $contact_id
AND contact_note_archived_at IS NULL
ORDER BY contact_note_created_at DESC"
);
$note_count = mysqli_num_rows($sql_related_notes);
// Note type icons, read from the categories list so the seeded icons stay the single source of truth
$note_type_icons = [];
$sql_note_type_icons = mysqli_query($mysqli, "SELECT category_name, category_icon FROM categories WHERE category_type = 'contact_note_type'");
while ($row = mysqli_fetch_assoc($sql_note_type_icons)) {
$note_type_icons[escapeHtml($row['category_name'])] = escapeHtml($row['category_icon']);
}
// The tabs, in the order a tech reaches for them
// Its history, from the audit log
$history = auditLogHistory('Contact', $contact_id);
$contact_tabs = [
'assets' => ['label' => 'Assets', 'icon' => 'fa-desktop', 'count' => $asset_count],
'credentials' => ['label' => 'Credentials', 'icon' => 'fa-key', 'count' => $credential_count],
'tickets' => ['label' => 'Tickets', 'icon' => 'fa-life-ring', 'count' => $ticket_count, 'badge' => $ticket_open_count],
'recurring' => ['label' => 'Recurring', 'icon' => 'fa-redo-alt', 'count' => $recurring_ticket_count],
'licenses' => ['label' => 'Licenses', 'icon' => 'fa-cube', 'count' => $software_count],
'documents' => ['label' => 'Documents', 'icon' => 'fa-file-alt', 'count' => $document_count],
'files' => ['label' => 'Files', 'icon' => 'fa-paperclip', 'count' => $file_count],
'services' => ['label' => 'Services', 'icon' => 'fa-stream', 'count' => $service_count],
'notes' => ['label' => 'Notes', 'icon' => 'fa-sticky-note', 'count' => $note_count],
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => $history['count']],
];
if (!$can_credentials) {
unset($contact_tabs['credentials']);
}
if (!$can_support) {
// Assets and documentation are Support's too
unset($contact_tabs['assets'], $contact_tabs['tickets'], $contact_tabs['recurring'], $contact_tabs['licenses'],
$contact_tabs['documents'], $contact_tabs['files'], $contact_tabs['services']);
}
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="client_overview.php?client_id=<?= $client_id ?>"><?= $client_name ?></a>
</li>
<li class="breadcrumb-item">
<a href="contacts.php?client_id=<?= $client_id ?>">Contacts</a>
</li>
<li class="breadcrumb-item active"><?= $contact_name ?></li>
</ol>
<!-- Who they are -->
<div class="card mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?php if ($contact_row['contact_photo']) { ?>
<a href="<?= "../uploads/clients/$client_id/" . escapeHtml($contact_row['contact_photo']) ?>" target="_blank" rel="noopener" title="Open photo">
<?= contactAvatar($client_id, $contact_id, $contact_row['contact_name'], $contact_row['contact_photo'], 'lg') ?>
</a>
<?php } else { ?>
<?= contactAvatar($client_id, $contact_id, $contact_row['contact_name'], '', 'lg') ?>
<?php } ?>
<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><?= $contact_name ?></span>
<span class="fs-6 fw-normal d-inline-flex flex-wrap gap-1"><?= contactFlagPills($contact_row) ?></span>
<?php if ($contact_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
</h1>
<?php if ($contact_role_line) { ?>
<div class="text-body-secondary"><?= $contact_role_line ?></div>
<?php } ?>
<?php if ($contact_tags_display) { ?>
<div class="mt-2"><?= $contact_tags_display ?></div>
<?php } ?>
</div>
<div class="d-flex flex-wrap gap-2 d-print-none">
<?php if ($contact_dial === 'phone') { ?>
<a class="btn btn-outline-primary" href="tel:<?= escapeHtml(phoneDialNumber($contact_row['contact_phone'], $contact_row['contact_phone_country_code']) . ($contact_row['contact_extension'] ? ',' . preg_replace('/[^0-9]/', '', $contact_row['contact_extension']) : '')) ?>" title="Call <?= $contact_phone_copy ?>">
<i class="fas fa-phone me-2"></i>Call
</a>
<?php } elseif ($contact_dial === 'mobile') { ?>
<a class="btn btn-outline-primary" href="tel:<?= escapeHtml(phoneDialNumber($contact_row['contact_mobile'], $contact_row['contact_mobile_country_code'])) ?>" title="Call <?= $contact_mobile_copy ?>">
<i class="fas fa-mobile-alt me-2"></i>Call
</a>
<?php } ?>
<?php if ($contact_email) { ?>
<a class="btn btn-outline-primary" href="mailto:<?= $contact_email ?>" title="Email <?= $contact_email ?>">
<i class="far fa-envelope me-2"></i>Email
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<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 ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>" data-modal-size="lg">
<i class="fa fa-fw fa-life-ring text-secondary me-2"></i>Ticket
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/recurring_ticket/recurring_ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>" data-modal-size="lg">
<i class="fa fa-fw fa-redo-alt text-secondary me-2"></i>Recurring Ticket
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/calendar/calendar_event_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>&lock_client=1" data-modal-size="lg">
<i class="far fa-fw fa-calendar-plus text-secondary me-2"></i>Meeting
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fa fa-fw fa-desktop text-secondary me-2"></i>Asset
</a>
<?php } ?>
<?php if ($can_credentials_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/credential/credential_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Credential
</a>
<?php } ?>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/document/document_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>" data-modal-size="xl">
<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_url ?>&contact_id=<?= $contact_id ?>">
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&contact_id=<?= $contact_id ?>">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_note_add.php?id=<?= $contact_id ?>">
<i class="fas fa-fw fa-sticky-note text-secondary me-2"></i>Note
</a>
</div>
</div>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-link me-2"></i>Link</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_asset.php?id=<?= $contact_id ?>">
<i class="fa fa-fw fa-desktop text-secondary me-2"></i>Asset
</a>
<?php if ($can_credentials_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_credential.php?id=<?= $contact_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Credential
</a>
<?php } ?>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/link/link.php?from=contact&id=<?= $contact_id ?>&to=software">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
</a>
<?php } ?>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_service.php?id=<?= $contact_id ?>">
<i class="fa fa-fw fa-stream text-secondary me-2"></i>Service
</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_document.php?id=<?= $contact_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/contact/contact_link_file.php?id=<?= $contact_id ?>">
<i class="fa fa-fw fa-paperclip text-secondary me-2"></i>File
</a>
</div>
</div>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/contact/contact_edit.php?id=<?= $contact_id ?>">
<i class="fas fa-user-edit me-2"></i>Edit
</button>
<?php if (!$contact_primary) { ?>
<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 ($contact_archived_at) { ?>
<a class="dropdown-item confirm-link" href="post.php?restore_contact=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_contact=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?anonymize_contact=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-user-secret me-2"></i>Anonymize &amp; Archive
</a>
<?php } ?>
<?php } ?>
<?php if ($can_delete && $config_destructive_deletes_enable) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_contact=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
<?php } ?>
</div>
</div>
</div>
<div class="row g-3">
<!-- How to reach them, and what a tech needs on a call -->
<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-phone-alt text-secondary me-2"></i>Reach</h3>
</div>
<div class="card-body">
<?php if ($contact_phone_link || $contact_mobile_link || $contact_email) { ?>
<dl class="itflow-asset-dl mb-0">
<?php if ($contact_phone_link) { ?>
<dt>Phone</dt>
<dd><?= $contact_phone_link ?><button type="button" class="itflow-copy clipboardjs" title="Copy phone" data-clipboard-text="<?= $contact_phone_copy ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($contact_mobile_link) { ?>
<dt>Mobile</dt>
<dd><?= $contact_mobile_link ?><button type="button" class="itflow-copy clipboardjs" title="Copy mobile" data-clipboard-text="<?= $contact_mobile_copy ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($contact_email) { ?>
<dt>Email</dt>
<dd class="d-flex align-items-center min-w-0"><a class="text-truncate" href="mailto:<?= $contact_email ?>" title="<?= $contact_email ?>"><?= $contact_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $contact_email ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
</dl>
<?php } else { ?>
<span class="text-body-secondary">No phone or email recorded.</span>
<?php } ?>
</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>Details</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Location</dt>
<dd>
<?php if ($can_edit && !$contact_archived_at) { ?>
<?= quickPick('contact_location', $contact_id, (string) intval($contact_row['contact_location_id']), contactLocationHtml($contact_row, 'detail'), 'detail', 0, $client_id) ?>
<?php } else { ?>
<?= contactLocationHtml($contact_row, 'detail') ?>
<?php } ?>
</dd>
<?php if ($contact_pin) { ?>
<dt>PIN</dt>
<dd><span class="font-monospace"><?= $contact_pin ?></span> <span class="small text-body-secondary">to verify them on a call</span></dd>
<?php } ?>
<dt>Portal</dt>
<dd>
<?= $contact_portal ? "Can log in &middot; <span class='text-body-secondary'>$contact_portal" . ($contact_portal_mfa ? ' and a code' : '') . "</span>" : '<span class="text-body-secondary">No access</span>' ?>
<?php if ($contact_portal) { ?>
<div class="small text-body-secondary"><?= $contact_portal_last ? 'Last signed in <span title="' . escapeHtml($contact_portal_last) . '">' . timeAgo($contact_portal_last) . '</span>' : 'Has not signed in yet' ?></div>
<?php } ?>
<?php if ($contact_portal_joined) { ?>
<div class="small text-body-secondary" data-contact-joined><?= $contact_portal_joined['kind'] === 'signup' ? 'Signed up themselves' : 'Joined from ' . ($contact_portal_joined['by'] !== '' ? escapeHtml($contact_portal_joined['by']) . "'s invite" : 'an invite') ?> on <?= date('M j, Y', strtotime($contact_portal_joined['at'])) ?></div>
<?php } ?>
<?php if ($can_view_as || ($contact_portal_mfa && $can_edit)) { ?>
<div class="d-flex flex-wrap gap-2 mt-1 small d-print-none">
<?php if ($can_view_as) { ?>
<a href="post.php?portal_view_as=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" target="_blank" rel="noopener" title="Open the client portal as <?= $contact_name ?> sees it - read only, in a new tab"><i class="fas fa-eye me-1"></i>View portal as them</a>
<?php } ?>
<?php if ($contact_portal_mfa && $can_edit) { ?>
<a class="text-danger confirm-link" href="post.php?reset_contact_portal_mfa=<?= $contact_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" data-confirm-text="<?= $contact_name ?> will sign in with their password alone until they set it up again. For a lost phone." title="For a lost phone - they can set it up again from Your account"><i class="fas fa-mobile-alt me-1"></i>Turn off two-step sign-in</a>
<?php } ?>
</div>
<?php } ?>
</dd>
<?php if ($contact_portal || $contact_row['contact_ticket_emails'] !== 'all' || $contact_row['contact_company_ticket_emails'] !== 'none') { ?>
<dt>Ticket emails</dt>
<dd>
<?= escapeHtml(PORTAL_TICKET_EMAILS[$contact_row['contact_ticket_emails']]['label'] ?? 'Every email') ?>
<?php if (($contact_primary || intval($contact_row['contact_technical'])) && $contact_row['contact_company_ticket_emails'] !== 'none') { ?>
<div class="small text-body-secondary">And about the company's tickets: <?= escapeHtml(strtolower(PORTAL_COMPANY_TICKET_EMAILS[$contact_row['contact_company_ticket_emails']]['label'] ?? '')) ?></div>
<?php } ?>
</dd>
<?php } ?>
<dt>Added</dt>
<dd title="<?= $contact_created_at ?>"><?= timeAgo($contact_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-pen text-secondary me-2"></i>Quick notes</h3>
</div>
<textarea class="form-control border-0 rounded-0 rounded-bottom" rows="5" id="contactNotes" placeholder="Personal tidbits, how they like to be contacted - saves when you click away" onblur="updateContactNotes(<?= $contact_id ?>)" <?= $can_edit ? '' : 'readonly' ?>><?= $contact_notes ?></textarea>
</div>
</div>
<!-- Everything linked to 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 ($contact_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab['count'] ? '' : 'is-empty' ?>" id="contact-tab-<?= $tab_key ?>" data-bs-toggle="pill" data-bs-target="#contact-pane-<?= $tab_key ?>" data-contact-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 ($can_support) { ?>
<!-- Assets -->
<div class="tab-pane fade" id="contact-pane-assets" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Assets assigned to <?= $contact_name ?>.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_asset.php?id=<?= $contact_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Asset
</button>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($asset_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr><th>Asset</th><th>Model / Serial</th><th>IP</th><th>Status</th><th></th></tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_assets)) {
$asset_id = intval($row['asset_id']);
$asset_serial = escapeHtml($row['asset_serial']);
$asset_ip = escapeHtml($row['interface_ip']);
$asset_make_model = escapeHtml(trim($row['asset_make'] . ' ' . $row['asset_model'])); ?>
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-<?= assetTypeTone($row['asset_type']) ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= getAssetIcon($row['asset_type']) ?>"></i></span>
<div class="min-w-0">
<a class="fw-semibold text-body ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= $asset_id ?>"><?= escapeHtml($row['asset_name']) ?></a>
<?php if ($row['asset_favorite']) { ?><i class="fas fa-star text-warning ms-1" title="Favorite"></i><?php } ?>
<div class="small text-body-secondary"><?= escapeHtml($row['asset_type']) ?></div>
</div>
</div>
</td>
<td>
<div><?= $asset_make_model ?: '<span class="text-body-secondary">-</span>' ?></div>
<?php if ($asset_serial) { ?><div class="small text-body-secondary text-nowrap"><span class="font-monospace"><?= $asset_serial ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy serial" data-clipboard-text="<?= $asset_serial ?>"><i class="far fa-copy"></i></button></div><?php } ?>
</td>
<td class="text-nowrap">
<?php if ($asset_ip) { ?><span class="font-monospace"><?= $asset_ip ?></span><?php if ($asset_ip !== 'DHCP') { ?><button type="button" class="itflow-copy clipboardjs" title="Copy IP" data-clipboard-text="<?= $asset_ip ?>"><i class="far fa-copy"></i></button><?php } ?><?php } else { ?><span class="text-body-secondary">-</span><?php } ?>
</td>
<td><?= assetStatusPill($row['asset_status']) ?: '<span class="text-body-secondary">-</span>' ?></td>
<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" href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-fw fa-arrow-right text-secondary me-2"></i>Open
</a>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset_edit.php?id=<?= $asset_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="post.php?unlink_asset_from_contact&contact_id=<?= $contact_id ?>&asset_id=<?= $asset_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-unlink text-secondary me-2"></i>Unlink
</a>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No assets assigned.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_credentials) { ?>
<!-- Credentials -->
<div class="tab-pane fade" id="contact-pane-credentials" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Their logins. Passwords and codes are fetched only when you ask for them.</span>
<?php if ($can_edit && $can_credentials_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_credential.php?id=<?= $contact_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/credential/credential_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Credential
</button>
</div>
<?php } ?>
</div>
<?php if ($related_credentials) { ?>
<?= credentialTableHtml($related_credentials, [
'unlink' => function ($row) use ($contact_id, $can_edit) {
return $can_edit && credentialCan(2) ? "post.php?unlink_credential_from_contact&contact_id=$contact_id&credential_id=" . intval($row['credential_id']) . "&csrf_token=" . $_SESSION['csrf_token'] : '';
},
]) ?>
<?php } else { ?>
<div class="itflow-asset-empty">No credentials for <?= $contact_name ?>.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_support) { ?>
<!-- Tickets -->
<div class="tab-pane fade" id="contact-pane-tickets" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Tickets they are the contact on. Open ones first.</span>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Ticket
</button>
<?php } ?>
</div>
<?php if ($related_tickets) { ?>
<?= ticketTableHtml($related_tickets) ?>
<?php } else { ?>
<div class="itflow-asset-empty">No tickets yet.</div>
<?php } ?>
</div>
<!-- Recurring tickets -->
<div class="tab-pane fade" id="contact-pane-recurring" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Tickets that open on a schedule with them as the contact.</span>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Recurring Ticket
</button>
<?php } ?>
</div>
<?php if ($recurring_ticket_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr><th>Subject</th><th>Priority</th><th>Frequency</th><th>Next run</th><th></th></tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_recurring_tickets)) {
$recurring_ticket_id = intval($row['recurring_ticket_id']); ?>
<tr>
<td>
<?php if ($can_support_edit) { ?>
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_edit.php?id=<?= $recurring_ticket_id ?>"><?= escapeHtml($row['recurring_ticket_subject']) ?></a>
<?php } else { ?>
<?= escapeHtml($row['recurring_ticket_subject']) ?>
<?php } ?>
</td>
<td><?= ticketPriorityHtml($row['recurring_ticket_priority'], 'detail') ?></td>
<td><?= escapeHtml($row['recurring_ticket_frequency']) ?></td>
<td class="text-nowrap"><?= recurringTicketNextRunPill($row) ?> <span class="small text-body-secondary ms-1"><?= escapeHtml($row['recurring_ticket_next_run']) ?></span></td>
<td class="text-end">
<?php if ($can_support_edit) { ?>
<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-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_edit.php?id=<?= $recurring_ticket_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<a class="dropdown-item confirm-link" href="post.php?force_recurring_ticket=<?= $recurring_ticket_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="A ticket is raised now, and the next run moves on a step - this takes its place.">
<i class="fas fa-fw fa-paper-plane text-secondary me-2"></i>Raise now
</a>
<?php if (lookupUserPermission('module_support') >= 3) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_recurring_ticket=<?= $recurring_ticket_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No recurring tickets.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_support) { ?>
<!-- Licenses -->
<div class="tab-pane fade" id="contact-pane-licenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Licenses they hold a seat on, and the key they were given.</span>
<?php if ($can_support_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/link/link.php?from=contact&id=<?= $contact_id ?>&to=software">
<i class="fas fa-link me-2"></i>Link
</button>
<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 ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New License
</button>
</div>
<?php } ?>
</div>
<?php if ($software_count) { ?>
<?= softwareHeldTable($held_software, 'contact', $contact_id, $can_support_edit) ?>
<?php } else { ?>
<div class="itflow-asset-empty">No licenses.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_support) { ?>
<!-- Documents -->
<div class="tab-pane fade" id="contact-pane-documents" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Documentation about or for them.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_document.php?id=<?= $contact_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="xl" data-modal-url="modals/document/document_add.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Document
</button>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($document_count) { ?>
<div class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_linked_documents)) {
$document_id = intval($row['document_id']);
$document_last = $row['document_updated_at'] ?: $row['document_created_at']; ?>
<div class="list-group-item d-flex align-items-center gap-3 py-2">
<?= documentTile('sm') ?>
<div class="flex-grow-1 min-w-0">
<a class="fw-semibold" href="document.php?client_id=<?= $client_id ?>&document_id=<?= $document_id ?>"><?= escapeHtml($row['document_name']) ?></a>
<?php if ($row['document_description']) { ?><div class="small text-body-secondary text-truncate"><?= escapeHtml($row['document_description']) ?></div><?php } ?>
</div>
<span class="small text-body-secondary text-nowrap" title="<?= escapeHtml($document_last) ?>"><?= escapeHtml($row['user_name']) ?> &middot; <?= timeAgo($document_last) ?></span>
<a class="btn btn-sm btn-light ajax-modal" href="#" title="View" data-modal-size="lg" data-modal-url="modals/document/document_view.php?id=<?= $document_id ?>"><i class="fas fa-fw fa-eye"></i></a>
<?php if ($can_support_edit) { ?>
<a class="btn btn-sm btn-light" title="Unlink" href="post.php?unlink_contact_from_document&contact_id=<?= $contact_id ?>&document_id=<?= $document_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-unlink"></i></a>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No documents.</div>
<?php } ?>
</div>
<!-- Files -->
<div class="tab-pane fade" id="contact-pane-files" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Files linked to them.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_file.php?id=<?= $contact_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/file/file_upload.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
<i class="fas fa-upload me-2"></i>Upload
</button>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($file_count) { ?>
<div class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_linked_files)) {
$file_id = intval($row['file_id']); ?>
<div class="list-group-item d-flex align-items-center gap-3 py-2">
<?= fileTypeTile($row['file_ext'], 'sm') ?>
<div class="flex-grow-1 min-w-0">
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/file/file.php?id=<?= $file_id ?>"><?= escapeHtml($row['file_name']) ?></a>
<?php if ($row['file_description']) { ?><div class="small text-body-secondary text-truncate"><?= escapeHtml($row['file_description']) ?></div><?php } ?>
</div>
<span class="small text-body-secondary text-nowrap" title="<?= escapeHtml($row['file_created_at']) ?>"><?= formatBytes($row['file_size']) ?> &middot; <?= timeAgo($row['file_created_at']) ?></span>
<?php if ($can_edit) { ?>
<a class="btn btn-sm btn-light" title="Unlink" href="post.php?unlink_contact_from_file&contact_id=<?= $contact_id ?>&file_id=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-unlink"></i></a>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No files.</div>
<?php } ?>
</div>
<!-- Services -->
<div class="tab-pane fade" id="contact-pane-services" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Services they own or are the contact for at the client.</span>
<?php if ($can_support_edit) { ?>
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_service.php?id=<?= $contact_id ?>">
<i class="fas fa-link me-2"></i>Link Service
</button>
<?php } ?>
</div>
<?php if ($service_count) { ?>
<?= serviceRowsHtml($linked_services, $can_support_edit ? function ($row) use ($contact_id) {
return "post.php?unlink_item&from=contact&id=$contact_id&to=service&other=" . intval($row['service_id']) . "&csrf_token=" . $_SESSION['csrf_token'];
} : null) ?>
<?php } else { ?>
<div class="itflow-asset-empty">Not a contact for any service.</div>
<?php } ?>
</div>
<?php } ?>
<!-- History, from the audit log -->
<div class="tab-pane fade" id="contact-pane-history" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Changes to them, their portal access and notes - newest first.</span>
</div>
<?= auditLogHistoryHtml($history, ['client_id' => $client_id]) ?>
</div>
<!-- Notes -->
<div class="tab-pane fade" id="contact-pane-notes" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">A dated log - calls, meetings, anything worth remembering.</span>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/contact/contact_note_add.php?id=<?= $contact_id ?>">
<i class="fas fa-plus me-2"></i>New Note
</button>
<?php } ?>
</div>
<?php if ($note_count) { ?>
<div class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_related_notes)) {
$contact_note_id = intval($row['contact_note_id']);
$contact_note_type = escapeHtml($row['contact_note_type']);
$contact_note_created_at = escapeHtml($row['contact_note_created_at']);
$note_type_icon = !empty($note_type_icons[$contact_note_type]) ? $note_type_icons[$contact_note_type] : 'fa-sticky-note'; ?>
<div class="list-group-item d-flex gap-3 py-3">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-other" title="<?= $contact_note_type ?>"><i class="fas fa-fw <?= $note_type_icon ?>"></i></span>
<div class="flex-grow-1 min-w-0">
<div class="small text-body-secondary mb-1">
<span class="fw-semibold text-body"><?= $contact_note_type ?></span>
&middot; <?= escapeHtml($row['user_name']) ?> &middot; <span title="<?= $contact_note_created_at ?>"><?= timeAgo($contact_note_created_at) ?></span>
</div>
<div><?= nl2br(escapeHtml($row['contact_note'])) ?></div>
</div>
<?php if ($can_edit) { ?>
<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 text-danger" href="post.php?archive_contact_note=<?= $contact_note_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_contact_note=<?= $contact_note_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No notes yet.</div>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<script>
function updateContactNotes(contact_id) {
itflowPost("ajax.php", {
contact_set_notes: 'TRUE',
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
contact_id: contact_id,
notes: document.getElementById("contactNotes").value
});
}
/*
* Which tab is open: the URL hash wins (#tickets - the contacts list's
* counts link straight to their tab), then the tab last used on any
* contact, then Assets. Choosing a tab updates the hash without adding a
* history entry, so a refresh or 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.contactTab';
var tabs = document.querySelectorAll('[data-contact-tab]');
if (!tabs.length) {
return;
}
function byName(name) {
return document.querySelector('[data-contact-tab="' + name + '"]');
}
var wanted = location.hash.replace('#', '');
var remembered = '';
try { remembered = localStorage.getItem(key) || ''; } catch (e) {}
var start = byName(wanted) || byName(remembered) || tabs[0];
bootstrap.Tab.getOrCreateInstance(start).show();
tabs.forEach(function (tab) {
tab.addEventListener('shown.bs.tab', function () {
var name = tab.getAttribute('data-contact-tab');
try { localStorage.setItem(key, name); } catch (e) {}
history.replaceState(null, '', location.pathname + location.search + '#' + name);
});
});
});
</script>
<?php
require_once "../includes/footer.php";