1
0
forked from baron/baron-sso
Files
baron-sso/adminfront/src/features/tenants/routes/worksmobileComparison.ts
2026-06-12 18:36:18 +09:00

525 lines
14 KiB
TypeScript

import type { WorksmobileComparisonItem } from "../../../lib/adminApi";
export type WorksmobileComparisonFilter =
| "works_only"
| "baron_only"
| "needs_update"
| "matched";
export type WorksmobileAccountStatusFilter =
| "all"
| "active"
| "invited"
| "suspended"
| "inactive"
| "deleted";
export type WorksmobileComparisonSummary = {
total: number;
matched: number;
needsUpdate: number;
missingInWorksmobile: number;
missingInBaron: number;
missingExternalKey: number;
};
export type WorksmobileComparisonColumnKey =
| "status"
| "baronId"
| "baron"
| "baronOrg"
| "worksmobileId"
| "externalKey"
| "worksmobileDomain"
| "worksmobile"
| "worksmobileOrg"
| "manage";
export type WorksmobileComparisonColumnVisibility = Record<
WorksmobileComparisonColumnKey,
boolean
>;
export function getDefaultWorksmobileComparisonColumns(): WorksmobileComparisonColumnVisibility {
return {
status: true,
baronId: false,
baron: true,
baronOrg: true,
worksmobileId: false,
externalKey: false,
worksmobileDomain: true,
worksmobile: true,
worksmobileOrg: true,
manage: true,
};
}
export function summarizeWorksmobileComparison(
rows: WorksmobileComparisonItem[],
): WorksmobileComparisonSummary {
return rows.reduce<WorksmobileComparisonSummary>(
(summary, row) => {
if (row.status === "matched") {
summary.matched += 1;
} else if (row.status === "needs_update") {
summary.needsUpdate += 1;
} else if (row.status === "missing_in_worksmobile") {
summary.missingInWorksmobile += 1;
} else if (row.status === "missing_in_baron") {
summary.missingInBaron += 1;
} else if (row.status === "missing_external_key") {
summary.missingExternalKey += 1;
}
return summary;
},
{
total: rows.length,
matched: 0,
needsUpdate: 0,
missingInWorksmobile: 0,
missingInBaron: 0,
missingExternalKey: 0,
},
);
}
export function getWorksmobileComparisonStatusLabel(status: string) {
switch (status) {
case "matched":
return "일치";
case "missing_in_worksmobile":
return "WORKS 없음";
case "needs_update":
return "업데이트 필요";
case "missing_in_baron":
return "Baron 없음";
case "missing_external_key":
return "ex_key 없음";
default:
return status;
}
}
export function canCreateWorksmobileRow(row: WorksmobileComparisonItem) {
return row.status === "missing_in_worksmobile" && Boolean(row.baronId);
}
const immutableWorksmobileAccountEmails = new Set([
"cyhan@samaneng.com",
"cyhan1@hanmaceng.co.kr",
"cyhan2@baroncs.co.kr",
"cyhan3@brsw.kr",
"su-@samaneng.com",
]);
const hiddenWorksmobileMemberEmails = new Set([
"su-@samaneng.com",
"cyhan1@hanmaceng.co.kr",
"cyhan2@baroncs.co.kr",
"cyhan3@brsw.kr",
]);
function normalizeWorksmobileEmail(email?: string) {
return email?.trim().toLowerCase() ?? "";
}
export function isImmutableWorksmobileAccount(row: WorksmobileComparisonItem) {
return (
row.resourceType === "USER" &&
immutableWorksmobileAccountEmails.has(
normalizeWorksmobileEmail(row.worksmobileEmail),
)
);
}
export function isHiddenWorksmobileMember(row: WorksmobileComparisonItem) {
if (row.resourceType !== "USER") {
return false;
}
return [row.worksmobileEmail, row.baronEmail].some((email) =>
hiddenWorksmobileMemberEmails.has(normalizeWorksmobileEmail(email)),
);
}
export function filterVisibleWorksmobileComparisonRows(
rows: WorksmobileComparisonItem[],
) {
return rows.filter((row) => !isHiddenWorksmobileMember(row));
}
export function getWorksmobileRowSelectionKey(row: WorksmobileComparisonItem) {
if (row.baronId) {
return `${row.resourceType}:baron:${row.baronId}`;
}
if (row.worksmobileId) {
return `${row.resourceType}:works:${row.worksmobileId}`;
}
if (row.externalKey) {
return `${row.resourceType}:external:${row.externalKey}`;
}
return "";
}
export function canSelectWorksmobileRow(row: WorksmobileComparisonItem) {
return (
Boolean(getWorksmobileRowSelectionKey(row)) &&
!isImmutableWorksmobileAccount(row)
);
}
export function getWorksmobileSelectedActionIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
) {
const selected = new Set(selectedKeys);
return rows
.filter((row) => selected.has(getWorksmobileRowSelectionKey(row)))
.map((row) => row.baronId)
.filter((id): id is string => Boolean(id));
}
export function getWorksmobileSelectedCreateUserIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
) {
const selected = new Set(selectedKeys);
return rows
.filter(
(row) =>
row.resourceType === "USER" &&
row.status === "missing_in_worksmobile" &&
selected.has(getWorksmobileRowSelectionKey(row)),
)
.map((row) => row.baronId)
.filter((id): id is string => Boolean(id));
}
export function getWorksmobileSelectedUpdateUserIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
) {
const selected = new Set(selectedKeys);
return rows
.filter(
(row) =>
row.resourceType === "USER" &&
row.status === "needs_update" &&
selected.has(getWorksmobileRowSelectionKey(row)),
)
.map((row) => row.baronId)
.filter((id): id is string => Boolean(id));
}
export function formatWorksmobileSelectionFailureDescription(
successCount: number,
failures: string[],
) {
const summary = `성공 ${successCount}건, 실패 ${failures.length}`;
const visibleFailures = failures.slice(0, 3);
if (failures.length <= visibleFailures.length) {
return [summary, ...visibleFailures].join("\n");
}
return [
summary,
...visibleFailures,
`${failures.length - visibleFailures.length}건 실패`,
].join("\n");
}
export function getWorksmobileSelectedMissingExternalKeyOrgUnitIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
) {
return getWorksmobileSelectedWorksOnlyOrgUnitIds(rows, selectedKeys).filter(
(id) =>
rows.some(
(row) =>
row.worksmobileId === id && row.status === "missing_external_key",
),
);
}
export function getWorksmobileSelectedWorksOnlyOrgUnitIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
) {
const selected = new Set(selectedKeys);
return rows
.filter(
(row) =>
row.resourceType === "GROUP" &&
(row.status === "missing_external_key" ||
row.status === "missing_in_baron") &&
selected.has(getWorksmobileRowSelectionKey(row)),
)
.map((row) => row.worksmobileId)
.filter((id): id is string => Boolean(id));
}
const worksmobileComparisonSearchFields: Array<
keyof WorksmobileComparisonItem
> = [
"baronId",
"baronSlug",
"baronName",
"baronEmail",
"baronPrimaryOrgId",
"baronPrimaryOrgSlug",
"baronPrimaryOrgName",
"baronParentId",
"baronParentSlug",
"baronParentName",
"worksmobileId",
"externalKey",
"worksmobileName",
"worksmobileEmail",
"worksmobileAccountStatus",
"worksmobileLevelId",
"worksmobileLevelName",
"worksmobileTask",
"worksmobileDomainId",
"worksmobileDomainName",
"worksmobilePrimaryOrgId",
"worksmobilePrimaryOrgName",
"worksmobilePrimaryOrgPositionId",
"worksmobilePrimaryOrgPositionName",
"baronParentWorksmobileId",
"baronParentWorksmobileName",
"baronParentWorksmobileEmail",
"worksmobileParentId",
"worksmobileParentName",
"worksmobileParentEmail",
"worksmobileParentExternalKey",
];
export function filterWorksmobileComparisonRowsBySearch(
rows: WorksmobileComparisonItem[],
search: string,
) {
const keyword = search.trim().toLowerCase();
if (!keyword) {
return rows;
}
return rows.filter((row) =>
worksmobileComparisonSearchFields.some((field) => {
const value = row[field];
if (value === undefined || value === null) {
return false;
}
return String(value).toLowerCase().includes(keyword);
}),
);
}
export function filterWorksmobileComparisonRows(
rows: WorksmobileComparisonItem[],
filters: WorksmobileComparisonFilter[],
onlyMissingExternalKey = false,
accountStatus: WorksmobileAccountStatusFilter = "all",
) {
const allowedStatuses = new Set(
filters.flatMap((filter) => worksmobileFilterStatuses[filter]),
);
if (filters.includes("works_only")) {
if (onlyMissingExternalKey) {
allowedStatuses.delete("missing_in_baron");
}
allowedStatuses.add("missing_external_key");
}
return rows.filter((row) => {
if (accountStatus !== "all") {
return row.worksmobileAccountStatus === accountStatus;
}
if (!allowedStatuses.has(row.status)) {
return false;
}
return true;
});
}
export function formatWorksmobilePersonName(row: WorksmobileComparisonItem) {
return [
row.worksmobileName,
row.worksmobileLevelName ?? row.worksmobileLevelId,
]
.filter(Boolean)
.join(" ");
}
export function formatWorksmobileOrgDetails(row: WorksmobileComparisonItem) {
const details: string[] = [];
const position =
row.worksmobilePrimaryOrgPositionName ??
row.worksmobilePrimaryOrgPositionId;
if (position) {
details.push(`직책 ${position}`);
}
if (row.worksmobileTask) {
details.push(`직무 ${row.worksmobileTask}`);
}
if (typeof row.worksmobilePrimaryOrgIsManager === "boolean") {
details.push(row.worksmobilePrimaryOrgIsManager ? "조직장" : "조직장 아님");
}
return details;
}
export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
if (row.status === "missing_in_worksmobile" && row.worksmobileLastError) {
return [`최근 실패: ${row.worksmobileLastError}`];
}
if (row.status !== "needs_update") {
return [];
}
const details: string[] = [];
const baronName = row.baronName?.trim();
const worksmobileName = row.worksmobileName?.trim();
if (baronName && worksmobileName && baronName !== worksmobileName) {
details.push(`이름: ${worksmobileName} -> ${baronName}`);
}
if (row.resourceType === "USER") {
const expectedExternalKey = row.baronId?.trim() ?? "";
const actualExternalKey = row.externalKey?.trim() ?? "";
if (expectedExternalKey && expectedExternalKey !== actualExternalKey) {
details.push(
`external_key: ${actualExternalKey || "없음"} -> ${expectedExternalKey}`,
);
}
const expectedEmail = row.baronEmail?.trim().toLowerCase() ?? "";
const actualEmail = row.worksmobileEmail?.trim().toLowerCase() ?? "";
if (expectedEmail && actualEmail && expectedEmail !== actualEmail) {
details.push(`이메일: ${actualEmail} -> ${expectedEmail}`);
}
const expectedPhone = row.baronPhone?.trim() ?? "";
const actualPhone = row.worksmobilePhone?.trim() ?? "";
if (expectedPhone && actualPhone && expectedPhone !== actualPhone) {
details.push(`전화번호: ${actualPhone} -> ${expectedPhone}`);
}
const expectedEmployeeNumber = row.baronEmployeeNumber?.trim() ?? "";
const actualEmployeeNumber = row.worksmobileEmployeeNumber?.trim() ?? "";
if (
expectedEmployeeNumber &&
actualEmployeeNumber &&
expectedEmployeeNumber !== actualEmployeeNumber
) {
details.push(
`사번: ${actualEmployeeNumber} -> ${expectedEmployeeNumber}`,
);
}
return details;
}
const expectedParent =
row.baronParentWorksmobileName ??
row.baronParentName ??
row.baronParentWorksmobileId ??
row.baronParentId ??
"";
const actualParent =
row.worksmobileParentName ??
row.worksmobileParentExternalKey ??
row.worksmobileParentId ??
"";
const expectedParentKey =
row.baronParentWorksmobileId ?? row.baronParentId ?? "";
const actualParentKey =
row.worksmobileParentId ?? row.worksmobileParentExternalKey ?? "";
if (expectedParentKey !== actualParentKey) {
details.push(
`상위: ${actualParent || "없음"} -> ${expectedParent || "없음"}`,
);
}
return details;
}
export function buildWorksmobilePasswordManageUrl({
tenantId,
domainId,
userIdNo,
}: {
tenantId?: string;
domainId?: number;
userIdNo?: string;
}) {
const normalizedTenantId = tenantId?.trim();
const normalizedUserIdNo = userIdNo?.trim();
if (
!normalizedTenantId ||
!domainId ||
domainId <= 0 ||
!normalizedUserIdNo
) {
return "";
}
const url = new URL("https://auth.worksmobile.com/integrate/password/manage");
url.searchParams.set("usage", "admin");
url.searchParams.set("targetUserTenantId", normalizedTenantId);
url.searchParams.set("targetUserDomainId", String(domainId));
url.searchParams.set("targetUserIdNo", normalizedUserIdNo);
url.searchParams.set(
"accessUrl",
"https://admin.worksmobile.com/assets/self-close.html",
);
return url.toString();
}
export function canOpenWorksmobilePasswordManage(
row: WorksmobileComparisonItem,
tenantId?: string,
) {
return (
row.resourceType === "USER" &&
!isImmutableWorksmobileAccount(row) &&
Boolean(
buildWorksmobilePasswordManageUrl({
tenantId,
domainId: row.worksmobileDomainId,
userIdNo: row.worksmobileId,
}),
)
);
}
export const comparisonFilterOptions: Array<{
value: WorksmobileComparisonFilter;
label: string;
}> = [
{ value: "baron_only", label: "바론에만 있음" },
{ value: "needs_update", label: "업데이트 필요" },
{ value: "works_only", label: "웍스에만 있음" },
{ value: "matched", label: "양쪽 다 있음" },
];
export const userFilterOptions = comparisonFilterOptions;
export const worksmobileAccountStatusFilterOptions: Array<{
value: WorksmobileAccountStatusFilter;
label: string;
}> = [
{ value: "all", label: "WORKS 전체" },
{ value: "active", label: "active" },
{ value: "invited", label: "invited" },
{ value: "suspended", label: "suspended" },
{ value: "inactive", label: "inactive" },
{ value: "deleted", label: "deleted" },
];
export function getDefaultUserComparisonFilters(): WorksmobileComparisonFilter[] {
return ["baron_only", "needs_update", "works_only"];
}
export function getDefaultGroupComparisonFilters(): WorksmobileComparisonFilter[] {
return ["baron_only", "needs_update", "works_only"];
}
const worksmobileFilterStatuses: Record<WorksmobileComparisonFilter, string[]> =
{
baron_only: ["missing_in_worksmobile"],
needs_update: ["needs_update"],
works_only: ["missing_in_baron"],
matched: ["matched"],
};