feat(dashboard): fix logout flow and redesign executive dashboard with real live data, charts and feeds
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
statusStats: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { isDarkTheme } = useLayout();
|
||||
|
||||
const chartData = ref(null);
|
||||
const chartOptions = ref(null);
|
||||
|
||||
const defaultColors = ['#10b981', '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#64748b'];
|
||||
|
||||
function updateChart() {
|
||||
const statuses = props.statusStats || [];
|
||||
const labels = statuses.length > 0 ? statuses.map((s) => s.name) : ['نامشخص'];
|
||||
const data = statuses.length > 0 ? statuses.map((s) => s.count) : [0];
|
||||
const colors = statuses.map((s, i) => s.color || defaultColors[i % defaultColors.length]);
|
||||
|
||||
chartData.value = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data,
|
||||
backgroundColor: colors,
|
||||
borderWidth: 2,
|
||||
borderColor: isDarkTheme.value ? '#18181b' : '#ffffff'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartOptions.value = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
rtl: true,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
padding: 16,
|
||||
font: {
|
||||
family: 'Estedad',
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
rtl: true,
|
||||
titleFont: { family: 'Estedad', size: 13 },
|
||||
bodyFont: { family: 'Estedad', size: 12 },
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const count = context.parsed;
|
||||
const statusItem = statuses[context.dataIndex];
|
||||
const pct = statusItem?.percentage !== undefined ? ` (${statusItem.percentage}٪)` : '';
|
||||
return `${context.label}: ${Number(count).toLocaleString('fa-IR')} ملک${pct}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: '65%'
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => props.statusStats, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
watch(isDarkTheme, () => {
|
||||
updateChart();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
updateChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card h-full p-5 border border-surface-200 dark:border-surface-800 rounded-2xl bg-surface-0 dark:bg-surface-900">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-surface-900 dark:text-surface-0 mb-1">وضعیت اسناد و املاک</h3>
|
||||
<p class="text-xs text-muted-color">تفکیک املاک بر اساس وضعیت سند و مالکیت</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400">
|
||||
<i class="pi pi-check-circle text-[10px]"></i>
|
||||
وضعیتها
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="h-72 flex items-center justify-center">
|
||||
<i class="pi pi-spin pi-spinner text-primary text-3xl"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!statusStats || statusStats.length === 0" class="h-72 flex flex-col items-center justify-center text-muted-color">
|
||||
<i class="pi pi-chart-pie text-4xl mb-2 opacity-40"></i>
|
||||
<span class="text-sm">اطلاعات وضعیت املاک ثبت نشده است</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="h-72">
|
||||
<Chart type="doughnut" :data="chartData" :options="chartOptions" class="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const actions = [
|
||||
{
|
||||
title: 'ثبت ملک جدید',
|
||||
description: 'افزودن زمین، ساختمان یا آپارتمان',
|
||||
icon: 'pi pi-file-plus',
|
||||
color: 'bg-blue-500 text-white',
|
||||
bgSoft: 'bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800',
|
||||
route: '/property/create-property'
|
||||
},
|
||||
{
|
||||
title: 'ثبت تراکنش انبار',
|
||||
description: 'ثبت ورود یا خروج مصالح و اقلام',
|
||||
icon: 'pi pi-plus-circle',
|
||||
color: 'bg-emerald-500 text-white',
|
||||
bgSoft: 'bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-800',
|
||||
route: '/warehouse-items/create-transaction'
|
||||
},
|
||||
{
|
||||
title: 'فهرست املاک',
|
||||
description: 'جستجو، فیلتر و مدیریت اسناد',
|
||||
icon: 'pi pi-list',
|
||||
color: 'bg-purple-500 text-white',
|
||||
bgSoft: 'bg-purple-50 dark:bg-purple-950/30 border-purple-200 dark:border-purple-800',
|
||||
route: '/property/list-properties'
|
||||
},
|
||||
{
|
||||
title: 'اقلام و موجودی انبار',
|
||||
description: 'مشاهده کاردکس و موجودی انبارها',
|
||||
icon: 'pi pi-box',
|
||||
color: 'bg-amber-500 text-white',
|
||||
bgSoft: 'bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800',
|
||||
route: '/warehouse-items/list'
|
||||
}
|
||||
];
|
||||
|
||||
function navigate(route) {
|
||||
if (route) {
|
||||
router.push(route);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card p-5 border border-surface-200 dark:border-surface-800 rounded-2xl bg-surface-0 dark:bg-surface-900">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-surface-900 dark:text-surface-0 mb-1">دسترسی و عملیات سریع</h3>
|
||||
<p class="text-xs text-muted-color">میانبرهای پرکاربرد برای تسهیل امور مدیریت و ثبت</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/40 px-2.5 py-1 rounded-full font-medium">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
سیستم آنلاین
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div
|
||||
v-for="item in actions"
|
||||
:key="item.title"
|
||||
@click="navigate(item.route)"
|
||||
class="p-4 rounded-xl border transition-all duration-200 cursor-pointer hover:shadow-md flex items-center gap-3.5 group bg-surface-0 dark:bg-surface-850 hover:border-primary-400 dark:hover:border-primary-600"
|
||||
>
|
||||
<div :class="['w-11 h-11 rounded-xl flex items-center justify-center shrink-0 shadow-sm transition-transform group-hover:scale-110', item.color]">
|
||||
<i :class="[item.icon, 'text-lg']"></i>
|
||||
</div>
|
||||
<div class="truncate">
|
||||
<h4 class="font-semibold text-sm text-surface-900 dark:text-surface-100 group-hover:text-primary transition-colors">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<p class="text-[11px] text-muted-color truncate mt-0.5">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,40 +1,143 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { PropertyService } from '@/service/PropertyService';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { parseDateToJalali } from '@/utils/jalali';
|
||||
|
||||
const properties = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
PropertyService.getAllProperties().then((data) => (properties.value = data));
|
||||
const props = defineProps({
|
||||
properties: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function editProperty(propertyId) {
|
||||
if (propertyId) {
|
||||
router.push('/property/edit-property/' + propertyId);
|
||||
}
|
||||
}
|
||||
|
||||
function viewAllProperties() {
|
||||
router.push('/property/list-properties');
|
||||
}
|
||||
|
||||
function formatJalaliDate(dateVal) {
|
||||
if (!dateVal) return '—';
|
||||
const j = parseDateToJalali(dateVal);
|
||||
if (!j) return '—';
|
||||
return `${j.jy}/${String(j.jm).padStart(2, '0')}/${String(j.jd).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '۰';
|
||||
return Number(num).toLocaleString('fa-IR');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="font-semibold text-xl mb-4">آخرین املاک افزوده شده</div>
|
||||
<DataTable :value="properties" :rows="5" :paginator="true" responsiveLayout="scroll">
|
||||
<Column style="min-width: 15%" header="تصویر">
|
||||
<div class="card h-full p-5 border border-surface-200 dark:border-surface-800 rounded-2xl bg-surface-0 dark:bg-surface-900">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-surface-900 dark:text-surface-0 mb-1">آخرین املاک ثبت شده</h3>
|
||||
<p class="text-xs text-muted-color">جدیدترین املاک و داراییهای ثبت شده در سیستم</p>
|
||||
</div>
|
||||
<Button
|
||||
label="مشاهده همه"
|
||||
icon="pi pi-arrow-left"
|
||||
iconPos="right"
|
||||
size="small"
|
||||
text
|
||||
class="text-xs"
|
||||
@click="viewAllProperties"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="py-12 flex items-center justify-center">
|
||||
<i class="pi pi-spin pi-spinner text-primary text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!properties || properties.length === 0" class="py-12 flex flex-col items-center justify-center text-muted-color">
|
||||
<i class="pi pi-building text-4xl mb-2 opacity-40"></i>
|
||||
<span class="text-sm">ملکی ثبت نشده است</span>
|
||||
<router-link to="/property/create-property" class="mt-3">
|
||||
<Button label="ثبت ملک جدید" icon="pi pi-plus" size="small" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<DataTable :value="properties" responsiveLayout="scroll" class="p-datatable-sm">
|
||||
<!-- Image -->
|
||||
<Column header="تصویر" style="width: 60px">
|
||||
<template #body="slotProps">
|
||||
<img :src="`${slotProps.data.mainImageUrl || slotProps.data.mainImageUrls?.[0]}`" :alt="slotProps.data.name" width="50" class="shadow" />
|
||||
<div class="w-11 h-11 rounded-lg overflow-hidden border border-surface-200 dark:border-surface-700 bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
|
||||
<img
|
||||
:src="slotProps.data.imageUrl"
|
||||
:alt="slotProps.data.name"
|
||||
class="w-full h-full object-cover"
|
||||
@error="(e) => e.target.src = 'https://cloud.mbmanager.ir/images/item-no-image.png'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" header="نام" :sortable="true" style="min-width: 35%"></Column>
|
||||
<Column field="category" header="دسته بندی" :sortable="true" style="min-width: 30%">
|
||||
|
||||
<!-- Code & Name -->
|
||||
<Column header="نام و کد ملک" style="min-width: 160px">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.category?.nativeName }}
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-100 text-sm hover:text-primary cursor-pointer" @click="editProperty(slotProps.data._id)">
|
||||
{{ slotProps.data.name }}
|
||||
</span>
|
||||
<span class="text-[11px] text-muted-color font-mono">
|
||||
کد: {{ slotProps.data.propertyCode }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column style="min-width: 15%" header="نمایش">
|
||||
|
||||
<!-- Category -->
|
||||
<Column header="دستهبندی" style="min-width: 110px">
|
||||
<template #body="slotProps">
|
||||
<Button icon="pi pi-search" type="button" class="p-button-text" @click="editProperty(slotProps.data._id)"></Button>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300">
|
||||
{{ slotProps.data.category }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<!-- Status / Owner -->
|
||||
<Column header="مشخصات" style="min-width: 130px">
|
||||
<template #body="slotProps">
|
||||
<div class="flex flex-col text-xs gap-0.5">
|
||||
<span class="text-surface-600 dark:text-surface-400">مالک: <b class="text-surface-800 dark:text-surface-200">{{ slotProps.data.ownerName }}</b></span>
|
||||
<span class="text-muted-color text-[11px]">مساحت: {{ formatNumber(slotProps.data.area) }} م² ({{ slotProps.data.dong }} دانگ)</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<!-- Date -->
|
||||
<Column header="تاریخ ثبت" style="min-width: 95px">
|
||||
<template #body="slotProps">
|
||||
<span class="text-xs text-muted-color font-mono">
|
||||
{{ formatJalaliDate(slotProps.data.createdAt) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<!-- Action -->
|
||||
<Column header="عملیات" style="width: 50px">
|
||||
<template #body="slotProps">
|
||||
<Button
|
||||
icon="pi pi-arrow-left"
|
||||
class="p-button-rounded p-button-text p-button-sm !w-8 !h-8"
|
||||
v-tooltip.top="'مشاهده و ویرایش'"
|
||||
@click="editProperty(slotProps.data._id)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,95 +1,145 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
categoryStats: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { getPrimary, getSurface, isDarkTheme } = useLayout();
|
||||
|
||||
const chartData = ref(null);
|
||||
const chartOptions = ref(null);
|
||||
|
||||
function setChartData() {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const palette = [
|
||||
'#3b82f6', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6',
|
||||
'#06b6d4', '#f97316', '#6366f1', '#14b8a6', '#84cc16'
|
||||
];
|
||||
|
||||
return {
|
||||
labels: ['زمین', 'ساختمان'],
|
||||
function updateChart() {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const textMutedColor = documentStyle.getPropertyValue('--text-color-secondary') || '#6b7280';
|
||||
const borderColor = documentStyle.getPropertyValue('--surface-border') || '#e5e7eb';
|
||||
|
||||
const categories = props.categoryStats || [];
|
||||
const labels = categories.length > 0 ? categories.map((c) => c.name) : ['بدون دستهبندی'];
|
||||
const data = categories.length > 0 ? categories.map((c) => c.count) : [0];
|
||||
const backgroundColors = labels.map((_, i) => palette[i % palette.length]);
|
||||
|
||||
chartData.value = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'bar',
|
||||
label: 'تعداد املاک',
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-primary-400'),
|
||||
font: {
|
||||
family: 'Estedad',
|
||||
size: 14
|
||||
},
|
||||
data: [18, 13],
|
||||
barThickness: 32
|
||||
data: data,
|
||||
backgroundColor: backgroundColors,
|
||||
borderRadius: 8,
|
||||
barThickness: 28,
|
||||
maxBarThickness: 36
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function setChartOptions() {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const borderColor = documentStyle.getPropertyValue('--surface-border');
|
||||
const textMutedColor = documentStyle.getPropertyValue('--text-color-secondary');
|
||||
|
||||
return {
|
||||
chartOptions.value = {
|
||||
maintainAspectRatio: false,
|
||||
aspectRatio: 0.8,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
rtl: true,
|
||||
titleFont: { family: 'Estedad', size: 13 },
|
||||
bodyFont: { family: 'Estedad', size: 12 },
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const count = context.parsed.y !== undefined ? context.parsed.y : context.parsed;
|
||||
const cat = categories[context.dataIndex];
|
||||
const areaText = cat?.totalArea ? ` (${Number(cat.totalArea).toLocaleString('fa-IR')} م²)` : '';
|
||||
return `تعداد: ${Number(count).toLocaleString('fa-IR')} ملک${areaText}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
ticks: {
|
||||
color: textMutedColor,
|
||||
font: {
|
||||
family: 'Estedad',
|
||||
size: 14
|
||||
size: 13
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'transparent',
|
||||
borderColor: 'transparent'
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
color: textMutedColor,
|
||||
font: {
|
||||
family: 'Estedad',
|
||||
size: 14
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: borderColor,
|
||||
borderColor: 'transparent',
|
||||
drawTicks: false
|
||||
drawBorder: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => props.categoryStats, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||
chartData.value = setChartData();
|
||||
chartOptions.value = setChartOptions();
|
||||
updateChart();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
chartData.value = setChartData();
|
||||
chartOptions.value = setChartOptions();
|
||||
updateChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="font-semibold text-xl mb-4">تقسیم بندی املاک</div>
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-80" />
|
||||
<div class="card h-full p-5 border border-surface-200 dark:border-surface-800 rounded-2xl bg-surface-0 dark:bg-surface-900">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-surface-900 dark:text-surface-0 mb-1">تفکیک املاک بر اساس دستهبندی</h3>
|
||||
<p class="text-xs text-muted-color">توزیع و تعداد املاک ثبت شده در دستهبندیهای مختلف سامانه</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-primary-50 dark:bg-primary-950/40 text-primary">
|
||||
<i class="pi pi-th-large text-[10px]"></i>
|
||||
{{ categoryStats?.length || 0 }} دستهبندی
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="h-72 flex items-center justify-center">
|
||||
<i class="pi pi-spin pi-spinner text-primary text-3xl"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!categoryStats || categoryStats.length === 0" class="h-72 flex flex-col items-center justify-center text-muted-color">
|
||||
<i class="pi pi-chart-bar text-4xl mb-2 opacity-40"></i>
|
||||
<span class="text-sm">اطلاعاتی برای نمایش وجود ندارد</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="h-72">
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
font-family: 'Estedad', sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,121 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
stats: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '۰';
|
||||
return Number(num).toLocaleString('fa-IR');
|
||||
};
|
||||
|
||||
const overview = computed(() => props.stats?.overview || {});
|
||||
const warehouseFlow = computed(() => props.stats?.warehouseFlow || {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-span-12 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0">
|
||||
<div class="flex justify-between mb-4">
|
||||
<!-- Total Properties Card -->
|
||||
<div class="col-span-12 sm:col-span-6 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0 h-full p-5 border border-surface-200 dark:border-surface-800 hover:shadow-lg transition-all duration-300 rounded-2xl relative overflow-hidden bg-gradient-to-br from-surface-0 to-surface-50 dark:from-surface-900 dark:to-surface-950">
|
||||
<div class="absolute -right-6 -bottom-6 w-24 h-24 bg-blue-500/5 dark:bg-blue-500/10 rounded-full blur-xl pointer-events-none"></div>
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span class="block text-muted-color font-medium mb-4">تعداد املاک</span>
|
||||
<div class="text-surface-900 dark:text-surface-0 font-medium text-xl">100</div>
|
||||
<span class="block text-muted-color font-medium text-sm mb-1">املاک و مستغلات</span>
|
||||
<div v-if="loading" class="h-8 w-20 bg-surface-200 dark:bg-surface-700 animate-pulse rounded my-1"></div>
|
||||
<div v-else class="text-surface-900 dark:text-surface-0 font-bold text-2xl tracking-tight">
|
||||
{{ formatNumber(overview.totalProperties) }} <span class="text-xs font-normal text-muted-color">ملک</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-blue-100 dark:bg-blue-400/10 rounded-border" style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-shopping-cart text-blue-500 !text-xl"></i>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded-xl shadow-sm" style="width: 3rem; height: 3rem">
|
||||
<i class="pi pi-building text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs font-medium text-surface-600 dark:text-surface-400 pt-2 border-t border-surface-100 dark:border-surface-800">
|
||||
<i class="pi pi-map text-blue-500"></i>
|
||||
<span>مساحت کل: <b class="text-surface-900 dark:text-surface-100">{{ formatNumber(overview.totalArea) }}</b> م²</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0">
|
||||
<div class="flex justify-between mb-4">
|
||||
|
||||
<!-- Warehouse Items Card -->
|
||||
<div class="col-span-12 sm:col-span-6 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0 h-full p-5 border border-surface-200 dark:border-surface-800 hover:shadow-lg transition-all duration-300 rounded-2xl relative overflow-hidden bg-gradient-to-br from-surface-0 to-surface-50 dark:from-surface-900 dark:to-surface-950">
|
||||
<div class="absolute -right-6 -bottom-6 w-24 h-24 bg-amber-500/5 dark:bg-amber-500/10 rounded-full blur-xl pointer-events-none"></div>
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span class="block text-muted-color font-medium mb-4">ارز ها</span>
|
||||
<div class="text-surface-900 dark:text-surface-0 font-medium text-xl">2800 دلار</div>
|
||||
<span class="block text-muted-color font-medium text-sm mb-1">اقلام انبار</span>
|
||||
<div v-if="loading" class="h-8 w-20 bg-surface-200 dark:bg-surface-700 animate-pulse rounded my-1"></div>
|
||||
<div v-else class="text-surface-900 dark:text-surface-0 font-bold text-2xl tracking-tight">
|
||||
{{ formatNumber(overview.totalWarehouseItems) }} <span class="text-xs font-normal text-muted-color">قلم کالا</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-orange-100 dark:bg-orange-400/10 rounded-border" style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-dollar text-orange-500 !text-xl"></i>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded-xl shadow-sm" style="width: 3rem; height: 3rem">
|
||||
<i class="pi pi-box text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs font-medium text-surface-600 dark:text-surface-400 pt-2 border-t border-surface-100 dark:border-surface-800">
|
||||
<i class="pi pi-warehouse text-amber-500"></i>
|
||||
<span>مستقر در <b class="text-surface-900 dark:text-surface-100">{{ formatNumber(overview.totalWarehouses) }}</b> انبار فعال</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0">
|
||||
<div class="flex justify-between mb-4">
|
||||
|
||||
<!-- Warehouse Transactions Card -->
|
||||
<div class="col-span-12 sm:col-span-6 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0 h-full p-5 border border-surface-200 dark:border-surface-800 hover:shadow-lg transition-all duration-300 rounded-2xl relative overflow-hidden bg-gradient-to-br from-surface-0 to-surface-50 dark:from-surface-900 dark:to-surface-950">
|
||||
<div class="absolute -right-6 -bottom-6 w-24 h-24 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-full blur-xl pointer-events-none"></div>
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span class="block text-muted-color font-medium mb-4">تعداد کاربران</span>
|
||||
<div class="text-surface-900 dark:text-surface-0 font-medium text-xl">3 عدد</div>
|
||||
<span class="block text-muted-color font-medium text-sm mb-1">گردش انبارداری</span>
|
||||
<div v-if="loading" class="h-8 w-20 bg-surface-200 dark:bg-surface-700 animate-pulse rounded my-1"></div>
|
||||
<div v-else class="text-surface-900 dark:text-surface-0 font-bold text-2xl tracking-tight">
|
||||
{{ formatNumber(overview.totalWarehouseTransactions) }} <span class="text-xs font-normal text-muted-color">تراکنش</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-cyan-100 dark:bg-cyan-400/10 rounded-border" style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-users text-cyan-500 !text-xl"></i>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-xl shadow-sm" style="width: 3rem; height: 3rem">
|
||||
<i class="pi pi-sync text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs font-medium text-surface-600 dark:text-surface-400 pt-2 border-t border-surface-100 dark:border-surface-800">
|
||||
<span class="text-emerald-600 dark:text-emerald-400 flex items-center gap-1">
|
||||
<i class="pi pi-arrow-down-left"></i>
|
||||
{{ formatNumber(warehouseFlow.inCount) }} ورود
|
||||
</span>
|
||||
<span class="text-rose-600 dark:text-rose-400 flex items-center gap-1">
|
||||
<i class="pi pi-arrow-up-right"></i>
|
||||
{{ formatNumber(warehouseFlow.outCount) }} خروج
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Users & Roles Card -->
|
||||
<div class="col-span-12 sm:col-span-6 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0 h-full p-5 border border-surface-200 dark:border-surface-800 hover:shadow-lg transition-all duration-300 rounded-2xl relative overflow-hidden bg-gradient-to-br from-surface-0 to-surface-50 dark:from-surface-900 dark:to-surface-950">
|
||||
<div class="absolute -right-6 -bottom-6 w-24 h-24 bg-purple-500/5 dark:bg-purple-500/10 rounded-full blur-xl pointer-events-none"></div>
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span class="block text-muted-color font-medium text-sm mb-1">کاربران سیستم</span>
|
||||
<div v-if="loading" class="h-8 w-20 bg-surface-200 dark:bg-surface-700 animate-pulse rounded my-1"></div>
|
||||
<div v-else class="text-surface-900 dark:text-surface-0 font-bold text-2xl tracking-tight">
|
||||
{{ formatNumber(overview.totalUsers) }} <span class="text-xs font-normal text-muted-color">کاربر</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-purple-100 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400 rounded-xl shadow-sm" style="width: 3rem; height: 3rem">
|
||||
<i class="pi pi-users text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs font-medium text-surface-600 dark:text-surface-400 pt-2 border-t border-surface-100 dark:border-surface-800">
|
||||
<i class="pi pi-shield text-purple-500"></i>
|
||||
<span>تعریف شده در <b class="text-surface-900 dark:text-surface-100">{{ formatNumber(overview.totalRoles) }}</b> نقش سازمانی</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { parseDateToJalali } from '@/utils/jalali';
|
||||
|
||||
const props = defineProps({
|
||||
transactions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
warehouseFlow: {
|
||||
type: Object,
|
||||
default: () => ({ inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0 })
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function viewAllTransactions() {
|
||||
router.push('/warehouse-items/transactions');
|
||||
}
|
||||
|
||||
function createTransaction() {
|
||||
router.push('/warehouse-items/create-transaction');
|
||||
}
|
||||
|
||||
function formatJalaliDate(dateVal) {
|
||||
if (!dateVal) return '—';
|
||||
const j = parseDateToJalali(dateVal);
|
||||
if (!j) return '—';
|
||||
return `${j.jy}/${String(j.jm).padStart(2, '0')}/${String(j.jd).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '۰';
|
||||
return Number(num).toLocaleString('fa-IR');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card h-full p-5 border border-surface-200 dark:border-surface-800 rounded-2xl bg-surface-0 dark:bg-surface-900 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-surface-900 dark:text-surface-0 mb-1">آخرین گردشهای انبار</h3>
|
||||
<p class="text-xs text-muted-color">گزارش لحظهای ثبت ورود و خروج کالا در انبارها</p>
|
||||
</div>
|
||||
<Button
|
||||
label="مشاهده همه"
|
||||
icon="pi pi-arrow-left"
|
||||
iconPos="right"
|
||||
size="small"
|
||||
text
|
||||
class="text-xs"
|
||||
@click="viewAllTransactions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Flow stats summary banner -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
<div class="p-3 rounded-xl bg-emerald-50/60 dark:bg-emerald-950/20 border border-emerald-200/50 dark:border-emerald-800/30 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center">
|
||||
<i class="pi pi-arrow-down-left text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[11px] text-muted-color block">مجموع ورودی</span>
|
||||
<span class="text-xs font-bold text-emerald-700 dark:text-emerald-400">
|
||||
{{ formatNumber(warehouseFlow.inQuantity) }} واحد
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
||||
{{ formatNumber(warehouseFlow.inCount) }} تراکنش
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-xl bg-rose-50/60 dark:bg-rose-950/20 border border-rose-200/50 dark:border-rose-800/30 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-lg bg-rose-500/10 text-rose-600 dark:text-rose-400 flex items-center justify-center">
|
||||
<i class="pi pi-arrow-up-right text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[11px] text-muted-color block">مجموع خروجی</span>
|
||||
<span class="text-xs font-bold text-rose-700 dark:text-rose-400">
|
||||
{{ formatNumber(warehouseFlow.outQuantity) }} واحد
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-rose-600 dark:text-rose-400">
|
||||
{{ formatNumber(warehouseFlow.outCount) }} تراکنش
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="py-10 flex items-center justify-center">
|
||||
<i class="pi pi-spin pi-spinner text-primary text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="!transactions || transactions.length === 0" class="py-10 flex flex-col items-center justify-center text-muted-color">
|
||||
<i class="pi pi-history text-4xl mb-2 opacity-40"></i>
|
||||
<span class="text-sm">تراکنش انباری ثبت نشده است</span>
|
||||
<Button label="ثبت اولین تراکنش" icon="pi pi-plus" size="small" class="mt-3" @click="createTransaction" />
|
||||
</div>
|
||||
|
||||
<!-- Transactions List -->
|
||||
<ul v-else class="list-none p-0 m-0 divide-y divide-surface-100 dark:divide-surface-800">
|
||||
<li
|
||||
v-for="tx in transactions"
|
||||
:key="tx._id"
|
||||
class="py-3 flex items-center justify-between gap-3 hover:bg-surface-50 dark:hover:bg-surface-800/50 px-2 rounded-xl transition-colors duration-150"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
:class="[
|
||||
'w-9 h-9 rounded-xl flex items-center justify-center shrink-0 font-bold text-xs',
|
||||
tx.type === 'IN'
|
||||
? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-rose-100 dark:bg-rose-500/20 text-rose-600 dark:text-rose-400'
|
||||
]"
|
||||
>
|
||||
<i :class="['pi text-xs', tx.type === 'IN' ? 'pi-arrow-down' : 'pi-arrow-up']"></i>
|
||||
</div>
|
||||
|
||||
<div class="truncate">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-surface-900 dark:text-surface-100 text-sm truncate">
|
||||
{{ tx.itemName }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'px-1.5 py-0.2 rounded text-[10px] font-bold',
|
||||
tx.type === 'IN'
|
||||
? 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-300'
|
||||
: 'bg-rose-100 dark:bg-rose-900/40 text-rose-700 dark:text-rose-300'
|
||||
]"
|
||||
>
|
||||
{{ tx.type === 'IN' ? 'ورود' : 'خروج' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-color flex items-center gap-2 mt-0.5">
|
||||
<span>انبار: {{ tx.warehouseName }}</span>
|
||||
<span>•</span>
|
||||
<span>توسط: {{ tx.actorName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left shrink-0">
|
||||
<div
|
||||
:class="[
|
||||
'font-bold text-sm font-mono',
|
||||
tx.type === 'IN' ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'
|
||||
]"
|
||||
>
|
||||
{{ tx.type === 'IN' ? '+' : '-' }}{{ formatNumber(tx.quantity) }} {{ tx.unit }}
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-color font-mono block">
|
||||
{{ formatJalaliDate(tx.date) }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+65
-20
@@ -2,20 +2,62 @@
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import AppConfigurator from './AppConfigurator.vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter()
|
||||
const signOut = () => {
|
||||
Cookies.set('bm-token', '')
|
||||
router.push('/auth/login')
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await axios.post('/api/users/logout', {}, { withCredentials: true });
|
||||
} catch (e) {
|
||||
console.warn('Logout API error:', e?.message || e);
|
||||
}
|
||||
|
||||
// Comprehensive cookie cleaning
|
||||
Cookies.remove('bm-token');
|
||||
Cookies.remove('bm-token', { domain: '.mbmanager.ir', path: '/' });
|
||||
Cookies.remove('bm-token', { path: '/' });
|
||||
Cookies.remove('belonging_token');
|
||||
Cookies.remove('belonging_token', { domain: '.mbmanager.ir', path: '/' });
|
||||
Cookies.remove('belonging_token', { path: '/' });
|
||||
Cookies.remove('jwt');
|
||||
Cookies.remove('jwt', { domain: '.mbmanager.ir', path: '/' });
|
||||
Cookies.remove('jwt', { path: '/' });
|
||||
|
||||
// Force expire raw cookies in document.cookie
|
||||
if (typeof document !== 'undefined') {
|
||||
const cookiesToClear = ['bm-token', 'belonging_token', 'jwt'];
|
||||
const domains = ['', '; domain=.mbmanager.ir', '; domain=' + window.location.hostname];
|
||||
cookiesToClear.forEach((name) => {
|
||||
domains.forEach((dom) => {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${dom}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Clear local & session storage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('bm-token');
|
||||
localStorage.removeItem('belonging_token');
|
||||
localStorage.removeItem('jwt');
|
||||
}
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
|
||||
await router.replace('/auth/login');
|
||||
};
|
||||
|
||||
const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-topbar">
|
||||
<div class="layout-topbar-logo-container">
|
||||
<button class="layout-menu-button layout-topbar-action" @click="toggleMenu">
|
||||
<button class="layout-menu-button layout-topbar-action" @click="toggleMenu" title="منو">
|
||||
<i class="pi pi-bars"></i>
|
||||
</button>
|
||||
<router-link to="/" class="layout-topbar-logo">
|
||||
@@ -43,7 +85,7 @@ const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
|
||||
<div class="layout-topbar-actions">
|
||||
<div class="layout-config-menu">
|
||||
<button type="button" class="layout-topbar-action" @click="toggleDarkMode">
|
||||
<button type="button" class="layout-topbar-action" @click="toggleDarkMode" title="تغییر حالت شب/روز">
|
||||
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
|
||||
</button>
|
||||
<div class="relative">
|
||||
@@ -51,6 +93,7 @@ const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
v-styleclass="{ selector: '@next', enterFromClass: 'hidden', enterActiveClass: 'animate-scalein', leaveToClass: 'hidden', leaveActiveClass: 'animate-fadeout', hideOnOutsideClick: true }"
|
||||
type="button"
|
||||
class="layout-topbar-action layout-topbar-action-highlight"
|
||||
title="تنظیمات تم"
|
||||
>
|
||||
<i class="pi pi-palette"></i>
|
||||
</button>
|
||||
@@ -61,27 +104,29 @@ const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
<button
|
||||
class="layout-topbar-menu-button layout-topbar-action"
|
||||
v-styleclass="{ selector: '@next', enterFromClass: 'hidden', enterActiveClass: 'animate-scalein', leaveToClass: 'hidden', leaveActiveClass: 'animate-fadeout', hideOnOutsideClick: true }"
|
||||
title="منوی کاربری"
|
||||
>
|
||||
<i class="pi pi-ellipsis-v"></i>
|
||||
</button>
|
||||
|
||||
<div class="layout-topbar-menu hidden lg:block">
|
||||
<div class="layout-topbar-menu-content">
|
||||
<button type="button" class="layout-topbar-action">
|
||||
<i class="pi pi-calendar"></i>
|
||||
<span>Calendar</span>
|
||||
</button>
|
||||
<button type="button" class="layout-topbar-action">
|
||||
<i class="pi pi-inbox"></i>
|
||||
<span>Messages</span>
|
||||
</button>
|
||||
<button type="button" class="layout-topbar-action">
|
||||
<router-link to="/property/create-property" class="layout-topbar-action" title="ثبت ملک جدید">
|
||||
<i class="pi pi-plus-circle"></i>
|
||||
<span>ثبت ملک</span>
|
||||
</router-link>
|
||||
<router-link to="/users/list-users" class="layout-topbar-action" title="مدیریت کاربران">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Profile</span>
|
||||
</button>
|
||||
<button type="button" class="layout-topbar-action" @click="signOut">
|
||||
<i class="pi pi-power-off"></i>
|
||||
<span>Profile</span>
|
||||
<span>کاربران</span>
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="layout-topbar-action text-red-500 hover:text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||
title="خروج از حساب کاربری"
|
||||
@click="signOut"
|
||||
>
|
||||
<i class="pi pi-power-off text-red-500"></i>
|
||||
<span class="text-red-500">خروج</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const getAuthHeader = () => {
|
||||
const bmToken =
|
||||
(typeof document !== 'undefined'
|
||||
? document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1]
|
||||
: null) || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : null);
|
||||
|
||||
return bmToken && bmToken !== 'undefined' && bmToken !== 'null' ? { Authorization: `Bearer ${bmToken}` } : {};
|
||||
};
|
||||
|
||||
export const DashboardService = {
|
||||
/**
|
||||
* Fetch aggregated live statistics for the dashboard
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getDashboardStats: async () => {
|
||||
const response = await axios.get('/api/dashboard/stats', {
|
||||
withCredentials: true,
|
||||
headers: getAuthHeader()
|
||||
});
|
||||
return response.data?.data || response.data;
|
||||
}
|
||||
};
|
||||
+122
-12
@@ -1,22 +1,132 @@
|
||||
<script setup>
|
||||
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
|
||||
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
|
||||
import RecentSalesWidget from '@/components/dashboard/RecentSalesWidget.vue';
|
||||
import RevenueStreamWidget from '@/components/dashboard/RevenueStreamWidget.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import StatsWidget from '@/components/dashboard/StatsWidget.vue';
|
||||
import RevenueStreamWidget from '@/components/dashboard/RevenueStreamWidget.vue';
|
||||
import PropertyStatusWidget from '@/components/dashboard/PropertyStatusWidget.vue';
|
||||
import RecentSalesWidget from '@/components/dashboard/RecentSalesWidget.vue';
|
||||
import WarehouseActivityWidget from '@/components/dashboard/WarehouseActivityWidget.vue';
|
||||
import QuickActionsWidget from '@/components/dashboard/QuickActionsWidget.vue';
|
||||
import { DashboardService } from '@/service/DashboardService';
|
||||
import { gregorianToJalali, persianMonths } from '@/utils/jalali';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const stats = ref(null);
|
||||
const loading = ref(true);
|
||||
const refreshing = ref(false);
|
||||
|
||||
const dayNames = ['یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه'];
|
||||
|
||||
const getTodayPersianDate = () => {
|
||||
const now = new Date();
|
||||
const j = gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
const dayName = dayNames[now.getDay()];
|
||||
const monthName = persianMonths.find((m) => m.value === j.jm)?.name || '';
|
||||
return `${dayName} ${j.jd} ${monthName} ${j.jy}`;
|
||||
};
|
||||
|
||||
const todayFormatted = getTodayPersianDate();
|
||||
|
||||
const fetchDashboardStats = async (isManual = false) => {
|
||||
try {
|
||||
if (isManual) refreshing.value = true;
|
||||
else loading.value = true;
|
||||
|
||||
const data = await DashboardService.getDashboardStats();
|
||||
stats.value = data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load dashboard statistics:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchDashboardStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-12 gap-8">
|
||||
<StatsWidget />
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Dashboard Header Welcome Banner -->
|
||||
<div class="card p-6 border border-surface-200 dark:border-surface-800 rounded-2xl bg-gradient-to-l from-primary-500/10 via-surface-0 to-surface-0 dark:from-primary-500/15 dark:via-surface-900 dark:to-surface-900 shadow-sm relative overflow-hidden">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-primary-100 dark:bg-primary-900/40 text-primary">
|
||||
<i class="pi pi-compass text-[10px]"></i>
|
||||
مرکز مدیریت و پایش
|
||||
</span>
|
||||
<span class="text-xs text-muted-color">|</span>
|
||||
<span class="text-xs font-medium text-surface-600 dark:text-surface-400">
|
||||
{{ todayFormatted }}
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-black text-surface-900 dark:text-surface-0 tracking-tight">
|
||||
داشبورد جامع مدیریت اموال و انبارداری
|
||||
</h1>
|
||||
<p class="text-sm text-muted-color mt-1">
|
||||
خلاصه آماری داراییها، املاک ثبتی و گزارش لحظهای گردش کالاهای انبار
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<RecentSalesWidget />
|
||||
<!-- <BestSellingWidget />-->
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:class="['p-button-outlined p-button-secondary !w-10 !h-10 p-button-rounded', { 'pi-spin': refreshing }]"
|
||||
v-tooltip.bottom="'بروزرسانی دادهها'"
|
||||
@click="fetchDashboardStats(true)"
|
||||
/>
|
||||
<router-link to="/property/create-property">
|
||||
<Button label="ثبت ملک جدید" icon="pi pi-plus" class="shadow-sm" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4 KPI Stats Cards Row -->
|
||||
<div class="grid grid-cols-12 gap-5">
|
||||
<StatsWidget :stats="stats" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Shortcuts -->
|
||||
<QuickActionsWidget />
|
||||
|
||||
<!-- Real Data Visual Charts Row -->
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- Property Category Distribution Chart -->
|
||||
<div class="col-span-12 xl:col-span-7">
|
||||
<RevenueStreamWidget :category-stats="stats?.propertiesByCategory || []" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<!-- Property Status & Deeds Breakdown Chart -->
|
||||
<div class="col-span-12 xl:col-span-5">
|
||||
<PropertyStatusWidget :status-stats="stats?.propertiesByStatus || []" :loading="loading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Feeds: Recent Properties & Recent Warehouse Transactions Row -->
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- Recent Properties Table -->
|
||||
<div class="col-span-12 xl:col-span-7">
|
||||
<RecentSalesWidget :properties="stats?.recentProperties || []" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<!-- Recent Warehouse Transactions Feed -->
|
||||
<div class="col-span-12 xl:col-span-5">
|
||||
<WarehouseActivityWidget
|
||||
:transactions="stats?.recentWarehouseTransactions || []"
|
||||
:warehouse-flow="stats?.warehouseFlow || { inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0 }"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<RevenueStreamWidget />
|
||||
<!-- <NotificationsWidget />-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
font-family: 'Estedad', sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user