Browse Source

new changes

master
unknown 1 year ago
parent
commit
a6f34be446
56 changed files with 8445 additions and 1557 deletions
  1. +2
    -1
      .env
  2. +18
    -0
      package-lock.json
  3. +2
    -0
      package.json
  4. BIN
      src/assets/custom/دسته بندی ها.png
  5. BIN
      src/assets/custom/صفحه اصلی.png
  6. +73
    -12
      src/components/customSidebar.vue
  7. +4
    -7
      src/components/modals/Brands/addBrand.vue
  8. +7
    -24
      src/components/modals/Brands/editBrand.vue
  9. +7
    -9
      src/components/modals/addUser.vue
  10. +5
    -8
      src/components/modals/attribute/addAttribute.vue
  11. +4
    -7
      src/components/modals/attribute/editAttribute.vue
  12. +5
    -14
      src/components/modals/blogCat/addBlogCat.vue
  13. +9
    -16
      src/components/modals/blogCat/editBlogCat.vue
  14. +78
    -10
      src/components/modals/categories/addCat.vue
  15. +102
    -28
      src/components/modals/categories/editCat.vue
  16. +9
    -8
      src/components/modals/commonModals/showDescription.vue
  17. +175
    -0
      src/components/modals/commonModals/showText.vue
  18. +6
    -9
      src/components/modals/editUser.vue
  19. +130
    -0
      src/components/modals/helperModals/catBanner.vue
  20. +130
    -0
      src/components/modals/helperModals/mainPageBanner.vue
  21. +290
    -0
      src/components/modals/identity/addIdentity.vue
  22. +316
    -0
      src/components/modals/identity/editIdentity.vue
  23. +164
    -0
      src/components/modals/profile/accountInfo.vue
  24. +221
    -0
      src/components/modals/profile/addAddress.vue
  25. +146
    -0
      src/components/modals/profile/addressList.vue
  26. +44
    -0
      src/components/navbar.vue
  27. +79
    -0
      src/router/routes.js
  28. +1622
    -879
      src/views/live-preview/application/e-commerce/product-list.vue
  29. +3
    -2
      src/views/live-preview/pages/attributes/attributes.vue
  30. +539
    -0
      src/views/live-preview/pages/banners/addBanner.vue
  31. +473
    -0
      src/views/live-preview/pages/banners/banners.vue
  32. +577
    -0
      src/views/live-preview/pages/banners/editBanner.vue
  33. +2
    -1
      src/views/live-preview/pages/blogCats/blogCat.vue
  34. +10
    -12
      src/views/live-preview/pages/blogs/addBlog.vue
  35. +3
    -3
      src/views/live-preview/pages/blogs/blogs.vue
  36. +10
    -12
      src/views/live-preview/pages/blogs/editBlog.vue
  37. +2
    -2
      src/views/live-preview/pages/brands/brands.vue
  38. +546
    -0
      src/views/live-preview/pages/calls/calls.vue
  39. +121
    -74
      src/views/live-preview/pages/catrgories/cats.vue
  40. +7
    -13
      src/views/live-preview/pages/comments/comments.vue
  41. +43
    -34
      src/views/live-preview/pages/discounts/addDiscount.vue
  42. +6
    -5
      src/views/live-preview/pages/discounts/discounts.vue
  43. +30
    -27
      src/views/live-preview/pages/discounts/editDiscount.vue
  44. +1
    -1
      src/views/live-preview/pages/faqs/editFaqs.vue
  45. +4
    -4
      src/views/live-preview/pages/faqs/faqs.vue
  46. +451
    -0
      src/views/live-preview/pages/identity/idenities.vue
  47. +304
    -0
      src/views/live-preview/pages/orders/allOrdersItems.vue
  48. +466
    -0
      src/views/live-preview/pages/orders/approvedOrders.vue
  49. +128
    -5
      src/views/live-preview/pages/orders/orders.vue
  50. +108
    -141
      src/views/live-preview/pages/orders/singleOrder.vue
  51. +219
    -46
      src/views/live-preview/pages/products/addProduct.vue
  52. +574
    -104
      src/views/live-preview/pages/products/editProduct.vue
  53. +12
    -28
      src/views/live-preview/pages/products/products.vue
  54. +146
    -0
      src/views/live-preview/pages/profile/profile.vue
  55. +3
    -3
      src/views/live-preview/pages/test
  56. +9
    -8
      src/views/live-preview/pages/users/users.vue

+ 2
- 1
.env View File

@@ -1 +1,2 @@
VUE_APP_ROOT_URL="http://192.168.1.198:8000/api/v1/"
VUE_APP_ROOT_URL="https://api.novinplast.org/api/v1/"
COLOR_ATTRIBUTE_ID = "1"

+ 18
- 0
package-lock.json View File

@@ -39,6 +39,7 @@
"form-wizard-vue3": "^1.1.0",
"fslightbox": "^3.4.1",
"is": "^3.3.0",
"jalaali-js": "^1.2.7",
"jalali-moment": "^3.3.11",
"jquery": "^3.7.1",
"moment": "^2.30.1",
@@ -64,6 +65,7 @@
"vue3-datepicker": "^0.4.0",
"vue3-google-map": "^0.18.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-select2-component": "^0.1.7",
"vue3-toastify": "^0.2.5",
"vuex": "^4.1.0",
"yarn": "^1.22.21"
@@ -11982,6 +11984,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/select2": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz",
"integrity": "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw==",
"license": "MIT"
},
"node_modules/selfsigned": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
@@ -13689,6 +13697,16 @@
"moment-jalaali": "^0.9.4"
}
},
"node_modules/vue3-select2-component": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/vue3-select2-component/-/vue3-select2-component-0.1.7.tgz",
"integrity": "sha512-8UPZmFl02I47XwW+j8ICmHyAND8wfbavlvMrqh10wwdSVER1aF2kI9XCmfIrNlVQvxSbvc0mJjWQAHHSCW9dkw==",
"license": "MIT",
"dependencies": {
"jquery": "^3.3.1",
"select2": "^4.0.7-rc.0"
}
},
"node_modules/vue3-toastify": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/vue3-toastify/-/vue3-toastify-0.2.5.tgz",


+ 2
- 0
package.json View File

@@ -39,6 +39,7 @@
"form-wizard-vue3": "^1.1.0",
"fslightbox": "^3.4.1",
"is": "^3.3.0",
"jalaali-js": "^1.2.7",
"jalali-moment": "^3.3.11",
"jquery": "^3.7.1",
"moment": "^2.30.1",
@@ -64,6 +65,7 @@
"vue3-datepicker": "^0.4.0",
"vue3-google-map": "^0.18.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-select2-component": "^0.1.7",
"vue3-toastify": "^0.2.5",
"vuex": "^4.1.0",
"yarn": "^1.22.21"


BIN
src/assets/custom/دسته بندی ها.png View File

Before After
Width: 10610  |  Height: 15546  |  Size: 954 KiB

BIN
src/assets/custom/صفحه اصلی.png View File

Before After
Width: 10610  |  Height: 15546  |  Size: 994 KiB

+ 73
- 12
src/components/customSidebar.vue View File

