Add attendance hub, categorized sidebar, and UI preferences page.

Improve dashboard and navigation access to attendance, group the right-side menu by category, and let each account save default theme and font size.
This commit is contained in:
2026-08-16 07:28:58 +03:30
parent 9c4182ea5a
commit ba509734ae
12 changed files with 731 additions and 39 deletions
+4
View File
@@ -4,6 +4,7 @@ import { ref, computed } from 'vue';
import { authApi } from '@/api/authApi';
import Cookies from 'js-cookie';
import { usePermissionStore } from './permissionStore';
import { useThemeStore } from './themeStore';
export const useAuthStore = defineStore('auth', () => {
const accessToken = ref(localStorage.getItem('accessToken') || Cookies.get('accessToken') || '');
@@ -53,6 +54,9 @@ export const useAuthStore = defineStore('auth', () => {
const roleName = data.user?.role?.name || data.role || '';
permissionStore.setPermissions(perms, roleName);
const themeStore = useThemeStore();
themeStore.loadFromUser(data.user);
return data;
}
+9
View File
@@ -2,6 +2,8 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { authApi } from '@/api/authApi';
import { useAuthStore } from './authStore';
import { useThemeStore } from './themeStore';
export const usePermissionStore = defineStore('permission', () => {
const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]'));
@@ -31,6 +33,13 @@ export const usePermissionStore = defineStore('permission', () => {
const perms = userData.role?.permissions || [];
const role = userData.role?.name || '';
setPermissions(perms, role);
const authStore = useAuthStore();
authStore.setUser(userData);
const themeStore = useThemeStore();
themeStore.loadFromUser(userData);
return permissions.value;
} catch (err) {
console.error('Failed to fetch user permissions:', err);
+127 -9
View File
@@ -1,35 +1,153 @@
// /src/stores/themeStore.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { authApi } from '@/api/authApi';
const FONT_SIZE_VALUES = {
small: '12px',
medium: '14px',
large: '16px'
};
const STORAGE_KEY = 'uiPreferences';
function readStoredPreferences() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function writeStoredPreferences(preferences) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
}
export const useThemeStore = defineStore('theme', () => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDarkMode = ref(savedTheme ? savedTheme === 'dark' : prefersDark);
const stored = readStoredPreferences();
const themePreference = ref(stored?.theme || 'system');
const fontSize = ref(stored?.fontSize || 'medium');
const isDarkMode = ref(false);
let systemThemeListener = null;
function resolveDarkMode() {
if (themePreference.value === 'dark') return true;
if (themePreference.value === 'light') return false;
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
}
function applyTheme() {
isDarkMode.value = resolveDarkMode();
const htmlEl = document.documentElement;
if (isDarkMode.value) {
htmlEl.classList.add('dark-mode', 'dark');
localStorage.setItem('theme', 'dark');
} else {
htmlEl.classList.remove('dark-mode', 'dark');
localStorage.setItem('theme', 'light');
}
}
function toggleTheme() {
isDarkMode.value = !isDarkMode.value;
function applyFontSize() {
const size = FONT_SIZE_VALUES[fontSize.value] || FONT_SIZE_VALUES.medium;
document.documentElement.style.setProperty('--app-font-size', size);
document.documentElement.dataset.fontSize = fontSize.value;
}
function persistLocal() {
writeStoredPreferences({
theme: themePreference.value,
fontSize: fontSize.value
});
}
function applyPreferences() {
applyTheme();
applyFontSize();
persistLocal();
}
function setupSystemThemeListener() {
if (!window.matchMedia) return;
if (systemThemeListener) {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', systemThemeListener);
}
systemThemeListener = () => {
if (themePreference.value === 'system') {
applyTheme();
}
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', systemThemeListener);
}
function loadFromUser(user) {
if (!user?.preferences) return;
if (user.preferences.theme) {
themePreference.value = user.preferences.theme;
}
if (user.preferences.fontSize) {
fontSize.value = user.preferences.fontSize;
}
applyPreferences();
}
function toggleTheme() {
themePreference.value = isDarkMode.value ? 'light' : 'dark';
applyPreferences();
saveToServer();
}
function setThemePreference(value) {
themePreference.value = value;
applyPreferences();
}
function setFontSize(value) {
fontSize.value = value;
applyFontSize();
persistLocal();
}
async function saveToServer() {
try {
const res = await authApi.updateSelf({
preferences: {
theme: themePreference.value,
fontSize: fontSize.value
}
});
const user = res.data || res;
return user;
} catch (err) {
console.warn('Failed to save UI preferences:', err);
throw err;
}
}
function initTheme() {
applyTheme();
applyPreferences();
setupSystemThemeListener();
}
return {
isDarkMode,
themePreference,
fontSize,
fontSizeOptions: [
{ label: 'کوچک', value: 'small', sample: '۱۲px' },
{ label: 'متوسط', value: 'medium', sample: '۱۴px' },
{ label: 'بزرگ', value: 'large', sample: '۱۶px' }
],
themeOptions: [
{ label: 'روشن', value: 'light', icon: 'pi pi-sun' },
{ label: 'تیره', value: 'dark', icon: 'pi pi-moon' },
{ label: 'سیستم', value: 'system', icon: 'pi pi-desktop' }
],
toggleTheme,
initTheme
setThemePreference,
setFontSize,
loadFromUser,
saveToServer,
initTheme,
applyPreferences
};
});