Dokumentit: asiakaskohtaiset kansiot

- Dokumentit-tab näyttää ensin asiakaskansioruudukon (jokainen asiakas = oma kansio)
- Klikkaamalla asiakaskansiota → avaa asiakkaan dokumenttilista
- Takaisin-nappi palaa kansionäkymään
- Asiakas-sarake poistettu dokumenttitaulusta (tarpeeton kansiossa)
- Asiakas-dropdown piilotettu dokumentin luonnissa (valitaan automaattisesti)
- Hakukenttä asiakkaiden suodatukseen kansionäkymässä
- Kansiot järjestetty: ensin eniten dokumentteja, sitten aakkosittain
- URL hash tuki: #documents/customerId

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 09:57:56 +02:00
parent de384b5cb9
commit 711193e1ce
3 changed files with 197 additions and 45 deletions

View File

@@ -686,20 +686,37 @@
<!-- Tab: Dokumentit -->
<div class="tab-content" id="tab-content-documents">
<div class="sub-tab-bar" id="doc-sub-tab-bar">
<button class="sub-tab active" data-doc-subtab="docs-all">Kaikki</button>
<button class="sub-tab" data-doc-subtab="docs-kokoukset">Kokoukset</button>
</div>
<div class="main-container">
<!-- Listanäkymä -->
<div id="docs-list-view">
<!-- Asiakaskansionäkymä -->
<div id="docs-customer-folders-view">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem;">
<h3 style="color:var(--primary-dark);margin:0;">📁 Dokumentit — Asiakaskansiot</h3>
<input type="text" id="doc-folder-search" placeholder="Hae asiakasta..." style="max-width:250px;">
</div>
<div id="doc-customer-folders-grid" class="laitetilat-grid"></div>
<div id="no-doc-folders" class="empty-state" style="display:none;">
<div class="empty-icon">📁</div>
<p>Ei asiakkaita joilla on dokumentteja.</p>
</div>
</div>
<!-- Asiakkaan dokumenttilista -->
<div id="docs-list-view" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem;">
<div style="display:flex;align-items:center;gap:0.75rem;">
<button class="btn-secondary" id="btn-docs-back-to-folders">← Takaisin</button>
<h3 style="color:var(--primary-dark);margin:0;" id="docs-list-title">📄 Dokumentit</h3>
</div>
<div style="display:flex;gap:0.5rem;">
<button class="btn-primary" id="btn-new-document">+ Uusi dokumentti</button>
<button class="btn-primary" id="btn-new-meeting-note" style="display:none;">+ Uusi kokousmuistio</button>
</div>
</div>
<!-- Sub-tabit asiakkaan sisällä -->
<div class="sub-tab-bar" id="doc-sub-tab-bar">
<button class="sub-tab active" data-doc-subtab="docs-all">Kaikki</button>
<button class="sub-tab" data-doc-subtab="docs-kokoukset">Kokoukset</button>
</div>
<!-- Kansionavigointi -->
<div id="doc-folder-bar" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;">
<span id="doc-breadcrumbs" style="font-size:0.9rem;color:#555;"></span>
@@ -708,9 +725,6 @@
<div id="doc-folders-grid" style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;"></div>
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;">
<input type="text" id="doc-search" placeholder="Hae dokumentteja..." style="flex:1;min-width:150px;">
<select id="doc-filter-customer" style="min-width:140px;">
<option value="">Kaikki asiakkaat</option>
</select>
<select id="doc-filter-category" style="min-width:120px;">
<option value="">Kaikki kategoriat</option>
<option value="sopimus">Sopimus</option>
@@ -726,7 +740,6 @@
<thead>
<tr>
<th>Otsikko</th>
<th>Asiakas</th>
<th>Kategoria</th>
<th>Versio</th>
<th>Päivitetty</th>

163
script.js
View File

