305 lines
14 KiB
HTML
305 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}File Ticket on Behalf{% endblock %}
|
|
{% block page_title %}File Ticket on Behalf of Employee{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
|
|
<!-- Context banner -->
|
|
<div class="alert alert-info mb-4" style="font-size:13px;">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<strong>Filing on behalf</strong> — Use this form when an employee reports an issue by phone or email
|
|
and cannot submit a ticket themselves. The ticket will appear in their dashboard and they will
|
|
receive a confirmation notification.
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="bi bi-person-plus me-2"></i>New Ticket — Filed by IT Staff
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" enctype="multipart/form-data" id="behalf-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
|
|
<!-- ── Employee selector ── -->
|
|
<div class="mb-4" style="background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:18px;">
|
|
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:12px;">
|
|
<i class="bi bi-person-badge me-1"></i> Reporting Employee *
|
|
</div>
|
|
|
|
<!-- Live search input (visible) -->
|
|
<div style="position:relative;">
|
|
<input type="text" id="employee-search"
|
|
class="form-control"
|
|
placeholder="Search by name, email, or department…"
|
|
autocomplete="off"
|
|
style="padding-left:36px;"/>
|
|
<i class="bi bi-search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:14px;pointer-events:none;"></i>
|
|
<!-- Dropdown results -->
|
|
<div id="employee-results"
|
|
style="display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;
|
|
background:#fff;border:1px solid var(--border);border-radius:8px;
|
|
box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:100;
|
|
max-height:260px;overflow-y:auto;">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden field that actually gets submitted -->
|
|
<input type="hidden" name="employee_id" id="employee-id"
|
|
value="{{ selected_employee_id or '' }}"/>
|
|
|
|
<!-- Selected employee display card -->
|
|
<div id="selected-employee" style="display:none;margin-top:10px;
|
|
background:#fff;border:1px solid var(--border);border-radius:8px;padding:12px 14px;
|
|
display:flex;align-items:center;gap:12px;">
|
|
<div id="sel-avatar"
|
|
style="width:36px;height:36px;border-radius:50%;background:var(--accent);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-size:14px;font-weight:700;color:#fff;flex-shrink:0;">
|
|
</div>
|
|
<div style="flex:1;min-width:0;">
|
|
<div id="sel-name" style="font-size:14px;font-weight:600;color:var(--text);"></div>
|
|
<div id="sel-meta" style="font-size:12px;color:var(--muted);margin-top:1px;"></div>
|
|
</div>
|
|
<button type="button" id="clear-employee"
|
|
class="btn btn-secondary btn-sm" title="Change employee">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div id="employee-hint" style="font-size:11px;color:var(--muted);margin-top:6px;">
|
|
Start typing to search all active users — employees, IT staff, and admins.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Ticket fields (identical to create.html) ── -->
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label class="form-label">Issue Title *</label>
|
|
<input type="text" class="form-control" name="title" required
|
|
placeholder="Brief description of the issue (e.g. 'Cannot connect to VPN')"/>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Category *</label>
|
|
<select class="form-select" name="category" required>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat }}">{{ cat.replace('_',' ').title() }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Priority *</label>
|
|
<select class="form-select" name="priority" required>
|
|
{% for p in priorities %}
|
|
<option value="{{ p }}" {% if p == 'medium' %}selected{% endif %}>{{ p.upper() }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Location / Floor</label>
|
|
<input type="text" class="form-control" name="location" placeholder="e.g. 3rd Floor, Room 302"/>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Asset Tag / Device Serial</label>
|
|
<input type="text" class="form-control" name="asset_tag" placeholder="e.g. ASSET-1234"/>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Detailed Description *</label>
|
|
<textarea class="form-control" name="description" rows="6" required
|
|
placeholder="Please describe the issue in detail: - What were you trying to do? - What happened? - Any error messages? - When did it start?"></textarea>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Attachments (screenshots, logs, etc.)</label>
|
|
<input type="file" class="form-control" name="attachments" multiple
|
|
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.zip,.log"/>
|
|
<div style="font-size:11px;color:var(--muted);margin-top:5px;">
|
|
Accepted: PNG, JPG, PDF, DOC, DOCX, TXT, ZIP, LOG. Max 16 MB per file.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Priority guide -->
|
|
<div class="col-12">
|
|
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;">
|
|
<div style="font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;">Priority Guide</div>
|
|
<div class="row g-2" style="font-size:12px;">
|
|
<div class="col-sm-3"><span class="badge badge-low me-1">LOW</span> Minor inconvenience, no work stoppage</div>
|
|
<div class="col-sm-3"><span class="badge badge-medium me-1">MEDIUM</span> Impacting productivity, workaround available</div>
|
|
<div class="col-sm-3"><span class="badge badge-high me-1">HIGH</span> Significant impact, no workaround</div>
|
|
<div class="col-sm-3"><span class="badge badge-critical me-1">CRITICAL</span> Complete outage or security incident</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 mt-4">
|
|
<button type="submit" class="btn btn-primary" id="submit-btn">
|
|
<i class="bi bi-send me-2"></i>Submit Ticket
|
|
</button>
|
|
<a href="{{ url_for('tickets.ticket_list') }}" class="btn btn-secondary">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Sidebar tips ── -->
|
|
<div class="col-lg-4 d-none d-lg-block">
|
|
<div class="card mb-3">
|
|
<div class="card-header"><i class="bi bi-shield-check me-2"></i>Audit Trail</div>
|
|
<div class="card-body" style="font-size:13px;color:var(--muted);">
|
|
<p>The ticket will be registered under the <strong>employee's account</strong>.
|
|
Your name is recorded in the activity log as the filing IT staff member.</p>
|
|
<p class="mt-2">The employee will receive a notification and can view, comment on,
|
|
and track the ticket from their dashboard.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header"><i class="bi bi-lightbulb me-2"></i>Tips</div>
|
|
<div class="card-body" style="font-size:13px;color:var(--muted);">
|
|
<p>✅ Confirm the employee's name and department before submitting</p>
|
|
<p>✅ Include verbatim error messages they described</p>
|
|
<p>✅ Note the contact method (phone / email) in the description</p>
|
|
<p>✅ Attach any files the employee sent you</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// ── Employee live search ──────────────────────────────────────────────────────
|
|
const searchInput = document.getElementById('employee-search');
|
|
const resultsBox = document.getElementById('employee-results');
|
|
const employeeIdEl = document.getElementById('employee-id');
|
|
const selectedCard = document.getElementById('selected-employee');
|
|
const selAvatar = document.getElementById('sel-avatar');
|
|
const selName = document.getElementById('sel-name');
|
|
const selMeta = document.getElementById('sel-meta');
|
|
const clearBtn = document.getElementById('clear-employee');
|
|
const hintEl = document.getElementById('employee-hint');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
|
|
let debounceTimer = null;
|
|
|
|
// Disable submit until an employee is chosen
|
|
function refreshSubmitState() {
|
|
submitBtn.disabled = !employeeIdEl.value;
|
|
}
|
|
refreshSubmitState();
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
const q = searchInput.value.trim();
|
|
if (q.length < 1) { resultsBox.style.display = 'none'; return; }
|
|
debounceTimer = setTimeout(() => fetchUsers(q), 250);
|
|
});
|
|
|
|
searchInput.addEventListener('focus', () => {
|
|
if (searchInput.value.trim().length >= 1) fetchUsers(searchInput.value.trim());
|
|
});
|
|
|
|
document.addEventListener('click', e => {
|
|
if (!e.target.closest('#employee-search') && !e.target.closest('#employee-results')) {
|
|
resultsBox.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
async function fetchUsers(q) {
|
|
try {
|
|
const r = await fetch(`/api/users/search?q=${encodeURIComponent(q)}&limit=20`);
|
|
const data = await r.json();
|
|
renderResults(data.users || []);
|
|
} catch {
|
|
resultsBox.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function renderResults(users) {
|
|
if (users.length === 0) {
|
|
resultsBox.innerHTML = '<div style="padding:12px 16px;font-size:13px;color:var(--muted);">No users found.</div>';
|
|
resultsBox.style.display = 'block';
|
|
return;
|
|
}
|
|
resultsBox.innerHTML = users.map(u => `
|
|
<div class="employee-option" data-id="${u.id}" data-name="${u.full_name}"
|
|
data-email="${u.email}" data-dept="${u.department}" data-role="${u.role}"
|
|
style="padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--border);
|
|
display:flex;align-items:center;gap:10px;transition:background .1s;">
|
|
<div style="width:30px;height:30px;border-radius:50%;background:var(--accent);flex-shrink:0;
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-size:12px;font-weight:700;color:#fff;">
|
|
${u.full_name[0].toUpperCase()}
|
|
</div>
|
|
<div style="flex:1;min-width:0;">
|
|
<div style="font-size:13px;font-weight:600;color:var(--text);">${u.full_name}</div>
|
|
<div style="font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
${u.email}${u.department ? ' · ' + u.department : ''}
|
|
${u.role !== 'employee' ? ' · <span style="color:var(--accent);">' + u.role.replace('_',' ') + '</span>' : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
resultsBox.querySelectorAll('.employee-option').forEach(el => {
|
|
el.addEventListener('mouseenter', () => el.style.background = 'var(--surface2)');
|
|
el.addEventListener('mouseleave', () => el.style.background = '');
|
|
el.addEventListener('click', () => selectEmployee(el.dataset));
|
|
});
|
|
|
|
resultsBox.style.display = 'block';
|
|
}
|
|
|
|
function selectEmployee(data) {
|
|
employeeIdEl.value = data.id;
|
|
searchInput.value = '';
|
|
resultsBox.style.display = 'none';
|
|
|
|
// Populate selected card
|
|
selAvatar.textContent = data.name[0].toUpperCase();
|
|
selName.textContent = data.name;
|
|
const metaParts = [data.email];
|
|
if (data.dept) metaParts.push(data.dept);
|
|
if (data.role && data.role !== 'employee') metaParts.push(data.role.replace(/_/g,' '));
|
|
selMeta.textContent = metaParts.join(' · ');
|
|
|
|
selectedCard.style.display = 'flex';
|
|
searchInput.style.display = 'none';
|
|
hintEl.style.display = 'none';
|
|
refreshSubmitState();
|
|
}
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
employeeIdEl.value = '';
|
|
selectedCard.style.display = 'none';
|
|
searchInput.style.display = '';
|
|
searchInput.value = '';
|
|
hintEl.style.display = '';
|
|
searchInput.focus();
|
|
refreshSubmitState();
|
|
});
|
|
|
|
// Pre-select employee if form re-rendered after validation error
|
|
(function() {
|
|
const preselId = '{{ selected_employee_id or "" }}';
|
|
if (!preselId) return;
|
|
fetch(`/api/users/search?q=&limit=50`)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const u = (data.users || []).find(x => String(x.id) === preselId);
|
|
if (u) selectEmployee({
|
|
id: String(u.id), name: u.full_name,
|
|
email: u.email, dept: u.department || '', role: u.role
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|