| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- /* global $, postData, toastr, window */
- 'use strict';
- $(function() {
- let initialized = false;
- let initializing = false;
- let loading = false;
- let saving = false;
- let stale = false;
- let version = 0;
- let projects = [];
- let selectedProject = '';
- let changes = {};
- let projectChanges = {};
- let cache = {};
- let requestId = 0;
- let matches = [];
- let matchIndex = -1;
- let searchTimer = null;
- const collapsedProjects = new Set();
- let collapsedTenders = new Set();
- const prefix = '#project-screen-tender-';
- const element = function(name) {
- return $(prefix + name);
- };
- const escapeHtml = function(value) {
- return $('<div>').text(value === null || value === undefined ? '' : String(value)).html();
- };
- const post = function(type, data, success, failure) {
- postData('/setting/module/projectscreen', $.extend({ type }, data), success, failure);
- };
- const currentData = function() {
- return cache[selectedProject];
- };
- const dirty = function() {
- return Object.keys(changes).length > 0 || Object.keys(projectChanges).length > 0;
- };
- const isChecked = function(node) {
- if (changes[node.tid]) return changes[node.tid].checked;
- return !currentData().excluded.has(node.tid);
- };
- const projectLevels = createShowLevel(true);
- const tenderLevels = createShowLevel(false);
- const updateControls = function() {
- const disabled = !initialized || initializing || loading || saving || stale;
- const tenderDisabled = !canUseTenderControls();
- const data = currentData();
- const leaves = data && !loading ? data.nodes.filter(node => !node.is_folder) : [];
- const checked = leaves.filter(isChecked).length;
- element('all').prop('disabled', tenderDisabled || !leaves.length)
- .prop('checked', leaves.length > 0 && checked === leaves.length)
- .prop('indeterminate', checked > 0 && checked < leaves.length);
- element('locate').prop('disabled', tenderDisabled || !leaves.length);
- element('search-prev').add(element('search-next')).prop('disabled', tenderDisabled || !matches.length);
- element('tree').find('input[data-tid]').each(function() {
- const tid = Number($(this).attr('data-tid'));
- this.checked = changes[tid] ? changes[tid].checked : !data.excluded.has(tid);
- this.disabled = tenderDisabled;
- });
- element('tree').find('[data-folder]').prop('disabled', tenderDisabled);
- const counts = getProjectCounts();
- let total = 0;
- let selected = 0;
- const projectById = new Map(projects.map(project => [project.id, project]));
- counts.forEach((count, id) => {
- total += count.total;
- if (isProjectChecked(projectById.get(id))) selected += count.selected;
- });
- updateProjectCounts(counts);
- updateProjectVisibility();
- const leavesProjects = projects.filter(project => !project.is_folder);
- const shownProjects = leavesProjects.filter(isProjectChecked).length;
- element('total').text(initialized ? `展示项目 ${shownProjects} / ${leavesProjects.length},展示标段 ${selected} / ${total}` : '');
- element('save').prop('disabled', disabled || !dirty());
- element('cancel').prop('disabled', saving || initializing || !dirty());
- projectLevels.refreshMenuVisible();
- tenderLevels.refreshMenuVisible();
- element('project-level').find('button').prop('disabled', !initialized || initializing || saving || stale || !getTreeLevelCount(true));
- element('tender-level').find('button').prop('disabled', tenderDisabled || !getTreeLevelCount(false));
- if (!stale) element('save-status').text(saving ? '正在保存…' : dirty() ? '有未保存的修改' : '');
- };
- const renderProjects = function(revealMatches = false) {
- const keyword = element('project-search').val().trim().toLocaleLowerCase();
- const byId = new Map(projects.map(project => [project.id, project]));
- const included = new Set();
- if (keyword) {
- projects.filter(project => project.name.toLocaleLowerCase().includes(keyword)).forEach(project => {
- const visited = new Set();
- let node = project;
- while (node && !visited.has(node.id)) {
- visited.add(node.id);
- included.add(node.id);
- if (revealMatches) collapsedProjects.delete(node.id);
- node = byId.get(node.tree_pid);
- }
- });
- }
- const children = new Map();
- projects.forEach(project => {
- const parent = byId.has(project.tree_pid) ? project.tree_pid : '';
- if (!children.has(parent)) children.set(parent, []);
- children.get(parent).push(project);
- });
- const html = ['<table class="table mb-0 project-screen-tender-list project-screen-tender-project-list"><thead class="project-screen-sticky-head"><tr><th>项目名称</th><th class="text-center project-screen-tender-project-total" title="已勾选标段 / 标段总数">标段范围</th><th class="text-center project-screen-tender-project-check-cell"><input id="project-screen-tender-project-all" type="checkbox" aria-label="展示全部项目" title="展示全部项目" disabled></th></tr></thead><tbody>'];
- const visited = new Set();
- let firstProject = '';
- const visit = function(parent, depth) {
- const nodes = (children.get(parent) || []).filter(node => !keyword || included.has(node.id)).sort((a, b) => a.tree_order - b.tree_order);
- nodes.forEach((node, index) => {
- if (visited.has(node.id) || (keyword && !included.has(node.id))) return;
- visited.add(node.id);
- const open = !collapsedProjects.has(node.id);
- const active = selectedProject === node.id ? ' project-screen-tender-project-active' : '';
- html.push(`<tr class="project-screen-tender-project-node${active}${node.is_folder ? ' project-screen-tender-directory project-screen-tender-project-directory' : ''}" data-project="${escapeHtml(node.id)}"><td style="--tree-depth:${depth}"><div class="project-screen-tender-project-label">`);
- if (node.is_folder) {
- if ((children.get(node.id) || []).length) {
- html.push(`<button type="button" class="fold-switch project-screen-tender-fold" data-project-folder="${escapeHtml(node.id)}" aria-expanded="${!!open}" aria-label="展开或收起${escapeHtml(node.name)}" title="${open ? '收起' : '展开'}"><i class="fa ${open ? 'fa-minus-square-o' : 'fa-plus-square-o'}" aria-hidden="true"></i></button>`);
- }
- html.push(`<i class="fa fa-folder-o" aria-hidden="true"></i><span class="project-screen-tender-project-folder-name" title="${escapeHtml(node.name)}">${escapeHtml(node.name)}</span>`);
- } else {
- if (!firstProject) firstProject = node.id;
- const branch = index === nodes.length - 1 ? '└' : '├';
- html.push(`<span class="text-muted project-screen-tender-project-branch" aria-hidden="true">${branch}</span><button type="button" class="btn btn-link btn-sm p-0 project-screen-tender-project-name" aria-pressed="${!!active}" title="${escapeHtml(node.name)}">${escapeHtml(node.name)}</button>`);
- }
- html.push(`</div></td><td class="text-center project-screen-tender-project-total">${node.is_folder ? '' : `<span class="project-screen-tender-project-count" data-project-count="${escapeHtml(node.id)}"></span>`}</td>`);
- html.push(`<td class="text-center project-screen-tender-project-check-cell"><input type="checkbox" ${node.is_folder ? 'data-project-group' : 'data-project-check'}="${escapeHtml(node.id)}" aria-label="展示${escapeHtml(node.name)}${node.is_folder ? '下的全部项目' : ''}" disabled></td></tr>`);
- if (open) visit(node.id, depth + 1);
- });
- };
- visit('', 0);
- html.push('</tbody></table>');
- element('project-tree').html(visited.size ? html.join('') : '<div class="text-muted p-3 text-center">没有匹配的项目</div>');
- updateProjectCounts(getProjectCounts());
- updateProjectVisibility();
- return firstProject;
- };
- const applyTenderVisibility = function() {
- const data = currentData();
- if (!data) return;
- const byId = new Map(data.nodes.map(node => [node.id, node]));
- element('tree').find('tr[data-node]').each(function() {
- const node = byId.get($(this).attr('data-node'));
- let parent = byId.get(node.parentId);
- let visible = true;
- while (parent) {
- if (collapsedTenders.has(parent.id)) visible = false;
- parent = byId.get(parent.parentId);
- }
- $(this).toggle(visible);
- });
- element('tree').find('[data-folder]').each(function() {
- const open = !collapsedTenders.has($(this).attr('data-folder'));
- $(this).attr('aria-expanded', String(open)).attr('title', open ? '收起' : '展开')
- .find('i').toggleClass('fa-minus-square-o', open).toggleClass('fa-plus-square-o', !open);
- });
- };
- const locate = function(index) {
- if (!matches.length || !canUseTenderControls()) return;
- matchIndex = (index + matches.length) % matches.length;
- element('tree').find('.project-screen-tender-current-match').removeClass('project-screen-tender-current-match');
- const target = $(matches[matchIndex]).addClass('project-screen-tender-current-match');
- const row = target.closest('tr');
- const byId = new Map(currentData().nodes.map(node => [node.id, node]));
- let node = byId.get(row.attr('data-node'));
- while (node && node.parentId) {
- collapsedTenders.delete(node.parentId);
- node = byId.get(node.parentId);
- }
- applyTenderVisibility();
- element('search-result').text(`${matchIndex + 1}/${matches.length}`);
- const scroll = element('tree-scroll');
- scroll.scrollTop(scroll.scrollTop() + target.offset().top - scroll.offset().top - 35);
- };
- const searchTenders = function() {
- const keyword = element('locate').val().trim().toLocaleLowerCase();
- element('tree').find('.project-screen-tender-name').each(function() {
- const name = $(this).attr('data-name');
- const lower = name.toLocaleLowerCase();
- const html = [];
- let start = 0;
- let found = keyword ? lower.indexOf(keyword) : -1;
- while (found >= 0) {
- html.push(escapeHtml(name.slice(start, found)));
- html.push(`<mark class="project-screen-tender-match">${escapeHtml(name.slice(found, found + keyword.length))}</mark>`);
- start = found + keyword.length;
- found = lower.indexOf(keyword, start);
- }
- html.push(escapeHtml(name.slice(start)));
- $(this).html(html.join(''));
- });
- matches = element('tree').find('.project-screen-tender-match').toArray();
- matchIndex = -1;
- element('search-result').text(`0/${matches.length}`);
- element('search-prev').add(element('search-next')).prop('disabled', !matches.length || !canUseTenderControls());
- if (matches.length && canUseTenderControls()) locate(0);
- };
- const renderTenders = function() {
- const data = currentData();
- const html = [];
- const lastChildren = new Map();
- if (data) {
- data.nodes.forEach(node => lastChildren.set(node.parentId, node.id));
- data.nodes.forEach(node => {
- html.push(`<tr${node.is_folder ? ' class="project-screen-tender-directory"' : ''} data-node="${escapeHtml(node.id)}"><td style="--tree-depth:${node.tree_level}">`);
- if (node.is_folder) {
- const name = escapeHtml(node.name);
- html.push(`<button type="button" class="fold-switch mr-1 project-screen-tender-fold" data-folder="${escapeHtml(node.id)}" aria-expanded="true" aria-label="展开或收起${name}" title="收起"><i class="fa fa-minus-square-o" aria-hidden="true"></i></button> <i class="fa fa-folder-o mr-1" aria-hidden="true"></i>${node.tree_level === 0 ? `<b>${name}</b>` : name}`);
- } else {
- const branch = lastChildren.get(node.parentId) === node.id ? '└' : '├';
- html.push(`<span class="text-muted mr-2">${branch}</span><span class="project-screen-tender-name text-primary" data-name="${escapeHtml(node.name)}">${escapeHtml(node.name)}</span>`);
- }
- html.push('</td><td class="text-center project-screen-tender-check-cell">');
- if (!node.is_folder) html.push(`<input type="checkbox" data-tid="${node.tid}" aria-label="展示${escapeHtml(node.name)}">`);
- html.push('</td></tr>');
- });
- }
- element('tree').html(html.join(''));
- element('message').text('该项目暂无标段').toggle(!data || !data.nodes.length);
- applyTenderVisibility();
- updateControls();
- searchTenders();
- };
- const markStale = function() {
- stale = true;
- element('save-status').text('配置已更新,请重新加载配置后设置');
- element('retry').text('重新加载配置').show();
- updateControls();
- };
- const loadProject = function(spid, reload) {
- if (saving) return;
- selectedProject = spid;
- collapsedTenders = cache[spid] ? cache[spid].collapsed : new Set();
- clearTimeout(searchTimer);
- matches = [];
- matchIndex = -1;
- element('locate').val('');
- element('search-result').text('0/0');
- element('retry').hide();
- const project = projects.find(project => project.id === spid);
- element('project-name').text(project ? project.name : '请选择项目').attr('title', project ? project.name : '');
- renderProjects();
- const id = ++requestId;
- if (cache[spid] && !reload) {
- loading = false;
- renderTenders();
- return;
- }
- loading = true;
- element('tree').empty();
- element('message').text('正在加载全部标段…').show();
- updateControls();
- post('tenderload', { spid }, function(result) {
- if (id !== requestId) return;
- loading = false;
- if (result.version !== version) {
- element('message').text('配置已发生变化,请重新加载').show();
- markStale();
- return;
- }
- cache[spid] = { nodes: result.nodes, excluded: new Set(result.excludedIds), collapsed: collapsedTenders };
- const leaves = result.nodes.filter(node => !node.is_folder);
- project.total = leaves.length;
- project.excludedTotal = leaves.filter(node => cache[spid].excluded.has(node.tid)).length;
- renderProjects();
- renderTenders();
- }, function() {
- if (id !== requestId) return;
- loading = false;
- element('message').text('标段加载失败,请重试').show();
- element('retry').text('重新加载').show();
- updateControls();
- });
- };
- const initialize = function() {
- if (initializing || saving) return;
- initializing = true;
- loading = false;
- clearTimeout(searchTimer);
- matches = [];
- matchIndex = -1;
- element('locate').val('');
- element('search-result').text('0/0');
- ++requestId;
- element('message').text('正在加载项目配置…').show();
- element('retry').hide();
- updateControls();
- post('tenderinit', {}, function(result) {
- initializing = false;
- initialized = true;
- stale = false;
- version = result.version;
- projects = result.projects;
- cache = {};
- changes = {};
- projectChanges = {};
- selectedProject = projects.some(project => project.id === selectedProject && !project.is_folder) ? selectedProject : '';
- if (!selectedProject) {
- collapsedProjects.clear();
- element('project-search').val('');
- }
- const firstProject = renderProjects();
- if (!selectedProject) selectedProject = firstProject;
- if (selectedProject) loadProject(selectedProject);
- else {
- loading = false;
- element('tree').empty();
- element('project-name').text('暂无项目').attr('title', '');
- element('message').text('当前总项目尚未添加项目').show();
- updateControls();
- }
- }, function() {
- initializing = false;
- element('message').text('配置加载失败,请重试').show();
- element('retry').text('重新加载配置').show();
- updateControls();
- });
- };
- const setChange = function(tid, checked) {
- const original = !currentData().excluded.has(tid);
- if (checked === original) delete changes[tid];
- else changes[tid] = { spid: selectedProject, tid, checked };
- };
- $('.project-screen-tab[data-tab="tender"]').click(function() {
- if (!initialized) initialize();
- });
- element('project-tree').on('click', '[data-project]', function(event) {
- if (saving || initializing || stale) return;
- if ($(event.target).is('input[type="checkbox"]')) return;
- const id = $(this).attr('data-project');
- const project = projects.find(project => project.id === id);
- if (project.is_folder) {
- if (!$(event.target).closest('[data-project-folder]').length) return;
- if (collapsedProjects.has(id)) collapsedProjects.delete(id);
- else collapsedProjects.add(id);
- renderProjects();
- } else loadProject(id);
- });
- element('project-search').on('input', function() { renderProjects(true); });
- element('tree').on('click', '[data-folder]', function() {
- if (!canUseTenderControls()) return;
- const id = $(this).attr('data-folder');
- if (collapsedTenders.has(id)) collapsedTenders.delete(id);
- else collapsedTenders.add(id);
- applyTenderVisibility();
- });
- element('tree').on('change', 'input[data-tid]', function() {
- if (!canUseTenderControls()) return updateControls();
- setChange(Number($(this).attr('data-tid')), this.checked);
- updateControls();
- });
- element('all').change(function() {
- if (!canUseTenderControls()) return updateControls();
- const checked = this.checked;
- currentData().nodes.filter(node => !node.is_folder).forEach(node => setChange(node.tid, checked));
- updateControls();
- });
- element('locate').on('input', function() {
- clearTimeout(searchTimer);
- searchTimer = setTimeout(searchTenders, 200);
- }).on('keydown', function(event) {
- if (event.keyCode === 13) {
- event.preventDefault();
- locate(matchIndex + (event.shiftKey ? -1 : 1));
- }
- });
- element('search-prev').click(function() { locate(matchIndex - 1); });
- element('search-next').click(function() { locate(matchIndex + 1); });
- element('retry').click(function() {
- if (!initialized || stale) {
- // eslint-disable-next-line no-alert
- if (dirty() && !window.confirm('重新加载会取消未保存的修改,是否继续?')) return;
- initialize();
- } else loadProject(selectedProject, true);
- });
- element('cancel').click(function() {
- changes = {};
- projectChanges = {};
- updateControls();
- });
- element('save').click(function() {
- if (!dirty() || saving || loading || stale) return;
- const submitted = Object.values(changes);
- const submittedProjects = Object.values(projectChanges);
- saving = true;
- updateControls();
- post('tendersave', { version, changes: submitted, projectChanges: submittedProjects }, function(result) {
- submitted.forEach(change => {
- const data = cache[change.spid];
- if (change.checked) data.excluded.delete(change.tid);
- else data.excluded.add(change.tid);
- });
- projects.forEach(project => {
- if (cache[project.id]) project.excludedTotal = cache[project.id].nodes.filter(node => !node.is_folder && cache[project.id].excluded.has(node.tid)).length;
- });
- submittedProjects.forEach(change => { projects.find(project => project.id === change.spid).checked = change.checked; });
- version = result.version;
- changes = {};
- projectChanges = {};
- saving = false;
- updateControls();
- toastr.success('项目大屏展示范围保存成功');
- }, function(message) {
- saving = false;
- if (typeof message === 'string' && message.includes('配置已被其他管理员修改')) markStale();
- updateControls();
- });
- });
- $(window).on('beforeunload.projectScreenTender', function(event) {
- if (!dirty()) return;
- event.preventDefault();
- event.originalEvent.returnValue = '展示范围尚未保存';
- return '展示范围尚未保存';
- });
- element('project-tree').on('change', 'input[data-project-check], input[data-project-group], #project-screen-tender-project-all', function() {
- if (!initialized || initializing || saving || stale) return;
- const id = $(this).attr('data-project-check');
- const group = $(this).attr('data-project-group');
- const targets = id ? projects.filter(project => project.id === id && !project.is_folder) :
- group ? getProjectGroups().get(group) || [] : projects.filter(project => !project.is_folder);
- targets.forEach(project => setProjectChange(project, this.checked));
- updateControls();
- });
- updateControls();
- function setTreeLevel(projectTree, tag) {
- const data = currentData();
- if (!initialized || initializing || saving || stale) return;
- if (!projectTree && !canUseTenderControls()) return;
- const level = tag === 'last' ? Infinity : Number(tag);
- if (!(level > 0)) return;
- const depths = getTreeDepths(projectTree);
- const collapsed = projectTree ? collapsedProjects : collapsedTenders;
- collapsed.clear();
- (projectTree ? projects : data.nodes).filter(node => node.is_folder && depths.get(node.id) >= level)
- .forEach(node => collapsed.add(node.id));
- if (projectTree) renderProjects();
- else applyTenderVisibility();
- }
- function getTreeDepths(projectTree) {
- if (!projectTree) {
- const data = currentData();
- return new Map((data ? data.nodes : []).map(node => [node.id, Number(node.tree_level) + 1]));
- }
- const byId = new Map(projects.map(project => [project.id, project]));
- const depths = new Map();
- projects.forEach(project => {
- const visited = new Set([project.id]);
- let parent = byId.get(project.tree_pid);
- let depth = 1;
- while (parent && !visited.has(parent.id)) {
- visited.add(parent.id);
- depth++;
- parent = byId.get(parent.tree_pid);
- }
- depths.set(project.id, depth);
- });
- return depths;
- }
- function getTreeLevelCount(projectTree) {
- let maxDepth = 0;
- getTreeDepths(projectTree).forEach(depth => { maxDepth = Math.max(maxDepth, depth); });
- return Math.max(0, maxDepth - 1);
- }
- function createShowLevel(projectTree) {
- const name = projectTree ? 'project-level' : 'tender-level';
- const menu = $.cs_showLevel({
- selector: prefix + name,
- buttonId: `project-screen-tender-${name}-toggle`,
- levels: [
- { type: 'sort', count: 5, visible_count() { return getTreeLevelCount(projectTree); } },
- { type: 'last', title: '最底层', visible() { return getTreeLevelCount(projectTree) > 0; } },
- ],
- showLevel(tag) { setTreeLevel(projectTree, tag); },
- });
- menu.initShowLevel();
- element(name).find('button').attr('aria-label', projectTree ? '项目显示层级' : '标段显示层级');
- return menu;
- }
- function getProjectCounts() {
- const counts = new Map();
- projects.filter(project => !project.is_folder).forEach(project => {
- const total = Number(project.total) || 0;
- counts.set(project.id, { total, selected: total - (Number(project.excludedTotal) || 0) });
- });
- Object.values(changes).forEach(change => {
- const count = counts.get(change.spid);
- if (count) count.selected += change.checked ? 1 : -1;
- });
- return counts;
- }
- function updateProjectCounts(counts) {
- element('project-tree').find('[data-project-count]').each(function() {
- const count = counts.get($(this).attr('data-project-count'));
- if (!count) return;
- const description = `已勾选 ${count.selected} 个,共 ${count.total} 个标段`;
- $(this).text(`${count.selected} / ${count.total}`).attr('title', description).attr('aria-label', description);
- });
- }
- function isProjectChecked(project) {
- if (!project) return false;
- return projectChanges[project.id] ? projectChanges[project.id].checked : project.checked !== false;
- }
- function setProjectChange(project, checked) {
- if (checked === (project.checked !== false)) delete projectChanges[project.id];
- else projectChanges[project.id] = { spid: project.id, checked };
- }
- function getProjectGroups() {
- const byId = new Map(projects.map(project => [project.id, project]));
- const groups = new Map(projects.filter(project => project.is_folder).map(project => [project.id, []]));
- projects.filter(project => !project.is_folder).forEach(project => {
- const visited = new Set([project.id]);
- let parent = byId.get(project.tree_pid);
- while (parent && !visited.has(parent.id)) {
- visited.add(parent.id);
- if (groups.has(parent.id)) groups.get(parent.id).push(project);
- parent = byId.get(parent.tree_pid);
- }
- });
- return groups;
- }
- function updateProjectVisibility() {
- const disabled = !initialized || initializing || saving || stale;
- const byId = new Map(projects.map(project => [project.id, project]));
- const groups = getProjectGroups();
- element('project-tree').find('input[data-project-check]').each(function() {
- this.checked = isProjectChecked(byId.get($(this).attr('data-project-check')));
- this.disabled = disabled;
- });
- element('project-tree').find('input[data-project-group], #project-screen-tender-project-all').each(function() {
- const group = $(this).attr('data-project-group');
- const targets = group ? groups.get(group) || [] : projects.filter(project => !project.is_folder);
- const checked = targets.filter(isProjectChecked).length;
- this.checked = targets.length > 0 && checked === targets.length;
- this.indeterminate = checked > 0 && checked < targets.length;
- this.disabled = disabled || !targets.length;
- });
- }
- function canUseTenderControls() {
- return initialized && !initializing && !loading && !saving && !stale && !!currentData() &&
- isProjectChecked(projects.find(project => project.id === selectedProject));
- }
- });
|