fix(dashboard): fix properties freeze, category/feature list views, and auth interceptor
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
+29
-2
@@ -1,6 +1,33 @@
|
||||
<script setup>
|
||||
import axios from 'axios'
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
// Global Axios Request Interceptor for Authentication
|
||||
axios.interceptors.request.use((config) => {
|
||||
config.withCredentials = true;
|
||||
|
||||
// Resolve token from Cookies, localStorage, or document.cookie
|
||||
let token = Cookies.get('bm-token') || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : null);
|
||||
|
||||
if (!token && typeof document !== 'undefined') {
|
||||
token = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
}
|
||||
|
||||
if (token && token !== 'undefined' && token !== 'null' && token.trim() !== '') {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${token.trim()}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
}, (error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const loginGuard = async (to, from, next) => {
|
||||
const token = Cookies.get('bm-token');
|
||||
const token = Cookies.get('bm-token') || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : null);
|
||||
|
||||
if (!token) {
|
||||
next('/auth/login'); // Redirect to log in if no token
|
||||
|
||||
@@ -2,46 +2,37 @@ import axios from 'axios';
|
||||
|
||||
export const CategoryService = {
|
||||
getAllCategories: async () => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/categories/admin/get-all',
|
||||
withCredentials: true
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
const response = await axios.get('/api/categories/admin/get-all');
|
||||
return response.data?.data || [];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error('Error in getAllCategories:', err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
createCategory: async (data) => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
url: '/api/categories/admin/create',
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
const response = await axios.post('/api/categories/admin/create', data);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
return err.response?.data?.message || 'خطا در ساخت دستهبندی';
|
||||
}
|
||||
},
|
||||
updateCategory: async (categoryId, data) => {
|
||||
const config = {
|
||||
method: 'put',
|
||||
url: '/api/categories/admin/update/' + categoryId,
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
const response = await axios.patch('/api/categories/admin/update/' + categoryId, data);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
return err.response?.data?.message || 'خطا در ویرایش دستهبندی';
|
||||
}
|
||||
},
|
||||
deleteCategory: async (categoryId) => {
|
||||
try {
|
||||
await axios.delete('/api/categories/admin/delete/' + categoryId);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response?.data?.message || 'خطا در حذف دستهبندی';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,46 +2,46 @@ import axios from 'axios';
|
||||
|
||||
export const FeatureService = {
|
||||
getAllFeatures: async () => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/categories/admin/get-all',
|
||||
withCredentials: true
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
const response = await axios.get('/api/features/admin/get-all');
|
||||
return response.data?.data || [];
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error('Error in getAllFeatures:', err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getFeaturesByCategory: async (categoryId) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/features/admin/get-by-category/${categoryId}`);
|
||||
return response.data?.data || [];
|
||||
} catch (err) {
|
||||
console.error('Error in getFeaturesByCategory:', err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
createFeature: async (data) => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
url: '/api/features/admin/create',
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
await axios.post('/api/features/admin/create', data);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
return err.response?.data?.message || 'خطا در ساخت ویژگی';
|
||||
}
|
||||
},
|
||||
updateFeature: async (featureId, data) => {
|
||||
const config = {
|
||||
method: 'put',
|
||||
url: '/api/features/admin/update/' + featureId,
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
await axios.put('/api/features/admin/update/' + featureId, data);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
return err.response?.data?.message || 'خطا در ویرایش ویژگی';
|
||||
}
|
||||
},
|
||||
deleteFeature: async (featureId) => {
|
||||
try {
|
||||
await axios.delete('/api/features/admin/delete/' + featureId);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response?.data?.message || 'خطا در حذف ویژگی';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import axios from 'axios';
|
||||
|
||||
export const PropertyService = {
|
||||
_getAuthHeader: () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
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 {
|
||||
Authorization: `Bearer ${bmToken}`
|
||||
};
|
||||
return bmToken && bmToken !== 'undefined' && bmToken !== 'null'
|
||||
? { Authorization: `Bearer ${bmToken}` }
|
||||
: {};
|
||||
},
|
||||
|
||||
getProperty: async (propertyId) => {
|
||||
|
||||
@@ -2,14 +2,13 @@ import axios from 'axios';
|
||||
|
||||
export const SettingService = {
|
||||
_getAuthHeader: () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
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 {
|
||||
Authorization: `Bearer ${bmToken}`
|
||||
};
|
||||
return bmToken && bmToken !== 'undefined' && bmToken !== 'null'
|
||||
? { Authorization: `Bearer ${bmToken}` }
|
||||
: {};
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,14 +2,13 @@ import axios from 'axios';
|
||||
|
||||
export const TransactionService = {
|
||||
_getAuthHeader: () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
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 {
|
||||
Authorization: `Bearer ${bmToken}`
|
||||
};
|
||||
return bmToken && bmToken !== 'undefined' && bmToken !== 'null'
|
||||
? { Authorization: `Bearer ${bmToken}` }
|
||||
: {};
|
||||
},
|
||||
/**
|
||||
* Get a single transaction by ID
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const getAuthHeader = () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
return { Authorization: `Bearer ${bmToken}` };
|
||||
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 WarehouseItemService = {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const getAuthHeader = () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
return { Authorization: `Bearer ${bmToken}` };
|
||||
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 WarehouseService = {
|
||||
|
||||
@@ -3,17 +3,30 @@ import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { CategoryService } from '@/service/CategoryService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
const toast = useToast();
|
||||
import axios from 'axios';
|
||||
|
||||
const dataLoading = ref(true);
|
||||
const dt = ref();
|
||||
const categories = ref([]);
|
||||
const categoryDialog = ref(false);
|
||||
const deleteCategoryDialog = ref(false);
|
||||
const deleteSelectedDialog = ref(false);
|
||||
const category = ref({});
|
||||
const selectedCategories = ref([]);
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
||||
});
|
||||
const submitted = ref(false);
|
||||
|
||||
const loadData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
const data = await CategoryService.getAllCategories();
|
||||
categories.value = Array.isArray(data) ? data : [];
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات دستهبندیها', life: 3000 });
|
||||
categories.value = [];
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
@@ -23,19 +36,6 @@ onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const dt = ref();
|
||||
const categories = ref();
|
||||
const categoryDialog = ref(false);
|
||||
const deleteCategoryDialog = ref(false);
|
||||
const deleteProductsDialog = ref(false);
|
||||
const category = ref({});
|
||||
const selectedCategories = ref();
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
||||
});
|
||||
const submitted = ref(false);
|
||||
|
||||
|
||||
function openNew() {
|
||||
category.value = {};
|
||||
submitted.value = false;
|
||||
@@ -49,24 +49,25 @@ function hideDialog() {
|
||||
|
||||
async function saveCategory() {
|
||||
submitted.value = true;
|
||||
let status = 'failed';
|
||||
if (!category.value?.nativeName || !category.value?.englishName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (category?.value._id?.trim()) {
|
||||
let status = 'failed';
|
||||
if (category.value._id?.trim()) {
|
||||
status = await CategoryService.updateCategory(category.value._id, category.value);
|
||||
} else {
|
||||
status = await CategoryService.createCategory(category.value);
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'دسته بندی جدید ساخته شد.', life: 3000 });
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'دستهبندی با موفقیت ذخیره شد.', life: 3000 });
|
||||
categoryDialog.value = false;
|
||||
category.value = {};
|
||||
await loadData();
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
|
||||
categoryDialog.value = false;
|
||||
category.value = {};
|
||||
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
}
|
||||
|
||||
function editCategory(cat) {
|
||||
@@ -74,42 +75,47 @@ function editCategory(cat) {
|
||||
categoryDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteCategory(prod) {
|
||||
category.value = prod;
|
||||
function confirmDeleteCategory(cat) {
|
||||
category.value = cat;
|
||||
deleteCategoryDialog.value = true;
|
||||
}
|
||||
|
||||
async function deleteCategory() {
|
||||
const config = {
|
||||
method: 'delete',
|
||||
url: '/api/categories/admin/delete/' + category.value._id,
|
||||
data: category.value,
|
||||
withCredentials: true
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'دسته بندی جدید حذف شد.', life: 3000 });
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: err.response.data.message, life: 3000 });
|
||||
if (!category.value?._id) return;
|
||||
const status = await CategoryService.deleteCategory(category.value._id);
|
||||
if (status === 'success') {
|
||||
toast.add({ severity: 'success', summary: 'موفقیتآمیز', detail: 'دستهبندی با موفقیت حذف شد.', life: 3000 });
|
||||
deleteCategoryDialog.value = false;
|
||||
category.value = {};
|
||||
await loadData();
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
deleteCategoryDialog.value = false;
|
||||
category.value = {};
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
dt.value.exportCSV();
|
||||
}
|
||||
|
||||
function confirmDeleteSelected() {
|
||||
deleteProductsDialog.value = true;
|
||||
deleteSelectedDialog.value = true;
|
||||
}
|
||||
|
||||
function deleteSelectedCategories() {
|
||||
categories.value = categories.value.filter((val) => !selectedCategories.value.includes(val));
|
||||
deleteProductsDialog.value = false;
|
||||
selectedCategories.value = null;
|
||||
toast.add({ severity: 'success', summary: 'موفقیت آمیز', detail: 'دسته بندی مورد نظر حذف شد.', life: 3000 });
|
||||
async function deleteSelectedCategories() {
|
||||
if (!selectedCategories.value || !selectedCategories.value.length) return;
|
||||
|
||||
let successCount = 0;
|
||||
for (const cat of selectedCategories.value) {
|
||||
if (cat._id) {
|
||||
const res = await CategoryService.deleteCategory(cat._id);
|
||||
if (res === 'success') successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
toast.add({ severity: 'success', summary: 'موفقیتآمیز', detail: `${successCount} دستهبندی حذف شد.`, life: 3000 });
|
||||
deleteSelectedDialog.value = false;
|
||||
selectedCategories.value = [];
|
||||
await loadData();
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
dt.value?.exportCSV();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -119,11 +125,13 @@ function deleteSelectedCategories() {
|
||||
<div class="card">
|
||||
<Toolbar class="mb-6">
|
||||
<template #start>
|
||||
<Button label="ساخت دسته بندی جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="openNew" />
|
||||
<Button label="حذف دسته بندی" icon="pi pi-trash" severity="secondary" @click="confirmDeleteSelected" :disabled="!selectedCategories || !selectedCategories.length" />
|
||||
<Button label="ساخت دسته بندی جدید" icon="pi pi-plus" severity="primary" class="mr-2 mx-3" @click="openNew" />
|
||||
<Button label="حذف موارد انتخابی" icon="pi pi-trash" severity="danger" outlined @click="confirmDeleteSelected" :disabled="!selectedCategories || !selectedCategories.length" />
|
||||
</template>
|
||||
|
||||
<template #end> </template>
|
||||
<template #end>
|
||||
<Button label="خروجی CSV" icon="pi pi-upload" severity="secondary" @click="exportCSV" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<DataTable
|
||||
@@ -131,7 +139,7 @@ function deleteSelectedCategories() {
|
||||
v-model:selection="selectedCategories"
|
||||
:value="categories"
|
||||
:loading="dataLoading"
|
||||
dataKey="id"
|
||||
dataKey="_id"
|
||||
:paginator="true"
|
||||
:rows="20"
|
||||
:filters="filters"
|
||||
@@ -153,7 +161,7 @@ function deleteSelectedCategories() {
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<h4 class="m-0">مدیریت دسته بندی ها</h4>
|
||||
<h4 class="m-0 font-bold">مدیریت دسته بندی ها ({{ categories.length }} مورد)</h4>
|
||||
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
@@ -167,10 +175,17 @@ function deleteSelectedCategories() {
|
||||
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
||||
<Column field="nativeName" header="نام فارسی" sortable style="min-width: 16rem"></Column>
|
||||
<Column field="englishName" header="نام انگلیسی" sortable style="min-width: 16rem"></Column>
|
||||
<Column header="تعداد ویژگیها" style="min-width: 10rem">
|
||||
<template #body="slotProps">
|
||||
<span class="px-2.5 py-1 text-xs rounded-full bg-indigo-50 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300 font-medium">
|
||||
{{ slotProps.data.features?.length || 0 }} ویژگی
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :exportable="false" style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<Button icon="pi pi-pencil" outlined rounded class="mr-2 mx-3" @click="editCategory(slotProps.data)" />
|
||||
<Button icon="pi pi-trash" class="mx-3" outlined rounded severity="danger" @click="confirmDeleteCategory(slotProps.data)" />
|
||||
<Button icon="pi pi-pencil" outlined rounded class="mr-2 mx-2" severity="info" title="ویرایش" @click="editCategory(slotProps.data)" />
|
||||
<Button icon="pi pi-trash" class="mx-2" outlined rounded severity="danger" title="حذف" @click="confirmDeleteCategory(slotProps.data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
@@ -185,7 +200,7 @@ function deleteSelectedCategories() {
|
||||
</div>
|
||||
<div>
|
||||
<label for="englishName" class="block font-bold mb-3">نام انگلیسی</label>
|
||||
<InputText id="englishName" v-model.trim="category.englishName" required="true" autofocus :invalid="submitted && !category.englishName" fluid />
|
||||
<InputText id="englishName" v-model.trim="category.englishName" required="true" :invalid="submitted && !category.englishName" fluid />
|
||||
<small v-if="submitted && !category.englishName" class="text-red-500">ورود نام انگلیسی الزامی است.</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,27 +211,27 @@ function deleteSelectedCategories() {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteCategoryDialog" :style="{ width: '450px' }" header="تایید نهایی" :modal="true">
|
||||
<Dialog v-model:visible="deleteCategoryDialog" :style="{ width: '450px' }" header="تایید حذف" :modal="true">
|
||||
<div class="flex items-center gap-4">
|
||||
<i class="pi pi-exclamation-triangle !text-3xl" />
|
||||
<i class="pi pi-exclamation-triangle !text-3xl text-red-500" />
|
||||
<span v-if="category"
|
||||
>آیا از حذف دسته بندی <b>{{ category.nativeName }} </b> مطمئن هستید؟</span
|
||||
>آیا از حذف دسته بندی <b>{{ category.nativeName }}</b> مطمئن هستید؟</span
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="خیر" icon="pi pi-times" text @click="deleteCategoryDialog = false" />
|
||||
<Button label="بله" icon="pi pi-check" @click="deleteCategory" />
|
||||
<Button label="بله، حذف شود" icon="pi pi-check" severity="danger" @click="deleteCategory" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteProductsDialog" :style="{ width: '450px' }" header="Confirm" :modal="true">
|
||||
<Dialog v-model:visible="deleteSelectedDialog" :style="{ width: '450px' }" header="تایید حذف گروهی" :modal="true">
|
||||
<div class="flex items-center gap-4">
|
||||
<i class="pi pi-exclamation-triangle !text-3xl" />
|
||||
<span v-if="product">Are you sure you want to delete the selected products?</span>
|
||||
<i class="pi pi-exclamation-triangle !text-3xl text-red-500" />
|
||||
<span>آیا از حذف <b>{{ selectedCategories?.length || 0 }}</b> دستهبندی انتخاب شده اطمینان دارید؟</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="No" icon="pi pi-times" text @click="deleteProductsDialog = false" />
|
||||
<Button label="Yes" icon="pi pi-check" text @click="deleteSelectedCategories" />
|
||||
<Button label="خیر" icon="pi pi-times" text @click="deleteSelectedDialog = false" />
|
||||
<Button label="بله، حذف شوند" icon="pi pi-check" severity="danger" @click="deleteSelectedCategories" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,148 +1,300 @@
|
||||
<script setup>
|
||||
import { onBeforeMount, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { CategoryService } from '@/service/CategoryService';
|
||||
import { Tabs, TabList, Tab } from 'primevue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import axios from 'axios';
|
||||
import { FeatureService } from '@/service/FeatureService';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'primevue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
const toast = useToast();
|
||||
const selectedTab = ref('');
|
||||
const selectedFeature = ref();
|
||||
const submitted = ref(false);
|
||||
const feature = ref({});
|
||||
const categories = ref([]);
|
||||
const dataLoading = ref(true);
|
||||
const featureDialog = ref(false);
|
||||
const deleteFeatureDialog = ref(false);
|
||||
const featureToDelete = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
const featureTypes = [
|
||||
{ value: 'string', name: 'متن (String)' },
|
||||
{ value: 'number', name: 'عدد (Number)' },
|
||||
{ value: 'boolean', name: 'بله یا خیر (Boolean)' },
|
||||
{ value: 'date', name: 'تاریخ (Date)' }
|
||||
];
|
||||
|
||||
const loadCategories = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
if (categories.value.length > 0 && !selectedTab.value) {
|
||||
const data = await CategoryService.getAllCategories();
|
||||
categories.value = Array.isArray(data) ? data : [];
|
||||
if (categories.value.length > 0 && (!selectedTab.value || !categories.value.some(c => c._id === selectedTab.value))) {
|
||||
selectedTab.value = categories.value[0]._id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error('Error loading categories:', e);
|
||||
categories.value = [];
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCategories();
|
||||
});
|
||||
|
||||
const currentCategory = computed(() => {
|
||||
return categories.value.find((c) => c._id === selectedTab.value) || null;
|
||||
});
|
||||
|
||||
const featureType = [
|
||||
{
|
||||
value: 'string',
|
||||
name: 'متن'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
name: 'بله یا خیر'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
name: 'عدد'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
name: 'تاریخ'
|
||||
}
|
||||
];
|
||||
function hideDialog() {
|
||||
featureDialog.value = false;
|
||||
submitted.value = false;
|
||||
feature.value = {};
|
||||
}
|
||||
|
||||
function openNew(categoryId) {
|
||||
feature.value = {};
|
||||
feature.value.category = categoryId;
|
||||
feature.value = {
|
||||
category: categoryId || selectedTab.value,
|
||||
featureType: 'string',
|
||||
isFilterable: true
|
||||
};
|
||||
submitted.value = false;
|
||||
featureDialog.value = true;
|
||||
}
|
||||
|
||||
const editFeature = (event, categoryId) => {
|
||||
feature.value = event.value;
|
||||
const editFeature = (feat, categoryId) => {
|
||||
feature.value = { ...feat, category: categoryId || selectedTab.value };
|
||||
submitted.value = false;
|
||||
featureDialog.value = true;
|
||||
};
|
||||
|
||||
const confirmDelete = (feat) => {
|
||||
featureToDelete.value = feat;
|
||||
deleteFeatureDialog.value = true;
|
||||
};
|
||||
|
||||
const deleteFeature = async () => {
|
||||
if (!featureToDelete.value?._id) return;
|
||||
try {
|
||||
const status = await FeatureService.deleteFeature(featureToDelete.value._id);
|
||||
if (status === 'success') {
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'ویژگی با موفقیت حذف شد.', life: 3000 });
|
||||
deleteFeatureDialog.value = false;
|
||||
featureToDelete.value = null;
|
||||
await loadCategories();
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در حذف ویژگی', life: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
const saveFeature = async () => {
|
||||
submitted.value = true;
|
||||
if (!feature.value.nativeName || !feature.value.englishName || !feature.value.featureType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let status = 'failed';
|
||||
if (feature?.value._id?.trim()) {
|
||||
status = await FeatureService.updateFeature(feature.value._id, feature.value);
|
||||
} else {
|
||||
status = await FeatureService.createFeature(feature.value);
|
||||
try {
|
||||
if (feature.value._id?.trim()) {
|
||||
status = await FeatureService.updateFeature(feature.value._id, feature.value);
|
||||
} else {
|
||||
status = await FeatureService.createFeature(feature.value);
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'ویژگی با موفقیت ذخیره شد.', life: 3000 });
|
||||
featureDialog.value = false;
|
||||
feature.value = {};
|
||||
await loadCategories();
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در ذخیره ویژگی', life: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'success') {
|
||||
toast.add({ severity: 'success', summary: 'موفق', detail: 'ویژگی جدید ساخته شد.', life: 3000 });
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
|
||||
featureDialog.value = false;
|
||||
feature.value = {};
|
||||
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
const getFeatureTypeLabel = (type) => {
|
||||
const found = featureTypes.find((t) => t.value === type);
|
||||
return found ? found.name : (type || '-');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div className="card">
|
||||
<div class="font-semibold text-xl mb-4">ویژگی دسته بندی ها</div>
|
||||
|
||||
<template>
|
||||
<div class="card p-4">
|
||||
<Toast></Toast>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold m-0 text-slate-800 dark:text-slate-100">ویژگی دستهبندیها</h3>
|
||||
<p class="text-xs text-slate-500 mt-1 mb-0">تعریف و مدیریت فیلدهای اختصاصی برای هر دستهبندی</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="selectedTab"
|
||||
label="افزودن ویژگی جدید"
|
||||
icon="pi pi-plus"
|
||||
severity="primary"
|
||||
@click="openNew(selectedTab)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Skeleton Loading -->
|
||||
<div v-if="dataLoading" class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<Skeleton v-for="i in 4" :key="i" width="8rem" height="2.5rem" class="rounded-xl" />
|
||||
</div>
|
||||
<div class="w-1/3 my-4 space-y-2">
|
||||
<Skeleton width="100%" height="15rem" class="rounded-xl" />
|
||||
<Skeleton width="6rem" height="2.5rem" class="rounded-lg" />
|
||||
<div class="w-full my-4 space-y-3">
|
||||
<Skeleton width="100%" height="4rem" class="rounded-xl" />
|
||||
<Skeleton width="100%" height="4rem" class="rounded-xl" />
|
||||
<Skeleton width="100%" height="4rem" class="rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-else :value="selectedTab" :pt="{}">
|
||||
<TabList>
|
||||
<Tab v-for="category in categories" :key="category._id" :value="category._id">
|
||||
{{ category.nativeName }}
|
||||
<!-- Empty state if no categories exist -->
|
||||
<div v-else-if="!categories.length" class="p-8 text-center text-slate-500">
|
||||
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
|
||||
<p>هیچ دستهبندیای یافت نشد. ابتدا یک دستهبندی ایجاد کنید.</p>
|
||||
</div>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<Tabs v-else :value="selectedTab" @update:value="selectedTab = $event">
|
||||
<TabList class="mb-4">
|
||||
<Tab v-for="cat in categories" :key="cat._id" :value="cat._id">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-tag text-xs"></i>
|
||||
<span>{{ cat.nativeName }}</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
|
||||
{{ cat.features?.length || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel v-for="category in categories" :key="category._id + '_content'" :value="category._id">
|
||||
<p>لیست ویژگی های {{ category.nativeName }}</p>
|
||||
<div class="w-1/3 my-3">
|
||||
<Listbox filter :options="category.features" option-value="_id" option-label="nativeName" @item-dblclick="editFeature($event, category._id)" emptyMessage="موردی برای نمایش وجود ندارد."></Listbox>
|
||||
<Button class="my-3" :categoryId="category._id" @click="openNew(category._id)">افزودن</Button>
|
||||
<TabPanel v-for="cat in categories" :key="cat._id + '_content'" :value="cat._id">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200">
|
||||
لیست ویژگیهای «{{ cat.nativeName }}» ({{ cat.features?.length || 0 }} مورد)
|
||||
</span>
|
||||
<Button
|
||||
label="افزودن ویژگی"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="openNew(cat._id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Features Table / List -->
|
||||
<div v-if="cat.features && cat.features.length > 0" class="overflow-x-auto rounded-xl border border-surface-200 dark:border-surface-700">
|
||||
<table class="w-full text-right text-sm">
|
||||
<thead class="bg-surface-50 dark:bg-surface-800/60 text-surface-700 dark:text-surface-200 font-semibold border-b border-surface-200 dark:border-surface-700">
|
||||
<tr>
|
||||
<th class="p-3">#</th>
|
||||
<th class="p-3">نام فارسی</th>
|
||||
<th class="p-3">نام انگلیسی</th>
|
||||
<th class="p-3">نوع ویژگی</th>
|
||||
<th class="p-3">واحد</th>
|
||||
<th class="p-3">تگ دستهبندی</th>
|
||||
<th class="p-3 text-center">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-surface-100 dark:divide-surface-800">
|
||||
<tr v-for="(feat, idx) in cat.features" :key="feat._id || idx" class="hover:bg-surface-50/50 dark:hover:bg-surface-800/40 transition-colors">
|
||||
<td class="p-3 font-mono text-xs text-slate-400">{{ idx + 1 }}</td>
|
||||
<td class="p-3 font-semibold text-slate-800 dark:text-slate-100">{{ feat.nativeName }}</td>
|
||||
<td class="p-3 font-mono text-xs text-slate-600 dark:text-slate-300">{{ feat.englishName }}</td>
|
||||
<td class="p-3">
|
||||
<span class="px-2 py-0.5 text-xs rounded-full bg-blue-50 dark:bg-blue-950/60 text-blue-700 dark:text-blue-300 font-medium">
|
||||
{{ getFeatureTypeLabel(feat.featureType) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-3 text-xs text-slate-500">{{ feat.unit || '-' }}</td>
|
||||
<td class="p-3 text-xs text-slate-500">{{ feat.tag || '-' }}</td>
|
||||
<td class="p-3 text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
size="small"
|
||||
outlined
|
||||
rounded
|
||||
severity="info"
|
||||
title="ویرایش"
|
||||
@click="editFeature(feat, cat._id)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
size="small"
|
||||
outlined
|
||||
rounded
|
||||
severity="danger"
|
||||
title="حذف"
|
||||
@click="confirmDelete(feat)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-else class="p-8 text-center text-slate-400 bg-surface-50 dark:bg-surface-800/30 rounded-xl border border-dashed border-surface-200 dark:border-surface-700">
|
||||
<i class="pi pi-tags text-3xl mb-2 text-slate-300 dark:text-slate-600"></i>
|
||||
<p class="mb-3 text-sm">هیچ ویژگیای برای این دستهبندی تعریف نشده است.</p>
|
||||
<Button label="ساخت اولین ویژگی" icon="pi pi-plus" size="small" @click="openNew(cat._id)" />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Dialog v-model:visible="featureDialog" :style="{ width: '450px' }" header="ساخت ویژگی" :modal="true">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<!-- Feature Create / Edit Dialog -->
|
||||
<Dialog v-model:visible="featureDialog" :style="{ width: '480px' }" :header="feature._id ? 'ویرایش ویژگی' : 'ساخت ویژگی جدید'" :modal="true">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label for="nativeName" class="block font-bold mb-3">نام فارسی</label>
|
||||
<InputText id="nativeName" v-model.trim="feature.nativeName" required="true" autofocus :invalid="submitted && !feature.nativeName" fluid />
|
||||
<label for="nativeName" class="block font-semibold text-sm mb-1.5">نام فارسی <span class="text-red-500">*</span></label>
|
||||
<InputText id="nativeName" v-model.trim="feature.nativeName" placeholder="مثلا: متراژ، تعداد خواب" required autofocus :invalid="submitted && !feature.nativeName" fluid />
|
||||
<small v-if="submitted && !feature.nativeName" class="text-red-500">ورود نام فارسی الزامی است.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="englishName" class="block font-bold mb-3">نام انگلیسی</label>
|
||||
<InputText id="englishName" v-model.trim="feature.englishName" required="true" autofocus :invalid="submitted && !feature.englishName" fluid />
|
||||
<label for="englishName" class="block font-semibold text-sm mb-1.5">نام انگلیسی <span class="text-red-500">*</span></label>
|
||||
<InputText id="englishName" v-model.trim="feature.englishName" placeholder="مثلا: area, roomCount" required :invalid="submitted && !feature.englishName" fluid />
|
||||
<small v-if="submitted && !feature.englishName" class="text-red-500">ورود نام انگلیسی الزامی است.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit" class="block font-bold mb-3">واحد</label>
|
||||
<InputText id="unit" v-model.trim="feature.unit" required="true" autofocus :invalid="submitted && !feature.unit" fluid />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="featureType" class="block font-bold mb-3">نوع ویژگی</label>
|
||||
<Select id="featureType" option-label="name" option-value="value" :options="featureType" v-model.trim="feature.featureType" autofocus :invalid="submitted && !feature.featureType" fluid />
|
||||
<label for="featureType" class="block font-semibold text-sm mb-1.5">نوع دادهای <span class="text-red-500">*</span></label>
|
||||
<Select id="featureType" option-label="name" option-value="value" :options="featureTypes" v-model="feature.featureType" :invalid="submitted && !feature.featureType" fluid />
|
||||
<small v-if="submitted && !feature.featureType" class="text-red-500">ورود نوع ویژگی الزامی است.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit" class="block font-semibold text-sm mb-1.5">واحد اندازهگیری (اختیاری)</label>
|
||||
<InputText id="unit" v-model.trim="feature.unit" placeholder="مثلا: متر مربع، دستگاه" fluid />
|
||||
</div>
|
||||
<div>
|
||||
<label for="tag" class="block font-semibold text-sm mb-1.5">تگ گروهی (اختیاری)</label>
|
||||
<InputText id="tag" v-model.trim="feature.tag" placeholder="مثلا: مشخصات کلی، امکانات" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="لغو" icon="pi pi-times" text @click="hideDialog" />
|
||||
<Button label="ذخیره" icon="pi pi-check" @click="saveFeature" />
|
||||
<Button label="ذخیره" icon="pi pi-check" severity="primary" @click="saveFeature" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<Dialog v-model:visible="deleteFeatureDialog" :style="{ width: '420px' }" header="تایید حذف ویژگی" :modal="true">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="pi pi-exclamation-triangle text-2xl text-red-500" />
|
||||
<span v-if="featureToDelete">
|
||||
آیا از حذف ویژگی <b>«{{ featureToDelete.nativeName }}»</b> مطمئن هستید؟
|
||||
</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="انصراف" icon="pi pi-times" text @click="deleteFeatureDialog = false" />
|
||||
<Button label="حذف شود" icon="pi pi-trash" severity="danger" @click="deleteFeature" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import { onMounted, ref, watch, computed, nextTick } from 'vue';
|
||||
import { PropertyService } from '@/service/PropertyService';
|
||||
import { SettingService } from '@/service/SettingService';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
@@ -33,6 +33,7 @@ const dt = ref();
|
||||
const properties = ref([]);
|
||||
const selectedProperties = ref();
|
||||
const selectedPropertyId = ref(null);
|
||||
const hoveredPropertyId = ref(null);
|
||||
const mapRef = ref(null);
|
||||
const splitMapRef = ref(null);
|
||||
const propertyCardRefs = ref({});
|
||||
@@ -241,7 +242,11 @@ const onSelectProperty = (propertyId) => {
|
||||
};
|
||||
|
||||
const onCardHover = (propertyId) => {
|
||||
selectedPropertyId.value = propertyId;
|
||||
hoveredPropertyId.value = propertyId;
|
||||
};
|
||||
|
||||
const onCardLeave = () => {
|
||||
hoveredPropertyId.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -467,8 +472,12 @@ const onCardHover = (propertyId) => {
|
||||
:key="prop._id || prop.id"
|
||||
:id="`property-card-${prop._id || prop.id}`"
|
||||
class="property-card"
|
||||
:class="{ 'card-selected': selectedPropertyId === (prop._id || prop.id) }"
|
||||
:class="{
|
||||
'card-selected': selectedPropertyId === (prop._id || prop.id),
|
||||
'card-hovered': hoveredPropertyId === (prop._id || prop.id)
|
||||
}"
|
||||
@mouseenter="onCardHover(prop._id || prop.id)"
|
||||
@mouseleave="onCardLeave"
|
||||
@click="onSelectProperty(prop._id || prop.id)"
|
||||
>
|
||||
<!-- Card Image -->
|
||||
@@ -564,6 +573,7 @@ const onCardHover = (propertyId) => {
|
||||
ref="splitMapRef"
|
||||
:properties="properties"
|
||||
:selectedPropertyId="selectedPropertyId"
|
||||
:hoveredPropertyId="hoveredPropertyId"
|
||||
@select-property="onSelectProperty"
|
||||
@edit-property="editProperty"
|
||||
/>
|
||||
@@ -576,6 +586,7 @@ const onCardHover = (propertyId) => {
|
||||
ref="mapRef"
|
||||
:properties="properties"
|
||||
:selectedPropertyId="selectedPropertyId"
|
||||
:hoveredPropertyId="hoveredPropertyId"
|
||||
@select-property="onSelectProperty"
|
||||
@edit-property="editProperty"
|
||||
/>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from 'leaflet';
|
||||
import { LMap, LTileLayer, LMarker, LPopup, LControlLayers, LControlScale } from '@vue-leaflet/vue-leaflet';
|
||||
@@ -190,6 +190,10 @@ const props = defineProps({
|
||||
selectedPropertyId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
hoveredPropertyId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
@@ -313,12 +317,14 @@ const getCategoryName = (prop) => {
|
||||
* Creates custom Airbnb-style Leaflet pin icon
|
||||
*/
|
||||
const getMarkerIcon = (prop) => {
|
||||
const isSelected = props.selectedPropertyId === (prop._id || prop.id);
|
||||
const propId = prop._id || prop.id;
|
||||
const isSelected = props.selectedPropertyId === propId;
|
||||
const isHovered = props.hoveredPropertyId === propId;
|
||||
const code = prop.propertyCode ? `#${prop.propertyCode}` : '';
|
||||
const cat = getCategoryName(prop) || 'ملک';
|
||||
|
||||
const html = `
|
||||
<div class="airbnb-pin ${isSelected ? 'is-selected' : ''}">
|
||||
<div class="airbnb-pin ${isSelected ? 'is-selected' : ''} ${isHovered ? 'is-hovered' : ''}">
|
||||
<div class="pin-content">
|
||||
<span class="pin-icon">🏠</span>
|
||||
<span class="pin-label">${code || cat}</span>
|
||||
@@ -421,9 +427,19 @@ const toggleFullscreen = () => {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
const onFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
nextTick(() => map.value?.leafletObject?.invalidateSize());
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for selected property changes to zoom to it
|
||||
@@ -628,7 +644,8 @@ defineExpose({
|
||||
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.airbnb-pin:hover {
|
||||
.airbnb-pin:hover,
|
||||
.airbnb-pin.is-hovered {
|
||||
transform: scale(1.15) translateY(-4px);
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
@@ -654,7 +671,8 @@ defineExpose({
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.airbnb-pin:hover .pin-content {
|
||||
.airbnb-pin:hover .pin-content,
|
||||
.airbnb-pin.is-hovered .pin-content {
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.45);
|
||||
|
||||
@@ -16,11 +16,12 @@ const login = async () => {
|
||||
email: email.value,
|
||||
password: password.value
|
||||
});
|
||||
const isSecure = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||
const env = import.meta.env.VITE_ENV;
|
||||
if (env === 'production') {
|
||||
if (env === 'production' && isSecure) {
|
||||
Cookies.set('bm-token', response.data.data.token, { sameSite: 'none', secure: true, expires: 1000, domain: '.mbmanager.ir' });
|
||||
} else {
|
||||
Cookies.set('bm-token', response.data.data.token, { sameSite: 'none', secure: true, expires: 1000 });
|
||||
Cookies.set('bm-token', response.data.data.token, { expires: 1000, secure: isSecure, sameSite: isSecure ? 'none' : 'lax' });
|
||||
}
|
||||
localStorage.setItem('bm-token', response.data.data.token);
|
||||
await router.push('/');
|
||||
@@ -31,17 +32,16 @@ const login = async () => {
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const token = Cookies.get('bm-token');
|
||||
try {
|
||||
const response = await axios.get('/api/users/user/get-self', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
const token = Cookies.get('bm-token') || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : null);
|
||||
if (token) {
|
||||
try {
|
||||
const response = await axios.get('/api/users/user/get-self');
|
||||
if (response.data?.data?.user) {
|
||||
await router.push('/');
|
||||
}
|
||||
});
|
||||
console.log(response);
|
||||
await router.push('/');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user