import { computed, ref } from 'vue' import { defineStore } from 'pinia' import { changeMerchantMemberStatus, changeMerchantRoleStatus, createMerchantMember, createMerchantRole, deleteTenantQualification, deleteMerchantRole, getMerchantMembers, getMerchantPermissionResources, getMerchantRoles, getTenantProfile, getTenantQualifications, resetMerchantMemberPassword, replaceMerchantRolePermissions, updateMerchantMember, updateMerchantRole, updateTenantProfile, type MerchantRole, type MerchantMember, type MerchantPermissionResource, } from '@/modules/tenant-management/api/merchantIam' import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, REQUIRED_TENANT_PERMISSION_CODES, TENANT_PERMISSION_GROUPS, hasMappedMerchantPermission, type MerchantTenantStatus, } from '@/modules/tenant-management/contracts' import { normalizeQualificationStatus, type TenantQualificationStatus } from '@/modules/tenant-management/utils/qualificationStatus' export type TenantRecordStatus = 'ACTIVE' | 'INACTIVE' export interface TenantCompanyProfile { companyName: string creditCode: string legalRepresentative: string contactName: string contactPhone: string contactEmail: string contactWechat: string enterpriseType: string establishedDate: string registeredCapital: number | string | null registeredCapitalCurrency: string registeredAddress: string auditStatus: MerchantTenantStatus } export interface TenantQualification { id: string type: string fileName: string status: TenantQualificationStatus expiresAt: string updatedAt: string documentUrl: string } export interface TenantMember { id: string name: string account: string phone: string email: string roleIds: string[] status: TenantRecordStatus lastLoginAt: string isAdministrator: boolean } export interface TenantRole { id: string name: string code: string description: string status: TenantRecordStatus builtin: boolean memberCount: number permissionIds: string[] permissionCodes: string[] } const allPermissionCodes = TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)) const mappedMerchantPermissionCodes = new Set(Object.values(FRONTEND_TO_MERCHANT_PERMISSION_CODES).flat()) function formatLastLoginAt(value: string | null) { if (!value) return '从未登录' const date = new Date(value) if (Number.isNaN(date.getTime())) return '-' return date.toLocaleString('zh-CN', { hour12: false }) } function formatQualificationUpdatedAt(value: string | null | undefined) { if (!value) return '—' const date = new Date(value) if (Number.isNaN(date.getTime())) return value return date.toLocaleString('zh-CN', { hour12: false }) } const emptyCompany: TenantCompanyProfile = { companyName: '', creditCode: '', legalRepresentative: '', contactName: '', contactPhone: '', contactEmail: '', contactWechat: '', enterpriseType: '', establishedDate: '', registeredCapital: null, registeredCapitalCurrency: '', registeredAddress: '', auditStatus: 'PROFILE_REQUIRED', } export const useTenantAdministrationStore = defineStore('tenant-administration', () => { const company = ref({ ...emptyCompany }) const qualifications = ref([]) const members = ref([]) const roles = ref([]) const permissionResources = ref([]) const activeMemberCount = computed(() => members.value.filter((member) => member.status === 'ACTIVE').length) async function loadCompanyContext() { const [profile, qualificationRecords] = await Promise.all([ getTenantProfile(), getTenantQualifications(), ]) company.value = { companyName: profile.tenantName, creditCode: profile.unifiedSocialCreditCode ?? '', legalRepresentative: profile.legalRepresentative, contactName: profile.contactName, contactPhone: profile.contactPhone, contactEmail: profile.contactEmail ?? '', contactWechat: profile.contactWechat ?? '', enterpriseType: profile.enterpriseType ?? '', establishedDate: profile.establishedDate ?? '', registeredCapital: profile.registeredCapital, registeredCapitalCurrency: profile.registeredCapitalCurrency ?? '', registeredAddress: profile.registeredAddress ?? '', auditStatus: profile.status, } qualifications.value = qualificationRecords.map((item) => { const documentUrl = typeof item.documentUrl === 'string' ? item.documentUrl : '' return { id: String(item.id), type: ({ BUSINESS_LICENSE: '营业执照', LEGAL_PERSON_ID: '法人身份证', FREIGHT_FORWARDER_CERTIFICATE: '货代资质证书', BANK_ACCOUNT_LICENSE: '银行开户许可证', } as const)[item.qualificationType] || '企业资质', fileName: item.originalFileName?.trim() || '—', status: normalizeQualificationStatus(item.status), expiresAt: item.validTo || '长期有效', updatedAt: formatQualificationUpdatedAt(item.updatedAt), documentUrl, } }) } function mapMerchantRole(role: MerchantRole): TenantRole { const grantedIds = (role.permissionIds ?? []).map(String) const grantedBackendCodes = new Set(permissionResources.value .filter((resource) => grantedIds.includes(String(resource.id))) .map((resource) => resource.permissionCode)) const permissionCodes = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES) .filter(([frontendCode]) => hasMappedMerchantPermission(frontendCode, grantedBackendCodes)) .map(([frontendCode]) => frontendCode) const effectivePermissionCodes = [...new Set([...REQUIRED_TENANT_PERMISSION_CODES, ...permissionCodes])] return { id: String(role.id), name: role.roleName, code: role.roleCode, description: role.description ?? '暂无角色说明', status: role.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE', builtin: role.roleType === 'SYSTEM', memberCount: role.memberCount, permissionIds: grantedIds, permissionCodes: role.roleType === 'SYSTEM' && role.roleCode === 'MERCHANT_OWNER' ? [...allPermissionCodes] : effectivePermissionCodes, } } function mapMerchantMember(member: MerchantMember): TenantMember { return { id: String(member.membershipId), name: member.displayName || `企业成员 ${member.membershipId}`, account: member.username, phone: member.phone ?? '', email: member.email ?? '', roleIds: member.roleIds.map(String), status: member.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE', lastLoginAt: formatLastLoginAt(member.lastLoginAt), isAdministrator: member.memberType === 'OWNER', } } async function loadRoles() { roles.value = [] permissionResources.value = [] const [roleRecords, resources] = await Promise.all([getMerchantRoles(), getMerchantPermissionResources()]) permissionResources.value = resources.filter((item) => item.status === 'ACTIVE') roles.value = roleRecords.map(mapMerchantRole) } async function deleteQualification(id: string) { await deleteTenantQualification(id) qualifications.value = qualifications.value.filter((item) => item.id !== id) } async function loadMembers() { members.value = [] members.value = (await getMerchantMembers()).map(mapMerchantMember) } async function updateCompany(payload: TenantCompanyProfile) { const updated = await updateTenantProfile({ tenantName: payload.companyName.trim(), legalName: payload.companyName.trim(), legalRepresentative: payload.legalRepresentative.trim(), contactName: payload.contactName.trim(), contactPhone: payload.contactPhone.trim(), contactEmail: payload.contactEmail.trim(), }) company.value = { companyName: updated.tenantName, creditCode: updated.unifiedSocialCreditCode ?? '', legalRepresentative: updated.legalRepresentative, contactName: updated.contactName, contactPhone: updated.contactPhone, contactEmail: updated.contactEmail ?? '', contactWechat: updated.contactWechat ?? '', enterpriseType: updated.enterpriseType ?? '', establishedDate: updated.establishedDate ?? '', registeredCapital: updated.registeredCapital, registeredCapitalCurrency: updated.registeredCapitalCurrency ?? '', registeredAddress: updated.registeredAddress ?? '', auditStatus: updated.status, } } async function createMember(payload: { name: string; account: string; password: string; phone: string; email: string; roleIds: string[]; memberNo: string }) { const created = await createMerchantMember({ username: payload.account.trim(), password: payload.password, displayName: payload.name.trim(), phone: payload.phone.trim() || null, email: payload.email.trim() || null, memberType: 'EMPLOYEE', memberNo: payload.memberNo.trim(), roleIds: payload.roleIds, }) members.value.unshift(mapMerchantMember(created)) } async function updateMember(id: string, payload: Pick & { memberNo: string }) { const member = members.value.find((item) => item.id === id) if (!member) throw new Error('成员不存在') const updated = await updateMerchantMember(id, { displayName: payload.name.trim(), phone: payload.phone.trim() || null, email: payload.email.trim() || null, memberType: member.isAdministrator ? 'OWNER' : 'EMPLOYEE', memberNo: payload.memberNo.trim(), roleIds: payload.roleIds, }) Object.assign(member, mapMerchantMember(updated)) } async function toggleMemberStatus(id: string) { const member = members.value.find((item) => item.id === id) if (!member) throw new Error('成员不存在') if (member.isAdministrator) throw new Error('企业管理员不能停用') const updated = await changeMerchantMemberStatus(id, member.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE') Object.assign(member, mapMerchantMember(updated)) } async function resetMemberPassword(id: string, newPassword: string) { await resetMerchantMemberPassword(id, newPassword) } async function createRole(payload: Pick) { if (roles.value.some((role) => role.code === payload.code)) throw new Error('角色编码已存在') const created = await createMerchantRole({ roleCode: payload.code, roleName: payload.name, description: payload.description }) roles.value.push(mapMerchantRole(created)) } async function updateRole(id: string, payload: Pick) { const role = roles.value.find((item) => item.id === id) if (!role) throw new Error('角色不存在') if (role.builtin) throw new Error('内置角色不能编辑') const updated = await updateMerchantRole(id, { roleName: payload.name.trim(), description: payload.description.trim() }) Object.assign(role, mapMerchantRole(updated)) } async function toggleRoleStatus(id: string) { const role = roles.value.find((item) => item.id === id) if (!role) throw new Error('角色不存在') if (role.builtin) throw new Error('内置角色不能停用') const updated = await changeMerchantRoleStatus(id, role.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE') Object.assign(role, mapMerchantRole(updated)) } async function deleteRole(id: string) { const role = roles.value.find((item) => item.id === id) if (!role) throw new Error('角色不存在') if (role.builtin) throw new Error('内置角色不能删除') if (members.value.some((member) => member.roleIds.includes(id))) throw new Error('该角色仍有成员使用,不能删除') await deleteMerchantRole(id) roles.value = roles.value.filter((item) => item.id !== id) } async function setRolePermissions(id: string, permissionCodes: string[]) { const role = roles.value.find((item) => item.id === id) if (!role) throw new Error('角色不存在') if (role.builtin) throw new Error('内置管理员权限不能修改') const effectivePermissionCodes = [...new Set([...REQUIRED_TENANT_PERMISSION_CODES, ...permissionCodes])] const backendCodes = new Set(effectivePermissionCodes.flatMap((code) => FRONTEND_TO_MERCHANT_PERMISSION_CODES[code] ?? [])) const selectedPermissionIds = permissionResources.value .filter((resource) => backendCodes.has(resource.permissionCode)) .map((resource) => resource.id) const mappedResourceIds = new Set(permissionResources.value .filter((resource) => mappedMerchantPermissionCodes.has(resource.permissionCode)) .map((resource) => String(resource.id))) // The endpoint replaces every ID, including MENU nodes this screen does not render. const preservedPermissionIds = role.permissionIds.filter((permissionId) => !mappedResourceIds.has(permissionId)) const nextPermissionIds = [...new Set([...preservedPermissionIds, ...selectedPermissionIds])] await replaceMerchantRolePermissions(id, nextPermissionIds) role.permissionIds = nextPermissionIds.map(String) role.permissionCodes = effectivePermissionCodes } function roleMemberCount(roleId: string) { return roles.value.find((role) => role.id === roleId)?.memberCount ?? 0 } function roleNames(roleIds: readonly string[]) { return roleIds.map((id) => roles.value.find((role) => role.id === id)?.name).filter((name): name is string => Boolean(name)) } return { company, qualifications, members, roles, activeMemberCount, loadCompanyContext, deleteQualification, loadRoles, loadMembers, updateCompany, createMember, updateMember, toggleMemberStatus, resetMemberPassword, createRole, updateRole, toggleRoleStatus, deleteRole, setRolePermissions, roleMemberCount, roleNames, } })