691 lines
24 KiB
PHP
691 lines
24 KiB
PHP
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>CRM</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
|
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
|
|
</head>
|
|
<body>
|
|
<?php
|
|
use App\Core\Auth;
|
|
use App\Core\DB;
|
|
|
|
$authUser = Auth::user();
|
|
$pdo = DB::connection();
|
|
$stmt = $pdo->query("
|
|
SELECT name, code
|
|
FROM boards
|
|
WHERE is_active = 1
|
|
ORDER BY id ASC
|
|
");
|
|
$boards = $stmt->fetchAll();
|
|
|
|
$currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
?>
|
|
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="/">CRM</a>
|
|
<div class="dropdown ms-3">
|
|
<button
|
|
class="btn btn-light rounded-circle position-relative"
|
|
id="notificationBell"
|
|
data-bs-toggle="dropdown"
|
|
aria-expanded="false"
|
|
style="width: 42px; height: 42px;"
|
|
>
|
|
<i class="bi bi-bell-fill" id="notificationBellIcon"></i>
|
|
<span
|
|
id="notificationBadge"
|
|
class="start-100 translate-middle badge rounded-pill bg-primary d-none"
|
|
style="font-size: 0.65rem;"
|
|
>
|
|
0
|
|
</span>
|
|
</button>
|
|
|
|
<div class="dropdown-menu dropdown-menu-end p-0" style="width: 360px;">
|
|
<div class="p-3 border-bottom d-flex justify-content-between align-items-center">
|
|
<strong>Уведомления</strong>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="markAllNotificationsRead">
|
|
Прочитать все
|
|
</button>
|
|
</div>
|
|
|
|
<div id="notificationList" style="max-height: 420px; overflow-y: auto;">
|
|
<div class="p-3 text-muted">Загрузка...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button class="btn btn-outline-light btn-sm"
|
|
type="button"
|
|
data-bs-toggle="offcanvas"
|
|
data-bs-target="#boardsMenuCanvas"
|
|
aria-controls="boardsMenuCanvas">
|
|
Доски
|
|
</button>
|
|
|
|
<?php if ((int)($authUser['is_admin'] ?? 0) === 1): ?>
|
|
<a href="/admin" class="btn btn-sm btn-warning">Админка</a>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="/logout" class="d-inline">
|
|
<button class="btn btn-sm btn-danger" type="submit">Выход</button>
|
|
</form>
|
|
</div>
|
|
<span id="headerClock" style="color: white" data-server-time="<?= date('Y-m-d H:i:s') ?>">
|
|
<?= date('d.m.Y H:i:s') ?>
|
|
</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container-fluid py-4">
|
|
<?= $content ?>
|
|
</div>
|
|
|
|
<div class="modal fade" id="taskModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content" id="taskModalContent">
|
|
<div class="modal-body p-4 text-center">
|
|
Загрузка...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="boardModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content" id="boardModalContent">
|
|
<div class="modal-body p-4 text-center">
|
|
Загрузка...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="imagePreviewModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
|
<div class="modal-content bg-dark">
|
|
<div class="modal-header border-0">
|
|
<h5 class="modal-title text-white" id="imagePreviewTitle">Просмотр изображения</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<img id="imagePreviewModalImg" src="" alt="" class="img-fluid rounded">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="offcanvas offcanvas-start" tabindex="-1" id="boardsMenuCanvas" aria-labelledby="boardsMenuCanvasLabel">
|
|
<div class="offcanvas-header">
|
|
<h5 class="offcanvas-title" id="boardsMenuCanvasLabel">Доски</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
|
</div>
|
|
|
|
<div class="offcanvas-body">
|
|
<input type="text" class="form-control mb-3" placeholder="Поиск..." id="boardsSearch">
|
|
|
|
<div class="list-group" id="boardsList">
|
|
<?php foreach ($boards as $b): ?>
|
|
<?php $boardUrl = '/boards/' . $b['code']; ?>
|
|
|
|
<a href="<?= htmlspecialchars($boardUrl) ?>"
|
|
class="list-group-item list-group-item-action <?= $currentPath === $boardUrl ? 'active' : '' ?>">
|
|
<?= htmlspecialchars((string)$b['name']) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
document.addEventListener('click', async function (e) {
|
|
const btn = e.target.closest('[data-task-modal]');
|
|
if (!btn) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const taskId = btn.getAttribute('data-task-id');
|
|
const modalElement = document.getElementById('taskModal');
|
|
const modalContent = document.getElementById('taskModalContent');
|
|
|
|
modalContent.innerHTML = '<div class="modal-body p-4 text-center">Загрузка...</div>';
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
|
|
try {
|
|
const response = await fetch('/tasks/show?id=' + encodeURIComponent(taskId), {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const html = await response.text();
|
|
modalContent.innerHTML = html;
|
|
const badge = document.getElementById('comment-badge-' + taskId);
|
|
if (badge) {
|
|
const lastCommentId = parseInt(badge.dataset.lastCommentId || '0', 10);
|
|
badge.dataset.lastReadCommentId = String(lastCommentId);
|
|
badge.classList.remove('bg-primary');
|
|
badge.classList.add('bg-secondary');
|
|
}
|
|
} catch (error) {
|
|
modalContent.innerHTML = '<div class="modal-body p-4 text-danger">Ошибка загрузки карточки задачи</div>';
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
let wsProtocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
let wsUrl = wsProtocol + location.host + '/ws/';
|
|
|
|
try {
|
|
const socket = new WebSocket(wsUrl);
|
|
|
|
socket.onmessage = function (event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'notification_created') {
|
|
|
|
if (typeof window.CRM_USER_ID !== 'undefined'
|
|
&& parseInt(data.user_id, 10) !== parseInt(window.CRM_USER_ID, 10)) {
|
|
return;
|
|
}
|
|
|
|
if (typeof window.refreshNotifications === 'function') {
|
|
window.refreshNotifications();
|
|
}
|
|
|
|
showToast(data.title + ': ' + data.message, 'primary');
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'comment_added') {
|
|
updateCommentBadge(data);
|
|
}
|
|
if (
|
|
data.type === 'task_created' ||
|
|
data.type === 'task_updated' ||
|
|
data.type === 'import_finished'
|
|
) {
|
|
const path = window.location.pathname;
|
|
|
|
if (
|
|
path === '/tasks' ||
|
|
path.startsWith('/boards/')
|
|
) {
|
|
location.reload();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('WS parse error', e);
|
|
}
|
|
};
|
|
|
|
function updateCommentBadge(data) {
|
|
const badge = document.getElementById('comment-badge-' + data.task_id);
|
|
if (!badge) return;
|
|
|
|
const innerCount = badge.querySelector('.comment-count');
|
|
|
|
if (innerCount) {
|
|
innerCount.textContent = data.comment_count;
|
|
} else {
|
|
badge.textContent = data.comment_count;
|
|
}
|
|
|
|
const lastRead = parseInt(badge.dataset.lastReadCommentId || '0', 10);
|
|
const lastComment = parseInt(data.last_comment_id || '0', 10);
|
|
|
|
badge.dataset.lastCommentId = String(lastComment);
|
|
|
|
badge.classList.remove('bg-secondary', 'bg-primary');
|
|
|
|
if (lastComment > lastRead) {
|
|
badge.classList.add('bg-primary');
|
|
} else {
|
|
badge.classList.add('bg-secondary');
|
|
}
|
|
}
|
|
|
|
window.crmSocket = socket;
|
|
} catch (e) {
|
|
console.error('WS connection error', e);
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
document.addEventListener('click', async function (e) {
|
|
const btn = e.target.closest('[data-board-modal]');
|
|
if (!btn) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const boardId = btn.getAttribute('data-board-id');
|
|
const modalElement = document.getElementById('boardModal');
|
|
const modalContent = document.getElementById('boardModalContent');
|
|
|
|
modalContent.innerHTML = '<div class="modal-body p-4 text-center">Загрузка...</div>';
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
|
|
try {
|
|
const response = await fetch('/admin/boards/show?id=' + encodeURIComponent(boardId), {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const html = await response.text();
|
|
modalContent.innerHTML = html;
|
|
} catch (error) {
|
|
modalContent.innerHTML = '<div class="modal-body p-4 text-danger">Ошибка загрузки доски</div>';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener('click', function (e) {
|
|
const btn = e.target.closest('[data-image-modal]');
|
|
if (!btn) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const src = btn.getAttribute('data-image-src');
|
|
const title = btn.getAttribute('data-image-title') || 'Просмотр изображения';
|
|
|
|
const modalElement = document.getElementById('imagePreviewModal');
|
|
const modalTitle = document.getElementById('imagePreviewTitle');
|
|
const modalImg = document.getElementById('imagePreviewModalImg');
|
|
|
|
modalTitle.textContent = title;
|
|
modalImg.src = src;
|
|
modalImg.alt = title;
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
});
|
|
</script>
|
|
<script>
|
|
window.reloadTaskModal = async function (taskId) {
|
|
const modalContent = document.getElementById('taskModalContent');
|
|
|
|
if (!taskId || !modalContent) return;
|
|
|
|
modalContent.innerHTML = '<div class="modal-body p-4 text-center">Загрузка...</div>';
|
|
|
|
const response = await fetch('/tasks/show?id=' + encodeURIComponent(taskId), {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const html = await response.text();
|
|
modalContent.innerHTML = html;
|
|
|
|
const badge = document.getElementById('comment-badge-' + taskId);
|
|
if (badge) {
|
|
const lastCommentId = parseInt(badge.dataset.lastCommentId || '0', 10);
|
|
badge.dataset.lastReadCommentId = String(lastCommentId);
|
|
badge.classList.remove('bg-primary');
|
|
badge.classList.add('bg-secondary');
|
|
}
|
|
};
|
|
</script>
|
|
<script>
|
|
document.addEventListener('submit', async function (e) {
|
|
const statusForm = e.target.closest('.js-task-status-form');
|
|
const commentForm = e.target.closest('.js-task-comment-form');
|
|
const uploadForm = e.target.closest('.js-task-file-upload-form');
|
|
const deleteForm = e.target.closest('.js-task-file-delete-form');
|
|
|
|
const form = statusForm || commentForm || uploadForm || deleteForm;
|
|
if (!form) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const taskId = form.getAttribute('data-task-id');
|
|
const formData = new FormData(form);
|
|
|
|
try {
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
alert(result.message || 'Ошибка выполнения действия');
|
|
return;
|
|
}
|
|
|
|
await window.reloadTaskModal(taskId);
|
|
} catch (error) {
|
|
alert('Ошибка выполнения запроса');
|
|
}
|
|
});
|
|
</script>
|
|
<div class="toast-container position-fixed top-0 end-0 p-3" id="crmToastContainer" style="z-index: 2000;"></div>
|
|
<script>
|
|
(function () {
|
|
let reloadTimer = null;
|
|
|
|
function showToast(message, type = 'primary') {
|
|
const container = document.getElementById('crmToastContainer');
|
|
if (!container) return;
|
|
|
|
const toastEl = document.createElement('div');
|
|
toastEl.className = 'toast align-items-center text-bg-' + type + ' border-0';
|
|
toastEl.setAttribute('role', 'alert');
|
|
toastEl.setAttribute('aria-live', 'assertive');
|
|
toastEl.setAttribute('aria-atomic', 'true');
|
|
|
|
toastEl.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">${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(toastEl);
|
|
|
|
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
|
|
toast.show();
|
|
|
|
toastEl.addEventListener('hidden.bs.toast', function () {
|
|
toastEl.remove();
|
|
});
|
|
}
|
|
|
|
function getCurrentBoardCode() {
|
|
const match = window.location.pathname.match(/^\/boards\/([^\/]+)$/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
function scheduleBoardRefresh() {
|
|
if (reloadTimer) {
|
|
clearTimeout(reloadTimer);
|
|
}
|
|
|
|
reloadTimer = setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
}
|
|
|
|
function handleTaskCreated(data) {
|
|
showToast('Новая задача: ' + (data.name || 'без названия'), 'success');
|
|
|
|
if (typeof window.refreshNotifications === 'function') {
|
|
window.refreshNotifications();
|
|
}
|
|
|
|
scheduleBoardRefresh();
|
|
}
|
|
|
|
function handleTaskUpdated(data) {
|
|
showToast('Обновлена задача: ' + (data.name || 'без названия'), 'primary');
|
|
|
|
if (typeof window.refreshNotifications === 'function') {
|
|
window.refreshNotifications();
|
|
}
|
|
|
|
scheduleBoardRefresh();
|
|
}
|
|
|
|
function handleImportFinished(data) {
|
|
if ((parseInt(data.created || 0, 10) > 0) || (parseInt(data.updated || 0, 10) > 0)) {
|
|
showToast(
|
|
'Импорт завершен. Новых: ' + (data.created || 0) + ', обновлено: ' + (data.updated || 0),
|
|
'dark'
|
|
);
|
|
}
|
|
|
|
if (typeof window.refreshNotifications === 'function') {
|
|
window.refreshNotifications();
|
|
}
|
|
}
|
|
|
|
let wsProtocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
let wsUrl = wsProtocol + location.host + '/ws/';
|
|
|
|
try {
|
|
const socket = new WebSocket(wsUrl);
|
|
|
|
socket.onmessage = function (event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.type === 'comment_added') {
|
|
if (typeof updateCommentBadge === 'function') {
|
|
updateCommentBadge(data);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'task_created') {
|
|
handleTaskCreated(data);
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'task_updated') {
|
|
handleTaskUpdated(data);
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'import_finished') {
|
|
handleImportFinished(data);
|
|
return;
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('WS parse error', e);
|
|
}
|
|
};
|
|
|
|
window.crmSocket = socket;
|
|
} catch (e) {
|
|
console.error('WS connection error', e);
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
(async function () {
|
|
const bellIcon = document.getElementById('notificationBellIcon');
|
|
const badge = document.getElementById('notificationBadge');
|
|
const list = document.getElementById('notificationList');
|
|
const markAllBtn = document.getElementById('markAllNotificationsRead');
|
|
|
|
if (!bellIcon || !badge || !list || !markAllBtn) return;
|
|
|
|
function updateBell(unreadCount) {
|
|
bellIcon.classList.remove('text-secondary', 'text-primary');
|
|
bellIcon.classList.add(unreadCount > 0 ? 'text-primary' : 'text-secondary');
|
|
|
|
if (unreadCount > 0) {
|
|
badge.textContent = String(unreadCount);
|
|
badge.classList.remove('d-none');
|
|
} else {
|
|
badge.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
function renderNotifications(items) {
|
|
if (!items.length) {
|
|
list.innerHTML = '<div class="p-3 text-muted">Уведомлений пока нет</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = items.map(item => {
|
|
const payload = item.payload_json ? JSON.parse(item.payload_json) : {};
|
|
const boardCode = payload.board_code || '';
|
|
const taskId = payload.task_id || 0;
|
|
|
|
let link = '#';
|
|
if (taskId) {
|
|
link = '/tasks/show?id=' + taskId;
|
|
} else if (boardCode) {
|
|
link = '/boards/' + encodeURIComponent(boardCode);
|
|
}
|
|
|
|
return `
|
|
<a href="${link}" class="dropdown-item border-bottom py-3 ${item.is_read == 0 ? 'bg-light' : ''}" ${taskId ? `data-task-modal data-task-id="${taskId}"` : ''}>
|
|
<div class="fw-semibold">${escapeHtml(item.title || '')}</div>
|
|
<div class="small text-muted">${escapeHtml(item.message || '')}</div>
|
|
<div class="small text-muted mt-1">${escapeHtml(item.created_at || '')}</div>
|
|
</a>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
async function loadNotifications() {
|
|
try {
|
|
const response = await fetch('/notifications/list', {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
renderNotifications(data.items || []);
|
|
updateBell(parseInt(data.unread_count || 0, 10));
|
|
} catch (e) {
|
|
list.innerHTML = '<div class="p-3 text-danger">Ошибка загрузки уведомлений</div>';
|
|
}
|
|
}
|
|
|
|
markAllBtn.addEventListener('click', async function (e) {
|
|
e.preventDefault();
|
|
|
|
try {
|
|
const response = await fetch('/notifications/read-all', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
await loadNotifications();
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
|
|
await loadNotifications();
|
|
|
|
window.refreshNotifications = loadNotifications;
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
const clock = document.getElementById('headerClock');
|
|
if (!clock) return;
|
|
|
|
let current = new Date(clock.dataset.serverTime.replace(' ', 'T'));
|
|
|
|
function pad(n) {
|
|
return String(n).padStart(2, '0');
|
|
}
|
|
|
|
function render() {
|
|
current.setSeconds(current.getSeconds() + 1);
|
|
|
|
const y = current.getFullYear();
|
|
const m = pad(current.getMonth() + 1);
|
|
const d = pad(current.getDate());
|
|
const h = pad(current.getHours());
|
|
const i = pad(current.getMinutes());
|
|
const s = pad(current.getSeconds());
|
|
|
|
clock.textContent = `${d}.${m}.${y} ${h}:${i}:${s}`;
|
|
}
|
|
|
|
setInterval(render, 1000);
|
|
})();
|
|
</script>
|
|
<script>
|
|
document.addEventListener('click', function (e) {
|
|
const editBtn = e.target.closest('#taskEditBtn');
|
|
const cancelBtn = e.target.closest('#taskCancelEditBtn');
|
|
|
|
if (editBtn) {
|
|
e.preventDefault();
|
|
|
|
const modal = editBtn.closest('.modal-content') || document;
|
|
|
|
modal.querySelectorAll('.task-view-value').forEach(el => {
|
|
el.classList.add('d-none');
|
|
});
|
|
|
|
modal.querySelectorAll('.task-edit-field').forEach(el => {
|
|
el.classList.remove('d-none');
|
|
});
|
|
|
|
const actions = modal.querySelector('#taskEditActions');
|
|
if (actions) {
|
|
actions.classList.remove('d-none');
|
|
}
|
|
|
|
editBtn.classList.add('d-none');
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
e.preventDefault();
|
|
|
|
const modal = cancelBtn.closest('.modal-content') || document;
|
|
|
|
modal.querySelectorAll('.task-view-value').forEach(el => {
|
|
el.classList.remove('d-none');
|
|
});
|
|
|
|
modal.querySelectorAll('.task-edit-field').forEach(el => {
|
|
el.classList.add('d-none');
|
|
});
|
|
|
|
const actions = modal.querySelector('#taskEditActions');
|
|
if (actions) {
|
|
actions.classList.add('d-none');
|
|
}
|
|
|
|
const editBtn = modal.querySelector('#taskEditBtn');
|
|
if (editBtn) {
|
|
editBtn.classList.remove('d-none');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
window.CRM_USER_ID = <?= \App\Core\Auth::check() ? (int)\App\Core\Auth::user()['id'] : 0 ?>;
|
|
</script>
|
|
<script>
|
|
document.addEventListener('input', function (e) {
|
|
if (e.target.id !== 'boardsSearch') return;
|
|
|
|
const search = e.target.value.toLowerCase();
|
|
|
|
document.querySelectorAll('#boardsList a').forEach(item => {
|
|
item.style.display = item.textContent.toLowerCase().includes(search) ? '' : 'none';
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|