@@ -307,7 +307,18 @@ function switchToTab(target, subTab) {
switchSupportSubTab('support-tickets');
}
}
if (target === 'documents') { loadDocuments(); showDocsListView(); if (subTab === 'kokoukset') switchDocSubTab('docs-kokoukset'); else switchDocSubTab('docs-all'); }
if (target === 'documents') {
if (subTab && subTab !== 'kokoukset') {
// subTab on customer_id → avaa suoraan asiakkaan kansio
currentDocCustomerId = subTab;
loadDocuments();
openDocCustomerFolder(subTab);
} else {
currentDocCustomerId = null;
loadDocuments();
showDocCustomerFoldersView();
}
}
if (target === 'netadmin') loadNetadmin();
if (target === 'users') loadUsers();
if (target === 'settings') loadSettings();
@@ -4833,19 +4844,31 @@ const docCategoryLabels = {
muu: 'Muu'
};
let currentDocCustomerId = null; // Valittu asiakaskansio
function showDocCustomerFoldersView() {
document.getElementById('docs-customer-folders-view').style.display = '';
document.getElementById('docs-list-view').style.display = 'none';
document.getElementById('doc-read-view').style.display = 'none';
document.getElementById('doc-edit-view').style.display = 'none';
}
function showDocsListView() {
document.getElementById('docs-customer-folders-view').style.display = 'none';
document.getElementById('docs-list-view').style.display = '';
document.getElementById('doc-read-view').style.display = 'none';
document.getElementById('doc-edit-view').style.display = 'none';
}
function showDocReadView() {
document.getElementById('docs-customer-folders-view').style.display = 'none';
document.getElementById('docs-list-view').style.display = 'none';
document.getElementById('doc-read-view').style.display = '';
document.getElementById('doc-edit-view').style.display = 'none';
}
function showDocEditView() {
document.getElementById('docs-customer-folders-view').style.display = 'none';
document.getElementById('docs-list-view').style.display = 'none';
document.getElementById('doc-read-view').style.display = 'none';
document.getElementById('doc-edit-view').style.display = '';
@@ -4855,35 +4878,114 @@ async function loadDocuments() {
try {
allDocuments = await apiCall('documents');
try { allDocFolders = await apiCall('document_folders'); } catch (e2) { allDocFolders = []; }
populateDocCustomerFilter();
if (currentDocCustomerId) {
// Ollaan asiakkaan kansion sisällä → näytä dokumenttilista
renderDocFolderBar();
renderDocumentsList();
} else {
// Näytä asiakaskansiot
renderDocCustomerFolders();
}
} catch (e) { console.error('Dokumenttien lataus epäonnistui:', e); }
}
function populateDocCustomerFilter() {
const sel = document.getElementById('doc-filter-customer');
const existing = sel.value;
// Kerää uniikki lista asiakkaista
const customerMap = {};
function renderDocCustomerFolders() {
const grid = document.getElementById('doc-customer-folders-grid');
const noFolders = document.getElementById('no-doc-folders');
const search = (document.getElementById('doc-folder-search')?.value || '').toLowerCase().trim();
// Hae asiakasnimien map
const customerNameMap = {};
if (typeof customers !== 'undefined') {
customers.forEach(c => { customerNameMap[c.id] = c.yritys; });
}
// Laske dokumenttien määrä per asiakas
const docCountMap = {};
allDocuments.forEach(d => {
if (d.customer_id) {
customerMap[d.customer_id] = d.customer_id; // käytetään myöhemmin nimeä jos saatavilla
docCountMap[d.customer_id] = (docCountMap[d.customer_id] || 0) + 1;
}
});
// Käytä customers-listaa nimien näyttämiseen
sel.innerHTML = '<option value="">Kaikki asiakkaat</option>';
if (typeof customers !== 'undefined' && customers.length > 0) {
// Näytä kaikki asiakkaat joilla on dokumentteja TAI kaikki aktiiviset asiakkaat
let folderList = [];
if (typeof customers !== 'undefined') {
customers.forEach(c => {
sel.innerHTML += `<option value="${c.id}">${esc(c.yritys)}</option>`;
});
} else {
Object.keys(customerMap).forEach(id => {
sel.innerHTML += `<option value="${id}">${id}</option>`;
const count = docCountMap[c.id] || 0;
folderList.push({ id: c.id, name: c.yritys || c.id, count });
});
}
sel.value = existing || '';
// Lisää asiakkaat jotka ovat dokumenteissa mutta eivät customers-listassa
Object.keys(docCountMap).forEach(custId => {
if (!folderList.find(f => f.id === custId)) {
folderList.push({ id: custId, name: customerNameMap[custId] || custId, count: docCountMap[custId] });
}
});
// Suodata hakusanalla
if (search) {
folderList = folderList.filter(f => f.name.toLowerCase().includes(search));
}
// Järjestä: ensin ne joilla on dokumentteja (count desc), sitten aakkosjärjestys
folderList.sort((a, b) => {
if (b.count !== a.count) return b.count - a.count;
return a.name.localeCompare(b.name, 'fi');
});
if (folderList.length === 0) {
grid.innerHTML = '';
noFolders.style.display = '';
return;
}
noFolders.style.display = 'none';
grid.innerHTML = folderList.map(f => `
<div class="doc-customer-folder${f.count === 0 ? ' empty' : ''}" onclick="openDocCustomerFolder('${f.id}')">
<div class="doc-customer-folder-icon">🏢</div>
<div class="doc-customer-folder-name">${esc(f.name)}</div>
<div class="doc-customer-folder-count">${f.count} ${f.count === 1 ? 'dokumentti' : 'dokumenttia'}</div>
</div>
`).join('');
}
function openDocCustomerFolder(customerId) {
currentDocCustomerId = customerId;
currentDocFolderId = null;
docSubTabMode = 'docs-all';
// Aseta otsikko
const customerNameMap = {};
if (typeof customers !== 'undefined') {
customers.forEach(c => { customerNameMap[c.id] = c.yritys; });
}
const name = customerNameMap[customerId] || customerId;
document.getElementById('docs-list-title').textContent = '📄 ' + name;
// Reset sub-tab
document.querySelectorAll('#doc-sub-tab-bar .sub-tab').forEach(t => t.classList.remove('active'));
const allBtn = document.querySelector('[data-doc-subtab="docs-all"]');
if (allBtn) allBtn.classList.add('active');
document.getElementById('btn-new-document').style.display = '';
document.getElementById('btn-new-meeting-note').style.display = 'none';
showDocsListView();
renderDocFolderBar();
renderDocumentsList();
window.location.hash = 'documents/' + customerId;
}
function backToDocCustomerFolders() {
currentDocCustomerId = null;
currentDocFolderId = null;
showDocCustomerFoldersView();
renderDocCustomerFolders();
window.location.hash = 'documents';
}
document.getElementById('btn-docs-back-to-folders')?.addEventListener('click', backToDocCustomerFolders);
document.getElementById('doc-folder-search')?.addEventListener('input', renderDocCustomerFolders);
// ---- Kansionavigointi ----
@@ -4974,11 +5076,15 @@ document.querySelectorAll('#doc-sub-tab-bar .sub-tab').forEach(btn => {
function renderDocumentsList() {
const query = (document.getElementById('doc-search')?.value || '').toLowerCase().trim();
const filterCustomer = document.getElementById('doc-filter-customer')?.value || '';
const filterCategory = document.getElementById('doc-filter-category')?.value || '';
let filtered = allDocuments;
// Suodata valitun asiakkaan perusteella
if (currentDocCustomerId) {
filtered = filtered.filter(d => d.customer_id === currentDocCustomerId);
}
// Sub-tab suodatus: kokoukset = vain kokousmuistiot
if (docSubTabMode === 'docs-kokoukset') {
filtered = filtered.filter(d => d.category === 'kokousmuistio');
@@ -5000,9 +5106,6 @@ function renderDocumentsList() {
(d.description || '').toLowerCase().includes(query)
);
}
if (filterCustomer) {
filtered = filtered.filter(d => d.customer_id === filterCustomer);
}
if (filterCategory && docSubTabMode !== 'docs-kokoukset') {
filtered = filtered.filter(d => d.category === filterCategory);
}
@@ -5017,21 +5120,13 @@ function renderDocumentsList() {
}
noDocsEl.style.display = 'none';
// Hae asiakasnimien map
const customerNameMap = {};
if (typeof customers !== 'undefined') {
customers.forEach(c => { customerNameMap[c.id] = c.yritys; });
}
tbody.innerHTML = filtered.map(d => {
const customerName = d.customer_id ? (customerNameMap[d.customer_id] || d.customer_id) : '<span style="color:#aaa;">Yleinen</span>';
const catLabel = docCategoryLabels[d.category] || d.category || '-';
const version = d.current_version || 0;
const date = d.muokattu ? new Date(d.muokattu).toLocaleDateString('fi-FI') : '-';
const author = d.version_author || d.created_by || '-';
return `<tr onclick="openDocRead('${d.id}')" style="cursor:pointer;">
<td><strong>${esc(d.title)}</strong></td>
<td>${customerName}</td>
<td><span class="doc-category cat-${d.category || 'muu'}">${catLabel}</span></td>
<td style="text-align:center;">v${version}</td>
<td>${date}</td>
@@ -5041,7 +5136,6 @@ function renderDocumentsList() {
}
document.getElementById('doc-search')?.addEventListener('input', renderDocumentsList);
document.getElementById('doc-filter-customer')?.addEventListener('change', renderDocumentsList);
document.getElementById('doc-filter-category')?.addEventListener('change', renderDocumentsList);
async function openDocRead(docId) {
@@ -5267,7 +5361,7 @@ function openDocEdit(doc, forceCategory, forceCustomerId) {
? (isMeeting ? 'Muokkaa kokousmuistiota' : 'Muokkaa dokumenttia')
: (isMeeting ? 'Uusi kokousmuistio' : 'Uusi dokumentti');
// Täytä asiakas-dropdown
// Aseta asiakas automaattisesti nykyisen kansion perusteella
const custSel = document.getElementById('doc-edit-customer');
custSel.innerHTML = '<option value="">Ei asiakasta (yleinen)</option>';
if (typeof customers !== 'undefined') {
@@ -5275,8 +5369,13 @@ function openDocEdit(doc, forceCategory, forceCustomerId) {
custSel.innerHTML += `<option value="${c.id}" ${(forceCustomerId === c.id || doc?.customer_id === c.id) ? 'selected' : ''}>${esc(c.yritys)}</option>`;
});
}
if (forceCustomerId) custSel.value = forceCustomerId;
// Aseta customer_id: kansionäkymästä tai parametrista
const effectiveCustomerId = forceCustomerId || currentDocCustomerId;
if (effectiveCustomerId) custSel.value = effectiveCustomerId;
else if (doc?.customer_id) custSel.value = doc.customer_id;
// Piilota asiakas-dropdown kun ollaan asiakkaan kansiossa (automaattinen valinta)
const custGroup = custSel.closest('.form-group');
if (custGroup) custGroup.style.display = currentDocCustomerId ? 'none' : '';
// Toggle kokousmuistio vs tiedostokenttä
toggleDocMeetingFields(cat);

View File

@@ -1640,6 +1640,46 @@ span.empty {
line-height: 1.6;
}
/* Asiakaskansiot dokumentit-tabissa */
.doc-customer-folder {
background: white;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 1.25rem;
cursor: pointer;
text-align: center;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
}
.doc-customer-folder:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(15,52,96,0.12);
transform: translateY(-2px);
}
.doc-customer-folder.empty {
opacity: 0.5;
border-style: dashed;
}
.doc-customer-folder.empty:hover {
opacity: 0.8;
}
.doc-customer-folder-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.doc-customer-folder-name {
font-weight: 600;
color: var(--primary-dark);
font-size: 0.95rem;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doc-customer-folder-count {
font-size: 0.8rem;
color: #888;
}
/* Asiakasprofiilin dokumentit */
.customer-doc-item {
display: flex;