feat: improve bill flow, user columns, and admin notes UI
Select class before user when creating bills, fix national ID and registered classes display, remove course ratings, fix notification badge, and show Persian datetime in the topbar.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<!-- Reusable admin notes list (string[]) for users, classes, sessions -->
|
||||
<template>
|
||||
<div class="admin-notes-field flex flex-column gap-2">
|
||||
<label v-if="label" class="font-semibold text-sm">{{ label }}</label>
|
||||
<p v-if="hint" class="text-muted text-xs m-0">{{ hint }}</p>
|
||||
|
||||
<div v-for="(note, index) in model" :key="index" class="flex gap-2 align-items-start">
|
||||
<Textarea
|
||||
:modelValue="note"
|
||||
rows="2"
|
||||
class="w-full text-sm flex-grow-1"
|
||||
:placeholder="placeholder"
|
||||
@update:modelValue="updateNote(index, $event)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
:aria-label="'حذف یادداشت'"
|
||||
@click="removeNote(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
label="افزودن یادداشت ادمین"
|
||||
icon="pi pi-plus"
|
||||
text
|
||||
size="small"
|
||||
class="align-self-start"
|
||||
@click="addNote"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Button from 'primevue/button';
|
||||
|
||||
const model = defineModel({ type: Array, default: () => [] });
|
||||
|
||||
defineProps({
|
||||
label: { type: String, default: 'یادداشتهای ادمین' },
|
||||
hint: {
|
||||
type: String,
|
||||
default: 'برای موارد غیرعادی یا نکات داخلی، یادداشت اضافه کنید'
|
||||
},
|
||||
placeholder: { type: String, default: 'یادداشت…' }
|
||||
});
|
||||
|
||||
const addNote = () => {
|
||||
model.value = [...(model.value || []), ''];
|
||||
};
|
||||
|
||||
const updateNote = (index, value) => {
|
||||
const next = [...(model.value || [])];
|
||||
next[index] = value;
|
||||
model.value = next;
|
||||
};
|
||||
|
||||
const removeNote = (index) => {
|
||||
model.value = (model.value || []).filter((_, i) => i !== index);
|
||||
};
|
||||
</script>
|
||||
@@ -20,6 +20,10 @@
|
||||
<i class="pi pi-building text-2xl"></i>
|
||||
<span>{{ $t('app.title') }}</span>
|
||||
</div>
|
||||
<div class="persian-datetime hidden md:flex flex-column align-items-start line-height-2 mr-2">
|
||||
<span class="text-sm font-semibold text-color">{{ persianDate }}</span>
|
||||
<span class="text-xs text-color-secondary" dir="ltr">{{ persianTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-3">
|
||||
@@ -63,12 +67,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import moment from 'jalali-moment';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { notificationApi } from '@/api/notificationApi';
|
||||
import Button from 'primevue/button';
|
||||
import Avatar from 'primevue/avatar';
|
||||
import Menu from 'primevue/menu';
|
||||
@@ -78,9 +85,30 @@ const themeStore = useThemeStore();
|
||||
const uiStore = useUiStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { toPersianDigits } = usePersianDate();
|
||||
|
||||
const userMenu = ref(null);
|
||||
const notificationCount = ref(3);
|
||||
const notificationCount = ref(0);
|
||||
const persianDate = ref('');
|
||||
const persianTime = ref('');
|
||||
let clockTimer = null;
|
||||
|
||||
const updateClock = () => {
|
||||
const now = moment().locale('fa');
|
||||
persianDate.value = toPersianDigits(now.format('dddd jD jMMMM jYYYY'));
|
||||
persianTime.value = toPersianDigits(now.format('HH:mm:ss'));
|
||||
};
|
||||
|
||||
const fetchNotificationCount = async () => {
|
||||
try {
|
||||
const res = await notificationApi.getAll({ limit: 1, page: 1 });
|
||||
// axios interceptor returns body: { success, data, meta }
|
||||
const total = res?.meta?.totalCount ?? 0;
|
||||
notificationCount.value = Number(total) || 0;
|
||||
} catch {
|
||||
notificationCount.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUserMenu = (event) => {
|
||||
userMenu.value.toggle(event);
|
||||
@@ -111,6 +139,16 @@ const menuItems = ref([
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
updateClock();
|
||||
clockTimer = setInterval(updateClock, 1000);
|
||||
fetchNotificationCount();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (clockTimer) clearInterval(clockTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -121,6 +159,11 @@ const menuItems = ref([
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.persian-datetime {
|
||||
padding-inline-start: 0.75rem;
|
||||
border-inline-start: 1px solid var(--surface-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.notification-trigger {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
Reference in New Issue
Block a user