213 lines
12 KiB
HTML
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 %} |