feat: compact logout-time cell and optional login block after logout time

Owners set the daily logout time in a modal and can block re-login until the next Tehran day.
This commit is contained in:
2026-10-21 14:20:15 +03:30
parent 7ae6a5837a
commit ce27df20ca
9 changed files with 195 additions and 25 deletions
+24 -3
View File
@@ -1,4 +1,4 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ForbiddenException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type { Actor } from '../common/record-scope';
import { PrismaService } from '../prisma/prisma.service';
@@ -13,6 +13,9 @@ import {
export const WORKDAY_ENDED_MESSAGE =
'ساعت کاری امروز تمام شده است. برای ادامه دوباره وارد شوید.';
export const LOGIN_BLOCKED_AFTER_HOURS_MESSAGE =
'ساعت کاری امروز شما تمام شده و ورود دوباره تا فردا مجاز نیست.';
type RecordInput = {
organizationId: string;
userId: string;
@@ -61,6 +64,18 @@ export class AttendanceService {
});
}
/** با «مسدودسازی بعد از ساعت خروج»، از آن ساعت تا پایان روز تهران ورود رد می‌شود. */
async assertLoginAllowed(organizationId: string, userId: string, at = new Date()) {
const membership = await this.prisma.membership.findFirst({
where: { organizationId, userId, isActive: true },
select: { role: true, logoutTime: true, blockLoginAfterLogout: true },
});
if (!membership || membership.role === 'owner' || !membership.blockLoginAfterLogout) return;
if (loggedInAfterCutoff(at, membership.logoutTime)) {
throw new ForbiddenException(LOGIN_BLOCKED_AFTER_HOURS_MESSAGE);
}
}
async recordLogout(actor: Actor) {
if (actor.viewAsReadOnly) return { ok: true };
const at = new Date();
@@ -81,14 +96,20 @@ export class AttendanceService {
if (actor.viewAsReadOnly || actor.role === 'owner' || !actor.sessionStartedAt) return;
const membership = await this.prisma.membership.findFirst({
where: { userId: actor.userId, organizationId: actor.organizationId, isActive: true },
select: { logoutTime: true, role: true },
select: { logoutTime: true, role: true, blockLoginAfterLogout: true },
});
const logoutTime = membership?.logoutTime;
if (!logoutTime || membership?.role === 'owner') return;
const now = new Date();
const cutoff = tehranCutoff(now, logoutTime);
if (!cutoff || now.getTime() < cutoff.getTime()) return;
if (actor.sessionStartedAt.getTime() >= cutoff.getTime()) return;
// نشستی که بعد از ساعت خروج شروع شده فقط وقتی مجاز است که ورود بعد از ساعت مسدود نباشد
if (
actor.sessionStartedAt.getTime() >= cutoff.getTime() &&
!membership?.blockLoginAfterLogout
) {
return;
}
await this.write({
organizationId: actor.organizationId,