2026-03-15 14:47:58 -05:00
|
|
|
import type {
|
|
|
|
|
AdminDiagnosticsDto,
|
|
|
|
|
AdminPermissionOptionDto,
|
|
|
|
|
AdminRoleDto,
|
|
|
|
|
AdminRoleInput,
|
|
|
|
|
AdminUserDto,
|
|
|
|
|
AdminUserInput,
|
|
|
|
|
AuditEventDto,
|
|
|
|
|
} from "@mrp/shared";
|
2026-03-15 14:11:21 -05:00
|
|
|
import fs from "node:fs/promises";
|
|
|
|
|
|
|
|
|
|
import { env } from "../../config/env.js";
|
|
|
|
|
import { paths } from "../../config/paths.js";
|
2026-03-15 14:47:58 -05:00
|
|
|
import { logAuditEvent } from "../../lib/audit.js";
|
|
|
|
|
import { hashPassword } from "../../lib/password.js";
|
2026-03-15 14:11:21 -05:00
|
|
|
import { prisma } from "../../lib/prisma.js";
|
|
|
|
|
|
|
|
|
|
function mapAuditEvent(record: {
|
|
|
|
|
id: string;
|
|
|
|
|
actorId: string | null;
|
|
|
|
|
entityType: string;
|
|
|
|
|
entityId: string | null;
|
|
|
|
|
action: string;
|
|
|
|
|
summary: string;
|
|
|
|
|
metadataJson: string;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
actor: {
|
|
|
|
|
firstName: string;
|
|
|
|
|
lastName: string;
|
|
|
|
|
} | null;
|
|
|
|
|
}): AuditEventDto {
|
|
|
|
|
return {
|
|
|
|
|
id: record.id,
|
|
|
|
|
actorId: record.actorId,
|
|
|
|
|
actorName: record.actor ? `${record.actor.firstName} ${record.actor.lastName}`.trim() : null,
|
|
|
|
|
entityType: record.entityType,
|
|
|
|
|
entityId: record.entityId,
|
|
|
|
|
action: record.action,
|
|
|
|
|
summary: record.summary,
|
|
|
|
|
metadataJson: record.metadataJson,
|
|
|
|
|
createdAt: record.createdAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:47:58 -05:00
|
|
|
function mapRole(record: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
updatedAt: Date;
|
|
|
|
|
rolePermissions: Array<{
|
|
|
|
|
permission: {
|
|
|
|
|
key: string;
|
|
|
|
|
};
|
|
|
|
|
}>;
|
|
|
|
|
_count: {
|
|
|
|
|
userRoles: number;
|
|
|
|
|
};
|
|
|
|
|
}): AdminRoleDto {
|
|
|
|
|
return {
|
|
|
|
|
id: record.id,
|
|
|
|
|
name: record.name,
|
|
|
|
|
description: record.description,
|
|
|
|
|
permissionKeys: record.rolePermissions.map((rolePermission) => rolePermission.permission.key).sort(),
|
|
|
|
|
userCount: record._count.userRoles,
|
|
|
|
|
createdAt: record.createdAt.toISOString(),
|
|
|
|
|
updatedAt: record.updatedAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mapUser(record: {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
firstName: string;
|
|
|
|
|
lastName: string;
|
|
|
|
|
isActive: boolean;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
updatedAt: Date;
|
|
|
|
|
userRoles: Array<{
|
|
|
|
|
role: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
rolePermissions: Array<{
|
|
|
|
|
permission: {
|
|
|
|
|
key: string;
|
|
|
|
|
};
|
|
|
|
|
}>;
|
|
|
|
|
};
|
|
|
|
|
}>;
|
|
|
|
|
}): AdminUserDto {
|
|
|
|
|
const permissionKeys = new Set<string>();
|
|
|
|
|
for (const userRole of record.userRoles) {
|
|
|
|
|
for (const rolePermission of userRole.role.rolePermissions) {
|
|
|
|
|
permissionKeys.add(rolePermission.permission.key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: record.id,
|
|
|
|
|
email: record.email,
|
|
|
|
|
firstName: record.firstName,
|
|
|
|
|
lastName: record.lastName,
|
|
|
|
|
isActive: record.isActive,
|
|
|
|
|
roleIds: record.userRoles.map((userRole) => userRole.role.id),
|
|
|
|
|
roleNames: record.userRoles.map((userRole) => userRole.role.name),
|
|
|
|
|
permissionKeys: [...permissionKeys].sort(),
|
|
|
|
|
createdAt: record.createdAt.toISOString(),
|
|
|
|
|
updatedAt: record.updatedAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validatePermissionKeys(permissionKeys: string[]) {
|
|
|
|
|
const uniquePermissionKeys = [...new Set(permissionKeys)];
|
|
|
|
|
const permissions = await prisma.permission.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
key: {
|
|
|
|
|
in: uniquePermissionKeys,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (permissions.length !== uniquePermissionKeys.length) {
|
|
|
|
|
return { ok: false as const, reason: "One or more selected permissions are invalid." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, permissions };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateRoleIds(roleIds: string[]) {
|
|
|
|
|
const uniqueRoleIds = [...new Set(roleIds)];
|
|
|
|
|
const roles = await prisma.role.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: {
|
|
|
|
|
in: uniqueRoleIds,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (roles.length !== uniqueRoleIds.length) {
|
|
|
|
|
return { ok: false as const, reason: "One or more selected roles are invalid." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, roles };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listAdminPermissions(): Promise<AdminPermissionOptionDto[]> {
|
|
|
|
|
const permissions = await prisma.permission.findMany({
|
|
|
|
|
orderBy: [{ key: "asc" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return permissions.map((permission) => ({
|
|
|
|
|
key: permission.key,
|
|
|
|
|
description: permission.description,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listAdminRoles(): Promise<AdminRoleDto[]> {
|
|
|
|
|
const roles = await prisma.role.findMany({
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
userRoles: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: [{ name: "asc" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return roles.map(mapRole);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createAdminRole(payload: AdminRoleInput, actorId?: string | null) {
|
|
|
|
|
const validatedPermissions = await validatePermissionKeys(payload.permissionKeys);
|
|
|
|
|
if (!validatedPermissions.ok) {
|
|
|
|
|
return { ok: false as const, reason: validatedPermissions.reason };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const role = await prisma.role.create({
|
|
|
|
|
data: {
|
|
|
|
|
name: payload.name.trim(),
|
|
|
|
|
description: payload.description,
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
create: validatedPermissions.permissions.map((permission) => ({
|
|
|
|
|
permissionId: permission.id,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
userRoles: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logAuditEvent({
|
|
|
|
|
actorId,
|
|
|
|
|
entityType: "role",
|
|
|
|
|
entityId: role.id,
|
|
|
|
|
action: "created",
|
|
|
|
|
summary: `Created role ${role.name}.`,
|
|
|
|
|
metadata: {
|
|
|
|
|
name: role.name,
|
|
|
|
|
permissionKeys: role.rolePermissions.map((rolePermission) => rolePermission.permission.key),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, role: mapRole(role) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateAdminRole(roleId: string, payload: AdminRoleInput, actorId?: string | null) {
|
|
|
|
|
const existingRole = await prisma.role.findUnique({
|
|
|
|
|
where: { id: roleId },
|
|
|
|
|
select: { id: true, name: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existingRole) {
|
|
|
|
|
return { ok: false as const, reason: "Role was not found." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validatedPermissions = await validatePermissionKeys(payload.permissionKeys);
|
|
|
|
|
if (!validatedPermissions.ok) {
|
|
|
|
|
return { ok: false as const, reason: validatedPermissions.reason };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const role = await prisma.role.update({
|
|
|
|
|
where: { id: roleId },
|
|
|
|
|
data: {
|
|
|
|
|
name: payload.name.trim(),
|
|
|
|
|
description: payload.description,
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
deleteMany: {},
|
|
|
|
|
create: validatedPermissions.permissions.map((permission) => ({
|
|
|
|
|
permissionId: permission.id,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
userRoles: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logAuditEvent({
|
|
|
|
|
actorId,
|
|
|
|
|
entityType: "role",
|
|
|
|
|
entityId: role.id,
|
|
|
|
|
action: "updated",
|
|
|
|
|
summary: `Updated role ${role.name}.`,
|
|
|
|
|
metadata: {
|
|
|
|
|
previousName: existingRole.name,
|
|
|
|
|
name: role.name,
|
|
|
|
|
permissionKeys: role.rolePermissions.map((rolePermission) => rolePermission.permission.key),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, role: mapRole(role) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listAdminUsers(): Promise<AdminUserDto[]> {
|
|
|
|
|
const users = await prisma.user.findMany({
|
|
|
|
|
include: {
|
|
|
|
|
userRoles: {
|
|
|
|
|
include: {
|
|
|
|
|
role: {
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: [{ firstName: "asc" }, { lastName: "asc" }, { email: "asc" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return users.map(mapUser);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createAdminUser(payload: AdminUserInput, actorId?: string | null) {
|
|
|
|
|
if (!payload.password || payload.password.trim().length < 8) {
|
|
|
|
|
return { ok: false as const, reason: "A password with at least 8 characters is required for new users." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validatedRoles = await validateRoleIds(payload.roleIds);
|
|
|
|
|
if (!validatedRoles.ok) {
|
|
|
|
|
return { ok: false as const, reason: validatedRoles.reason };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const user = await prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
email: payload.email.trim().toLowerCase(),
|
|
|
|
|
firstName: payload.firstName.trim(),
|
|
|
|
|
lastName: payload.lastName.trim(),
|
|
|
|
|
isActive: payload.isActive,
|
|
|
|
|
passwordHash: await hashPassword(payload.password.trim()),
|
|
|
|
|
userRoles: {
|
|
|
|
|
create: validatedRoles.roles.map((role) => ({
|
|
|
|
|
roleId: role.id,
|
|
|
|
|
assignedBy: actorId ?? null,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
userRoles: {
|
|
|
|
|
include: {
|
|
|
|
|
role: {
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logAuditEvent({
|
|
|
|
|
actorId,
|
|
|
|
|
entityType: "user",
|
|
|
|
|
entityId: user.id,
|
|
|
|
|
action: "created",
|
|
|
|
|
summary: `Created user account for ${user.email}.`,
|
|
|
|
|
metadata: {
|
|
|
|
|
email: user.email,
|
|
|
|
|
isActive: user.isActive,
|
|
|
|
|
roleNames: user.userRoles.map((userRole) => userRole.role.name),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, user: mapUser(user) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateAdminUser(userId: string, payload: AdminUserInput, actorId?: string | null) {
|
|
|
|
|
const existingUser = await prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existingUser) {
|
|
|
|
|
return { ok: false as const, reason: "User was not found." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validatedRoles = await validateRoleIds(payload.roleIds);
|
|
|
|
|
if (!validatedRoles.ok) {
|
|
|
|
|
return { ok: false as const, reason: validatedRoles.reason };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
email: payload.email.trim().toLowerCase(),
|
|
|
|
|
firstName: payload.firstName.trim(),
|
|
|
|
|
lastName: payload.lastName.trim(),
|
|
|
|
|
isActive: payload.isActive,
|
|
|
|
|
...(payload.password?.trim()
|
|
|
|
|
? {
|
|
|
|
|
passwordHash: await hashPassword(payload.password.trim()),
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
userRoles: {
|
|
|
|
|
deleteMany: {},
|
|
|
|
|
create: validatedRoles.roles.map((role) => ({
|
|
|
|
|
roleId: role.id,
|
|
|
|
|
assignedBy: actorId ?? null,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const user = await prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data,
|
|
|
|
|
include: {
|
|
|
|
|
userRoles: {
|
|
|
|
|
include: {
|
|
|
|
|
role: {
|
|
|
|
|
include: {
|
|
|
|
|
rolePermissions: {
|
|
|
|
|
include: {
|
|
|
|
|
permission: {
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logAuditEvent({
|
|
|
|
|
actorId,
|
|
|
|
|
entityType: "user",
|
|
|
|
|
entityId: user.id,
|
|
|
|
|
action: "updated",
|
|
|
|
|
summary: `Updated user account for ${user.email}.`,
|
|
|
|
|
metadata: {
|
|
|
|
|
previousEmail: existingUser.email,
|
|
|
|
|
email: user.email,
|
|
|
|
|
isActive: user.isActive,
|
|
|
|
|
roleNames: user.userRoles.map((userRole) => userRole.role.name),
|
|
|
|
|
passwordReset: Boolean(payload.password?.trim()),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { ok: true as const, user: mapUser(user) };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:11:21 -05:00
|
|
|
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
|
|
|
|
const [
|
|
|
|
|
companyProfile,
|
|
|
|
|
userCount,
|
|
|
|
|
activeUserCount,
|
|
|
|
|
roleCount,
|
|
|
|
|
permissionCount,
|
|
|
|
|
customerCount,
|
|
|
|
|
vendorCount,
|
|
|
|
|
inventoryItemCount,
|
|
|
|
|
warehouseCount,
|
|
|
|
|
workOrderCount,
|
|
|
|
|
projectCount,
|
|
|
|
|
purchaseOrderCount,
|
|
|
|
|
salesQuoteCount,
|
|
|
|
|
salesOrderCount,
|
|
|
|
|
shipmentCount,
|
|
|
|
|
attachmentCount,
|
|
|
|
|
auditEventCount,
|
|
|
|
|
recentAuditEvents,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
prisma.companyProfile.findFirst({ where: { isActive: true }, select: { id: true } }),
|
|
|
|
|
prisma.user.count(),
|
|
|
|
|
prisma.user.count({ where: { isActive: true } }),
|
|
|
|
|
prisma.role.count(),
|
|
|
|
|
prisma.permission.count(),
|
|
|
|
|
prisma.customer.count(),
|
|
|
|
|
prisma.vendor.count(),
|
|
|
|
|
prisma.inventoryItem.count(),
|
|
|
|
|
prisma.warehouse.count(),
|
|
|
|
|
prisma.workOrder.count(),
|
|
|
|
|
prisma.project.count(),
|
|
|
|
|
prisma.purchaseOrder.count(),
|
|
|
|
|
prisma.salesQuote.count(),
|
|
|
|
|
prisma.salesOrder.count(),
|
|
|
|
|
prisma.shipment.count(),
|
|
|
|
|
prisma.fileAttachment.count(),
|
|
|
|
|
prisma.auditEvent.count(),
|
|
|
|
|
prisma.auditEvent.findMany({
|
|
|
|
|
include: {
|
|
|
|
|
actor: {
|
|
|
|
|
select: {
|
|
|
|
|
firstName: true,
|
|
|
|
|
lastName: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: [{ createdAt: "desc" }],
|
|
|
|
|
take: 25,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await Promise.all([fs.access(paths.dataDir), fs.access(paths.uploadsDir)]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
serverTime: new Date().toISOString(),
|
|
|
|
|
nodeVersion: process.version,
|
|
|
|
|
databaseUrl: env.DATABASE_URL,
|
|
|
|
|
dataDir: paths.dataDir,
|
|
|
|
|
uploadsDir: paths.uploadsDir,
|
|
|
|
|
clientOrigin: env.CLIENT_ORIGIN,
|
|
|
|
|
companyProfilePresent: Boolean(companyProfile),
|
|
|
|
|
userCount,
|
|
|
|
|
activeUserCount,
|
|
|
|
|
roleCount,
|
|
|
|
|
permissionCount,
|
|
|
|
|
customerCount,
|
|
|
|
|
vendorCount,
|
|
|
|
|
inventoryItemCount,
|
|
|
|
|
warehouseCount,
|
|
|
|
|
workOrderCount,
|
|
|
|
|
projectCount,
|
|
|
|
|
purchaseOrderCount,
|
|
|
|
|
salesDocumentCount: salesQuoteCount + salesOrderCount,
|
|
|
|
|
shipmentCount,
|
|
|
|
|
attachmentCount,
|
|
|
|
|
auditEventCount,
|
|
|
|
|
recentAuditEvents: recentAuditEvents.map(mapAuditEvent),
|
|
|
|
|
};
|
|
|
|
|
}
|