nordabiz/templates/konto/dane.html
Maciej Pienczyn 3a7faa782b
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: user avatar upload with crop, resize, and EXIF strip
- POST /konto/avatar: upload, center-crop to square, resize 300x300
- POST /konto/avatar/delete: remove file and clear DB
- dane.html: interactive avatar editor with hover overlay
- person_detail.html: show photo if available, fallback to initials
- Migration 070: avatar_path column on users table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:18:29 +01:00

520 lines
18 KiB
HTML

{% extends "base.html" %}
{% block title %}Twoje dane - Moje konto - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.konto-layout {
display: grid;
grid-template-columns: 240px 1fr;
gap: var(--spacing-xl);
max-width: 1000px;
margin: 0 auto;
}
.konto-sidebar {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
height: fit-content;
}
.konto-sidebar-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding-bottom: var(--spacing-md);
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.konto-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), #1e40af);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
position: relative;
overflow: hidden;
}
.konto-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-edit-section {
text-align: center;
padding: var(--spacing-md) 0;
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.avatar-edit-large {
width: 96px;
height: 96px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), #1e40af);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: 600;
margin: 0 auto var(--spacing-sm);
position: relative;
overflow: hidden;
cursor: pointer;
}
.avatar-edit-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-edit-large:hover .avatar-overlay {
opacity: 1;
}
.avatar-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
border-radius: 50%;
}
.avatar-overlay svg {
width: 24px;
height: 24px;
color: white;
}
.avatar-actions {
display: flex;
gap: var(--spacing-xs);
justify-content: center;
margin-top: var(--spacing-sm);
}
.avatar-actions button, .avatar-actions label {
font-size: 11px;
padding: 4px 10px;
border-radius: var(--radius);
cursor: pointer;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
transition: all 0.2s;
}
.avatar-actions label:hover, .avatar-actions button:hover {
background: var(--background);
color: var(--text-primary);
}
.avatar-actions .btn-delete-avatar {
color: #dc2626;
border-color: #fecaca;
}
.avatar-actions .btn-delete-avatar:hover {
background: #fef2f2;
}
.konto-sidebar-name {
font-weight: 600;
color: var(--text-primary);
font-size: var(--font-size-sm);
}
.konto-sidebar-email {
font-size: 11px;
color: var(--text-secondary);
word-break: break-all;
}
.konto-nav {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.konto-nav a {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
text-decoration: none;
color: var(--text-secondary);
font-size: var(--font-size-sm);
transition: all 0.2s;
}
.konto-nav a:hover {
background: var(--background);
color: var(--text-primary);
}
.konto-nav a.active {
background: var(--primary);
color: white;
}
.konto-nav a svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.konto-content {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
}
.konto-header {
margin-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--border);
}
.konto-header h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.konto-header p {
color: var(--text-secondary);
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
font-weight: 500;
margin-bottom: var(--spacing-sm);
color: var(--text-primary);
}
.form-input {
width: 100%;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-family: var(--font-family);
transition: var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-input:disabled {
background: var(--background);
color: var(--text-secondary);
cursor: not-allowed;
}
.form-help {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
.info-box {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: var(--radius);
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-sm);
color: #0369a1;
}
.info-box a {
color: #0369a1;
font-weight: 500;
}
.form-actions {
display: flex;
gap: var(--spacing-md);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
}
.badge-row {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.profile-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
}
.badge-verified {
background: #dcfce7;
color: #166534;
}
.badge-member {
background: #dbeafe;
color: #1e40af;
}
@media (max-width: 768px) {
.konto-layout {
grid-template-columns: 1fr;
}
.konto-sidebar {
position: static;
}
.konto-nav {
flex-direction: row;
flex-wrap: wrap;
}
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
}
</style>
{% endblock %}
{% block content %}
<div class="konto-layout">
<aside class="konto-sidebar">
<div class="konto-sidebar-header">
<div class="konto-avatar">
{% if current_user.avatar_path %}
<img src="{{ url_for('static', filename=current_user.avatar_path) }}" alt="Zdjęcie profilowe">
{% else %}
{{ (current_user.name or current_user.email)[0].upper() }}
{% endif %}
</div>
<div>
<div class="konto-sidebar-name">{{ current_user.name or 'Użytkownik' }}</div>
<div class="konto-sidebar-email">{{ current_user.email }}</div>
</div>
</div>
<div class="avatar-edit-section">
<label for="avatar-input" class="avatar-edit-large" title="Zmień zdjęcie profilowe">
{% if current_user.avatar_path %}
<img src="{{ url_for('static', filename=current_user.avatar_path) }}" alt="Zdjęcie profilowe">
{% else %}
{{ (current_user.name or current_user.email)[0].upper() }}
{% endif %}
<div class="avatar-overlay">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
<circle cx="12" cy="13" r="3"/>
</svg>
</div>
</label>
<form id="avatar-form" method="POST" action="{{ url_for('auth.konto_avatar_upload') }}" enctype="multipart/form-data" style="display:none;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="file" id="avatar-input" name="avatar" accept="image/jpeg,image/png,image/gif">
</form>
<div class="avatar-actions">
<label for="avatar-input" style="cursor:pointer;">Zmień zdjęcie</label>
{% if current_user.avatar_path %}
<form method="POST" action="{{ url_for('auth.konto_avatar_delete') }}" style="display:inline;" onsubmit="return confirm('Usunąć zdjęcie profilowe?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn-delete-avatar">Usuń</button>
</form>
{% endif %}
</div>
</div>
<nav class="konto-nav">
<a href="{{ url_for('konto_dane') }}" class="active">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Twoje dane
</a>
<a href="{{ url_for('konto_prywatnosc') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
Prywatność
</a>
<a href="{{ url_for('konto_bezpieczenstwo') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
Bezpieczeństwo
</a>
<a href="{{ url_for('konto_blokady') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
Blokady
</a>
{% if current_user.company_id %}
<a href="{{ url_for('auth.konto_integracje') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
Integracje
</a>
{% endif %}
<div style="border-top: 1px solid var(--border); margin: var(--spacing-md) 0; padding-top: var(--spacing-md);">
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: var(--spacing-xs); text-transform: uppercase; letter-spacing: 0.5px;">Członkostwo</div>
</div>
{% if not current_user.company_id %}
<a href="{{ url_for('membership.apply') }}" style="color: var(--primary);">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/>
</svg>
Złóż deklarację
</a>
<a href="{{ url_for('membership.status') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
Status deklaracji
</a>
{% elif current_user.company and not current_user.company.nip %}
<a href="{{ url_for('membership.data_request') }}" style="color: var(--warning);">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Uzupełnij dane firmy
</a>
{% else %}
<a href="{{ url_for('membership.status') }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Historia deklaracji
</a>
{% endif %}
</nav>
</aside>
<main class="konto-content">
<div class="konto-header">
<h1>Twoje dane</h1>
<p>Zarządzaj swoimi danymi osobowymi</p>
<div class="badge-row">
{% if current_user.is_verified %}
<span class="profile-badge badge-verified">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Zweryfikowany
</span>
{% endif %}
{% if current_user.is_norda_member %}
<span class="profile-badge badge-member">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
Członek NORDA
</span>
{% endif %}
</div>
</div>
<form method="POST" action="{{ url_for('konto_dane') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="name" class="form-label">Imię i nazwisko</label>
<input type="text" id="name" name="name" class="form-input"
value="{{ current_user.name or '' }}"
placeholder="Jan Kowalski"
maxlength="255">
<p class="form-help">Twoje imię i nazwisko widoczne dla innych użytkowników</p>
</div>
<div class="form-group">
<label for="phone" class="form-label">Numer telefonu</label>
<input type="tel" id="phone" name="phone" class="form-input"
value="{{ current_user.phone or '' }}"
placeholder="+48 123 456 789"
maxlength="50">
<p class="form-help">Opcjonalnie - ułatwi kontakt z innymi członkami. Możesz ukryć telefon w ustawieniach prywatności.</p>
</div>
<div class="form-group">
<label for="email" class="form-label">Adres email</label>
<input type="email" id="email" class="form-input"
value="{{ current_user.email }}"
disabled>
<p class="form-help">Adres email nie może być zmieniony</p>
</div>
{% if current_user.company %}
<div class="info-box">
<strong>Powiązana firma:</strong> {{ current_user.company.name }}<br>
<a href="{{ url_for('company_detail_by_slug', slug=current_user.company.slug) }}">Zobacz profil firmy →</a>
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Zapisz zmiany
</button>
<a href="{{ url_for('index') }}" class="btn btn-outline">
Anuluj
</a>
</div>
</form>
</main>
</div>
{% endblock %}
{% block extra_js %}
document.getElementById('avatar-input').addEventListener('change', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
if (file.size > 5 * 1024 * 1024) {
alert('Plik jest za duży (max 5MB)');
return;
}
document.getElementById('avatar-form').submit();
}
});
{% endblock %}