feat(project): 업무 클릭 시 페이지 이동 없이 그 자리에서 팝업 편집
- 좌측 트리(프로젝트 상세 페이지): 이미 로드된 업무 데이터로 즉시 팝업 — 더 이상 페이지를 새로고침하며 달력 탭으로 넘어가지 않음 - 홈 화면 카드/내 업무 리스트: 홈에도 업무편집 모달을 추가해 페이지 이동 없이 팝업으로 편집(담당자·단계는 해당 프로젝트를 API 로 조회해 채움, 첨부·댓글 포함) - 교차 프로젝트 업무 편집 시 담당자 select 가 현재 프로젝트 멤버만 보이던 문제 수정 (해당 업무의 프로젝트 멤버를 다시 조회) - 완료 프로젝트 숨기기 토글을 인라인 style.display 로 직접 제어하도록 변경 ([hidden] 속성 + 클래스 규칙 조합이 기대대로 안 먹히는 경우 대비, 원인 불문 확실히 동작) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -408,6 +408,16 @@ async def index(request: Request) -> HTMLResponse:
|
||||
my_groups[pid] = g
|
||||
g["tasks"].append(t)
|
||||
my_projects = list(my_groups.values())
|
||||
|
||||
# 홈 화면 카드/내업무에 노출된 업무 전체(id→업무) — 클릭 시 페이지 이동 없이
|
||||
# 팝업으로 편집하기 위해 필요한 원본 데이터를 그대로 클라이언트에 넘긴다.
|
||||
home_tasks: dict[str, Any] = {}
|
||||
for p in tree:
|
||||
for t in p.get("task_preview", []):
|
||||
home_tasks[str(t["id"])] = t
|
||||
for t in my_tasks:
|
||||
home_tasks[str(t["id"])] = t
|
||||
|
||||
return render_template(
|
||||
request,
|
||||
"project/index.html",
|
||||
@@ -421,6 +431,7 @@ async def index(request: Request) -> HTMLResponse:
|
||||
"done_projects": done_projects,
|
||||
"my_projects": my_projects,
|
||||
"avatars": avatars,
|
||||
"home_tasks": home_tasks,
|
||||
"today": today_kst().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915c" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915e" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="/static/project.js?v=20260915b" defer></script>
|
||||
<script src="/static/project.js?v=20260915d" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915c" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915e" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<ul class="pj-project-tasklist">
|
||||
{% for t in p.task_preview %}
|
||||
<li class="pj-project-taskrow {% if t.completed_at %}is-done{% endif %}">
|
||||
<a href="/project/p/{{ p.id }}?task={{ t.id }}">
|
||||
<a href="/project/p/{{ p.id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link">
|
||||
<span class="material-symbols-outlined pj-project-task-ic">{% if t.completed_at %}task_alt{% else %}radio_button_unchecked{% endif %}</span>
|
||||
<span class="pj-project-task-title">{{ t.title }}</span>
|
||||
{% if t.stage_name %}<span class="pj-project-task-stage">{{ t.stage_name }}</span>{% endif %}
|
||||
@@ -138,7 +138,7 @@
|
||||
<ul class="pj-mytree-tasks">
|
||||
{% for t in p.tasks %}
|
||||
<li class="pj-mytree-task {% if t.completed_at %}is-done{% endif %}">
|
||||
<a href="/project/p/{{ t.project_id }}?task={{ t.id }}">
|
||||
<a href="/project/p/{{ t.project_id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link">
|
||||
<span class="material-symbols-outlined pj-mytree-ic" style="color: {{ t.project_color }};">{% if t.completed_at %}task_alt{% else %}radio_button_unchecked{% endif %}</span>
|
||||
<span class="pj-mytree-body">
|
||||
<span class="pj-mytree-title">{{ t.title }}</span>
|
||||
@@ -210,8 +210,74 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 업무 편집 모달 — 카드/내 업무에서 클릭 시 페이지 이동 없이 팝업으로 편집 -->
|
||||
<div class="pj-modal" id="pj-modal-task" hidden>
|
||||
<div class="pj-modal-card">
|
||||
<h3 id="pj-task-modal-title">업무 편집</h3>
|
||||
<input type="hidden" id="pj-t-id" />
|
||||
<input type="hidden" id="pj-t-project-id" />
|
||||
<label>제목<input type="text" id="pj-t-title" placeholder="업무 제목" /></label>
|
||||
<label>설명<textarea id="pj-t-desc" rows="2"></textarea></label>
|
||||
<div class="pj-form-row">
|
||||
<label>담당자
|
||||
<select id="pj-t-assignee"><option value="">불러오는 중…</option></select>
|
||||
</label>
|
||||
<label>단계
|
||||
<select id="pj-t-stage"><option value="">불러오는 중…</option></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pj-form-row">
|
||||
<label>우선순위
|
||||
<select id="pj-t-priority">
|
||||
<option value="low">낮음</option>
|
||||
<option value="normal" selected>보통</option>
|
||||
<option value="high">높음</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>시작일<input type="date" id="pj-t-start" /></label>
|
||||
<label>마감일<input type="date" id="pj-t-due" /></label>
|
||||
</div>
|
||||
<label class="pj-time-toggle">
|
||||
<input type="checkbox" id="pj-t-usetime" /> 시간 지정 (체크 안 하면 종일)
|
||||
</label>
|
||||
<div class="pj-form-row" id="pj-time-row">
|
||||
<label>시작 시간<input type="time" id="pj-t-stime" disabled /></label>
|
||||
<label>마감 시간<input type="time" id="pj-t-dtime" disabled /></label>
|
||||
</div>
|
||||
<div class="pj-task-extra" id="pj-attach-block">
|
||||
<div class="pj-extra-head">
|
||||
<span class="material-symbols-outlined pj-gicon pj-gicon-sm">attach_file</span> 첨부파일
|
||||
<label class="pj-upload-btn">
|
||||
파일 추가<input type="file" id="pj-attach-input" hidden />
|
||||
</label>
|
||||
</div>
|
||||
<ul class="pj-attach-list" id="pj-attach-list"></ul>
|
||||
</div>
|
||||
<div class="pj-task-extra" id="pj-comment-block">
|
||||
<div class="pj-extra-head">
|
||||
<span class="pj-cmt-emoji">💬</span> 댓글
|
||||
</div>
|
||||
<ul class="pj-comment-list" id="pj-comment-list"></ul>
|
||||
<div class="pj-comment-form">
|
||||
<input type="text" id="pj-comment-input" placeholder="댓글 입력…" />
|
||||
<button type="button" class="pj-btn pj-btn-primary" id="pj-comment-send">등록</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pj-modal-actions">
|
||||
<button type="button" class="pj-btn pj-btn-danger" id="pj-delete-task">삭제</button>
|
||||
<span style="flex:1"></span>
|
||||
<a class="pj-btn" id="pj-t-open-project" href="#" target="_self">프로젝트에서 열기</a>
|
||||
<button type="button" class="pj-btn" data-close>취소</button>
|
||||
<button type="button" class="pj-btn pj-btn-primary" id="pj-save-task">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="pj-home-data" type="application/json">
|
||||
{{ home_tasks | tojson }}
|
||||
</script>
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260915b" defer></script>
|
||||
<script src="/static/project.js?v=20260915d" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915c" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260915e" />
|
||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
<!-- 타임라인 vis-timeline — self-host -->
|
||||
@@ -291,5 +291,5 @@
|
||||
</script>
|
||||
|
||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260915b" defer></script>
|
||||
<script src="/static/project.js?v=20260915d" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
@media (max-width: 980px) { .pj-home { grid-template-columns: 1fr; } }
|
||||
|
||||
.pj-project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; align-items: start; }
|
||||
.pj-project-grid[hidden] { display: none; }
|
||||
.pj-project-card {
|
||||
position: relative; border: 1px solid #e7e9ec; border-radius: 14px;
|
||||
background: #fff; transition: box-shadow .14s, transform .1s;
|
||||
|
||||
+221
-8
@@ -72,20 +72,23 @@
|
||||
}
|
||||
|
||||
// ── 완료 프로젝트 섹션 접기/펼치기 (localStorage 기억) ──
|
||||
// display 를 인라인 스타일로 직접 제어 — 외부 CSS 규칙과의 특이도/우선순위
|
||||
// 문제를 원천 차단(어떤 스타일시트보다 인라인 style 이 항상 우선한다).
|
||||
const toggleBtn = el("pj-toggle-done");
|
||||
const doneGrid = el("pj-project-grid-done");
|
||||
if (toggleBtn && doneGrid) {
|
||||
const KEY = "pj_hide_done";
|
||||
function apply(hide) {
|
||||
doneGrid.hidden = hide;
|
||||
let hideDone = localStorage.getItem(KEY) === "1";
|
||||
function apply() {
|
||||
doneGrid.style.display = hideDone ? "none" : "";
|
||||
const n = toggleBtn.dataset.count;
|
||||
toggleBtn.textContent = hide ? "완료 " + n + "개 보기" : "완료 " + n + "개 숨기기";
|
||||
toggleBtn.textContent = hideDone ? "완료 " + n + "개 보기" : "완료 " + n + "개 숨기기";
|
||||
}
|
||||
apply(localStorage.getItem(KEY) === "1");
|
||||
apply();
|
||||
toggleBtn.addEventListener("click", function () {
|
||||
const hide = !doneGrid.hidden;
|
||||
try { localStorage.setItem(KEY, hide ? "1" : "0"); } catch (_) {}
|
||||
apply(hide);
|
||||
hideDone = !hideDone;
|
||||
try { localStorage.setItem(KEY, hideDone ? "1" : "0"); } catch (_) {}
|
||||
apply();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +159,190 @@
|
||||
catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── 업무 편집 팝업 (카드/내 업무 클릭 시 페이지 이동 없이) ──
|
||||
const homeTaskModal = el("pj-modal-task");
|
||||
const homeDataEl = el("pj-home-data");
|
||||
if (homeTaskModal && homeDataEl) {
|
||||
wireModalClose(homeTaskModal);
|
||||
let homeTasks = {};
|
||||
try { homeTasks = JSON.parse(homeDataEl.textContent) || {}; } catch (_) {}
|
||||
|
||||
async function fillAssignee(pid, currentEmail) {
|
||||
const sel = el("pj-t-assignee");
|
||||
let list = [];
|
||||
try { list = (await api("GET", "/project/api/projects/" + pid + "/members")).members || []; }
|
||||
catch (_) { list = []; }
|
||||
sel.innerHTML = '<option value="">(미지정)</option>' + list.map(function (m) {
|
||||
const name = idOf(m.user_email);
|
||||
return '<option value="' + m.user_email + '" data-name="' + escapeHtml(name) + '">' + escapeHtml(name) + "</option>";
|
||||
}).join("");
|
||||
sel.value = currentEmail || "";
|
||||
}
|
||||
async function fillStage(pid, currentStageId) {
|
||||
const sel = el("pj-t-stage");
|
||||
let list = [];
|
||||
try { list = (await api("GET", "/project/api/projects/" + pid + "/stages")).stages || []; }
|
||||
catch (_) { list = []; }
|
||||
sel.innerHTML = list.map(function (s) {
|
||||
return '<option value="' + s.id + '">' + escapeHtml(s.name) + "</option>";
|
||||
}).join("");
|
||||
sel.value = currentStageId || (list[0] ? list[0].id : "");
|
||||
}
|
||||
async function loadHomeAttachments(taskId) {
|
||||
const ul = el("pj-attach-list");
|
||||
ul.innerHTML = "<li class='pj-empty-sm'>불러오는 중…</li>";
|
||||
try {
|
||||
const res = await api("GET", "/project/api/tasks/" + taskId + "/attachments");
|
||||
if (!res.attachments.length) { ul.innerHTML = "<li class='pj-empty-sm'>없음</li>"; return; }
|
||||
ul.innerHTML = "";
|
||||
res.attachments.forEach(function (a) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "pj-attach-item";
|
||||
li.innerHTML =
|
||||
'<a href="/project/api/attachments/' + a.id + '/download">' + escapeHtml(a.filename) + "</a>" +
|
||||
'<button type="button" class="pj-icon-btn pj-attach-del" data-id="' + a.id + '">×</button>';
|
||||
ul.appendChild(li);
|
||||
});
|
||||
ul.querySelectorAll(".pj-attach-del").forEach(function (b) {
|
||||
b.addEventListener("click", async function () {
|
||||
if (!confirm("이 첨부를 삭제할까요?")) return;
|
||||
try { await api("DELETE", "/project/api/attachments/" + b.dataset.id); loadHomeAttachments(taskId); }
|
||||
catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
});
|
||||
} catch (e) { ul.innerHTML = "<li class='pj-empty-sm'>불러오기 실패</li>"; }
|
||||
}
|
||||
const homeAttachInput = el("pj-attach-input");
|
||||
if (homeAttachInput) homeAttachInput.addEventListener("change", async function () {
|
||||
const taskId = el("pj-t-id").value;
|
||||
if (!taskId || !homeAttachInput.files.length) return;
|
||||
const fd = new FormData();
|
||||
fd.append("file", homeAttachInput.files[0]);
|
||||
try {
|
||||
const res = await fetch("/project/api/tasks/" + taskId + "/attachments", { method: "POST", body: fd });
|
||||
if (!res.ok) { let m = res.statusText; try { m = (await res.json()).detail; } catch (_) {} throw new Error(m); }
|
||||
homeAttachInput.value = "";
|
||||
loadHomeAttachments(taskId);
|
||||
} catch (e) { alert("업로드 실패: " + e.message); }
|
||||
});
|
||||
async function loadHomeComments(taskId) {
|
||||
const ul = el("pj-comment-list");
|
||||
ul.innerHTML = "<li class='pj-empty-sm'>불러오는 중…</li>";
|
||||
try {
|
||||
const res = await api("GET", "/project/api/tasks/" + taskId + "/comments");
|
||||
if (!res.comments.length) { ul.innerHTML = "<li class='pj-empty-sm'>댓글 없음</li>"; return; }
|
||||
ul.innerHTML = "";
|
||||
res.comments.forEach(function (c) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "pj-comment-item";
|
||||
li.innerHTML =
|
||||
'<span class="material-symbols-outlined pj-gicon pj-gicon-sm">account_circle</span>' +
|
||||
'<div class="pj-comment-main"><div class="pj-comment-head">' +
|
||||
escapeHtml(idOf(c.author_email)) + ' <span class="pj-comment-time">' +
|
||||
(c.created_at || "").slice(0, 16).replace("T", " ") + "</span></div>" +
|
||||
'<div class="pj-comment-body"></div></div>' +
|
||||
'<span class="pj-comment-acts"><button type="button" class="pj-icon-btn pj-comment-del" title="삭제">×</button></span>';
|
||||
li.querySelector(".pj-comment-body").textContent = c.body;
|
||||
li.querySelector(".pj-comment-del").addEventListener("click", async function () {
|
||||
try { await api("DELETE", "/project/api/comments/" + c.id); loadHomeComments(taskId); }
|
||||
catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
ul.appendChild(li);
|
||||
});
|
||||
} catch (e) { ul.innerHTML = "<li class='pj-empty-sm'>불러오기 실패</li>"; }
|
||||
}
|
||||
el("pj-comment-send").addEventListener("click", async function () {
|
||||
const taskId = el("pj-t-id").value;
|
||||
const inp = el("pj-comment-input");
|
||||
const body = inp.value.trim();
|
||||
if (!taskId || !body) return;
|
||||
try {
|
||||
await api("POST", "/project/api/tasks/" + taskId + "/comments", { body: body });
|
||||
inp.value = "";
|
||||
loadHomeComments(taskId);
|
||||
} catch (e) { alert("등록 실패: " + e.message); }
|
||||
});
|
||||
el("pj-comment-input").addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") { e.preventDefault(); el("pj-comment-send").click(); }
|
||||
});
|
||||
|
||||
async function openHomeTaskModal(task) {
|
||||
el("pj-task-modal-title").textContent = "업무 편집";
|
||||
el("pj-t-id").value = task.id;
|
||||
el("pj-t-project-id").value = task.project_id;
|
||||
el("pj-t-title").value = task.title || "";
|
||||
el("pj-t-desc").value = task.description || "";
|
||||
el("pj-t-priority").value = task.priority || "normal";
|
||||
el("pj-t-start").value = task.start_date || "";
|
||||
el("pj-t-due").value = task.due_date || "";
|
||||
const useTime = !!(task.start_time || task.due_time);
|
||||
el("pj-t-usetime").checked = useTime;
|
||||
el("pj-t-stime").value = task.start_time || "";
|
||||
el("pj-t-dtime").value = task.due_time || "";
|
||||
el("pj-t-stime").disabled = !useTime;
|
||||
el("pj-t-dtime").disabled = !useTime;
|
||||
el("pj-t-open-project").href = "/project/p/" + task.project_id + "?task=" + task.id;
|
||||
openModal(homeTaskModal);
|
||||
await Promise.all([
|
||||
fillAssignee(task.project_id, task.assignee_email || ""),
|
||||
fillStage(task.project_id, task.stage_id),
|
||||
]);
|
||||
loadHomeAttachments(task.id);
|
||||
loadHomeComments(task.id);
|
||||
}
|
||||
|
||||
el("pj-t-usetime").addEventListener("change", function () {
|
||||
el("pj-t-stime").disabled = !this.checked;
|
||||
el("pj-t-dtime").disabled = !this.checked;
|
||||
});
|
||||
["pj-t-stime", "pj-t-dtime"].forEach(function (id) {
|
||||
const inp = el(id);
|
||||
if (inp) inp.addEventListener("click", function () { try { this.showPicker(); } catch (_) {} });
|
||||
});
|
||||
|
||||
el("pj-save-task").addEventListener("click", async function () {
|
||||
const id = el("pj-t-id").value;
|
||||
if (!id) return;
|
||||
const assigneeSel = el("pj-t-assignee");
|
||||
const assigneeName = assigneeSel.selectedOptions[0] ? (assigneeSel.selectedOptions[0].dataset.name || "") : "";
|
||||
const useTime = el("pj-t-usetime").checked;
|
||||
const payload = {
|
||||
title: el("pj-t-title").value.trim(),
|
||||
description: el("pj-t-desc").value.trim(),
|
||||
assignee_email: assigneeSel.value,
|
||||
assignee_name: assigneeName,
|
||||
stage_id: parseInt(el("pj-t-stage").value, 10) || null,
|
||||
priority: el("pj-t-priority").value,
|
||||
start_date: el("pj-t-start").value || null,
|
||||
due_date: el("pj-t-due").value || null,
|
||||
start_time: useTime ? (el("pj-t-stime").value || null) : null,
|
||||
due_time: useTime ? (el("pj-t-dtime").value || null) : null,
|
||||
};
|
||||
if (!payload.title) { alert("제목을 입력하세요."); return; }
|
||||
try {
|
||||
await api("PUT", "/project/api/tasks/" + id, payload);
|
||||
location.reload();
|
||||
} catch (e) { alert("저장 실패: " + e.message); }
|
||||
});
|
||||
el("pj-delete-task").addEventListener("click", async function () {
|
||||
const id = el("pj-t-id").value;
|
||||
if (!id) return;
|
||||
if (!confirm("이 업무를 삭제할까요?")) return;
|
||||
try { await api("DELETE", "/project/api/tasks/" + id); location.reload(); }
|
||||
catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
|
||||
document.querySelectorAll(".pj-task-popup-link").forEach(function (a) {
|
||||
a.addEventListener("click", function (e) {
|
||||
const tid = a.dataset.taskId;
|
||||
const task = homeTasks[tid];
|
||||
if (!task) return; // 정보 없으면 기존 링크 이동으로 폴백
|
||||
e.preventDefault();
|
||||
openHomeTaskModal(task);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
@@ -783,12 +970,28 @@
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// 담당자 select — 업무가 현재 로드된 프로젝트가 아니면 그 프로젝트 멤버를 새로 불러온다
|
||||
// (전체 뷰/좌측 트리에서 타 프로젝트 업무를 열 때 담당자 옵션이 비어 보이는 문제 방지).
|
||||
async function fillAssigneeSelect(projId, currentEmail) {
|
||||
const sel = el("pj-t-assignee");
|
||||
let list = members;
|
||||
if (projId != null && projId !== projectId) {
|
||||
try { list = (await api("GET", "/project/api/projects/" + projId + "/members")).members || []; }
|
||||
catch (_) { list = []; }
|
||||
}
|
||||
sel.innerHTML = '<option value="">(미지정)</option>' + list.map(function (m) {
|
||||
const name = idOf(m.user_email);
|
||||
return '<option value="' + m.user_email + '" data-name="' + escapeHtml(name) + '">' + escapeHtml(name) + "</option>";
|
||||
}).join("");
|
||||
sel.value = currentEmail || "";
|
||||
}
|
||||
|
||||
function openTaskModal(task) {
|
||||
el("pj-task-modal-title").textContent = task ? "업무 편집" : "새 업무";
|
||||
el("pj-t-id").value = task ? task.id : "";
|
||||
el("pj-t-title").value = task ? task.title : "";
|
||||
el("pj-t-desc").value = task ? (task.description || "") : "";
|
||||
el("pj-t-assignee").value = task ? (task.assignee_email || "") : "";
|
||||
fillAssigneeSelect(task ? task.project_id : projectId, task ? (task.assignee_email || "") : "");
|
||||
fillStageSelect(task ? task.project_id : projectId);
|
||||
el("pj-t-stage").value = task && task.stage_id ? task.stage_id : (stages[0] ? stages[0].id : "");
|
||||
el("pj-t-priority").value = task ? (task.priority || "normal") : "normal";
|
||||
@@ -1048,6 +1251,16 @@
|
||||
return h + "</li>";
|
||||
}
|
||||
function wireTreeHandlers(rootUl) {
|
||||
// 좌측 트리의 업무 클릭 — 이미 로드된 tasks 배열에서 바로 찾아 팝업(페이지 이동 없음).
|
||||
rootUl.querySelectorAll(".pj-tree-tasklink").forEach(function (a) {
|
||||
a.addEventListener("click", function (e) {
|
||||
const tid = a.dataset.task;
|
||||
const t = tasks.find(function (x) { return String(x.id) === String(tid); });
|
||||
if (!t) return; // 못 찾으면 기존 링크 이동으로 폴백
|
||||
e.preventDefault();
|
||||
openTaskModal(t);
|
||||
});
|
||||
});
|
||||
rootUl.querySelectorAll(".pj-tree-caret").forEach(function (caret) {
|
||||
caret.addEventListener("click", function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user