This commit is contained in:
mohadese namavar
2024-06-16 00:22:14 +04:30
commit ec84dfd222
322 changed files with 77942 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="become-seller">
<p>
{{ $t('becomeSeller') }}!
<br />
{{ $t('sellingInAsanMarket') }}
</p>
<Button :label="$t('becomeSeller')" rasiad text @click="$router.push('/panel/seller/become')" />
</div>
</template>
<script setup>
</script>
<style lang="scss">
.become-seller {
@apply w-[21.4rem] h-[29.4rem] p-6 flex flex-col justify-between rounded-[0.6rem];
@apply bg-no-repeat bg-cover bg-[url('/images/sell.svg')];
@apply lg:w-[24.2rem] lg:h-[34.9rem] lg:p-8;
p {
@apply text-right text-white text-xl lg:text-2xl font-bold font-vazir;
}
button {
@apply w-[18.4rem] h-12 lg:w-[20.2rem] bg-white rounded-[0.6rem] mx-auto border-none #{!important};
.p-button-label {
@apply text-[#47B556] text-xs lg:text-sm font-medium font-vazir;
}
}
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<div class="comment-item">
<img :src="product.coverImage" />
<div>
<div>
<div>
<h2>{{ title }}</h2>
<span>{{ (new Date(createdAt)).toLocaleDateString('fa-IR') }}</span>
</div>
<span>{{ statuses[confirm] }}</span>
<Button v-bind="btns.options.props" @click="showOptions" />
</div>
<p>{{ content }}</p>
</div>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { id, product, title, content, createdAt, confirm } = props.data
const { statuses, btns, options } = inject('init')
const { device, dialog, menu } = inject('service')
const showOptions = (e) => {
if (device.value == 'desktop') {
menu.items = options.comment;
menu.ref.toggle(e)
} else if (device.value == 'mobile')
dialog.show(resolveComponent(''), { id })
}
</script>
<style lang="scss">
.comment-item {
@apply w-[21.4rem] flex gap-[1.1rem] lg:gap-[1.9rem] lg:w-full lg:h-20;
img {
@apply w-[3.8rem] h-[3.8rem] lg:w-[5rem] lg:h-[5rem];
}
&>div {
@apply flex flex-col gap-3 lg:w-full;
&>div {
@apply h-20 flex justify-between items-center pb-3 border-b border-[#F0F0F1] gap-[1.9rem];
&>div {
@apply flex flex-col gap-1.5 font-vazir;
h3 {
@apply flex items-center gap-2 text-[#019907] lg:hidden;
i {
@apply text-base;
}
small {
@apply text-[0.6em];
}
}
h2 {
@apply text-[#333333] text-sm lg:text-base font-bold;
}
span {
@apply text-neutral-400 text-[0.7em] lg:text-sm font-medium;
}
}
&>span {
@apply text-[#019907] text-xs lg:text-base font-vazir mr-auto;
}
&>button {
@apply h-8 w-8 -ml-3 #{!important};
.p-button-icon {
@apply text-[#333333] text-lg;
}
}
}
&>p {
@apply w-[16.5rem] text-[#4C4C4C] text-xs leading-5 font-vazir lg:w-full;
}
}
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div class="factor-filters">
<div v-if="device == 'mobile'">
<Button v-bind="btns.close.props" @click="dialog.close()" />
<h2>{{ $t("sortBy") }}</h2>
</div>
<ul>
<li v-for="(s, i) in switches" :key="i">
<label :for="s">
{{ $t("searchBy" + s[0].toUpperCase() + s.slice(1)) }}
</label>
<InputSwitch v-model="filters[s]" :value="s" :inputId="s" />
</li>
<li v-for="(r, i) in radios" :key="i">
<label :for="r">{{ $t(r) }}</label>
<RadioButton v-model="filters[sort]" :value="r" :inputId="r" />
</li>
</ul>
</div>
</template>
<script setup>
import init from '../../initialize/panel.js'
const { btns, radios, switches } = init();
const { device } = inject('service')
const dialog = inject('dialogRef')
const filters = defineModel({ default: {} });
</script>
<style lang="scss">
.factor-filters {
@apply lg:w-[17.8rem] lg:h-[19.1rem] bg-white rounded-t-2xl lg:rounded-2xl lg:border p-6 lg:p-8;
@apply flex flex-col gap-9 rtl;
&>div {
@apply flex items-center gap-4;
&>button {
@apply -mx-5 -my-3 text-[#333333] text-xl #{!important};
}
&>h2 {
@apply text-[#333333] text-sm font-medium font-vazir;
}
}
ul {
@apply flex flex-col gap-[1.1rem] lg:gap-4;
li {
@apply w-full flex justify-between items-center;
&:nth-child(2) {
@apply border-b pb-4;
}
label {
@apply text-[#333333] text-xs lg:text-[1.1em] font-medium font-vazir cursor-pointer;
}
.p-inputswitch {
@apply w-8 h-[1.1rem] lg:w-6 lg:h-4;
.p-inputswitch-slider {
@apply bg-[#A6A6A6];
@apply w-8 h-[1.1rem] lg:w-6 lg:h-4 hover:bg-[#929191] border-0 #{!important};
&::before {
@apply w-3 h-3 left-1 lg:w-2.5 lg:h-2.5 lg:left-0.5 mt-0 top-1/2 -translate-y-1/2 bg-white;
}
}
}
.p-inputswitch-checked .p-inputswitch-slider {
@apply bg-primary-600 hover:bg-primary-600 #{!important};
&::before {
@apply translate-x-3 lg:translate-x-2;
}
}
.p-radiobutton-checked {
.p-radiobutton-box {
@apply bg-white border-primary-600 border hover:bg-white #{!important};
.p-radiobutton-icon {
@apply w-2.5 h-2.5 bg-primary-600;
}
}
}
}
}
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div class="information">
<h2>{{ $t('userInformation') }}</h2>
<div>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }} : </label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import init from '../../initialize/userProfileInformation'
const { items } = init()
const profile = inject('data')
const data = reactive({});
const temp = {
true: 'تایید شده', false: 'تایید نشده', '0': 'مرد', '1': 'خانم'
}
items.forEach(key => {
const value = profile[key];
data[key] = temp[value] || value
});
</script>
<style lang="scss">
.information {
@apply w-[21.4rem] lg:w-[49.8rem] lg:h-[34.9rem];
@apply lg:bg-[#FAFAFA] rounded-[0.6rem] lg:border-2 lg:border-[#E5E5E5];
@apply flex flex-col gap-6 lg:p-8 lg:pl-2.5;
h2 {
@apply max-lg:hidden text-[#333333] text-2xl font-bold font-iran-sans flex items-center gap-3.5;
&::before {
@apply content-['_'] h-6 w-1.5 rounded-full mb-1 bg-primary-600;
}
}
div {
@apply w-full h-full lg:h-[27.1rem] lg:overflow-y-auto;
&::-webkit-scrollbar {
@apply w-[0.2rem] lg:w-1.5 bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply bg-[#E5E5E5];
}
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
ul {
@apply flex flex-col gap-y-3 gap-x-6 pl-4 lg:pl-3 h-max lg:h-[26.3rem] lg:flex-wrap;
li {
@apply w-[21.4rem] lg:w-[22.1rem] h-[2.8rem] lg:h-12;
@apply bg-[#F2F2F2] rounded-lg px-4 flex items-center gap-2 font-vazir;
@apply last:py-2 last:items-start;
label {
@apply text-[#7F7F7F] text-xs lg:text-base whitespace-nowrap leading-6;
}
span {
@apply text-[#333333] text-xs lg:text-[1.1em] font-medium leading-6;
}
}
}
}
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<div class="national-card">
<h2>{{ $t("nationalCard") }}</h2>
<p>{{ $t("nationalCardDesc") }}</p>
<img :src="nationalCard" />
<label>
<i :class="tags[confirmToSales].icon"></i>
<span>{{ tags[confirmToSales].label }}</span>
</label>
<div>
<span>{{ nationalCode }}</span>
<i class="isax isax-link-21" />
</div>
</div>
</template>
<script setup>
const { nationalCard, confirmToSales, nationalCode } = inject('data')
const tags = reactive({
true: {
icon: 'isax isax-tick-circle',
label: 'تاییده شده'
},
false: {
icon: 'isax isax-close-circle',
label: 'تاییده نشده'
}
})
</script>
<style lang="scss">
.national-card {
@apply w-[21.4rem] h-[28.4rem] lg:w-[24.2rem] lg:h-[34.9rem];
@apply flex flex-col gap-4 p-6 lg:p-8 bg-neutral-50 rounded-[0.6rem] border-neutral-200;
@apply border lg:border-2;
h2 {
@apply text-[#333333] text-lg lg:text-2xl font-bold font-iran-sans;
@apply flex items-center gap-4 lg:gap-3.5;
&::before {
@apply content-['_'] w-[0.2rem] h-[1.1rem] lg:h-6 lg:w-1.5 lg:mb-1 rounded-full bg-primary-600;
}
}
p {
@apply text-[#7F7F7F] text-xs lg:text-base font-medium font-vazir;
}
img {
@apply w-[18.4rem] h-[11.6rem] lg:w-[20.2rem] lg:h-[12.8rem] bg-[#F2F2F2] rounded-[0.6rem] overflow-hidden relative;
@apply object-cover object-center;
}
label {
@apply flex items-center gap-2 text-[#7F7F7F];
span {
@apply text-xs font-iran-sans;
}
i {
@apply text-xl;
}
}
div {
@apply h-[2.7rem] lg:h-[3.1rem] flex justify-end items-center gap-2 px-4;
@apply text-[#333333] bg-[#F2F2F2] rounded-lg shrink-0 mt-auto;
i {
@apply text-2xl;
}
span {
@apply font-iran-sans text-xs lg:text-base;
}
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<div class="notification-item" :class="{ 'unread': !read }">
<h2>{{ title }} </h2>
<div>
<span>{{ new Date(createdAt).toLocaleTimeString('fa-IR') }}</span>
<i />
<span>{{ new Date(createdAt).toLocaleDateString('fa-IR') }}</span>
</div>
<p v-html="content"></p>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { title, content, createdAt, read } = props.data
</script>
<style lang="scss" scoped>
.notification-item {
@apply w-full h-[5.8rem] lg:h-[6.4rem] font-vazir rounded-[0.6rem] bg-[#FAFAFA];
@apply flex flex-wrap items-center justify-between gap-y-2 p-6 lg:gap-2.5;
h2 {
@apply text-[#333333] text-xs lg:text-base lg:font-bold lg:w-full truncate;
}
p {
@apply text-neutral-600 text-xs lg:text-base leading-loose truncate w-full lg:w-max;
}
div {
@apply flex items-center gap-1.5 lg:gap-3 lg:order-last;
span {
@apply text-zinc-500 text-[0.6em] lg:text-base font-medium whitespace-nowrap;
}
i {
@apply w-2 h-2 bg-neutral-200 rounded-full;
}
}
&.unread {
@apply shadow-[0px_0px_7px_#0000001a] bg-white;
h2 {
@apply font-bold;
}
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<div class="order-item">
<div>
<ul>
<li v-for="(value, key) in items" :key="key">
<label>{{ $t(key) }}:</label>
<span>{{ value }}</span>
</li>
</ul>
<router-link :to="`/panel/factors?id=${id}`">
<Button :label="$t('showFactor')" size="small" />
</router-link>
</div>
<Swiper :slides-per-view="'auto'" :space-between="16">
<SwiperSlide v-for="(item, i) in products" :key="i">
<ProductCard :data="item.product" type="minimal" />
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { _id: id, totalAmount, status, orderDate, products } = props.data
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
const { t } = useI18n()
const items = reactive({
documentNumber: id,
amount: numberFormat(totalAmount) + ' ' + t('priceUnit'),
orderStatus: t('orderStatuses.' + status.toLowerCase()),
orderDate: new Date(orderDate).toLocaleString('fa-IR')
})
</script>
<style lang="scss">
.order-item {
@apply w-[21.4rem] h-[29.1rem] lg:w-full lg:h-[19.3rem] overflow-hidden;
@apply flex flex-col-reverse lg:flex-row bg-white border-2 border-[#E5E5E5];
@apply rounded-[0.6rem] max-lg:rounded-t-xl;
&>div:first-of-type {
@apply w-full h-[15.1rem] lg:w-[29.6rem] lg:h-full max-lg:border-t-2 border-[#E5E5E5];
@apply flex flex-col justify-between whitespace-nowrap;
@apply gap-[1.9rem] p-6 lg:gap-10 lg:p-8;
ul {
@apply flex flex-col gap-4;
li {
@apply flex gap-3 items-center font-vazir text-xs lg:text-lg;
label {
@apply text-zinc-500;
}
span {
@apply text-zinc-800 font-medium;
}
&:last-child span {
@apply ltr;
}
}
}
button {
@apply w-full h-10 lg:w-[9.3rem] rounded-lg #{!important};
.p-button-panel {
@apply font-medium font-vazir text-xs lg:text-base;
}
}
}
.swiper {
@apply w-full max-w-[67.6rem] h-56 lg:h-full relative ltr px-[0.9rem];
.swiper-wrapper {
@apply items-center z-0;
.swiper-slide {
@apply h-max w-max;
}
}
&::before {
@apply shadow-[-5px_0px_23px_0px_#00000040];
@apply content-['_'] absolute inset-y-0 w-6 -right-8 lg:-right-6 opacity-50 z-10;
}
&::after {
@apply shadow-[5px_0px_23px_0px_#00000040];
@apply content-['_'] absolute inset-y-0 w-6 -left-8 lg:-left-6 opacity-50 z-10;
}
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<template>
<ul class="profile-menu">
<li v-for="(item, i) in menu.profile" :key="i">
<template v-if="i != 2 || user?.profile?.confirmToSales">
<component :is="item.link ? 'router-link': 'div'" :to="'/panel/' + item.url" @click="handleClick(item.key)"
v-ripple>
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
<i v-if="item.key != 'logout'" class="isax isax-arrow-left-2" />
</component>
</template>
</li>
</ul>
</template>
<script setup>
const { menu } = inject('init');
const { dialog } = inject('service')
const handleClick = (key) => {
if(key == 'logout')
dialog.show(resolveComponent('PanelDialogLogout'))
else if(key == 'password')
dialog.show(resolveComponent('PanelDialogPasswordChange'))
}
</script>
<style lang="scss">
.profile-menu {
@apply flex flex-col gap-1.5 md:order-first;
li {
a,
div {
@apply w-[21.4rem] h-[3.1rem] bg-[#FAFAFA] rounded-[0.6rem] overflow-hidden relative;
@apply flex items-center gap-3 px-[1.1rem] text-[#7F7F7F];
i:first-child {
@apply text-[1.4em];
}
span {
@apply text-sm font-vazir ml-auto;
}
i:last-child {
@apply text-base;
}
}
&:last-child a {
i:first-child,
span {
@apply text-[#AD3434];
}
}
&:not(:has(*:first-child)){
@apply -mt-1.5;
}
}
}
</style>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div class="user-profile-card">
<img :src="profilePhoto" />
<h2>{{ firstName }} {{ lastName }}</h2>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
<template v-if="device == 'desktop'">
<router-link to="/panel/edit">
<Button v-bind="btns.edit.props" />
</router-link>
<Button v-bind="btns.security.props" @click="handleClick" />
</template>
</div>
</template>
<script setup>
import init from '../../initialize/userProfileCard.js'
const { btns, items } = init()
const { device, dialog } = inject('service')
const profile = inject('data')
const { profilePhoto = '/images/empty-profile.svg', firstName, lastName } = profile
const data = reactive({});
const temp = {
true: 'تایید شده', false: 'تایید نشده', 'WHOLESALER': 'عمده فروش', 'BUYER': 'خریدار', 'RETAILER': 'خرده فروش'
}
items.forEach(key => {
const value = profile[key];
data[key] = temp[value] || value
});
const handleClick = () =>
dialog.show(resolveComponent('PanelDialogPasswordChange'))
</script>
<style lang="scss">
.user-profile-card {
@apply bg-[#FAFAFA] rounded-[0.6rem] border-[#E5E5E5];
@apply flex flex-col gap-3 p-6 relative overflow-hidden;
@apply w-[21.4rem] h-[20.1rem] lg:w-[20.8rem] lg:h-[34.9rem] lg:border-2;
&::before {
@apply content-['_'] top-0 inset-x-0 h-[4.4rem] lg:h-[6.3rem] bg-primary-600 absolute;
}
img {
@apply w-20 h-20 lg:w-[7.5rem] lg:h-[7.5rem] rounded-full border-2 border-[#FAFAFA] z-0 self-center mt-4;
}
h2 {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir self-center;
}
ul {
@apply flex flex-col gap-4 my-auto;
li {
@apply flex justify-between items-center pb-4 border-b border-dotted;
@apply text-xs lg:text-base font-medium font-vazir border-[#E5E5E5] last:border-0;
label {
@apply text-[#999999];
}
span {
@apply text-[#333333];
}
}
}
a {
@apply w-full flex;
}
button {
@apply w-full h-[2.8rem] lg:h-12 justify-center #{!important};
span:not(.isax) {
@apply grow-0 text-[0.7em] lg:text-sm font-medium font-iran-sans;
}
.isax {
@apply text-xl;
}
}
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<div class="recent-factor-item">
<div>
<div>
<span>{{ $t("factor") }} :</span>
<span>{{ invoiceNumber }}</span>
</div>
<div>
<span>{{ $t("date") }} :</span>
<span>{{ new Date(createdAt).toLocaleDateString('fa-IR') }}</span>
</div>
</div>
<router-link :to="{ query: { id } }">
<Button icon="isax isax-arrow-left" severity="secondary" text rounded />
</router-link>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { _id: id, invoiceNumber, createdAt } = props.data
</script>
<style lang="scss">
.recent-factor-item {
@apply w-[19.9rem] sm:w-[16.8rem] h-20 lg:h-[5.6rem] flex gap-2 bg-[#F2F2F2] rounded-[0.6rem] p-4;
&>div {
@apply flex flex-col h-full justify-between gap-3;
&>div {
@apply flex gap-2 items-center font-vazir;
span:nth-of-type(1) {
@apply text-[#999999] text-xs lg:text-[1.1em];
}
span:nth-of-type(2) {
@apply text-[#333] text-xs lg:text-[1.1em] font-medium;
}
}
}
a {
@apply self-end mr-auto #{!important};
button {
@apply -m-2 w-10 h-10;
.p-button-icon {
@apply text-xl text-[#292D32] lg:text-2xl;
}
}
}
}
</style>
+198
View File
@@ -0,0 +1,198 @@
<template>
<div class="sales-factor">
<div>
<h2>{{ $t("salesFactor") }}</h2>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }}:</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
<table>
<thead>
<tr>
<th v-for="(h, i) in headers" :key="i" v-text="$t(h)" />
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in products" :key="i">
<td>{{ i + 1 }}</td>
<td>{{ item.product.uid }}</td>
<td>{{ item.product.title }}</td>
<td>{{ item.quantity }}</td>
<td>{{ $t('number') }}</td>
<td>{{ numberFormat(item.price) }}</td>
<td>{{ 100 - Math.round(((item.specialPrice || item.price) / item.price) * 100) }} %</td>
<td>{{ numberFormat(item.specialPrice || item.price) }}</td>
<td></td>
</tr>
</tbody>
</table>
<div>
<div>
<div>
<label v-for="(word, i) in words" :key="i">
{{ $t(word) }}:
</label>
</div>
<label>{{ $t("debtor") }}:</label>
</div>
<ul>
<li v-for="(value, key) in summary" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ numberFormat(value) }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import init from '../../initialize/panel.js'
const { headers, words } = init()
const props = defineProps(['data'])
const { products, userID, _id, invoiceNumber, shippingAddress, createdAt } = props.data
const data = reactive({
customerName: userID.firstName + ' ' + userID.lastName,
phone: userID.phoneNumber,
documentNumber: _id,
customerCode: userID._id,
fax: userID.cellNumber,
factorNumber: invoiceNumber,
address: shippingAddress,
factorDate: new Date(createdAt).toLocaleDateString('fa-IR'),
});
const sumPrices = products.reduce((sum, item) => sum + item.price, 0)
const sumSpecialPrices = products.reduce((sum, item) => sum + item.specialPrice || item.price, 0)
const summary = reactive({
netSum: sumPrices,
discountPercent: 100 - Math.round((sumSpecialPrices / sumPrices) * 100),
sheetDiscount: sumPrices - sumSpecialPrices,
totalAmountFactor: sumSpecialPrices
});
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.sales-factor {
@apply w-[77.5rem] flex flex-col [&_*]:border-[#E5E5E5];
&>div:nth-of-type(1) {
@apply flex flex-col;
&>h2 {
@apply h-[2.8rem] bg-zinc-100 flex items-center justify-center;
@apply text-[#333333] text-sm lg:text-base font-bold font-vazir;
}
ul {
@apply grid grid-cols-3 grid-rows-3 py-8 px-6 gap-6 border text-xs lg:text-base;
li {
@apply flex items-center gap-2.5 font-vazir;
label {
@apply text-[#333333] font-bold;
}
span {
@apply text-[#7F7F7F] font-medium;
}
&:nth-child(7) {
@apply col-span-2;
}
}
}
}
table {
thead {
tr {
@apply h-[2.8rem] bg-[#F2F2F2];
th {
@apply border border-t-0;
@apply text-center text-[#333333] text-xs lg:text-base font-medium font-vazir;
}
th:nth-of-type(9) {
@apply text-right pr-3;
}
}
}
tbody {
tr {
@apply h-[2.8rem];
td {
@apply border border-t-0;
@apply text-center text-[#333333] text-[0.7em] lg:text-sm font-medium font-vazir;
}
td:nth-of-type(3) {
@apply w-[22.5rem];
}
td:nth-of-type(9) {
@apply w-[13.4rem];
}
}
}
}
&>div:nth-of-type(2) {
@apply w-[64.1rem] flex;
label {
@apply text-[#333333] text-xs lg:text-base font-bold font-vazir;
}
&>div {
@apply w-[43.1rem] flex flex-col border border-t-0;
&>div {
@apply h-[6.9rem] p-6 flex flex-wrap gap-y-4 border-b;
label {
@apply first:w-full last:mx-auto;
}
}
&>label {
@apply h-[10.2rem] flex items-end p-6;
}
}
&>ul {
@apply flex flex-col justify-end h-[17.1rem] border-l;
li {
@apply flex border-b first:border-t;
* {
@apply w-[10.5rem] h-[3.3rem] flex items-center justify-center;
}
label {
@apply border-l;
}
span {
@apply text-[#333333] text-[0.7em] lg:text-sm font-medium font-vazir;
}
}
}
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
<template>
<nav class="panel-toolbar">
<ul>
<li v-for="(item, i) in menu" :key="i">
<router-link :to="item.url" v-if="i < 7 || user.profile.confirmToSales">
<Button v-bind="getBind(item)" @click="toFloat(i)" />
</router-link>
</li>
</ul>
<span ref="selector" />
<Button v-bind="logoutBtn.props" @click="logout()" />
</nav>
</template>
<script setup>
import init from '../../initialize/panelToolbar.js'
const { menu, logoutBtn, getBind, floating } = init()
const route = useRoute()
const selector = ref()
const { dialog } = inject('service')
const logout = () => dialog.show(resolveComponent('PanelDialogLogout'))
const toFloat = (index = menu.findIndex((item) => item.url == route.path)) => {
try {
floating(menu, selector, index ??= 7)
} catch (error) {
}
}
const user = useUserStore()
user.get()
watch(() => route.path, () => toFloat())
onMounted(() => toFloat())
</script>
<style lang="scss">
.panel-toolbar {
@apply w-full container bg-[#FAFAFA] rounded-[0.6rem] border-2 border-[#E5E5E5];
@apply flex flex-wrap items-center justify-between mx-auto px-2 xl:px-4 2xl:px-6 xl:py-2 2xl:py-4 relative mt-10;
ul {
@apply flex xl:gap-4 2xl:gap-[1.1rem] overflow-hidden;
li {
button {
@apply h-10 py-1.5 justify-end overflow-visible whitespace-nowrap px-3 xl:px-3.5 2xl:px-4;
.p-button-icon {
@apply text-[#7F7F7F] lg:text-lg xl:text-xl 2xl:text-2xl 2xl:ml-2;
}
.p-button-label {
@apply text-[#7F7F7F] lg:text-sm xl:text-base 2xl:text-lg font-medium font-vazir grow-0;
}
&[active] * {
@apply text-primary-600;
}
}
}
}
&>button {
@apply h-10 xl:h-12 2xl:h-[3.7rem] bg-[#F2F2F2] hover:bg-[#eaeaea] active:bg-[#d8d8d8] px-3 xl:px-4 2xl:px-5 mr-auto #{!important};
.p-button-icon {
@apply text-[#AD3434] lg:text-base xl:text-lg 2xl:text-2xl xl:ml-4;
}
.p-button-label {
@apply text-[#AD3434] lg:text-sm xl:text-base 2xl:text-lg font-medium font-vazir grow-0;
}
}
&>span {
@apply content-['_'] right-6 h-1 rounded-full bg-primary-600 absolute -bottom-[0.2rem];
@apply transition-['right'] duration-200;
}
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="logout">
<h2>
<i class="isax isax-logout" />
<span>{{ $t("logoutUserAccount") }}</span>
</h2>
<p>{{ $t("logoutUserAccountQuest") }}</p>
<div>
<Button v-bind="btns.logout.props" @click="logout()" />
<Button v-bind="btns.cancel.props" @click="dialog.close()" />
</div>
</div>
</template>
<script setup>
import init from '../../../initialize/logout.js'
const { btns } = init();
const dialog = inject('dialogRef');
const router = useRouter()
const logout = () => {
const token = useCookie('token')
token.value = null
router.push('/')
dialog.value.close()
}
</script>
<style lang="scss">
.logout {
@apply bg-white w-full lg:w-[32.1rem] h-64 flex flex-col items-center gap-2 rounded-[0.6rem] max-lg:rounded-b-none p-[2.4rem];
h2 {
@apply flex items-center gap-4 text-[#333];
i {
@apply text-[2em];
}
span {
@apply text-xl font-bold font-vazir leading-[2.8rem];
}
}
p {
@apply text-[#B2B2B2] text-base font-medium font-vazir leading-[1.7rem];
}
div {
@apply flex items-center gap-4 mt-auto;
button {
@apply w-36 lg:w-[10.4rem] h-11 border-[#AD3434] #{!important};
.p-button-label {
@apply font-medium font-vazir leading-tight #{!important};
}
}
button:first-child {
@apply bg-[#AD3434];
}
button:last-child .p-button-label {
@apply text-[#AD3434];
}
}
}
</style>
+160
View File
@@ -0,0 +1,160 @@
<template>
<dialog class="panel-otp-code-enter">
<Button v-bind="btns.close.props" @click="dialogRef.close()" />
<h2>
<i class="isax isax-message-notif" />
<span>{{ $t("enterReceivedCode") }}</span>
</h2>
<p>{{ $t("enterReceivedCodeDesc") }}</p>
<ul ref="list">
<li v-for="(_, i) in 6" :key="i">
<InputNumber
v-model="code[i]"
@input="events.input($event, i)"
@keyup="events.keyup($event, i)"
@focus="events.focus($event, i)"
:disabled="inputs.length < 1"
/>
</li>
</ul>
<span :class="{ invisible: seconds < 1 }">
{{ $t("moreSecondReceiveCode", { seconds }) }}
</span>
<Button v-bind="btns.next.props" @click="events.next()" />
<div v-show="seconds < 1">
<p>{{ $t("notReceivedCode") }}</p>
<Button :label="$t('tryAgain')" link @click="events.try()" />
</div>
</dialog>
</template>
<script setup>
const { btns } = inject('init');
const seconds = ref(0);
const code = ref([]);
const form = reactive({});
const userStore = useUserStore();
const optCodeStore = useOtpCodeStore();
const list = ref();
let inputs = reactive([]);
const events = reactive({
next: async () => {
if (code.value.length == 6) {
if (optCodeStore.type == "email") form.email = optCodeStore.email;
else form.phoneNumber = optCodeStore.number;
form.code = code.value.join("");
const { status } = await userStore.forgetPassword(form);
// if (status == "success")
emit("dialog", "PanelPasswordRenewal");
}
},
try: async () => {
const form = { number: otpCode.number };
const { status } = await otpCode.first(form);
if (status == "success") seconds.value = 30;
},
input: (e, i) => {
if (e.value != null) inputs[i++].value = e.originalEvent.key;
if (i < 6) inputs[i].focus();
else {
inputs[5].blur();
events.next();
}
},
keyup: (e, i) => {
if (e.code == "Backspace" && i > 0) {
code.value[i - 1] = null;
inputs[i - 1].focus();
}
},
focus: (e, i) => {
const length = code.value.filter((c) => typeof c == "number").length;
if (length < i) inputs[length].focus();
},
});
onMounted(() => {
inputs.push(...list.value.querySelectorAll("input"));
setTimeout(() => inputs[0].focus(), 1);
seconds.value = 30;
setInterval(() => {
if (seconds.value > 0) seconds.value--;
}, 1000);
});
</script>
<style lang="scss">
.panel-otp-code-enter {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[7.2rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
& > button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
& > h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
& > p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
& > ul {
@apply flex gap-2.5 mt-4 lg:mt-10 ltr;
li {
input {
@apply w-9 h-9 lg:w-10 lg:h-10 p-0 rounded-lg border border-neutral-200 text-center;
@apply focus:text-primary-600 lg:text-xl font-medium font-vazir leading-[1.9rem];
}
}
}
& > span {
@apply text-[#999999] text-[0.7em] lg:text-sm font-medium font-vazir mt-5 lg:mt-4;
}
& > button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[19.5rem] h-[2.9rem] lg:h-14 mt-8 lg:mt-11;
.p-button-label {
@apply text-sm lg:text-base font-bold font-vazir leading-tight;
}
}
& > div {
@apply flex items-center -mb-3 whitespace-nowrap;
p {
@apply text-[#999999] text-sm font-medium font-vazir;
}
button {
.p-button-label {
@apply text-sm font-bold font-vazir;
}
}
}
}
</style>
+173
View File
@@ -0,0 +1,173 @@
<template>
<dialog class="panel-password-change">
<img src="/images/password-change.svg" />
<div>
<Button v-bind="btns.close.props" @click="dialogRef.close()" />
<h2>{{ $t("changePassword") }}</h2>
<p>{{ $t("enterCurrentAndNewPassword") }}</p>
<ul>
<li v-for="(item, key) in fields.changePassword" :key="key">
<Password v-model="form[key]" v-bind="item.props" :inputId="key" />
<label :for="key"> {{ $t(key) }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
</ul>
<Button v-bind="btns.save.props" :loading="loading" @click="save()" />
<div>
<p>{{ $t("forgetPassword") }}</p>
<Button v-bind="btns.recovery.props" @click="recovery()" />
</div>
</div>
</dialog>
</template>
<script setup>
const { meta } = useRoute()
const { btns, fields } = meta.init;
const { device, dialog, toast, t } = inject('service')
const dialogRef = inject('dialogRef')
const form = reactive({});
const errors = ref({});
const loading = ref(false);
btns.save.props.disabled = computed(() => Object.keys(errors.value).length > 1)
const save = async () => {
loading.value = true
const { status, data, error } = await useFetch('/api/users/setpass', {
headers: { Authorization: useCookie('token') }, method: 'post', body: Object.assign({}, form)
})
if (status.value == 'success') {
const token = useCookie('token')
token.value = data.value.accessToken
toast.add({
life: 3000, severity: 'success', summary: t('success'), detail: t('updatePasswordSuccessfull')
})
dialogRef.value.close()
} else {
if (error.value.statusCode == 422)
errors.value = error.value.data
else
toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.message
})
}
loading.value = false
}
const recovery = () => {
if (device.value == 'desktop')
dialogRef.value.close()
dialog.show('PanelDialogPasswordRestore')
}
watch(computed(() => form), (value, old) => {
Object.keys(errors.value).forEach(key => {
if (value[key] != old[key]) delete errors.value[key]
});
})
</script>
<style lang="scss">
.panel-password-change {
@apply w-full lg:w-[61.6rem] lg:h-[36.9rem] bg-white rounded-t-2xl lg:rounded-2xl flex p-6 lg:p-4 lg:pl-6 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-28 h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>img {
@apply hidden lg:block w-[27.2rem];
}
&>div {
@apply flex flex-col gap-4 lg:pt-2 items-center w-full;
&>h2 {
@apply self-start -mt-[3.4rem] text-[#333333] text-sm font-bold font-vazir leading-[1.7rem];
@apply lg:text-white lg:text-xl lg:-mt-10 lg:-mr-[25.3rem];
}
&>p {
@apply self-start w-[17.8rem] -mt-2 text-[#7F7F7F] text-[0.7em] font-vazir mb-5;
@apply lg:w-[22.9rem] lg:text-[#E5E5E5] lg:text-base lg:leading-[1.7rem] lg:-mr-[25.3rem];
}
&>button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333];
@apply lg:text-[2em] lg:text-[#b2b2b29b];
}
}
&>ul {
@apply flex flex-col items-center gap-6 mt-auto w-full sm:w-auto lg:-mt-16;
li {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] lg:h-14 relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium cursor-text font-vazir;
}
.p-password {
input {
@apply w-full rounded-[0.6rem] border text-sm lg:text-base shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
svg {
@apply text-[#CCCCCC] w-5 h-5 -mt-2.5;
}
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
small {
@apply text-red-500 font-vazir float-left;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
&>button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[2.9rem] mt-4 lg:h-14 lg:mt-16 #{!important};
.p-button-label {
@apply font-bold font-vazir leading-tight text-sm lg:text-base;
}
}
&>div {
@apply flex items-center justify-center -my-4 hidden;
p {
@apply text-[#999999] text-xs lg:text-sm font-medium font-vazir;
}
button {
@apply px-2;
.p-button-label {
@apply text-xs lg:text-sm font-bold font-vazir text-primary-600;
}
}
}
}
}
</style>
@@ -0,0 +1,103 @@
<template>
<dialog class="panel-password-renewal">
<Button v-bind="btns.close.props" @click="$emit('close')" />
<h2>
<i class="isax isax-lock-1"></i>
<span>{{ $t("newPassword") }}</span>
</h2>
<p>{{ $t("passwordMustBeCombination") }}</p>
<ul>
<li v-for="(item, key) in fields" :key="key">
<Password v-model="form[key]" v-bind="item.props" :inputId="key" />
<label :for="key"> {{ $t(key) }} </label>
</li>
</ul>
<Button v-bind="btns.save.props" @click="save()" />
</dialog>
</template>
<script setup>
const { btns, fields } = inject('init');
const form = reactive({});
const userStore = useUserStore();
const save = async () => {
const result = await userStore.resetPassword(form);
}
</script>
<style lang="scss">
.panel-password-renewal {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
&>h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
&>p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
&>ul {
@apply flex flex-col gap-6 mt-4 lg:mt-10 w-full items-center;
li {
@apply w-[21.4rem] lg:w-[23.3rem] lg:h-14 relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium cursor-text font-vazir;
}
.p-password {
input {
@apply rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
svg {
@apply text-[#CCCCCC] w-5 h-5 -mt-2.5;
}
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
}
}
&>button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[2.9rem] lg:h-14 mt-8 lg:mt-10;
.p-button-label {
@apply text-sm lg:text-base font-bold font-vazir leading-tight;
}
}
}
</style>
@@ -0,0 +1,119 @@
<template>
<dialog class="pamel-password-restore">
<Button v-bind="closeBtn.props" @click="$emit('close')" />
<h2>
<i class="isax isax-shield-search"></i>
<span>{{ $t("passwordRecovery") }}</span>
</h2>
<p>
{{ $t("recoverPasswordByEmailOrMobile") }} <br />
{{ $t("pleaseSelectMethod") }}
</p>
<SelectButton v-model="way" :options="ways">
<template #option="{ option }">
<span>{{ $t(option.replace("number", "sms")) }}</span>
</template>
</SelectButton>
<div>
<InputText v-model="form[way]" id="input" placeholder />
<label for="input">{{ $t(labels[way]) }}</label>
</div>
<Button v-bind="recoveryBtn.props" @click="events.recovery()" />
</dialog>
</template>
<script setup>
import init from "../../../../initialize/panelPasswordRestore";
const { closeBtn, ways, labels, fields, recoveryBtn } = init();
const emit = defineEmits(["dialog"]);
const way = ref("email");
const form = reactive({});
const validator = useValidator();
const optCode = useOtpCodeStore();
const events = reactive({
recovery: async () => {
if (validator.isValid(fields, form)) {
const { status } = await optCode.send(way.value, form);
if (status == "success") emit("dialog", "PanelOTPCodeEnter");
}
},
});
</script>
<style lang="scss">
.pamel-password-restore {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
& > button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
& > h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
& > p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
& > div:nth-of-type(1) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] rounded-lg border border-[#E5E5E5] mt-1.5 flex items-center p-1 gap-1;
& > div {
@apply flex justify-center items-center p-0 text-sm lg:text-base;
@apply w-[11.4rem] h-[2.9rem] rounded-[0.4rem] border-none text-neutral-400 hover:text-neutral-400;
&.p-highlight {
@apply bg-neutral-200 text-zinc-800 hover:bg-neutral-200 hover:text-zinc-800;
}
}
}
& > div:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] lg:h-14 relative mt-[1.9rem] lg:mt-10;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
}
& > button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-12 lg:h-14 mt-8 lg:mt-6;
.p-button-label {
@apply font-bold font-vazir leading-tight text-sm lg:text-base;
}
}
}
</style>
+156
View File
@@ -0,0 +1,156 @@
<template>
<DataTable :value="muchSell" class="seller-best-selling" :responsiveLayout="rl">
<template #header>
<h2>{{ $t('bestsellingPorducts') }}</h2>
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
</template>
<Column v-for="(col, i) in columns[device]" :key="i" v-bind="col"
class="!text-center [&:first-child_button]:hidden">
<template #body="{ data, field }">
<img v-if="device == 'desktop' && i === 1" :src="field(data)" alt="">
<template v-else>
<span>{{ field(data) }}</span>
<Button v-if="device == 'mobile'" :label="$t('moreDetails')" icon="isax isax-arrow-left-2" link @click="show(data)"/>
</template>
</template>
</Column>
<template #empty>
<p>{{ $t('notFound') }}</p>
</template>
</DataTable>
</template>
<script setup>
import PanelSellerDialogDetailsBestSelling from './dialog/details/BestSelling.vue'
const { device, dialog, t } = inject('service')
const rl = computed(() => device.value == 'mobile' ? 'stack' : '')
const { muchSell = [] } = inject('data')
const columns = reactive({
desktop: [
{ header: t('row'), field: (data) => muchSell.indexOf(data) + 1 },
{ header: t('image'), field: (data) => data.product.coverImage },
{ header: t('productTitle'), field: (data) => data.product.title },
{ header: t('salesPrice'), field: (data) => numberFormat(data.total) + ' ' + t('priceUnit') },
{ header: t('inventory'), field: (data) => data.quantity },
],
mobile: [
{ field: (data) => data.product.title },
{ field: (data) => numberFormat(data.total) + ' ' + t('priceUnit') },
]
})
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
const show = (data) => {
dialog.show(PanelSellerDialogDetailsBestSelling, data)
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.seller-best-selling {
@apply rtl border-2 border-[#E5E5E5] rounded-[0.6rem] overflow-hidden w-full;
.p-datatable-header {
@apply flex items-center justify-between bg-white pt-3 pl-3 pr-[1.1rem] pb-[1.1rem] lg:py-2.5 lg:px-5;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir;
}
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
.p-datatable-wrapper {
@apply max-lg:overflow-hidden #{!important};
.p-datatable-table {
@apply border-separate border-spacing-y-[0.8rem] px-[1.1rem] py-3;
.p-datatable-thead {
@apply w-full h-[3.1rem] lg:px-5 max-lg:hidden;
th {
@apply text-[#5D5D5D] text-base font-normal font-vazir bg-[#F2F2F2];
@apply first:rounded-r-[0.6rem] last:rounded-l-[0.6rem];
}
}
.p-datatable-tbody {
@apply lg:translate-y-1.5;
tr {
@apply w-full rounded-[0.6rem] bg-transparent shadow-[0px_0px_6px_#0000001f];
td {
@apply max-lg:w-full max-lg:px-3 text-right lg:text-center;
@apply lg:first:rounded-r-[0.6rem] lg:last:rounded-l-[0.6rem] py-0 lg:h-[4.5rem];
&>span {
@apply max-lg:py-3 min-h-[3.8rem] flex items-center text-xs lg:text-base text-[#333333] font-vazir;
}
img {
@apply w-[3.75rem] h-[3.75rem];
}
@media(max-width: 1024px) {
&>button {
@apply -m-3 mr-auto #{!important};
.p-button-label {
@apply text-xs font-medium font-vazir whitespace-nowrap #{!important};
}
.p-button-icon {
@apply text-sm mr-1;
}
}
&:first-child span {
@apply w-full border-b py-3;
}
.p-column-title {
@apply hidden #{!important};
}
}
}
}
.p-datatable-emptymessage p {
@apply lg:h-[3.8rem] flex justify-center items-center
}
}
}
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="seller-management">
<h2>{{ $t(title + 'Mangement') }}</h2>
<ul>
<li v-for="(item, i) in items[title]" :key="i">
<small>{{ item.label }}</small>
<span>{{ item.value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps(['title'])
const {
itemHighStock, zeroStock, returnedProducts, itemLowStock,
oneWeekAgo, oneDayAgo, pendingProducts = [], receivedProducts,
} = inject('data')
const items = reactive({
warehouse: [
{ label: 'کالاهای موجود', value: itemHighStock },
{ label: 'کالاهای بدون موجودی', value: zeroStock },
{ label: 'کالاهای مرجوعی یک هفته گذشته', value: returnedProducts },
{ label: 'کالاهای در حال اتمام موجودی', value: itemLowStock },
],
orders: [
{ label: 'سفارشات این هفته', value: oneWeekAgo },
{ label: 'سفارش های امروز', value: oneDayAgo },
{ label: 'سفارش های پذیرش نشده', value: pendingProducts.length },
{ label: 'سفارش های ارسال شده', value: receivedProducts },
]
})
</script>
<style lang="scss">
.seller-management {
@apply w-full md:w-[21.4rem] lg:w-[28.1rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-col gap-6 lg:gap-[1.9rem] p-[1.1rem] lg:pt-6 border-t border-[#E5E5E5];
li {
@apply flex justify-between items-center text-[#333333] font-medium font-vazir;
small {
@apply text-xs lg:text-base;
}
span {
@apply text-sm lg:text-xl;
}
}
}
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<ul class="seller-menu">
<li v-for="(item, i) in items" :key="i">
<router-link :to="item.link" v-ripple>
<i :class="item.icon" />
<span>{{ item.label }}</span>
</router-link>
</li>
</ul>
</template>
<script setup>
const items = reactive([
{ link: '/panel/seller/store', label: 'فروشگاه', icon: 'isax isax-shop' },
{ link: '/panel/seller/products', label: 'همه کالاها', icon: 'isax isax-bag' },
{ link: '/panel/seller/new-product', label: 'معرفی کالا', icon: 'isax isax-bag-cross' },
{ link: '/panel/seller/orders', label: 'سفارشات', icon: 'isax isax-bag-tick-2' },
])
</script>
<style lang="scss">
.seller-menu {
@apply flex flex-col gap-[0.3rem] pt-4 border-t border-[#E5E5E5];
a {
@apply w-72 h-[3.2rem] py-1.5 pr-[1.1rem] flex items-center gap-[0.9rem] text-[#333333] overflow-hidden relative;
&.router-link-active {
@apply bg-[#47B556]/20 text-[#47B556];
&:after {
@apply content-[''] absolute right-0 h-[2.4rem] w-[0.2rem] rounded-tl-sm rounded-bl-sm bg-[#47B556];
}
}
i {
@apply text-2xl;
}
span {
@apply text-base font-medium font-vazir
}
}
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<template>
<div class="seller-order">
<div>
<div>
<img :src="coverImage" alt="">
<h2>{{ title }}</h2>
</div>
<div>
<span>{{ $t('number') }} : {{ data.quantity }}</span>
<span>{{ $t('amount') }} : {{ numberFormat(data.price) }}</span>
</div>
</div>
<div>
<Tag v-bind="tag" rounded />
<div v-if="status == 'Pending'">
<Button icon="isax isax-tick-square" link severity="secondary" :loading="accepting" @click="accept()" />
<Button icon="isax isax-close-square" link severity="secondary" :loading="canceling"
@click="cancel()" />
</div>
<small v-else>{{ new Date(sendDate).toLocaleDateString('fa-IR') }}</small>
</div>
</div>
</template>
<script setup>
const props = defineProps(['data'])
let { product, status, _id: id, sendDate } = props.data
const { coverImage, title } = product
const { t } = inject('service')
const tags = reactive({
pending: { severity: 'warn', value: t('pending') },
accepted: { severity: 'info', value: t('accepted') },
received: { severity: 'success', value: t('received') },
returned: { severity: 'error', value: t('returned') },
canceled: { severity: 'error', value: t('canceled') },
})
const tag = ref(tags[status.toLowerCase()])
const accepting = ref(false)
const canceling = ref(false)
const accept = async () => {
accepting.value = true
const result = await useFetch('/api/shop/product/accept/' + id, {
headers: {
Authorization: useCookie('token')
}
})
accepting.value = false
if (result.status.value == 'success') {
status = 'accepted'
tag.value = tags[status]
sendDate = Date.now()
}
}
const cancel = async () => {
canceling.value = true
const result = await useFetch('/api/shop/product/cancel/' + id, {
headers: {
Authorization: useCookie('token')
}
})
canceling.value = false
if (result.status.value == 'success') {
status = 'canceled'
tag.value = tags[status]
sendDate = Date.now()
}
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.seller-order {
@apply w-full h-[12.1rem] rounded-[0.6rem] border border-[#E5E5E5] px-[1.1rem] lg:px-[1.9rem] flex flex-col;
&>div:first-child {
@apply w-full flex max-lg:flex-col max-lg:gap-[1.9rem] justify-between items-center py-[1.1rem] lg:py-5 border-b border-[#E5E5E5];
&>div:first-child {
@apply flex items-center gap-1.5 lg:gap-5;
img {
@apply w-[3.8rem] h-[3.8rem] lg:w-20 lg:h-20 bg-gray-200 shrink-0;
}
h2 {
@apply text-[#333333] text-sm lg:text-base font-medium font-vazir;
}
}
&>div:last-child {
@apply max-lg:w-full lg:h-full flex lg:flex-col justify-between items-end;
span {
@apply text-[#333333] text-xs lg:text-base font-normal font-vazir;
}
}
}
&>div:last-child {
@apply w-full h-full flex justify-between items-center;
.p-tag {
@apply h-[1.8rem];
}
&>div {
@apply flex gap-1.5 lg:gap-3;
.p-button {
@apply p-0 w-max h-max #{!important};
.p-button-icon {
@apply text-xl lg:text-2xl text-[#333333];
}
}
}
&>small {
@apply text-[#333333] text-sm lg:text-base font-vazir;
}
}
}
</style>
+101
View File
@@ -0,0 +1,101 @@
<template>
<div class="seller-chart">
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
<Chart type="pie" :data="chartData" :options="chartOptions" />
</div>
</template>
<script setup>
import Chart from 'primevue/chart';
const chartData = ref();
const chartOptions = ref();
const setChartData = () => {
const documentStyle = getComputedStyle(document.body);
return {
labels: ['سود', 'درآمد', 'فروش'],
datasets: [
{
data: [21451000, 20123000, 10000000],
backgroundColor: ['#FF6384', '#FF9F40', '#4BC0C0'],
hoverBackgroundColor: ['#FF6384cc', '#FF9F40cc', '#4BC0C0cc']
}
]
};
};
const setChartOptions = () => {
return {
plugins: {
legend: {
labels: {
usePointStyle: true,
color: '#333333',
pointStyle: 'rect',
pointStyleWidth: 20,
font: {
family: 'vazir',
size: 14
}
},
position: 'bottom',
rtl: true
},
tooltip: {
backgroundColor: '#050B20',
bodyFont: 'vazir',
bodyColor: 'white',
rtl: true,
displayColors: false,
callbacks: {
title: () => null
}
}
}
};
};
const { t } = inject('service')
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
onMounted(() => {
chartData.value = setChartData();
chartOptions.value = setChartOptions();
});
</script>
<style lang="scss">
.seller-chart {
@apply w-[21.4rem] h-[25.4rem] lg:w-[18.3rem] lg:h-[22.8rem] rounded-[0.6rem] border-2 border-[#E5E5E5] p-3;
@apply flex flex-col gap-[1.1rem] max-xl:hidden lg:hidden xl:flex;
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
</style>
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="seller-profile">
<span v-if="shop.rate > -1">
<small>{{ shop.rate }}</small>
<i class="isax isax-star-15" />
</span>
<div>
<picture>
<img :src="shop?.userID?.profilePhoto">
</picture>
<div>
<h2>{{ shop.shopName }}</h2>
<span>
{{ $t('registerDate') }} : {{ new Date(shop.createdAt).toLocaleDateString('fa-IR') }}
</span>
</div>
</div>
<PanelSellerMenu v-if="device == 'desktop'" />
</div>
</template>
<script setup>
const { device } = inject('service')
const { shop } = inject('data')
</script>
<style lang="scss">
.seller-profile {
@apply w-full md:w-[21.4rem] min-h-[6.4rem] lg:w-[18.3rem] lg:h-[30rem] rounded-[0.6rem] border-2 border-neutral-200 relative;
&>span {
@apply absolute left-4 top-4 lg:left-3 lg:top-3 flex items-center gap-2;
i {
@apply text-lg text-[#FFB628] -mt-0.5;
}
small {
@apply text-zinc-800 text-sm font-medium font-poppins;
}
}
&>div {
@apply flex lg:flex-col gap-[1.1rem] lg:gap-3 items-center max-lg:h-full max-lg:pr-4 lg:pt-7 lg:pb-4;
picture {
@apply flex w-[4.1rem] h-[4.1rem] lg:w-[5.6rem] lg:h-[5.6rem] rounded-full border border-[#47B556];
img {
@apply w-[3.6rem] h-[3.6rem] lg:w-[4.9rem] lg:h-[4.9rem] bg-zinc-300 rounded-full m-auto;
}
}
&>div {
@apply flex flex-col lg:items-center gap-5;
h2 {
@apply text-zinc-800 text-xs lg:text-base font-medium font-vazir;
}
span {
@apply text-zinc-500 text-[0.6em] lg:text-xs font-medium font-vazir;
}
}
}
}
</style>
+68
View File
@@ -0,0 +1,68 @@
<template>
<div class="seller-sales">
<h2>{{ $t(title + 'Sales') }}</h2>
<ul>
<li v-for="(item, i) in items[title]" :key="i">
<p>
{{ numberFormat(item.value) }} <span>{{ suffix[title] }}</span>
</p>
<span>{{ item.label }}</span>
</li>
</ul>
</div>
</template>
<script setup>
defineProps(['title'])
const {
salesCurrentWeek = 0, salesAgoWeek = 0, salseAgoMonth = 0,
oneDayAgo = 0, oneWeekAgo = 0, oneMonthAgo = 0
} = inject('data')
const items = reactive({
status: [
{ label: 'فروش هفته جاری', value: salesCurrentWeek },
{ label: 'فروش هفته گذشته', value: salesAgoWeek },
{ label: 'فروش ماه گذشته', value: salseAgoMonth },
],
number: [
{ label: 'تعداد کالای فروش رفته هفته جاری', value: oneDayAgo },
{ label: 'تعداد کالای فروش رفته هفته گذشته', value: oneWeekAgo },
{ label: 'تعداد کالای فروش رفته ماه گذشته', value: oneMonthAgo },
]
})
const suffix = reactive({ status: 'ریال', number: 'عدد' })
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss" scoped>
.seller-sales {
@apply w-full md:w-[21.4rem] lg:w-[28.1rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-col gap-6 lg:gap-[1.9rem] p-[1.1rem] lg:pt-6 border-t border-[#E5E5E5];
li {
@apply flex flex-col text-[#333333] [&>p]:first:text-[#47B556] font-medium font-vazir;
p {
@apply text-lg lg:text-2xl;
}
span {
@apply text-xs lg:text-base;
}
}
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="seller-score">
<h2>{{ $t('yourePerformanceScore') }}</h2>
<ul>
<li v-for="(item, i) in items" :key="i">
<small>{{ item.title }}</small>
<span>{{ item.percent }}%</span>
<svg viewBox="0 0 250 250" :style="{'--percent': item.percent }">
<circle v-for="j in 2" :key="j" />
</svg>
</li>
</ul>
</div>
</template>
<script setup>
const { shop } = inject('data')
const items = reactive([
{ title: 'رضایت مشتریان', percent: shop.rate * 10 },
{ title: 'تاخیر در ارسال', percent: shop.warn * 10 },
{ title: 'بازگشت کالا', percent: shop.returnedScore },
{ title: 'لغو سفارش', percent: shop.canceledScore },
])
</script>
<style lang="scss">
.seller-score {
@apply w-full md:w-[21.4rem] h-[10.1rem] lg:w-[18.3rem] lg:h-[21.9rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-wrap gap-2.5 lg:gap-x-[1.9rem] lg:gap-y-5 pt-[1.1rem] lg:pt-5 justify-center border-t border-[#E5E5E5];
li {
@apply w-[4.3rem] h-[4.3rem] lg:w-[6.9rem] lg:h-[6.9rem] relative flex flex-col items-center justify-center gap-2 rounded-full;
small {
@apply text-[#333333] text-[0.4em] lg:text-[0.6em] font-bold font-vazir mt-1;
}
span {
@apply text-[#333333] text-xs lg:text-base font-medium font-vazir;
}
svg {
@apply absolute inset-0;
--stroke-width: 5px;
@screen lg {
--stroke-width: 12px;
}
--radius: calc((250px - var(--stroke-width)) / 2);
circle {
cx: 125px;
cy: 125px;
r: var(--radius);
stroke-width: var(--stroke-width);
fill: none;
stroke-linecap: round;
}
circle:first-child {
stroke: #D9D9D9;
}
circle:last-child {
transition: stroke-dasharray 0.3s linear 0s;
stroke: #47B556;
transform-origin: 125px 125px;
--circumference: calc(var(--radius) * 3.14 * 2);
--dash: calc((var(--percent) * var(--circumference)) / 100);
stroke-dasharray: var(--dash) calc(var(--circumference) - var(--dash));
}
}
}
}
}
</style>
+212
View File
@@ -0,0 +1,212 @@
<template>
<DataTable :expandedRows="[]" :value="pendingProducts" class="seller-upcoming" :responsiveLayout="rl">
<template #header>
<h2>{{ $t('upcomingShipments') }}</h2>
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
</template>
<Column v-for="(col, i) in columns[device]" :key="i" v-bind="col" class="[&:first-child_button]:hidden">
<template #body="{ data, field }">
<span>{{ field(data) }}</span>
<Button v-if="device == 'mobile'" :label="$t('moreDetails')" icon="isax isax-arrow-left-2"
link @click="show(data)" />
</template>
</Column>
<Column v-if="device == 'desktop'" expander />
<template #expansion="{ data }">
<ul>
<li>
<span v-if="data.quantity > 1">
{{ data.quantity }}x
</span>
<img :src="data.product.coverImage">
</li>
</ul>
<Button :label="$t('showFactor')" icon="isax isax-receipt-item" iconPos="right" link />
</template>
<template #empty>
<p>{{ $t('notFound') }}</p>
</template>
</DataTable>
</template>
<script setup>
import PanelSellerDialogDetailsOrder from './dialog/details/Order.vue'
const { t, device, dialog } = inject('service')
const rl = computed(() => device.value == 'mobile' ? 'stack' : '')
const { pendingProducts = [] } = inject('data')
const columns = reactive({
desktop: [
{ header: t('row'), field: (item) => pendingProducts.indexOf(item) + 1 },
{ header: t('purchaseDate'), field: (item) => new Date(item.orderDate).toLocaleDateString() },
{ header: t('postageDate'), field: (item) => new Date(item.sendDate).toLocaleDateString() },
{ header: t('price'), field: (item) => new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(item.price) },
],
mobile: [
{ field: (item) => item.product.title },
{ field: (item) => new Date(item.sendDate).toLocaleDateString() },
]
})
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
const show = (data) => {
dialog.show(PanelSellerDialogDetailsOrder, data)
}
</script>
<style lang="scss">
.seller-upcoming {
@apply rtl border-2 border-[#E5E5E5] rounded-[0.6rem] overflow-hidden w-full;
.p-datatable-header {
@apply flex items-center justify-between bg-white pt-3 pl-3 pr-[1.1rem] pb-[1.1rem] lg:py-2.5 lg:px-5;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir;
}
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
.p-datatable-wrapper {
@apply max-lg:overflow-hidden #{!important};
.p-datatable-table {
@apply border-separate border-spacing-y-[0.8rem] px-[1.1rem] py-3;
.p-datatable-thead {
@apply w-full h-[3.1rem] max-lg:hidden;
th {
@apply text-[#5D5D5D] text-base font-normal font-vazir bg-[#F2F2F2];
@apply first:rounded-r-[0.6rem] last:rounded-l-[0.6rem];
}
}
.p-datatable-tbody {
@apply lg:translate-y-1.5;
tr {
@apply max-lg:h-[7.5rem] bg-transparent relative #{!important};
&:not(.p-datatable-row-expansion)::after {
@apply content-['_'] absolute inset-x-0 z-0 max-lg:inset-y-0 lg:h-[3.8rem] rounded-[0.6rem];
@apply shadow-[0px_0px_6px_#0000001f];
}
td {
@apply max-lg:w-full max-lg:px-3 text-right last:text-left #{!important};
@apply lg:first:rounded-r-[0.6rem] lg:last:rounded-l-[0.6rem] py-0 align-top lg:border-none;
&>span {
@apply max-lg:py-3 min-h-[3.8rem] flex items-center text-xs lg:text-base text-[#333333] font-vazir;
}
.p-row-toggler {
@apply mt-3.5 z-10;
}
@media(max-width: 1024px) {
&>button {
@apply z-[1] -m-3 mr-auto #{!important};
.p-button-label {
@apply text-xs font-medium font-vazir;
}
.p-button-icon {
@apply text-sm mr-1;
}
}
&:first-child span {
@apply max-lg:w-full max-lg:border-b;
}
.p-column-title {
@apply hidden #{!important};
}
}
}
&.p-datatable-row-expansion {
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
animation: fade 0.2s ease 0.1s forwards;
@apply opacity-0 -translate-y-3 shadow-none;
ul {
@apply flex items-center gap-[1.1rem] rtl h-[9.9rem] border-y border-[#E5E5E5];
li {
@apply flex flex-col items-center justify-end h-[7.3rem] gap-3;
span {
@apply text-[#333333] text-base font-normal font-poppins
}
img {
@apply w-20 h-20;
}
}
}
button {
@apply float-left;
}
}
&:has(+.p-datatable-row-expansion)::after {
@apply h-[17.5rem] transition-[height];
}
}
.p-datatable-emptymessage p {
@apply lg:h-[3.8rem] flex justify-center items-center
}
}
}
}
}
</style>
@@ -0,0 +1,39 @@
<template>
<div class="upload-national-card" @click="showImages">
<img v-if="source" :src="source" />
<i v-else class="isax isax-document-upload"></i>
<input ref="input" type="file" hidden @input="chooseImage">
</div>
</template>
<script setup>
const file = defineModel()
const input = ref()
const source = ref()
const filename = ref()
const showImages = () => input.value.click();
const chooseImage = (e) => {
const binaryData = [];
binaryData.push(e.target.files[0]);
source.value = URL.createObjectURL(new Blob(binaryData, { type: "image" }))
file.value = e.target.files[0];
}
</script>
<style lang="scss">
.upload-national-card {
@apply cursor-pointer min-h-14 h-max relative w-[21.8rem] lg:w-[27.1rem] border border-[#CCCCCC] rounded-[0.6rem] p-4 rtl;
i {
@apply text-xl float-left;
}
img {
@apply w-full h-[11.6rem] lg:h-[14.4rem] object-cover object-center rounded-[0.6rem];
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<div class="seller-add-spec">
<div>
<div>
<h2>{{ $t("addSpec") }}</h2>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
</div>
<p>{{ dialog.data.title }}</p>
</div>
<ul>
<li>
<InputText v-model="form.key" placeholder id="key" />
<label for="key">{{ $t('title') }}</label>
</li>
<li>
<Textarea v-model="form.value" placeholder id="value" />
<label for="value">{{ $t("description") }}</label>
</li>
</ul>
<Button :label="$t('submitSpec')" @click="add()" />
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const form = reactive({})
const add = () => {
dialog.value.data.specs[form.key] = form.value
dialog.value.close();
}
</script>
<style lang="scss">
.seller-add-spec {
@apply w-full lg:w-[40.1rem] lg:h-[38.6rem] bg-white rounded-t-[0.6rem] lg:rounded-[0.6rem] flex flex-col relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>div {
@apply flex flex-col gap-1.5 lg:h-[7.8rem] lg:gap-3 border-b lg:border-b-2 border-[#E5E5E5] p-6 lg:px-10 lg:pt-8 pb-6;
&>div {
@apply flex justify-between items-center;
h2 {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-[1.4em] lg:text-3xl text-[#191919];
}
}
}
p {
@apply text-[#7F7F7F] text-[0.7em] lg:text-lg font-normal font-vazir;
}
}
&>ul {
@apply flex flex-col h-full items-center gap-[1.1rem] px-6 pt-9 lg:gap-5 lg:pt-6 lg:px-10;
li {
@apply flex items-center gap-4 cursor-pointer w-max select-none relative;
@apply w-[21.4rem] lg:w-[27.1rem];
label {
@apply w-max absolute right-3 top-4 lg:top-3.5 px-1 bg-white transition-['top'];
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text;
@apply pointer-events-none;
}
input, textarea {
@apply h-[3.4rem] pl-10 rtl w-full rounded-[0.6rem] border shadow-none;
@apply text-[#4C4C4C] font-vazir grow;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(input:not(:placeholder-shown)),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-[#4C4C4C] text-xs font-normal font-vazir;
}
}
}
li:last-child {
@apply w-full lg:w-[27.1rem] h-full flex flex-col items-start gap-4 mt-3 lg:mt-4;
// label {
// @apply text-[#7F7F7F] text-xs lg:text-xl font-medium font-vazir max-lg:mr-2.5;
// }
textarea {
@apply w-full h-[9.6rem] lg:h-full p-4 rounded-[0.6rem] border border-[#E5E5E5];
// &::placeholder {
// @apply text-[#999999] text-[0.7em] lg:text-lg font-normal font-vazir;
// }
}
}
}
&>button {
@apply h-[2.9rem] m-6 mt-8 lg:h-14 lg:m-10 lg:mt-9 shrink-0 #{!important};
.p-button-label {
@apply font-bold font-iran-sans text-sm lg:text-base;
}
}
}
</style>
@@ -0,0 +1,31 @@
<template>
<div class="seller-product-added">
<div>
<i class="isax isax-tick-square"></i>
<h2>{{ $t('productAdded') }}</h2>
</div>
<Button :label="$t('confirm')" />
</div>
</template>
<script setup>
</script>
<style lang="scss">
.eller-product-added {
@apply flex flex-col justify-between pt-[1.1rem] max-lg:h-[11.8rem] lg:w-[28.3rem] lg:h-64 lg:p-10;
div {
@apply flex max-lg:flex-col items-center gap-4 lg:mt-4;
h2 {
@apply text-[#333333] text-xl font-bold font-vazir leading-[2.8rem];
}
}
button {
@apply w-full h-12 lg:h-14;
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<div class="seller-details-best-selling">
<div>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
<h2>{{ title }}</h2>
<img :src="coverImage">
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const { product, quantity, total } = dialog.value.data
const { title, coverImagem, stockQuantity} = product
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
const items = reactive({
salesNumber: quantity,
salesPrice: numberFormat(total),
inventory: stockQuantity,
status: "دارای رقیب",
})
</script>
<style lang="scss">
.seller-details-best-selling {
@apply flex flex-col bg-white rounded-t-[0.6rem] gap-[1.9rem] px-6 py-[1.1rem] rtl;
&>div {
@apply flex gap-1.5;
h2 {
@apply w-56 text-[#333333] text-sm font-medium font-vazir pt-0.5;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-lg text-[#B2B2B2] #{!important};
}
}
img {
@apply w-[3.8rem] h-[3.8rem] mr-auto;
}
}
&>ul {
@apply flex flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
}
</style>
@@ -0,0 +1,98 @@
<template>
<div class="seller-details-order">
<div>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
<h2>{{ $t('orderDetails') }}</h2>
<Button icon="isax isax-receipt-item" iconPos="right" link :label="$t('showFactor')" @click="show()" />
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
<ul>
<li v-for="(product, k) in [product]" :key="k">
<span v-if="product.quantity > 1">
{{ product.quantity }}x
</span>
<img :src="product.coverImage">
</li>
</ul>
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const { total, product} = dialog.value.data
const items = reactive({
address: ".........................................................................",
receiver: "علی مصلحی",
purchaseDate: "1402/05/13",
postageDate: "1402/05/20",
price: "252,000",
})
</script>
<style lang="scss">
.seller-details-order {
@apply flex flex-col bg-white rounded-t-[0.6rem] gap-[1.1rem] px-6 pt-[1.1rem] rtl;
&>div {
@apply flex items-center gap-1.5;
h2 {
@apply text-[#333333] text-sm font-medium font-vazir;
}
&>button {
@apply -mx-4 -my-3 last:mr-auto;
&:first-child {
@apply text-lg text-[#B2B2B2] #{!important};
}
&:last-child{
@apply text-[#47B556] font-vazir;
.p-button-label{
@apply text-xs;
}
}
}
}
&>ul:first-of-type {
@apply flex flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
&>ul:last-of-type {
@apply flex items-center gap-[1.1rem] pb-[1.1rem] border-t border-[#E5E5E5];
li {
@apply flex flex-col h-28 items-center justify-end gap-2.5;
span {
@apply text-[#333333] text-xs font-normal font-poppins
}
img {
@apply w-[3.8rem] h-[3.8rem];
}
}
}
}
</style>
@@ -0,0 +1,62 @@
<template>
<div class="seller-details-product">
<div>
<h2>{{ title }}</h2>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const { title = 'هارددیسک اکسترنال ای دیتا مدل HM900 ظرفیت 4 ترابایت' } = reactive({})
const items = reactive({
salesPrice: "242,523",
inventory: "1",
status: "دارای رقیب",
})
</script>
<style lang="scss">
.seller-details-product {
@apply flex flex-col gap-[1.9rem] px-6 py-[1.1rem] rtl;
&>div {
@apply flex items-center justify-between gap-1.5;
h2 {
@apply w-56 text-[#333333] text-sm font-medium font-vazir;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-lg text-[#B2B2B2] #{!important};
}
}
}
&>ul {
@apply felx flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<section class="seller-first-step">
<ul>
<li v-for="({ is, props, label }, key) in fields.first" :key="key">
<Editor v-if="key == 'description'" v-bind="props" v-model="form[key]"/>
<component v-else :is="is" v-bind="props" v-model="form[key]" />
<label :for="key"> {{ label }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
</ul>
<div>
<Button v-bind="btns.next.props" @click="next()" />
</div>
</section>
</template>
<script setup>
import Editor from 'primevue/editor';
const { t } = inject('service')
const { fields, btns } = inject('init')
const form = inject('form')
const step = inject('step')
const { brands } = inject('data')
const errors = ref({})
fields.first.brandID.props.options = brands
const categories = useCategoriesStore()
fields.first.subCategory.props.options = computed(() => {
let items = []
categories.items.forEach((item) => {
if (item.sub) {
item.sub.forEach((item2) => {
if (item2._id == form.category) {
items = item2.sub ?? [item2]
}
})
}
})
return items;
})
fields.first.minBuy.props.disabled = computed(() => !form.boxSeller)
fields.first.maxBuy.props.disabled = computed(() => !form.boxSeller)
fields.first.itemInBox.props.disabled = computed(() => !form.boxSeller)
if (form.boxSeller) {
const { min = 1, max = 20 } = form?.orderRange
form.minBuy = min
form.maxBuy = max
}
watch(
computed(() => Object.assign({}, form)),
(value, old) => {
Object.keys(errors.value).forEach((key) => {
if (value[key] != old[key])
delete errors.value[key]
})
},
{ deep: true })
const next = () => {
errors.value = {}
Object.keys(fields.first).forEach((key) => {
if (!['localSend', 'boxSeller', 'warranty'].includes(key) && !fields.first[key].props.disabled) {
if ([undefined, null, ''].includes(form[key])) {
errors.value[key] = t('required')
}
}
})
if (Object.keys(errors.value).length < 1)
step.value++;
}
</script>
<style lang="scss">
.seller-first-step {
@apply w-full flex flex-col pb-20 gap-8 lg:gap-[3.8rem];
&>ul {
@apply grid grid-cols-2 lg:grid-cols-4 gap-x-3 gap-y-6 lg:gap-x-6 lg:gap-y-10 w-full max-w-full;
li {
@apply w-full h-14 relative max-xl:col-span-2;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] select-none;
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text pointer-events-none;
}
input,
textarea {
@apply rtl w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(.p-inputwrapper-filled),
&:has(input:not(:placeholder-shown)),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal;
}
}
.p-dropdown,
.p-inputnumber {
@apply w-full;
input {
@apply ltr text-left;
}
}
.p-dropdown-label {
@apply font-vazir pt-4;
}
&:nth-of-type(2) {
label {
@apply left-3 right-auto;
}
}
&:nth-of-type(1),
&:nth-of-type(2) {
@apply col-span-2;
}
&:nth-of-type(11) {
@apply col-span-2 lg:col-span-3 row-span-3 h-full max-lg:order-last;
textarea {
@apply grow h-full;
}
}
&:has(.p-checkbox) {
@apply border border-[#CCCCCC] rounded-[0.6rem] flex flex-row-reverse justify-between items-center px-4;
label {
@apply static text-sm lg:text-base text-neutral-400 h-max pointer-events-auto cursor-pointer #{!important};
}
.p-checkbox {
@apply w-[1.1rem] lg:w-5 shrink-0;
}
}
&:nth-of-type(9),
&:nth-of-type(10),
&:nth-of-type(12),
&:nth-of-type(13) {
@apply max-lg:col-span-1;
}
small {
@apply text-red-500 font-vazir;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
&:has(.p-overlay-open)::after {
@apply content-['_'] inset-0 fixed bg-black/20 z-[1];
}
&:has(.p-disabled),
&:has([disabled]) {
@apply max-lg:hidden lg:invisible;
}
.p-editor-content{
@apply rtl text-right;
}
}
}
&>div {
@apply inset-x-0 bottom-0 flex gap-3 w-full lg:w-max self-end;
@apply max-lg:fixed max-lg:border-t max-lg:shadow-[0_-2px_5px_#0000000a];
@apply h-[4.8rem] px-6 pt-3 lg:p-0 lg:h-max bg-white;
&>button {
@apply w-full h-[2.9rem] lg:w-[11.4rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
</style>
+122
View File
@@ -0,0 +1,122 @@
<template>
<section class="seller-second-step">
<ul>
<li v-for="(_, key) in form.specs" :key="key">
<IconField iconPosition="left">
<InputText :id="key" placeholder v-model="form.specs[key]" />
<InputIcon class="isax isax-close-circle" @click="delete form.specs[key]" />
</IconField>
<label :for="key"> {{ key }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
<li @click="handleClick()" v-ripple>
<span>{{ $t('addSpec') }}</span>
<i class="isax isax-box-add" />
</li>
</ul>
<div>
<Button v-bind="btns.prev.props" @click="step--" />
<Button v-bind="btns.next.props" :disabled="disabled" @click="next()" />
</div>
</section>
</template>
<script setup>
const { dialog, t } = inject('service')
const { btns } = inject('init')
const form = inject('form')
const step = inject('step')
const disabled = computed(() => form.specs.length > 0)
const errors = ref({})
const next = () => {
errors.value = {}
Object.entries(form.specs).forEach(([key, value]) => {
if ([undefined, null, ''].includes(value))
errors.value[key] = t('required')
})
if (Object.keys(errors.value).length < 1)
step.value++;
}
const handleClick = () => {
dialog.show(resolveComponent('PanelSellerDialogAddSpec'), form)
}
</script>
<style lang="scss">
.seller-second-step {
@apply w-full flex flex-col gap-8 lg:gap-[3.8rem];
ul {
@apply grid lg:grid-cols-3 gap-x-3 gap-y-6 lg:gap-x-6 lg:gap-y-10 w-full max-w-full;
li {
@apply w-full h-14 relative;
label {
@apply w-max absolute right-3 max-lg:translate-y-0.5 top-4 px-1 bg-white transition-['top'] select-none;
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text pointer-events-none;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir rtl;
}
.p-input-icon {
@apply text-lg lg:text-xl h-max -translate-y-[20%] cursor-pointer;
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal;
}
}
.p-inputnumber {
@apply w-full;
}
&:last-child {
@apply cursor-pointer border border-[#CCCCCC] rounded-[0.6rem] px-4 pointer-events-auto;
@apply flex justify-between items-center overflow-hidden relative;
span {
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir;
}
i {
@apply text-[#47B556] text-2xl;
}
}
small {
@apply text-red-500 font-vazir;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
&>div {
@apply inset-x-0 bottom-0 flex gap-3 w-full lg:w-max self-end;
@apply max-lg:fixed max-lg:border-t max-lg:shadow-[0_-2px_5px_#0000000a];
@apply h-[4.8rem] px-6 pt-3 lg:p-0 lg:h-max bg-white;
&>button {
@apply w-full h-[2.9rem] lg:w-[11.4rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
</style>
+171
View File
@@ -0,0 +1,171 @@
<template>
<section class="seller-thirdy-step">
<ul>
<li>
<UploadImage v-model="form.coverImage" cover @update:modelValue="delete errors.coverImage" />
<small v-if="errors.coverImage">{{ errors.coverImage }}</small>
</li>
<li v-for="(_, i) in form.images" :key="i">
<UploadImage v-model="form.images[i]" @update:modelValue="update" />
<small v-if="errors.images && i < 1">{{ errors.images }}</small>
</li>
</ul>
<div>
<Button v-bind="btns.prev.props" @click="step--" />
<Button v-bind="btns.submit.props" :loading="loading" @click="submit()" />
</div>
</section>
</template>
<script setup>
import UploadImage from '../َUploadImage.vue'
const { btns } = inject('init')
const { toast, t } = inject('service')
const router = useRouter()
const form = inject('form')
const step = inject('step')
const loading = ref(false)
const errors = ref({})
const update = () => {
form.images = form.images.filter((image) => image != null)
form.images.push(null)
delete errors.value.images;
}
const submit = async () => {
errors.value = {}
const images = form.images.filter((image) => image != null)
if (!form.coverImage)
errors.value.coverImage = t('required')
if (images.length < 1)
errors.value.images = t('required')
if (Object.keys(errors.value).length < 1) {
loading.value = true
const body = Object.assign({}, form)
body.images = body.images.filter((image) => image)
const { minBuy: min, maxBuy: max } = body
body.orderRange = { min, max }
delete body.minBuy
delete body.maxBuy
//Upload File
if (typeof body.coverImage === 'object') {
const formData = new FormData()
formData.append('images', form.coverImage)
const { status, data, error } = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie("token") }, params: { type: 1 }, body: formData, method: 'post'
})
if (status.value == 'success')
body.coverImage = data.value.urls[0]
else {
loading.value = false
return toast.add({
life: 2000, severity: 'error', summary: t('error'), detail: error.value.msg
})
}
}
//upload images
const files = body.images.filter((image) => typeof image === 'object')
if (files.length > 0) {
const formData = new FormData()
files.forEach((image, i) => {
formData.append('images', image)
});
const { status, data, error } = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie("token") }, params: { type: 1 }, body: formData, method: 'post'
})
if (status.value == 'success') {
body.images = body.images.map((image) => {
if (typeof image === 'object')
return data.value.urls.shift();
return image;
})
} else {
loading.value = false
return toast.add({
life: 2000, severity: 'error', summary: t('error'), detail: error.value.msg
})
}
}
let endPoint = '/api/products'
let method = 'post'
if (body.id) {
endPoint += '/' + body.id
method = 'put'
}
const { status, data, error } = await useFetch(endPoint, {
headers: { Authorization: useCookie("token") }, method, body
})
if (status.value == 'success')
router.push('/panel/seller/products')
else if (error.value.statusCode == 422)
errors.value = error.value.data
else toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.message
})
loading.value = false
}
}
onMounted(() => {
if (form.id) update()
})
</script>
<style lang="scss">
.seller-thirdy-step {
@apply w-full flex flex-col gap-8 pb-20 lg:gap-[3.8rem];
&>ul {
@apply flex flex-wrap gap-6;
li {
small {
@apply text-red-500 font-vazir;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
&>div {
@apply inset-x-0 bottom-0 flex gap-3 w-full lg:w-[27.1rem] self-end;
@apply max-lg:fixed max-lg:border-t max-lg:shadow-[0_-2px_5px_#0000000a];
@apply h-[4.8rem] px-6 pt-3 lg:p-0 lg:h-max bg-white;
&>button {
@apply w-full h-[2.9rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
</style>
+82
View File
@@ -0,0 +1,82 @@
<template>
<div class="seller-upload-image">
<div v-if="!source" @drop.prevent="chooseImage('dataTransfer', $event)" @dragover.prevent>
<i class="isax isax-gallery-export"></i>
<p>{{ 'cover' in $attrs ? $t('coverImage') : '&nbsp;' }}</p>
<h2>{{ $t('forUploadImages') }} </h2>
<span @click="showImages()">{{ $t('clickHere') }}</span>
</div>
<div v-else>
<img :src="source" />
<Button :label="$t('placement')" severity="secondary" text raised @click="showImages()" />
<Button :label="$t('delete')" severity="secondary" text raised @click="deleteImage()" />
</div>
<input ref="input" type="file" hidden @input="chooseImage('target', $event)">
</div>
</template>
<script setup>
const image = defineModel()
const input = ref()
const source = ref()
const showImages = () => input.value.click();
const deleteImage = () => {
image.value = null
source.value = null
input.value.value = null
}
const chooseImage = (name, e) => {
const binaryData = [];
binaryData.push(e[name].files[0]);
source.value = URL.createObjectURL(new Blob(binaryData, { type: "image" }))
image.value = e[name].files[0];
}
onMounted(() => {
if(image.value) source.value = image.value
})
</script>
<style lang="scss">
.seller-upload-image {
@apply w-[21.4rem] h-[21.4rem] lg:w-[24.8rem] lg:h-[24.8rem] bg-white;
&>div:not(:has(img)) {
@apply h-full flex flex-col justify-center items-center border border-[#47B556] border-dashed rounded-[0.6rem];
i {
@apply text-[3.1em] lg:text-[3.8em] text-[#47B556];
}
p {
@apply text-[#7F7F7F] text-[0.6em] lg:text-sm font-medium font-vazir mt-6;
}
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir mt-1.5;
}
span {
@apply text-[#47B556] text-sm lg:text-lg font-medium font-vazir cursor-pointer;
}
}
&>div:has(img) {
@apply h-full flex flex-col gap-3 justify-center items-center relative;
img {
@apply h-full w-full absolute object-cover rounded-[0.6rem];
}
button {
@apply w-[7.5rem] h-[3.1rem] bg-white z-[1];
}
&::after{
@apply bg-[#47B556]/20 absolute content-['_'] inset-0 rounded-[0.6rem];
}
}
}
</style>