File manager - Edit - /var/www/html/portalHomolog/public/pmar/js/cronogramas.js
Back
/** * @file Módulo Cronogramas (painel administrativo): edição de atividades, Gantt, Kanban, * reordenação, comentários e anexos. Padrões herdados do módulo Linhas do Tempo * (toast, correção de modais/offcanvas no layout admin). */ (function () { 'use strict'; var root = document.getElementById('cronogramas-app'); var D = root ? root.dataset : {}; var csrf = D.csrf || ''; var canEdit = D.canEdit === '1'; var canDelete = D.canDelete === '1'; var currentActivityId = null; /* ------------------------------------------------------------------ */ /* Utilidades */ /* ------------------------------------------------------------------ */ function qs(sel, ctx) { return (ctx || document).querySelector(sel); } function fillUrl(tpl, params) { var url = tpl || ''; params = params || {}; if (params.activity) { url = url.replace('__ACTIVITY__', params.activity); } if (params.comment) { url = url.replace('__COMMENT__', params.comment); } if (params.attachment) { url = url.replace('__ATTACHMENT__', params.attachment); } return url; } function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; } function toast(msg, type) { var el = document.createElement('div'); el.className = 'alert alert-' + (type || 'success') + ' position-fixed bottom-0 end-0 m-3 shadow'; el.style.zIndex = '2000'; el.setAttribute('role', 'status'); el.textContent = msg; document.body.appendChild(el); setTimeout(function () { el.remove(); }, 3200); } function jsonHeaders() { return { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }; } /** * Recarrega a página preservando filtros, aba e páginas das listas (query string atual). * * @param {Object<string, string|null|undefined>} overrides */ function reloadWithCurrentParams(overrides) { var params = new URLSearchParams(window.location.search); if (overrides) { Object.keys(overrides).forEach(function (key) { if (overrides[key] === null || overrides[key] === undefined || overrides[key] === '') { params.delete(key); } else { params.set(key, String(overrides[key])); } }); } var qs = params.toString(); window.location.href = window.location.pathname + (qs ? '?' + qs : ''); } var tableRows = []; var archivedRows = []; var ganttDataFull = []; var rowsDataEl = null; var activitySearchTerm = ''; var searchStorageKey = ''; function loadJsonScript(id) { var el = document.getElementById(id); if (!el) { return []; } try { return JSON.parse(el.textContent || '[]'); } catch (e) { return []; } } function bootPageData() { rowsDataEl = document.getElementById('cr-table-rows'); tableRows = loadJsonScript('cr-table-rows'); archivedRows = loadJsonScript('cr-archived-rows'); ganttDataFull = loadJsonScript('cr-gantt-data'); searchStorageKey = 'cr_activity_search_' + (root ? (root.getAttribute('data-project-id') || '') : ''); } function getCurrentTab() { if (root) { var fromAttr = (root.getAttribute('data-current-tab') || '').trim().toLowerCase(); if (fromAttr) { return fromAttr; } } var active = document.querySelector('.cr-view-tabs .nav-link.active'); if (active && active.href) { try { var tab = new URL(active.href, window.location.origin).searchParams.get('tab'); if (tab) { return tab.trim().toLowerCase(); } } catch (e) { /* ignore */ } } var match = window.location.search.match(/[?&]tab=([^&]+)/i); return match ? decodeURIComponent(match[1]).trim().toLowerCase() : 'table'; } function findRow(id) { for (var i = 0; i < tableRows.length; i++) { if (tableRows[i].id === id) { return tableRows[i]; } } return null; } function normalizeSearchTerm(q) { var s = String(q || '').trim().toLowerCase(); try { return s.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } catch (e) { return s; } } function elementMatchesSearch(el, term) { if (!term) { return true; } return normalizeSearchTerm(el.textContent || '').indexOf(term) !== -1; } function rowSearchHaystack(row) { if (!row) { return ''; } return normalizeSearchTerm([ row.code, row.title, row.responsible, row.status_label, row.status, row.predecessors, row.planned_start_date, row.planned_end_date, ].filter(Boolean).join(' ')); } function rowMatchesSearch(row, term) { if (!term) { return true; } return rowSearchHaystack(row).indexOf(term) !== -1; } function ganttItemMatches(item, term) { if (!term) { return true; } var hay = normalizeSearchTerm([ item.code, item.title, item.responsible, item.status, ].filter(Boolean).join(' ')); return hay.indexOf(term) !== -1; } /** * Correspondência individual: marca apenas as atividades que casam com o * termo, sem expandir pais ou filhos. Assim, filtrar por um responsável * (ex.: "HTS") traz somente as atividades dele, em vez de arrastar pais e * subatividades de outros responsáveis. Mantém coerência com as abas * Kanban/Linha do tempo e com o filtro server-side por responsável. */ function buildVisibleIdSet(rows, term, matcher) { if (!term) { return null; } var visible = {}; rows.forEach(function (row) { if (matcher(row, term)) { visible[row.id] = true; } }); return visible; } function filterGanttData(term) { if (!term) { return ganttDataFull.slice(); } var visible = buildVisibleIdSet(ganttDataFull, term, ganttItemMatches); return ganttDataFull.filter(function (item) { return !!visible[item.id]; }); } function formatSearchCount(visible, total) { var tpl = root ? (root.getAttribute('data-msg-search-count') || ':visible de :total atividade(s)') : ''; return tpl .replace(':visible', String(visible)) .replace(':total', String(total)); } function applyActivitySearch(term) { activitySearchTerm = normalizeSearchTerm(term); var currentTab = getCurrentTab(); var total = tableRows.length; var visible = total; if (currentTab === 'table') { visible = 0; var tableVisible = buildVisibleIdSet(tableRows, activitySearchTerm, rowMatchesSearch); document.querySelectorAll('#cr-activities-tbody tr[data-activity-id]').forEach(function (tr) { var id = tr.getAttribute('data-activity-id'); var show = activitySearchTerm ? (tableVisible && tableVisible[id]) || elementMatchesSearch(tr, activitySearchTerm) : true; tr.classList.toggle('d-none', !show); if (show) { visible++; } }); } else if (currentTab === 'kanban') { visible = 0; document.querySelectorAll('.cr-kanban-card').forEach(function (card) { var row = findRow(card.getAttribute('data-activity-id')); var show = activitySearchTerm ? rowMatchesSearch(row, activitySearchTerm) || elementMatchesSearch(card, activitySearchTerm) : true; card.classList.toggle('d-none', !show); if (show) { visible++; } }); document.querySelectorAll('.cr-kanban-column').forEach(function (col) { var st = col.getAttribute('data-status'); var count = col.querySelectorAll('.cr-kanban-card:not(.d-none)').length; var badge = document.querySelector('.cr-col-badge[data-col-status="' + st + '"]'); if (badge) { badge.textContent = count; } }); } else if (currentTab === 'timeline') { visible = 0; document.querySelectorAll('.cr-timeline-month').forEach(function (month) { var monthVisible = false; month.querySelectorAll('.cr-timeline-item').forEach(function (item) { var row = findRow(item.getAttribute('data-activity-id')); var show = activitySearchTerm ? rowMatchesSearch(row, activitySearchTerm) || elementMatchesSearch(item, activitySearchTerm) : true; item.classList.toggle('d-none', !show); if (show) { monthVisible = true; visible++; } }); month.classList.toggle('d-none', !monthVisible); }); } else if (currentTab === 'gantt') { var filtered = filterGanttData(activitySearchTerm); visible = filtered.length; try { renderGantt(filtered); } catch (e) { /* ignore */ } } var hint = qs('#cr-search-hint'); var clearBtn = qs('#cr-search-clear'); if (clearBtn) { clearBtn.classList.toggle('d-none', !activitySearchTerm); } if (hint) { if (!activitySearchTerm) { hint.classList.add('d-none'); hint.textContent = ''; } else { hint.classList.remove('d-none'); hint.textContent = visible === 0 ? (root.getAttribute('data-msg-no-search-results') || 'Nenhuma atividade encontrada.') : formatSearchCount(visible, total); } } } function initActivitySearch() { var input = document.getElementById('cr-activity-search'); if (!input || input.getAttribute('data-cr-search-bound') === '1') { return; } input.setAttribute('data-cr-search-bound', '1'); var saved = ''; try { saved = sessionStorage.getItem(searchStorageKey) || ''; } catch (e) { /* ignore */ } if (saved) { input.value = saved; } var timer = null; function runSearch() { try { if (searchStorageKey) { sessionStorage.setItem(searchStorageKey, input.value); } } catch (e) { /* ignore */ } applyActivitySearch(input.value); } function onSearchInput() { if (timer) { clearTimeout(timer); } timer = setTimeout(runSearch, 150); } input.addEventListener('input', onSearchInput); input.addEventListener('keyup', onSearchInput); input.addEventListener('search', runSearch); var clearBtn = document.getElementById('cr-search-clear'); if (clearBtn) { clearBtn.addEventListener('click', function () { input.value = ''; try { sessionStorage.removeItem(searchStorageKey); } catch (e) { /* ignore */ } applyActivitySearch(''); input.focus(); }); } runSearch(); } /* ------------------------------------------------------------------ */ /* Correções de modal/offcanvas no layout admin */ /* ------------------------------------------------------------------ */ function moveToBody(selector) { document.querySelectorAll(selector).forEach(function (el) { if (el.parentElement !== document.body) { document.body.appendChild(el); } }); } function cleanupModalArtifacts() { if (!document.querySelector('.modal.show')) { document.querySelectorAll('.modal-backdrop').forEach(function (b) { b.remove(); }); document.body.classList.remove('modal-open'); document.body.style.removeProperty('padding-right'); document.body.style.removeProperty('overflow'); } } function cleanupOffcanvasArtifacts() { if (!document.querySelector('.offcanvas.show')) { document.querySelectorAll('.offcanvas-backdrop').forEach(function (b) { b.remove(); }); } } function initLayoutFixes() { moveToBody('#crModalActivity'); moveToBody('#crModalDeleteProject'); moveToBody('#crOffcanvasDetails'); cleanupModalArtifacts(); cleanupOffcanvasArtifacts(); ['crModalActivity', 'crModalDeleteProject'].forEach(function (id) { var el = document.getElementById(id); if (el) { el.addEventListener('hidden.bs.modal', function () { setTimeout(cleanupModalArtifacts, 0); }); } }); var oc = document.getElementById('crOffcanvasDetails'); if (oc) { oc.addEventListener('hidden.bs.offcanvas', function () { setTimeout(cleanupOffcanvasArtifacts, 0); }); } } /* ------------------------------------------------------------------ */ /* Progresso do projeto */ /* ------------------------------------------------------------------ */ function updateProjectProgress(value) { if (value === undefined || value === null) { return; } var bar = qs('#cr-project-progress'); var label = qs('#cr-project-progress-label'); if (bar) { bar.style.width = value + '%'; var wrap = bar.closest('.progress'); if (wrap) { wrap.setAttribute('aria-valuenow', value); } } if (label) { label.textContent = value; } } function updateKanbanCardProgress(activityId, value) { if (value === undefined || value === null || !activityId) { return; } var card = document.querySelector('.cr-kanban-card[data-activity-id="' + activityId + '"]'); if (!card) { return; } var bar = card.querySelector('.progress-bar'); if (bar) { bar.style.width = value + '%'; } } /** * Sincroniza todos os indicadores visuais após qualquer operação inline. * - Barra de progresso do projeto (header) * - Contadores "Concluídas" e "Atrasadas" (header) * - Badge de contagem de cada coluna do Kanban * - Mini barra de progresso do cartão no Kanban (quando activityId fornecido) */ function writeJsonScript(id, data) { var el = document.getElementById(id); if (el) { el.textContent = JSON.stringify(data); } } function syncActivitiesData(payload) { if (!payload) { return; } if (Array.isArray(payload.table_rows)) { tableRows = payload.table_rows; writeJsonScript('cr-table-rows', tableRows); } if (Array.isArray(payload.gantt)) { ganttDataFull = payload.gantt; writeJsonScript('cr-gantt-data', ganttDataFull); } } function getValidActivityIds() { var valid = {}; tableRows.forEach(function (r) { if (r && r.id) { valid[r.id] = true; } }); return valid; } function purgeStaleActivityDom() { var valid = getValidActivityIds(); document.querySelectorAll('tr[data-activity-id]').forEach(function (tr) { var id = tr.getAttribute('data-activity-id'); if (id && !valid[id]) { tr.remove(); } }); document.querySelectorAll('.cr-kanban-card[data-activity-id]').forEach(function (card) { var id = card.getAttribute('data-activity-id'); if (id && !valid[id]) { card.remove(); } }); document.querySelectorAll('.cr-timeline-item[data-activity-id]').forEach(function (item) { var id = item.getAttribute('data-activity-id'); if (id && !valid[id]) { var month = item.closest('.cr-timeline-month'); item.remove(); if (month && !month.querySelector('.cr-timeline-item')) { month.remove(); } } }); document.querySelectorAll('.cr-btn-details[data-activity-id], .cr-btn-edit[data-activity-id]').forEach(function (el) { var id = el.getAttribute('data-activity-id'); if (id && !valid[id]) { el.remove(); } }); } function refreshActivitiesViews() { purgeStaleActivityDom(); var elTotal = qs('#cr-stat-total'); if (elTotal) { elTotal.textContent = tableRows.length; } var searchInput = document.getElementById('cr-activity-search'); applyActivitySearch(searchInput ? searchInput.value : ''); } function fetchActivitiesData() { var url = D.urlActivitiesData; if (!url) { return Promise.resolve(null); } return fetch(url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, }).then(function (r) { if (!r.ok) { throw new Error('fail'); } return r.json(); }).then(function (data) { if (!data || !data.ok) { throw new Error('fail'); } syncActivitiesData(data); updateAllStats(data); refreshActivitiesViews(); return data; }); } function initActivitiesSync() { window.addEventListener('pageshow', function (ev) { if (ev.persisted) { fetchActivitiesData().catch(function () { /* ignore */ }); } }); } function updateArchivedBadge(count) { var badge = qs('#cr-archived-tab-badge'); if (!badge) { return; } badge.textContent = count; badge.classList.toggle('d-none', !count); } function postActivityAction(url, confirmMsg, onSuccess) { if (!url) { return; } if (confirmMsg && !window.confirm(confirmMsg)) { return; } var body = new URLSearchParams(); body.append('_token', csrf); fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }, body: body }).then(function (r) { return r.json(); }).then(function (data) { if (!data || !data.ok) { throw new Error('fail'); } if (typeof onSuccess === 'function') { onSuccess(data); } }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); } function getSelectedArchivedIds() { var ids = []; document.querySelectorAll('.cr-archived-check:checked').forEach(function (cb) { if (cb.value) { ids.push(cb.value); } }); return ids; } function updateArchivedBulkUi() { var btn = document.getElementById('cr-btn-bulk-delete-archived'); var selectAll = document.getElementById('cr-archived-select-all'); var countBadge = document.getElementById('cr-archived-selected-count'); var hint = document.getElementById('cr-archived-selection-hint'); var checks = document.querySelectorAll('.cr-archived-check'); var selected = getSelectedArchivedIds(); var n = selected.length; if (btn) { btn.disabled = n === 0; } if (countBadge) { countBadge.textContent = String(n); countBadge.classList.toggle('d-none', n === 0); } if (selectAll && checks.length) { selectAll.checked = n > 0 && n === checks.length; selectAll.indeterminate = n > 0 && n < checks.length; } if (hint) { if (n === 0) { hint.textContent = ''; } else if (n === 1) { hint.textContent = root.getAttribute('data-msg-selected-one') || ''; } else { var manyTpl = root.getAttribute('data-msg-selected-many') || ''; hint.textContent = manyTpl.replace(':count', String(n)); } } } function deleteArchivedActivitiesBulk(activityIds) { var url = D.urlActivitiesBulkDestroy; if (!url || !activityIds.length) { return; } var tpl = root.getAttribute('data-msg-confirm-bulk-delete') || ''; var msg = tpl.replace(':count', String(activityIds.length)); if (!window.confirm(msg)) { return; } fetch(url, { method: 'DELETE', headers: jsonHeaders(), body: JSON.stringify({ activity_ids: activityIds }), }).then(function (r) { return r.json(); }).then(function (data) { if (!data || !data.ok) { throw new Error('fail'); } if (data.archived_count !== undefined) { updateArchivedBadge(data.archived_count); } syncActivitiesData(data); updateAllStats(data); toast(root.getAttribute('data-msg-saved')); reloadWithCurrentParams({ tab: 'archived' }); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); } function initArchivedBulkDelete() { if (!canDelete) { return; } var selectAll = document.getElementById('cr-archived-select-all'); var bulkBtn = document.getElementById('cr-btn-bulk-delete-archived'); var tbody = document.getElementById('cr-archived-tbody'); if (selectAll) { selectAll.addEventListener('change', function () { var checked = selectAll.checked; document.querySelectorAll('.cr-archived-check').forEach(function (cb) { cb.checked = checked; }); updateArchivedBulkUi(); }); } if (tbody) { tbody.addEventListener('change', function (e) { if (e.target && e.target.classList && e.target.classList.contains('cr-archived-check')) { updateArchivedBulkUi(); } }); } if (bulkBtn) { bulkBtn.addEventListener('click', function () { deleteArchivedActivitiesBulk(getSelectedArchivedIds()); }); } updateArchivedBulkUi(); } function deleteArchivedActivity(activityId) { var url = fillUrl(D.urlActivityDestroy, { activity: activityId }); var msg = root.getAttribute('data-msg-confirm-delete-activity'); if (!window.confirm(msg)) { return; } var body = new URLSearchParams(); body.append('_token', csrf); body.append('_method', 'DELETE'); fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }, body: body }).then(function (r) { return r.json(); }).then(function (data) { if (!data || !data.ok) { throw new Error('fail'); } if (data.archived_rows) { archivedRows = data.archived_rows; } (data.deleted_ids || [activityId]).forEach(function (id) { var tr = document.querySelector('#cr-archived-tbody tr[data-activity-id="' + id + '"]'); if (tr) { tr.remove(); } }); if (data.archived_count !== undefined) { updateArchivedBadge(data.archived_count); } var tbody = qs('#cr-archived-tbody'); if (tbody && !tbody.querySelector('tr')) { var panel = tbody.closest('.tab-pane') || tbody.closest('[role="tabpanel"]'); if (panel) { panel.innerHTML = '<div class="text-center text-muted py-5 border rounded">' + '<i class="fa-solid fa-box-archive fa-2x mb-2 d-block"></i>' + escapeHtml(root.getAttribute('data-msg-no-archived') || '') + '</div>'; } } toast(root.getAttribute('data-msg-saved')); reloadWithCurrentParams(); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); } function initActivityLifecycle() { if (!root) { return; } root.addEventListener('click', function (e) { var archiveBtn = e.target.closest('.cr-btn-archive'); if (archiveBtn) { e.preventDefault(); var archiveId = archiveBtn.getAttribute('data-activity-id'); postActivityAction( fillUrl(D.urlActivityArchive, { activity: archiveId }), root.getAttribute('data-msg-confirm-archive-activity'), function (data) { if (data.archived_count !== undefined) { updateArchivedBadge(data.archived_count); } reloadWithCurrentParams(); } ); return; } var restoreBtn = e.target.closest('.cr-btn-restore'); if (restoreBtn) { e.preventDefault(); var restoreId = restoreBtn.getAttribute('data-activity-id'); postActivityAction( fillUrl(D.urlActivityRestore, { activity: restoreId }), root.getAttribute('data-msg-confirm-restore-activity'), function () { reloadWithCurrentParams({ tab: 'table' }); } ); return; } var deleteBtn = e.target.closest('.cr-btn-delete-archived'); if (deleteBtn) { e.preventDefault(); deleteArchivedActivity(deleteBtn.getAttribute('data-activity-id')); } }); } function updateAllStats(data, activityId) { // barra de progresso do projeto updateProjectProgress(data.project_completion); // mini barra do cartão no Kanban if (activityId && data.activity) { updateKanbanCardProgress(activityId, data.activity.completion_percent); } var stats = data.stats; if (!stats) { return; } // contadores do cabeçalho var elTotal = qs('#cr-stat-total'); if (elTotal && stats.total_count !== undefined) { elTotal.textContent = stats.total_count; } var elCompleted = qs('#cr-stat-completed'); if (elCompleted) { elCompleted.textContent = stats.completed_count !== undefined ? stats.completed_count : elCompleted.textContent; } var elOverdue = qs('#cr-stat-overdue'); if (elOverdue) { elOverdue.textContent = stats.overdue_count !== undefined ? stats.overdue_count : elOverdue.textContent; } if (data.archived_count !== undefined) { updateArchivedBadge(data.archived_count); } // badges de coluna do Kanban (status persistido; "Atrasado" é derivado e não tem coluna) var statusCounts = stats.status_counts || {}; document.querySelectorAll('.cr-col-badge[data-col-status]').forEach(function (badge) { var colStatus = badge.getAttribute('data-col-status'); badge.textContent = statusCounts[colStatus] || 0; }); } function syncRowOverdueBadge(tr, isOverdue) { if (!tr) { return; } tr.classList.toggle('table-danger', !!isOverdue); var badge = tr.querySelector('.cr-status-overdue-badge'); if (badge) { badge.classList.toggle('d-none', !isOverdue); } } /* ------------------------------------------------------------------ */ /* Modal de atividade (criar / editar) - submit normal, JS só prefill */ /* ------------------------------------------------------------------ */ /* ---- Atividade pai: habilitar/desabilitar opção da própria atividade ---- */ function enableAllOptions() { var el = qs('#cr-f-parent'); if (!el) { return; } Array.prototype.forEach.call(el.options, function (opt) { opt.disabled = false; }); } function disableSelfOption(id) { var el = qs('#cr-f-parent'); if (!el) { return; } Array.prototype.forEach.call(el.options, function (opt) { opt.disabled = (opt.value === id); }); } function autosaveKey(id) { return 'cr_activity_' + (D.urlActivityStore || 'p') + '_' + (id || 'new'); } /* ---- Responsáveis (múltiplos) ---- */ var responsiblesState = []; function showResponsiblePicker(type) { document.querySelectorAll('.cr-responsible-picker').forEach(function (el) { var isActive = el.getAttribute('data-responsible-type') === type; el.classList.toggle('d-none', !isActive); el.hidden = !isActive; }); } function typeLabelFor(type) { var typeSel = qs('#cr-f-responsible-type'); var label = type; if (typeSel) { Array.prototype.forEach.call(typeSel.options, function (o) { if (o.value === type) { label = o.textContent.trim(); } }); } return label; } function entityLabelFor(type, id) { var picker = qs('#cr-f-responsible-' + type); var label = ''; if (picker) { Array.prototype.forEach.call(picker.options, function (o) { if (o.value === id) { label = o.textContent.trim(); } }); } return label; } function renderResponsibles() { var list = qs('#cr-f-responsibles-list'); var hidden = qs('#cr-f-responsibles-hidden'); if (!list || !hidden) { return; } list.innerHTML = ''; hidden.innerHTML = ''; responsiblesState.forEach(function (r, i) { var chip = document.createElement('span'); chip.className = 'badge text-bg-light border cr-responsible-chip d-inline-flex align-items-center gap-1'; var txt = document.createElement('span'); txt.className = 'small fw-normal'; txt.textContent = (r.typeLabel ? r.typeLabel + ': ' : '') + r.label; chip.appendChild(txt); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn-close'; btn.setAttribute('aria-label', root.getAttribute('data-label-remove') || 'Remover'); btn.addEventListener('click', function () { responsiblesState = responsiblesState.filter(function (item) { return !(item.type === r.type && item.id === r.id); }); renderResponsibles(); }); chip.appendChild(btn); list.appendChild(chip); var hType = document.createElement('input'); hType.type = 'hidden'; hType.name = 'responsibles[' + i + '][type]'; hType.value = r.type; hidden.appendChild(hType); var hId = document.createElement('input'); hId.type = 'hidden'; hId.name = 'responsibles[' + i + '][id]'; hId.value = r.id; hidden.appendChild(hId); }); } function addResponsible(type, id) { if (!type || !id) { return; } var exists = responsiblesState.some(function (r) { return r.type === type && r.id === id; }); if (exists) { return; } responsiblesState.push({ type: type, id: id, label: entityLabelFor(type, id), typeLabel: typeLabelFor(type) }); renderResponsibles(); } function addResponsibleFromAdder() { var typeSel = qs('#cr-f-responsible-type'); var type = typeSel ? typeSel.value : 'user'; var picker = qs('#cr-f-responsible-' + type); var id = picker ? picker.value : ''; if (!id) { return; } addResponsible(type, id); if (picker) { picker.value = ''; } } function setResponsiblesState(items) { responsiblesState = []; (items || []).forEach(function (it) { if (!it || !it.id || !it.type) { return; } responsiblesState.push({ type: it.type, id: it.id, label: it.label || entityLabelFor(it.type, it.id), typeLabel: it.typeLabel || typeLabelFor(it.type) }); }); renderResponsibles(); } function initResponsibleAdder() { var typeSel = qs('#cr-f-responsible-type'); if (typeSel) { typeSel.addEventListener('change', function () { showResponsiblePicker(typeSel.value); }); showResponsiblePicker(typeSel.value); } var addBtn = qs('#cr-f-responsible-add'); if (addBtn) { addBtn.addEventListener('click', addResponsibleFromAdder); } } /* ---- Predecessoras (lista pesquisável com checkboxes) ---- */ function getCheckedPredecessorIds() { var ids = []; document.querySelectorAll('.cr-pred-check:checked').forEach(function (cb) { ids.push(cb.value); }); return ids; } function updatePredItemState(cb) { var item = cb.closest('.cr-pred-item'); if (item) { item.classList.toggle('cr-pred-checked', cb.checked); } } function updatePredecessorCount() { var c = qs('#cr-f-pred-count'); if (c) { c.textContent = getCheckedPredecessorIds().length; } } function setCheckedPredecessors(ids) { var set = {}; (ids || []).forEach(function (v) { set[v] = true; }); document.querySelectorAll('.cr-pred-check').forEach(function (cb) { cb.checked = !!set[cb.value]; updatePredItemState(cb); }); updatePredecessorCount(); } function clearPredecessors() { document.querySelectorAll('.cr-pred-check').forEach(function (cb) { cb.checked = false; updatePredItemState(cb); }); var search = qs('#cr-f-pred-search'); if (search) { search.value = ''; } filterPredecessors(''); updatePredecessorCount(); } function filterPredecessors(term) { term = (term || '').toLowerCase().trim(); var anyVisible = false; document.querySelectorAll('#cr-f-pred-list .cr-pred-item').forEach(function (item) { var text = item.getAttribute('data-pred-text') || ''; var show = term === '' || text.indexOf(term) !== -1; item.classList.toggle('d-none', !show); if (show) { anyVisible = true; } }); var empty = qs('#cr-f-pred-empty'); if (empty) { empty.classList.toggle('d-none', anyVisible); } } function disablePredecessorSelf(id) { document.querySelectorAll('.cr-pred-check').forEach(function (cb) { var isSelf = cb.value === id; cb.disabled = isSelf; if (isSelf) { cb.checked = false; updatePredItemState(cb); } var item = cb.closest('.cr-pred-item'); if (item) { item.classList.toggle('cr-pred-self-hidden', isSelf); item.classList.toggle('d-none', isSelf); } }); } function initPredecessors() { var search = qs('#cr-f-pred-search'); if (search) { search.addEventListener('input', function () { filterPredecessors(search.value); }); } document.querySelectorAll('.cr-pred-check').forEach(function (cb) { cb.addEventListener('change', function () { updatePredItemState(cb); updatePredecessorCount(); }); }); } function collectActivityForm() { return { code: valOf('#cr-f-code'), title: valOf('#cr-f-title'), description: valOf('#cr-f-description'), parent_id: valOf('#cr-f-parent'), responsibles: responsiblesState.map(function (r) { return { type: r.type, id: r.id, label: r.label, typeLabel: r.typeLabel }; }), status: valOf('#cr-f-status'), predecessor_ids: getCheckedPredecessorIds(), planned_start_date: valOf('#cr-f-start'), planned_end_date: valOf('#cr-f-end'), notes: valOf('#cr-f-notes') }; } function valOf(sel) { var el = qs(sel); return el ? el.value : ''; } function setVal(sel, v) { var el = qs(sel); if (el) { el.value = v == null ? '' : v; } } function initActivityModal() { var form = qs('#cr-activity-form'); if (!form) { return; } var titleEl = qs('#crModalActivityLabel'); var methodEl = qs('#cr-activity-method'); var autosaveEl = qs('#cr-activity-autosave'); var editingId = null; function resetForCreate() { editingId = null; form.setAttribute('action', D.urlActivityStore); if (methodEl) { methodEl.value = 'POST'; } if (titleEl) { titleEl.textContent = form.getAttribute('data-label-new') || 'Nova atividade'; } form.reset(); enableAllOptions(); clearPredecessors(); setResponsiblesState([]); setVal('#cr-f-status', 'not_started'); // restaura rascunho local (apenas criação) try { var raw = localStorage.getItem(autosaveKey(null)); if (raw) { applyData(JSON.parse(raw)); if (autosaveEl) { autosaveEl.textContent = ''; } } } catch (e) { /* ignore */ } } function applyData(d) { if (!d) { return; } setVal('#cr-f-code', d.code); setVal('#cr-f-title', d.title); setVal('#cr-f-description', d.description); setVal('#cr-f-parent', d.parent_id); setResponsiblesState(d.responsibles || []); setVal('#cr-f-status', d.status || 'not_started'); setVal('#cr-f-start', d.planned_start_date || d.actual_start_date || ''); setVal('#cr-f-end', d.planned_end_date || d.actual_end_date || ''); setVal('#cr-f-notes', d.notes); setCheckedPredecessors(d.predecessor_ids || []); } function fillForEdit(row) { editingId = row.id; form.reset(); form.setAttribute('action', fillUrl(D.urlActivityUpdate, { activity: row.id })); if (methodEl) { methodEl.value = 'PUT'; } if (titleEl) { titleEl.textContent = (form.getAttribute('data-label-edit') || 'Editar atividade') + ' - ' + row.code; } enableAllOptions(); applyData(row); // remove a própria atividade das opções de pai/predecessora disableSelfOption(row.id); disablePredecessorSelf(row.id); } var newBtn = qs('#cr-btn-new-activity'); if (newBtn) { newBtn.addEventListener('click', resetForCreate); } document.querySelectorAll('.cr-btn-edit').forEach(function (btn) { btn.addEventListener('click', function () { var row = findRow(btn.getAttribute('data-activity-id')); if (row) { fillForEdit(row); } }); }); // autosave local apenas durante a criação var saveTimer = null; form.addEventListener('input', function () { if (editingId) { return; } if (saveTimer) { clearTimeout(saveTimer); } saveTimer = setTimeout(function () { try { localStorage.setItem(autosaveKey(null), JSON.stringify(collectActivityForm())); if (autosaveEl) { autosaveEl.textContent = (root.getAttribute('data-msg-saved') ? 'Rascunho salvo' : 'Rascunho salvo') + ' ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } } catch (e) { /* ignore */ } }, 600); }); form.addEventListener('submit', function () { if (!editingId) { try { localStorage.removeItem(autosaveKey(null)); } catch (e) { /* ignore */ } } }); initResponsibleAdder(); initPredecessors(); } /* ------------------------------------------------------------------ */ /* Edição inline (status / progresso) */ /* ------------------------------------------------------------------ */ function patchInline(activityId, payload) { return fetch(fillUrl(D.urlActivityInline, { activity: activityId }), { method: 'PATCH', headers: jsonHeaders(), body: JSON.stringify(payload) }).then(function (r) { if (!r.ok) { throw new Error('fail'); } return r.json(); }); } function initInlineEdits() { if (!canEdit) { return; } document.querySelectorAll('.cr-inline-status').forEach(function (el) { el.addEventListener('change', function () { var tr = el.closest('tr'); var id = tr && tr.getAttribute('data-activity-id'); if (!id) { return; } patchInline(id, { status: el.value }).then(function (data) { updateAllStats(data, null); if (data.activity) { syncRowProgress(tr, data.activity.completion_percent); syncRowOverdueBadge(tr, data.activity.is_overdue); } toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); window.location.reload(); }); }); }); } function syncRowProgress(tr, value) { var bar = tr.querySelector('.cr-row-progress .progress-bar'); if (bar) { bar.style.width = value + '%'; } var label = tr.querySelector('.cr-row-progress-label'); if (label) { label.textContent = Math.round(value) + '%'; } } /* ------------------------------------------------------------------ */ /* Reordenação por arrastar (tabela) */ /* ------------------------------------------------------------------ */ function initReorder() { if (!canEdit || D.allowReorder !== '1') { return; } var tbody = qs('#cr-activities-tbody'); if (!tbody) { return; } var dragRow = null; tbody.querySelectorAll('.cr-drag-handle').forEach(function (h) { h.addEventListener('mousedown', function () { var tr = h.closest('tr'); if (tr) { tr.setAttribute('draggable', 'true'); } }); }); tbody.addEventListener('dragstart', function (e) { dragRow = e.target.closest('tr'); if (dragRow) { dragRow.classList.add('cr-dragging'); e.dataTransfer.effectAllowed = 'move'; } }); tbody.addEventListener('dragover', function (e) { e.preventDefault(); var over = e.target.closest('tr'); if (!over || !dragRow || over === dragRow) { return; } var rect = over.getBoundingClientRect(); var after = (e.clientY - rect.top) > rect.height / 2; tbody.insertBefore(dragRow, after ? over.nextSibling : over); }); tbody.addEventListener('dragend', function () { if (!dragRow) { return; } dragRow.classList.remove('cr-dragging'); dragRow.removeAttribute('draggable'); dragRow = null; var order = []; tbody.querySelectorAll('tr[data-activity-id]').forEach(function (tr) { order.push(tr.getAttribute('data-activity-id')); }); fetch(D.urlActivityReorder, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ order: order }) }).then(function (r) { if (r.ok) { toast(root.getAttribute('data-msg-saved')); } else { throw new Error('fail'); } }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); }); } /* ------------------------------------------------------------------ */ /* Kanban (arrastar e soltar) */ /* ------------------------------------------------------------------ */ function initKanban() { if (!canEdit) { return; } var dragged = null; document.querySelectorAll('.cr-kanban-card').forEach(function (card) { card.addEventListener('dragstart', function (e) { dragged = card; e.dataTransfer.effectAllowed = 'move'; }); }); document.querySelectorAll('.cr-kanban-column[data-droppable="1"]').forEach(function (col) { col.addEventListener('dragover', function (e) { e.preventDefault(); col.classList.add('drag-over'); }); col.addEventListener('dragleave', function () { col.classList.remove('drag-over'); }); col.addEventListener('drop', function (e) { e.preventDefault(); col.classList.remove('drag-over'); if (!dragged) { return; } var status = col.dataset.status; var id = dragged.getAttribute('data-activity-id'); col.appendChild(dragged); dragged = null; patchInline(id, { status: status }).then(function (data) { updateAllStats(data, id); toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); window.location.reload(); }); }); }); } /* ------------------------------------------------------------------ */ /* Gantt v2 */ /* ------------------------------------------------------------------ */ /** * Converte Y-m-d em Date local (evita deslocamento de fuso com new Date('Y-m-d')). */ var GANTT_LAYOUT_KEY = 'cronogramas.gantt.layout.v1'; var GANTT_DEFAULT_COLS = [ { key: 'code', label: 'EAP', w: 52, minW: 40, align: 'center' }, { key: 'title', label: 'Atividades', w: 320, minW: 140, align: 'left' }, { key: 'responsible', label: 'Resp.', w: 96, minW: 64, align: 'left' }, { key: 'start', label: 'Início', w: 84, minW: 68, align: 'center' }, { key: 'end', label: 'Término', w: 84, minW: 68, align: 'center' }, { key: 'progress', label: '% Concl.', w: 62, minW: 52, align: 'center' }, ]; function cloneGanttCols() { return GANTT_DEFAULT_COLS.map(function (c) { return { key: c.key, label: c.label, w: c.w, minW: c.minW, align: c.align }; }); } function loadGanttLayout() { try { var raw = localStorage.getItem(GANTT_LAYOUT_KEY); if (!raw) return null; return JSON.parse(raw); } catch (e) { return null; } } function saveGanttLayout(cols, rowH) { var colWidths = {}; cols.forEach(function (c) { colWidths[c.key] = c.w; }); try { localStorage.setItem(GANTT_LAYOUT_KEY, JSON.stringify({ colWidths: colWidths, rowH: rowH, })); } catch (e) { /* quota / modo privado */ } } function applySavedGanttLayout(cols, saved) { if (!saved || !saved.colWidths) return cols; return cols.map(function (c) { if (saved.colWidths[c.key]) { c.w = Math.max(c.minW || 40, parseInt(saved.colWidths[c.key], 10) || c.w); } return c; }); } function ganttLeftWidth(cols) { return cols.reduce(function (s, c) { return s + c.w; }, 0); } function ganttSetW(el, w) { el.style.width = w + 'px'; el.style.minWidth = w + 'px'; el.style.maxWidth = w + 'px'; } function ganttApplyColumnWidths(leftPanel, cols) { var lHdr = leftPanel.querySelector('.cr-gantt-v2-lhdr'); if (!lHdr) return; var ths = lHdr.querySelectorAll('.cr-gantt-v2-th'); ths.forEach(function (th, i) { if (cols[i]) ganttSetW(th, cols[i].w); }); leftPanel.querySelectorAll('.cr-gantt-v2-lrow').forEach(function (row) { row.querySelectorAll('.cr-gantt-v2-td').forEach(function (td, i) { if (cols[i]) ganttSetW(td, cols[i].w); }); }); } function ganttApplyRowHeights(leftPanel, rightPanel, rowH) { leftPanel.querySelectorAll('.cr-gantt-v2-lrow').forEach(function (row) { row.style.height = rowH + 'px'; row.querySelectorAll('.cr-gantt-v2-td').forEach(function (td) { td.style.height = rowH + 'px'; }); }); if (rightPanel) { rightPanel.querySelectorAll('.cr-gantt-v2-grow').forEach(function (row) { row.style.height = rowH + 'px'; }); } } function ganttUpdateWrapMinWidth(wrap, cols, totalW) { wrap.style.minWidth = (ganttLeftWidth(cols) + totalW) + 'px'; } function attachGanttResizers(wrap, leftPanel, rightPanel, lHdr, cols, hdrH, totalW, layoutState) { var colTitle = root ? (root.getAttribute('data-msg-gantt-resize-col') || 'Arraste para ajustar a largura da coluna') : ''; var rowTitle = root ? (root.getAttribute('data-msg-gantt-resize-row') || 'Arraste para ajustar a altura das linhas') : ''; lHdr.querySelectorAll('.cr-gantt-v2-th').forEach(function (th, colIndex) { var handle = document.createElement('div'); handle.className = 'cr-gantt-v2-col-resize'; handle.setAttribute('role', 'separator'); handle.setAttribute('aria-orientation', 'vertical'); handle.setAttribute('aria-label', colTitle); handle.title = colTitle; th.appendChild(handle); handle.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); var startX = e.clientX; var startW = cols[colIndex].w; var minW = cols[colIndex].minW || 40; function onMove(ev) { var newW = Math.max(minW, startW + (ev.clientX - startX)); cols[colIndex].w = newW; ganttSetW(leftPanel, ganttLeftWidth(cols)); ganttApplyColumnWidths(leftPanel, cols); ganttUpdateWrapMinWidth(wrap, cols, totalW); } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.classList.remove('cr-gantt-resizing-col'); saveGanttLayout(cols, layoutState.rowH); } document.body.classList.add('cr-gantt-resizing-col'); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); }); var rowHandle = document.createElement('div'); rowHandle.className = 'cr-gantt-v2-row-resize'; rowHandle.setAttribute('role', 'separator'); rowHandle.setAttribute('aria-orientation', 'horizontal'); rowHandle.setAttribute('aria-label', rowTitle); rowHandle.title = rowTitle; lHdr.appendChild(rowHandle); rowHandle.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); var startY = e.clientY; var startH = layoutState.rowH; var minH = 24; var maxH = 96; function onMove(ev) { var newH = Math.min(maxH, Math.max(minH, startH + (ev.clientY - startY))); layoutState.rowH = newH; ganttApplyRowHeights(leftPanel, rightPanel, newH); } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.classList.remove('cr-gantt-resizing-row'); saveGanttLayout(cols, layoutState.rowH); } document.body.classList.add('cr-gantt-resizing-row'); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); } function parseLocalDate(dateStr) { if (!dateStr) return null; var p = String(dateStr).split('-'); if (p.length !== 3) return null; var y = parseInt(p[0], 10); var m = parseInt(p[1], 10) - 1; var d = parseInt(p[2], 10); if (isNaN(y) || isNaN(m) || isNaN(d)) return null; return new Date(y, m, d, 0, 0, 0, 0); } /** * Normaliza par início/término: término nunca pode ser anterior ao início. */ function normalizeGanttDates(item) { var start = item.start || null; var end = item.end || null; if (start && end) { var ds = parseLocalDate(start); var de = parseLocalDate(end); if (ds && de && de < ds) { end = start; } } if (!start && end) start = end; if (start && !end) end = start; return { start: start, end: end }; } function renderGantt(dataOverride) { var container = qs('#cr-gantt-container'); if (!container) return; var data; if (Array.isArray(dataOverride)) { data = dataOverride; } else if (activitySearchTerm) { data = filterGanttData(activitySearchTerm); } else { data = ganttDataFull.slice(); } container.innerHTML = ''; if (!data.length) { var emptyMsg = activitySearchTerm && root ? (root.getAttribute('data-msg-no-search-results') || 'Nenhuma atividade encontrada.') : 'Sem atividades cadastradas.'; container.innerHTML = '<p class="text-muted text-center py-4 mb-0">' + escapeHtml(emptyMsg) + '</p>'; return; } // ── Zoom: pixels por dia ────────────────────────────────────────── var zoomBtn = document.querySelector('.cr-gantt-zoom.active'); var zoom = zoomBtn ? (zoomBtn.dataset.zoom || 'month') : 'month'; var PX_DAY = zoom === 'day' ? 24 : zoom === 'week' ? 9 : 4; var savedLayout = loadGanttLayout(); var layoutState = { rowH: (savedLayout && savedLayout.rowH) ? Math.min(96, Math.max(24, parseInt(savedLayout.rowH, 10) || 28)) : 28, }; var ROW_H = layoutState.rowH; var HDR_H = 36; var MS_DAY = 86400000; // ── Intervalo de datas (somente pares válidos) ───────────────────── var allTs = [Date.now()]; data.forEach(function (i) { var norm = normalizeGanttDates(i); if (norm.start) allTs.push(+parseLocalDate(norm.start)); if (norm.end) allTs.push(+parseLocalDate(norm.end)); }); var tsMin = Math.min.apply(null, allTs); var tsMax = Math.max.apply(null, allTs); var rStart = new Date(tsMin); rStart.setDate(1); rStart.setHours(0, 0, 0, 0); var rEnd = new Date(tsMax); rEnd.setMonth(rEnd.getMonth() + 1, 1); rEnd.setHours(0, 0, 0, 0); // Margem de 1 mês antes/depois para leitura rStart.setMonth(rStart.getMonth() - 1); function toX(dateInput) { var d = dateInput instanceof Date ? new Date(dateInput.getTime()) : parseLocalDate(dateInput); if (!d) return 0; d.setHours(0, 0, 0, 0); return Math.round((+d - +rStart) / MS_DAY) * PX_DAY; } // ── Meses para o cabeçalho ──────────────────────────────────────── var PT_MON = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']; var months = []; var mCur = new Date(rStart); while (+mCur < +rEnd) { var mNext = new Date(mCur.getFullYear(), mCur.getMonth() + 1, 1); var mDays = Math.round((+mNext - +mCur) / MS_DAY); months.push({ label: PT_MON[mCur.getMonth()] + ' ' + mCur.getFullYear(), x : Math.round((+mCur - +rStart) / MS_DAY) * PX_DAY, w : mDays * PX_DAY }); mCur = mNext; } var totalW = months.reduce(function (s, m) { return Math.max(s, m.x + m.w); }, 0); var todayX = toX(new Date()); // Colunas do painel esquerdo (larguras persistidas no navegador) var COLS = applySavedGanttLayout(cloneGanttCols(), savedLayout); var leftW = ganttLeftWidth(COLS); function fmtDate(d) { if (!d) return '-'; var p = parseLocalDate(d); if (!p) return '-'; return ('0' + p.getDate()).slice(-2) + '/' + ('0' + (p.getMonth() + 1)).slice(-2) + '/' + p.getFullYear(); } function mk(tag, cls, styles) { var e = document.createElement(tag); if (cls) e.className = cls; if (styles) Object.keys(styles).forEach(function (k) { e.style[k] = styles[k]; }); return e; } function setW(el, w) { ganttSetW(el, w); } // ── Wrapper externo - largura mínima força scroll horizontal ──────── var wrap = mk('div', 'cr-gantt-v2'); wrap.style.minWidth = (leftW + totalW) + 'px'; // ════════════════════════════════════════════════════════════════ // PAINEL ESQUERDO // ════════════════════════════════════════════════════════════════ var leftPanel = mk('div', 'cr-gantt-v2-left'); setW(leftPanel, leftW); // cabeçalho var lHdr = mk('div', 'cr-gantt-v2-lhdr'); lHdr.style.height = HDR_H + 'px'; COLS.forEach(function (c) { var th = mk('div', 'cr-gantt-v2-th'); setW(th, c.w); th.style.height = HDR_H + 'px'; if (c.align === 'center') th.style.justifyContent = 'center'; th.textContent = c.label; th.title = c.label; lHdr.appendChild(th); }); leftPanel.appendChild(lHdr); // linhas data.forEach(function (item) { var isParent = !item.parent_id; var dates = normalizeGanttDates(item); var row = mk('div', 'cr-gantt-v2-lrow' + (isParent ? ' is-parent' : '')); row.style.height = ROW_H + 'px'; COLS.forEach(function (c) { var td = mk('div', 'cr-gantt-v2-td'); setW(td, c.w); td.style.height = ROW_H + 'px'; if (c.align === 'center') { td.classList.add('cr-gantt-td-center'); } var val; var skipDefaultText = false; switch (c.key) { case 'start' : val = fmtDate(dates.start); break; case 'end' : val = fmtDate(dates.end); break; case 'progress' : val = item.progress + '%'; break; case 'title' : val = item.title || '-'; td.classList.add('cr-gantt-td-title'); td.style.paddingLeft = isParent ? '8px' : '20px'; if (isParent) td.classList.add('fw-semibold'); var titleSpan = document.createElement('span'); titleSpan.className = 'cr-gantt-td-title-text'; titleSpan.textContent = val; titleSpan.title = val; td.appendChild(titleSpan); td.title = val; skipDefaultText = true; break; default: val = item[c.key] || '-'; } if (!skipDefaultText) { td.textContent = val; td.title = val; } row.appendChild(td); }); leftPanel.appendChild(row); }); wrap.appendChild(leftPanel); // ════════════════════════════════════════════════════════════════ // PAINEL DIREITO (grade) // ════════════════════════════════════════════════════════════════ var rightPanel = mk('div', 'cr-gantt-v2-right'); setW(rightPanel, totalW); // cabeçalho de meses var gHdr = mk('div', 'cr-gantt-v2-ghdr'); gHdr.style.height = HDR_H + 'px'; gHdr.style.width = totalW + 'px'; months.forEach(function (m, idx) { var cell = mk('div', 'cr-gantt-v2-mcell'); cell.style.left = m.x + 'px'; cell.style.width = m.w + 'px'; cell.style.height = HDR_H + 'px'; cell.textContent = m.label; cell.title = m.label; if (idx % 2 === 1) cell.style.background = 'rgba(255,255,255,0.09)'; gHdr.appendChild(cell); }); rightPanel.appendChild(gHdr); // corpo da grade var gBody = mk('div', 'cr-gantt-v2-gbody'); gBody.style.width = totalW + 'px'; // listras alternadas por mês months.forEach(function (m, idx) { if (idx % 2 === 1) { var s = mk('div', 'cr-gantt-v2-stripe'); s.style.left = m.x + 'px'; s.style.width = m.w + 'px'; gBody.appendChild(s); } }); // divisórias verticais de mês months.forEach(function (m) { var d = mk('div', 'cr-gantt-v2-mdiv'); d.style.left = m.x + 'px'; gBody.appendChild(d); }); // linha do hoje if (todayX >= 0 && todayX <= totalW) { var tl = mk('div', 'cr-gantt-v2-today'); tl.style.left = todayX + 'px'; tl.title = 'Hoje'; gBody.appendChild(tl); } // barras por atividade data.forEach(function (item) { var isParent = !item.parent_id; var dates = normalizeGanttDates(item); var row = mk('div', 'cr-gantt-v2-grow' + (isParent ? ' is-parent' : '')); row.style.height = ROW_H + 'px'; row.style.width = totalW + 'px'; if (dates.start) { var endD = dates.end || dates.start; var bx = toX(dates.start); var bex = toX(endD) + PX_DAY; var bw = Math.max(bex - bx, PX_DAY); var barCls = 'cr-gantt-v2-bar'; if (isParent) barCls += ' g-parent'; else if (item.overdue) barCls += ' g-overdue'; else if (item.progress >= 100) barCls += ' g-done'; var bar = mk('div', barCls); bar.style.left = bx + 'px'; bar.style.width = bw + 'px'; bar.title = (item.code ? item.code + ' - ' : '') + item.title + ' (' + item.progress + '%)'; if (!isParent) { if (item.progress > 0) { var fill = mk('div', 'cr-gantt-v2-fill'); fill.style.width = Math.min(item.progress, 100) + '%'; bar.appendChild(fill); } if (bw >= 30 && item.progress > 0) { var pct = mk('span', 'cr-gantt-v2-pct'); pct.textContent = item.progress + '%'; bar.appendChild(pct); } } row.appendChild(bar); } gBody.appendChild(row); }); rightPanel.appendChild(gBody); wrap.appendChild(rightPanel); container.appendChild(wrap); attachGanttResizers(wrap, leftPanel, rightPanel, lHdr, COLS, HDR_H, totalW, layoutState); } function initGantt() { document.querySelectorAll('.cr-gantt-zoom').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('.cr-gantt-zoom').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); renderGantt(); }); }); renderGantt(); } /* ------------------------------------------------------------------ */ /* Offcanvas de detalhes (comentários, anexos, histórico) */ /* ------------------------------------------------------------------ */ function setBadge(selector, activityId, value) { var el = document.querySelector(selector + '[data-activity-id="' + activityId + '"]'); if (el) { el.textContent = value ? value : ''; } } function renderComments(list) { var ul = qs('#cr-comments-list'); if (!ul) { return; } ul.innerHTML = ''; if (!list || !list.length) { ul.innerHTML = '<li class="list-group-item text-muted">' + escapeHtml(root.getAttribute('data-msg-no-comments')) + '</li>'; return; } list.forEach(function (c) { var li = document.createElement('li'); li.className = 'list-group-item'; li.setAttribute('data-comment-id', c.id); var del = ''; if (canDelete) { del = '<button type="button" class="btn btn-link btn-sm text-danger p-0 float-end cr-comment-delete" data-comment-id="' + escapeHtml(c.id) + '" title="' + escapeHtml(root.getAttribute('data-label-delete')) + '"><i class="fa-solid fa-trash"></i></button>'; } li.innerHTML = del + '<div class="small text-muted mb-1">' + escapeHtml(c.created_at || '') + ' - ' + escapeHtml(c.author || '') + '</div>' + '<div>' + escapeHtml(c.content || '') + '</div>'; ul.appendChild(li); }); wireCommentDelete(); } function renderAttachments(list) { var ul = qs('#cr-attachments-list'); if (!ul) { return; } ul.innerHTML = ''; if (!list || !list.length) { ul.innerHTML = '<li class="list-group-item text-muted">' + escapeHtml(root.getAttribute('data-msg-no-attachments')) + '</li>'; return; } list.forEach(function (a) { var li = document.createElement('li'); li.className = 'list-group-item d-flex justify-content-between align-items-center gap-2'; li.setAttribute('data-attachment-id', a.id); var dl = fillUrl(D.urlAttachmentDownload, { activity: currentActivityId, attachment: a.id }); var actions = '<a class="btn btn-sm btn-outline-primary" href="' + escapeHtml(dl) + '">' + escapeHtml(root.getAttribute('data-label-download')) + '</a>'; if (canDelete) { actions += ' <button type="button" class="btn btn-sm btn-outline-danger cr-attachment-delete" data-attachment-id="' + escapeHtml(a.id) + '"><i class="fa-solid fa-trash"></i></button>'; } li.innerHTML = '<span class="text-truncate"><i class="fa-solid fa-paperclip text-muted me-1"></i>' + escapeHtml(a.name) + ' <small class="text-muted">' + escapeHtml(a.size || '') + '</small></span>' + '<span class="text-nowrap">' + actions + '</span>'; ul.appendChild(li); }); wireAttachmentDelete(); } function renderHistory(list) { var ul = qs('#cr-history-list'); if (!ul) { return; } ul.innerHTML = ''; if (!list || !list.length) { ul.innerHTML = '<li class="list-group-item text-muted">' + escapeHtml(root.getAttribute('data-msg-no-history')) + '</li>'; return; } list.forEach(function (h) { var li = document.createElement('li'); li.className = 'list-group-item d-flex justify-content-between gap-2'; var details = h.details ? '<small class="text-muted d-block mt-1">' + escapeHtml(h.details) + '</small>' : ''; li.innerHTML = '<div class="flex-grow-1">' + '<span>' + escapeHtml(h.action) + ' <small class="text-muted">(' + escapeHtml(h.user) + ')</small></span>' + details + '</div>' + '<small class="text-muted text-nowrap align-self-start">' + escapeHtml(h.created_at || '') + '</small>'; ul.appendChild(li); }); } function buildActivityDetailUrl(activityId, options) { options = options || {}; var url = fillUrl(D.urlActivityShow, { activity: activityId }); var qs = []; if (options.commentsPage) { qs.push('cp=' + encodeURIComponent(options.commentsPage)); } if (options.historyPage) { qs.push('hp=' + encodeURIComponent(options.historyPage)); } return qs.length ? (url + '?' + qs.join('&')) : url; } function renderDetailPagination(containerId, meta, pane, onPage) { var nav = document.getElementById(containerId); if (!nav || !meta || meta.last_page <= 1) { if (nav) { nav.classList.add('d-none'); nav.innerHTML = ''; } return; } nav.classList.remove('d-none'); var html = '<ul class="pagination pagination-sm justify-content-center mb-0">'; for (var p = 1; p <= meta.last_page; p++) { html += '<li class="page-item' + (p === meta.current_page ? ' active' : '') + '">' + '<button type="button" class="page-link cr-detail-page" data-pane="' + escapeHtml(pane) + '" data-page="' + p + '">' + p + '</button></li>'; } html += '</ul>'; nav.innerHTML = html; nav.querySelectorAll('.cr-detail-page').forEach(function (btn) { btn.addEventListener('click', function () { var page = parseInt(btn.getAttribute('data-page'), 10); if (!isNaN(page)) { onPage(page); } }); }); } function loadDetails(activityId, options) { options = options || {}; currentActivityId = activityId; renderComments(null); renderAttachments(null); renderHistory(null); renderDetailPagination('cr-comments-pagination', null, 'comments', function () {}); renderDetailPagination('cr-history-pagination', null, 'history', function () {}); var meta = qs('#cr-details-meta'); var row = findRow(activityId); if (meta && row) { meta.innerHTML = '<div class="fw-semibold">' + escapeHtml(row.code) + ' - ' + escapeHtml(row.title) + '</div>' + '<div class="small text-muted">' + escapeHtml(row.status_label) + ' · ' + escapeHtml(row.responsible) + ' · ' + Math.round(row.completion_percent) + '%</div>'; } fetch(buildActivityDetailUrl(activityId, options), { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }).then(function (r) { return r.json(); }).then(function (data) { if (!data.ok) { throw new Error('fail'); } renderComments(data.comments); renderAttachments(data.attachments); renderHistory(data.history); renderDetailPagination('cr-comments-pagination', data.comments_pagination, 'comments', function (page) { loadDetails(activityId, { commentsPage: page, historyPage: options.historyPage || 1 }); }); renderDetailPagination('cr-history-pagination', data.history_pagination, 'history', function (page) { loadDetails(activityId, { commentsPage: options.commentsPage || 1, historyPage: page }); }); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); } function wireCommentDelete() { document.querySelectorAll('.cr-comment-delete').forEach(function (btn) { btn.addEventListener('click', function () { if (!window.confirm(root.getAttribute('data-msg-confirm-delete-comment'))) { return; } var cid = btn.getAttribute('data-comment-id'); fetch(fillUrl(D.urlCommentDestroy, { activity: currentActivityId, comment: cid }), { method: 'DELETE', headers: jsonHeaders() }).then(function (r) { return r.json(); }).then(function (data) { var li = document.querySelector('li[data-comment-id="' + cid + '"]'); if (li) { li.remove(); } setBadge('.cr-badge-comments', currentActivityId, data.comments_count); if (!qs('#cr-comments-list').children.length) { renderComments([]); } toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); }); }); } function wireAttachmentDelete() { document.querySelectorAll('.cr-attachment-delete').forEach(function (btn) { btn.addEventListener('click', function () { if (!window.confirm(root.getAttribute('data-msg-confirm-delete-attachment'))) { return; } var aid = btn.getAttribute('data-attachment-id'); fetch(fillUrl(D.urlAttachmentDestroy, { activity: currentActivityId, attachment: aid }), { method: 'DELETE', headers: jsonHeaders() }).then(function (r) { return r.json(); }).then(function (data) { var li = document.querySelector('li[data-attachment-id="' + aid + '"]'); if (li) { li.remove(); } setBadge('.cr-badge-attachments', currentActivityId, data.attachments_count); if (!qs('#cr-attachments-list').children.length) { renderAttachments([]); } toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); }); }); } function initDetails() { document.querySelectorAll('.cr-btn-details').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-activity-id'); loadDetails(id); var oc = document.getElementById('crOffcanvasDetails'); if (oc && window.bootstrap) { bootstrap.Offcanvas.getOrCreateInstance(oc).show(); } }); }); // alternância de painéis (comentários/anexos/histórico) document.querySelectorAll('[data-cr-pane]').forEach(function (tab) { tab.addEventListener('click', function () { document.querySelectorAll('[data-cr-pane]').forEach(function (t) { t.classList.remove('active'); }); tab.classList.add('active'); var name = tab.getAttribute('data-cr-pane'); document.querySelectorAll('[data-cr-pane-content]').forEach(function (p) { p.hidden = (p.getAttribute('data-cr-pane-content') !== name); }); }); }); var commentForm = qs('#cr-comment-form'); if (commentForm) { commentForm.addEventListener('submit', function (e) { e.preventDefault(); var input = qs('#cr-comment-input'); if (!input || !input.value.trim() || !currentActivityId) { return; } fetch(fillUrl(D.urlCommentStore, { activity: currentActivityId }), { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ content: input.value }) }).then(function (r) { return r.json(); }).then(function (data) { if (!data.ok) { throw new Error('fail'); } input.value = ''; setBadge('.cr-badge-comments', currentActivityId, data.comments_count); // recarrega lista mantendo ordem (mais recentes primeiro) loadCommentsOnly(); toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); }); } var attForm = qs('#cr-attachment-form'); if (attForm) { attForm.addEventListener('submit', function (e) { e.preventDefault(); var input = qs('#cr-attachment-input'); if (!input || !input.files.length || !currentActivityId) { return; } var fd = new FormData(); fd.append('_token', csrf); for (var i = 0; i < input.files.length; i++) { fd.append('attachments[]', input.files[i]); } fetch(fillUrl(D.urlAttachmentStore, { activity: currentActivityId }), { method: 'POST', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }, body: fd }).then(function (r) { return r.json(); }).then(function (data) { if (!data.ok) { throw new Error('fail'); } input.value = ''; setBadge('.cr-badge-attachments', currentActivityId, data.attachments_count); loadAttachmentsOnly(); toast(root.getAttribute('data-msg-saved')); }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); }); } } function loadCommentsOnly() { loadDetails(currentActivityId, { commentsPage: 1 }); } function loadAttachmentsOnly() { fetch(buildActivityDetailUrl(currentActivityId), { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }).then(function (r) { return r.json(); }).then(function (data) { if (data.ok) { renderAttachments(data.attachments); } }); } /* ------------------------------------------------------------------ */ /* Bootstrap */ /* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */ /* Formulário de projeto (criar/editar) - autosave local */ /* ------------------------------------------------------------------ */ function initProjectForm() { var form = document.getElementById('cr-project-form'); if (!form) { return; } var projectId = form.getAttribute('data-project-id') || ''; var isCreate = projectId === ''; var statusEl = document.getElementById('cr-project-autosave'); var key = 'cr_project_' + (projectId || 'new'); var fields = ['name', 'description', 'start_date', 'planned_end_date']; function collect() { var o = {}; fields.forEach(function (f) { var el = form.querySelector('[name="' + f + '"]'); o[f] = el ? el.value : ''; }); return o; } function apply(d) { if (!d) { return; } fields.forEach(function (f) { var el = form.querySelector('[name="' + f + '"]'); if (el && d[f] !== undefined && d[f] !== '') { el.value = d[f]; } }); } if (isCreate) { var nameEl = form.querySelector('[name="name"]'); if (nameEl && !nameEl.value) { try { var raw = localStorage.getItem(key); if (raw) { apply(JSON.parse(raw)); } } catch (e) { /* ignore */ } } var timer = null; form.addEventListener('input', function () { if (timer) { clearTimeout(timer); } timer = setTimeout(function () { try { localStorage.setItem(key, JSON.stringify(collect())); if (statusEl) { statusEl.textContent = (form.getAttribute('data-msg-draft') || 'Rascunho salvo') + ' ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } } catch (e) { /* ignore */ } }, 600); }); } form.addEventListener('submit', function () { try { localStorage.removeItem(key); } catch (e) { /* ignore */ } }); } function deleteArchivedProject() { var url = D.urlProjectDestroy; if (!url) { return; } var msg = root.getAttribute('data-msg-confirm-delete-project'); if (msg && !window.confirm(msg)) { return; } fetch(url, { method: 'DELETE', headers: jsonHeaders(), }).then(function (r) { return r.json(); }).then(function (data) { if (!data || !data.ok) { throw new Error('fail'); } toast(root.getAttribute('data-msg-saved')); window.location.href = D.urlDashboard || '/admin/cronogramas?tab=archived'; }).catch(function () { toast(root.getAttribute('data-msg-error'), 'danger'); }); } function initProjectDelete() { if (!canDelete) { return; } document.querySelectorAll('.cr-btn-delete-project').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); deleteArchivedProject(); }); }); } function init() { initProjectForm(); if (!root) { return; } bootPageData(); initActivitiesSync(); initActivitySearch(); initLayoutFixes(); initActivityModal(); initInlineEdits(); initReorder(); initKanban(); if (getCurrentTab() !== 'archived') { try { initGantt(); } catch (e) { /* ignore */ } } initDetails(); initActivityLifecycle(); initArchivedBulkDelete(); initProjectDelete(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings