nordabiz/templates/messages/group_compose.html
Maciej Pienczyn e0e0ea2cf6
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
fix: back links in messages point to new conversations page
All "Powrót do wiadomości" links in compose, view, sent, and group_compose
templates now point to messages.conversations_page instead of legacy
messages_inbox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:32:16 +02:00

679 lines
22 KiB
HTML

{% extends "base.html" %}
{% block title %}Nowa grupa - Norda Biznes Partner{% endblock %}
{% block head_extra %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}">
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
{% endblock %}
{% block extra_css %}
.quill-container {
border: 1px solid var(--border);
border-radius: var(--radius);
}
.quill-container .ql-toolbar {
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
}
.quill-container .ql-container {
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
font-size: var(--font-size-base);
}
.quill-container .ql-editor {
min-height: 200px;
}
.quill-container .ql-editor img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
margin: var(--spacing-sm) 0;
}
.compose-container {
max-width: 700px;
margin: 0 auto;
}
.compose-header {
margin-bottom: var(--spacing-xl);
}
.compose-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.compose-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: var(--spacing-xs);
color: var(--text-primary);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
transition: var(--transition);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
}
.back-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
text-decoration: none;
margin-bottom: var(--spacing-lg);
}
.back-link:hover {
color: var(--primary);
}
/* Member picker */
.member-picker {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.selected-members {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
min-height: 44px;
align-items: center;
border-bottom: 1px solid var(--border);
background: var(--background);
}
.selected-members:empty {
border-bottom: none;
}
.member-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--primary);
color: white;
border-radius: 16px;
font-size: var(--font-size-sm);
font-weight: 500;
}
.member-pill-avatar {
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
}
.member-pill-initial {
width: 22px;
height: 22px;
border-radius: 50%;
background: rgba(255,255,255,0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.member-pill-remove {
background: none;
border: none;
color: rgba(255,255,255,0.7);
cursor: pointer;
padding: 0;
font-size: 14px;
line-height: 1;
}
.member-pill-remove:hover {
color: white;
}
.member-search-box {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.member-search-box input {
width: 100%;
border: none;
outline: none;
font-size: var(--font-size-sm);
padding: 4px 0;
background: transparent;
}
.member-picker-collapsible {
display: none;
}
.member-picker.open .member-picker-collapsible {
display: block;
}
.member-picker-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-xs) var(--spacing-md);
background: var(--background);
cursor: pointer;
font-size: var(--font-size-sm);
color: var(--primary);
font-weight: 500;
border-top: 1px solid var(--border);
user-select: none;
}
.member-picker-toggle:hover {
background: rgba(46, 72, 114, 0.05);
}
.member-picker-toggle .toggle-arrow {
transition: transform 0.2s;
}
.member-picker.open .member-picker-toggle .toggle-arrow {
transform: rotate(180deg);
}
.member-list {
max-height: 240px;
overflow-y: auto;
}
.member-list-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 6px var(--spacing-md);
cursor: pointer;
transition: background 0.15s;
}
.member-list-item:hover {
background: var(--background);
}
.member-list-item.selected {
background: rgba(37, 99, 235, 0.06);
}
.member-list-item input[type="checkbox"] {
width: auto;
flex-shrink: 0;
}
.member-list-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.member-list-initial {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 12px;
flex-shrink: 0;
}
.member-list-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
overflow: hidden;
}
.member-list-name {
font-weight: 500;
font-size: var(--font-size-sm);
color: var(--text-primary);
white-space: nowrap;
}
.member-list-sep {
color: var(--border-color, #cbd5e1);
flex-shrink: 0;
}
.member-list-company {
font-size: var(--font-size-xs);
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-list-empty {
padding: var(--spacing-md);
text-align: center;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.selected-count {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
@media (max-width: 640px) {
.compose-card {
padding: var(--spacing-md);
}
}
{% endblock %}
{% block content %}
<div class="compose-container">
<a href="{{ url_for('messages.conversations_page') }}" class="back-link">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Powrot do wiadomosci
</a>
<div class="compose-header">
<h1>Nowa grupa</h1>
</div>
<div class="compose-card">
<form method="POST" action="{{ url_for('messages.group_create') }}" enctype="multipart/form-data" id="group-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div id="members-hidden-inputs"></div>
<div class="form-group">
<label for="group-name">Nazwa grupy (opcjonalnie)</label>
<input type="text" id="group-name" name="name" maxlength="100" placeholder="np. Komisja ds. PEJ">
</div>
<div class="form-group">
<label>Uczestnicy *</label>
<div class="member-picker" id="member-picker">
<div class="selected-members" id="selected-members">
<span style="color: var(--text-secondary); font-size: var(--font-size-sm);" id="placeholder-text">Wybierz uczestnikow z listy ponizej...</span>
</div>
<div class="member-picker-toggle" id="member-picker-toggle">
<span id="toggle-label">▼ Rozwiń listę osób</span>
<span class="selected-count" id="selected-count">Wybrano: 0</span>
</div>
<div class="member-picker-collapsible" id="member-picker-body">
<div class="member-search-box">
<input type="text" id="member-search" placeholder="Szukaj po imieniu, nazwisku lub firmie..." autocomplete="off">
</div>
<div class="member-list" id="member-list"></div>
</div>
</div>
</div>
<div class="form-group">
<label>Pierwsza wiadomosc *</label>
<div id="quill-content" class="quill-container" style="min-height: 200px; background: var(--surface);"></div>
<textarea id="content" name="content" style="display:none;" required></textarea>
</div>
<div class="form-group">
<label>Zalaczniki (maks. 3 pliki, 15MB lacznie)</label>
<div class="file-upload-zone" id="file-drop-zone" style="border: 2px dashed var(--border-color); border-radius: var(--radius); padding: 20px; text-align: center; cursor: pointer; transition: border-color 0.2s;">
<input type="file" name="attachments" id="file-input" multiple accept=".jpg,.jpeg,.png,.gif,.pdf,.docx,.xlsx" style="display: none;">
<p style="margin: 0; color: var(--text-secondary); font-size: var(--font-size-sm);">
Przeciagnij pliki tutaj lub <a href="#" onclick="document.getElementById('file-input').click(); return false;" style="color: var(--primary);">wybierz z dysku</a>
</p>
<p style="margin: 4px 0 0; color: var(--text-secondary); font-size: var(--font-size-xs);">
JPG, PNG, GIF (5MB) · PDF, DOCX, XLSX (10MB)
</p>
</div>
<div id="file-list" style="margin-top: 8px;"></div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Utworz grupe</button>
<a href="{{ url_for('messages.conversations_page') }}" class="btn btn-secondary">Anuluj</a>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
/* Member picker */
(function() {
var users = [
{% for user in users %}
{id: {{ user.id }}, name: {{ (user.name or user.email.split('@')[0]) | tojson }}, email: {{ user.email | tojson }}, companyName: {{ (user._company_name or '') | tojson }}, avatarPath: {% if user.avatar_path %}{{ url_for('static', filename=user.avatar_path) | tojson }}{% else %}""{% endif %}}{{ ',' if not loop.last }}
{% endfor %}
];
var selectedIds = new Set();
var searchInput = document.getElementById('member-search');
var listDiv = document.getElementById('member-list');
var selectedDiv = document.getElementById('selected-members');
var countDiv = document.getElementById('selected-count');
var hiddenInputsDiv = document.getElementById('members-hidden-inputs');
var placeholderText = document.getElementById('placeholder-text');
function normalize(str) {
return str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function filterUsers(query) {
if (!query) return users;
var q = normalize(query);
return users.filter(function(u) {
return normalize(u.name).indexOf(q) !== -1 ||
normalize(u.companyName).indexOf(q) !== -1 ||
normalize(u.email).indexOf(q) !== -1;
});
}
function renderList() {
var query = searchInput.value.trim();
var filtered = filterUsers(query);
if (filtered.length === 0) {
listDiv.innerHTML = '<div class="member-list-empty">Nie znaleziono</div>';
return;
}
listDiv.innerHTML = filtered.map(function(u) {
var checked = selectedIds.has(u.id) ? 'checked' : '';
var selectedClass = selectedIds.has(u.id) ? ' selected' : '';
var initial = (u.name || u.email)[0].toUpperCase();
var avatarHtml = u.avatarPath
? '<img src="' + u.avatarPath + '" class="member-list-avatar" alt="">'
: '<div class="member-list-initial">' + initial + '</div>';
var companyHtml = u.companyName ? '<span class="member-list-sep">|</span><span class="member-list-company">' + u.companyName + '</span>' : '';
return '<label class="member-list-item' + selectedClass + '" data-user-id="' + u.id + '">' +
'<input type="checkbox" ' + checked + ' data-id="' + u.id + '" style="width:auto;">' +
avatarHtml +
'<div class="member-list-info">' +
'<span class="member-list-name">' + u.name + '</span>' +
companyHtml +
'</div>' +
'</label>';
}).join('');
}
function renderPills() {
if (selectedIds.size === 0) {
selectedDiv.innerHTML = '<span style="color: var(--text-secondary); font-size: var(--font-size-sm);" id="placeholder-text">Wybierz uczestnikow z listy ponizej...</span>';
} else {
var html = '';
selectedIds.forEach(function(id) {
var user = users.find(function(u) { return u.id === id; });
if (!user) return;
var initial = (user.name || user.email)[0].toUpperCase();
var avatarHtml = user.avatarPath
? '<img src="' + user.avatarPath + '" class="member-pill-avatar" alt="">'
: '<div class="member-pill-initial">' + initial + '</div>';
html += '<span class="member-pill">' +
avatarHtml +
'<span>' + user.name + '</span>' +
'<button type="button" class="member-pill-remove" data-id="' + id + '" title="Usun"></button>' +
'</span>';
});
selectedDiv.innerHTML = html;
}
countDiv.textContent = 'Wybrano: ' + selectedIds.size + ' osob';
}
function updateHiddenInputs() {
hiddenInputsDiv.innerHTML = '';
selectedIds.forEach(function(id) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'members';
input.value = id;
hiddenInputsDiv.appendChild(input);
});
}
function toggleUser(id) {
if (selectedIds.has(id)) {
selectedIds.delete(id);
} else {
selectedIds.add(id);
searchInput.value = '';
searchInput.focus();
}
renderPills();
updateHiddenInputs();
renderList();
}
listDiv.addEventListener('change', function(e) {
if (e.target.type === 'checkbox') {
toggleUser(parseInt(e.target.dataset.id));
}
});
selectedDiv.addEventListener('click', function(e) {
var btn = e.target.closest('.member-pill-remove');
if (btn) {
toggleUser(parseInt(btn.dataset.id));
}
});
searchInput.addEventListener('input', renderList);
// Toggle collapsible picker
var picker = document.getElementById('member-picker');
var toggleBtn = document.getElementById('member-picker-toggle');
var toggleLabel = document.getElementById('toggle-label');
toggleBtn.addEventListener('click', function() {
picker.classList.toggle('open');
toggleLabel.textContent = picker.classList.contains('open') ? '▲ Zwiń listę osób' : '▼ Rozwiń listę osób';
if (picker.classList.contains('open')) {
searchInput.focus();
}
});
// Close picker when clicking outside
document.addEventListener('click', function(e) {
if (picker.classList.contains('open') && !picker.contains(e.target)) {
picker.classList.remove('open');
toggleLabel.textContent = '▼ Rozwiń listę osób';
}
});
renderList();
/* Form validation */
document.getElementById('group-form').addEventListener('submit', function(e) {
if (selectedIds.size < 1) {
e.preventDefault();
alert('Wybierz co najmniej jednego uczestnika.');
return;
}
});
})();
/* File attachment handling */
(function() {
var dropZone = document.getElementById('file-drop-zone');
var fileInput = document.getElementById('file-input');
var fileList = document.getElementById('file-list');
if (!dropZone) return;
dropZone.addEventListener('click', function(e) {
if (e.target.tagName !== 'A') fileInput.click();
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.style.borderColor = 'var(--primary)';
});
dropZone.addEventListener('dragleave', function() {
dropZone.style.borderColor = 'var(--border-color)';
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.style.borderColor = 'var(--border-color)';
var dt = new DataTransfer();
Array.from(e.dataTransfer.files).forEach(function(f) { dt.items.add(f); });
Array.from(fileInput.files).forEach(function(f) { dt.items.add(f); });
fileInput.files = dt.files;
updateFileList();
});
fileInput.addEventListener('change', updateFileList);
function updateFileList() {
var files = Array.from(fileInput.files);
if (files.length === 0) { fileList.innerHTML = ''; return; }
fileList.innerHTML = files.map(function(f, i) {
var sizeMB = (f.size / 1024 / 1024).toFixed(1);
return '<div style="display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: var(--font-size-sm);">' +
'<span style="color: var(--text-secondary);">📎</span> ' +
'<span>' + f.name + '</span> ' +
'<span style="color: var(--text-secondary);">(' + sizeMB + ' MB)</span> ' +
'<a href="#" onclick="removeFile(' + i + '); return false;" style="color: var(--danger); margin-left: auto;"></a>' +
'</div>';
}).join('');
}
window.removeFile = function(index) {
var dt = new DataTransfer();
Array.from(fileInput.files).forEach(function(f, i) {
if (i !== index) dt.items.add(f);
});
fileInput.files = dt.files;
updateFileList();
};
})();
/* Quill editor for message content */
(function() {
var csrfToken = '{{ csrf_token() }}';
var quill = new Quill('#quill-content', {
theme: 'snow',
placeholder: 'Napisz wiadomosc...',
modules: {
toolbar: {
container: [
['bold', 'italic'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['link', 'image'],
['clean']
],
handlers: {
image: function() {
var input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = function() {
if (input.files && input.files[0]) {
uploadImage(input.files[0]);
}
};
}
}
}
}
});
function uploadImage(file) {
var fd = new FormData();
fd.append('image', file);
fetch('/api/messages/upload-image', {
method: 'POST',
headers: {'X-CSRFToken': csrfToken},
body: fd
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.url) {
var range = quill.getSelection(true);
quill.insertEmbed(range.index, 'image', data.url);
quill.setSelection(range.index + 1);
} else {
alert(data.error || 'Blad uploadu');
}
})
.catch(function() { alert('Blad polaczenia'); });
}
quill.root.addEventListener('paste', function(e) {
var items = (e.clipboardData || {}).items || [];
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.stopImmediatePropagation();
e.preventDefault();
var file = items[i].getAsFile();
if (file) uploadImage(file);
return;
}
}
}, true);
var textarea = document.getElementById('content');
quill.on('text-change', function() {
var html = quill.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
});
document.getElementById('group-form').addEventListener('submit', function(e) {
var html = quill.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
if (!textarea.value.trim()) {
e.preventDefault();
alert('Tresc wiadomosci jest wymagana.');
}
});
})();
{% endblock %}