161 lines
6.7 KiB
JavaScript
161 lines
6.7 KiB
JavaScript
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, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
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);
|
||
}
|
||
});
|