39428-vm/assets/js/main.js
Flatlogic Bot 765d998fa1 V1
2026-04-01 10:36:51 +00:00

161 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', () => {
const keyResultsContainer = document.getElementById('keyResultsContainer');
const addKeyResultButton = document.querySelector('[data-add-key-result]');
const searchInput = document.querySelector('[data-search-input]');
const flash = document.querySelector('.app-flash');
const feedContainers = [...document.querySelectorAll('[data-feed-url]')];
const notificationCount = document.getElementById('notificationCount');
const escapeHtml = (value = '') => String(value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
const formatUtc = (value) => {
if (!value) return '—';
const date = new Date(String(value).replace(' ', 'T') + 'Z');
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
const showToast = (message, type = 'success') => {
if (!message || !window.bootstrap) return;
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
const tone = type === 'danger' ? 'text-bg-dark' : 'text-bg-success';
const toast = document.createElement('div');
toast.className = `toast align-items-center border-0 ${tone}`;
toast.role = 'alert';
toast.ariaLive = 'assertive';
toast.ariaAtomic = 'true';
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${escapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
container.appendChild(toast);
const instance = new bootstrap.Toast(toast, { delay: 3200 });
instance.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
};
if (flash) {
showToast(flash.dataset.flashMessage || '', flash.dataset.flashType || 'success');
}
if (addKeyResultButton && keyResultsContainer) {
addKeyResultButton.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'key-result-row';
row.innerHTML = `
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label class="form-label">Key result</label>
<input class="form-control" name="key_result_title[]" placeholder="Improve weekly OKR review completion rate">
</div>
<div class="col-md-3">
<label class="form-label">Due date</label>
<input class="form-control" type="date" name="key_result_due[]">
</div>
<div class="col-md-1">
<button class="btn btn-outline-secondary w-100" type="button" data-remove-key-result aria-label="Remove key result">×</button>
</div>
</div>
`;
keyResultsContainer.appendChild(row);
});
keyResultsContainer.addEventListener('click', (event) => {
const button = event.target.closest('[data-remove-key-result]');
if (!button) return;
const rows = keyResultsContainer.querySelectorAll('.key-result-row');
if (rows.length <= 1) {
const input = rows[0]?.querySelector('input[name="key_result_title[]"]');
if (input) input.focus();
return;
}
button.closest('.key-result-row')?.remove();
});
}
if (searchInput) {
const tables = [...document.querySelectorAll('[data-search-table]')];
const applySearch = () => {
const term = searchInput.value.trim().toLowerCase();
tables.forEach((table) => {
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row) => {
if (row.querySelector('.empty-state')) {
row.classList.remove('is-hidden-search');
return;
}
const text = row.textContent.toLowerCase();
row.classList.toggle('is-hidden-search', term !== '' && !text.includes(term));
});
});
};
searchInput.addEventListener('input', applySearch);
}
const renderFeed = (notifications) => {
const html = notifications.length
? notifications.map((item) => `
<a href="okr_detail.php?id=${encodeURIComponent(item.objective_id)}" class="activity-item text-decoration-none">
<div class="activity-topline">
<strong>${escapeHtml(item.actor_name || 'System')}</strong>
<span>${escapeHtml(formatUtc(item.time))}</span>
</div>
<div class="activity-text">${escapeHtml(item.message || '')}</div>
<div class="activity-meta">${escapeHtml(item.objective_title || '')}</div>
</a>
`).join('')
: `
<div class="empty-state compact-empty">
<strong>No notifications yet.</strong>
<span>Create the first OKR draft to start the activity stream.</span>
</div>
`;
feedContainers.forEach((container) => {
container.innerHTML = html;
});
if (notificationCount) {
notificationCount.textContent = String(notifications.length);
}
};
const refreshFeed = async () => {
if (feedContainers.length === 0) return;
const url = feedContainers[0].dataset.feedUrl;
if (!url) return;
try {
const response = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
if (!response.ok) return;
const data = await response.json();
if (!data || data.success !== true || !Array.isArray(data.notifications)) return;
renderFeed(data.notifications);
} catch (error) {
console.error('Feed refresh failed', error);
}
};
if (feedContainers.length > 0) {
window.setInterval(refreshFeed, 20000);
}
});