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
+143 -21
View File
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useEscClose } from '@/lib/modal-stack';
import { matchesSearch } from '@/lib/search';
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
@@ -504,20 +505,54 @@ async function toggleStatus(user: OrgUserDto) {
}
}
const logoutModal = ref<{ user: OrgUserDto; time: string } | null>(null);
const logoutSaving = ref(false);
function openLogoutModal(user: OrgUserDto) {
if (!canSetLogoutTime.value || user.role === 'owner') return;
logoutModal.value = { user, time: user.logoutTime || '18:00' };
}
async function changeLogoutTime(user: OrgUserDto, raw: string) {
if (!canSetLogoutTime.value || user.role === 'owner') return;
const next = raw.trim();
if (next === (user.logoutTime ?? '')) return;
if (next === (user.logoutTime ?? '')) {
logoutModal.value = null;
return;
}
error.value = '';
ok.value = '';
logoutSaving.value = true;
try {
await api.patch(`/users/${user.id}/logout-time`, { logoutTime: next || null });
ok.value = next
? `ساعت خروج «${user.fullName}» روی ${toFaDigits(next)} تنظیم شد.`
: `محدودیت خروج «${user.fullName}» برداشته شد.`;
logoutModal.value = null;
await load();
} catch (e) {
error.value = apiError(e, 'ذخیره ساعت خروج ناموفق بود');
} finally {
logoutSaving.value = false;
}
}
async function toggleBlockAfterLogout(user: OrgUserDto) {
if (!canSetLogoutTime.value || user.role === 'owner' || !user.logoutTime) return;
const next = !user.blockLoginAfterLogout;
error.value = '';
ok.value = '';
try {
await api.patch(`/users/${user.id}/logout-time`, {
logoutTime: user.logoutTime,
blockLoginAfterLogout: next,
});
ok.value = next
? `«${user.fullName}» بعد از ساعت ${toFaDigits(user.logoutTime)} دیگر نمی‌تواند وارد شود.`
: `«${user.fullName}» بعد از ساعت خروج هم می‌تواند وارد شود.`;
await load();
} catch (e) {
error.value = apiError(e, 'تغییر محدودیت ورود ناموفق بود');
}
}
@@ -1051,15 +1086,34 @@ onUnmounted(() => {
<td class="muted">{{ formatLastLogin(user.lastLoginAt) }}</td>
<td v-if="canSetLogoutTime" @click.stop>
<span v-if="user.role === 'owner'" class="muted">—</span>
<input
v-else
class="logout-time"
type="time"
dir="ltr"
:value="user.logoutTime || ''"
:aria-label="`ساعت خروج ${user.fullName}`"
@change="changeLogoutTime(user, ($event.target as HTMLInputElement).value)"
/>
<span v-else class="logout-cell">
<button
type="button"
class="logout-btn"
:class="{ set: Boolean(user.logoutTime) }"
:aria-label="user.logoutTime ? `ویرایش ساعت خروج ${user.fullName}` : `تعیین ساعت خروج ${user.fullName}`"
:title="user.logoutTime ? 'ویرایش ساعت خروج' : 'تعیین ساعت خروج'"
@click="openLogoutModal(user)"
>
<span v-if="user.logoutTime" dir="ltr">{{ toFaDigits(user.logoutTime) }}</span>
<UiIcon v-else name="clock" :size="16" />
</button>
<button
v-if="user.logoutTime"
type="button"
class="block-btn"
:class="{ on: user.blockLoginAfterLogout }"
:aria-pressed="Boolean(user.blockLoginAfterLogout)"
:title="
user.blockLoginAfterLogout
? 'ورود بعد از ساعت خروج مسدود است — برای آزاد کردن بزنید'
: 'ورود بعد از ساعت خروج آزاد است — برای مسدود کردن بزنید'
"
@click="toggleBlockAfterLogout(user)"
>
<UiIcon :name="user.blockLoginAfterLogout ? 'check' : 'x'" :size="14" />
</button>
</span>
</td>
<td class="ops" @click.stop>
<UiDropdownMenu
@@ -1205,6 +1259,43 @@ onUnmounted(() => {
</template>
</UiModal>
<UiModal
:open="Boolean(logoutModal)"
:title="logoutModal ? `ساعت خروج ${logoutModal.user.fullName}` : 'ساعت خروج'"
@close="logoutModal = null"
>
<form v-if="logoutModal" class="form" @submit.prevent="changeLogoutTime(logoutModal.user, logoutModal.time)">
<label>
ساعت خروج روزانه
<JalaliDateInput v-model="logoutModal.time" mode="time" :clearable="false" />
</label>
<p class="hint">
در این ساعت (به وقت تهران) کاربر خودکار از سامانه خارج می‌شود. با تیک کنار ساعت می‌توانید
ورود دوباره تا پایان همان روز را هم ببندید.
</p>
</form>
<template #footer>
<button
v-if="logoutModal?.user.logoutTime"
type="button"
class="btn ghost"
:disabled="logoutSaving"
@click="logoutModal && changeLogoutTime(logoutModal.user, '')"
>
حذف ساعت خروج
</button>
<button type="button" class="btn ghost" @click="logoutModal = null">انصراف</button>
<button
type="button"
class="btn primary"
:disabled="logoutSaving || !logoutModal?.time"
@click="logoutModal && changeLogoutTime(logoutModal.user, logoutModal.time)"
>
{{ logoutSaving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
</template>
</UiModal>
<UiModal :open="editOpen" title="ویرایش مشخصات کاربر" @close="editOpen = false">
<form class="form" @submit.prevent="submitEditUser">
<label>نام و نام خانوادگی<input v-model="editName" required minlength="2" /></label>
@@ -1875,22 +1966,53 @@ tbody tr + tr td {
color: #94a3b8;
}
.logout-time {
width: 7.2rem;
min-height: 2rem;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 0 0.45rem;
font: inherit;
background: #fff;
box-sizing: border-box;
.logout-cell {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.logout-time:disabled {
background: #f8fafc;
.logout-btn,
.block-btn {
display: inline-grid;
place-items: center;
height: 1.9rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
color: #64748b;
font: inherit;
cursor: pointer;
}
.logout-btn {
min-width: 1.9rem;
padding: 0 0.4rem;
}
.logout-btn.set {
color: #0f172a;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.logout-btn:hover,
.block-btn:hover {
border-color: #0d9488;
color: #0f766e;
}
.block-btn {
width: 1.9rem;
color: #94a3b8;
}
.block-btn.on {
border-color: #99f6e4;
background: #f0fdfa;
color: #0f766e;
}
.roles-grid,
.roles-tree {
display: grid;