Files
IT_Ticket_System/app/templates/auth/profile.html
T

213 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}My Profile{% endblock %}
{% block page_title %}My Profile{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-7">
<div class="card">
<div class="card-header"><i class="bi bi-person-circle me-2"></i>Account Settings</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Avatar -->
<div class="text-center mb-4">
{% if current_user.avatar_url %}
<img src="{{ url_for('auth.serve_avatar', filename=current_user.avatar_url) }}"
alt="Avatar"
style="width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--border);margin:0 auto 10px;display:block;"/>
{% else %}
<div style="width:80px;height:80px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#fff;margin:0 auto 10px;">
{{ current_user.full_name[0].upper() }}
</div>
{% endif %}
<div style="font-size:12px;color:var(--muted);">{{ current_user.email }}</div>
<div style="font-size:11px;background:rgba(0,180,216,.1);color:var(--accent3);display:inline-block;padding:2px 10px;border-radius:12px;margin-top:4px;">
{{ current_user.role.replace('_',' ').upper() }}
</div>
<div class="mt-3">
<label class="form-label" style="font-size:12px;">Profile Picture
<span style="color:var(--muted);font-weight:400;">(PNG, JPG, GIF, WebP — max 5 MB)</span>
</label>
<input type="file" class="form-control form-control-sm" name="avatar"
accept=".png,.jpg,.jpeg,.gif,.webp" style="max-width:300px;margin:0 auto;"/>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Full Name</label>
<input type="text" class="form-control" name="full_name" value="{{ current_user.full_name }}" required/>
</div>
<div class="col-md-6">
<label class="form-label">Department</label>
<input type="text" class="form-control" name="department" value="{{ current_user.department or '' }}"/>
</div>
<div class="col-md-6">
<label class="form-label">Phone</label>
<input type="text" class="form-control" name="phone" value="{{ current_user.phone or '' }}"/>
</div>
<div class="col-md-6">
<label class="form-label">Username</label>
<input type="text" class="form-control" value="{{ current_user.username }}" disabled/>
</div>
</div>
<hr style="border-color:var(--border);margin:24px 0;"/>
<h6 style="font-size:14px;font-weight:700;margin-bottom:16px;">Notification Preferences</h6>
<div class="d-flex flex-column gap-2 mb-4">
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">
<input type="checkbox" name="email_notif" {% if current_user.email_notif %}checked{% endif %}
style="accent-color:var(--accent3);width:16px;height:16px;"/>
<span style="font-size:14px;"><i class="bi bi-envelope me-2" style="color:var(--accent3);"></i>Email Notifications</span>
</label>
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">
<input type="checkbox" name="web_notif" {% if current_user.web_notif %}checked{% endif %}
style="accent-color:var(--accent3);width:16px;height:16px;"/>
<span style="font-size:14px;"><i class="bi bi-bell me-2" style="color:var(--accent3);"></i>In-App Notifications</span>
</label>
</div>
<hr style="border-color:var(--border);margin:24px 0;"/>
<h6 style="font-size:14px;font-weight:700;margin-bottom:16px;">Change Password <span style="font-size:12px;color:var(--muted);font-weight:400;">(leave blank to keep current)</span></h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">New Password</label>
<!-- Show/hide toggle wrapper -->
<div style="position:relative;">
<input type="password" class="form-control" name="new_password"
id="prof-pw" placeholder="Min. 8 characters"
autocomplete="new-password"
oninput="profPwInput()" style="padding-right:40px;"/>
<button type="button" id="prof-pw-toggle"
onclick="profToggle('prof-pw','prof-pw-toggle')"
tabindex="-1"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
<i class="bi bi-eye"></i>
</button>
</div>
<!-- Strength bar (hidden until typing starts) -->
<div id="prof-strength-wrap" style="display:none;margin-top:6px;">
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:5px;">
<div id="prof-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
</div>
<div id="prof-strength-label" style="font-size:11px;font-weight:600;font-family:'Space Mono',monospace;display:flex;align-items:center;gap:5px;"></div>
</div>
<!-- Rule checklist -->
<div id="prof-rules" style="display:none;margin-top:7px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 12px;display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;">
<div class="prof-rule" id="prof-rule-len" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> 8+ characters</div>
<div class="prof-rule" id="prof-rule-upper" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Uppercase letter</div>
<div class="prof-rule" id="prof-rule-digit" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Number</div>
<div class="prof-rule" id="prof-rule-special"style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Special character</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Confirm Password</label>
<div style="position:relative;">
<input type="password" class="form-control" name="confirm_password"
id="prof-pw2" placeholder="Repeat password"
autocomplete="new-password"
oninput="profConfirmInput()" style="padding-right:40px;"/>
<button type="button" id="prof-pw2-toggle"
onclick="profToggle('prof-pw2','prof-pw2-toggle')"
tabindex="-1"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
<i class="bi bi-eye"></i>
</button>
</div>
<div id="prof-match-msg" style="font-size:11.5px;margin-top:6px;font-family:'Space Mono',monospace;font-weight:600;min-height:18px;"></div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-2"></i>Save Changes</button>
<a href="{{ url_for('tickets.dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
// ── Profile page password strength — mirrors register.html rules exactly ──────
const PROF_RULES = {
len: pw => pw.length >= 8,
upper: pw => /[A-Z]/.test(pw),
digit: pw => /\d/.test(pw),
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\|`~]/.test(pw),
};
const PROF_LEVELS = [
{ label:'Very Weak', color:'#ef4444', pct:12 },
{ label:'Weak', color:'#f97316', pct:30 },
{ label:'Fair', color:'#eab308', pct:52 },
{ label:'Good', color:'#22c55e', pct:76 },
{ label:'Strong', color:'#059669', pct:100 },
];
function profScore(pw) {
return [PROF_RULES.len,PROF_RULES.upper,PROF_RULES.digit,PROF_RULES.special]
.filter(r => r(pw)).length + (pw.length >= 16 ? 1 : 0);
}
function profSetRule(id, met) {
const el = document.getElementById('prof-rule-' + id);
if (!el) return;
el.style.color = met ? 'var(--success)' : 'var(--muted)';
el.querySelector('.prof-ri').innerHTML = met
? '<i class="bi bi-check-circle-fill" style="color:var(--success);"></i>'
: '<i class="bi bi-circle"></i>';
}
function profPwInput() {
const pw = document.getElementById('prof-pw').value;
const wrap = document.getElementById('prof-strength-wrap');
const rules = document.getElementById('prof-rules');
const bar = document.getElementById('prof-strength-bar');
const lbl = document.getElementById('prof-strength-label');
if (!pw) {
wrap.style.display = 'none'; rules.style.display = 'none';
document.getElementById('prof-pw').classList.remove('is-valid','is-invalid');
profConfirmInput(); return;
}
wrap.style.display = 'block'; rules.style.display = 'grid';
profSetRule('len', PROF_RULES.len(pw));
profSetRule('upper', PROF_RULES.upper(pw));
profSetRule('digit', PROF_RULES.digit(pw));
profSetRule('special', PROF_RULES.special(pw));
const score = profScore(pw);
const lvl = PROF_LEVELS[Math.max(0, score - 1)] || PROF_LEVELS[0];
bar.style.width = lvl.pct + '%';
bar.style.background = lvl.color;
lbl.style.color = lvl.color;
const icon = score >= 4 ? 'shield-fill' : score >= 2 ? 'shield-half' : 'shield';
lbl.innerHTML = `<i class="bi bi-${icon}"></i> ${lvl.label}`;
const allMet = Object.values(PROF_RULES).every(r => r(pw));
document.getElementById('prof-pw').classList.toggle('is-valid', allMet);
document.getElementById('prof-pw').classList.toggle('is-invalid', !allMet);
profConfirmInput();
}
function profConfirmInput() {
const pw = document.getElementById('prof-pw').value;
const pw2 = document.getElementById('prof-pw2').value;
const msg = document.getElementById('prof-match-msg');
const el2 = document.getElementById('prof-pw2');
if (!pw2) { msg.innerHTML=''; el2.classList.remove('is-valid','is-invalid'); return; }
const ok = pw === pw2;
msg.innerHTML = ok
? '<span style="color:var(--success);"><i class="bi bi-check-circle-fill me-1"></i>Passwords match</span>'
: '<span style="color:var(--danger);"><i class="bi bi-x-circle-fill me-1"></i>Passwords do not match</span>';
el2.classList.toggle('is-valid', ok);
el2.classList.toggle('is-invalid', !ok);
}
function profToggle(inputId, btnId) {
const inp = document.getElementById(inputId);
const btn = document.getElementById(btnId);
const isPassword = inp.type === 'password';
inp.type = isPassword ? 'text' : 'password';
btn.querySelector('i').className = isPassword ? 'bi bi-eye-slash' : 'bi bi-eye';
}
</script>
{% endblock %}
{% endblock %}