Files
IT_Ticket_System/app/templates/tickets/create_behalf.html
T

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:&#10;- What were you trying to do?&#10;- What happened?&#10;- Any error messages?&#10;- 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('admin.all_tickets') }}" 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 %}