@@ -30,7 +30,6 @@ export default {
const logoutUser = async () => {
try {
const result = await Swal.fire({
title: "آیا مطمئن هستید؟",
text: "شما از سیستم خارج خواهید شد.",
icon: "warning",
showCancelButton: true,
@@ -51,6 +50,10 @@ export default {
}
};

const gotoAccount = () => {
router.push({ name: "profile" });
};

onMounted(() => {
updateLogo();

@@ -68,7 +71,7 @@ export default {
});
});

return { currentLogo, user, logoutUser };
return { currentLogo, user, logoutUser, gotoAccount };
},
components: {
ChevronDownIcon,
@@ -134,7 +137,6 @@ export default {
let collapses = document.querySelectorAll(".navbar-content .collapse");

collapses.forEach((collapse) => {
// Hide sibling collapses on `show.bs.collapse`
collapse.addEventListener("show.bs.collapse", (e) => {
e.stopPropagation();
let closestCollapse = collapse.parentElement.closest(".collapse");
@@ -246,6 +248,17 @@ export default {
<span class="pc-mtext">کاربران</span></router-link
>
</li>
<li
class="pc-item"
:class="{ active: this.$route.path === '/banners' }"
>
<router-link to="/banners" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-flag"></i>
</span>
<span class="pc-mtext">بنر ها</span></router-link
>
</li>
<li class="pc-item" :class="{ active: this.$route.path === '/brands' }">
<router-link to="/brands" class="pc-link">
<span class="pc-micon">
@@ -266,6 +279,17 @@ export default {
<span class="pc-mtext">ویژگی ها</span></router-link
>
</li>
<li
class="pc-item"
:class="{ active: this.$route.path === '/idenities' }"
>
<router-link to="/idenities" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-barcode"></i>
</span>
<span class="pc-mtext">مشخصات</span></router-link
>
</li>
<li class="pc-item" :class="{ active: this.$route.path === '/blogs' }">
<router-link to="/blogs" class="pc-link">
<span class="pc-micon">
@@ -296,13 +320,44 @@ export default {
<span class="pc-mtext">تخفیف ها</span></router-link
>
</li>
<li class="pc-item" :class="{ active: this.$route.path === '/orders' }">
<router-link to="/orders" class="pc-link">

<li class="pc-item pc-hasmenu">
<BLink
class="pc-link"
data-bs-toggle="collapse"
href="#collapse26"
role="button"
aria-expanded="false"
aria-controls="collapse26"
>
<span class="pc-micon">
<i class="ph-duotone ph-shopping-cart"></i>
</span>
<span class="pc-mtext">سفارشات</span></router-link
>
<span class="pc-mtext">سفارشات</span
><span class="pc-arrow">
<ChevronDownIcon></ChevronDownIcon>
</span>
</BLink>
<div class="collapse" id="collapse26">
<ul class="pc-submenu">
<li
class="pc-item"
:class="{ active: this.$route.path === '/orders' }"
>
<router-link to="/orders" class="pc-link">
<span class="pc-mtext">سفارشات</span></router-link
>
</li>
<li
class="pc-item"
:class="{ active: this.$route.path === '/approvedOrders' }"
>
<router-link to="/approvedOrders" class="pc-link">
<span class="pc-mtext">آیتم های تایید شده</span></router-link
>
</li>
</ul>
</div>
</li>
<li
class="pc-item"
@@ -315,10 +370,7 @@ export default {
<span class="pc-mtext">نظرات</span></router-link
>
</li>
<li
class="pc-item"
:class="{ active: this.$route.path === '/faqs' }"
>
<li class="pc-item" :class="{ active: this.$route.path === '/faqs' }">
<router-link to="/faqs" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-file-text"></i>
@@ -365,6 +417,15 @@ export default {
</div>
</li>

<li class="pc-item" :class="{ active: this.$route.path === '/calls' }">
<router-link to="/calls" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-clock"></i>
</span>
<span class="pc-mtext">پیگیری</span></router-link
>
</li>

<!-- other -->
</ul>
</simplebar>
@@ -392,7 +453,7 @@ export default {
</span>
</template>
<BRow xl="6">
<BCol xl="6">
<BCol @click="gotoAccount()" xl="6">
<BDropdownItem class="pc-user-links p-0">
<i class="ph-duotone ph-user"></i>
<br />


+ 4
- 7
src/components/modals/Brands/addBrand.vue View File

@@ -128,7 +128,6 @@

<script>
import { ref } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -226,12 +225,10 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `افزودن برند با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!مشکلی در ایجاد برند پیش آمد", {
position: "top-right",
autoClose: 1000
});
})
.finally(() => {


+ 7
- 24
src/components/modals/Brands/editBrand.vue View File

@@ -60,13 +60,7 @@
alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview"
/>
<button
type="button"
@click="removeImage()"
class="delete-btn"
>
<i class="fa fa-trash f-16"></i>
</button>
</div>

<small v-if="errors.localImage" class="text-danger">
@@ -129,7 +123,7 @@
<script>
import { ref, toRef, watch } from "vue";
import ApiServiece from "@/services/ApiService";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";

@@ -206,15 +200,7 @@ export default {
(newVal) => (localId.value = newVal)
);

const removeImage = () => {
localImage.value = null;
imagePreview.value = null;

const fileInput = document.querySelector('input[type="file"]');
if (fileInput) {
fileInput.value = "";
}
};

const validateForm = () => {
errors.value = {};
@@ -265,12 +251,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `ویرایش برند با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!ویرایش برند با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
@@ -288,7 +271,7 @@ export default {
localImage,
handleImageChange,
imagePreview,
removeImage,
};
},
};


+ 7
- 9
src/components/modals/addUser.vue View File

@@ -63,8 +63,9 @@
class="form-select"
v-model="role"
@change="clearError('role')"
placeholder="نوع کاربر"
>
<option disabled value="">نوع کاربر</option>
<option value="admin">مدیر</option>
<option value="client">مشتری</option>
</select>
@@ -140,7 +141,7 @@

<script>
import { ref } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -204,13 +205,10 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `افزودن کاربر با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
confirmButtonText: "باشه",
toast.error("!افزودن کاربر با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 5
- 8
src/components/modals/attribute/addAttribute.vue View File

@@ -100,7 +100,6 @@

<script>
import { ref, toRef, watch } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -145,7 +144,8 @@ export default {
console.log(localAttributeValues.value);
formData.append("title", colorName.value);
formData.append("code", colorCode.value);
formData.append("attribute_id", localAttributeValues.value[0].id);
console.log(localAttributeValues)
formData.append("attribute_id", 1);

ApiServiece.post(`admin/attribute-values`, formData)
.then((resp) => {
@@ -163,12 +163,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `افزودن ویژگی با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.success("!مشکلی در ایجاد ویژگی پیش آمد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 4
- 7
src/components/modals/attribute/editAttribute.vue View File

@@ -98,7 +98,7 @@

<script>
import { ref, toRef, watch } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -190,12 +190,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `ویرایش ویژگی با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.success("!مشکلی در ویرایش ویژگی پیش آمد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 5
- 14
src/components/modals/blogCat/addBlogCat.vue View File

@@ -107,25 +107,19 @@

<script>
import { iconData } from "../../../views/live-preview/icon/data";
import { ref,} from "vue";
import Swal from "sweetalert2";
import { ref } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";

export default {
props: {
},
props: {},
setup(props, { emit }) {
const title = ref();
const selectedIcon = ref();
const errors = ref({});
const loading = ref(false);


const clearError = (field) => {
errors.value[field] = "";
};
@@ -164,12 +158,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `!افزودن دسته با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!مشکلی در اضافه کردن دسته پیش آمد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 9
- 16
src/components/modals/blogCat/editBlogCat.vue View File

@@ -59,14 +59,14 @@
</b-dropdown-item>
</b-dropdown>

<div v-if="selectedIcon" class="mt-2">
<div v-if="localIcon" class="mt-2">
<label class="form-label">آیکن انتخاب شده:</label>
<div class="selected-icon-container">
<i :class="`ph-duotone ${selectedIcon}`"></i>
<i :class="`ph-duotone ${localIcon}`"></i>
</div>
</div>

<div v-if="!selectedIcon" class="mt-2">
<div v-if="!localIcon" class="mt-2">
<label class="form-label">آیکن انتخاب شده:</label>
<div class="selected-icon-container">
<i :class="`ph-duotone ${localIcon}`"></i>
@@ -113,7 +113,6 @@
<script>
import { iconData } from "../../../views/live-preview/icon/data";
import { ref, toRef, watch } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -136,7 +135,6 @@ export default {
setup(props, { emit }) {
const localTitle = toRef(props.title);
const localIcon = ref(props.icon);
const selectedIcon = ref();
const localId = toRef(props.id);
const errors = ref({});
const loading = ref(false);
@@ -164,22 +162,21 @@ export default {
errors.value = {};
if (!localTitle.value)
errors.value.localTitle = "وارد کردن عنوان ضروری می باشد";
if (!selectedIcon.value) errors.value.icon = "انتخاب آیکن ضروری است";
if (!localIcon.value) errors.value.icon = "انتخاب آیکن ضروری است";
return Object.keys(errors.value).length === 0;
};

const setSelectedIcon = (icon) => {
selectedIcon.value = icon;
localIcon.value = icon;
};

const editCat = () => {
console.log(selectedIcon.value);
if (!validateForm()) return;
loading.value = true;

const formData = new FormData();
formData.append("title", localTitle.value);
formData.append("icon", selectedIcon.value);
formData.append("icon", localIcon.value);
ApiServiece.put(`admin/blog-categories/${localId.value}`, formData)
.then(() => {
toast.success("!دسته با موفقیت ویرایش شد", {
@@ -195,12 +192,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `!ویرایش دسته با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!ویرایش دسته با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
@@ -214,7 +208,6 @@ export default {
clearError,
editCat,
localTitle,
selectedIcon,
iconData,
setSelectedIcon,
localIcon,


+ 78
- 10
src/components/modals/categories/addCat.vue View File

@@ -98,11 +98,11 @@

<BRow class="g-3">
<!-- Brand Description -->
<BCol lg="12">
<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب پدر</label>
<select v-model="selectedPaernt" class="form-control">
<option value="" disabled selected>انتخاب کنید</option>
<select v-model="selectedPaernt" class="form-control" placeholder="انتخاب کنید">
<option
v-for="parent in localParents"
:key="parent.id"
@@ -113,6 +113,40 @@
</select>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب آیکن</label>
<b-dropdown
variant="outline-primary"
class="w-100"
@change="clearError('icon')"
>
<b-dropdown-item
v-for="(icon, index) in iconData"
:key="index"
:value="icon.component"
class="icon-item"
@click="setSelectedIcon(icon.component)"
>
<div class="icon-container">
<i :class="`ph-duotone ${icon.component}`"></i>
</div>
</b-dropdown-item>
</b-dropdown>

<div v-if="selectedIcon" class="mt-2">
<label class="form-label">آیکن انتخاب شده:</label>
<div class="selected-icon-container">
<i :class="`ph-duotone ${selectedIcon}`"></i>
</div>
</div>

<small v-if="errors.icon" class="text-danger">
{{ errors.icon }}
</small>
</div>
</BCol>
</BRow>

<!-- Submit Buttons -->
@@ -146,8 +180,8 @@
</template>

<script>
import { iconData } from "../../../views/live-preview/icon/data";
import { ref, toRef, watch } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -160,6 +194,7 @@ export default {
},
},
setup(props, { emit }) {
const selectedIcon = ref();
const selectedPaernt = ref();
const localParents = toRef(props.parents);
const image = ref(null);
@@ -213,12 +248,17 @@ export default {
}
};

const setSelectedIcon = (icon) => {
selectedIcon.value = icon;
};

const validateForm = () => {
errors.value = {};
if (!description.value)
errors.value.description = "وارد کردن توضیحات ضروری می باشد";
if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد";
if (!image.value) errors.value.image = "یک عکس انتخاب نمایید";
if (!selectedIcon.value) errors.value.icon = "انتخاب آیکن ضروری است";
return Object.keys(errors.value).length === 0;
};

@@ -234,6 +274,7 @@ export default {
formData.append("title", title.value);
formData.append("description", description.value);
formData.append("image", image.value);
formData.append("icon", selectedIcon.value);
if (selectedPaernt.value) {
formData.append("parent_id", selectedPaernt.value);
}
@@ -258,12 +299,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `!افزودن دسته با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!افزودن دسته با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
@@ -284,6 +322,9 @@ export default {
description,
localParents,
selectedPaernt,
setSelectedIcon,
iconData,
selectedIcon,
};
},
};
@@ -367,4 +408,31 @@ export default {
.delete-btn:focus {
outline: none;
}
.icon-item {
display: inline-block;
width: 33%;
padding: 5px;
text-align: center;
}
.icon-container i {
font-size: 2rem;
margin-right: 10px;
}
.selected-icon-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}

.selected-icon-container i {
font-size: 2.5rem;
}

.icon-container {
display: flex;
justify-content: center;
align-items: center;
margin: 8px;
}
</style>

+ 102
- 28
src/components/modals/categories/editCat.vue View File

@@ -10,7 +10,7 @@
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">ویرایش برند</h5>
<h5 class="modal-title" id="exampleModalLabel">ویرایش دسته</h5>

<button
type="button"
@@ -60,13 +60,6 @@
alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview"
/>
<button
type="button"
@click="removeImage()"
class="delete-btn"
>
<i class="fa fa-trash f-16"></i>
</button>
</div>

<small v-if="errors.localImage" class="text-danger">
@@ -98,11 +91,14 @@

<BRow class="g-3">
<!-- Brand Description -->
<BCol lg="12">
<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب پدر</label>
<select v-model="localParent" class="form-control">
<option value="" disabled selected>انتخاب کنید</option>
<select
v-model="localParent"
class="form-control"
placeholder="انتخاب کنید"
>
<option
v-for="parent in allLocalParents"
:key="parent.id"
@@ -113,6 +109,46 @@
</select>
</div>
</BCol>
<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب آیکن</label>
<b-dropdown
variant="outline-primary"
class="w-100"
@change="clearError('icon')"
>
<b-dropdown-item
v-for="(icon, index) in iconData"
:key="index"
:value="icon.component"
class="icon-item"
@click="setSelectedIcon(icon.component)"
>
<div class="icon-container">
<i :class="`ph-duotone ${icon.component}`"></i>
</div>
</b-dropdown-item>
</b-dropdown>

<div v-if="localIcon" class="mt-2">
<label class="form-label">آیکن انتخاب شده:</label>
<div class="selected-icon-container">
<i :class="`ph-duotone ${localIcon}`"></i>
</div>
</div>

<div v-if="!localIcon" class="mt-2">
<label class="form-label">آیکن انتخاب شده:</label>
<div class="selected-icon-container">
<i :class="`ph-duotone ${localIcon}`"></i>
</div>
</div>

<small v-if="errors.icon" class="text-danger">
{{ errors.icon }}
</small>
</div>
</BCol>
</BRow>

<!-- Submit Buttons -->
@@ -148,9 +184,10 @@
<script>
import { ref, toRef, watch } from "vue";
import ApiServiece from "@/services/ApiService";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import { iconData } from "../../../views/live-preview/icon/data";

export default {
props: {
@@ -178,9 +215,14 @@ export default {
type: Array,
Required: true,
},
icon: {
type: String,
Required: true,
},
},

setup(props, { emit }) {
const localIcon = toRef(props.icon);
const imagePreview = ref(null);
const allLocalParents = toRef(props.allParents);
const localParent = toRef(props.parent);
@@ -190,6 +232,7 @@ export default {
const image = ref(null);
const localId = toRef(props.id);
const errors = ref({});

const loading = ref(false);

const handleImageChange = (event) => {
@@ -214,6 +257,10 @@ export default {
}
};

const setSelectedIcon = (icon) => {
localIcon.value = icon;
};

watch(
() => props.title,
(newVal) => (localTitle.value = newVal)
@@ -245,15 +292,10 @@ export default {
(newVal) => (allLocalParents.value = newVal)
);

const removeImage = () => {
localImage.value = null;
imagePreview.value = null;

const fileInput = document.querySelector('input[type="file"]');
if (fileInput) {
fileInput.value = "";
}
};
watch(
() => props.icon,
(newVal) => (localIcon.value = newVal)
);

const validateForm = () => {
errors.value = {};
@@ -264,6 +306,7 @@ export default {
if (!localImage.value && !imagePreview.value) {
errors.value.localImage = "یک عکس انتخاب نمایید";
}
if (!localIcon.value) errors.value.icon = "انتخاب آیکن ضروری است";
return Object.keys(errors.value).length === 0;
};

@@ -272,12 +315,14 @@ export default {
};

const editCat = () => {
console.log(localId.value);
if (!validateForm()) return;
loading.value = true;

const formData = new FormData();
formData.append("title", localTitle.value);
formData.append("description", localDesc.value);
formData.append("icon", localIcon.value);

if (image.value) {
formData.append("image", image.value);
@@ -307,12 +352,9 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `ویرایش دسته با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!ویراش دسته با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
@@ -330,9 +372,12 @@ export default {
localImage,
handleImageChange,
imagePreview,
removeImage,
iconData,
localParent,
allLocalParents,
setSelectedIcon,

localIcon,
};
},
};
@@ -516,4 +561,33 @@ export default {
.delete-btn:focus {
outline: none;
}
.selected-icon-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}

.selected-icon-container i {
font-size: 2.5rem;
}

.icon-container {
display: flex;
justify-content: center;
align-items: center;
margin: 8px;
}

.icon-container i {
font-size: 2rem;
margin-right: 10px;
}

.icon-item {
display: inline-block;
width: 33%;
padding: 5px;
text-align: center;
}
</style>

+ 9
- 8
src/components/modals/commonModals/showDescription.vue View File

@@ -1,12 +1,13 @@
<template>
<div
class="modal fade"
id="showDescription"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div
class="modal fade"
id="showDescription"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
:inert="localDesc"
>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">


+ 175
- 0
src/components/modals/commonModals/showText.vue View File

@@ -0,0 +1,175 @@
<template>
<div
class="modal fade"
id="showText"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">
<i class="fas fa-clipboard-list"></i> توضیح کامل
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="subject-container">
<textarea
disabled
class="subject-text"
v-model="localDesc"
></textarea>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
بستن
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import "vue3-toastify/dist/index.css";
import { watch, ref } from "vue";
export default {
props: {
desc: {
type: String,
required: true,
},
},
setup(props) {
const localDesc = ref();
watch(
() => props.desc,
(newVal) => (localDesc.value = newVal)
);
return {
localDesc,
};
},
};
</script>
<style scoped>
.modal-dialog {
max-width: 600px; /* Larger size for better content visibility */
margin-top: 10vh; /* Center vertically */
}
.modal-content {
border-radius: 16px; /* More rounded corners */
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1); /* Larger shadow for more depth */
background: linear-gradient(
to bottom right,
#f5f7fb,
#e0e8ed
); /* Soft gradient background */
}
.modal-header {
border-bottom: none; /* Remove default border */
}
.modal-title {
color: #007bff;
font-weight: 600;
font-size: 1.75rem;
display: flex;
align-items: center;
}
.modal-title i {
margin-right: 12px;
font-size: 1.75rem;
}
.btn-close {
background: none;
border: none;
font-size: 1.75rem;
color: #007bff;
transition: color 0.3s ease;
}
.btn-close:hover {
color: #0056b3;
}
.modal-body {
padding: 25px;
}
.subject-container {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-top: 10px;
}
.subject-container i {
color: #007bff;
margin-right: 15px;
font-size: 1.75rem;
}
.subject-text {
color: #333;
font-weight: 500;
border: 2px solid #007bff;
width: 100%;
height: 180px;
padding: 12px;
border-radius: 10px;
font-size: 1rem;
background-color: #f9f9f9;
box-sizing: border-box;
resize: none;
transition: border-color 0.3s;
}
.subject-text:focus {
border-color: #0056b3; /* Highlight border on focus */
outline: none;
}
.modal-footer {
display: flex;
justify-content: flex-end;
margin-top: 1.5rem;
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
transition: background 0.3s;
font-size: 1.1rem;
}
.btn-secondary:hover {
background-color: #5a6268;
}
</style>

+ 6
- 9
src/components/modals/editUser.vue View File

@@ -61,8 +61,9 @@
class="form-select"
v-model="localRole"
@change="clearError('role')"
placeholder="نوع کاربر"
>
<option disabled value="">نوع کاربر</option>
<option value="admin">مدیر</option>
<option value="client">مشتری</option>
</select>
@@ -121,7 +122,6 @@
<script>
import ApiServiece from "@/services/ApiService";
import { ref, toRef, watch } from "vue";
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";

@@ -217,13 +217,10 @@ export default {
})
.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `ویرایش کاربر با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
confirmButtonText: "باشه",
toast.error("!ویرایش کاربر با مشکل مواحه شد", {
position: "top-right",
autoClose: 1000,
onClose: () => emit("user-updated"),
});
})
.finally(() => {


+ 130
- 0
src/components/modals/helperModals/catBanner.vue View File

@@ -0,0 +1,130 @@
<template>
<div
class="modal fade"
id="catBanner"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">
<i class="fa fa-info-circle me-2"></i> بنر دسته ها
</h5>
</div>
<div class="modal-body">
<div class="subject-container">
<img
src="../../../assets/custom/دسته بندی ها.png"
alt="Guidance Image"
class="img-fluid rounded"
/>
</div>
</div>
<div class="modal-footer">
<div class="w-100 d-flex justify-content-center">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
بستن
</button>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
setup() {
return {};
},
};
</script>

<style scoped>
.modal-dialog {
max-width: 700px; /* Increase the size of the modal for better visibility */
margin-top: 15vh; /* Center vertically with some padding */
}

.modal-content {
border-radius: 16px; /* Rounded corners */
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.15); /* Larger shadow for depth */
background: #ffffff; /* White background */
border: none;
}

.modal-header {
border-bottom: 1px solid #ddd; /* Soft border at the bottom */
padding: 1rem 1.5rem;
}

.modal-title {
color: #007bff;
font-weight: 600;
font-size: 1.75rem;
display: flex;
align-items: center;
}

.modal-title i {
margin-right: 12px;
font-size: 2rem;
}

.btn-close {
background: none;
border: none;
font-size: 1.75rem;
color: #007bff;
transition: color 0.3s ease;
}

.btn-close:hover {
color: #0056b3;
}

.modal-body {
padding: 2rem;
text-align: center; /* Center the image */
}

.subject-container {
background: #f9f9f9;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.subject-container img {
max-width: 100%;
max-height: 400px;
object-fit: cover;
border-radius: 12px;
}

.modal-footer {
display: flex;
justify-content: flex-end;
padding: 1rem 1.5rem;
}

.btn-secondary {
background-color: #6c757d;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
transition: background 0.3s;
font-size: 1.1rem;
}

.btn-secondary:hover {
background-color: #5a6268;
}
</style>

+ 130
- 0
src/components/modals/helperModals/mainPageBanner.vue View File

@@ -0,0 +1,130 @@
<template>
<div
class="modal fade"
id="mainPageBanner"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">
<i class="fa fa-info-circle me-2"></i> بنر صفحه اصلی
</h5>
</div>
<div class="modal-body">
<div class="subject-container">
<img
src="../../../assets/custom/صفحه اصلی.png"
alt="Guidance Image"
class="img-fluid rounded"
/>
</div>
</div>
<div class="modal-footer">
<div class="w-100 d-flex justify-content-center">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
بستن
</button>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
setup() {
return {};
},
};
</script>

<style scoped>
.modal-dialog {
max-width: 700px; /* Increase the size of the modal for better visibility */
margin-top: 15vh; /* Center vertically with some padding */
}

.modal-content {
border-radius: 16px; /* Rounded corners */
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.15); /* Larger shadow for depth */
background: #ffffff; /* White background */
border: none;
}

.modal-header {
border-bottom: 1px solid #ddd; /* Soft border at the bottom */
padding: 1rem 1.5rem;
}

.modal-title {
color: #007bff;
font-weight: 600;
font-size: 1.75rem;
display: flex;
align-items: center;
}

.modal-title i {
margin-right: 12px;
font-size: 2rem;
}

.btn-close {
background: none;
border: none;
font-size: 1.75rem;
color: #007bff;
transition: color 0.3s ease;
}

.btn-close:hover {
color: #0056b3;
}

.modal-body {
padding: 2rem;
text-align: center; /* Center the image */
}

.subject-container {
background: #f9f9f9;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.subject-container img {
max-width: 100%;
max-height: 400px;
object-fit: cover;
border-radius: 12px;
}

.modal-footer {
display: flex;
justify-content: flex-end;
padding: 1rem 1.5rem;
}

.btn-secondary {
background-color: #6c757d;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
transition: background 0.3s;
font-size: 1.1rem;
}

.btn-secondary:hover {
background-color: #5a6268;
}
</style>

+ 290
- 0
src/components/modals/identity/addIdentity.vue View File

@@ -0,0 +1,290 @@
<template>
<div
class="modal fade"
id="addIdentity"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="false"
>
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">
اضافه کردن مشخصه جدید
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form @submit.prevent="addIdentity">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">عنوان مشخصه</label>

<input
v-model="title"
@input="clearError('title')"
type="text"
class="form-control"
placeholder="عنوان مشخصه"
:class="{ 'is-invalid': errors.title }"
/>
<small v-if="errors.title" class="text-danger">
{{ errors.title }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب دسته</label>

<div class="color-picker-wrapper">
<select
class="form-control selector"
@change="clearError(`selectedCat`)"
v-model="selectedCat"
:class="{ 'is-invalid': errors.selectedCat }"
placeholder="انتخاب دسته"
>
<option
v-for="cat in localCat"
:key="cat.id"
:value="cat.id"
>
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedCat" class="text-danger">
{{ errors.selectedCat }}
</small>
</div>
</BCol>
</BRow>

<!-- Submit Buttons -->
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
ذخیره
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>

<script>
import { ref, toRef, watch } from "vue";

import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";

export default {
props: {
cats: {
type: Array,
Required: true,
},
},
setup(props, { emit }) {
const localCat = toRef(props.cats);
const selectedCat = ref();
const title = ref();
const errors = ref({});
const loading = ref(false);

watch(
() => props.cats,
(newVal) => (localCat.value = newVal)
);

const validateForm = () => {
errors.value = {};
if (!title.value)
errors.value.title = "وارد کردن عنوان مشخصه ضروری می باشد";
if (!selectedCat.value)
errors.value.selectedCat = "انتخاب دسته ضروری می باشد";
return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

const addIdentity = () => {
if (!validateForm()) return;
loading.value = true;

const formData = new FormData();
formData.append("category_id", selectedCat.value);
formData.append("title", title.value);

ApiServiece.post(`admin/attributes`, formData)
.then((resp) => {
console.log(resp);
toast.success("!مشخصه با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
document.getElementById("close").click();
emit("attribute-updated");
}, 500);
})
.catch((error) => {
console.error(error);
toast.error("!اضافه کردن مشخصه با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
});
};

return {
errors,
loading,
clearError,
addIdentity,
localCat,
title,
selectedCat,
};
},
};
</script>

<style scoped>
.modal-dialog {
max-width: 50%;
}

.modal-content {
padding: 20px;
}

.modal-body {
padding: 20px;
padding: 1rem 1.5rem;
}

.form-group {
margin-bottom: 1rem;
}

.input-group {
margin-top: 0.5rem;
}

.modal-dialog {
max-width: 50%;
}

.modal-content {
padding: 1.5rem;
}

.modal-header {
border-bottom: 1px solid #dee2e6;
padding-bottom: 1rem;
}

.form-group {
margin-bottom: 1.5rem;
}

.input-group {
margin-top: 0.5rem;
}

.Image-Preview {
min-width: 100px;
max-height: 100px;
max-width: 100px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.delete-btn {
display: flex;
align-items: center;
padding: 3px;
font-size: 10px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
gap: 8px; /* Space between icon and text */
margin-right: 100px;
}

.delete-btn:hover {
background-color: #c0392b;
transform: scale(1.05);
}

.delete-btn:active {
background-color: #a93226;
}

.delete-btn:focus {
outline: none;
}
.color-picker-wrapper {
position: relative;
display: flex;
align-items: center;
}

.color-picker {
width: 300px;
height: 45px;
padding: 0;
border-radius: 10px;
border: none;
cursor: pointer;
}

.color-display {
width: 30px;
height: 30px;
margin-left: 10px;
border-radius: 50%;
border: 2px solid #ccc;
display: inline-block;
}
</style>

+ 316
- 0
src/components/modals/identity/editIdentity.vue View File

@@ -0,0 +1,316 @@
<template>
<div
class="modal fade"
id="editIdentity"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="false"
>
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">ویرایش مشخصه</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form @submit.prevent="editIdentity">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">عنوان مشخصه</label>

<input
v-model="localTitle"
@input="clearError('localTitle')"
type="text"
class="form-control"
placeholder="عنوان مشخصه"
:class="{ 'is-invalid': errors.localTitle }"
/>
<small v-if="errors.localTitle" class="text-danger">
{{ errors.localTitle }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب دسته</label>

<div class="color-picker-wrapper">
<select
class="form-control selector"
@change="clearError(`localCat`)"
v-model="localCatId"
:class="{ 'is-invalid': errors.localCat }"
placeholder="انتخاب دسته"
>
<option
v-for="cat in localCat"
:key="cat.id"
:value="cat.id"
>
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedCat" class="text-danger">
{{ errors.selectedCat }}
</small>
</div>
</BCol>
</BRow>

<!-- Submit Buttons -->
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
ویرایش
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>

<script>
import { ref, toRef, watch } from "vue";

import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";

export default {
props: {
title: {
type: String,
Required: true,
},
catId: {
type: String,
Required: true,
},
id: {
type: String,
Required: true,
},
cats: {
type: Array,
Required: true,
},
},
setup(props, { emit }) {
const localCat = toRef(props.cats);
const localTitle = toRef(props.title);
const localCatId = toRef(props.catId);
const localId = toRef(props.id);
const errors = ref({});
const loading = ref(false);

watch(
() => props.cats,
(newVal) => (localCat.value = newVal)
);

watch(
() => props.title,
(newVal) => (localTitle.value = newVal)
);

watch(
() => props.id,
(newVal) => (localId.value = newVal)
);

watch(
() => props.catId,
(newVal) => (localCatId.value = newVal)
);

const validateForm = () => {
errors.value = {};
if (!localTitle.value)
errors.value.localTitle = "وارد کردن عنوان مشخصه ضروری می باشد";
if (!localCat.value) errors.value.localCat = "انتخاب دسته ضروری می باشد";
return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

const editIdentity = () => {
if (!validateForm()) return;
loading.value = true;

const formData = new FormData();
formData.append("category_id", localCatId.value);
formData.append("title", localTitle.value);

ApiServiece.put(`admin/attributes/${localId.value}`, formData)
.then((resp) => {
console.log(resp);
toast.success("!مشخصه با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
document.getElementById("close").click();
emit("attribute-updated");
}, 500);
})
.catch((error) => {
console.error(error);
toast.error("!ویرایش مشخصه با مشکل مواجه شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
});
};

return {
errors,
loading,
clearError,
editIdentity,
localCat,
localTitle,
localId,
localCatId,
};
},
};
</script>

<style scoped>
.modal-dialog {
max-width: 50%;
}

.modal-content {
padding: 20px;
}

.modal-body {
padding: 20px;
padding: 1rem 1.5rem;
}

.form-group {
margin-bottom: 1rem;
}

.input-group {
margin-top: 0.5rem;
}

.modal-dialog {
max-width: 50%;
}

.modal-content {
padding: 1.5rem;
}

.modal-header {
border-bottom: 1px solid #dee2e6;
padding-bottom: 1rem;
}

.form-group {
margin-bottom: 1.5rem;
}

.input-group {
margin-top: 0.5rem;
}

.Image-Preview {
min-width: 100px;
max-height: 100px;
max-width: 100px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.delete-btn {
display: flex;
align-items: center;
padding: 3px;
font-size: 10px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
gap: 8px; /* Space between icon and text */
margin-right: 100px;
}

.delete-btn:hover {
background-color: #c0392b;
transform: scale(1.05);
}

.delete-btn:active {
background-color: #a93226;
}

.delete-btn:focus {
outline: none;
}
.color-picker-wrapper {
position: relative;
display: flex;
align-items: center;
}

.color-picker {
width: 300px;
height: 45px;
padding: 0;
border-radius: 10px;
border: none;
cursor: pointer;
}

.color-display {
width: 30px;
height: 30px;
margin-left: 10px;
border-radius: 50%;
border: 2px solid #ccc;
display: inline-block;
}
</style>

+ 164
- 0
src/components/modals/profile/accountInfo.vue View File

@@ -0,0 +1,164 @@
<template>
<div
class="tab-pane fade show active"
id="user-set-profile"
role="tabpanel"
aria-labelledby="user-set-profile-tab"
>
<BCard no-body>
<BCardHeader>
<h5>اطلاعات حساب</h5>
</BCardHeader>
<BCardBody class="card-body">
<BRow>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">نام</label>
<input
type="text"
v-model="name"
:class="{ 'is-invalid': errors.name }"
class="form-control"
placeholder="نام"
@input="clearError('name')"
/>
<small v-if="errors.name" class="text-danger">
{{ errors.name }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">شماره تماس</label>
<input
type="text"
:class="{ 'is-invalid': errors.mobile }"
v-model="mobile"
class="form-control"
placeholder="شماره تماس"
@input="clearError('mobile')"
/>
<small v-if="errors.mobile" class="text-danger">
{{ errors.mobile }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">رمز عبور</label>
<input
type="password"
v-model="password"
class="form-control"
placeholder="رمز عبور"
/>
</div>
</BCol>
</BRow>
</BCardBody>
<BcardFooter>
<div class="text-center btn-page">
<div @click="editProfile" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
<span v-if="!loading">ویرایش</span>
</div>
</div>
</BcardFooter>
</BCard>
</div>
</template>
<script>
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { toRef, watch, ref } from "vue";
import moment from "jalali-moment";
export default {
props: {
user: {
type: String,
required: true,
},
},
setup(props) {
const loading = ref(false);
const errors = ref({});
const localUser = toRef(props.user);
const name = ref(props.user.name);
const mobile = ref(props.user.mobile);
const password = ref();

const validateForm = () => {
errors.value = {};
if (!name.value) errors.value.name = "وارد کردن نام ضروری می باشد";
if (!mobile.value) errors.value.mobile = "وارد کردن موبایل ضروری می باشد";

return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};

watch(
() => props.user,
(newVal) => {
if (newVal) {
localUser.value = newVal;
name.value = localUser.value.name;
mobile.value = localUser.value.mobile;
}
}
);

const editProfile = () => {
if (!validateForm()) return;
loading.value = true;
const formData = new FormData();
formData.append("name", name.value);
formData.append("mobile", mobile.value);
if (password.value) {
formData.append("password", password.value);
}
ApiServiece.put("client/me", formData)
.then(() => {
toast.success("!پروفایل با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
loading.value = false;
})
.catch(() => {
loading.value = false;
toast.error("!مشکلی در ویرایش پروفایل ایجاد شد", {
position: "top-right",
autoClose: 1000,
});
});
};

return {
convertToJalali,
localUser,
name,
mobile,
editProfile,
clearError,
errors,
password,
loading,
};
},
};
</script>

+ 221
- 0
src/components/modals/profile/addAddress.vue View File

@@ -0,0 +1,221 @@
<template>
<div
class="tab-pane fade"
id="user-set-information"
role="tabpanel"
aria-labelledby="user-set-information-tab"
>
<BCard no-body>
<BCardHeader>
<h5>اضافه کردن آدرس</h5>
</BCardHeader>
<BCardBody class="card-body">
<BRow>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">طول جغرافیایی</label>
<input
type="text"
v-model="lat"
class="form-control"
:class="{ 'is-invalid': errors.lat }"
placeholder="طول جغرافیای "
@input="clearError('lat')"
/>
<small v-if="errors.lat" class="text-danger">
{{ errors.lat }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">عرض جغرافیایی</label>
<input
type="text"
v-model="long"
class="form-control"
placeholder="عرض جغرافیایی"
:class="{ 'is-invalid': errors.long }"
@input="clearError('long')"
/>
<small v-if="errors.long" class="text-danger">
{{ errors.long }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">استان</label>
<input
v-model="city"
type="text"
class="form-control"
:class="{ 'is-invalid': errors.city }"
placeholder="استان"
@input="clearError('city')"
/>
<small v-if="errors.city" class="text-danger">
{{ errors.city }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">شهر</label>
<input
v-model="town"
type="text"
class="form-control"
placeholder="شهر"
:class="{ 'is-invalid': errors.town }"
@input="clearError('town')"
/>
<small v-if="errors.town" class="text-danger">
{{ errors.town }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">کد پستی</label>
<input
v-model="postcode"
type="text"
class="form-control"
placeholder="کد پستی"
@input="clearError('postcode')"
:class="{ 'is-invalid': errors.postcode }"
/>
<small v-if="errors.postcode" class="text-danger">
{{ errors.postcode }}
</small>
</div>
</BCol>
<BCol class="col-sm-6">
<div class="mb-3">
<label class="form-label">عنوان</label>
<input
v-model="title"
type="text"
class="form-control"
placeholder="کد پستی"
/>
</div>
</BCol>
<BCol class="col-sm-12">
<div class="mb-3">
<label class="form-label">آدرس</label>
<textarea
v-model="address"
class="form-control"
placeholder="آدرس"
@input="clearError('address')"
:class="{ 'is-invalid': errors.address }"
>
</textarea>
<small v-if="errors.address" class="text-danger">
{{ errors.address }}
</small>
</div>
</BCol>
</BRow>
</BCardBody>
<BcardFooter>
<div class="text-center btn-page">
<div
@click="createAddress"
class="btn btn-primary"
:disabled="loading"
>
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
<span v-if="!loading">ثبت</span>
</div>
</div>
</BcardFooter>
</BCard>
</div>
</template>
<script>
import { ref } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
export default {
setup() {
const loading = ref(false);
const title = ref();
const address = ref();
const lat = ref();
const long = ref();
const city = ref();
const town = ref();
const postcode = ref();
const errors = ref({});
const validateForm = () => {
errors.value = {};
if (!address.value)
errors.value.address = "وارد کردن آدرس الزامی می باشد";
if (!lat.value)
errors.value.lat = "وارد کردن عرض جغرافیایی الزامی می باشد";
if (!long.value)
errors.value.long = "وارد کردن طول جغرافیایی الزامی می باشد";
if (!city.value) errors.value.city = "وارد کردن استان الزامی می باشد";
if (!town.value) errors.value.town = "وارد کردن استان الزامی می باشد";
if (!postcode.value)
errors.value.postcode = "وارد کردن کد پستی الزامی می باشد";
return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

const createAddress = () => {
if (!validateForm()) return;
loading.value = true;
const formData = new FormData();
formData.append("title", title.value);
formData.append("address", address.value);
formData.append("location[lat]", lat.value);
formData.append("location[lng]", long.value);
formData.append("city", city.value);
formData.append("town", town.value);
formData.append("postcode", postcode.value);
ApiServiece.post(`wholesale/my-addresses`, formData)
.then(() => {
loading.value = false;
toast.success("!آدرس با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
})
.catch(() => {
loading.value = false;
toast.error("!مشکلی در ایجاد آدرس بوجود آمد", {
position: "top-right",
autoClose: 1000,
});
});
};

return {
clearError,
errors,
createAddress,
address,
lat,
long,
postcode,
town,
city,
title,
loading,
};
},
};
</script>

+ 146
- 0
src/components/modals/profile/addressList.vue View File

@@ -0,0 +1,146 @@
<script>
import moment from "jalali-moment";
import { watch, toRef } from "vue";
export default {
name: "PRODUCT-LIST",
props: {
addresses: {
type: String,
required: true,
},
},
setup(props) {
const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};
const localAddresses = toRef(props.addresses);
watch(
() => props.addresses,
(newVal) => (localAddresses.value = newVal)
);
return {
localAddresses,
convertToJalali,
};
},
};
</script>

<template>
<div
class="tab-pane fade"
id="addressList"
role="tabpanel"
aria-labelledby="user-list-address-tab"
>
<BRow>
<BCol class="col-sm-12">
<BCard no-body class="table-card">
<BCardBody>
<div class="table-responsive">
<table
v-if="localAddresses.length > 0"
class="table table-hover tbl-product"
id="pc-dt-simple"
>
<thead>
<tr>
<th>آدرس</th>
<th>استان</th>
<th class="text-end">شهر</th>
<th class="text-end">کد پستی</th>
<th class="text-center">عنوان</th>
<th class="text-center">تاریخ ایجاد</th>
</tr>
</thead>
<tbody>
<!-- Iterate over localAddresses -->
<tr v-for="addres in localAddresses" :key="addres.id">
<td>
<BRow>
<BCol>
<h6 class="mb-1">
{{ addres.address.slice(0, 20) }}...
</h6>
<p class="text-muted f-12 mb-0">
عرض جغرافیایی:
{{ addres.location.lat }} | طول جغرافیایی:
{{ addres.location.lng }}
</p>
</BCol>
</BRow>
</td>
<td>{{ addres.city }}</td>
<td class="text-end">{{ addres.town }}</td>
<td class="text-end">{{ addres.postcode }}</td>
<td v-if="!addres.title" class="text-center">
<i
class="ph-duotone ph-x-circle text-danger f-24"
data-bs-toggle="tooltip"
data-bs-title="danger"
></i>
</td>
<td v-if="addres.title" class="text-center">
{{ addres.title }}
</td>
<td class="text-center">
<div class="prod-action-links">
<ul class="list-inline me-auto mb-0">
<li
class="list-inline-item align-bottom"
data-bs-toggle="tooltip"
title="Edit"
>
<router-link
to="/add-product"
class="avtar avtar-xs btn-link-success btn-pc-default"
>
<i class="ti ti-edit-circle f-18"></i>
</router-link>
</li>
<li
class="list-inline-item align-bottom"
data-bs-toggle="tooltip"
title="Delete"
>
<a
href="#"
class="avtar avtar-xs btn-link-danger btn-pc-default"
>
<i class="ti ti-trash f-18"></i>
</a>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Message when no addresses are available -->
<div v-else class="text-center p-5">
<BRow>
<BCol
class="col-lg-12 d-flex justify-content-center align-items-center"
>
<p class="text-muted">هیچ آدرسی برای نمایش وجود ندارد.</p>
</BCol>
<BCol
class="col-lg-10 d-flex justify-content-center align-items-center ms-5"
>
<img
class="img-fluid"
src="@/assets/images/pages/img-connection-lost.png"
alt="img"
/>
</BCol>
</BRow>
</div>
</div>
</BCardBody>
</BCard>
</BCol>
</BRow>
</div>
</template>

+ 44
- 0
src/components/navbar.vue View File

@@ -1,4 +1,5 @@
<script>
import rightBar from "./right-bar.vue";
export default {
name: "NAVBAR",
components: {},
@@ -7,10 +8,15 @@ export default {
isFullscreen: false,
isSidebarHidden: false,
currentMode: "light",
rightBar,
show: false,
};
},
mounted() {
const savedMode = localStorage.getItem("themeMode");
if (savedMode) {
this.changeMode(savedMode); // Apply the saved theme mode
}
// Add event listener for keydown events
document.addEventListener("keydown", this.handleKeyDown);
},
@@ -43,6 +49,7 @@ export default {
},
changeMode(mode) {
this.currentMode = mode;
localStorage.setItem("themeMode", mode);
if (mode === "dark") {
document.body.setAttribute("data-pc-theme", "dark");
document.body.setAttribute("data-topbar", "dark");
@@ -90,8 +97,45 @@ export default {
<i class="ti ti-menu-2"></i>
</a>
</li>
</ul>
</div>
<BDropdown
variant="transparent"
class="pc-h-item"
toggle-class="text-reset dropdown-btn pc-head-link arrow-none p-0"
menu-class="dropdown-menu-end"
aria-haspopup="true"
:offset="{ alignmentAxis: -150, crossAxis: 0, mainAxis: 20 }"
>
<template #button-content
><span class="text-muted pc-head-link"
><i
:class="
currentMode === 'dark'
? 'ph-duotone ph-moon'
: currentMode === 'light'
? 'ph-duotone ph-sun-dim'
: 'ph-duotone ph-cpu'
"
></i
></span>
</template>
<a href="#" class="dropdown-item" @click.prevent="changeMode('dark')">
<i class="ph-duotone ph-moon"></i>
<span>تاریک</span>
</a>
<a
href="#"
class="dropdown-item"
@click.prevent="changeMode('light')"
>
<i class="ph-duotone ph-sun-dim"></i>
<span>روشن</span>
</a>
</BDropdown>
<rightBar />
</div>
</template>



+ 79
- 0
src/router/routes.js View File

@@ -1,3 +1,4 @@
// پنل ادمین
export default [
{
path: "/",
@@ -165,6 +166,16 @@ export default [
},
component: () => import("../views/live-preview/pages/orders/orders.vue"),
},
{
path: "/allOrdersItems",
name: "allOrdersItems",
meta: {
title: "سفارسات",
requiresAuth: true,
},
component: () =>
import("../views/live-preview/pages/orders/allOrdersItems.vue"),
},
{
path: "/singleOrder/:id",
name: "singleOrder",
@@ -175,6 +186,16 @@ export default [
component: () =>
import("../views/live-preview/pages/orders/singleOrder.vue"),
},
{
path: "/approvedOrders",
name: "approvedOrders",
meta: {
title: "سفارسات",
requiresAuth: true,
},
component: () =>
import("../views/live-preview/pages/orders/approvedOrders.vue"),
},
{
path: "/comments",
name: "comments",
@@ -203,6 +224,64 @@ export default [
},
component: () => import("../views/live-preview/pages/faqs/editFaqs.vue"),
},
{
path: "/profile",
name: "profile",
meta: {
title: "پروفایل",
requiresAuth: true,
},
component: () => import("../views/live-preview/pages/profile/profile.vue"),
},
{
path: "/banners",
name: "banners",
meta: {
title: "بنر ",
requiresAuth: true,
},
component: () => import("../views/live-preview/pages/banners/banners.vue"),
},
{
path: "/addBanner",
name: "addBanner",
meta: {
title: "بنر ",
requiresAuth: true,
},
component: () =>
import("../views/live-preview/pages/banners/addBanner.vue"),
},
{
path: "/editBanner/:id",
name: "editBanner",
meta: {
title: "بنر ",
requiresAuth: true,
},
component: () =>
import("../views/live-preview/pages/banners/editBanner.vue"),
},
{
path: "/idenities",
name: "idenities",
meta: {
title: "مشخضات",
requiresAuth: true,
},
component: () =>
import("../views/live-preview/pages/identity/idenities.vue"),
},

{
path: "/calls",
name: "calls",
meta: {
title: "پیگیری ها",
requiresAuth: true,
},
component: () => import("../views/live-preview/pages/calls/calls.vue"),
},
{
path: "/",
name: "live-preview",


+ 1622
- 879
src/views/live-preview/application/e-commerce/product-list.vue
File diff suppressed because it is too large
View File


+ 3
- 2
src/views/live-preview/pages/attributes/attributes.vue View File

@@ -39,7 +39,7 @@ export default {
const getAttributes = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/attribute-values?title=${encodeURIComponent(
`admin/attribute-values?attribute_id=1&title=${encodeURIComponent(
searchQuery.value || ""
)}&code=${encodeURIComponent(searchQuery.value || "")}
&paginate=${paginate.value || 10}&page=${page.value || 1}
@@ -156,6 +156,7 @@ export default {

const getAttributeValues = () => {
ApiServiece.get(`admin/attributes`).then((resp) => {
console.log(resp)
attributeValues.value = resp.data.data;
});
};
@@ -195,7 +196,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3 "
dir="rtl"
>
<div class="d-flex align-items-center">


+ 539
- 0
src/views/live-preview/pages/banners/addBanner.vue View File

@@ -0,0 +1,539 @@
<template>
<Layout>
<BRow>
<BCol sm="12">
<BCard no-body>
<BCardHeader>
<div class="d-flex justify-content-between align-items-center">
<h5>ایجاد بنر</h5>

<!-- Help and Modal buttons -->
<div>
<button
data-bs-toggle="modal"
data-bs-target="#mainPageBanner"
class="btn btn-info btn-sm mx-2"
@click="showHelp"
>
<i class="fa fa-question-circle"></i> راهنمایی بنر صفحه اصلی
</button>
<button
data-bs-toggle="modal"
data-bs-target="#catBanner"
class="btn btn-warning btn-sm mx-2"
@click="showModal"
>
<i class="fa fa-info-circle"></i> راهنمایی بنر دسته ها
</button>
</div>
</div>
</BCardHeader>
<BCardBody>
<BRow class="g-3">
<BCol md="6">
<div class="form-group">
<label class="form-label">عنوان</label>
<input
type="text"
v-model="title"
class="form-control"
placeholder="عنوان بنر"
:class="{ 'is-invalid': errors.title }"
@input="clearError('title')"
/>
</div>
<small v-if="errors.title" class="text-danger">
{{ errors.title }}
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">پنل نمایش</label>
<select
class="form-select"
aria-label="Default select example"
v-model="pannel"
@change="clearError('pannel')"
:class="{ 'is-invalid': errors.pannel }"
placeholder="انتخاب پنل"
>
<option value="wholesale">پنل عمده فروشی</option>
<option value="web">وب سایت واپلیکیشن</option>
</select>
</div>
<small v-if="errors.pannel" class="text-danger">
{{ errors.pannel }}
</small>
</BCol>

<BCol v-if="pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">نمایش در</label>
<select
v-model="pageType"
:class="{ 'is-invalid': errors.pageType }"
class="form-select"
aria-label="Default select example"
placeholder="انتخاب صفحه"
@change="clearError('pageType')"
>
<option value="main">صفحه اصلی</option>
<option value="cat">صفحه دسته</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'cat' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>
<select
v-model="selectedCatPage"
class="form-select"
:class="{ 'is-invalid': errors.selectedCatPage }"
aria-label="Default select example"
@change="clearError('selectedCatPage')"
placeholder="انتخاب صفحه دسته"
>
<option :value="cat.id" v-for="cat in cats" :key="cat.id">
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pannel != 'wholesale'" md="6">
<div class="form-group">
<label class="form-label">انتخاب صفحه فرود</label>
<select
class="form-select"
aria-label="Default select example"
v-model="landingType"
@change="clearError('landingType')"
:class="{ 'is-invalid': errors.landingType }"
placeholder="انتخاب صفحه فرود"
>
<option value="product">صفحه محصولات</option>
<option value="cat">صفحه دسته ها</option>
</select>
</div>
<small v-if="errors.landingType" class="text-danger">
{{ errors.landingType }}
</small>
</BCol>

<BCol
v-if="landingType === 'product'"
sm="6"
class="mt-3"
style="margin-top: 30px"
>
<label for="token">صفحه محصول</label>

<Select2
id="token"
v-model="selectedLandingProduct"
:options="formattedUsers"
:settings="{ settingOption: value, settingOption: value }"
style="height: 60px"
/>
<small v-if="errors.selectedLandingProduct" class="text-danger">
{{ errors.selectedLandingProduct }}
</small>
</BCol>

<BCol v-if="landingType === 'cat'" md="6">
<div class="form-group">
<label class="form-label">صفحه کدام دسته ؟</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLandingCat"
@change="clearError('selectedLandingCat')"
:class="{ 'is-invalid': errors.selectedLandingCat }"
placeholder="انتخاب صفحه دسته"
>
<option :value="cat.id" v-for="cat in cats" :key="cat.id">
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>
</BCol>

<BCol v-if="pageType === 'main' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">موقعیت در صفحه اصلی</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLoc"
@change="clearError('selectedLoc')"
:class="{ 'is-invalid': errors.selectedLoc }"
placeholder="موقعیت بنر"
>
<option value="A">َA-Slideshow</option>
<option value="B">B-Banner</option>
<option value="C">C-Banner</option>
<option value="D">D-Banner</option>
<option value="F">E-Banner</option>
<option value="G">F-Banner</option>
<option value="H">G-Banner</option>
<option value="G">H-Banner</option>
<option value="I">I-Banner</option>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'cat' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">موقعیت در صفحه دسته ها</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLoc"
@change="clearError('selectedLoc')"
:class="{ 'is-invalid': errors.selectedLoc }"
placeholder="موقعیت بنر"
>
<option value="A">َA-Slideshow</option>
<option value="B">B-Banner</option>
<option value="C">C-Banner</option>
<option value="D">D-Banner</option>
<option value="E">E-Banner</option>
<option value="F">F-Banner</option>
<option value="G">G-Banner</option>
<option value="H">H-Banner</option>
<option value="I">I-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">تصویر بنر</label>

<input
type="file"
accept="image/*"
@change="handleImageUpload"
class="form-control"
:class="{ 'is-invalid': errors.image }"
/>

<div v-if="imagePreview" class="mt-2">
<img
:src="imagePreview"
alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview"
/>
</div>

<small v-if="errors.image" class="text-danger">
{{ errors.image }}
</small>
</div>
</BCol>
</BRow>
</BCardBody>
<BCardFooter>
<div class="d-flex justify-content-center">
<div class="text-center">
<button
type="submit"
class="btn btn-primary"
@click.prevent="submitForm"
:disabled="loading"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
</span>
<span v-else>ایجاد</span>
</button>
</div>
</div>
</BCardFooter>
</BCard>
</BCol>
</BRow>

<mainPageBanner />
<catBanner />
</Layout>
</template>

<script>
import Select2 from "vue3-select2-component";
import catBanner from "@/components/modals/helperModals/catBanner.vue";
import mainPageBanner from "@/components/modals/helperModals/mainPageBanner.vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { ref, onMounted, computed } from "vue";
import Layout from "@/layout/custom.vue";

export default {
name: "SAMPLE-PAGE",
components: {
Layout,
mainPageBanner,
catBanner,
Select2,
},
setup() {
const title = ref();
const pageType = ref();
const products = ref([]);
const cats = ref([]);
const landingType = ref();
const selectedCatPage = ref();
const selectedLandingCat = ref();
const selectedLandingProduct = ref();
const selectedLoc = ref();
const pannel = ref();
const image = ref();
const imagePreview = ref();

const loading = ref(false);
const errors = ref({});

const getCats = () => {
ApiServiece.get(`admin/categories`)
.then((resp) => {
cats.value = resp.data.data;
})
.catch((err) => {
console.log(err);
});
};

const getProduct = () => {
ApiServiece.get(`admin/products`)
.then((resp) => {
products.value = resp.data.data;
})
.catch((err) => {
console.log(err);
});
};

const handleImageUpload = (event) => {
const file = event.target.files[0];

if (file) {
errors.value.image = null;

image.value = file;

const reader = new FileReader();
reader.onload = () => {
imagePreview.value = reader.result;
};
reader.readAsDataURL(file);
}
};

const formattedUsers = computed(() => {
return products.value.map((product) => ({
id: product.id,
text: product.title,
}));
});

const validateForm = () => {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان بنر الزامی است";
if (!pageType.value && pannel.value != "wholesale")
errors.value.pageType = "مشخص کنید بنر کجا نشان داده شود";
if (pageType.value === "cat" && !selectedCatPage.value)
errors.value.selectedCatPage = "صفحه دسته را انتخاب کنید";
if (!landingType.value && pannel.value != 'wholesale')
errors.value.landingType = "صفحه فرود را انتخاب نمایید";
if (
landingType.value === "cat" &&
!selectedLandingCat.value &&
pannel.value != "wholesale"
)
errors.value.selectedLandingCat = "صفحه فرود دسته را انتخاب کنید";
if (
landingType.value === "product" &&
!selectedLandingProduct.value &&
pannel.value != "wholesale"
)
errors.value.selectedLandingProduct = "صفحه فرود محصول را انتخاب کنید";

if (!selectedLoc.value && pannel.value != "wholesale")
errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید";

if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید";

if (!image.value) errors.value.image = "عکس بنر را وارد نمایید";

return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

onMounted(() => {
getCats();
getProduct();
});

const submitForm = () => {
console.log(errors.value);
if (!validateForm()) return;
loading.value = true;
const formData = new FormData();
formData.append("title", title.value);

if (pageType.value === "cat") {
formData.append("page_id", selectedCatPage.value);
}

if (landingType.value === "product" && pannel.value != "wholesale") {
formData.append("product_id", selectedLandingProduct.value);
}

if (landingType.value === "cat" && pannel.value != "wholesale") {
formData.append("category_id", selectedLandingCat.value);
}

if (selectedLoc.value === "A") {
formData.append("type", "slider");
}

if (selectedLoc.value !== "A") {
formData.append("type", "banner");
}

if (pannel.value === "wholesale") {
formData.append("type", "slider");
formData.append("location", "A");
}

if (selectedLoc.value) {
formData.append("location", selectedLoc.value);
}
formData.append("panel", pannel.value);
formData.append("image", image.value);
formData.append("sort", 1);

ApiServiece.post(`admin/banners`, formData, {
headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then((resp) => {
toast.success("!بنر با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
console.log(resp);
loading.value = false;
})
.catch((error) => {
console.log(error.response.message);
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
});
};

return {
cats,
errors,
title,
products,
selectedCatPage,
submitForm,
clearError,
pageType,
formattedUsers,
landingType,
selectedLandingCat,
selectedLandingProduct,
selectedLoc,
pannel,
handleImageUpload,
image,
imagePreview,
loading,
};
},
};
</script>

<style scoped>
.ql-editor {
direction: rtl;
text-align: right;
}

.ql-editor::before {
content: attr(placeholder);
direction: rtl !important;
text-align: right;
}
.Image-Preview {
min-width: 200px;
max-height: 200px;
min-height: 200px;
max-width: 200px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.delete-btn {
display: flex;
align-items: center;
padding: 3px;
font-size: 10px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
gap: 8px;
margin-right: 200px;
}

.delete-btn:hover {
background-color: #c0392b;
transform: scale(1.05);
}

.delete-btn:active {
background-color: #a93226;
}

.delete-btn:focus {
outline: none;
}
</style>

+ 473
- 0
src/views/live-preview/pages/banners/banners.vue View File

@@ -0,0 +1,473 @@
<script>
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import moment from "jalali-moment";
import { onMounted, ref, watch, computed } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";

import showDescription from "@/components/modals/commonModals/showDescription.vue";

export default {
name: "BORDER",
components: {
Layout,
showDescription,
},
setup() {
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
const catDescription = ref();
const filterLoading = ref(false);
const searchQuery = ref("");
const banners = ref();
const catTitle = ref();
const catId = ref();
const catParent = ref();
const catImage = ref();
const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};
watch(searchQuery, () => {
getBanners();
});
const getBanners = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/banners?paginate=${paginate.value || 10}&page=${page.value || 1}`
)
.then((resp) => {
filterLoading.value = false;
banners.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
})
.catch(() => {
filterLoading.value = false;
});
};

const visiblePages = computed(() => {
const pages = [];
if (totalPages.value <= 5) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
let start = currentPage.value - 2;
let end = currentPage.value + 2;

if (start < 1) start = 1;
if (end > totalPages.value) end = totalPages.value;

for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
});

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
} else if (searchPage.value > totalPages.value) {
searchPage.value = totalPages.value;
}

if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
page.value = searchPage.value;
}
}

const nextPage = () => {
if (currentPage.value < totalPages.value) {
page.value++;
getBanners();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
page.value--;
getBanners();
}
};

const handleCatUpdated = () => {
getBanners();
};
const deleteBanner = (id, title) => {
Swal.fire({
text: `می خواهید بنر ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.delete(`admin/banners/${id}`)
.then(() => {
toast.success("!بنر با موفقیت حذف شد", {
position: "top-right",
autoClose: 3000,
});
getBanners()
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در حذف کردن بنر پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

const descriptionModal = (desc) => {
catDescription.value = desc;
};

watch(searchQuery, () => {
getBanners();
});

watch(page, () => {
getBanners();
});

const restoreBanner = (id, title) => {
Swal.fire({
text: `می خواهید بنر ${title} را بازیابی کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.put(`admin/banners/${id}/restore`)
.then(() => {
toast.success("!بنر با موفقیت بازیابی شد", {
position: "top-right",
autoClose: 3000,
});
})
.then(() => {
getBanners();
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در بازیابی بنر پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

onMounted(() => {
getBanners();
});
return {
banners,
convertToJalali,
handleCatUpdated,
restoreBanner,
deleteBanner,
searchQuery,
filterLoading,
descriptionModal,
catDescription,
catTitle,
catParent,
catImage,
catId,
currentPage,
totalPages,
nextPage,
prevPage,
page,
handlePageInput,
searchPage,
visiblePages,
};
},
};
</script>
<template>
<Layout>
<BRow>
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">
<router-link to="/addBanner">
<button class="btn btn-light text-primary btn-sm px-3">
افزودن بنر
</button>
</router-link>
</div>
</div>
<div v-if="!filterLoading" class="card-body table-border-style p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered m-0" dir="rtl">
<thead class="table-light">
<tr>
<th>عکس</th>
<th>عنوان</th>
<td>موقعیت</td>
<td>نوع</td>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="banner in banners" :key="banner.id">
<td v-if="banner?.image">
<img
:src="banner?.image"
alt="Brand Image"
class="Brand-Image"
/>
</td>
<td v-if="!banner.image">ندارد</td>

<td>
<div
type="button"
data-bs-toggle="modal"
data-bs-target="#showDescription"
@click="descriptionModal(call.text)"
class="subject-box"
aria-haspopup="dialog"
aria-controls="showDescription"
>
<i class="fas fa-comments subject-icon"></i>
<span class="subject-text">
{{ call.text?.slice(0, 20) }}
{{ call.text?.length > 20 ? "..." : "" }}
</span>
</div>
</td>
<td>{{ banner.location }}</td>
<td v-if="banner.type === 'slider'">اسلایدر</td>
<td v-if="banner.type === 'banner'">بنر</td>
<td>{{ convertToJalali(banner.created_at) }}</td>
<td>
<router-link
:to="`/editBanner/${banner?.id}`"
class="btn btn-sm btn-outline-warning me-1"
>
ویرایش
</router-link>
<button
v-if="!banner.deleted_at"
@click="deleteBanner(banner.id, banner.title)"
class="btn btn-sm btn-outline-danger"
>
حذف
</button>

<button
v-else
@click="restoreBanner(banner?.id, banner?.title)"
class="btn btn-sm btn-outline-success"
>
بازیابی
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
v-else
class="filter-loader card table-card user-profile-list"
></div>
</div>
</div>

<showDescription :desc="catDescription" />
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>

<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>

<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>

<style scoped>
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-3px);
}
.table th,
.table td {
vertical-align: middle;
text-align: center;
}
.filter-loader {
border: 4px solid rgba(0, 123, 255, 0.3);
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.Brand-Image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.subject-box {
padding: 8px 14px;
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
color: #ef6c00;
font-weight: 600;
border-radius: 10px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
display: inline-flex;
align-items: center;
gap: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.subject-box i {
color: #ef6c00;
font-size: 1rem;
}

.subject-box:hover {
transform: translateY(-2px);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
}
.search-number {
display: flex;
align-items: center;
}

.search-number input {
width: 150px;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.375rem;
margin-bottom: 7px;
border: 1px solid #ced4da;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.search-number input:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
}

.search-number input::placeholder {
color: #6c757d;
}

.search-number input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.pagination {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.page-item {
flex: 0 0 auto;
}

.page-link {
cursor: pointer;
user-select: none;
}
</style>

+ 577
- 0
src/views/live-preview/pages/banners/editBanner.vue View File

@@ -0,0 +1,577 @@
<template>
<Layout>
<BRow>
<BCol sm="12">
<BCard no-body>
<BCardHeader>
<div class="d-flex justify-content-between align-items-center">
<h5>ویرایش بنر</h5>

<!-- Help and Modal buttons -->
<div>
<button
data-bs-toggle="modal"
data-bs-target="#mainPageBanner"
class="btn btn-info btn-sm mx-2"
@click="showHelp"
>
<i class="fa fa-question-circle"></i> راهنمایی بنر صفحه اصلی
</button>
<button
data-bs-toggle="modal"
data-bs-target="#catBanner"
class="btn btn-warning btn-sm mx-2"
@click="showModal"
>
<i class="fa fa-info-circle"></i> راهنمایی بنر دسته ها
</button>
</div>
</div>
</BCardHeader>
<BCardBody>
<BRow class="g-3">
<BCol md="6">
<div class="form-group">
<label class="form-label">عنوان</label>
<input
type="text"
v-model="title"
class="form-control"
placeholder="عنوان بنر"
:class="{ 'is-invalid': errors.title }"
@input="clearError('title')"
/>
</div>
<small v-if="errors.title" class="text-danger">
{{ errors.title }}
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">پنل نمایش</label>
<select
class="form-select"
aria-label="Default select example"
v-model="pannel"
@change="clearError('pannel')"
:class="{ 'is-invalid': errors.pannel }"
placeholder="انخاب پنل"
>
<option value="wholesale">پنل عمده فروشی</option>
<option value="web">وب سایت واپلیکیشن</option>
</select>
</div>
<small v-if="errors.pannel" class="text-danger">
{{ errors.pannel }}
</small>
</BCol>

<BCol v-if="pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">نمایش در</label>
<select
v-model="pageType"
:class="{ 'is-invalid': errors.pageType }"
class="form-select"
aria-label="Default select example"
@change="clearError('pageType')"
placeholder="انخاب صفحه اصلی"
>
<option value="main">صفحه اصلی</option>
<option value="cat">صفحه دسته</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'cat' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>
<select
v-model="selectedCatPage"
class="form-select"
:class="{ 'is-invalid': errors.selectedCatPage }"
aria-label="Default select example"
@change="clearError('selectedCatPage')"
placeholder="انخاب صفحه دسته"
>
<option :value="cat.id" v-for="cat in cats" :key="cat.id">
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pannel == 'web'" md="6">
<div class="form-group">
<label class="form-label">انتخاب صفحه فرود</label>
<select
class="form-select"
aria-label="Default select example"
v-model="landingType"
@change="clearError('landingType')"
:class="{ 'is-invalid': errors.landingType }"
placeholder="انتخاب صفحه فرود"
>
<option value="product">صفحه محصولات</option>
<option value="cat">صفحه دسته ها</option>
</select>
</div>
<small v-if="errors.landingType" class="text-danger">
{{ errors.landingType }}
</small>
</BCol>

<BCol
v-if="landingType === 'product'"
sm="6"
class="mt-3"
style="margin-top: 30px"
>
<label for="token">صفحه محصول</label>

<Select2
id="token"
v-model="selectedLandingProduct"
:options="formattedUsers"
:settings="{ settingOption: value, settingOption: value }"
style="height: 60px"
/>
<small v-if="errors.selectedLandingProduct" class="text-danger">
{{ errors.selectedLandingProduct }}
</small>
</BCol>

<BCol v-if="landingType === 'cat'" md="6">
<div class="form-group">
<label class="form-label">صفحه کدام دسته ؟</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLandingCat"
@change="clearError('selectedLandingCat')"
:class="{ 'is-invalid': errors.selectedLandingCat }"
placeholder="انتخاب صفحه دسته"
>
<option :value="cat.id" v-for="cat in cats" :key="cat.id">
{{ cat.title }}
</option>
</select>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>
</BCol>

<BCol v-if="pageType === 'main' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">موقعیت در صفحه اصلی</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLoc"
@change="clearError('selectedLoc')"
:class="{ 'is-invalid': errors.selectedLoc }"
placeholder="موقعیت بنر"
>
<option value="A">َA-Slideshow</option>
<option value="B">B-Banner</option>
<option value="C">C-Banner</option>
<option value="D">D-Banner</option>
<option value="F">E-Banner</option>
<option value="G">F-Banner</option>
<option value="H">G-Banner</option>
<option value="G">H-Banner</option>
<option value="I">I-Banner</option>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'cat' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">موقعیت در صفحه دسته ها</label>
<select
class="form-select"
aria-label="Default select example"
v-model="selectedLoc"
@change="clearError('selectedLoc')"
:class="{ 'is-invalid': errors.selectedLoc }"
placeholder="موقعیت بنر"
>
<option value="A">َA-Slideshow</option>
<option value="B">B-Banner</option>
<option value="C">C-Banner</option>
<option value="D">D-Banner</option>
<option value="E">E-Banner</option>
<option value="F">F-Banner</option>
<option value="G">G-Banner</option>
<option value="H">H-Banner</option>
<option value="I">I-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">تصویر بنر</label>

<input
type="file"
accept="image/*"
@change="handleImageUpload"
class="form-control"
:class="{ 'is-invalid': errors.imagePreview }"
/>

<div v-if="imagePreview" class="mt-2">
<img
:src="imagePreview"
alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview"
/>
</div>

<small v-if="errors.imagePreview" class="text-danger">
{{ errors.imagePreview }}
</small>
</div>
</BCol>
</BRow>
</BCardBody>
<BCardFooter>
<div class="d-flex justify-content-center">
<div class="text-center">
<button
type="submit"
class="btn btn-primary"
@click.prevent="submitForm"
:disabled="loading"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> ویرایش...
</span>
<span v-else>ویرایش</span>
</button>
</div>
</div>
</BCardFooter>
</BCard>
</BCol>
</BRow>

<mainPageBanner />
<catBanner />
</Layout>
</template>

<script>
import { useRoute } from "vue-router";
import Select2 from "vue3-select2-component";
import catBanner from "@/components/modals/helperModals/catBanner.vue";
import mainPageBanner from "@/components/modals/helperModals/mainPageBanner.vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { ref, onMounted, computed } from "vue";
import Layout from "@/layout/custom.vue";

export default {
name: "SAMPLE-PAGE",
components: {
Layout,
mainPageBanner,
catBanner,
Select2,
},
setup() {
const route = useRoute();
const title = ref();
const pageType = ref();
const products = ref([]);
const cats = ref([]);
const landingType = ref();
const selectedCatPage = ref();
const selectedLandingCat = ref();
const selectedLandingProduct = ref();
const selectedLoc = ref();
const pannel = ref();
const image = ref();
const imagePreview = ref();

const loading = ref(false);
const errors = ref({});

const getCats = () => {
ApiServiece.get(`admin/categories`)
.then((resp) => {
cats.value = resp.data.data;
})
.catch((err) => {
console.log(err);
});
};

const getProduct = () => {
ApiServiece.get(`admin/products`)
.then((resp) => {
products.value = resp.data.data;
})
.catch((err) => {
console.log(err);
});
};

const handleImageUpload = (event) => {
const file = event.target.files[0];

if (file) {
errors.value.image = null;

image.value = file;

const reader = new FileReader();
reader.onload = () => {
imagePreview.value = reader.result;
};
reader.readAsDataURL(file);
}
};

const formattedUsers = computed(() => {
return products.value.map((product) => ({
id: product.id,
text: product.title,
}));
});

const validateForm = () => {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان بنر الزامی است";
if (!pageType.value)
errors.value.pageType = "مشخص کنید بنر کجا نشان داده شود";
if (pageType.value === "cat" && !selectedCatPage.value)
errors.value.selectedCatPage = "صفحه دسته را انتخاب کنید";
if (!landingType.value)
errors.value.landingType = "صفحه فرود را انتخاب نمایید";
if (landingType.value === "cat" && !selectedLandingCat.value)
errors.value.selectedLandingCat = "صفحه فرود دسته را انتخاب کنید";
if (landingType.value === "product" && !selectedLandingProduct.value)
errors.value.selectedLandingProduct = "صفحه فرود محصول را انتخاب کنید";

if (!selectedLoc.value)
errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید";

if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید";

if (!imagePreview.value)
errors.value.imagePreview = "عکس بنر را وارد نمایید";

return Object.keys(errors.value).length === 0;
};

const clearError = (field) => {
errors.value[field] = "";
};

const getBanner = () => {
ApiServiece.get(`admin/banners/${route.params.id}`)
.then((resp) => {
const data = resp.data.data;
title.value = data?.title;
pannel.value = data?.panel;
imagePreview.value = data?.image;
selectedLoc.value = data?.location;
selectedLandingCat.value = data?.category_id;
selectedLandingProduct.value = data?.product_id;
console.log(data)
if (data.page_id) {
pageType.value = "cat";
selectedCatPage.value = data?.page_id;
console.log(data);
}
if (!data.page_id) {
pageType.value = "main";
}

if (selectedLandingProduct.value) {
landingType.value = "product";
}

if (selectedLandingCat.value) {
landingType.value = "cat";
console.log("Asd")
}

if(!selectedCatPage.value && !selectedLandingProduct.value){
pannel.value = "wholesale"
}
})
.catch((err) => {
console.log(err);
});
};

onMounted(() => {
getCats();
getProduct();
getBanner();
});

const submitForm = () => {
if (!validateForm()) return;
loading.value = true;
const formData = new FormData();
formData.append("title", title.value);

if (pageType.value === "cat") {
formData.append("page_id", selectedCatPage.value);
}

if (landingType.value === "product") {
formData.append("product_id", selectedLandingProduct.value);
}

if (landingType.value === "cat") {
formData.append("category_id", selectedLandingCat.value);
}

if (selectedLoc.value === "A") {
formData.append("type", "slider");
}

if (selectedLoc.value !== "A") {
formData.append("type", "banner");
}

if (pannel.value === "wholesale") {
formData.append("type", "slider");
formData.append("location", "A");
}

formData.append("location", selectedLoc.value);
formData.append("panel", pannel.value);
if (image.value) {
formData.append("image", image.value);
}

formData.append("sort", 1);
formData.append("_method", "put");

ApiServiece.post(`admin/banners/${route.params.id}`, formData, {
headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then((resp) => {
loading.value = false;
toast.success("!بنر با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
console.log(resp);
})
.catch((error) => {
loading.value = false;
console.log(error.response.message);
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
});
};

return {
cats,
errors,
title,
products,
selectedCatPage,
submitForm,
clearError,
pageType,
formattedUsers,
landingType,
selectedLandingCat,
selectedLandingProduct,
selectedLoc,
pannel,
handleImageUpload,
image,
imagePreview,
loading,
};
},
};
</script>

<style scoped>
.ql-editor {
direction: rtl;
text-align: right;
}

.ql-editor::before {
content: attr(placeholder);
direction: rtl !important;
text-align: right;
}
.Image-Preview {
min-width: 200px;
max-height: 200px;
min-height: 200px;
max-width: 200px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.delete-btn {
display: flex;
align-items: center;
padding: 3px;
font-size: 10px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
gap: 8px;
margin-right: 200px;
}

.delete-btn:hover {
background-color: #c0392b;
transform: scale(1.05);
}

.delete-btn:active {
background-color: #a93226;
}

.delete-btn:focus {
outline: none;
}
</style>

+ 2
- 1
src/views/live-preview/pages/blogCats/blogCat.vue View File

@@ -137,6 +137,7 @@ export default {
};

const editModalData = (id, title, icon) => {
catId.value = id;
catTitle.value = title;
catIcon.value = icon;
@@ -183,7 +184,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">


+ 10
- 12
src/views/live-preview/pages/blogs/addBlog.vue View File

@@ -27,12 +27,12 @@

<BCol md="6">
<div class="form-group">
<label class="form-label">اسلاگ</label>
<label class="form-label">کلمه کلیدی</label>
<input
type="text"
v-model="slug"
class="form-control"
placeholder="اسلاگ بلاگ"
placeholder="کلمه کلیدی بلاگ"
:class="{ 'is-invalid': errors.slug }"
@input="clearError('slug')"
/>
@@ -98,9 +98,10 @@
:class="{ 'is-invalid': errors.blogCat }"
v-model="blogCat"
class="form-control"
@input="clearError('blogCat')"
@change="clearError('blogCat')"
placeholder="انتخاب دسته بلاگ"
>
<option value="" disabled selected>انتخاب دسته بلاگ</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -165,7 +166,7 @@
</template>

<script>
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -232,7 +233,7 @@ export default {
const validateForm = () => {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان بلاگ الزامی است";
if (!slug.value) errors.value.slug = "وارد کردن اسلاگ بلاگ ضروری می باشد";
if (!slug.value) errors.value.slug = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد";
if (!blogCat.value)
@@ -313,12 +314,9 @@ export default {

.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `!افزودن بلاگ با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!مشکلی در اضافه کردن بلاگ پیش آمد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 3
- 3
src/views/live-preview/pages/blogs/blogs.vue View File

@@ -73,7 +73,7 @@ export default {

const deleteBlog = (id, title) => {
Swal.fire({
text: `می خواهید بلاگ ${title} را حذف کنید ؟`,
text: `می خواهید بلاگ ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -162,7 +162,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3 "
dir="rtl"
>
<div class="d-flex align-items-center">
@@ -188,7 +188,7 @@ export default {
<tr>
<th>عکس</th>
<th>عنوان</th>
<td>اسلاگ</td>
<td>کلمه کلیدی</td>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
</tr>


+ 10
- 12
src/views/live-preview/pages/blogs/editBlog.vue View File

@@ -28,12 +28,12 @@
<!-- Second Input Field (Slug) -->
<BCol md="6">
<div class="form-group">
<label class="form-label">اسلاگ</label>
<label class="form-label">کلمه کلیدی</label>
<input
type="text"
v-model="slug"
class="form-control"
placeholder="اسلاگ بلاگ"
placeholder="کلمه کلیدی بلاگ"
:class="{ 'is-invalid': errors.slug }"
@input="clearError('slug')"
/>
@@ -94,8 +94,9 @@
v-model="blogCat"
class="form-control"
@input="clearError('blogCat')"
placeholder="انتخاب دسته بلاگ"
>
<option value="" disabled selected>انتخاب دسته بلاگ</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -161,7 +162,7 @@
</template>

<script>
import Swal from "sweetalert2";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
@@ -220,7 +221,7 @@ export default {
const validateForm = () => {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان بلاگ الزامی است";
if (!slug.value) errors.value.slug = "وارد کردن اسلاگ بلاگ ضروری می باشد";
if (!slug.value) errors.value.slug = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد";
if (!blogCat.value)
@@ -229,7 +230,7 @@ export default {
errors.value.author = "وارد کردن نویسنده بلاگ ضروری می باشد";
if (!editorContent.value)
errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد";
if (!image.value) errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد";
if (!imagePreview.value) errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد";
return Object.keys(errors.value).length === 0;
};

@@ -313,12 +314,9 @@ export default {

.catch((error) => {
console.error(error);
Swal.fire({
icon: "error",
title: "خطا",
text: `!ویرایش بلاگ با مشکل مواجه شد: ${
error.response?.data?.message || "خطای غیرمنتظره رخ داد."
}`,
toast.error("!مشکلی در ویرایش باگ پیش آمد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {


+ 2
- 2
src/views/live-preview/pages/brands/brands.vue View File

@@ -92,7 +92,7 @@ export default {
};
const deleteBrand = (id, title) => {
Swal.fire({
title: `می خواهید برند ${title} را حذف کنید ؟`,
text: `می خواهید برند ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -190,7 +190,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center text-white"
class="card-header d-flex justify-content-between align-items-center"
dir="rtl"
>
<div class="d-flex align-items-center">


+ 546
- 0
src/views/live-preview/pages/calls/calls.vue View File

@@ -0,0 +1,546 @@
<script>
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import moment from "jalali-moment";
import { onMounted, ref, watch, computed } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";

export default {
name: "BORDER",
components: {
Layout,
},
setup() {
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(5);
const page = ref(1);
const filterLoading = ref(false);
const selectedStatus = ref();
const calls = ref();
const callText = ref();
const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};
watch(selectedStatus, () => {
getCalls();
page.value = 1;
});
const getCalls = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/forms?status=${selectedStatus.value || ""}&paginate=${
paginate.value || 10
}&page=${page.value || 1}`
)
.then((resp) => {
console.log(resp);
filterLoading.value = false;
calls.value = resp.data.data.data;
console.log(calls.value);
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
})
.catch(() => {
filterLoading.value = false;
});
};

const visiblePages = computed(() => {
const pages = [];
if (totalPages.value <= 5) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
let start = currentPage.value - 2;
let end = currentPage.value + 2;

if (start < 1) start = 1;
if (end > totalPages.value) end = totalPages.value;

for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
});

const getStatusClass = (status) => {
const statusClasses = {
waiting: "badge-waiting",
answered: "badge-paid",
};
return statusClasses[status] || "badge-secondary";
};

const getStatusLabel = (status) => {
const statusLabels = {
waiting: "در انتظار",
answered: "پاسخ داده شده",
};
return statusLabels[status] || "نامشخص";
};

const deleteOrder = (id) => {
Swal.fire({
text: `می خواهید سفارش ${id} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.delete(`admin/orders/${id}`)
.then(() => {
toast.success("!سفارش با موفقیت حذف شد", {
position: "top-right",
autoClose: 3000,
});
calls.value = calls.value.filter((call) => call.id != id);
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در حذف کردن سفارش پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

const changeStatus = (id, status) => {
Swal.fire({
text: `آیا می خواهید وضعیت این پیام را به ${getStatusLabel(
status
)} تغییر دهید؟ `,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
const formData = new FormData();
formData.append("status", status);
ApiServiece.put(`admin/forms/${id}`, formData)
.then(() => {
toast.success("!تغییر وضعیت پیام با موفقیت انجام شد", {
position: "top-right",
autoClose: 3000,
});
})
.then(() => {
getCalls();
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در تغییر وضعیت پیام پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
} else if (searchPage.value > totalPages.value) {
searchPage.value = totalPages.value;
}

if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
page.value = searchPage.value;
}
}

watch(selectedStatus, () => {
getCalls();
});

watch(page, () => {
getCalls();
});

const nextPage = () => {
if (currentPage.value < totalPages.value) {
page.value++;
getCalls();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
page.value--;
getCalls();
}
};

onMounted(() => {
getCalls();
});
return {
calls,
convertToJalali,
deleteOrder,
selectedStatus,
filterLoading,
changeStatus,
currentPage,
totalPages,
nextPage,
prevPage,
page,
handlePageInput,
searchPage,
visiblePages,
getStatusClass,
getStatusLabel,

callText,
};
},
};
</script>
<template>
<Layout>
<BRow>
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">
<label for="statusSelect" class="form-label me-2"> وضعیت </label>
<select
v-model="selectedStatus"
id="statusSelect"
class="form-control form-control-sm d-inline-block me-2"
style="width: 250px; border-radius: 15px"
>
<option value="answered">پاسخ داده شده</option>
<option value="waiting">در انتظار</option>
</select>
</div>
</div>
<div v-if="!filterLoading" class="card-body table-border-style p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered m-0" dir="rtl">
<thead class="table-light">
<tr>
<th>کاربر</th>
<th>ایمیل</th>
<th>موضوع</th>
<th>پیام</th>
<th>وضعیت</th>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="call in calls" :key="call.id">
<td>{{ call.name }}</td>
<td v-if="call.email">{{ call.email }}</td>
<td v-if="!call.email">
<i
class="fas fa-times-circle status-icon unavailable"
></i>
</td>
<td>{{ call?.subject }}</td>
<td>
<textarea
:value="call.text"
disabled
readonly
style="
width: 100%;
resize: none;
border: none;
background: transparent;
"
></textarea>
</td>
<td>
<span class="badge" :class="getStatusClass(call.status)">
{{ getStatusLabel(call.status) }}
</span>
</td>

<td>{{ convertToJalali(call?.created_at) }}</td>

<td>
<!-- <router-link
:to="`/singleOrder/${order?.id}`"
class="btn btn-sm btn-outline-primary me-1"
>
مشاهده
</router-link> -->
<button
class="btn btn-sm btn-outline-warning dropdown-toggle me-1"
type="button"
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
>
ویرایش وضعیت
</button>
<ul
class="dropdown-menu"
aria-labelledby="dropdownMenuButton"
>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(call?.id, 'waiting')"
><span class="badge badge-waiting"
>در انتظار</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(call?.id, 'answered')"
><span class="badge badge-paid"
>پاسخ داده شده</span
></a
>
</li>
</ul>
<!-- <button
@click="deleteOrder(order?.id)"
class="btn btn-sm btn-outline-danger"
>
حذف
</button> -->
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div
v-else
class="filter-loader card table-card user-profile-list"
></div>
</div>
</div>
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<!-- Previous page -->
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>

<!-- Page numbers with dots logic -->
<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>

<!-- Page numbers -->
<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<!-- Next page -->
<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>

<style scoped>
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-3px);
}
.table th,
.table td {
vertical-align: middle;
text-align: center;
}
.filter-loader {
border: 4px solid rgba(0, 123, 255, 0.3);
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}

.subject-box {
padding: 8px 14px;
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
color: #ef6c00;
font-weight: 600;
border-radius: 10px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
display: inline-flex;
align-items: center;
gap: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.subject-box i {
color: #ef6c00;
font-size: 1rem;
}

.subject-box:hover {
transform: translateY(-2px);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
}
.search-number {
display: flex;
align-items: center;
}

.search-number input {
width: 150px;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.375rem;
margin-bottom: 7px;
border: 1px solid #ced4da;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.search-number input:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
}

.search-number input::placeholder {
color: #6c757d;
}

.search-number input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.pagination {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.page-item {
flex: 0 0 auto;
}

.page-link {
cursor: pointer;
user-select: none;
}
.badge {
display: inline-block;
padding: 5px 10px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
text-transform: capitalize;
color: white;
}

.badge-waiting {
background-color: #ffc107;
}
.badge-paid {
background-color: #28a745;
}
.badge-un_paid {
background-color: #dc3545;
}
.badge-approved {
background-color: #17a2b8;
}
.badge-processing {
background-color: #007bff;
}
.badge-shipping {
background-color: #6f42c1;
}
.badge-delivered {
background-color: #20c997;
}
.badge-canceled {
background-color: #6c757d;
}
.dropdown-item {
cursor: pointer;
}
.status-icon.unavailable {
color: #dc3545;
}
</style>

+ 121
- 74
src/views/live-preview/pages/catrgories/cats.vue View File

@@ -2,7 +2,7 @@
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import moment from "jalali-moment";
import { onMounted, ref, watch , computed } from "vue";
import { onMounted, ref, watch, computed } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";
@@ -18,6 +18,7 @@ export default {
editCat,
},
setup() {
const catIcon = ref();
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
@@ -41,9 +42,11 @@ export default {
});
const getCats = () => {
filterLoading.value = true;
ApiServiece.get(`admin/categories?title=${searchQuery.value}&paginate=${
ApiServiece.get(
`admin/categories?title=${searchQuery.value}&paginate=${
paginate.value || 10
}&page=${page.value || 1}`)
}&page=${page.value || 1}`
)
.then((resp) => {
filterLoading.value = false;
cats.value = resp.data.data.data;
@@ -107,7 +110,7 @@ export default {
};
const deleteCat = (id, title) => {
Swal.fire({
title: `می خواهید دسته ${title} را حذف کنید ؟`,
text: `می خواهید دسته ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -122,7 +125,7 @@ export default {
position: "top-right",
autoClose: 3000,
});
cats.value = cats.value.filter((cat) => cat.id !== id);
getCats();
})
.catch((err) => {
console.log(err);
@@ -135,12 +138,13 @@ export default {
});
};

const editModalData = (id, title, desc, parent, img) => {
const editModalData = (id, title, desc, parent, img, icon) => {
catId.value = id;
catTitle.value = title;
catDescription.value = desc;
catParent.value = parent;
catImage.value = img;
catIcon.value = icon;
};

const descriptionModal = (desc) => {
@@ -151,6 +155,38 @@ export default {
getCats();
});

const restoreCat = (id, title) => {
Swal.fire({
text: `می خواهید دسته ${title} را بازیابی کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.put(`admin/categories/${id}/restore`)
.then(() => {
toast.success("!دسته با موفقیت بازیابی شد", {
position: "top-right",
autoClose: 3000,
});
})
.then(() => {
getCats();
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در بازیابی دسته پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

onMounted(() => {
getCats();
});
@@ -176,6 +212,8 @@ export default {
handlePageInput,
searchPage,
visiblePages,
catIcon,
restoreCat,
};
},
};
@@ -186,7 +224,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">
@@ -256,7 +294,8 @@ export default {
cat?.title,
cat.description,
cat?.parent?.id,
cat?.image
cat?.image,
cat?.icon
)
"
data-bs-toggle="modal"
@@ -266,11 +305,19 @@ export default {
ویرایش
</button>
<button
v-if="!cat.deleted_at"
@click="deleteCat(cat.id, cat.title)"
class="btn btn-sm btn-outline-danger"
>
حذف
</button>
<button
v-else
@click="restoreCat(cat?.id, cat?.title)"
class="btn btn-sm btn-outline-success"
>
بازیابی
</button>
</td>
</tr>
</tbody>
@@ -291,82 +338,82 @@ export default {
:parent="catParent"
:image="catImage"
:allParents="cats"
:icon="catIcon"
@cat-updated="handleCatUpdated()"
/>
<showDescription :desc="catDescription" />
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>

<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>
<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>

<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>
<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>


+ 7
- 13
src/views/live-preview/pages/comments/comments.vue View File

@@ -80,7 +80,7 @@ export default {

const deleteDiscount = (id, title) => {
Swal.fire({
text: `می خواهید تخفیف ${title} را حذف کنید ؟`,
text: `می خواهید تخفیف ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -125,11 +125,11 @@ export default {
const changeCommentStatus = (id, op) => {
let text, successMessage, errorMessage;
if (op === "confirmed") {
text = `آیای می خواهید این نظر را قبول کنید؟`;
text = ` می خواهید این نظر را قبول کنید؟`;
successMessage = "!نظر با موفقیت قبول شد";
errorMessage = "!مشکلی در تغییر وضعیت نظر ایجاد شد";
} else if (op === "rejected") {
text = `آیای می خواهید این نظر را رد کنید؟`;
text = `می خواهید این نظر را رد کنید؟`;
successMessage = "!نظر با موفقیت رد شد";
errorMessage = "!مشکلی در تغییر وضعیت نظر ایجاد شد";
}
@@ -197,7 +197,6 @@ export default {
deleteDiscount,
searchQuery,
filterLoading,

currentPage,
totalPages,
nextPage,
@@ -218,10 +217,10 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3 "
dir="rtl"
>
<div class="d-flex align-items-center">
<!-- <div class="d-flex align-items-center">
<input
v-model="searchQuery"
type="text"
@@ -229,13 +228,8 @@ export default {
class="form-control form-control-sm d-inline-block me-2"
style="width: 250px; border-radius: 15px"
/>
<router-link
to="/addDiscount"
class="btn btn-light text-primary btn-sm px-3"
>
افزودن تخفیف
</router-link>
</div>
</div> -->
</div>
<div v-if="!filterLoading" class="card-body table-border-style p-0">
<div class="table-responsive">


+ 43
- 34
src/views/live-preview/pages/discounts/addDiscount.vue View File

@@ -33,10 +33,8 @@
class="form-control"
:class="{ 'is-invalid': errors.discountType }"
@change="clearError('discountType')"
placeholder="انتخاب نوع تخفیف"
>
<option value="" disabled>
لطفاً نوع تخفیف را انتخاب کنید
</option>
<option value="percentage">درصدی</option>
<option value="const">مبلغ ثابت</option>
</select>
@@ -105,10 +103,8 @@
class="form-control"
:class="{ 'is-invalid': errors.whichPart }"
@select="clearError('whichPart')"
placeholder="انتخاب محل اعمال تخفبف"
>
<option value="" disabled>
لطفاً اعمال تخفیف را انتخاب کنید
</option>
<option value="cat">دسته</option>
<option value="product">محصول</option>
<option value="both">هردو</option>
@@ -127,8 +123,8 @@
v-model="selectedCat"
class="form-control"
@select="clearError('selectedCat')"
placeholder="انتخاب دسته"
>
<option value="" disabled selected>انتخاب دسته</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -141,26 +137,19 @@

<BCol
v-if="whichPart === 'product' || whichPart === 'both'"
md="6"
sm="6"
class="mt-3"
style="margin-top: 30px"
>
<div class="form-group">
<label class="form-label">محصول</label>
<select
:class="{ 'is-invalid': errors.selectedProduct }"
v-model="selectedProduct"
class="form-control"
@select="clearError('selectedProduct')"
>
<option value="" disabled selected>انتخاب محصول</option>
<option
v-for="product in products"
:key="product.id"
:value="product.id"
>
{{ product.title }}
</option>
</select>
</div>
<label for="token"> انتخاب محصول </label>

<Select2
id="token"
v-model="selectedProduct"
:options="formattedProducts"
:settings="{ settingOption: value, settingOption: value }"
style="height: 60px"
/>
<small v-if="errors.selectedProduct" class="text-danger">
{{ errors.selectedProduct }}
</small>
@@ -171,7 +160,7 @@
<label class="form-label"> تاریخ اعمال تخفیف </label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="startDate"
@input="handleStartDateInput"
@@ -187,7 +176,7 @@
<label class="form-label"> تاریخ انقضای تخفیف </label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="expire"
@input="handleExpireDateInput"
@@ -209,7 +198,7 @@
:disabled="loading"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
<i class="fa fa-spinner fa-spin"></i> ایجاد...
</span>
<span v-else>ایجاد</span>
</button>
@@ -223,11 +212,12 @@
</template>

<script>
import Select2 from "vue3-select2-component";
import moment from "moment";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import Layout from "@/layout/custom.vue";
import DatePicker from "vue3-persian-datetime-picker";

@@ -236,6 +226,7 @@ export default {
components: {
Layout,
DatePicker,
Select2,
},
setup() {
const title = ref();
@@ -275,18 +266,32 @@ export default {

const handleStartDateInput = () => {
if (startDate.value) {
startDate.value = moment(startDate.value).format("YYYY-MM-DD HH:mm:ss");
startDate.value = moment(
startDate.value,
"jYYYY/jMM/jDD HH:mm:ss"
).format("YYYY-MM-DD HH:mm:ss");
} else {
clearError("startDate");
}
clearError("expire");
};

const handleExpireDateInput = () => {
if (expire.value) {
expire.value = moment(expire.value).format("YYYY-MM-DD HH:mm:ss");
expire.value = moment(expire.value, "jYYYY/jMM/jDD HH:mm:ss").format(
"YYYY-MM-DD HH:mm:ss"
);
} else {
clearError("expire");
}
clearError("expire");
};

const formattedProducts = computed(() => {
return products.value.map((product) => ({
id: product.id,
text: product.title,
}));
});

const validateForm = () => {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان تخفیف الزامی است";
@@ -348,6 +353,7 @@ export default {

ApiServiece.post(`/admin/discounts`, formData)
.then((resp) => {
loading.value = false;
toast.success("!تخفیف با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
@@ -355,6 +361,7 @@ export default {
console.log(resp);
})
.catch((error) => {
loading.value = false;
console.log(error.response.message);
toast.error(`${error.response.data.message}`, {
position: "top-right",
@@ -381,6 +388,8 @@ export default {
handleExpireDateInput,
whichPart,
clearError,
loading,
formattedProducts,
};
},
};


+ 6
- 5
src/views/live-preview/pages/discounts/discounts.vue View File

@@ -19,7 +19,7 @@ export default {
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(5);
const paginate = ref(20);
const page = ref(1);

const filterLoading = ref(false);
@@ -76,7 +76,7 @@ export default {

const deleteDiscount = (id, title) => {
Swal.fire({
text: `می خواهید تخفیف ${title} را حذف کنید ؟`,
text: `می خواهید تخفیف ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -167,7 +167,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center text-white"
class="card-header d-flex justify-content-between align-items-center"
dir="rtl"
>
<div class="d-flex align-items-center">
@@ -180,7 +180,7 @@ export default {
/>
<router-link
to="/addDiscount"
class="btn btn-light text-primary btn-sm px-3"
class="btn btn-light text-primary btn-sm px-3 "
>
افزودن تخفیف
</router-link>
@@ -203,7 +203,8 @@ export default {
<tbody>
<tr v-for="discount in discounts" :key="discount.id">
<td>{{ discount?.title }}</td>
<td>{{ discount.type }}</td>
<td v-if="discount.type === 'const'">مبلغی</td>
<td v-if="discount.type === 'percentage'">درصدی</td>
<td>{{ discount.amount }}</td>
<td>{{ discount.min_order }}</td>
<td>{{ convertToJalali(discount?.starts_at) }}</td>


+ 30
- 27
src/views/live-preview/pages/discounts/editDiscount.vue View File

@@ -4,7 +4,7 @@
<BCol sm="12">
<BCard no-body>
<BCardHeader>
<h5>ایجاد تخفیف</h5>
<h5>ویرایش تخفیف</h5>
</BCardHeader>
<BCardBody>
<BRow class="g-3">
@@ -34,10 +34,8 @@
class="form-control"
:class="{ 'is-invalid': errors.discountType }"
@change="clearError('discountType')"
placeholder="انتخاب نوع اعمال تخفیف"
>
<option value="" disabled>
لطفاً نوع تخفیف را انتخاب کنید
</option>
<option value="percentage">درصدی</option>
<option value="const">مبلغ ثابت</option>
</select>
@@ -106,10 +104,8 @@
class="form-control"
:class="{ 'is-invalid': errors.whichPart }"
@select="clearError('whichPart')"
placeholder="انتخاب محل اعمل تخفیف"
>
<option value="" disabled>
لطفاً اعمال تخفیف را انتخاب کنید
</option>
<option value="cat">دسته</option>
<option value="product">محصول</option>
<option value="both">هردو</option>
@@ -120,10 +116,7 @@
</small>
</BCol>

<BCol
v-if="whichPart === 'cat' || whichPart === 'both'"
md="6"
>
<BCol v-if="whichPart === 'cat' || whichPart === 'both'" md="6">
<div class="form-group">
<label class="form-label">دسته</label>
<select
@@ -131,8 +124,8 @@
v-model="selectedCat"
class="form-control"
@select="clearError('selectedCat')"
placeholder="انتخاب دسته"
>
<option value="" disabled selected>انتخاب دسته</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -154,8 +147,8 @@
v-model="selectedProduct"
class="form-control"
@select="clearError('selectedProduct')"
placeholder="انتخاب محصول"
>
<option value="" disabled selected>انتخاب محصول</option>
<option
v-for="product in products"
:key="product.id"
@@ -175,7 +168,7 @@
<label class="form-label"> تاریخ اعمال تخفیف </label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="startDate"
@input="handleStartDateInput"
@@ -191,7 +184,7 @@
<label class="form-label"> تاریخ انقضای تخفیف </label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="expire"
@input="handleExpireDateInput"
@@ -213,9 +206,9 @@
:disabled="loading"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
<i class="fa fa-spinner fa-spin"></i> ویرایش...
</span>
<span v-else>ایجاد</span>
<span v-else>ویرایش</span>
</button>
</div>
</div>
@@ -290,13 +283,13 @@ export default {
minOrder.value = discount.value.min_order;
selectedCat.value = discount.value.category_id;
if (discount.value.category_id) {
whichPart.value === "cat";
whichPart.value = "cat";
}
console.log(discount.value.product_id);

selectedProduct.value = discount.value.product_id;
if (discount.value.product_id) {
whichPart.value === "product";
whichPart.value = "product";
}
startDate.value = discount.value.starts_at;
expire.value = discount.value.expires_at;
@@ -309,16 +302,23 @@ export default {

const handleStartDateInput = () => {
if (startDate.value) {
startDate.value = moment(startDate.value).format("YYYY-MM-DD HH:mm:ss");
startDate.value = moment(
startDate.value,
"jYYYY/jMM/jDD HH:mm:ss"
).format("YYYY-MM-DD HH:mm:ss");
} else {
clearError("expire");
}
clearError("expire");
};

const handleExpireDateInput = () => {
if (expire.value) {
expire.value = moment(expire.value).format("YYYY-MM-DD HH:mm:ss");
expire.value = moment(expire.value, "jYYYY/jMM/jDD HH:mm:ss").format(
"YYYY-MM-DD HH:mm:ss"
);
} else {
clearError("expire");
}
clearError("expire");
};

const validateForm = () => {
@@ -330,12 +330,12 @@ export default {
errors.value.amount = "وارد کردن مقدار تخفیف الزامی می باشد";
if (!minOrder.value)
errors.value.minOrder = "وارد کردن حداقل میزان تخفیف الزامی می باشد";
if (
if (
!selectedCat.value &&
(whichPart.value === "cat" || whichPart.value === "both")
)
errors.value.selectedCat = "انتخاب دسته برای تخفیف الزامی می باشد";
if (
if (
!selectedProduct.value &&
(whichPart.value === "product" || whichPart.value === "both")
)
@@ -369,11 +369,11 @@ export default {
formData.append("type", discountType.value);
formData.append("amount", amount.value);
formData.append("min_order", minOrder.value);
if ( whichPart.value === "cat" || whichPart.value === 'both') {
if (whichPart.value === "cat" || whichPart.value === "both") {
formData.append("category_id", selectedCat.value);
}

if ( whichPart.value === "product" || whichPart.value === 'both') {
if (whichPart.value === "product" || whichPart.value === "both") {
formData.append("product_id", selectedProduct.value);
}

@@ -383,6 +383,7 @@ export default {

ApiServiece.post(`/admin/discounts`, formData)
.then((resp) => {
loading.value = false;
toast.success("!تخفیف با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
@@ -390,6 +391,7 @@ export default {
console.log(resp);
})
.catch((error) => {
loading.value = false;
console.log(error.response.message);
toast.error(`${error.response.data.message}`, {
position: "top-right",
@@ -416,6 +418,7 @@ export default {
handleExpireDateInput,
whichPart,
clearError,
loading,
};
},
};


+ 1
- 1
src/views/live-preview/pages/faqs/editFaqs.vue View File

@@ -27,7 +27,7 @@ export default {
faqs.value = resp.data.data;
text.value = faqs.value.text;
status.value = faqs.value.status;
answerText.value = faqs.value?.children[0]?.text;
answerText.value = faqs.value?.answer?.text;
console.log(resp.data.data);
if(faqs.value?.children.length > 0){
isAnswerExist.value = true


+ 4
- 4
src/views/live-preview/pages/faqs/faqs.vue View File

@@ -79,7 +79,7 @@ export default {

const deleteDiscount = (id, title) => {
Swal.fire({
text: `می خواهید تخفیف ${title} را حذف کنید ؟`,
text: `می خواهید تخفیف ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -170,10 +170,10 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3 "
dir="rtl"
>
<div class="d-flex align-items-center">
<!-- <div class="d-flex align-items-center">
<input
v-model="searchQuery"
type="text"
@@ -181,7 +181,7 @@ export default {
class="form-control form-control-sm d-inline-block me-2"
style="width: 250px; border-radius: 15px"
/>
</div>
</div> -->
</div>
<div v-if="!filterLoading" class="card-body table-border-style p-0">
<div class="table-responsive">


+ 451
- 0
src/views/live-preview/pages/identity/idenities.vue View File

@@ -0,0 +1,451 @@
<script>
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import moment from "jalali-moment";
import { onMounted, ref, watch, computed } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";
import addIdentity from "@/components/modals/identity/addIdentity.vue";
import editIdentity from "@/components/modals/identity/editIdentity.vue";
export default {
name: "BORDER",
components: {
Layout,
addIdentity,
editIdentity,
},
setup() {
const cats = ref([]);
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
const attributeValues = ref();
const filterLoading = ref(false);
const searchQuery = ref("");
const attributes = ref();
const attributeTitle = ref();
const attributeId = ref();
const attrebuteCat = ref();

const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};
watch(searchQuery, () => {
getAttributes();
});
const getAttributes = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/attributes?title=${searchQuery.value || ""}
&paginate=${paginate.value || 10}&page=${page.value || 1}
`
)
.then((resp) => {
filterLoading.value = false;
console.log(resp.data);
attributes.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
console.log(attributes.value);
})
.catch(() => {
filterLoading.value = false;
});
};

const getCategories = () => {
ApiServiece.get("admin/categories").then((resp) => {
console.log(resp.data.data);
cats.value = resp.data.data;
});
};

const handleAttributeUpdated = () => {
getAttributes();
};

const nextPage = () => {
if (currentPage.value < totalPages.value) {
page.value++;
getAttributes();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
page.value--;
getAttributes();
}
};

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
} else if (searchPage.value > totalPages.value) {
searchPage.value = totalPages.value;
}

if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
page.value = searchPage.value;
}
}

const visiblePages = computed(() => {
const pages = [];
if (totalPages.value <= 5) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
let start = currentPage.value - 2;
let end = currentPage.value + 2;

if (start < 1) start = 1;
if (end > totalPages.value) end = totalPages.value;

for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
});

watch(page, () => {
getAttributes();
});

const deleteAttribute = (id, title) => {
Swal.fire({
text: `می خواهید ویژگی ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.delete(`admin/attribute-values/${id}`)
.then(() => {
toast.success("!ویژگی با موفقیت حذف شد", {
position: "top-right",
autoClose: 3000,
});
attributes.value = attributes.value.filter(
(attribute) => attribute.id !== id
);
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در حذف کردن ویژگی پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

const editModalData = (id, title, cat) => {
attributeId.value = id;
attributeTitle.value = title;
attrebuteCat.value = cat;
};

watch(searchQuery, () => {
getAttributes();
});

onMounted(() => {
getAttributes();
getCategories();
});
return {
attributes,
convertToJalali,
handleAttributeUpdated,
editModalData,
deleteAttribute,
searchQuery,
filterLoading,
attributeId,
attrebuteCat,
attributeTitle,
attributeValues,
searchPage,
currentPage,
totalPages,
paginate,
page,
prevPage,
nextPage,
handlePageInput,
visiblePages,
getCategories,
cats,
};
},
};
</script>
<template>
<Layout>
<BRow>
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 "
dir="rtl"
>
<div class="d-flex align-items-center">
<input
v-model="searchQuery"
type="text"
placeholder="جستجو..."
class="form-control form-control-sm d-inline-block me-2"
style="width: 250px; border-radius: 15px"
/>
<button
data-bs-toggle="modal"
data-bs-target="#addIdentity"
class="btn btn-light text-primary btn-sm px-3"
>
افزودن مشخصه
</button>
</div>
</div>
<div v-if="!filterLoading" class="card-body table-border-style p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered m-0" dir="rtl">
<thead class="table-light">
<tr>
<th>نام</th>
<th>دسته</th>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="attribute in attributes" :key="attribute.id">
<td>{{ attribute.title }}</td>
<td
:style="{
backgroundColor: attribute.code,
textAlign: 'center',
}"
></td>
<td>{{ convertToJalali(attribute?.created_at) }}</td>
<td>
<button
@click="
editModalData(
attribute?.id,
attribute?.title,
attribute.category_id
)
"
data-bs-toggle="modal"
data-bs-target="#editIdentity"
class="btn btn-sm btn-outline-warning me-1"
>
ویرایش
</button>
<button
@click="deleteAttribute(attribute.id, attribute.title)"
class="btn btn-sm btn-outline-danger"
:disabled="attribute.id == 1"
>
حذف
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
v-else
class="filter-loader card table-card user-profile-list"
></div>
</div>
</div>
<addIdentity @attribute-updated="handleAttributeUpdated()" :cats="cats" />
<editIdentity
@attribute-updated="handleAttributeUpdated()"
:title="attributeTitle"
:catId="attrebuteCat"
:id="attributeId"
:cats="cats"
/>
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>

<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>

<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>

<style scoped>
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-3px);
}
.table th,
.table td {
vertical-align: middle;
text-align: center;
}
.filter-loader {
border: 4px solid rgba(0, 123, 255, 0.3);
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.Brand-Image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.subject-box {
padding: 8px 14px;
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
color: #ef6c00;
font-weight: 600;
border-radius: 10px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
display: inline-flex;
align-items: center;
gap: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.subject-box i {
color: #ef6c00;
font-size: 1rem;
}

.subject-box:hover {
transform: translateY(-2px);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
}
.search-number {
display: flex;
align-items: center;
}

.search-number input {
width: 150px;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.375rem;
margin-bottom: 7px;
border: 1px solid #ced4da;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.search-number input:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
}

.search-number input::placeholder {
color: #6c757d;
}

.search-number input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.pagination {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.page-item {
flex: 0 0 auto;
}

.page-link {
cursor: pointer;
user-select: none;
}
</style>

+ 304
- 0
src/views/live-preview/pages/orders/allOrdersItems.vue View File

@@ -0,0 +1,304 @@
<script>
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import { onMounted, ref, watch, computed } from "vue";

import moment from "jalali-moment";
export default {
name: "PRODUCT-LIST",
components: {
Layout,
},
setup() {
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
const filterLoading = ref(false);
const allOrders = ref([]);
const getAllOrders = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/orders?paginate=${paginate.value || 10}&page=${page.value || 1}`
)
.then((resp) => {
console.log(resp);
allOrders.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
})
.catch((err) => {
console.log(err);
});
};



const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};

const getStatusLabel = (status) => {
const statusLabels = {
waiting: "در انتظار",
paid: "پرداخت‌شده",
un_paid: "پرداخت‌نشده",
approved: "تأیید‌شده",
processing: "در حال پردازش",
shipping: "در حال ارسال",
delivered: "تحویل‌شده",
canceled: "لغو‌شده",
};
return statusLabels[status] || "نامشخص";
};






const visiblePages = computed(() => {
const pages = [];
if (totalPages.value <= 5) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
let start = currentPage.value - 2;
let end = currentPage.value + 2;

if (start < 1) start = 1;
if (end > totalPages.value) end = totalPages.value;

for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
});

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
} else if (searchPage.value > totalPages.value) {
searchPage.value = totalPages.value;
}

if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
page.value = searchPage.value;
}
}



watch(page, () => {
getAllOrders();
});

const nextPage = () => {
if (currentPage.value < totalPages.value) {
page.value++;
getAllOrders();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
page.value--;
getAllOrders();
}
};

onMounted(() => {
getAllOrders();

});
return {
allOrders,
visiblePages,
nextPage,
prevPage,
handlePageInput,
currentPage,
totalPages,
page,
convertToJalali,
searchPage,
getStatusLabel
};
},
};
</script>

<template>
<Layout>
<BRow>
<BCol class="col-sm-12">
<BCard no-body class="table-card">
<BCardBody>
<div class="text-end p-sm-4 pb-sm-2">
<!-- Button to Trigger Export -->

</div>

<div class="table-responsive">
<table class="table table-hover tbl-product" id="pc-dt-simple">
<thead>
<tr>
<th >شناسه</th>
<th>تاریخ ایحاد</th>
<th>کد رهگیری</th>
<th>وضعیت</th>
</tr>
</thead>
<tbody>
<tr v-for="order in allOrders" :key="order?.id">
<td >{{ order?.id }}</td>
<td>{{ convertToJalali(order?.created_at) }}</td>
<td>{{order?.tracking_code}}</td>
<td >{{ order?.status }}</td>
</tr>
</tbody>
</table>
</div>
</BCardBody>
</BCard>
</BCol>
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>
<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>
<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>

<style scoped>
.product-img {
max-width: 48px;
min-width: 48px;
max-height: 45px;
min-height: 45px;
object-fit: cover;
}
.brand-img {
max-width: 43px;
min-width: 43px;
max-height: 40px;
min-height: 40px;
object-fit: cover;
}
.search-number {
display: flex;
align-items: center;
}

.search-number input {
width: 150px;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.375rem;
margin-bottom: 7px;
border: 1px solid #ced4da;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.search-number input:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
}

.search-number input::placeholder {
color: #6c757d;
}

.search-number input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.pagination {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.page-item {
flex: 0 0 auto;
}

.page-link {
cursor: pointer;
user-select: none;
}
</style>

+ 466
- 0
src/views/live-preview/pages/orders/approvedOrders.vue View File

@@ -0,0 +1,466 @@
<script>
import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import { onMounted, ref, watch, computed } from "vue";
import DatePicker from "vue3-persian-datetime-picker";
import Select2 from "vue3-select2-component";
import moment from "jalali-moment";
export default {
name: "PRODUCT-LIST",
components: {
Layout,
Select2,
DatePicker,
},
setup() {
const isLoading = ref(false);
const date = ref([]);
const brands = ref([]);
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
const filterLoading = ref(false);
const selectedBrand = ref();
const allProducts = ref([]);
const getAllProducts = () => {
filterLoading.value = true;
ApiServiece.get(
`admin/orders/order-items/approved?brand_id=${
selectedBrand.value || ""
}&start_date=${date.value[0] || ""}&end_date=${
date.value[1] || ""
}&paginate=${paginate.value || 10}&page=${page.value || 1}`
)
.then((resp) => {
console.log(resp);
allProducts.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
})
.catch((err) => {
console.log(err);
});
};

const getAllBrands = () => {
ApiServiece.get("admin/brands")
.then((resp) => {
console.log(resp);
brands.value = resp.data.data;
})
.catch((err) => {
console.log(err);
});
};

const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};

const formattedUsers = computed(() => {
return brands.value.map((brand) => ({
id: brand?.id,
text: brand?.title,
}));
});

const getFile = () => {
isLoading.value = true;
ApiServiece.post(
"admin/orders/order-items/approved/export",
{},
{ responseType: "blob" }
)
.then((resp) => {
const excelBlob = resp.data;

const url = window.URL.createObjectURL(excelBlob);

const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "order-items-export.xlsx");
document.body.appendChild(link);

link.click();

window.URL.revokeObjectURL(url);
document.body.removeChild(link);
isLoading.value = false;
})
.catch((err) => {
console.log(err);
isLoading.value = false;
});
};

watch(selectedBrand, () => {
getAllProducts();
page.value = 1;
});

const visiblePages = computed(() => {
const pages = [];
if (totalPages.value <= 5) {
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
} else {
let start = currentPage.value - 2;
let end = currentPage.value + 2;

if (start < 1) start = 1;
if (end > totalPages.value) end = totalPages.value;

for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
});

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
} else if (searchPage.value > totalPages.value) {
searchPage.value = totalPages.value;
}

if (searchPage.value >= 1 && searchPage.value <= totalPages.value) {
page.value = searchPage.value;
}
}

watch(selectedBrand, () => {
getAllProducts();
});

watch(date, () => {
getAllProducts();
});

watch(page, () => {
getAllProducts();
});

const nextPage = () => {
if (currentPage.value < totalPages.value) {
page.value++;
getAllProducts();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
page.value--;
getAllProducts();
}
};

function formatWithCommas(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

onMounted(() => {
getAllProducts();
getAllBrands();
});
return {
allProducts,
visiblePages,
nextPage,
prevPage,
handlePageInput,
currentPage,
totalPages,
page,
getFile,
convertToJalali,
searchPage,
brands,
formattedUsers,
selectedBrand,
date,
isLoading,
formatWithCommas,
};
},
};
</script>

<template>
<Layout>
<BRow>
<BCol class="col-sm-12">
<BCard no-body class="table-card">
<BCardBody>
<div class="text-end p-sm-4 pb-sm-2">
<!-- Button to Trigger Export -->
<BRow>
<BCol sm="3" class="mt-3">
<Select2
id="token"
v-model="selectedBrand"
:options="formattedUsers"
:settings="{
placeholder: 'انتخاب برند',
dir: 'rtl',
width: '100%',
theme: 'classic',
}"
class="select2 custom-select2"
/>
</BCol>
<BCol style="margin-right: 180px" class="mt-3" sm="3">
<div class="form-group">
<DatePicker
format="YYYY/MM/DD HH:mm:ss"
type="date"
:range="true"
v-model="date"
@input="handleInput"
></DatePicker>
</div>
</BCol>
<BCol sm="4" class="mt-3">
<button
@click="getFile"
type="button"
class="btn btn-primary"
:disabled="isLoading"
>
<span
v-if="isLoading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
<span v-else>گرفتن خروجی</span>
</button>
</BCol>
</BRow>

<!-- Product Selection Section -->
</div>

<div class="table-responsive">
<table class="table table-hover tbl-product" id="pc-dt-simple">
<thead>
<tr>
<th class="text-end">#</th>
<th>جزییات محصول</th>
<th>تاریخ ایحاد</th>
<th class="text-end">قیمت عمده</th>
<th class="text-end">قیمت تک</th>
<th class="text-end">تعداد سفارش</th>
<th class="text-end">تعداد ارسال شده</th>
<th class="text-center">عنوان برند</th>
<th class="text-center">تصویر برند</th>
</tr>
</thead>
<tbody>
<tr v-for="product in allProducts" :key="product?.id">
<td class="text-end">5</td>
<td>
<BRow>
<BCol class="col-auto pe-5">
<img
:src="product?.product?.image"
alt="user-image"
class="wid-40 rounded product-img"
/>
</BCol>
<BCol>
<h6 class="mb-1">{{ product?.product?.title }}</h6>
<p class="text-muted f-12 mb-0">
{{ product.product.description.slice(0, 25)
}}{{
product.product.description.length > 25
? "..."
: ""
}}
</p>
</BCol>
</BRow>
</td>
<td>{{ convertToJalali(product.created_at) }}</td>
<td
v-if="product?.product?.wholesale_price"
class="text-end"
>
{{ formatWithCommas(product?.product?.wholesale_price) }}تومان
</td>
<td
v-if="!product?.product?.wholesale_price"
class="text-end"
>
<i
class="ph-duotone ph-x-circle text-danger f-24"
data-bs-toggle="tooltip"
data-bs-title="danger"
></i>
</td>
<td v-if="product?.product?.retail_price" class="text-end">
{{ formatWithCommas(product?.product?.retail_price) }}
</td>
<td v-if="!product?.product?.retail_price" class="text-end">
<i
class="ph-duotone ph-x-circle text-danger f-24"
data-bs-toggle="tooltip"
data-bs-title="danger"
></i>
</td>
<td class="text-end">{{ product.count }}</td>
<td class="text-end">{{ product.send_count }}</td>
<td class="text-center">
{{ product.product?.brand?.title }}
</td>
<td class="text-center">
<img
:src="product.product?.brand?.image"
alt="user-image"
class="wid-40 brand-img"
/>
</td>
</tr>
</tbody>
</table>
</div>
</BCardBody>
</BCard>
</BCol>
</BRow>
<BRow>
<BCol sm="12">
<div class="d-flex justify-content-center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: currentPage === 1 }">
<span class="page-link" @click="prevPage">قبلی</span>
</li>
<li v-if="currentPage > 2" class="page-item" @click="page = 1">
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="currentPage > 3" class="page-item" disabled>
<span class="page-link">...</span>
</li>
<li
v-for="n in visiblePages"
:key="n"
class="page-item"
:class="{ active: currentPage === n }"
>
<a
class="page-link"
href="javascript:void(0)"
@click="page = n"
>
{{ n }}
</a>
</li>

<li
v-if="currentPage < totalPages - 2"
class="page-item"
disabled
>
<span class="page-link">...</span>
</li>
<li
v-if="currentPage < totalPages - 1"
class="page-item"
@click="page = totalPages"
>
<a class="page-link" href="javascript:void(0)">{{
totalPages
}}</a>
</li>

<li
class="page-item"
:class="{ disabled: currentPage === totalPages }"
>
<span class="page-link" @click="nextPage">بعدی</span>
</li>
</ul>
</nav>
</div>
</BCol>
<BCol sm="4">
<div class="ms-0 search-number">
<input
v-model="searchPage"
type="text"
class="form-control"
placeholder="برو به صفحه"
:max="totalPages"
min="1"
@input="handlePageInput"
/>
</div>
</BCol>
</BRow>
</Layout>
</template>

<style scoped>
.product-img {
max-width: 48px;
min-width: 48px;
max-height: 45px;
min-height: 45px;
object-fit: cover;
}
.brand-img {
max-width: 43px;
min-width: 43px;
max-height: 40px;
min-height: 40px;
object-fit: cover;
}
.search-number {
display: flex;
align-items: center;
}

.search-number input {
width: 150px;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.375rem;
margin-bottom: 7px;
border: 1px solid #ced4da;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.search-number input:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.25);
}

.search-number input::placeholder {
color: #6c757d;
}

.search-number input:disabled {
background-color: #f8f9fa;
cursor: not-allowed;
}
.pagination {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.page-item {
flex: 0 0 auto;
}

.page-link {
cursor: pointer;
user-select: none;
}
</style>

+ 128
- 5
src/views/live-preview/pages/orders/orders.vue View File

@@ -98,9 +98,10 @@ export default {
};
return statusLabels[status] || "نامشخص";
};

const deleteOrder = (id) => {
Swal.fire({
text: `می خواهید سفارش ${id} را حذف کنید ؟`,
text: `می خواهید سفارش ${id} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -128,6 +129,42 @@ export default {
});
};

const changeStatus = (id, status) => {
Swal.fire({
text: `آیا می خواهید وضعیت سبد خرید را به ${getStatusLabel(
status
)} تغییر دهید؟ `,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
const formData = new FormData();
formData.append("status", status);
ApiServiece.put(`admin/orders/${id}`, formData)
.then(() => {
toast.success("!تغییر وضعیت سبد خرید با موفقیت انجام شد", {
position: "top-right",
autoClose: 3000,
});
})
.then(() => {
getOrders();
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در تغییر وضعیت سبد خرید پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

function handlePageInput() {
if (searchPage.value < 1) {
searchPage.value = 1;
@@ -168,11 +205,10 @@ export default {
return {
orders,
convertToJalali,

deleteOrder,
searchQuery,
filterLoading,
changeStatus,
currentPage,
totalPages,
nextPage,
@@ -193,7 +229,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">
@@ -238,11 +274,95 @@ export default {
مشاهده
</router-link>
<button
class="btn btn-sm btn-outline-warning dropdown-toggle me-1"
type="button"
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
>
ویرایش وضعیت
</button>
<ul
class="dropdown-menu"
aria-labelledby="dropdownMenuButton"
>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'waiting')"
><span class="badge badge-waiting"
>در انتظار</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'paid')"
><span class="badge badge-paid">پرداخت‌شده</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'un_paid')"
><span class="badge badge-un_paid"
>پرداخت‌نشده</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'approved')"
><span class="badge badge-approved"
>تأیید‌شده</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'processing')"
><span class="badge badge-processing"
>در حال پردازش</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'shipping')"
><span class="badge badge-shipping"
>در حال ارسال</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'delivered')"
><span class="badge badge-delivered"
>تحویل‌شده</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus(order?.id, 'canceled')"
><span class="badge badge-canceled"
>لغو‌شده</span
></a
>
</li>
</ul>
<!-- <button
@click="deleteOrder(order?.id)"
class="btn btn-sm btn-outline-danger"
>
حذف
</button>
</button> -->
</td>
</tr>
</tbody>
@@ -454,4 +574,7 @@ export default {
.badge-canceled {
background-color: #6c757d;
}
.dropdown-item {
cursor: pointer;
}
</style>

+ 108
- 141
src/views/live-preview/pages/orders/singleOrder.vue View File

@@ -3,9 +3,6 @@ import Layout from "@/layout/custom.vue";
import ApiServiece from "@/services/ApiService";
import { useRoute } from "vue-router";
import { onMounted, ref } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";

export default {
name: "OrderDetails",
@@ -59,40 +56,47 @@ export default {
});
};

const changeStatus = (status) => {
Swal.fire({
text: `آیا می خواهید وضعیت سفارش را به ${status} تغییر دهید ؟ `,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
const formData = new FormData();
formData.append("status", status);
ApiServiece.put(`admin/orders/${route.params.id}` , formData)
.then(() => {
toast.success("!تغییر وضعیت با موفقیت انجام شد", {
position: "top-right",
autoClose: 3000,
});
})
.then(()=>{
getOrder()
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در تغییر وضعیت سفارش پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
const updateShippedCount = (item) => {
const formData = new FormData();
formData.append("send_count", item.send_count);
ApiServiece.put(`admin/orders/order-items/${item.id}`, formData)
.then(() => {
console.log("Shipped quantity updated successfully.");
})
.catch((error) => {
console.error("Failed to update shipped quantity:", error);
});
};

const updateNote = (item) => {
// Call API to update the note
console.log(item);
const formData = new FormData();
formData.append("description", item.description);
ApiServiece.put(`admin/orders/order-items/${item.id}`, formData)
.then(() => {
console.log("Note updated successfully.");
})
.catch((error) => {
console.error("Failed to update note:", error);
});
};

const updateStatus = (item) => {
const formData = new FormData();
formData.append("status", item.status);
ApiServiece.put(`admin/orders/order-items/${item.id}`, formData)
.then(() => {
console.log("Status updated successfully.");
})
.catch((error) => {
console.error("Failed to update status:", error);
});
};
function formatWithCommas(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

onMounted(() => {
getOrder();
});
@@ -102,7 +106,10 @@ export default {
getStatusClass,
formatDate,
getStatusLabel,
changeStatus,
updateShippedCount,
updateNote,
updateStatus,
formatWithCommas
};
},
};
@@ -117,128 +124,81 @@ export default {
class="d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">جزئیات سفارش</h5>
<div class="dropdown">
<button
class="btn btn-sm btn-outline-warning dropdown-toggle me-1"
type="button"
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
>
ویرایش وضعیت
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('waiting')"
><span class="badge badge-waiting">در انتظار</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('paid')"
><span class="badge badge-paid">پرداخت‌شده</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('un_paid')"
><span class="badge badge-un_paid">پرداخت‌نشده</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('approved')"
><span class="badge badge-approved">تأیید‌شده</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('processing')"
><span class="badge badge-processing"
>در حال پردازش</span
></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('shipping')"
><span class="badge badge-shipping">در حال ارسال</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('delivered')"
><span class="badge badge-delivered">تحویل‌شده</span></a
>
</li>
<li>
<a
class="dropdown-item d-flex justify-content-center align-items-center"
@click="changeStatus('canceled')"
><span class="badge badge-canceled">لغو‌شده</span></a
>
</li>
</ul>
</div>
</BCardHeader>
<BCardBody v-if="order">
<BRow class="mb-3">
<BCol md="6"> <strong>شناسه سفارش:</strong> {{ order.id }} </BCol>
<BCol md="6">
<strong>وضعیت:</strong>
<span class="badge me-2" :class="getStatusClass(order.status)">
{{ getStatusLabel(order.status) }}
</span>
</BCol>
</BRow>
<BRow class="mb-3">
<BCol md="6">
<strong>قیمت کل:</strong> {{ order.total_price }} تومان
</BCol>
<BCol md="6">
<strong>هزینه ارسال:</strong> {{ order.shipping_price }} تومان
</BCol>
</BRow>
<BRow class="mb-3">
<BCol md="6">
<strong>تاریخ ثبت سفارش:</strong>
{{ formatDate(order.created_at) }}
</BCol>
<BCol md="6">
<strong>تاریخ بروزرسانی:</strong>
{{ formatDate(order.updated_at) }}
</BCol>
</BRow>
<!-- Order Details and Items Here -->

<!-- Order Items -->
<h6 class="mt-4 mb-3">آیتم های سفارش</h6>
<div v-if="order.order_items.length > 0">
<BTable
hover
class="table-header"
bordered
:items="order.order_items"
:fields="[
{ key: 'id', label: 'شناسه آیتم' },
{ key: 'count', label: 'تعداد' },
{ key: 'title', label: 'عنوان محصول' },
{ key: 'count', label: 'تعداد در خواستی ' },
{ key: 'price', label: 'قیمت' },
{ key: 'created_at', label: 'تاریخ ثبت' },
{ key: 'edit_count', label: 'تعداد فرستاده شده' },
{ key: 'description', label: 'یادداشت' },
{ key: 'status', label: 'ویرایش وضعیت' },
]"
>
<!-- Price formatting -->
<template #cell(price)="data">
{{ data.item.price }} تومان
{{ formatWithCommas(data.item.price) }} تومان
</template>

<template #cell(title)="data">
{{ data.item?.product?.title }}
</template>

<!-- Created Date formatting -->
<template #cell(created_at)="data">
{{ formatDate(data.item.created_at) }}
</template>

<!-- Requested Quantity (Non-editable) -->
<template #cell(count)="data">
{{ data.item.count }}
</template>

<!-- Editable shipped quantity with API call -->
<template #cell(edit_count)="data">
<input
v-model="data.item.send_count"
type="number"
class="form-control"
:min="1"
:max="data.item.count"
placeholder="تعداد فرستاده شده"
@input="updateShippedCount(data.item)"
/>
</template>

<!-- Editable note with API call on focus out -->
<template #cell(description)="data">
<textarea
v-model="data.item.description"
class="form-control"
placeholder="یادداشت"
@focusout="updateNote(data.item)"
></textarea>
</template>

<!-- Editable status cell -->
<template #cell(status)="data">
<select
v-model="data.item.status"
class="form-control selector"
@change="updateStatus(data.item)"
placeholder="وضعیت"
>
<option value="done">کامل شده</option>
<option value="processing">در انتظار</option>
</select>
</template>
</BTable>
</div>
<div v-else class="text-center text-muted my-3">
@@ -293,7 +253,14 @@ export default {
h5 {
font-weight: bold;
}
.dropdown-item{
cursor: pointer;
.dropdown-item {
cursor: pointer;
}
.table-header {
text-align: center;
}
.selector {
padding: 4px;
height: 35px;
}
</style>

+ 219
- 46
src/views/live-preview/pages/products/addProduct.vue View File

@@ -8,7 +8,6 @@
</BCardHeader>
<BCardBody>
<BRow class="g-3">
<BCol md="6">
<div class="form-group">
<label class="form-label">عنوان </label>
@@ -26,15 +25,14 @@
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">اسلاگ</label>
<label class="form-label">کلمه کلیدی</label>
<input
type="text"
v-model="slug"
class="form-control"
placeholder="اسلاگ محصول"
placeholder="کلمه کلیدی محصول"
:class="{ 'is-invalid': errors.slug }"
@input="clearError('slug')"
/>
@@ -44,7 +42,6 @@
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">خلاصه</label>
@@ -149,6 +146,23 @@
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">تعداد در کارتن</label>
<input
type="number"
v-model="countInCarton"
class="form-control"
placeholder="تعداد"
:class="{ 'is-invalid': errors.countInCarton }"
@input="clearError('countInCarton')"
/>
</div>
<small v-if="errors.countInCarton" class="text-danger">
{{ errors.countInCarton }}
</small>
</BCol>

<BCol v-if="productType == 1 || productType == 3" md="6">
<div class="form-group">
<label class="form-label">قیمت (تک) </label>
@@ -247,7 +261,7 @@
</label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="expire"
@input="handleInput"
@@ -266,10 +280,8 @@
v-model="selectedCat"
class="form-control"
@change="clearError('selectedCat')"
placeholder="انتخاب دسته محصول"
>
<option value="" disabled selected>
انتخاب دسته محصول
</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -288,10 +300,8 @@
v-model="selectedBrand"
class="form-control"
@change="clearError('selectedBrand')"
placeholder="انتخاب برند محصول"
>
<option value="" disabled selected>
انتخاب برند محصول
</option>
<option
v-for="brand in brands"
:key="brand.id"
@@ -311,7 +321,22 @@
<h5 class="mb-0">اضافه کردن ویژگی ها</h5>
</div>
<BRow class="g-3 mt-2">
<template v-if="attrebutes.length === 0">
<BCol>
<div class="alert alert-info text-center">
هیچ ویژگی وجود ندارد برای اضافه کردن ویژگی کلیک کنید
<BButton
variant="success"
size="lg"
class="mt-1 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addAttribute"
>
<i class="fas fa-plus me-2"></i> کلیک کنید
</BButton>
</div>
</BCol>
</template>
<BCol
v-for="attrebute in attrebutes"
:key="attrebute.id"
@@ -323,7 +348,6 @@
<label
class="form-label d-flex align-items-center mb-3"
>
<BCol md="2" class="d-flex justify-content-center">
<BFormCheckbox
:id="'checkbox-id-' + attrebute.id"
@@ -332,7 +356,6 @@
/>
</BCol>

<BCol
md="5"
class="d-flex align-items-center"
@@ -341,7 +364,6 @@
{{ attrebute.title }}
</BCol>

<BCol md="5">
<input
type="number"
@@ -370,7 +392,82 @@
</BCard>

<BCard>
<div class="card-header">
<h5 class="mb-0">اضافه کردن مشخصه ها</h5>
</div>

<BRow class="g-3 mt-2">
<template v-if="relatedAttrebutes.length === 0">
<BCol>
<div class="alert alert-info text-center">
دسته ای انتخاب نکرده اید یا دسته انتخابی شما مشخصه ای در
خود ندارد برای اضافه کردن مشخصه کلیک کنید
<BButton
variant="success"
size="lg"
class="mt-1 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addIdentity"
>
<i class="fas fa-plus me-2"></i> کلیک کنید
</BButton>
</div>
</BCol>
</template>

<template v-else>
<BCol
v-for="identity in relatedAttrebutes"
:key="identity.id"
md="6"
lg="4"
>
<div class="card shadow-sm">
<div class="card-body">
<label
class="form-label d-flex align-items-center mb-3"
>
<BCol md="2" class="d-flex justify-content-center">
<BFormCheckbox
:id="'checkbox-id-' + identity.id"
class="mr-2 test"
v-model="identity.isChecked"
/>
</BCol>

<BCol md="5" class="d-flex align-items-center">
{{ identity.title }}
</BCol>

<BCol md="5">
<input
type="text"
class="form-control"
placeholder="مقدار"
:disabled="!identity.isChecked"
v-model="identity.value"
@input="clearError(`identity${identity.id}`)"
/>
</BCol>
</label>
<small
v-if="errors[`identityVal_${identity.id}`]"
class="text-danger"
>
{{ errors[`identityVal_${identity.id}`] }}
</small>
</div>
</div>
</BCol>

<small v-if="errors.selectedIdentities" class="text-danger">
{{ errors.selectedIdentities }}
</small>
</template>
</BRow>
</BCard>

<BCard>
<div
class="card-header text-center p-4"
style="background-color: #f7f7f7"
@@ -386,12 +483,10 @@
md="6"
>
<div class="form-group position-relative">
<label class="form-label mb-2 fw-bold text-secondary"
>تصویر محصول</label
>

<div class="custom-file">
<input
type="file"
@@ -402,7 +497,6 @@
/>
</div>

<div v-if="image.preview" class="mt-3 position-relative">
<img
:src="image.preview"
@@ -410,7 +504,6 @@
class="img-fluid rounded-3 shadow-lg border Image-Preview"
/>

<button
type="button"
@click="removeImage()"
@@ -426,7 +519,6 @@
</small>
</BRow>

<div class="text-center mt-4">
<button
@click="addImage"
@@ -459,6 +551,11 @@
</div>
</div>
</BCardFooter>
<addIdentity :cats="cats" />
<addAttribute
:attributeValues="attributeValues"
@attribute-updated="handleAttributeUpdated()"
/>
</BCard>
</BCol>
</BRow>
@@ -466,22 +563,29 @@
</template>

<script>
import addAttribute from "@/components/modals/attribute/addAttribute.vue";
import moment from "moment";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { ref, onMounted } from "vue";
import { ref, onMounted, watch } from "vue";
import Layout from "@/layout/custom.vue";
import DatePicker from "vue3-persian-datetime-picker";
import addIdentity from "@/components/modals/identity/addIdentity.vue";
export default {
name: "SAMPLE-PAGE",
components: {
Layout,
DatePicker,
addIdentity,
addAttribute,
},
setup() {
const attributeValues = ref();
const relatedAttrebutes = ref([]);
const countInCarton = ref();
const selectedAttributes = ref();
const selectedIdentities = ref();
const expire = ref();
const chosenPrice = ref();
const spescialPrice = ref();
@@ -491,7 +595,7 @@ export default {
const productAttributes = ref([]);
const images = ref([{ file: null, preview: null }]);
const brands = ref();
const attrebutes = ref();
const attrebutes = ref([]);
const selectedBrand = ref();
const selectedCat = ref();
const date = ref();
@@ -521,13 +625,38 @@ export default {
});
};

const handleInput = () => {
if (expire.value) {
expire.value = moment(expire.value).format("YYYY-MM-DD HH:mm:ss");
}
clearError("expire");
const getAttributeValues = () => {
ApiServiece.get(`admin/attributes`).then((resp) => {
console.log(resp);
attributeValues.value = resp.data.data;
});
};

const handleAttributeUpdated = () => {
getAttrebuteValues();
};

watch(selectedCat, () => {
ApiServiece.get(`admin/attributes?category_id=${selectedCat.value}`)
.then((resp) => {
relatedAttrebutes.value = resp.data.data;
console.log(relatedAttrebutes.value);
})
.catch((err) => {
console.log(err);
});
});

const handleInput = () => {
if (expire.value) {
// Convert from Jalali to Georgian (Gregorian)
expire.value = moment(expire.value, "jYYYY/jMM/jDD HH:mm:ss").format("YYYY-MM-DD HH:mm:ss");
} else {
expire.value = null;
clearError("expire");
}
};

const getBrands = () => {
ApiServiece.get(`admin/brands`)
.then((resp) => {
@@ -539,7 +668,7 @@ export default {
};

const getAttrebuteValues = () => {
ApiServiece.get(`admin/attribute-values`)
ApiServiece.get(`admin/attribute-values?attribute_id=1`)
.then((resp) => {
attrebutes.value = resp.data.data;
console.log(attrebutes.value);
@@ -604,7 +733,7 @@ export default {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان محصول الزامی است";
if (!slug.value)
errors.value.slug = "وارد کردن اسلاگ محصول ضروری می باشد";
errors.value.slug = "وارد کردن کلمه کلیدی محصول ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه محصول ضروری می باشد";
if (!selectedCat.value)
@@ -633,6 +762,10 @@ export default {
if (!selectedBrand.value)
errors.value.selectedBrand = "انتخاب برند برای محصول ضروری می باشد";

if (!countInCarton.value)
errors.value.countInCarton =
"انتخاب تعداد محصول در هر کارتن ضروری می باشد";

if (images.value.length <= 0)
errors.value.images = "انتخاب عکس برای محصول ضروری می باشد";

@@ -650,6 +783,20 @@ export default {
});
}

const missingIdentityVal = relatedAttrebutes.value.filter(
(identity) =>
identity.isChecked &&
(identity.value == null || identity.value === "")
);

if (missingIdentityVal.length > 0) {
missingIdentityVal.forEach((identity) => {
errors.value[
`identityVal_${identity.id}`
] = `وارد کردن مقدار مشخصه الزامی می باشد`;
});
}

return Object.keys(errors.value).length === 0;
};

@@ -661,10 +808,10 @@ export default {
getCats();
getBrands();
getAttrebuteValues();
getAttributeValues();
});
let id = "";
const submitForm = () => {
console.log(expire.value);
if (!validateForm()) return;

loading.value = true;
@@ -713,6 +860,7 @@ export default {

formData.append("brand_id", selectedBrand.value);
formData.append("category_id", selectedCat.value);
formData.append("count_in_carton", countInCarton.value);
formData.append("image", image.value);

ApiServiece.post(`admin/products`, formData, {
@@ -740,8 +888,28 @@ export default {
const jsonString = JSON.stringify(finalPayload, null, 2);

console.log(jsonString);
ApiServiece.post(`admin/products/${id}/attributes`, jsonString).then(
(resp) => {
ApiServiece.post(`admin/products/${id}/attributes`, jsonString)
.then(() => {
selectedIdentities.value = relatedAttrebutes.value
.filter((identity) => identity.isChecked)
.map((identity) => ({
attribute_id: identity.id,
attribute_value_title: identity.value,
}));

const finalPayload = {
productSolidAttributes: selectedIdentities.value,
};

const jsonString = JSON.stringify(finalPayload, null, 2);
ApiServiece.post(
`admin/products/${id}/solid-attributes`,
jsonString
).then((resp) => {
console.log(resp);
});
})
.then((resp) => {
console.log(resp);
images.value.map((image) => {
console.log(image.file);
@@ -752,25 +920,25 @@ export default {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
});
});
}
);
});
})

.then(() => {
loading.value = false;
toast.success("!محصول با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
})

.catch((error) => {
console.error(error);
toast.error("!مشکلی در ایجاد محصوا بوجود آمد", {
toast.error(`${error?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
toast.success("!محصول با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
});
};

@@ -811,6 +979,11 @@ export default {
chosenPrice,
expire,
handleInput,
countInCarton,
relatedAttrebutes,
selectedIdentities,
attributeValues,
handleAttributeUpdated,
};
},
};


+ 574
- 104
src/views/live-preview/pages/products/editProduct.vue View File

@@ -29,12 +29,12 @@
<!-- Second Input Field (Slug) -->
<BCol md="6">
<div class="form-group">
<label class="form-label">اسلاگ</label>
<label class="form-label">کلمه کلیدی</label>
<input
type="text"
v-model="slug"
class="form-control"
placeholder="اسلاگ محصول"
placeholder="کلمه کلیدی محصول"
:class="{ 'is-invalid': errors.slug }"
@input="clearError('slug')"
/>
@@ -95,7 +95,6 @@
alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview"
/>
</div>

<small v-if="errors.image" class="text-danger">
@@ -112,10 +111,8 @@
v-model="productType"
class="form-control"
@select="clearError('productType')"
placeholder="انتخاب حالت محصول"
>
<option value="" disabled selected>
انتخاب حالت محصول
</option>
<option value="1">تک</option>
<option value="2">عمده</option>
<option value="3">هردو</option>
@@ -143,6 +140,23 @@
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">تعداد در کارتن</label>
<input
type="number"
v-model="countInCarton"
class="form-control"
placeholder="تعداد"
:class="{ 'is-invalid': errors.countInCarton }"
@input="clearError('countInCarton')"
/>
</div>
<small v-if="errors.countInCarton" class="text-danger">
{{ errors.countInCarton }}
</small>
</BCol>

<BCol v-if="productType == 1 || productType == 3" md="6">
<div class="form-group">
<label class="form-label">قیمت (تک) </label>
@@ -168,10 +182,8 @@
v-model="isChosen"
class="form-control"
@select="clearError('isChosen')"
placeholder="انتخاب حالت منتخب"
>
<option value="" disabled selected>
انتخاب حالت منتخب
</option>
<option value="1">هست</option>
<option value="0">نیست</option>
</select>
@@ -206,8 +218,8 @@
v-model="spescial"
class="form-control"
@select="clearError('spescial')"
placeholder="انتخاب حالت ویژه"
>
<option value="" disabled selected>انتخاب حالت ویژه</option>
<option value="1">هست</option>
<option value="0">نیست</option>
</select>
@@ -241,10 +253,9 @@
</label>

<DatePicker
format="YYYY/MM/DD HH:mm:ss"
:format="'jYYYY/jMM/jDD HH:mm:ss'"
type="datetime"
v-model="expire"
@input="handleInput"
></DatePicker>
</div>
<small v-if="errors.expire" class="text-danger">
@@ -260,10 +271,8 @@
v-model="selectedCat"
class="form-control"
@input="clearError('selectedCat')"
placeholder="انتخاب دسته محصول"
>
<option value="" disabled selected>
انتخاب دسته محصول
</option>
<option v-for="cat in cats" :key="cat.id" :value="cat.id">
{{ cat.title }}
</option>
@@ -282,10 +291,8 @@
v-model="selectedBrand"
class="form-control"
@select="clearError('selectedBrand')"
placeholder="انتخاب برند محصول"
>
<option value="" disabled selected>
انتخاب برند محصول
</option>
<option
v-for="brand in brands"
:key="brand.id"
@@ -304,6 +311,13 @@
<div class="card-header">
<h5 class="mb-0">ویرایش ویژگی ها</h5>
</div>
<template v-if="locals.length === 0">
<BCol>
<div class="alert alert-info text-center">
هیچ ویژگی برای این محصول انتخاب نکرده اید ...
</div>
</BCol>
</template>
<BRow class="g-3 mt-2">
<!-- Loop through attributes -->
<BCol
@@ -398,9 +412,149 @@

<BCard>
<div class="card-header">
<h5 class="mb-0">اضافه کردن ویژگی ها</h5>
<h5 class="mb-0">ویرایش مشخصه ها</h5>
</div>
<template v-if="localIdentities.length === 0">
<BCol>
<div class="alert alert-info text-center">
برای شما مشخصه‌ای ثبت نشده است. برای ساختن یک مشخصه کلیک
کنید.
</div>
</BCol>
</template>
<BRow class="g-3 mt-2">
<!-- Loop through attributes -->
<BCol
v-for="identity in localIdentities"
:key="identity.id"
md="6"
lg="4"
>
<div class="card shadow-sm">
<div class="card-body">
<!-- Card Header with Delete Icon -->
<div
class="d-flex justify-content-between align-items-center"
>
<!-- Optional Title or Heading for the card -->
<div class="card-title">
<!-- You can add a title or leave it empty if you don't need one -->
</div>

<!-- Delete Icon -->
<button
class="btn btn-link text-danger p-0"
@click="deleteIdentity(identity.id)"
title="حذف"
>
<i class="fas fa-trash-alt"></i>
</button>
</div>

<!-- Checkbox Column -->
<label
class="form-label d-flex align-items-center mb-3"
>
<BCol md="2" class="d-flex justify-content-center">
<BFormCheckbox
:id="'checkbox-id-' + identity.id"
class="mr-2 test"
v-model="identity.isChecked"
/>
</BCol>

<!-- Title Column with Dynamic Color -->
<BCol md="5" class="d-flex align-items-center">
{{ identity.title }}
</BCol>

<!-- Number Input Column -->
<BCol md="5">
<input
type="text"
class="form-control"
placeholder="تعداد"
:disabled="!identity.isChecked"
v-model="identity.value"
/>
</BCol>
</label>

<!-- Error Message -->

<!-- Edit Button -->
<div class="mt-3 text-center">
<button
class="btn btn-primary"
@click="
editIdentity(
identity.id,
identity.value,
identity.attribute_value_id
)
"
>
ویرایش
</button>
</div>
</div>
</div>
</BCol>

<small v-if="errors.selectedAttributes" class="text-danger">
{{ errors.selectedAttributes }}
</small>
</BRow>
</BCard>

<BCard>
<div class="card-header">
<h5 class="mb-0">اضافه کردن ویژگی جدید</h5>
</div>
<BRow class="g-3 mt-2">
<template v-if="attrebutes.length === 0 && !repeatedIdentity">
<BCol>
<div class="alert alert-info text-center">
برای شما ویژگی ثبت نشده است. برای ساختن یک ویژگی
<BButton
variant="success"
size="lg"
class="mt-2 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addIdentity"
>
<i class="fas fa-plus me-2"></i> کلیک کنید
</BButton>
</div>
</BCol>
</template>
<template v-if="attrebutes.length === 0 && repeatedAttrebute">
<BCol>
<div
class="alert alert-info text-center p-4 shadow-lg rounded-lg"
>
<h5 class="mb-3">
هیچ ویژگی جدیدی برای شما ثبت نشده است
</h5>
<p class="mb-3">
شما می‌توانید ویژگی های قبلی را ویرایش کنید.
<br />
برای اضافه کردن ویژگی جدید ، لطفاً دکمه زیر را کلیک
کنید .
</p>
<BButton
variant="success"
size="lg"
class="mt-3 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addAttribute"
>
<i class="fas fa-plus me-2"></i> اضافه کردن ویژگی جدید
</BButton>
</div>
</BCol>
</template>

<!-- Loop through attributes -->
<BCol
v-for="attrebute in attrebutes"
@@ -459,6 +613,105 @@
</BRow>
</BCard>

<BCard>
<div class="card-header">
<h5 class="mb-0">اضافه کردن مشخصه جدید</h5>
</div>
<template v-if="identities.length === 0 && !repeatedIdentity">
<BCol>
<div class="alert alert-info text-center">
برای شما مشخصه‌ای ثبت نشده است. برای ساختن یک مشخصه
<BButton
variant="success"
size="lg"
class="mt-2 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addIdentity"
>
<i class="fas fa-plus me-2"></i> کلیک کنید
</BButton>
</div>
</BCol>
</template>

<template v-if="identities.length === 0 && repeatedIdentity">
<BCol>
<div
class="alert alert-info text-center p-4 shadow-lg rounded-lg"
>
<h5 class="mb-3">
هیچ مشخصه جدیدی برای این دسته‌بندی ثبت نشده است
</h5>
<p class="mb-3">
شما می‌توانید مشخصه‌های قبلی را ویرایش کنید.
<br />
برای اضافه کردن مشخصه جدید به این دسته، لطفاً دکمه زیر
را کلیک کنید .
</p>
<BButton
variant="success"
size="lg"
class="mt-3 px-4 py-2 shadow-lg rounded-pill"
data-bs-toggle="modal"
data-bs-target="#addIdentity"
>
<i class="fas fa-plus me-2"></i> اضافه کردن مشخصه جدید
</BButton>
</div>
</BCol>
</template>
<BRow class="g-3 mt-2">
<!-- Loop through attributes -->
<BCol
v-for="identity in identities"
:key="identity.id"
md="6"
lg="4"
>
<div class="card shadow-sm">
<div class="card-body">
<label
class="form-label d-flex align-items-center mb-3"
>
<!-- Checkbox Column -->
<BCol md="2" class="d-flex justify-content-center">
<BFormCheckbox
:id="'checkbox-id-' + identity.id"
class="mr-2 test"
v-model="identity.isChecked"
/>
</BCol>

<!-- Title Column with Dynamic Color -->
<BCol md="5" class="d-flex align-items-center">
{{ identity.title }}
</BCol>

<!-- Number Input Column -->

<BCol md="5">
<input
type="text"
class="form-control"
placeholder="مقدار"
:disabled="!identity.isChecked"
v-model="identity.value"
@input="clearError(`identityVal_${identity.id}`)"
/>
</BCol>
</label>
<small
v-if="errors[`identityVal_${identity.id}`]"
class="text-danger"
>
{{ errors[`identityVal_${identity.id}`] }}
</small>
</div>
</div>
</BCol>
</BRow>
</BCard>

<BCard>
<!-- Card Header -->
<div
@@ -548,6 +801,11 @@
</button>
</div>
</div>
<addIdentity
:cats="cats"
@attribute-updated="handleAttributeUpdated()"
/>
<addAttribute @attribute-updated="handleAttributeUpdated()" />
</BCardFooter>
</BCard>
</BCol>
@@ -562,8 +820,10 @@ import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import moment from "moment";
import ApiServiece from "@/services/ApiService";
import { ref, onMounted } from "vue";
import { ref, onMounted, watch } from "vue";
import Layout from "@/layout/custom.vue";
import addIdentity from "@/components/modals/identity/addIdentity.vue";
import addAttribute from "@/components/modals/attribute/addAttribute.vue";
import DatePicker from "vue3-persian-datetime-picker";

export default {
@@ -571,15 +831,10 @@ export default {
components: {
Layout,
DatePicker,
addIdentity,
addAttribute,
},
setup() {
const selectedAttrebutes = ref([
{
inventory: null,
type: null,
attribute_value_id: null,
},
]);
const locals = ref([
{
id: null,
@@ -589,13 +844,18 @@ export default {
isChecked: null,
},
]);
const localIdentities = ref([]);
const countInCarton = ref();
const files = ref([]);
const productValueId = ref();
const localsIds = ref();
const identities = ref([]);
const localIdentitiesIds = ref([]);
const selectedAttributes = ref();
const selectedidentities = ref([]);
const product = ref();
const route = useRoute();
const imagesTosend = ref()
const imagesTosend = ref();
const expire = ref();
const chosenPrice = ref();
const spescialPrice = ref();
@@ -606,7 +866,7 @@ export default {
const images = ref([{ file: null, preview: null }]);
const localImages = ref([{ preview: null }]);
const brands = ref();
const attrebutes = ref();
const attrebutes = ref([]);
const selectedBrand = ref();
const selectedCat = ref();
const date = ref();
@@ -616,7 +876,8 @@ export default {
const loading = ref(false);
const image = ref();
const imagePreview = ref();

const repeatedIdentity = ref(false);
const repeatedAttrebute = ref(false);
const errors = ref({});
const title = ref("");
const slug = ref("");
@@ -637,13 +898,59 @@ export default {
});
};

const handleInput = () => {
if (expire.value) {
expire.value = moment(expire.value).format("YYYY-MM-DD HH:mm:ss");
}
clearError("expire");
watch(selectedCat, () => {
ApiServiece.get(`admin/attributes?category_id=${selectedCat.value}`)
.then((resp) => {
identities.value = resp.data.data;
console.log(identities.value);
})
.catch((err) => {
console.log(err);
});
});

const getIdentities = () => {
ApiServiece.get(`admin/attributes?category_id=${selectedCat.value}`)
.then((resp) => {
identities.value = resp.data.data;
console.log(identities.value);
})
.then(() => {
localIdentitiesIds.value = localIdentities.value.map(
(identity) => identity.title
);

let hasRepeatedIdentity = false;

identities.value = identities.value.filter((identity) => {
if (!localIdentitiesIds.value.includes(identity.title)) {
return true;
} else {
hasRepeatedIdentity = true;
return false;
}
});

repeatedIdentity.value = hasRepeatedIdentity;

console.log(repeatedIdentity.value);
});
};

const convertToGeorgian = (date) => {
return moment(date, "jYYYY/jMM/jDD HH:mm:ss").format(
"YYYY-MM-DD HH:mm:ss"
);
};

watch(expire, (newValue) => {
if (newValue) {
// Convert from Jalali to Georgian (Gregorian) format when `expire` changes
expire.value = convertToGeorgian(newValue);
console.log(expire.value); // Logs the Georgian format for debugging
}
});

const getBrands = () => {
ApiServiece.get(`admin/brands`)
.then((resp) => {
@@ -655,12 +962,11 @@ export default {
};

const getAttrebuteValues = () => {
ApiServiece.get(`admin/attribute-values`)
ApiServiece.get(`admin/attribute-values?attribute_id=1`)
.then((resp) => {
console.log(resp);
attrebutes.value = resp.data.data;
console.log("Attributes before filtering:", attrebutes.value);

console.log("Filtered attributes:", attrebutes.value);
})
.then(() => {
getProduct();
@@ -675,7 +981,7 @@ export default {

const deleteAttribute = (id) => {
Swal.fire({
title: `آیا می خواهید این ویژگی را حذف کنید ؟`,
text: `آیا می خواهید این ویژگی را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -705,9 +1011,43 @@ export default {
});
};

const deleteIdentity = (id) => {
Swal.fire({
text: `آیا می خواهید این مشخصه را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "بله!",
cancelButtonText: "خیر",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.delete(
`admin/products/${route.params.id}/attributes/${id}`
)
.then(() => {
toast.success("!مشخصه با موفقیت حذف شد", {
position: "top-right",
autoClose: 3000,
});
localIdentities.value = localIdentities.value.filter(
(identity) => identity.id !== id
);
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در حذف کردن مشخصه پیش آمد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

const deletImage = (name) => {
Swal.fire({
title: `آیا می خواهید این عکس را حذف کنید ؟`,
text: `آیا می خواهید این عکس را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -811,7 +1151,7 @@ export default {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان محصول الزامی است";
if (!slug.value)
errors.value.slug = "وارد کردن اسلاگ محصول ضروری می باشد";
errors.value.slug = "وارد کردن کلمه کلیدی محصول ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه محصول ضروری می باشد";
if (!selectedCat.value)
@@ -840,6 +1180,10 @@ export default {
if (!selectedBrand.value)
errors.value.selectedBrand = "انتخاب برند برای محصول ضروری می باشد";

if (!countInCarton.value)
errors.value.countInCarton =
"انتخاب تعداد محصول در هر کارتن ضروری می باشد";

if (images.value.length <= 0)
errors.value.images = "انتخاب عکس برای محصول ضروری می باشد";

@@ -857,6 +1201,20 @@ export default {
});
}

const missingIdentityVal = identities.value.filter(
(identity) =>
identity.isChecked &&
(identity.value == null || identity.value === "")
);

if (missingIdentityVal.length > 0) {
missingIdentityVal.forEach((identity) => {
errors.value[
`identityVal_${identity.id}`
] = `وارد کردن مقدار مشخصه الزامی می باشد`;
});
}

return Object.keys(errors.value).length === 0;
};

@@ -865,57 +1223,88 @@ export default {
};

const getProduct = () => {
ApiServiece.get(`admin/products/${route.params.id}`).then((resp) => {
productValueId.value = resp.data.data;
console.log(resp.data.data.product_attributes);
product.value = resp.data.data;
title.value = product.value?.title;
slug.value = product.value?.slug;
summary.value = product.value?.summary;
description.value = product.value?.description;
imagePreview.value = product.value?.image;

productType.value = product.value?.type;
wholesalePrice.value = product.value?.wholesale_price;
retailePrice.value = product.value?.retail_price;
isChosen.value = product.value?.is_chosen;
chosenPrice.value = product.value?.chosen_price;
spescial.value = product.value?.is_special;
spescialPrice.value = product.value?.special_price;
expire.value = product.value?.special_expires_at;
selectedBrand.value = product.value?.brand_id;
selectedCat.value = product.value?.category_id;

// Update images
images.value = product.value.images.map((imageUrl) => ({
preview: imageUrl,
}));

localImages.value = product.value.images.map((imageUrl) => ({
preview: imageUrl,
}));

locals.value = product.value.product_attributes.map(
(productAttribute) => {
return {
id: productAttribute.id,
title: productAttribute.attribute_value.title,
code: productAttribute.attribute_value.code,
inventory: productAttribute.inventory,
isChecked: productAttribute.inventory > 0,
value: productAttribute.inventory,
};
}
);
ApiServiece.get(`admin/products/${route.params.id}`)
.then((resp) => {
console.log(resp);
productValueId.value = resp.data.data;
console.log(resp.data.data.product_attributes);
product.value = resp.data.data;
title.value = product.value?.title;
slug.value = product.value?.slug;
summary.value = product.value?.summary;
description.value = product.value?.description;
imagePreview.value = product.value?.image;
productType.value = product.value?.type;
wholesalePrice.value = product.value?.wholesale_price;
retailePrice.value = product.value?.retail_price;
isChosen.value = product.value?.is_chosen;
chosenPrice.value = product.value?.chosen_price;
spescial.value = product.value?.is_special;
spescialPrice.value = product.value?.special_price;
expire.value = product.value?.special_expires_at;
selectedBrand.value = product.value?.brand_id;
selectedCat.value = product.value?.category_id;
countInCarton.value = product.value?.count_in_carton;

// Update images
images.value = product.value.images.map((imageUrl) => ({
preview: imageUrl,
}));

localImages.value = product.value.images.map((imageUrl) => ({
preview: imageUrl,
}));

locals.value = product.value.product_attributes.map(
(productAttribute) => {
return {
id: productAttribute.id,
title: productAttribute.attribute_value.title,
code: productAttribute.attribute_value.code,
inventory: productAttribute.inventory,
isChecked: productAttribute.inventory > 0,
value: productAttribute.inventory,
};
}
);

localsIds.value = locals.value.map((attribute) => attribute.code);

let hasRepeatedAttribute = false;

attrebutes.value = attrebutes.value.filter((attrebute) => {
if (!localsIds.value.includes(attrebute.code)) {
return true;
} else {
hasRepeatedAttribute = true;
return false;
}
});

localsIds.value = locals.value.map((attribute) => attribute.code);
console.log(localsIds.value);
repeatedAttrebute.value = hasRepeatedAttribute;

console.log(attrebutes.value);
attrebutes.value = attrebutes.value.filter(
(attribute) => !localsIds.value.includes(attribute.code)
);
});
console.log(attrebutes.value);

console.log(attrebutes.value);

localIdentities.value = product.value.product_solid_attributes.map(
(productIdentities) => {
return {
id: productIdentities.id,
title: productIdentities.attribute_value.attribute.title,
isChecked: true,
value: productIdentities.attribute_value.title,
attribute_value_id: productIdentities.attribute_value_id,
};
}
);
})
.then(() => {
getIdentities();
})
.catch((err) => {
console.log(err);
});
};

const editAttribute = (id, inventory) => {
@@ -951,6 +1340,41 @@ export default {
});
};

const editIdentity = (id, title, valueID) => {
Swal.fire({
text: "آیا برای ویرایش مشخصه اطمینان دارید؟",
icon: "warning",
showCancelButton: true,
confirmButtonText: "بله، ویرایش کن",
cancelButtonText: "لغو",
reverseButtons: true,
}).then((result) => {
if (result.isConfirmed) {
const formData = new FormData();

formData.append("attribute_id", id);
formData.append("title", title);
ApiServiece.put(`admin/attribute-values/:${valueID}`, formData)
.then(() => {
toast.success("!مشخصه با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 3000,
});
})
.catch(() => {
toast.error("!مشکلی در ویرایش مشخصه ایجاد شد", {
position: "top-right",
autoClose: 3000,
});
});
}
});
};

const handleAttributeUpdated = () => {
getProduct();
};

onMounted(() => {
getCats();
getBrands();
@@ -959,6 +1383,7 @@ export default {
let id = "";
const submitForm = () => {
console.log(errors.value);

if (!validateForm()) return;

loading.value = true;
@@ -969,6 +1394,7 @@ export default {
formData.append("slug", slug.value);
formData.append("summary", summary.value);
formData.append("description", description.value);
formData.append("count_in_carton", countInCarton.value);
if (productType.value == 2) {
formData.append("wholesale_price", wholesalePrice.value);
}
@@ -1021,6 +1447,7 @@ export default {
},
})
.then((resp) => {
loading.value = false;
id = resp.data.data.id;
console.log(id);

@@ -1045,19 +1472,38 @@ export default {
);
}

selectedidentities.value = identities.value
.filter((identity) => identity.isChecked)
.map((identity) => ({
attribute_id: identity.id,
attribute_value_title: identity.value,
}));
if (selectedidentities.value.length > 0) {
const finalPayload = {
productSolidAttributes: selectedidentities.value,
};
const jsonString = JSON.stringify(finalPayload, null, 2);
ApiServiece.post(
`admin/products/${route.params.id}/solid-attributes`,
jsonString
);
}

if (files.value.length > 0) {
console.log(files.value);

imagesTosend.value = images.value.filter((image) => {
imagesTosend.value = images.value.filter((image) => {
return !localImages.value.some(
(localImage) => localImage.preview === image.preview
);
});

imagesTosend.value.map((image) => {
// Collect all promises
const uploadPromises = imagesTosend.value.map((image) => {
const formData = new FormData();
formData.append("image", image.file);
ApiServiece.post(

return ApiServiece.post(
`admin/products/${route.params.id}/images`,
formData,
{
@@ -1066,26 +1512,42 @@ export default {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
).then((resp) => {
console.log(resp);
);
});

// Wait for all uploads to finish
Promise.all(uploadPromises)
.then((responses) => {
console.log("All images uploaded:", responses);
loading.value = false;
toast.success("!محصول با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
})
.catch((error) => {
console.error("Error uploading images:", error);
loading.value = false;
toast.error("خطایی در بارگذاری تصاویر رخ داد", {
position: "top-right",
autoClose: 1000,
});
});
} else {
toast.success("!محصول با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
}
})

.catch((error) => {
loading.value = false;
console.error(error);
toast.error("!مشکلی در ویرایش محصول ایحاد شد", {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
toast.success("!محصول با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
});
};

@@ -1117,7 +1579,6 @@ export default {
images,
addImage,
handleImageUpload,
selectedAttrebutes,
description,
onCheckboxChange,
retailePrice,
@@ -1125,12 +1586,21 @@ export default {
spescialPrice,
chosenPrice,
expire,
handleInput,
selectedAttributes,
selectedidentities,
locals,
editAttribute,
deleteAttribute,
deletImage,
countInCarton,
localIdentities,
deleteIdentity,
identities,
handleAttributeUpdated,
editIdentity,
repeatedIdentity,
repeatedAttrebute,
};
},
};


+ 12
- 28
src/views/live-preview/pages/products/products.vue View File

@@ -18,6 +18,9 @@ export default {
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
function formatWithCommas(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

const filterLoading = ref(false);
const searchQuery = ref("");
@@ -73,7 +76,7 @@ export default {

const deleteProduct = (id, title) => {
Swal.fire({
title: `می خواهید محصول ${title} را حذف کنید ؟`,
text: `می خواهید محصول ${title} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -105,7 +108,7 @@ export default {

const restoreProduct = (id, title) => {
Swal.fire({
title: `می خواهید محصول ${title} را بازیابی کنید ؟`,
text: `می خواهید محصول ${title} را بازیابی کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -126,7 +129,7 @@ export default {
})
.catch((err) => {
console.log(err);
toast.error("!مشکلی در بایابی محصول پیش آمد", {
toast.error("!مشکلی در بازیابی محصول پیش آمد", {
position: "top-right",
autoClose: 3000,
});
@@ -187,6 +190,7 @@ export default {
searchPage,
visiblePages,
restoreProduct,
formatWithCommas
};
},
};
@@ -197,7 +201,7 @@ export default {
<div class="col-md-12">
<div class="card shadow-sm border-0 rounded">
<div
class="card-header d-flex justify-content-between align-items-center p-3 bg-primary text-white"
class="card-header d-flex justify-content-between align-items-center p-3"
dir="rtl"
>
<div class="d-flex align-items-center">
@@ -253,36 +257,31 @@ export default {
<td>{{ product.sold_count }}</td>
<td>
<span v-if="product.is_chosen == 0">
نیست
<i
class="fas fa-times-circle status-icon unavailable"
></i>
</span>
<span v-if="product.is_chosen == 1">
انتخاب شده
<i
class="fas fa-check-circle status-icon available"
></i>
</span>
</td>
<td v-if="product?.chosen_price">
{{ product?.chosen_price }}
{{ formatWithCommas(product?.chosen_price) }}
</td>
<td v-if="!product.chosen_price">
ندارد
<i
class="fas fa-times-circle status-icon unavailable"
></i>
</td>
<td>
<span v-if="product.is_special == 0">
نیست
<i
class="fas fa-times-circle status-icon unavailable"
></i>
</span>
<span v-if="product.is_special == 1">
انتخاب شده
<i
class="fas fa-check-circle status-icon available"
></i>
@@ -290,10 +289,10 @@ export default {
</td>

<td v-if="product?.wholesale_price">
{{ product.wholesale_price }}
{{ formatWithCommas(product.wholesale_price) }}
</td>
<td v-if="product?.retail_price">
{{ product?.retail_price }}
{{ formatWithCommas(product?.retail_price) }}
</td>

<td v-if="!product.deleted_at">
@@ -498,7 +497,6 @@ export default {
user-select: none;
}
.table {
background-color: #f9f9f9;
border-radius: 8px;
overflow: hidden;
font-size: 0.9rem;
@@ -513,19 +511,10 @@ export default {
vertical-align: middle;
}

.table-light {
background-color: #f8f9fa;
}

.table th {
font-weight: bold;
}

.table td {
border: 1px solid #e0e0e0;
color: #333;
}

.table-hover tbody tr:hover {
background-color: #e9ecef;
cursor: pointer;
@@ -577,19 +566,14 @@ table tbody tr:hover {
}

td,
th {
color: #5f6368;
}

table[dir="rtl"] td,
table[dir="rtl"] th {
text-align: center;
}

td:last-child {
display: flex;
justify-content: center;
gap: 10px;
align-items: center;
}
</style>

+ 146
- 0
src/views/live-preview/pages/profile/profile.vue View File

@@ -0,0 +1,146 @@
<script>
import accountInfo from "@/components/modals/profile/accountInfo.vue";
import addAddress from "@/components/modals/profile/addAddress.vue";
import moment from "jalali-moment";
import addressList from "@/components/modals/profile/addressList.vue";
import Layout from "@/layout/custom.vue";
import { computed, onMounted, ref } from "vue";
import { useStore } from "vuex";
import ApiServiece from "@/services/ApiService";
export default {
name: "ACCOUNT-PROFILE",
components: {
Layout,
addAddress,
accountInfo,
addressList,
},
setup() {
const addresses = ref([]);
const store = useStore();
const user = computed(() => store.getters["user/getUser"]);
const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
.format("YYYY/MM/DD");
};
const getAddress = () => {
ApiServiece.get(`wholesale/my-addresses`).then((resp) => {
addresses.value = resp.data.data;
console.log(addresses.value);
});
};
onMounted(() => {
getAddress();
});
return {
user,
convertToJalali,
addresses,
};
},
};
</script>

<template>
<Layout>
<BRow>
<BCol class="col-sm-12">
<BCard v-if="addresses.length == 0 " no-body class="alert alert-warning p-0">
<BCardBody>
<div class="d-flex align-items-center">
<div class="flex-grow-1 me-3">
<h4 class="alert-heading">!هشدار</h4>
<p class="mb-2">
شما هنوز هیچ آدرسی را ثبت نکرده اید برای اضافه کردن آدرس لینک
زیر را کلیک کنید
</p>
<a href="#" class="alert-link"><u>اضافه کردن آدرس</u></a>
</div>
<div class="flex-shrink-0">
<img
src="@/assets/images/application/img-accout-password-alert.png"
alt="img"
class="img-fluid wid-80"
/>
</div>
</div>
</BCardBody>
</BCard>
<BRow>
<BCol class="col-lg-5 col-xxl-3">
<BCard no-body class="overflow-hidden">
<BCardBody class="position-relative">
<div class="text-center mt-3">
<div class="chat-avtar d-inline-flex mx-auto">
<img
class="rounded-circle img-fluid wid-90 img-thumbnail"
src="@/assets/images/user/avatar-1.jpg"
alt="User image"
/>
</div>
<h5 class="mb-2">{{ user.name }}</h5>
</div>
</BCardBody>
<div
class="nav flex-column nav-pills list-group list-group-flush account-pills mb-0"
id="user-set-tab"
role="tablist"
aria-orientation="vertical"
>
<a
class="nav-link list-group-item list-group-item-action active"
id="user-set-profile-tab"
data-bs-toggle="pill"
href="#user-set-profile"
role="tab"
aria-controls="user-set-profile"
aria-selected="true"
>
<span class="f-w-500"
><i class="ph-duotone ph-user-circle m-r-10"></i>اطلاعات
حساب
</span>
</a>
<a
class="nav-link list-group-item list-group-item-action"
id="user-set-information-tab"
data-bs-toggle="pill"
href="#user-set-information"
role="tab"
aria-controls="user-set-information"
aria-selected="false"
>
<span class="f-w-500"
><i class="ph-duotone ph-clipboard-text m-r-10"></i> اضافه
کردن آدرس</span
>
</a>
<a
class="nav-link list-group-item list-group-item-action"
id="user-list-address-tab"
data-bs-toggle="pill"
href="#addressList"
role="tab"
aria-controls="addressList"
aria-selected="false"
>
<span class="f-w-500"
><i class="ph-duotone ph-map-pin m-r-10"></i> مشاهده آدرس ها
</span>
</a>
</div>
</BCard>
</BCol>
<BCol class="col-lg-7 col-xxl-9">
<div class="tab-content" id="user-set-tabContent">
<addAddress />
<accountInfo :user="user" />
<addressList :addresses="addresses" />
</div>
</BCol>
</BRow>
</BCol>
</BRow>
</Layout>
</template>

+ 3
- 3
src/views/live-preview/pages/test View File

@@ -29,12 +29,12 @@
<!-- Second Input Field (Slug) -->
<BCol md="6">
<div class="form-group">
<label class="form-label">اسلاگ</label>
<label class="form-label">ککلمه کلیدی</label>
<input
type="text"
v-model="slug"
class="form-control"
placeholder="اسلاگ محصول"
placeholder="کلمه کلیدی محصول"
:class="{ 'is-invalid': errors.slug }"
@input="clearError('slug')"
/>
@@ -586,7 +586,7 @@ export default {
errors.value = {};
if (!title.value) errors.value.title = "وارد کردن عنوان محصول الزامی است";
if (!slug.value)
errors.value.slug = "وارد کردن اسلاگ محصول ضروری می باشد";
errors.value.slug = "وارد کردن کلمه کلیدی محصول ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه محصول ضروری می باشد";
if (!blogCat.value)


+ 9
- 8
src/views/live-preview/pages/users/users.vue View File

@@ -114,7 +114,7 @@ export default {

const blockUser = (id) => {
Swal.fire({
title: "آیا مطمئن هستید؟",
text: "آیا میخواهید این کاربر را مسدود کنید؟",
icon: "warning",
showCancelButton: true,
@@ -147,7 +147,7 @@ export default {

const unBlockUser = (id) => {
Swal.fire({
title: "آیا مطمئن هستید؟",
text: "آیا میخواهید این کاربر را فعال نمایید؟",
icon: "warning",
showCancelButton: true,
@@ -242,15 +242,16 @@ export default {
<li><a class="dropdown-item" href="#">بلاک</a></li>
</ul>
</div> -->
</div>

<button
<button
data-bs-toggle="modal"
data-bs-target="#addUser"
class="btn btn-add-user"
class="btn btn-add-user me-3"
>
<i class="ti ti-plus"></i> افزودن کاربر
افزودن کاربر
</button>
</div>

</div>

<div class="table-responsive">
@@ -262,7 +263,7 @@ export default {
<th>موبایل</th>
<th>نقش</th>
<th>تاریخ ایجاد</th>
<th>فعالیت</th>
<th>وضعیت</th>
</tr>
</thead>
<tbody>


Loading…
Cancel
Save