- 띠 조건에서 「회사 층일 때만」 뺌 — 회사를 고칠 수 있는 사람이면 개인 · 마스터 층에서도 보임 · 누르면 요청 목록 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_ShareRequest.ts
|
|
* 회사에 올리기 요청 · 승인(PLAN 706-1) — 서버 `M02_Share_Request.py` 짝(장부 = 회사 층 파일 하나).
|
|
* 일반 사용자 = 자기 개인 층 구조물 조합 · 사전조합 나무 오른쪽 클릭 「회사에 올리기 요청」 → 글(선택) ·
|
|
* 같은 것 요청이 남아 있으면 「요청 중」(흐림) · 결과는 다음에 M02 열 때 위 띠(닫으면 봄)
|
|
* 회사 관리자 = M02 위 띠 「올리기 요청 n」(어느 층이든) → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] ·
|
|
* [승인](폴더 고름 = 「회사로 복사…」와 같은 길 · 원본 그대로) · [거절](사유 한 줄)
|
|
* 관리자(회사 층을 고칠 수 있는 사람)는 「회사로 복사…」가 있어 요청 메뉴를 안 둠.
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createBanner } from "@ui/ui_template_banner";
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { createListRow } from "@ui/ui_template_list_row";
|
|
import type { TreeMenuItem } from "@ui/ui_template_tree";
|
|
import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
|
import { call, fetchPrecomboTree, fetchTree, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
|
import { accessFor, ownOwner } from "./M02_MasterTemplete_Layer";
|
|
import { askCopyFolder } from "./M02_MasterTemplete_TreePanel_CopyTo";
|
|
import { askFolder, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
|
import type { Target } from "./M02_MasterTemplete_TreePanel_Pick";
|
|
|
|
type ShareKind = "item" | "precombo";
|
|
|
|
export interface ShareRow {
|
|
id: string;
|
|
종류: ShareKind;
|
|
키: string;
|
|
이름: string;
|
|
경로: string[];
|
|
주인: string;
|
|
요청자: number;
|
|
요청자이름: string;
|
|
글: string;
|
|
날짜: string;
|
|
상태: "요청" | "승인" | "거절";
|
|
처리자?: string;
|
|
사유?: string;
|
|
사본?: string;
|
|
봄?: boolean;
|
|
}
|
|
|
|
const KIND_NAME: Record<ShareKind, string> = { item: "구조물 조합", precombo: "사전조합" };
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
|
const post = <T>(path: string, body: object = {}): Promise<T> =>
|
|
call<T>(path, { method: "POST", body: JSON.stringify(body) });
|
|
|
|
export interface ShareOptions {
|
|
user: SessionUser | null;
|
|
/** 볼 회사(시스템 관리자는 고른 회사) */
|
|
company: string | null;
|
|
/** 지금 페이지 층 — 개인 층이면 요청 메뉴 */
|
|
layer: Layer;
|
|
owner: string | null;
|
|
showView: (label: string, node: HTMLElement) => void;
|
|
/** 사전조합 미리 보기 — 편집 칸에 읽기만으로 */
|
|
openPrecombo: (row: ShareRow) => void;
|
|
/** 구조물 조합 미리 보기 — 요청자 개인 층으로 */
|
|
openItem: (row: ShareRow) => void;
|
|
/** 승인 뒤 — 나무 · 좌측 다시 읽기 */
|
|
changed: () => void;
|
|
}
|
|
|
|
export interface ShareHandle {
|
|
/** 편집 칸 맨 위 띠 자리(관리자 「올리기 요청 n」 · 요청자 결과) */
|
|
top: HTMLElement;
|
|
/** 구조물 조합 나무 줄 덧메뉴 — 자기 개인 층 항목만 */
|
|
itemMenu: (target: Target) => TreeMenuItem[];
|
|
/** 사전조합 나무 줄 덧메뉴 — 자기 개인 층만(`mine`) */
|
|
precomboMenu: (mine: boolean, key: string, name: string) => TreeMenuItem[];
|
|
}
|
|
|
|
export function createShare(opt: ShareOptions): ShareHandle {
|
|
const top = el("div", { attrs: { "data-share": "" } });
|
|
const companyOwner = opt.company ?? ownOwner("company", opt.user);
|
|
// 요청할 사람 = 회사가 있고 회사 층을 못 고침(고칠 수 있으면 「회사로 복사…」)
|
|
const asker =
|
|
!!ownOwner("personal", opt.user) && !accessFor("company", companyOwner, opt.user).canWrite;
|
|
const boss = !!companyOwner && accessFor("company", companyOwner, opt.user).canWrite;
|
|
const q = opt.company ? `?company=${encodeURIComponent(opt.company)}` : "";
|
|
let rows: ShareRow[] = [];
|
|
let listHost: HTMLElement | null = null;
|
|
|
|
const pending = (kind: ShareKind, key: string): boolean =>
|
|
rows.some((r) => r.종류 === kind && r.키 === key && r.상태 === "요청");
|
|
|
|
const refresh = async (): Promise<void> => {
|
|
if (!asker && !boss) return;
|
|
rows = (await call<{ 줄: ShareRow[] }>(`/share-requests${q}`).catch(() => ({ 줄: [] }))).줄;
|
|
paintTop();
|
|
if (listHost?.isConnected) paintList(listHost);
|
|
};
|
|
|
|
function paintTop(): void {
|
|
if (boss) {
|
|
const n = rows.filter((r) => r.상태 === "요청").length;
|
|
top.replaceChildren(
|
|
...(rows.length
|
|
? [
|
|
createBanner({
|
|
tone: n ? "warning" : "info",
|
|
text: n
|
|
? `회사에 올리기 요청 ${n} 건 — 승인하면 회사 층에 사본`
|
|
: "올리기 요청 없음",
|
|
actions: [
|
|
createButton({ label: `올리기 요청 ${n}`, size: "sm", onClick: openList }),
|
|
],
|
|
}),
|
|
]
|
|
: []),
|
|
);
|
|
return;
|
|
}
|
|
const done = rows.filter((r) => r.봄 === false);
|
|
top.replaceChildren(
|
|
...(done.length
|
|
? [
|
|
createBanner({
|
|
tone: done.some((r) => r.상태 === "거절") ? "warning" : "success",
|
|
title: "회사에 올리기 결과",
|
|
text: done
|
|
.map((r) => `「${r.이름}」 ${r.상태}${r.사유 ? `(${r.사유})` : ""}`)
|
|
.join(" · "),
|
|
onClose: () => void post(`/share-requests/seen`).catch(() => undefined),
|
|
}),
|
|
]
|
|
: []),
|
|
);
|
|
}
|
|
|
|
async function ask(kind: ShareKind, key: string, name: string): Promise<void> {
|
|
const got = await askText(
|
|
`회사에 올리기 요청 — ${name}`,
|
|
[{ key: "글", label: "요청 글(선택)", optional: true }],
|
|
"요청",
|
|
);
|
|
if (!got) return;
|
|
try {
|
|
await post("/share-requests", { 종류: kind, 키: key, 글: got.글 });
|
|
showToast("요청함 — 회사 관리자가 승인하면 회사 층에 올라감", "success");
|
|
} catch (error) {
|
|
showToast(`요청 못 함 — ${why(error)}`, "error");
|
|
}
|
|
await refresh();
|
|
}
|
|
|
|
const menu = (kind: ShareKind, key: string, name: string): TreeMenuItem[] =>
|
|
pending(kind, key)
|
|
? [
|
|
{
|
|
label: "회사에 올리기 요청 중",
|
|
run: () => {},
|
|
disabledReason: "회사 관리자가 아직 안 봄",
|
|
},
|
|
]
|
|
: [{ label: "회사에 올리기 요청", run: () => void ask(kind, key, name) }];
|
|
|
|
async function approve(row: ShareRow): Promise<void> {
|
|
const title = `회사에 올리기 승인 — ${row.이름}`;
|
|
const dest =
|
|
row.종류 === "item"
|
|
? await askCopyFolder(fetchTree, { layer: "company", owner: companyOwner, label: title }, 1)
|
|
: await askFolder(
|
|
title,
|
|
(await fetchPrecomboTree("company", companyOwner).catch(() => null))?.폴더 ?? [],
|
|
() => false,
|
|
[],
|
|
"넣을 폴더",
|
|
);
|
|
if (!dest) return;
|
|
try {
|
|
await post(`/share-requests/${row.id}/approve${q}`, { 경로: dest });
|
|
showToast(`「${row.이름}」 회사 층에 올림 — ${dest.join(" / ") || "맨 위"}`, "success");
|
|
opt.changed();
|
|
} catch (error) {
|
|
showToast(`승인 못 함 — ${why(error)}`, "error");
|
|
}
|
|
await refresh();
|
|
}
|
|
|
|
async function reject(row: ShareRow): Promise<void> {
|
|
const got = await askText(
|
|
`거절 — ${row.이름}`,
|
|
[{ key: "사유", label: "사유(선택)", optional: true }],
|
|
"거절",
|
|
);
|
|
if (!got) return;
|
|
try {
|
|
await post(`/share-requests/${row.id}/reject${q}`, { 사유: got.사유 });
|
|
} catch (error) {
|
|
showToast(`거절 못 함 — ${why(error)}`, "error");
|
|
}
|
|
await refresh();
|
|
}
|
|
|
|
function paintList(host: HTMLElement): void {
|
|
const line = (r: ShareRow): HTMLElement => {
|
|
const tools =
|
|
r.상태 === "요청"
|
|
? el("span", {
|
|
className: "ui-toolbar",
|
|
children: [
|
|
createButton({
|
|
label: "미리 보기",
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => (r.종류 === "item" ? opt.openItem(r) : opt.openPrecombo(r)),
|
|
}),
|
|
createButton({ label: "승인", size: "sm", onClick: () => void approve(r) }),
|
|
createButton({
|
|
label: "거절",
|
|
variant: "danger-ghost",
|
|
size: "sm",
|
|
onClick: () => void reject(r),
|
|
}),
|
|
],
|
|
})
|
|
: createBadge(
|
|
`${r.상태}${r.사유 ? ` · ${r.사유}` : ""}`,
|
|
r.상태 === "승인" ? "success" : "danger",
|
|
);
|
|
const sub = [KIND_NAME[r.종류], r.요청자이름, r.날짜.slice(0, 10).replace(/-/g, "."), r.글];
|
|
return createListRow({
|
|
label: r.이름,
|
|
sub: sub.filter(Boolean).join(" · "),
|
|
trailing: tools,
|
|
size: 36,
|
|
});
|
|
};
|
|
// 기다리는 것 먼저 · 그다음 최근 처리
|
|
const order = [...rows].sort(
|
|
(a, b) => Number(b.상태 === "요청") - Number(a.상태 === "요청") || (a.날짜 < b.날짜 ? 1 : -1),
|
|
);
|
|
host.replaceChildren(
|
|
...(order.length
|
|
? order.map(line)
|
|
: [createEmpty({ text: "올리기 요청 없음", small: true })]),
|
|
);
|
|
}
|
|
|
|
function openList(): void {
|
|
listHost = el("div", { className: "ui-sidebar-list", attrs: { "data-share-list": "" } });
|
|
paintList(listHost);
|
|
opt.showView("회사에 올리기 요청", listHost);
|
|
}
|
|
|
|
void refresh();
|
|
return {
|
|
top,
|
|
itemMenu: (t) =>
|
|
asker && opt.layer === "personal" && !opt.owner && t.kind === "item"
|
|
? menu("item", t.item.key, t.item.경로[t.item.경로.length - 1] ?? t.item.key)
|
|
: [],
|
|
precomboMenu: (mine, key, name) => (asker && mine ? menu("precombo", key, name) : []),
|
|
};
|
|
}
|