feat(board): Redesign proceedings editor with structured fields and visual cues
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
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
Major UX improvements to meeting protocol editing: - Numbered, color-coded cards with left accent borders - Three fields per agenda item: Discussion, Decisions, Tasks - Decisions and tasks as line-per-item textareas (auto-convert to arrays) - Fill status indicators (green check vs empty circle) - Collapsible sections to reduce visual clutter - Add CSRF token to form (was missing) - Better visual hierarchy with proceeding headers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c7d0ec12c
commit
58444ddb0d
@ -266,19 +266,153 @@
|
||||
|
||||
/* Proceedings */
|
||||
.proceeding-item {
|
||||
background: var(--bg-secondary);
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 4px solid var(--primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: 0;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.proceeding-item h4 {
|
||||
margin-bottom: var(--spacing-sm);
|
||||
.proceeding-item:nth-child(even) {
|
||||
border-left-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.proceeding-item:nth-child(3n) {
|
||||
border-left-color: #059669;
|
||||
}
|
||||
|
||||
.proceeding-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.proceeding-header:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.proceeding-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.proceeding-item:nth-child(even) .proceeding-number {
|
||||
background: #8b5cf6;
|
||||
}
|
||||
|
||||
.proceeding-item:nth-child(3n) .proceeding-number {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.proceeding-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.proceeding-item textarea {
|
||||
.proceeding-status-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.proceeding-status-icon.empty {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.proceeding-status-icon.filled {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.proceeding-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.proceeding-item.collapsed .proceeding-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.proceeding-body {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.proceeding-item.collapsed .proceeding-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.proceeding-field {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.proceeding-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.proceeding-field label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.proceeding-field label svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.proceeding-field textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.proceeding-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.proceeding-field textarea.field-discussion {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.proceeding-field .field-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
@ -381,6 +515,7 @@
|
||||
</div>
|
||||
|
||||
<form method="POST" id="meetingForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<!-- Tabs -->
|
||||
<div class="form-tabs">
|
||||
<button type="button" class="form-tab active" data-tab="basic">Dane podstawowe</button>
|
||||
@ -663,6 +798,25 @@ function updateAgendaJson() {
|
||||
// Proceedings
|
||||
let proceedings = {{ form_data.get('proceedings', [])|tojson|safe }};
|
||||
|
||||
// Helper: convert array to text (one item per line)
|
||||
function arrayToText(arr) {
|
||||
if (!arr) return '';
|
||||
if (typeof arr === 'string') return arr;
|
||||
if (Array.isArray(arr)) return arr.join('\n');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Helper: convert text to array (split by newlines, filter empty)
|
||||
function textToArray(text) {
|
||||
if (!text) return [];
|
||||
return text.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
||||
}
|
||||
|
||||
// Helper: get discussion text (supports both field names)
|
||||
function getDiscussion(proc) {
|
||||
return proc.discussion || proc.discussed || '';
|
||||
}
|
||||
|
||||
function renderProceedings() {
|
||||
const list = document.getElementById('proceedingsList');
|
||||
list.innerHTML = '';
|
||||
@ -670,32 +824,105 @@ function renderProceedings() {
|
||||
agendaItems.forEach((item, index) => {
|
||||
if (!item.title) return;
|
||||
|
||||
const proc = proceedings.find(p => p.agenda_item === index) || { discussed: '', decisions: '' };
|
||||
const proc = proceedings.find(p => p.agenda_item === index) || {};
|
||||
const discussion = getDiscussion(proc);
|
||||
const decisionsText = arrayToText(proc.decisions);
|
||||
const tasksText = arrayToText(proc.tasks);
|
||||
const isFilled = discussion || decisionsText || tasksText;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'proceeding-item';
|
||||
div.dataset.index = index;
|
||||
div.innerHTML = `
|
||||
<h4>Ad. ${index + 1}. ${item.title}</h4>
|
||||
<div class="form-group">
|
||||
<label>Omówiono:</label>
|
||||
<textarea onchange="updateProceeding(${index}, 'discussed', this.value)" placeholder="Opis przebiegu dyskusji...">${proc.discussed || ''}</textarea>
|
||||
<div class="proceeding-header" onclick="toggleProceeding(this)">
|
||||
<span class="proceeding-number">${index + 1}</span>
|
||||
<span class="proceeding-title">${escapeHtml(item.title)}</span>
|
||||
<svg class="proceeding-status-icon ${isFilled ? 'filled' : 'empty'}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
${isFilled
|
||||
? '<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>'
|
||||
: '<circle cx="12" cy="12" r="9"/>'}
|
||||
</svg>
|
||||
<svg class="proceeding-toggle" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ustalono / decyzje:</label>
|
||||
<textarea onchange="updateProceeding(${index}, 'decisions', this.value)" placeholder="Podjęte ustalenia i decyzje...">${proc.decisions || ''}</textarea>
|
||||
<div class="proceeding-body">
|
||||
<div class="proceeding-field">
|
||||
<label>
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
Omówiono:
|
||||
</label>
|
||||
<textarea class="field-discussion"
|
||||
oninput="updateProceeding(${index}, 'discussion', this.value); updateStatusIcon(this)"
|
||||
placeholder="Opis przebiegu dyskusji w tym punkcie...">${escapeHtml(discussion)}</textarea>
|
||||
</div>
|
||||
<div class="proceeding-field">
|
||||
<label>
|
||||
<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>
|
||||
Ustalono / decyzje:
|
||||
</label>
|
||||
<textarea oninput="updateProceeding(${index}, 'decisions', this.value); updateStatusIcon(this)"
|
||||
placeholder="Każda decyzja w nowej linii...">${escapeHtml(decisionsText)}</textarea>
|
||||
<div class="field-hint">Każda decyzja w osobnej linii</div>
|
||||
</div>
|
||||
<div class="proceeding-field">
|
||||
<label>
|
||||
<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>
|
||||
Zadania:
|
||||
</label>
|
||||
<textarea oninput="updateProceeding(${index}, 'tasks', this.value); updateStatusIcon(this)"
|
||||
placeholder="Każde zadanie w nowej linii (np. AW – przygotować prezentację – termin: 15.02)...">${escapeHtml(tasksText)}</textarea>
|
||||
<div class="field-hint">Każde zadanie w osobnej linii (osoba – opis – termin)</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function toggleProceeding(header) {
|
||||
header.closest('.proceeding-item').classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
function updateStatusIcon(textarea) {
|
||||
const item = textarea.closest('.proceeding-item');
|
||||
const textareas = item.querySelectorAll('textarea');
|
||||
const hasContent = Array.from(textareas).some(ta => ta.value.trim().length > 0);
|
||||
const icon = item.querySelector('.proceeding-status-icon');
|
||||
if (hasContent) {
|
||||
icon.classList.remove('empty');
|
||||
icon.classList.add('filled');
|
||||
icon.innerHTML = '<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>';
|
||||
} else {
|
||||
icon.classList.remove('filled');
|
||||
icon.classList.add('empty');
|
||||
icon.innerHTML = '<circle cx="12" cy="12" r="9"/>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateProceeding(agendaIndex, field, value) {
|
||||
let proc = proceedings.find(p => p.agenda_item === agendaIndex);
|
||||
if (!proc) {
|
||||
proc = { agenda_item: agendaIndex, discussed: '', decisions: '' };
|
||||
proc = { agenda_item: agendaIndex, title: agendaItems[agendaIndex]?.title || '', discussion: '', decisions: [], tasks: [] };
|
||||
proceedings.push(proc);
|
||||
}
|
||||
proc[field] = value;
|
||||
if (field === 'decisions' || field === 'tasks') {
|
||||
proc[field] = textToArray(value);
|
||||
} else {
|
||||
proc[field] = value;
|
||||
// Normalize old field name
|
||||
if (field === 'discussion') {
|
||||
delete proc.discussed;
|
||||
}
|
||||
}
|
||||
proc.title = agendaItems[agendaIndex]?.title || '';
|
||||
updateProceedingsJson();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user