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