feat: log staff sign-in and sign-out and force logout at a set time

This commit is contained in:
2026-10-19 13:34:55 +03:30
parent 3863c5e7d1
commit 8590579605
25 changed files with 588 additions and 15 deletions
+8
View File
@@ -89,6 +89,14 @@ export class UpdateUserCommissionDto {
commissionPercent!: number;
}
export class UpdateLogoutTimeDto {
@IsOptional()
@ValidateIf((_, value) => value != null && value !== '')
@IsString()
@Matches(/^([01]\d|2[0-3]):[0-5]\d$/, { message: 'ساعت خروج معتبر نیست' })
logoutTime?: string | null;
}
export class UpdateUserVisibilityDto {
@IsOptional()
@IsIn(['own', 'team', 'org'])
+16
View File
@@ -17,6 +17,7 @@ import {
InviteUserDto,
UpdateOrgUserDto,
UpdateUserCommissionDto,
UpdateLogoutTimeDto,
UpdateUserRoleDto,
UpdateUserStatusDto,
UpdateUserVisibilityDto,
@@ -66,6 +67,21 @@ export class UsersController {
);
}
@Patch(':id/logout-time')
@Roles('owner', 'admin')
updateLogoutTime(
@CurrentUser() user: { organizationId: string; role: UserRole },
@Param('id') id: string,
@Body() dto: UpdateLogoutTimeDto,
) {
return this.usersService.updateLogoutTime(
user.organizationId,
user.role,
id,
dto.logoutTime ?? null,
);
}
@Patch(':id/commission')
@Roles('owner', 'admin')
updateCommission(
+27
View File
@@ -45,6 +45,7 @@ type MembershipRow = {
dataVisibility?: string | null;
dataVisibilityScopes?: unknown;
priorArchiveEnabled?: boolean;
logoutTime?: string | null;
createdAt: Date;
orgRole: { id: string; name: string; color: string } | null;
user: {
@@ -242,6 +243,31 @@ export class UsersService {
return this.dtoOf(organizationId, targetId);
}
async updateLogoutTime(
organizationId: string,
actorRole: UserRole,
targetId: string,
logoutTime: string | null,
): Promise<OrgUserDto> {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('فقط مالک یا مدیر می‌تواند ساعت خروج را تعیین کند');
}
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
if (!target) throw new NotFoundException('کاربر پیدا نشد');
if (target.role === 'owner') {
throw new BadRequestException('برای مالک ساعت خروج تعیین نمی‌شود');
}
const value = logoutTime?.trim() || null;
if (value && !/^([01]\d|2[0-3]):[0-5]\d$/.test(value)) {
throw new BadRequestException('ساعت خروج معتبر نیست');
}
await this.prisma.membership.update({
where: { id: target.id },
data: { logoutTime: value },
});
return this.dtoOf(organizationId, targetId);
}
async updatePriorArchive(
organizationId: string,
actorRole: UserRole,
@@ -506,6 +532,7 @@ export class UsersService {
? {}
: parseVisibilityScopes(row.dataVisibilityScopes),
priorArchiveEnabled: Boolean(row.priorArchiveEnabled),
logoutTime: row.role === 'owner' ? null : row.logoutTime ?? null,
createdAt: row.createdAt.toISOString(),
};
}