fix(793-3b): 우리 기초로 바꾸기 — 옵션 = 블록 풀이 후보 바인딩 · 단위 성격 다르면 환산 계수 입력 필요 · 가져온 식 시트 M01 바뀜 표지 걷음
- to-base 몸 환산? · 환산 없이 단위 성격 다름 = 후 None · 필요 ["환산"](막지 않음 · 화면 붉은 칸) - 화면 bind = 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본) + 블록 검산 입력 필요 · 줄 검산(빈 입력값)은 옵션을 못 읽었음 - 가져온 식 SK = M01 짝 없음 — 시트 M01바뀜 · 짝잃음 안 띄움(저장 M01판이 바탕 줄과 달라 늘 떴음) - 시험 test_793_3b 8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -397,7 +397,7 @@ export function createJudgeBlock(opt: JudgeBlockOptions): {
|
||||
});
|
||||
ok.root.dataset.judgeOk = "block";
|
||||
const actions: HTMLElement[] = [ok.root];
|
||||
const open = createButton({ label: L("M02_CheckImported"), size: 24, variant: "ghost", disabled: opt.readOnly, onClick: () => void import("./M02_MasterTemplete_Hopyo_Imported").then((m) => m.openImported({ ...{ file: opt.file, key: opt.key, no: blk.번, target: opt.target, canWrite: !opt.readOnly }, bind: (code, given) => checkJudge(opt.file, opt.key, { 블록: blk.번, 고른: [bare({ 갈래: "기초", 키: code, 이름: code, 입력값: given, 출처: {}, 몫: null })] }).then((res) => ({ 입력값: res.고른[0]?.입력값 ?? given, 필요: res.고른[0]?.입력필요 ?? [] })) })) }); // prettier-ignore
|
||||
const open = createButton({ label: L("M02_CheckImported"), size: 24, variant: "ghost", disabled: opt.readOnly, onClick: () => void import("./M02_MasterTemplete_Hopyo_Imported").then((m) => m.openImported({ ...{ file: opt.file, key: opt.key, no: blk.번, target: opt.target, canWrite: !opt.readOnly } })) }); // prettier-ignore
|
||||
open.dataset.judgeImported = String(blk.번);
|
||||
const fresh = createBadge(L("M02_CheckNew"), "accent", { size: 18 }); // 「새」(793-3b) — CheckRows.newBadge 꼴(돌림 import 피함)
|
||||
fresh.dataset.checkNew = "";
|
||||
|
||||
@@ -253,22 +253,10 @@ export function openHopyoCheck(opt: CheckOptions): { close: () => void } {
|
||||
});
|
||||
}
|
||||
|
||||
/** 기초 옵션 묶음 — 이 줄 검산(엑셀 값 · 입력 필요) */
|
||||
const bindOf = (row: JudgeRow) => (code: string, given: Record<string, unknown>) =>
|
||||
checkJudge(opt.file, opt.key, {
|
||||
호표: no,
|
||||
차례: row.차례,
|
||||
고른: [bare({ 갈래: "기초", 키: code, 이름: code, 입력값: given, 출처: {}, 몫: null })],
|
||||
}).then(
|
||||
// prettier-ignore
|
||||
(res) => ({ 입력값: res.고른[0]?.입력값 ?? given, 필요: res.고른[0]?.입력필요 ?? [] }),
|
||||
);
|
||||
|
||||
/** [가져온 식으로 열기](793-3b) — 단산 블록 → 가져온 식 SK 모달 · [이 줄에 잇기] = 이 줄 고른 것 */
|
||||
const imported = (row: JudgeRow): void =>
|
||||
openImported({
|
||||
...{ file: opt.file, key: opt.key, no: row.번!, target: opt.target, canWrite: opt.canWrite },
|
||||
bind: bindOf(row),
|
||||
onLink: (p) => kit.edit(row, [p]),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* · 열기 = 서버 `…/sangeun/from-hopyo`(그 블록 → 가져온 식 SK · 만들 층 · 엑셀 단가 그대로 · 같은 내용 = 그 SK 다시 씀)
|
||||
* → 그 SK 를 읽어 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로) + 머리 판(`Hopyo_ImportedPanel`)
|
||||
* · [우리 단가로] · [엑셀 단가로] · [이음 고르기] · [우리 기초로 바꾸기] = 서버 `…/sangeun/imported/{price|link|to-base}`
|
||||
* (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶 · 기초로 바꿀 때 옵션 = 불러오기 검산(`bind` · 엑셀 값 묶음 · 입력 필요)
|
||||
* (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶 · 기초로 바꿀 때 옵션 = 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본) →
|
||||
* 블록 검산으로 입력 필요 · 단위 성격이 다르면 「환산 계수」도 입력 필요
|
||||
* · [저장] = 그 SK 저장(서버가 다시 셈) · [이 줄에 잇기] = 저장 뒤 단산 줄 고른 것 = 그 SK(갈래 단가산출 · 키) —
|
||||
* 확인 · [사전조합 생성]은 기존 길(자동 확인 · 옮김 없음) · 입력 필요가 비면 그 줄 확인 막힘
|
||||
* ========================================================================== */
|
||||
@@ -18,6 +19,7 @@ import { askConfirm, openModal } from "@ui/ui_template_modal";
|
||||
import { call, readTemplate, saveTemplate } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { checkJudge, readJudgeBlock } from "./M02_MasterTemplete_Hopyo_JudgeApi";
|
||||
import { importedPanel, type PanelState } from "./M02_MasterTemplete_Hopyo_ImportedPanel";
|
||||
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
|
||||
import {
|
||||
@@ -28,7 +30,7 @@ import {
|
||||
type Money4,
|
||||
type Tier,
|
||||
} from "./logic/M02_Hopyo_Imported_Model";
|
||||
import type { DraftPick } from "./logic/M02_MasterTemplete_Hopyo_Picks";
|
||||
import { bare, type DraftPick } from "./logic/M02_MasterTemplete_Hopyo_Picks";
|
||||
|
||||
const TEXT = {
|
||||
Title: ["가져온 식 단가산출 — 단산{n}", "Imported build-up — block {n}"],
|
||||
@@ -53,8 +55,6 @@ export interface ImportedOptions {
|
||||
/** SK 를 만들 층 */
|
||||
target: HopyoScope;
|
||||
canWrite: boolean;
|
||||
/** 기초 옵션 묶음(불러오기 검산) → 엔진 입력값 · 입력 필요 */
|
||||
bind: (code: string, given: Record<string, unknown>) => Promise<{ 입력값: Record<string, unknown>; 필요: string[] }>; // prettier-ignore
|
||||
/** 단산 줄에서 열었을 때만 — [이 줄에 잇기] */
|
||||
onLink?: (pick: DraftPick) => void;
|
||||
}
|
||||
@@ -70,8 +70,24 @@ const fromHopyo = (o: ImportedOptions): Promise<Made> =>
|
||||
body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: o.no }), // prettier-ignore
|
||||
});
|
||||
|
||||
/** 기초 옵션 — 블록 풀이 그 기초 후보 바인딩(엑셀 값 · 기본 · 근거 옵션) → 블록 검산 입력 필요 + 엇갈림 */
|
||||
async function bind(
|
||||
o: ImportedOptions,
|
||||
code: string,
|
||||
given: Record<string, unknown>,
|
||||
): Promise<{ 입력값: Record<string, unknown>; 필요: string[] }> {
|
||||
const blk = await readJudgeBlock(o.file, o.key, o.no, o.target).catch(() => null);
|
||||
const b = blk?.절.flatMap((s) => s.후보).find((c) => c.키 === code)?.바인딩;
|
||||
const values = { ...(b?.기본 ?? {}), ...(b?.입력값 ?? {}), ...given };
|
||||
const pick = bare({ 갈래: "기초", 키: code, 이름: code, 입력값: values, 출처: {}, 몫: null });
|
||||
const res = await checkJudge(o.file, o.key, { 블록: o.no, 고른: [pick] });
|
||||
const need = [...(res.고른[0]?.입력필요 ?? []), ...Object.keys(b?.엇갈림 ?? {})];
|
||||
return { 입력값: values, 필요: [...new Set(need)] };
|
||||
}
|
||||
|
||||
interface EditAnswer {
|
||||
문서: ImportedDoc;
|
||||
필요?: string[];
|
||||
전?: Money4 | null;
|
||||
후?: Money4 | null;
|
||||
이음없음?: { 자리: string; 이름: string }[];
|
||||
@@ -163,9 +179,9 @@ export function openImported(o: ImportedOptions): void {
|
||||
async function toBase(code: string, cand: BasisCand | null): Promise<void> {
|
||||
st.busy = true;
|
||||
draw();
|
||||
let bound: Awaited<ReturnType<ImportedOptions["bind"]>>;
|
||||
let bound: Awaited<ReturnType<typeof bind>>;
|
||||
try {
|
||||
bound = await o.bind(code, cand?.옵션 ?? {});
|
||||
bound = await bind(o, code, cand?.옵션 ?? {});
|
||||
} catch (e) {
|
||||
st.busy = false;
|
||||
draw();
|
||||
@@ -175,7 +191,7 @@ export function openImported(o: ImportedOptions): void {
|
||||
const got = await run("to-base", { 로직: code, 입력값: bound.입력값 });
|
||||
if (!got) return;
|
||||
tier = cand?.단 ?? null;
|
||||
st.need = bound.필요;
|
||||
st.need = [...bound.필요, ...(got.필요 ?? [])];
|
||||
st.unlinked = [];
|
||||
draw();
|
||||
}
|
||||
@@ -184,12 +200,18 @@ export function openImported(o: ImportedOptions): void {
|
||||
openFindPicker({ layer: o.target.layer, owner: o.target.owner, kinds: ["logic"], logic: (x) => void toBase(x.키, null) }); // prettier-ignore
|
||||
}
|
||||
|
||||
/** 입력 필요 칸 — 옵션 = SK 입력값 · 「환산」 = SK 환산 계수 */
|
||||
function fill(name: string, value: string): void {
|
||||
const doc = now();
|
||||
const kept = { ...(doc.입력값 ?? {}) };
|
||||
if (value.trim()) kept[name] = value.trim();
|
||||
else delete kept[name];
|
||||
const v = value.trim();
|
||||
dirty = true;
|
||||
if (name === "환산") {
|
||||
const { 환산: _old, ...rest } = doc;
|
||||
return mount(v && Number(v) > 0 ? { ...rest, 환산: { 계수: Number(v) } } : rest);
|
||||
}
|
||||
const kept = { ...(doc.입력값 ?? {}) };
|
||||
if (v) kept[name] = v;
|
||||
else delete kept[name];
|
||||
mount({ ...doc, 입력값: kept });
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { newBadge } from "./M02_MasterTemplete_Hopyo_CheckRows";
|
||||
import {
|
||||
gapText,
|
||||
isImported,
|
||||
needValues,
|
||||
stillNeed,
|
||||
tiers,
|
||||
TIERS,
|
||||
@@ -61,6 +62,7 @@ const TEXT = {
|
||||
PickBase: ["기초 고르기", "Pick basis"],
|
||||
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
||||
Need: ["입력 필요", "Input needed"],
|
||||
Swap: ["환산 계수(기초 1 단위 = 이 단가산출 몇 단위)", "Unit factor (1 basis unit = n of this)"],
|
||||
NeedHint: ["채워야 이 줄을 확인 · 옮김", "Fill to confirm and move this line"],
|
||||
Close: ["닫기", "Close"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
@@ -222,10 +224,12 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const g = tx("Gap").replace("{a}", won(gone.전)).replace("{b}", won(gone.후));
|
||||
kids.push(createBanner({ tone: "info", text: g.replace("{d}", gapText(gone.전, gone.후) || "0") })); // prettier-ignore
|
||||
}
|
||||
const left = stillNeed(s.need, s.doc.입력값);
|
||||
const values = needValues(s.doc);
|
||||
const left = stillNeed(s.need, values);
|
||||
if (s.need.length) {
|
||||
const fields = s.need.map((k) => {
|
||||
const f = createInputField({ label: k, size: 28, value: String(s.doc.입력값?.[k] ?? "") });
|
||||
const label = k === "환산" ? tx("Swap") : k;
|
||||
const f = createInputField({ label, size: 28, value: String(values[k] ?? "") });
|
||||
f.input.dataset.importedNeed = k;
|
||||
f.input.disabled = s.readOnly || s.busy;
|
||||
if (left.includes(k)) f.setError(tx("Need"));
|
||||
|
||||
@@ -384,6 +384,7 @@ def sheet(ans: dict, clean: dict, raw: Any, given: dict | None, trail: tuple) ->
|
||||
mids0, env0 = _mids(whole, key, base, values)
|
||||
mids1, _ = _mids(whole, key, formulas(base, clean), values)
|
||||
stamps = (raw.get("M01판") if isinstance(raw, dict) else None) or {}
|
||||
stamps = {} if imp.is_imported(clean) else stamps # 가져온 식 = M01 짝 없음(793-3b)
|
||||
fresh = stamp(base)["M01판"]
|
||||
why = clean.get("까닭") or {}
|
||||
nums, finds = clean.get("상수덮개") or {}, clean.get("표값덮개") or {}
|
||||
|
||||
@@ -28,7 +28,9 @@ SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않
|
||||
→ {이름(SK 키), 검산, 단가기준}(서버가 다시 셈해 엑셀과 맞댐 · 같은 내용 409 = 기존 길)
|
||||
POST …/sangeun/imported/price 몸 {문서, 기준} → {문서, 전, 후, 이음없음 [{자리, 이름}]}
|
||||
POST …/sangeun/imported/link 몸 {문서, 자리, 이음} → {문서}(이음 고름 · 덮개 열쇠 옮김)
|
||||
POST …/sangeun/imported/to-base 몸 {문서, 로직(M01 GC), 입력값} → {문서, 전, 후, 차이}
|
||||
POST …/sangeun/imported/to-base 몸 {문서, 로직(M01 GC), 입력값, 환산?} → {문서, 전, 후, 차이,
|
||||
필요?}(기초 결과단위 성격이 SK 단위와 달라 환산 계수가 없으면 후 = None ·
|
||||
필요 ["환산"] · 793-3b)
|
||||
셈 답(`…/sangeun/calc`) = 옛 꼴 그대로 · `원래` = 엑셀 금액(「M01 원래」 자리) · 덧칸 `가져온`
|
||||
{단가기준, 파일, 시트, 행, 갈래, 엑셀금액, 글줄 [{행, 글, 자리, 엑셀, 지금, 같음}], 이음없음}.
|
||||
"""
|
||||
@@ -447,21 +449,29 @@ def _rate_rekey(one: Any, move: dict) -> Any:
|
||||
return {**one, "고정": {**one["고정"], "비율": {**rate, "기준": keys}}}
|
||||
|
||||
|
||||
def to_base(doc: Any, code: str, given: dict | None = None) -> dict[str, Any]:
|
||||
def to_base(doc: Any, code: str, given: dict | None = None, swap: Any = None) -> dict[str, Any]:
|
||||
"""[우리 기초로 바꾸기] — 가져온 식 SK → M01 기초(바탕 로직) + 옵션 SK · 고침 걷힘 ·
|
||||
원본 · 검산 · 근거후보 보존 + `바꿈` {로직, 단가기준, 전, 후, 차이}(1 단위)."""
|
||||
원본 · 검산 · 근거후보 보존 + `바꿈` {로직, 단가기준, 전, 후, 차이}(1 단위) ·
|
||||
`swap` = SK 단위 환산 {계수} · 없는데 단위 성격이 다르면 후 = None · `필요` ["환산"](793-3b)."""
|
||||
doc = _need(doc)
|
||||
whole = _whole()
|
||||
before = _money(_run(doc, whole))
|
||||
head = {k: doc[k] for k in ("키", "이름", "단위", "구분", "상세구분") if k in doc}
|
||||
new = {**head, "바탕": {"로직": str(code or "").strip()}, "입력값": dict(given or {})}
|
||||
new |= {"열린입력": [], "고름": {}, "자재표시": {}, "덮개": {}, "뺀줄": [], "더한줄": []}
|
||||
after = _money(_run(new, whole))
|
||||
new |= {"환산": swap} if swap else {}
|
||||
need: list[str] = []
|
||||
try:
|
||||
after = _money(_run(new, whole))
|
||||
except sd.DocError as e:
|
||||
if swap or not str(e).startswith("단가산출 단위"):
|
||||
raise
|
||||
after, need = None, ["환산"]
|
||||
gap = {k: after[k] - before[k] for k in after} if before and after else None
|
||||
keep = {k: doc[KEY][k] for k in ("원본", "검산", "근거후보") if doc[KEY].get(k)}
|
||||
was = {k: doc[KEY].get(k) for k in ("로직", "단가기준")}
|
||||
new[KEY] = {**keep, "바꿈": {**was, "전": before, "후": after, "차이": gap}}
|
||||
return {"문서": new, "전": before, "후": after, "차이": gap}
|
||||
return {"문서": new, "전": before, "후": after, "차이": gap} | ({"필요": need} if need else {})
|
||||
|
||||
|
||||
# ── 만들기(개인 층) ───────────────────────────────────────────────────
|
||||
|
||||
@@ -152,6 +152,7 @@ class ImportedEditBody(BaseModel):
|
||||
이음: str | None = None # link
|
||||
로직: str | None = None # to-base
|
||||
입력값: dict[str, Any] | None = None # to-base
|
||||
환산: dict[str, Any] | None = None # to-base(793-3b · SK 단위 환산 계수)
|
||||
|
||||
|
||||
def _sd(fn: Any) -> Any:
|
||||
@@ -169,7 +170,7 @@ def _sd(fn: Any) -> Any:
|
||||
_EDIT = {
|
||||
"price": lambda b: (imp.to_price, b.문서, b.기준),
|
||||
"link": lambda b: (imp.link, b.문서, b.자리, b.이음),
|
||||
"to-base": lambda b: (imp.to_base, b.문서, b.로직, b.입력값),
|
||||
"to-base": lambda b: (imp.to_base, b.문서, b.로직, b.입력값, b.환산),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface ImportedDoc {
|
||||
단위?: string;
|
||||
바탕?: { 로직?: string } | null;
|
||||
입력값?: Record<string, unknown>;
|
||||
/** SK 단위 환산(기초 결과단위 성격이 다를 때 · 728-2) */
|
||||
환산?: { 계수?: number | string };
|
||||
가져온?: ImportedMeta;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -81,6 +83,12 @@ export const wrongLines = (d: ImportedDoc): CheckLine[] =>
|
||||
|
||||
const blank = (v: unknown): boolean => v === undefined || v === null || String(v).trim() === "";
|
||||
|
||||
/** 입력 필요 칸 값 — 입력값 + 「환산」 = 환산 계수 */
|
||||
export const needValues = (d: ImportedDoc): Record<string, unknown> => ({
|
||||
...(d.입력값 ?? {}),
|
||||
환산: d.환산?.계수,
|
||||
});
|
||||
|
||||
/** 입력 필요 중 아직 빈 칸 */
|
||||
export const stillNeed = (need: string[], values: Record<string, unknown> | undefined): string[] =>
|
||||
need.filter((k) => blank(values?.[k]));
|
||||
@@ -93,7 +101,7 @@ export function skPick(
|
||||
need: string[],
|
||||
marks: string[],
|
||||
): DraftPick & { 표지: string[] } {
|
||||
const values = doc.입력값 ?? {};
|
||||
const values = needValues(doc);
|
||||
return {
|
||||
갈래: "단가산출",
|
||||
키: sk,
|
||||
|
||||
@@ -144,3 +144,47 @@ def test_부품상자만_700줄_공용_안_고침():
|
||||
assert "#" not in "".join(re.findall(r"color:\s*[^;]+", text)), name
|
||||
for name in ("Labor_Rows", "Labor_SangeunModal", "Sangeun_Detail", "Sangeun_Sheet"):
|
||||
assert "Hopyo_Imported" not in _src(name), name # 공용 쪽은 새 모달을 모름
|
||||
|
||||
|
||||
def test_가져온_식_시트_M01바뀜_없음(client):
|
||||
"""가져온 식 SK 는 M01 짝이 없음 — 저장 M01판이 있어도 시트 「M01 바뀜」 · 짝잃음 안 띄움."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
client.app.include_router(sangeun_router.router)
|
||||
assert _post(client).status_code == 200
|
||||
name = _open(client).json()["이름"]
|
||||
folder = layers.personal_dir(7, 42)
|
||||
with store.at(folder):
|
||||
doc = store.read("sangeun", name)["문서"]
|
||||
spots = list(calc.calc_doc(doc)["시트"]["자리"])
|
||||
ans = calc.calc_doc({**doc, "M01판": {k: "다름" for k in [*spots, "x"]}})
|
||||
sheet = ans["시트"]
|
||||
assert sheet["자리"] and not any(x["M01바뀜"] for x in sheet["자리"].values())
|
||||
assert sheet["짝잃음"] == []
|
||||
|
||||
|
||||
def test_우리_기초로_바꾸기_환산_입력_필요(tmp_path):
|
||||
"""북부청 단산10 야면석소운반(㎡) → GC001140(원/자재 단위) — 환산 없으면 후 None ·
|
||||
필요 「환산」 · 블록 풀이 바인딩 옵션 + 계수 1 = 엑셀 94,227(품 같음)."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
path = golden.FILES["북부청임도"]
|
||||
if not path.exists():
|
||||
pytest.skip("실무 엑셀 없음")
|
||||
hs._dir(tmp_path).mkdir(parents=True)
|
||||
(hs._dir(tmp_path) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
|
||||
fh.keep(tmp_path, KEY, path.name, path.read_bytes())
|
||||
out = fh.emit(tmp_path, KEY, 10)
|
||||
doc = {"키": "SK-000000", "이름": "야면석소운반", "단위": "㎡", "바탕": None, "입력값": {}}
|
||||
doc[imp.KEY] = {"로직": out["로직"], "단가기준": "엑셀", "원본": out["원본"]}
|
||||
given = {"운반거리": 6, "경사각도": 35, "운반회수": 42, "운반무게": 575, "회당운반량": 25}
|
||||
given |= {"재료종별": "암석", "재료형상": "해당 없음", "재료갈래": "토사류", "운반로": "양호"}
|
||||
given |= {"실작업시간": 0}
|
||||
bare = imp.to_base(doc, "GC001140", given)
|
||||
assert bare["후"] is None and bare["필요"] == ["환산"] and round(bare["전"]["계"]) == 94227
|
||||
got = imp.to_base(doc, "GC001140", given, {"계수": 1})
|
||||
assert (
|
||||
"필요" not in got and round(got["후"]["계"]) == 94227 and got["문서"]["환산"] == {"계수": 1}
|
||||
)
|
||||
assert got["문서"]["바탕"] == {"로직": "GC001140"} and got["문서"][imp.KEY]["바꿈"]["전"]
|
||||
|
||||
Reference in New Issue
Block a user