...
This commit is contained in:
+4
-1
@@ -1,4 +1,7 @@
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import axios from 'axios'
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@font-face {
|
||||
font-family: 'Estedad';
|
||||
src: url('./fonts/estedad/variable/Estedad[KSHD,wght].woff2') format('woff2 supports variations'),
|
||||
url('./fonts/estedad/variable/Estedad[KSHD,wght].woff2') format('woff2-variations');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Estedad FD';
|
||||
src: url('./fonts/estedad/variable/Estedad-FD[KSHD,wght].woff2') format('woff2 supports variations'),
|
||||
url('./fonts/estedad/variable/Estedad-FD[KSHD,wght].woff2') format('woff2-variations');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
min-height: 100vh;
|
||||
justify-content: space-between;
|
||||
padding: 6rem 2rem 0 2rem;
|
||||
transition: margin-left var(--layout-section-transition-duration);
|
||||
transition: margin-right var(--layout-section-transition-duration);
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use 'mixins' as *;
|
||||
|
||||
.layout-sidebar {
|
||||
direction: ltr;
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
height: calc(100vh - 8rem);
|
||||
@@ -8,13 +9,14 @@
|
||||
overflow-y: auto;
|
||||
user-select: none;
|
||||
top: 6rem;
|
||||
left: 2rem;
|
||||
right: 2rem;
|
||||
transition:
|
||||
transform var(--layout-section-transition-duration),
|
||||
left var(--layout-section-transition-duration);
|
||||
background-color: var(--surface-overlay);
|
||||
border-radius: var(--content-border-radius);
|
||||
padding: 0.5rem 1.5rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.layout-menu {
|
||||
|
||||
@@ -41,17 +41,17 @@
|
||||
|
||||
&.layout-static {
|
||||
.layout-main-container {
|
||||
margin-left: 22rem;
|
||||
margin-right: 22rem;
|
||||
}
|
||||
|
||||
&.layout-static-inactive {
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
transform: translateX(110%);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
@import "./estedad.css";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
.eng-digits {
|
||||
font-family: "Estedad", sans-serif!important;
|
||||
}
|
||||
|
||||
.farsi-digits {
|
||||
font-family: "Estedad FD", sans-serif!important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Estedad FD", sans-serif!important;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ const presetOptions = ref(Object.keys(presets));
|
||||
|
||||
const menuMode = ref(layoutConfig.menuMode);
|
||||
const menuModeOptions = ref([
|
||||
{ label: 'Static', value: 'static' },
|
||||
{ label: 'Overlay', value: 'overlay' }
|
||||
{ label: 'Static', value: 'static' }
|
||||
// { label: 'Overlay', value: 'overlay' }
|
||||
]);
|
||||
|
||||
const primaryColors = ref([
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<div class="layout-footer">
|
||||
SAKAI by
|
||||
<a href="https://primevue.org" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">PrimeVue</a>
|
||||
</div>
|
||||
<div class="layout-footer">ساخته شده توسط تیم نیلگون</div>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +53,7 @@ function isOutsideClicked(event) {
|
||||
|
||||
return !(sidebarEl.isSameNode(event.target) || sidebarEl.contains(event.target) || topbarEl.isSameNode(event.target) || topbarEl.contains(event.target));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+8
-122
@@ -5,135 +5,21 @@ import AppMenuItem from './AppMenuItem.vue';
|
||||
|
||||
const model = ref([
|
||||
{
|
||||
label: 'Home',
|
||||
items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/' }]
|
||||
label: 'خانه',
|
||||
items: [{ label: 'داشبورد', icon: 'pi pi-fw pi-home', to: '/' }]
|
||||
},
|
||||
{
|
||||
label: 'UI Components',
|
||||
label: 'مدیریت اموال',
|
||||
items: [
|
||||
{ label: 'Form Layout', icon: 'pi pi-fw pi-id-card', to: '/uikit/formlayout' },
|
||||
{ label: 'Input', icon: 'pi pi-fw pi-check-square', to: '/uikit/input' },
|
||||
{ label: 'Button', icon: 'pi pi-fw pi-mobile', to: '/uikit/button', class: 'rotated-icon' },
|
||||
{ label: 'Table', icon: 'pi pi-fw pi-table', to: '/uikit/table' },
|
||||
{ label: 'List', icon: 'pi pi-fw pi-list', to: '/uikit/list' },
|
||||
{ label: 'Tree', icon: 'pi pi-fw pi-share-alt', to: '/uikit/tree' },
|
||||
{ label: 'Panel', icon: 'pi pi-fw pi-tablet', to: '/uikit/panel' },
|
||||
{ label: 'Overlay', icon: 'pi pi-fw pi-clone', to: '/uikit/overlay' },
|
||||
{ label: 'Media', icon: 'pi pi-fw pi-image', to: '/uikit/media' },
|
||||
{ label: 'Menu', icon: 'pi pi-fw pi-bars', to: '/uikit/menu' },
|
||||
{ label: 'Message', icon: 'pi pi-fw pi-comment', to: '/uikit/message' },
|
||||
{ label: 'File', icon: 'pi pi-fw pi-file', to: '/uikit/file' },
|
||||
{ label: 'Chart', icon: 'pi pi-fw pi-chart-bar', to: '/uikit/charts' },
|
||||
{ label: 'Timeline', icon: 'pi pi-fw pi-calendar', to: '/uikit/timeline' },
|
||||
{ label: 'Misc', icon: 'pi pi-fw pi-circle', to: '/uikit/misc' }
|
||||
{ label: 'ایجاد اموال', icon: 'pi pi-fw pi-file-plus', to: '/belonging/create-belonging' },
|
||||
{ label: 'لیست اموال', icon: 'pi pi-fw pi-file', to: '/belonging/list-belongings' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Pages',
|
||||
icon: 'pi pi-fw pi-briefcase',
|
||||
to: '/pages',
|
||||
label: 'دسته بندی اموال',
|
||||
items: [
|
||||
{
|
||||
label: 'Landing',
|
||||
icon: 'pi pi-fw pi-globe',
|
||||
to: '/landing'
|
||||
},
|
||||
{
|
||||
label: 'Auth',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
items: [
|
||||
{
|
||||
label: 'Login',
|
||||
icon: 'pi pi-fw pi-sign-in',
|
||||
to: '/auth/login'
|
||||
},
|
||||
{
|
||||
label: 'Error',
|
||||
icon: 'pi pi-fw pi-times-circle',
|
||||
to: '/auth/error'
|
||||
},
|
||||
{
|
||||
label: 'Access Denied',
|
||||
icon: 'pi pi-fw pi-lock',
|
||||
to: '/auth/access'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Crud',
|
||||
icon: 'pi pi-fw pi-pencil',
|
||||
to: '/pages/crud'
|
||||
},
|
||||
{
|
||||
label: 'Not Found',
|
||||
icon: 'pi pi-fw pi-exclamation-circle',
|
||||
to: '/pages/notfound'
|
||||
},
|
||||
{
|
||||
label: 'Empty',
|
||||
icon: 'pi pi-fw pi-circle-off',
|
||||
to: '/pages/empty'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Hierarchy',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1.1',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [
|
||||
{ label: 'Submenu 1.1.1', icon: 'pi pi-fw pi-bookmark' },
|
||||
{ label: 'Submenu 1.1.2', icon: 'pi pi-fw pi-bookmark' },
|
||||
{ label: 'Submenu 1.1.3', icon: 'pi pi-fw pi-bookmark' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 1.2',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [{ label: 'Submenu 1.2.1', icon: 'pi pi-fw pi-bookmark' }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 2',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 2.1',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [
|
||||
{ label: 'Submenu 2.1.1', icon: 'pi pi-fw pi-bookmark' },
|
||||
{ label: 'Submenu 2.1.2', icon: 'pi pi-fw pi-bookmark' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 2.2',
|
||||
icon: 'pi pi-fw pi-bookmark',
|
||||
items: [{ label: 'Submenu 2.2.1', icon: 'pi pi-fw pi-bookmark' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Get Started',
|
||||
items: [
|
||||
{
|
||||
label: 'Documentation',
|
||||
icon: 'pi pi-fw pi-book',
|
||||
to: '/documentation'
|
||||
},
|
||||
{
|
||||
label: 'View Source',
|
||||
icon: 'pi pi-fw pi-github',
|
||||
url: 'https://github.com/primefaces/sakai-vue',
|
||||
target: '_blank'
|
||||
}
|
||||
{ label: 'لیست دسته بندی ها', icon: 'pi pi-fw pi-th-large', to: '/categories/list-categories' },
|
||||
{ label: 'ویژگی دسته بندی ها', icon: 'pi pi-fw pi-sitemap', to: '/features/list-features' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { onBeforeMount, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -72,12 +73,14 @@ function checkActiveRoute(item) {
|
||||
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActiveMenu }">
|
||||
<div v-if="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
|
||||
<a v-if="(!item.to || item.items) && item.visible !== false" :href="item.url" @click="itemClick($event, item, index)" :class="item.class" :target="item.target" tabindex="0">
|
||||
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||
<i :class="item.icon" class="layout-menuitem-icon mx-1"></i>
|
||||
<font-awesome-icon :icon="item.faIcon" />
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||
</a>
|
||||
<router-link v-if="item.to && !item.items && item.visible !== false" @click="itemClick($event, item, index)" :class="[item.class, { 'active-route': checkActiveRoute(item) }]" tabindex="0" :to="item.to">
|
||||
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||
<i :class="item.icon" class="layout-menuitem-icon mx-1"></i>
|
||||
<font-awesome-icon :icon="item.faIcon" />
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||
</router-link>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup>
|
||||
import AppMenu from './AppMenu.vue';
|
||||
import { ScrollPanel } from 'primevue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-sidebar">
|
||||
<app-menu></app-menu>
|
||||
<div style="direction: rtl">
|
||||
<app-menu></app-menu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import AppConfigurator from './AppConfigurator.vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter()
|
||||
const signOut = () => {
|
||||
Cookies.set('bm-token', '')
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
</script>
|
||||
@@ -30,7 +37,7 @@ const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<span>SAKAI</span>
|
||||
<span>نیلگون</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +79,10 @@ const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Profile</span>
|
||||
</button>
|
||||
<button type="button" class="layout-topbar-action" @click="signOut">
|
||||
<i class="pi pi-power-off"></i>
|
||||
<span>Profile</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ const layoutState = reactive({
|
||||
configSidebarVisible: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
activeMenuItem: null
|
||||
activeMenuItem: null,
|
||||
});
|
||||
|
||||
export function useLayout() {
|
||||
|
||||
+10
-1
@@ -1,15 +1,21 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import Aura from '@primevue/themes/aura';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
import * as VueCookies from 'vue-cookies';
|
||||
|
||||
import '@/assets/styles.scss';
|
||||
import '@/assets/tailwind.css';
|
||||
|
||||
library.add(fas);
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
@@ -23,5 +29,8 @@ app.use(PrimeVue, {
|
||||
});
|
||||
app.use(ToastService);
|
||||
app.use(ConfirmationService);
|
||||
app.use(VueCookies, { expires: '1000d', sameSite: 'None', secure: true });
|
||||
|
||||
app.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const loginGuard = async (to, from, next) => {
|
||||
const token = Cookies.get('bm-token');
|
||||
|
||||
if (!token) {
|
||||
next('/auth/login'); // Redirect to log in if no token
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/users/user/get-self', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
withCredentials: true
|
||||
});
|
||||
next();
|
||||
} catch (error) {
|
||||
console.log('Authentication error:', error);
|
||||
next('/auth/login');
|
||||
}
|
||||
};
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -7,12 +31,33 @@ const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
beforeEnter: loginGuard,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'dashboard',
|
||||
component: () => import('@/views/Dashboard.vue')
|
||||
},
|
||||
{
|
||||
path: '/belonging/create-belonging',
|
||||
name: 'create-belonging',
|
||||
component: () => import('@/views/pages/Belonging/CreateBelonging.vue')
|
||||
},
|
||||
{
|
||||
path: '/belonging/list-belongings',
|
||||
name: 'list-belongings',
|
||||
component: () => import('@/views/pages/Belonging/ListBelongings.vue')
|
||||
},
|
||||
{
|
||||
path: '/categories/list-categories',
|
||||
name: 'list-categories',
|
||||
component: () => import('@/views/pages/Category/ListCategories.vue')
|
||||
},
|
||||
{
|
||||
path: '/features/list-features',
|
||||
name: 'list-features',
|
||||
component: () => import('@/views/pages/Features/ListFeatures.vue')
|
||||
},
|
||||
{
|
||||
path: '/uikit/formlayout',
|
||||
name: 'formlayout',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const BelongingService = {
|
||||
getAllBelongings: async ()=> {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/belongings/admin/get-all',
|
||||
withCredentials: true,
|
||||
}
|
||||
try {
|
||||
const response = await axios(config)
|
||||
return response.data.data
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
createCategory: async (data) => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
url: '/api/categories/admin/create',
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
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);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
createFeature: async (data) => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
url: '/api/features/admin/create',
|
||||
data,
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
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);
|
||||
return 'success';
|
||||
} catch (err) {
|
||||
return err.response.data.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
import { Listbox, InputText } from 'primevue';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { CategoryService } from '@/service/CategoryService';
|
||||
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
||||
import ImageGallery from '@/views/uikit/ImageGallery.vue';
|
||||
const categories = ref([]);
|
||||
const selectedCategory = ref();
|
||||
const belongingName = ref('');
|
||||
const description = ref('');
|
||||
const imageNames = ref([]);
|
||||
const documentNames = ref([]);
|
||||
const mainImageName = ref([]);
|
||||
const tags = ref('');
|
||||
const signedImageUrls = ref([{ url: 'https://upload.hitromall.com/product-images/0ff87b0cc333539a361e007d20aa9685-co3.jpg' }]);
|
||||
onBeforeMount(async () => {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div className="card">
|
||||
<div class="font-semibold text-xl mb-4">ایجاد اموال</div>
|
||||
<p>از طریق این صفحه می توانید اموال جدید را به سوابق اضافه کنید.</p>
|
||||
|
||||
<div class="flex my-6">
|
||||
<div class="w-3/4 ml-3">
|
||||
<div class="">
|
||||
<label> نام دارایی </label>
|
||||
<InputText v-model="belongingName" class="w-full my-2" placeholder="مثلا: 100 عدد کولر گازی" />
|
||||
</div>
|
||||
<div class="my-6">
|
||||
<label> توضیحات </label>
|
||||
<Textarea v-model="description" class="w-full my-2" placeholder="مثلا: در تاریخ 1403/09/11 خریداری شد." rows="6" />
|
||||
</div>
|
||||
<div class="my-6" v-if="signedImageUrls.length">
|
||||
<label> تصاویر </label>
|
||||
<ImageGallery v-model="signedImageUrls" gallery-id="images" />
|
||||
</div>
|
||||
<div class="my-6" v-if="signedImageUrls.length">
|
||||
<label> فایل ها و مدارک </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/4">
|
||||
<div>
|
||||
<label> دسته بندی </label>
|
||||
<Listbox v-model="selectedCategory" :options="categories" optionLabel="nativeName" optionValue="_id" checkmark :highlightOnSelect="false" class="w-full my-2" />
|
||||
</div>
|
||||
<div class="my-6">
|
||||
<label> تصویر اصلی </label>
|
||||
<ImageUploader v-model="mainImageName" :max-files="1" mode="private" delete-endpoint="/api/files/bucket/delete/belonging-images" upload-endpoint="/api/files/bucket/upload-multi-filepond/belonging-images" />
|
||||
</div>
|
||||
<div class="my-8">
|
||||
<label> برچسب ها </label>
|
||||
<Chips v-model="tags" separator="," class="w-full" placeholder="مثلا: زمین، ملک، کیانپارس..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<Button>ذخیره</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,279 @@
|
||||
<script setup>
|
||||
import { ProductService } from '@/service/ProductService';
|
||||
import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { BelongingService } from '@/service/BelongingService';
|
||||
|
||||
onMounted(() => {
|
||||
BelongingService.getAllBelongings().then((data) => (belongings.value = data));
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const dt = ref();
|
||||
const belongings = ref();
|
||||
const productDialog = ref(false);
|
||||
const deleteProductDialog = ref(false);
|
||||
const deleteProductsDialog = ref(false);
|
||||
const product = ref({});
|
||||
const selectedBelongings = ref();
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
||||
});
|
||||
const submitted = ref(false);
|
||||
const statuses = ref([
|
||||
{ label: 'INSTOCK', value: 'instock' },
|
||||
{ label: 'LOWSTOCK', value: 'lowstock' },
|
||||
{ label: 'OUTOFSTOCK', value: 'outofstock' }
|
||||
]);
|
||||
|
||||
function formatCurrency(value) {
|
||||
if (value) return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
||||
return;
|
||||
}
|
||||
|
||||
function openNew() {
|
||||
product.value = {};
|
||||
submitted.value = false;
|
||||
productDialog.value = true;
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
productDialog.value = false;
|
||||
submitted.value = false;
|
||||
}
|
||||
|
||||
function saveProduct() {
|
||||
submitted.value = true;
|
||||
|
||||
if (product?.value.name?.trim()) {
|
||||
if (product.value.id) {
|
||||
product.value.inventoryStatus = product.value.inventoryStatus.value ? product.value.inventoryStatus.value : product.value.inventoryStatus;
|
||||
products.value[findIndexById(product.value.id)] = product.value;
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Product Updated', life: 3000 });
|
||||
} else {
|
||||
product.value.id = createId();
|
||||
product.value.code = createId();
|
||||
product.value.image = 'product-placeholder.svg';
|
||||
product.value.inventoryStatus = product.value.inventoryStatus ? product.value.inventoryStatus.value : 'INSTOCK';
|
||||
products.value.push(product.value);
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Product Created', life: 3000 });
|
||||
}
|
||||
|
||||
productDialog.value = false;
|
||||
product.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
function editProduct(prod) {
|
||||
product.value = { ...prod };
|
||||
productDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteProduct(prod) {
|
||||
product.value = prod;
|
||||
deleteProductDialog.value = true;
|
||||
}
|
||||
|
||||
function deleteProduct() {
|
||||
products.value = products.value.filter((val) => val.id !== product.value.id);
|
||||
deleteProductDialog.value = false;
|
||||
product.value = {};
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Product Deleted', life: 3000 });
|
||||
}
|
||||
|
||||
function findIndexById(id) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < products.value.length; i++) {
|
||||
if (products.value[i].id === id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function createId() {
|
||||
let id = '';
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
dt.value.exportCSV();
|
||||
}
|
||||
|
||||
function confirmDeleteSelected() {
|
||||
deleteProductsDialog.value = true;
|
||||
}
|
||||
|
||||
function deleteSelectedBelongings() {
|
||||
products.value = products.value.filter((val) => !selectedBelongings.value.includes(val));
|
||||
deleteProductsDialog.value = false;
|
||||
selectedBelongings.value = null;
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Products Deleted', life: 3000 });
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
switch (status) {
|
||||
case 'INSTOCK':
|
||||
return 'success';
|
||||
|
||||
case 'LOWSTOCK':
|
||||
return 'warn';
|
||||
|
||||
case 'OUTOFSTOCK':
|
||||
return 'danger';
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<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="!selectedBelongings || !selectedBelongings.length" />
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<DataTable
|
||||
ref="dt"
|
||||
v-model:selection="selectedBelongings"
|
||||
:value="belongings"
|
||||
dataKey="id"
|
||||
:paginator="true"
|
||||
:rows="20"
|
||||
:filters="filters"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
:rowsPerPageOptions="[20, 40, 80]"
|
||||
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دارایی"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<h4 class="m-0">مدیریت اموال</h4>
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
||||
<Column header="تصویر">
|
||||
<template #body="slotProps">
|
||||
<img :src="`https://primefaces.org/cdn/primevue/images/product/${slotProps.data.image}`" :alt="slotProps.data.image" class="rounded" style="width: 64px" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="belongingCode" header="کد" style="min-width: 12rem"></Column>
|
||||
<Column field="name" header="نام" sortable style="min-width: 16rem"></Column>
|
||||
<Column field="category" header="دسته بندی" sortable style="min-width: 10rem">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.nativeName }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column :exportable="false" style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<Button icon="pi pi-pencil" outlined rounded class="mr-2" @click="editProduct(slotProps.data)" />
|
||||
<Button icon="pi pi-trash" outlined rounded severity="danger" @click="confirmDeleteProduct(slotProps.data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="productDialog" :style="{ width: '450px' }" header="Product Details" :modal="true">
|
||||
<div class="flex flex-col gap-6">
|
||||
<img v-if="product.image" :src="`https://primefaces.org/cdn/primevue/images/product/${product.image}`" :alt="product.image" class="block m-auto pb-4" />
|
||||
<div>
|
||||
<label for="name" class="block font-bold mb-3">Name</label>
|
||||
<InputText id="name" v-model.trim="product.name" required="true" autofocus :invalid="submitted && !product.name" fluid />
|
||||
<small v-if="submitted && !product.name" class="text-red-500">Name is required.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block font-bold mb-3">Description</label>
|
||||
<Textarea id="description" v-model="product.description" required="true" rows="3" cols="20" fluid />
|
||||
</div>
|
||||
<div>
|
||||
<label for="inventoryStatus" class="block font-bold mb-3">Inventory Status</label>
|
||||
<Select id="inventoryStatus" v-model="product.inventoryStatus" :options="statuses" optionLabel="label" placeholder="Select a Status" fluid></Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block font-bold mb-4">Category</span>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category1" v-model="product.category" name="category" value="Accessories" />
|
||||
<label for="category1">Accessories</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category2" v-model="product.category" name="category" value="Clothing" />
|
||||
<label for="category2">Clothing</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category3" v-model="product.category" name="category" value="Electronics" />
|
||||
<label for="category3">Electronics</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category4" v-model="product.category" name="category" value="Fitness" />
|
||||
<label for="category4">Fitness</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-6">
|
||||
<label for="price" class="block font-bold mb-3">Price</label>
|
||||
<InputNumber id="price" v-model="product.price" mode="currency" currency="USD" locale="en-US" fluid />
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<label for="quantity" class="block font-bold mb-3">Quantity</label>
|
||||
<InputNumber id="quantity" v-model="product.quantity" integeronly fluid />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancel" icon="pi pi-times" text @click="hideDialog" />
|
||||
<Button label="Save" icon="pi pi-check" @click="saveProduct" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteProductDialog" :style="{ width: '450px' }" header="Confirm" :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 <b>{{ product.name }}</b
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="No" icon="pi pi-times" text @click="deleteProductDialog = false" />
|
||||
<Button label="Yes" icon="pi pi-check" @click="deleteProduct" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteProductsDialog" :style="{ width: '450px' }" header="Confirm" :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>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="No" icon="pi pi-times" text @click="deleteProductsDialog = false" />
|
||||
<Button label="Yes" icon="pi pi-check" text @click="deleteselectedBelongings" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,194 @@
|
||||
<script setup>
|
||||
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';
|
||||
onMounted(() => {
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
});
|
||||
|
||||
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;
|
||||
categoryDialog.value = true;
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
categoryDialog.value = false;
|
||||
submitted.value = false;
|
||||
}
|
||||
|
||||
async function saveCategory() {
|
||||
submitted.value = true;
|
||||
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 });
|
||||
} 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) {
|
||||
category.value = { ...cat };
|
||||
categoryDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteCategory(prod) {
|
||||
category.value = prod;
|
||||
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 });
|
||||
}
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
deleteCategoryDialog.value = false;
|
||||
category.value = {};
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
dt.value.exportCSV();
|
||||
}
|
||||
|
||||
function confirmDeleteSelected() {
|
||||
deleteProductsDialog.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 });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Toast></Toast>
|
||||
<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" />
|
||||
</template>
|
||||
|
||||
<template #end> </template>
|
||||
</Toolbar>
|
||||
|
||||
<DataTable
|
||||
ref="dt"
|
||||
v-model:selection="selectedCategories"
|
||||
:value="categories"
|
||||
dataKey="id"
|
||||
:paginator="true"
|
||||
:rows="20"
|
||||
:filters="filters"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
:rowsPerPageOptions="[20, 40, 80]"
|
||||
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دارایی"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<h4 class="m-0">مدیریت اموال</h4>
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 :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)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="categoryDialog" :style="{ width: '450px' }" header="جزئیات دسته بندی" :modal="true">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<label for="nativeName" class="block font-bold mb-3">نام فارسی</label>
|
||||
<InputText id="nativeName" v-model.trim="category.nativeName" required="true" autofocus :invalid="submitted && !category.nativeName" fluid />
|
||||
<small v-if="submitted && !category.nativeName" class="text-red-500">ورود نام فارسی الزامی است.</small>
|
||||
</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 />
|
||||
<small v-if="submitted && !category.englishName" class="text-red-500">ورود نام انگلیسی الزامی است.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="لغو" icon="pi pi-times" text @click="hideDialog" />
|
||||
<Button label="ذخیره" icon="pi pi-check" @click="saveCategory" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<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" />
|
||||
<span v-if="category"
|
||||
>آیا از حذف دسته بندی <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" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteProductsDialog" :style="{ width: '450px' }" header="Confirm" :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>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="No" icon="pi pi-times" text @click="deleteProductsDialog = false" />
|
||||
<Button label="Yes" icon="pi pi-check" text @click="deleteSelectedCategories" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup>
|
||||
import { onBeforeMount, onMounted, ref } 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';
|
||||
const toast = useToast();
|
||||
const selectedTab = ref('');
|
||||
const selectedFeature = ref();
|
||||
const submitted = ref(false);
|
||||
const feature = ref({});
|
||||
const categories = ref([]);
|
||||
const featureDialog = ref(false);
|
||||
onMounted(async () => {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
});
|
||||
|
||||
const featureType = [
|
||||
{
|
||||
value: 'string',
|
||||
name: 'متن'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
name: 'بله یا خیر'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
name: 'عدد'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
name: 'تاریخ'
|
||||
}
|
||||
];
|
||||
function hideDialog() {
|
||||
featureDialog.value = false;
|
||||
submitted.value = false;
|
||||
}
|
||||
|
||||
function openNew(categoryId) {
|
||||
feature.value = {};
|
||||
feature.value.category = categoryId;
|
||||
submitted.value = false;
|
||||
featureDialog.value = true;
|
||||
}
|
||||
|
||||
const editFeature = (event, categoryId) => {
|
||||
feature.value = event.value;
|
||||
submitted.value = false;
|
||||
featureDialog.value = true;
|
||||
};
|
||||
|
||||
const saveFeature = async () => {
|
||||
submitted.value = true;
|
||||
let status = 'failed';
|
||||
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 });
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
||||
}
|
||||
|
||||
featureDialog.value = false;
|
||||
feature.value = {};
|
||||
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div className="card">
|
||||
<div class="font-semibold text-xl mb-4">ویژگی دسته بندی ها</div>
|
||||
<Tabs :value="selectedTab" :pt="{}">
|
||||
<TabList>
|
||||
<Tab v-for="category in categories" :key="category._id" :value="category._id">
|
||||
{{ category.nativeName }}
|
||||
</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 checkmark :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>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Dialog v-model:visible="featureDialog" :style="{ width: '450px' }" header="ساخت ویژگی" :modal="true">
|
||||
<div class="flex flex-col gap-6">
|
||||
<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 />
|
||||
<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 />
|
||||
<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 />
|
||||
<small v-if="submitted && !feature.featureType" class="text-red-500">ورود نوع ویژگی الزامی است.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="لغو" icon="pi pi-times" text @click="hideDialog" />
|
||||
<Button label="ذخیره" icon="pi pi-check" @click="saveFeature" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,10 +1,45 @@
|
||||
<script setup>
|
||||
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter()
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const checked = ref(false);
|
||||
const loginLoading = ref(false)
|
||||
import Cookies from 'js-cookie';
|
||||
const login = async () => {
|
||||
try {
|
||||
loginLoading.value = true
|
||||
const response = await axios.post('/api/users/user/login-email', {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
})
|
||||
console.log(response)
|
||||
Cookies.set('bm-token', response.data.data.token)
|
||||
localStorage.setItem('bm-token', response.data.data.token)
|
||||
await router.push('/')
|
||||
} catch (error) {
|
||||
console.log('err', error)
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const token = Cookies.get('bm-token')
|
||||
try {
|
||||
const response = await axios.get('/api/users/user/get-self', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log(response)
|
||||
await router.push('/')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,25 +66,21 @@ const checked = ref(false);
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mb-4">Welcome to PrimeLand!</div>
|
||||
<span class="text-muted-color font-medium">Sign in to continue</span>
|
||||
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mb-4">نرم افزار مدیریت اموال نیلگون</div>
|
||||
<span class="text-muted-color font-medium">برای ادامه وارد شوید</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">Email</label>
|
||||
<InputText id="email1" type="text" placeholder="Email address" class="w-full md:w-[30rem] mb-8" v-model="email" />
|
||||
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">ایمیل</label>
|
||||
<InputText id="email1" type="text" placeholder="آدرس ایمیل" class="w-full md:w-[30rem] mb-8" v-model="email" />
|
||||
|
||||
<label for="password1" class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">Password</label>
|
||||
<Password id="password1" v-model="password" placeholder="Password" :toggleMask="true" class="mb-4" fluid :feedback="false"></Password>
|
||||
<label for="password1" class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">رمز عبور</label>
|
||||
<Password id="password1" v-model="password" placeholder="رمز عبور" :toggleMask="true" class="mb-4" fluid :feedback="false"></Password>
|
||||
|
||||
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||
<div class="flex items-center">
|
||||
<Checkbox v-model="checked" id="rememberme1" binary class="mr-2"></Checkbox>
|
||||
<label for="rememberme1">Remember me</label>
|
||||
</div>
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary">Forgot password?</span>
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary">فراموشی رمز عبور</span>
|
||||
</div>
|
||||
<Button label="Sign In" class="w-full" as="router-link" to="/"></Button>
|
||||
<Button :loading="loginLoading" label="ورود" class="w-full" @click="login"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div
|
||||
:id="galleryId"
|
||||
class="pswp-gallery pswp-gallery--single-column my-3 flex min-w-0 flex-row flex-nowrap overflow-x-auto rounded-md bg-brand-dark-3 p-3"
|
||||
>
|
||||
<div v-for="(image, key) in imagesData" :key="key" class="relative">
|
||||
<a
|
||||
:href="image.url"
|
||||
:data-pswp-width="image.width"
|
||||
:data-pswp-height="image.height"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="mx-3 block flex-shrink-0"
|
||||
>
|
||||
<img :src="image.url" alt="" class="max-h-60 rounded-md" :style="{ height: '150px' }" />
|
||||
</a>
|
||||
<button
|
||||
class="absolute right-4 top-2 flex h-6 w-6 items-center justify-center rounded-full text-brand-danger"
|
||||
@click="removeImage(key)"
|
||||
>
|
||||
<font-awesome-icon icon="fa-solid fa-x" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox'
|
||||
import 'photoswipe/style.css'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
function updateImagesData(images) {
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = new Image()
|
||||
img.src = images[i].url
|
||||
img.onload = function () {
|
||||
images[i].height = img.naturalHeight
|
||||
images[i].width = img.naturalWidth
|
||||
}
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
galleryId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const imagesData = ref([])
|
||||
|
||||
let lightbox = null
|
||||
|
||||
// Lifecycle hook: mounted
|
||||
onMounted(() => {
|
||||
if (!lightbox) {
|
||||
console.log('Launching Gallery')
|
||||
console.log({
|
||||
gallery: '#' + props.galleryId,
|
||||
children: 'a'
|
||||
})
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '#' + props.galleryId,
|
||||
children: 'a',
|
||||
pswpModule: () => import('photoswipe')
|
||||
})
|
||||
lightbox.init()
|
||||
}
|
||||
imagesData.value = updateImagesData(props.modelValue)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newImages) => {
|
||||
console.log('propsimages', newImages)
|
||||
imagesData.value = updateImagesData(newImages)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
function removeImage(index) {
|
||||
const newImages = [...props.modelValue]
|
||||
newImages.splice(index, 1)
|
||||
emit('update:modelValue', newImages)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div
|
||||
:id="galleryId"
|
||||
class="pswp-gallery pswp-gallery--single-column my-3 flex min-w-0 flex-row flex-nowrap overflow-x-auto rounded-md bg-brand-dark-3 p-3"
|
||||
>
|
||||
<div v-for="(image, key) in imagesData" :key="key" class="relative">
|
||||
<a
|
||||
:href="image.url"
|
||||
:data-pswp-width="image.width"
|
||||
:data-pswp-height="image.height"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="mx-3 block flex-shrink-0"
|
||||
>
|
||||
<img :src="image.url" alt="" class="max-h-60 rounded-md" :style="{ height: '150px' }" />
|
||||
</a>
|
||||
<button
|
||||
class="absolute right-4 top-2 flex h-6 w-6 items-center justify-center rounded-full text-brand-danger"
|
||||
@click="removeImage(key)"
|
||||
>
|
||||
<font-awesome-icon icon="fa-solid fa-x" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox'
|
||||
import 'photoswipe/style.css'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
function updateImagesData(images) {
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = new Image()
|
||||
img.src = images[i].url
|
||||
img.onload = function () {
|
||||
images[i].height = img.naturalHeight
|
||||
images[i].width = img.naturalWidth
|
||||
}
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
galleryId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const imagesData = ref([])
|
||||
|
||||
let lightbox = null
|
||||
|
||||
// Lifecycle hook: mounted
|
||||
onMounted(() => {
|
||||
if (!lightbox) {
|
||||
console.log('Launching Gallery')
|
||||
console.log({
|
||||
gallery: '#' + props.galleryId,
|
||||
children: 'a'
|
||||
})
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '#' + props.galleryId,
|
||||
children: 'a',
|
||||
pswpModule: () => import('photoswipe')
|
||||
})
|
||||
lightbox.init()
|
||||
}
|
||||
imagesData.value = updateImagesData(props.modelValue)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newImages) => {
|
||||
console.log('propsimages', newImages)
|
||||
imagesData.value = updateImagesData(newImages)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
function removeImage(index) {
|
||||
const newImages = [...props.modelValue]
|
||||
newImages.splice(index, 1)
|
||||
emit('update:modelValue', newImages)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<FilePond
|
||||
ref="pond"
|
||||
name="filepond-image"
|
||||
:max-files="maxFiles"
|
||||
store-as-file="true"
|
||||
:server="{
|
||||
process: {
|
||||
url: API_URL + uploadEndpoint,
|
||||
method: 'POST',
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
authorization: 'Bearer ' + Cookies.get('bm-token')
|
||||
},
|
||||
onload: (response) => handleProcessResponse(response)
|
||||
},
|
||||
revert: {
|
||||
url: API_URL + deleteEndpoint,
|
||||
method: 'DELETE',
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
authorization: 'Bearer ' + Cookies.get('bm-token')
|
||||
},
|
||||
onload: (response) => handleRevertResponse(response)
|
||||
}
|
||||
}"
|
||||
label-idle="برای آپلود تصاویر کلیک کنید"
|
||||
label-tap-to-undo=" برای حذف کلیک کنید "
|
||||
label-file-processing=" در حال آپلود "
|
||||
label-file-processing-complete=" آپلود فایل تکمیل شد "
|
||||
:allow-multiple="true"
|
||||
accepted-file-types="image/jpeg, image/png"
|
||||
v-model="uploadedFiles"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, defineProps, computed } from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import vueFilePond from 'vue-filepond';
|
||||
import 'filepond/dist/filepond.min.css';
|
||||
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
||||
|
||||
// Define props
|
||||
const props = defineProps({
|
||||
uploadEndpoint: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
deleteEndpoint: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
maxFiles: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'public'
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
// Setup reactive variables
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const uploadedFiles = ref([...props.modelValue]);
|
||||
|
||||
// Define FilePond component
|
||||
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview);
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// Handle server responses for file processing
|
||||
function handleProcessResponse(response) {
|
||||
const data = JSON.parse(response);
|
||||
const fileName = data.data[0].fileRecord.fileName;
|
||||
uploadedFiles.value.push(fileName);
|
||||
emit('update:modelValue', uploadedFiles);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
// Handle server responses for file revert
|
||||
function handleRevertResponse(response) {
|
||||
const data = JSON.parse(response);
|
||||
const fileId = data.data.fileRecord.id;
|
||||
|
||||
// Find and remove the file name from uploadedFiles
|
||||
const fileIndex = uploadedFiles.value.findIndex((name) => name === fileId);
|
||||
if (fileIndex !== -1) {
|
||||
uploadedFiles.value.splice(fileIndex, 1);
|
||||
emit('update:modelValue', uploadedFiles);
|
||||
}
|
||||
|
||||
return fileId;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Apply dark mode styles to FilePond component */
|
||||
.filepond--root {
|
||||
font-family: 'Estedad FD', sans-serif;
|
||||
background-color: var(--surface-ground);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.app-dark .filepond--root {
|
||||
background-color: var(--code-background);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filepond--label {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.app-dark .filepond--label {
|
||||
color: var(--code-color);
|
||||
}
|
||||
|
||||
.app-dark .filepond--drop-label {
|
||||
background-color: var(--code-background);
|
||||
color: var(--code-color);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filepond--item-panel {
|
||||
background-color: var(--surface-card);
|
||||
}
|
||||
|
||||
.app-dark .filepond--item-panel {
|
||||
background-color: var(--code-background);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<FloatLabel>
|
||||
<ToggleSwitch :id="id" v-model="computedValue" :class="inputClass" @change="handleChange" @keyup="handleKeyUp" />
|
||||
<label :for="id">label</label>
|
||||
</FloatLabel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'key-up-event', 'change-event']);
|
||||
|
||||
const computedValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
});
|
||||
|
||||
const handleChange = (event) => {
|
||||
emit('change-event', event);
|
||||
};
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
emit('key-up-event', event);
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user