Kaynağa Gözat

completed admin panel

master
alireza 4 ay önce
ebeveyn
işleme
5409e4e1b3
34 değiştirilmiş dosya ile 7595 ekleme ve 3432 silme
  1. +1
    -1
      .env
  2. +256
    -0
      src/components/AtrributesProduct.vue
  3. +103
    -0
      src/components/ConfigCountriesSelect.vue
  4. +176
    -0
      src/components/ProductInfo.vue
  5. +337
    -0
      src/components/SpecificationsProduct.vue
  6. +61
    -49
      src/components/customSidebar.vue
  7. +198
    -92
      src/components/modals/Brands/addBrand.vue
  8. +323
    -139
      src/components/modals/Brands/editBrand.vue
  9. +36
    -29
      src/components/modals/addUser.vue
  10. +360
    -0
      src/components/modals/attribute-value/addAttributeValue.vue
  11. +444
    -0
      src/components/modals/attribute-value/editAttributeValue.vue
  12. +200
    -95
      src/components/modals/attribute/addAttribute.vue
  13. +311
    -125
      src/components/modals/attribute/editAttribute.vue
  14. +298
    -0
      src/components/modals/attribute/test.vue
  15. +4
    -4
      src/components/modals/blogCat/addBlogCat.vue
  16. +133
    -58
      src/components/modals/blogCat/editBlogCat.vue
  17. +249
    -0
      src/components/modals/countries/CountriesModal.vue
  18. +22
    -1
      src/components/modals/editUser.vue
  19. +18
    -2
      src/router/routes.js
  20. +1214
    -0
      src/utils/code-countries.js
  21. +463
    -0
      src/views/live-preview/pages/attributes-value/attributes-value.vue
  22. +34
    -33
      src/views/live-preview/pages/attributes/attributes.vue
  23. +459
    -355
      src/views/live-preview/pages/banners/addBanner.vue
  24. +3
    -3
      src/views/live-preview/pages/banners/banners.vue
  25. +498
    -371
      src/views/live-preview/pages/banners/editBanner.vue
  26. +10
    -6
      src/views/live-preview/pages/blogCats/blogCat.vue
  27. +9
    -13
      src/views/live-preview/pages/brands/brands.vue
  28. +526
    -0
      src/views/live-preview/pages/countries/countries.vue
  29. +3
    -3
      src/views/live-preview/pages/discounts/addDiscount.vue
  30. +14
    -11
      src/views/live-preview/pages/discounts/editDiscount.vue
  31. +245
    -584
      src/views/live-preview/pages/products/addProduct.vue
  32. +563
    -1399
      src/views/live-preview/pages/products/editProduct.vue
  33. +15
    -54
      src/views/live-preview/pages/products/products.vue
  34. +9
    -5
      src/views/live-preview/pages/users/users.vue

+ 1
- 1
.env Dosyayı Görüntüle

@@ -1 +1 @@
VUE_APP_ROOT_URL = http://192.168.100.126:8000/api/v1/
VUE_APP_ROOT_URL = http://85.208.254.227/api/v1/

+ 256
- 0
src/components/AtrributesProduct.vue Dosyayı Görüntüle

@@ -0,0 +1,256 @@
<script setup>
import { ref, defineProps, defineEmits, defineExpose, watch } from "vue"
import ApiService from "@/services/ApiService"
import { toast } from "vue3-toastify";

const props = defineProps({
categoryId: {
type: String,
required: true,
},
productId: {
type: Number,
required: true,
},
})

const emit = defineEmits(["nextStep"])

const attributes = ref([])

const selectedAttributes = ref([])

const rows = ref([])

const loading = ref(false)

const loadingAttributes = ref(false)

const getAttributes = async () => {
try {
loading.value = true

const { data: { data, success } } = await ApiService.get(`admin/attributes`, {
params: {
category_id: props.categoryId,
with_global: 1
}})

if (success) {
attributes.value = data?.filter(attribute => attribute?.translation)
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loading.value = false
}
}

const addRow = () => {
const newRow = {
attributes: {},
price: '',
stock: '',
}

selectedAttributes.value.forEach(attrId => {
const attr = getAttributeById(attrId)
if (attr && attr.attribute_values.length > 0) {
newRow.attributes[attrId] = attr.attribute_values[0].id
} else {
newRow.attributes[attrId] = ''
}
})
rows.value.push(newRow)
}

const removeRow = async (index) => {
rows.value.splice(index, 1)
// if (row.id) {
// try {
// const { data: { message, success } } = await ApiService.delete(`admin/products/${props.productId}/variants/${row.id}`)
//
// if (success) {
// toast.success(message)
//
// rows.value.splice(index, 1)
// }
// } catch (e) {
// toast.error(e?.response?.data?.message);
// }
// } else {
//
// }
}

const getAttributeById = (id) => {
return attributes.value.find(attr => attr.id === id)
}

watch(selectedAttributes, (newVal) => {
rows.value.forEach(row => {
newVal.forEach(attrId => {
if (!row.attributes[attrId]) {
const attr = getAttributeById(attrId)
if (attr && attr.attribute_values.length > 0) {
row.attributes[attrId] = attr.attribute_values[0].id
}
}
})
Object.keys(row.attributes).forEach(attrId => {
if (!newVal.includes(Number(attrId))) {
delete row.attributes[attrId]
}

if (selectedAttributes.value?.length === 0)
rows.value = []
})
})
})

const submitAttributes = async () => {
try {
loadingAttributes.value = true

await ApiService.post(`admin/products/${props.productId}/attributes`, {
attributes: selectedAttributes.value,
})

const { data: { success, message } } = await ApiService.post(`admin/products/${props.productId}/variants`, {
productVariants: rows.value?.map(variant => ({
price: variant?.price,
inventory: variant?.stock,
attributeValues: Object.values(variant?.attributes)
})),
})

if (success) {
toast.success(message)
emit('nextStep')
}
} catch (e) {
toast.error(e.response?.data?.message)
} finally {
loadingAttributes.value = false
}
}

const handlerChangeCheckbox = async (e, attr) => {
if (!e.target.checked) {
try {
const { data: { success, message } } = await ApiService.delete(`admin/products/${props.productId}/attributes/${attr.id}`)

if (success) {
toast.success(message)
}
} catch (e) {
toast.error(e.response?.data?.message)
}
}
}

getAttributes()

defineExpose({ attributes, rows, selectedAttributes })
</script>

<template>
<span v-if="loading" class="d-flex justify-content-center w-100">
<i class="fa fa-spinner fa-spin text-2xl"></i>
</span>

<div v-else class="pt-8">
<div>
<div v-for="attr in attributes" :key="attr.id" class="mb-2 form-check">
<input
type="checkbox"
:id="'attr-' + attr?.id"
:value="attr?.id"
v-model="selectedAttributes"
@change="handlerChangeCheckbox($event, attr)"
class="form-check-input"
/>
<label :for="'attr-' + attr.id" class="form-check-label">{{ attr?.translation?.title }}</label>
</div>

<template v-if="selectedAttributes?.length">
<div v-for="(row, i) in rows" :key="row.id" class="border p-3 rounded mt-4 d-flex items-center gap-3">
<div v-for="attrId in selectedAttributes" :key="attrId">
<select
v-model="row.attributes[attrId]"
:id="'attr-' + attrId"
class="border rounded px-2 py-1 form-select"
:style="{ minWidth: '200px' }"
>
<option
v-for="val in getAttributeById(attrId)?.attribute_values?.filter(attribute => attribute?.translation)"
:key="val.id"
:value="val.id"
>
{{ val.translation?.title }}
</option>
</select>
</div>

<!-- Extra fields -->
<input
v-model="row.price"
type="text"
placeholder="قیمت"
class="border rounded px-2 py-1 w-24"
/>
<input
v-model="row.stock"
type="text"
placeholder="تعداد موجود"
class="border rounded px-2 py-1 w-24"
/>

<!-- Delete button -->
<button
@click="removeRow(i)"
class="btn btn-danger btn-outline rounded btn-sm"
>
<i class="ti ti-trash me-1 text-xl"></i>
</button>
</div>

<button
v-if="selectedAttributes?.length"
@click="addRow"
class="mt-4 btn btn-primary text-white px-3 py-2 rounded w-100"
>
<i class="ti ti-plus"></i>
افزودن سطر جدید
</button>

<hr class="my-4" />

<div class="d-flex justify-content-between align-items-center">
<button
v-if="selectedAttributes?.length"
:disabled="loadingAttributes"
@click="submitAttributes"
class="mt-4 btn btn-primary text-white px-3 py-2 rounded"
>
<span v-if="loadingAttributes">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
</span>
<template v-else>
ذخیره ویژگی ها
</template>
</button>

<div>
<slot />
</div>
</div>

</template>
</div>
</div>
</template>

<style scoped>

</style>

+ 103
- 0
src/components/ConfigCountriesSelect.vue Dosyayı Görüntüle

@@ -0,0 +1,103 @@
<template>
<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label" for="country-code-select">انتخاب پیش شماره</label>
<select
id="country-code-select"
class="form-select"
v-model="country_code"
:disabled="loading"
aria-describedby="country-code-help"
>
<option value="" disabled>لطفاً یک کشور انتخاب کنید</option>
<option
v-for="code in countries"
:value="code?.id"
:key="code?.id"
>
{{ code?.title }}
</option>
</select>
<small id="country-code-help" class="form-text text-muted">
کد کشور را برای شماره تلفن انتخاب کنید.
</small>
<div v-if="loading" class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">در حال بارگذاری...</span>
</div>
</div>
</BCol>
</template>

<script setup>
import { ref, watch, onMounted, defineProps, defineEmits } from 'vue';
import { toast } from 'vue3-toastify';
import ApiService from '@/services/ApiService';

// Define props for v-model
const props = defineProps({
countryCode: {
type: [Number, String], // Allow both Number and String to handle type mismatches
default: '',
},
});

// Define emits for v-model
const emit = defineEmits(['update:country-code']);

// Reactive state
const countries = ref([]);
const country_code = ref(props.countryCode); // Initialize with prop value
const loading = ref(false);

// Fetch country configurations
const getConfigCountries = async () => {
try {
loading.value = true;
const { data: { data, success } } = await ApiService.get('admin/country-configs');

if (success)
countries.value = data;

if (props.countryCode && countries.value.some(code => code.id === props.countryCode)) {
country_code.value = props.countryCode;
} else {
country_code.value = ''; // Reset if invalid
}
} catch (error) {
const message = error?.response?.data?.message || 'خطا در دریافت لیست کشورها';
toast.error(message);
} finally {
loading.value = false;
}
};

// Watch for changes in country_code and emit to parent
watch(country_code, (newValue) => {
emit('update:country-code', Number(newValue)); // Ensure emitted value is a Number
});

// Watch for prop changes to update country_code
watch(() => props.countryCode, (newValue) => {
if (newValue && countries.value.some(code => code.id === newValue)) {
country_code.value = newValue;
} else {
country_code.value = '';
}
});

// Fetch data on component mount
onMounted(getConfigCountries);
</script>

<style scoped>
.form-group {
position: relative;
}

.spinner-border {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
}
</style>

+ 176
- 0
src/components/ProductInfo.vue Dosyayı Görüntüle

@@ -0,0 +1,176 @@
<script setup>
import {ref, reactive, defineProps, defineExpose, defineEmits } from "vue"
import ApiService from "@/services/ApiService";
import { toast } from "vue3-toastify";
import {BRow} from "bootstrap-vue-next";

const defaultFormState = {
title: null,
slug: null,
summary: null,
description: null,
locale: 'fa'
};

const props = defineProps({
productId: {
type: Number,
required: true,
}
})

const emit = defineEmits(["changeLng", "data"]);

const form = reactive({ ...defaultFormState });

const errors = ref({})

const loading = ref(false)

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

const submitForm = async () => {
try {
loading.value = true

const url = form?.id ? `admin/products/${props.productId}/translations/${form?.id}` : `admin/products/${props.productId}/translations`

const { data: { success, message, data } } = await ApiService[form?.id ? 'put' : 'post'](url, form)

if (success) {
Object.assign(form, defaultFormState)

emit('data', data)

toast.success(message)
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loading.value = false
}
}

defineExpose({ form })
</script>

<template>
<div class="p-2">
<BRow class="g-3">
<BCol md="6">
<div class="form-group">
<label class="form-label">عنوان</label>
<input
v-model="form.title"
:class="{ 'is-invalid': errors.title }"
class="form-control"
placeholder="عنوان محصول"
type="text"
@input="clearError('title')"
/>
</div>
<small v-if="errors.title" class="text-danger">
{{ errors.title }}
</small>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="form.locale"
class="form-control"
placeholder="انتخاب کنید"
@change="emit('changeLng', form.locale)"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>

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

<BCol md="6">
<div class="form-group">
<label class="form-label">خلاصه</label>
<textarea
v-model="form.summary"
:class="{ 'is-invalid': errors.summary }"
class="form-control"
placeholder="خلاصه ای از محصول"
@input="clearError('summary')"
/>
</div>
<small v-if="errors.summary" class="text-danger">
{{ errors.summary }}
</small>
</BCol>

<BCol md="6">
<div class="form-group">
<label class="form-label">توضیحات محصول</label>
<textarea
v-model="form.description"
:class="{ 'is-invalid': errors.description }"
class="form-control"
placeholder="توضیحات محصول"
@input="clearError('description')"
/>
</div>
<small v-if="errors.description" class="text-danger">
{{ errors.description }}
</small>
</BCol>
</BRow>

<button
type="submit"
class="btn btn-primary mt-4"
:disabled="loading"
@click.prevent="submitForm"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
<span v-else>ذخیره محصول</span>
</button>
</div>
</template>

<style scoped>

</style>

+ 337
- 0
src/components/SpecificationsProduct.vue Dosyayı Görüntüle

@@ -0,0 +1,337 @@
<script setup>
import { ref, defineProps, defineEmits, defineExpose } from "vue";
import ApiService from "@/services/ApiService";
import { toast } from "vue3-toastify";

const props = defineProps({
categoryId: {
type: String,
required: true,
},
productId: {
type: Number,
required: true,
},
});

const emit = defineEmits(["nextStep","previousStep"])

const loading = ref(false);

const loadingNewAttr = ref(false);

const loadingNewAttrValue = ref(false);

const loadingSaveSpecification = ref(false);

const attributes = ref([]);

const specifications = ref([{ selectedAttributeId: null, selectedValueId: null }]);

const showAddAttribute = ref(false);

const showAddAttributeValue = ref(null);

const newAttributeTitle = ref("");

const newAttributeValueTitle = ref("");

// Fetch attributes
const getAttributes = async () => {
try {
loading.value = true;

const { data: { data, success } } = await ApiService.get(`admin/attributes`, {
params: {
category_id: props.categoryId,
with_global: 1,
},
});

if (success) {
attributes.value = data?.filter(attribute => attribute?.translation);
}
} catch (e) {
toast.error(e?.response?.data?.message);
} finally {
loading.value = false;
}
};

const addSpecification = () => {
specifications.value.push({ selectedAttributeId: null, selectedValueId: null });
};

const getAttributeValues = (attributeId) => {
const attribute = attributes.value.find(attr => attr.id === attributeId);
return attribute ? attribute.attribute_values.filter(val => val.translation) : [];
};

const removeRow = async (index) => {
if (!specifications.value[index].id) {
specifications.value = specifications.value.filter((r,i) => i !== index)
} else {
try {
const { data: { message, success } } = await ApiService.delete(`admin/products/${props.productId}/specifications/${specifications.value[index]?.id}`)

if (success) {
toast.success(message);

specifications.value = specifications.value.filter((r,i) => i !== index)
}
} catch (e) {
toast.error(e?.response?.data?.message);
}
}
}

// Create a new attribute
const createAttribute = async () => {
if (!newAttributeTitle.value.trim()) {
toast.error("Attribute title is required");
return;
}

try {
loadingNewAttr.value = true;
const payload = {
category_id: props.categoryId,
title: newAttributeTitle.value
};
const { data: { data, success, message } } = await ApiService.post(`admin/attributes`, payload);

if (success) {
attributes.value.push(data);
newAttributeTitle.value = "";
showAddAttribute.value = false;
toast.success(message);
}
} catch (e) {
toast.error(e?.response?.data?.message);
console.log(e)
} finally {
loadingNewAttr.value = false;
}
};

// Create a new attribute value
const createAttributeValue = async (specIndex) => {
if (!newAttributeValueTitle.value.trim()) {
toast.error("Attribute value title is required");
return;
}

const spec = specifications.value[specIndex];
if (!spec.selectedAttributeId) {
toast.error("Please select an attribute first");
return;
}

try {
loadingNewAttrValue.value = true;
const payload = {
attribute_id: spec.selectedAttributeId,
title: newAttributeValueTitle.value
};
const { data: { data, success, message } } = await ApiService.post(`admin/attribute-values`, payload);

if (success) {
const attribute = attributes.value.find(attr => attr.id === spec.selectedAttributeId);
if (attribute) {
attribute.attribute_values.push(data);
}
newAttributeValueTitle.value = "";
showAddAttributeValue.value = null;
toast.success(message);
}
} catch (e) {
toast.error(e?.response?.data?.message);
} finally {
loadingNewAttrValue.value = false;
}
};

// Handle attribute selection
const handleAttributeSelect = (spec) => {
if (spec.selectedAttributeId === "add") {
showAddAttribute.value = true;
spec.selectedAttributeId = null; // Reset to prevent "add" being a valid selection
} else {
spec.selectedValueId = null; // Reset value when attribute changes
showAddAttributeValue.value = null; // Hide any open value input
}
};

// Handle attribute value selection
const handleAttributeValueSelect = (spec, index) => {
if (spec.selectedValueId === "add") {
showAddAttributeValue.value = index;
spec.selectedValueId = null; // Reset to prevent "add" being a valid selection
}
};

const submitSpecification = async () => {
try {
loadingSaveSpecification.value = true;

const { data: { message, success } } = await ApiService.post(
`admin/products/${props.productId}/specifications`, {
productSpecifications: specifications.value?.map(item => item.selectedValueId).filter(item => item !== null),
})

if (success) {
emit('nextStep')

toast.success(message)
}
} catch (e) {
toast.error(e?.response?.data?.message);
} finally {
loadingSaveSpecification.value = false;
}
}

// Initialize attributes
getAttributes();

defineExpose({ specifications })
</script>

<template>
<div>
<span v-if="loading" class="d-flex justify-content-center w-100">
<i class="fa fa-spinner fa-spin text-2xl"></i>
</span>

<div v-else class="pt-8">
<!-- Specifications -->
<div v-for="(spec, i) in specifications" :key="i" class="mb-2 d-flex gap-2 align-items-center">
<!-- First dropdown: Attributes -->
<select
v-model="spec.selectedAttributeId"
:id="'attr-' + i"
class="border rounded px-2 py-1 form-select"
:style="{ minWidth: '200px' }"
@change="handleAttributeSelect(spec, i)"
>
<option value="add">افزودن ویژگی</option>
<option
v-for="attr in attributes"
:key="attr.id"
:value="attr.id"
>
{{ attr.translation?.title }}
</option>
</select>

<!-- Second dropdown: Attribute Values -->
<select
v-model="spec.selectedValueId"
:id="'value-' + i"
class="border rounded px-2 py-1 form-select"
:style="{ minWidth: '200px' }"
:disabled="!spec.selectedAttributeId"
@change="handleAttributeValueSelect(spec, i)"
>
<option value="add">افزودن مقدار ویژگی</option>
<option
v-for="value in getAttributeValues(spec.selectedAttributeId)"
:key="value.id"
:value="value.id"
>
{{ value?.translation?.title }}
</option>
</select>

<button
@click="removeRow(i)"
class="btn btn-danger btn-outline rounded btn-sm"
>
<i class="ti ti-trash me-1 text-xl"></i>
</button>
</div>

<div class="d-flex justify-content-between">
<div v-if="showAddAttribute" class="mt-4 d-flex gap-2 align-items-center">
<input
v-model="newAttributeTitle"
type="text"
class="form-control"
placeholder="عنوان ویژگی جدید را وارد کنید"
:style="{ maxWidth: '400px' }"
/>
<button
@click="createAttribute"
class="px-4 py-2 btn btn-success text-white rounded text-nowrap"
:disabled="loadingNewAttr"
>
<span v-if="loadingNewAttr">
<i class="fa fa-spinner fa-spin text-2xl"></i>
</span>
<template v-else>
ذخیره ویژگی
</template>
</button>
<button
@click="showAddAttribute = false; newAttributeTitle = ''"
class="px-4 py-2 btn btn-link rounded"
>
انصراف
</button>
</div>

<div v-if="showAddAttributeValue !== null" class="mt-4 d-flex gap-2 align-items-center align-self-start">
<input
v-model="newAttributeValueTitle"
type="text"
class="form-control"
placeholder="مقدار ویژگی جدید را وارد کنید"
:style="{ maxWidth: '400px' }"
/>
<button
@click="createAttributeValue(showAddAttributeValue)"
class="px-4 py-2 btn btn-success rounded text-nowrap"
:disabled="loadingNewAttrValue"
>
<span v-if="loadingNewAttrValue">
<i class="fa fa-spinner fa-spin text-2xl"></i>
</span>
<template v-else>
ذخیره مقدار ویژگی
</template>
</button>
<button
@click="showAddAttributeValue = null; newAttributeValueTitle = ''"
class="px-4 py-2 btn btn-link rounded"
>
انصراف
</button>
</div>
</div>

<button
@click="addSpecification"
class="mt-4 px-4 py-2 btn btn-primary text-white rounded"
>
<i class="ti ti-plus"></i>
افزودن مشخصات
</button>

<hr class="py-2"/>

<button
:disabled="loadingSaveSpecification"
@click="submitSpecification"
class="px-4 py-2 btn btn-primary text-white rounded"
>
<span v-if="loadingSaveSpecification" class="d-flex justify-content-center w-100">
<i class="fa fa-spinner fa-spin text-2xl"></i>
</span>
<template v-else>
ذخیره مشخصات
</template>
</button>

</div>
</div>
</template>

+ 61
- 49
src/components/customSidebar.vue Dosyayı Görüntüle

@@ -264,6 +264,16 @@ export default {
<span class="pc-mtext">کاربران</span></router-link <span class="pc-mtext">کاربران</span></router-link
> >
</li> </li>

<li class="pc-item" :class="{ active: this.$route.path === '/countries' }">
<router-link to="/countries" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-user-circle"></i>
</span>
<span class="pc-mtext">کشورها</span>
</router-link>
</li>

<li <li
class="pc-item" class="pc-item"
:class="{ :class="{
@@ -280,6 +290,7 @@ export default {
<span class="pc-mtext">بنر ها</span></router-link <span class="pc-mtext">بنر ها</span></router-link
> >
</li> </li>

<li class="pc-item" :class="{ active: this.$route.path === '/brands' }"> <li class="pc-item" :class="{ active: this.$route.path === '/brands' }">
<router-link to="/brands" class="pc-link"> <router-link to="/brands" class="pc-link">
<span class="pc-micon"> <span class="pc-micon">
@@ -300,17 +311,17 @@ export default {
<span class="pc-mtext">ویژگی ها</span></router-link <span class="pc-mtext">ویژگی ها</span></router-link
> >
</li> </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 === '/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 <li
class="pc-item" class="pc-item"
:class="{ :class="{
@@ -327,6 +338,7 @@ export default {
<span class="pc-mtext">بلاگ ها</span></router-link <span class="pc-mtext">بلاگ ها</span></router-link
> >
</li> </li>

<li <li
class="pc-item" class="pc-item"
:class="{ :class="{
@@ -343,6 +355,7 @@ export default {
<span class="pc-mtext">محصولات</span></router-link <span class="pc-mtext">محصولات</span></router-link
> >
</li> </li>

<li <li
class="pc-item" class="pc-item"
:class="{ :class="{
@@ -360,50 +373,47 @@ export default {
> >
</li> </li>


<!-- سفاراشات -->
<li class="pc-item pc-hasmenu">
<BLink
class="pc-link"
data-bs-toggle="collapse"
href="#collapse-orders"
role="button"
aria-expanded="false"
aria-controls="collapse-orders"
>
<span class="pc-micon">
<i class="ph-duotone ph-shopping-cart"></i>
</span>
<span class="pc-mtext">سفارشات</span>
<span class="pc-arrow">
<ChevronDownIcon />
</span>
</BLink>
<div class="collapse" id="collapse-orders">
<ul class="pc-submenu">
<li
class="pc-item"
:class="{
<li
class="pc-item"
:class="{
active: active:
this.$route.path === '/orders' || this.$route.path === '/orders' ||
this.$route.name === 'singleOrder', this.$route.name === 'singleOrder',
}" }"
>
<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>
>
<router-link to="/orders" class="pc-link">
<span class="pc-micon">
<i class="ph-duotone ph-shopping-cart"></i>
</span>
<span class="pc-mtext">سفارشات</span>
</router-link>
</li> </li>


<!-- سفاراشات -->
<!-- <li class="pc-item pc-hasmenu">-->
<!-- <BLink-->
<!-- class="pc-link"-->
<!-- data-bs-toggle="collapse"-->
<!-- href="#collapse-orders"-->
<!-- role="button"-->
<!-- aria-expanded="false"-->
<!-- aria-controls="collapse-orders"-->
<!-- >-->
<!-- <span class="pc-micon">-->
<!-- <i class="ph-duotone ph-shopping-cart"></i>-->
<!-- </span>-->
<!-- <span class="pc-mtext">سفارشات</span>-->
<!-- <span class="pc-arrow">-->
<!-- <ChevronDownIcon />-->
<!-- </span>-->
<!-- </BLink>-->
<!-- <div class="collapse" id="collapse-orders">-->
<!-- <ul class="pc-submenu">-->
<!-- -->
<!-- </ul>-->
<!-- </div>-->
<!-- </li>-->

<!-- دسته ها --> <!-- دسته ها -->
<!-- <li class="pc-item pc-hasmenu"> <!-- <li class="pc-item pc-hasmenu">
<BLink <BLink
@@ -455,6 +465,7 @@ export default {
<span class="pc-mtext">نظرات</span></router-link <span class="pc-mtext">نظرات</span></router-link
> >
</li> </li>

<li <li
class="pc-item" class="pc-item"
:class="{ :class="{
@@ -469,6 +480,7 @@ export default {
<span class="pc-mtext">پرسش و پاسخ</span></router-link <span class="pc-mtext">پرسش و پاسخ</span></router-link
> >
</li> </li>

<li class="pc-item pc-hasmenu"> <li class="pc-item pc-hasmenu">
<BLink <BLink
class="pc-link" class="pc-link"


+ 198
- 92
src/components/modals/Brands/addBrand.vue Dosyayı Görüntüle

@@ -21,105 +21,166 @@
></button> ></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="addBrand">
<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>

<Steppy
v-model:step="step"
:tabs="[
{ title: 'ثبت دسته بندی', isValid: true },
{ title: 'ترجمه ها', isValid: true },
]"
backText="قبلی"
nextText="بعدی"
doneText="ذخیره"
primaryColor1="#04A9F5"
circleSize="45"
>
<template #1>
<!-- Brand Image Upload --> <!-- Brand Image Upload -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">تصویر برند</label>
<div class="form-group">
<label class="form-label">تصویر برند</label>


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


<div v-if="imagePreview" class="mt-2">
<img
<div v-if="imagePreview" class="mt-2">
<img
:src="imagePreview" :src="imagePreview"
alt="Image Preview" alt="Image Preview"
class="img-fluid rounded shadow-sm Image-Preview" class="img-fluid rounded shadow-sm Image-Preview"
/>
<button
/>
<button
type="button" type="button"
@click="removeImage()" @click="removeImage()"
class="delete-btn" class="delete-btn"
>
<i class="fa fa-trash f-16"></i>
</button>
</div>

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

<BRow class="g-3">
<!-- Brand Description -->
<BCol lg="12">
<div class="form-group">
<label class="form-label">توضیحات برند</label>
<textarea
v-model="description"
@input="clearError('description')"
type="text"
class="form-control"
placeholder="توضیحات برند"
:class="{ 'is-invalid': errors.description }"
></textarea>
<small v-if="errors.description" class="text-danger">
{{ errors.description }}
</small>
>
<i class="fa fa-trash f-16"></i>
</button>
</div> </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"

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

<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
> >
بستن
</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<button :disabled="loadingStep" @click="handlerAddBrand" class="btn btn-primary">
<span
v-if="loadingStep"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
ذخیره
</button>
</div>
</template>

<template #2>
<form @submit.prevent="addBrand">
<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>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
@change="handlerChangeLocale"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>

<!-- Brand Description -->
<BCol lg="12">
<div class="form-group">
<label class="form-label">توضیحات برند</label>
<textarea
v-model="description"
@input="clearError('description')"
type="text"
class="form-control"
placeholder="توضیحات برند"
:class="{ 'is-invalid': errors.description }"
></textarea>
<small v-if="errors.description" class="text-danger">
{{ errors.description }}
</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 <span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span> ></span>
ذخیره
</button>
</div>
</form>
ذخیره
</button>
</div>
</form>
</template>
</Steppy>


</div> </div>
</div> </div>
</div> </div>
@@ -131,13 +192,19 @@ import { ref } from "vue";
import { toast } from "vue3-toastify"; import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css"; import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";
import {Steppy} from "vue3-steppy";


export default { export default {
components: {Steppy},
setup(props, { emit }) { setup(props, { emit }) {
const image = ref(null); const image = ref(null);
const imagePreview = ref(null); const imagePreview = ref(null);
const description = ref(); const description = ref();
const title = ref(); const title = ref();
const loadingStep = ref(false);
const step = ref(1);
const brandId = ref(null);
const locale = ref('fa');


const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
@@ -150,8 +217,6 @@ export default {
if (fileInput) { if (fileInput) {
fileInput.value = ""; fileInput.value = "";
} }

console.log(image.value);
}; };
const handleImageChange = (event) => { const handleImageChange = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
@@ -204,14 +269,14 @@ export default {
} }
loading.value = true; loading.value = true;


const formData = new FormData();
formData.append("title", title.value);
formData.append("description", description.value);
formData.append("image", image.value);
console.log(image.value);
ApiServiece.post(`admin/brands`, formData, {
const params = {
title: title.value,
description: description.value,
locale: locale.value,
}

ApiServiece.post(`admin/brands/${brandId.value}/translations`, params, {
headers: { headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`, Authorization: `Bearer ${localStorage.getItem("token")}`,
}, },
}) })
@@ -224,11 +289,9 @@ export default {
}) })
.then(() => { .then(() => {
setTimeout(() => { setTimeout(() => {
document.getElementById("close").click();
emit("brand-updated"); emit("brand-updated");
title.value = ""; title.value = "";
description.value = ""; description.value = "";
image.value = null;
imagePreview.value = null; imagePreview.value = null;
}, 500); }, 500);
}) })
@@ -244,6 +307,44 @@ export default {
}); });
}; };


const handlerAddBrand = async () => {
if (!image.value) {
toast.error('تصویر برند نمی تواند خالی باشد.')

return
}

try {
loadingStep.value = true;

const formData = new FormData();

formData.append("image", image.value);

const { data: { message, success, data } } = await ApiServiece.post(
`admin/brands`,
formData,
{
headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})

if (success) {
toast.success(message)

brandId.value = data?.id

step.value++
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loadingStep.value = false;
}
}

return { return {
errors, errors,
loading, loading,
@@ -255,6 +356,11 @@ export default {
removeImage, removeImage,
title, title,
description, description,
loadingStep,
step,
handlerAddBrand,
brandId,
locale
}; };
}, },
}; };


+ 323
- 139
src/components/modals/Brands/editBrand.vue Dosyayı Görüntüle

@@ -20,99 +20,157 @@
></button> ></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="editBrand">
<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>

<!-- Brand Image Upload -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">تصویر برند</label>
<BTabs>
<BTab title="بارگذاری تصویر برند">
<div class="form-group mt-4">
<label class="form-label">تصویر برند</label>


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


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

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

<BRow class="g-3">
<!-- Brand Description -->
<BCol lg="12">
<div class="form-group">
<label class="form-label">توضیحات برند</label>
<textarea
v-model="localDesc"
@input="clearError('localDesc')"
type="text"
class="form-control"
placeholder="توضیحات برند"
:class="{ 'is-invalid': errors.localDesc }"
></textarea>
<small v-if="errors.localDesc" class="text-danger">
{{ errors.localDesc }}
</small>
/>
</div> </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="closeEdit"

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

<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
> >
بستن
</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>
<button :disabled="loadingImage" @click="editImageBrand" class="btn btn-primary">
<span
v-if="loadingImage"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
ذخیره
</button>
</div>
</BTab>
<BTab title="ترجمه ها">
<form @submit.prevent="editBrand" class="mt-4">
<BButton
:disabled="!findLocaleTranslation"
:loading="loadingDelete"
@click="handlerRemoveTranslation"
class="btn btn-sm rounded btn-danger d-block"
style="margin-right: auto"
>
حذف ترجمه {{ findLocaleTranslation }}
</BButton>

<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>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
@change="handlerChangeLocale"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<BRow class="g-3">
<!-- Brand Description -->
<BCol lg="12">
<div class="form-group">
<label class="form-label">توضیحات برند</label>
<textarea
v-model="localDesc"
@input="clearError('localDesc')"
type="text"
class="form-control"
placeholder="توضیحات برند"
:class="{ 'is-invalid': errors.localDesc }"
></textarea>
<small v-if="errors.localDesc" class="text-danger">
{{ errors.localDesc }}
</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="closeEdit"
>
بستن
</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>
</BTab>
</BTabs>
</div> </div>
</div> </div>
</div> </div>
@@ -120,13 +178,15 @@
</template> </template>


<script> <script>
import { ref, toRef, watch } from "vue";
import {computed, ref, toRef, watch} from "vue";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";


import { toast } from "vue3-toastify"; import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css"; import "vue3-toastify/dist/index.css";
import {BTabs} from "bootstrap-vue-next";


export default { export default {
components: {BTabs},
props: { props: {
id: { id: {
type: String, type: String,
@@ -144,6 +204,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
brandRow: {
type: Object,
required: true,
},
}, },


setup(props, { emit }) { setup(props, { emit }) {
@@ -152,9 +216,50 @@ export default {
const localDesc = toRef(props.description); const localDesc = toRef(props.description);
const localImage = toRef(props.image); const localImage = toRef(props.image);
const image = ref(null); const image = ref(null);
const localId = toRef(props.id);
const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
const loadingImage = ref(false);
const locale = ref('fa');
const loadingDelete = ref(false);

const brandRowModel = computed({
get: () => props.brandRow,
set: (newValue) => emit('update:categoryRow', newValue)
});

watch(
() => props.brandRow,
(newVal) => {
localTitle.value = newVal?.translation?.title

localDesc.value = newVal?.translation?.description

locale.value = newVal?.translation?.locale

imagePreview.value = newVal?.image
}
)

const findLocaleTranslation = computed(() => {
const foundTranslation = brandRowModel.value?.translations?.find(
item => item?.locale === locale.value
);

if (foundTranslation) {
switch (foundTranslation?.locale) {
case "en":
return "انگلیسی";
case "fa":
return "فارسی";
case "ar":
return "عربی";
default:
return null;
}
}

return null;
});


const handleImageChange = (event) => { const handleImageChange = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
@@ -178,27 +283,6 @@ export default {
} }
}; };


watch(
() => props.title,
(newVal) => (localTitle.value = newVal)
);
watch(
() => props.description,
(newVal) => (localDesc.value = newVal)
);
watch(
() => props.image,
(newVal) => {
localImage.value = newVal;
imagePreview.value = newVal;
}
);

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

const validateForm = () => { const validateForm = () => {
errors.value = {}; errors.value = {};
if (!localTitle.value) if (!localTitle.value)
@@ -215,7 +299,7 @@ export default {
errors.value[field] = ""; errors.value[field] = "";
}; };


const editBrand = () => {
const editBrand = async () => {
if (!validateForm()) { if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", { toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right", position: "top-right",
@@ -223,47 +307,140 @@ export default {
}); });
return; return;
} }
loading.value = true;


const formData = new FormData();
formData.append("title", localTitle.value);
formData.append("description", localDesc.value);
try {
loading.value = true;

const params = { title: localTitle.value, description: localDesc.value, locale: locale.value };

const existingTranslation = brandRowModel.value?.translations?.find(
t => t.locale === locale.value
);

const { data } = existingTranslation
? await ApiServiece.put(
`admin/brands/${brandRowModel.value?.id}/translations/${existingTranslation?.id}`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
)
: await ApiServiece.post(
`admin/brands/${brandRowModel.value?.id}/translations`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);

if (data?.success) {
const updatedBrands = brandRowModel.value ? {...brandRowModel.value} : null;

if (updatedBrands) {
updatedBrands.translations = [...(updatedBrands.translations || []), data?.data];

brandRowModel.value = updatedBrands;
}

toast.success(data?.message)


if (image.value) {
formData.append("image", image.value);
}
formData.append("_method", "put");

ApiServiece.post(`admin/brands/${localId.value}`, formData, {
headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then(() => {
toast.success("!برند با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => { setTimeout(() => {
document.getElementById("closeEdit").click();
emit("brand-updated");
}, 500);
})
.catch((error) => {
console.error(error);
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
emit("cat-updated");
}, 500)
}

} catch (error) {
toast.error(`${error?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
}); });
} finally {
loading.value = false;
}
}; };


const editImageBrand = async () => {
try {
loadingImage.value = true;

const formData = new FormData();

formData.append("image", image.value);

formData.append("_method", 'put');

const { data: { message, success } } = await ApiServiece.post(
`admin/brands/${brandRowModel.value?.id}`,
formData,
{
headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})

if (success) {
toast.success(message)
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loadingImage.value = false;
}
}

const handlerRemoveTranslation = async () => {
const findLocale = brandRowModel.value?.translations?.find(item => item?.locale === locale.value);

if (findLocale) {
try {
loadingDelete.value = true;

const { data: { success, message, data } } = await ApiServiece.delete(
`admin/brands/${brandRowModel.value?.id}/translations/${findLocale?.id}`
)

if (success) {
const updatedBrands = brandRowModel.value ? {...brandRowModel.value} : null;

if (updatedBrands) {
updatedBrands.translations = [...(updatedBrands.translations || []), data];

brandRowModel.value = updatedBrands;
}

localTitle.value = null

localDesc.value = null

toast.success(message)
}
} catch (e) {
console.log(e)
}finally {
loadingDelete.value = false
}
}
}

const handlerChangeLocale = (e) => {
const findLocale = brandRowModel.value?.translations?.find(item => item?.locale === e.target.value);

if (findLocale) {
localTitle.value = findLocale?.title

localDesc.value = findLocale?.description
} else {
localTitle.value = undefined

localDesc.value = undefined
}
}

return { return {
errors, errors,
loading, loading,
@@ -274,6 +451,13 @@ export default {
localImage, localImage,
handleImageChange, handleImageChange,
imagePreview, imagePreview,
locale,
loadingImage,
editImageBrand,
findLocaleTranslation,
handlerRemoveTranslation,
loadingDelete,
handlerChangeLocale
}; };
}, },
}; };


+ 36
- 29
src/components/modals/addUser.vue Dosyayı Görüntüle

@@ -103,6 +103,8 @@
}}</small> }}</small>
</div> </div>
</BCol> </BCol>

<ConfigCountriesSelect v-model:country-code="selectedCountryCode"/>
</BRow> </BRow>


<!-- Submit Buttons --> <!-- Submit Buttons -->
@@ -137,20 +139,22 @@


<script> <script>
import { ref } from "vue"; import { ref } from "vue";

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

export default { export default {
components: {ConfigCountriesSelect},
setup(props, { emit }) { setup(props, { emit }) {
const name = ref(); const name = ref();
const mobile = ref(); const mobile = ref();
const password = ref(); const password = ref();
const repeatPassword = ref(); const repeatPassword = ref();
const role = ref(); const role = ref();

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


const validateForm = () => { const validateForm = () => {
errors.value = {}; errors.value = {};
@@ -165,7 +169,7 @@ export default {
errors.value.password = "رمز عبور باید حداقل 8 کاراکتر باشد"; errors.value.password = "رمز عبور باید حداقل 8 کاراکتر باشد";
} else if (repeatPassword.value.length < 8) { } else if (repeatPassword.value.length < 8) {
errors.value.repeatPassword = errors.value.repeatPassword =
"تکرار رمز عبور باید حداقل 8 کاراکتر باشد";
"تکرار رمز عبور باید حداقل 8 کاراکتر باشد";
} }
return Object.keys(errors.value).length === 0; return Object.keys(errors.value).length === 0;
}; };
@@ -187,38 +191,40 @@ export default {
const formData = new FormData(); const formData = new FormData();
formData.append("mobile", mobile.value); formData.append("mobile", mobile.value);
if (name.value) if (name.value)
formData.append("name", name.value);
formData.append("name", name.value);
formData.append("role", role.value); formData.append("role", role.value);
formData.append("password", password.value); formData.append("password", password.value);
formData.append("password_confirmation", repeatPassword.value); formData.append("password_confirmation", repeatPassword.value);
formData.append("country_config_id", selectedCountryCode.value);


ApiServiece.post(`admin/users`, formData) ApiServiece.post(`admin/users`, formData)
.then(() => {
toast.success("!کاربر با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
mobile.value = "";
name.value = "";
role.value = "";
password.value = "";
repeatPassword.value = "";
})
.then(() => {
setTimeout(() => {
document.getElementById("addClose").click();
emit("user-updated");
}, 500);
})
.catch((error) => {
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
.then(() => {
toast.success("!کاربر با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
mobile.value = "";
name.value = "";
role.value = "";
password.value = "";
repeatPassword.value = "";
selectedCountryCode.value = "";
})
.then(() => {
setTimeout(() => {
document.getElementById("addClose").click();
emit("user-updated");
}, 500);
})
.catch((error) => {
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
}); });
})
.finally(() => {
loading.value = false;
});
}; };


return { return {
@@ -231,6 +237,7 @@ export default {
password, password,
repeatPassword, repeatPassword,
role, role,
selectedCountryCode,
}; };
}, },
}; };


+ 360
- 0
src/components/modals/attribute-value/addAttributeValue.vue Dosyayı Görüntüle

@@ -0,0 +1,360 @@
<script setup>
import { ref } from "vue"
import {Steppy} from "vue3-steppy";
import ApiServiece from "@/services/ApiService";
import {toast} from "vue3-toastify";
import {BRow} from "bootstrap-vue-next";
import { useRoute } from "vue-router";

// eslint-disable-next-line no-undef
const emit = defineEmits(['attribute-updated']);

const route = useRoute();

const attributes = ref([])

const step = ref(1)

const attrId = ref(null)

const errors = ref({})

const colorCode = ref();

const attrName = ref(null);

const loading = ref(false);

const loadingAttr = ref(false);

const locale = ref('fa');

const attrModel = ref(null)

const getAttributes = async () => {
try {
const { data: { success, data } } = await ApiServiece.get(`admin/attributes`)

if (success) {
attributes.value = data;
}
} catch (e) {
toast.error(e?.response?.data?.message)
}
}

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

const handlerSubmitAttr = async () => {
try {
loading.value = true;

const { data: { success, message, data } } = await ApiServiece.post(`admin/attribute-values`,{
code: colorCode.value,
attribute_id: route?.params?.id,
})

if (success) {
toast.success(message)

attrId.value = data?.id

step.value++
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loading.value = false;
}
}

const addAttribute = async () => {
try {
loadingAttr.value = true;

const params = {
title: attrName.value,
locale: locale.value,
}

const { data: { success, message, data } } = await ApiServiece.post(
`admin/attribute-values/${attrId.value}/translations`,
params
)

if (success) {
toast.success(message)
//document.getElementById("close").click();
emit("attribute-updated");

attrModel.value = data

attrName.value = undefined
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loadingAttr.value = false;
}
}

getAttributes()
</script>

<template>
<div
class="modal fade"
id="addAttributeValue"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<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">
<Steppy
v-model:step="step"
:tabs="[
{ title: 'انتخاب ویژگی و رنگ', isValid: true },
{ title: 'ترجمه ها', isValid: true },
]"
backText="قبلی"
nextText="بعدی"
doneText="ذخیره"
primaryColor1="#04A9F5"
circleSize="45"
>
<template #1>
<BRow>
<!-- <BCol lg="6">-->
<!-- <div class="form-group">-->
<!-- <label class="form-label">ویژگی ها</label>-->
<!-- <select-->
<!-- v-model="attrId"-->
<!-- class="form-control"-->
<!-- placeholder="دسته بندی انتخاب کنید"-->
<!-- >-->
<!-- <option-->
<!-- v-for="attribute in attributes"-->
<!-- :key="attribute?.id"-->
<!-- :value="attribute?.id"-->
<!-- >-->
<!-- {{ attribute?.translation?.title ?? 'بدون نام' }}-->
<!-- </option>-->
<!-- </select>-->
<!-- </div>-->
<!-- </BCol>-->

<BCol>
<div class="form-group">
<label class="form-label">انتخاب رنگ</label>
<div class="color-picker-wrapper">
<input
v-model="colorCode"
@input="clearError('colorCode')"
type="color"
class="form-control color-picker"
:class="{ 'is-invalid': errors.colorCode }"
/>
<span
v-if="colorCode"
class="color-display"
:style="{ backgroundColor: colorCode }"
></span>
</div>
<span> {{ colorCode }}</span>

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

<!-- Submit Buttons -->
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>

<button @click="handlerSubmitAttr" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
ذخیره
</button>
</div>
</template>

<template #2>
<form @submit.prevent="addAttribute">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">مقدار ویژگی</label>

<input
v-model="attrName"
@input="clearError('attrName')"
type="text"
class="form-control"
placeholder="عنوان ویژگی"
:class="{ 'is-invalid': errors.attrName }"
/>
<small v-if="errors.attrName" class="text-danger">
{{ errors.attrName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>

<button type="submit" class="btn btn-primary" :disabled="loadingAttr">
<span
v-if="loadingAttr"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
ذخیره
</button>
</div>
</form>
</template>
</Steppy>
</div>
</div>
</div>
</div>
</template>

<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;
}

.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>

+ 444
- 0
src/components/modals/attribute-value/editAttributeValue.vue Dosyayı Görüntüle

@@ -0,0 +1,444 @@
<script setup>
import { computed, ref, watch, defineProps, defineEmits } from "vue"
import { BRow } from "bootstrap-vue-next";
import {toast} from "vue3-toastify";
import ApiServiece from "@/services/ApiService";

const colorCode = ref(null)
const errors = ref({})
const loading = ref(false)
const attrName = ref(null)
const locale = ref('fa')
const loadingDelete = ref(false)

const props = defineProps({
attrRow: {
type: Object,
required: true
}
})

const emit = defineEmits(['update:categoryRow'])

const attrRowModel = computed({
get: () => props.attrRow,
set: (newValue) => emit('update:categoryRow', newValue)
});

watch(
() => props.attrRow,
(newVal) => {
colorCode.value = newVal?.code

attrName.value = newVal?.translation?.title

locale.value = newVal?.translation?.locale
}
)

const findLocaleTranslation = computed(() => {
const foundTranslation = attrRowModel.value?.translations?.find(
item => item?.locale === locale.value
);

if (foundTranslation) {
switch (foundTranslation?.locale) {
case "en":
return "انگلیسی";
case "fa":
return "فارسی";
case "ar":
return "عربی";
default:
return null;
}
}

return null;
});

const handlerChangeLocale = (e) => {
const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === e.target.value);

if (findLocale) {
attrName.value = findLocale?.title
} else {
attrName.value = undefined
}
}

const editAttribute = async () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
autoClose: 1000,
});
return;
}

try {
loading.value = true;

const params = { title: attrName.value, locale: locale.value };

const existingTranslation = attrRowModel.value?.translations?.find(
t => t.locale === locale.value
);

const { data } = existingTranslation
? await ApiServiece.put(
`admin/attribute-values/${attrRowModel.value?.id}/translations/${existingTranslation?.id}`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
)
: await ApiServiece.post(
`admin/attribute-values/${attrRowModel.value?.id}/translations`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);

if (data?.success) {
const updatedCategory = props.attrRow;

if (existingTranslation) {
updatedCategory.translations = data?.data?.translations || [];
} else {
updatedCategory.translations = [
...(updatedCategory.translations || []),
...(data?.data?.translations || [])
];
}

attrRowModel.value = updatedCategory;

toast.success(data?.message)

emit("cat-updated");
}

} catch (error) {
toast.error(`${error?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
});
} finally {
loading.value = false;
}
};

const validateForm = () => {
errors.value = {};
if (!attrName.value)
errors.value.colorName = "وارد کردن مقدار ویژگی ضروری می باشد";
if (!locale.value)
errors.value.colorCode = "انتخاب زبان ضروری می باشد";

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

const handlerRemoveTranslation = async () => {
const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === locale.value);

if (findLocale) {
try {
loadingDelete.value = true;

const { data: { success, message, data } } = await ApiServiece.delete(
`admin/attribute-values/${attrRowModel.value?.id}/translations/${findLocale?.id}`
)

if (success) {
const updatedCategory = props.attrRow;

if (findLocale) {
updatedCategory.translations = data?.data?.translations || [];
} else {
updatedCategory.translations = [
...(updatedCategory.translations || []),
...(data?.data?.translations || [])
];
}

attrRowModel.value = updatedCategory;

attrName.value = null

toast.success(message)
}
} catch (e) {
toast.error(e?.response?.data?.message)
}finally {
loadingDelete.value = false
}
}
}

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

// const handlerSubmitColor = async () => {
// try {
// loading.value = true
//
// const { data: { message, success } } = await ApiService.put(
// `admin/attribute-values/${attrRowModel.value?.id}`, {
// code: colorCode.value,
// })
//
// if (success) {
// toast.success(message)
// }
// } catch (e) {
// toast.error(e?.response?.data?.message)
// } finally {
// loading.value = false
// }
// }
</script>

<template>
<div
class="modal fade"
id="editAttributeValue"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<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="editAttribute" class="mt-5">

<BButton
:disabled="!findLocaleTranslation"
:loading="loadingDelete"
@click="handlerRemoveTranslation"
class="btn btn-sm rounded btn-danger d-block"
style="margin-right: auto"
>
حذف ترجمه {{ findLocaleTranslation }}
</BButton>

<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">مقدار ویژگی</label>

<input
v-model="attrName"
@input="clearError('attrName')"
type="text"
class="form-control"
placeholder="عنوان ویژگی"
:class="{ 'is-invalid': errors.attrName }"
/>
<small v-if="errors.attrName" class="text-danger">
{{ errors.attrName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
@change="handlerChangeLocale"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<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"
/>
ذخیره
</button>
</div>
</form>
<!-- <BTabs>-->
<!-- <BTab title="انتخاب رنگ">-->
<!-- <BRow>-->
<!-- <BCol class="mt-5">-->
<!-- <div class="form-group">-->
<!-- <label class="form-label">انتخاب رنگ</label>-->
<!-- <div class="color-picker-wrapper">-->
<!-- <input-->
<!-- v-model="colorCode"-->
<!-- @input="clearError('colorCode')"-->
<!-- type="color"-->
<!-- class="form-control color-picker"-->
<!-- :class="{ 'is-invalid': errors.colorCode }"-->
<!-- />-->
<!-- <span-->
<!-- v-if="colorCode"-->
<!-- class="color-display"-->
<!-- :style="{ backgroundColor: colorCode }"-->
<!-- ></span>-->
<!-- </div>-->
<!-- <span>{{ colorCode }}</span>-->

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

<!-- &lt;!&ndash; Submit Buttons &ndash;&gt;-->
<!-- <div-->
<!-- class="d-flex justify-content-end gap-2"-->
<!-- style="margin-top: 20px"-->
<!-- >-->
<!-- <button-->
<!-- class="btn btn-secondary"-->
<!-- data-bs-dismiss="modal"-->
<!-- id="close"-->
<!-- >-->
<!-- بستن-->
<!-- </button>-->

<!-- <button class="btn btn-primary" :disabled="loading" @click="handlerSubmitColor">-->
<!-- <span-->
<!-- v-if="loading"-->
<!-- class="spinner-border spinner-border-sm"-->
<!-- role="status"-->
<!-- aria-hidden="true"-->
<!-- />-->
<!-- ذخیره-->
<!-- </button>-->
<!-- </div>-->
<!-- </BTab>-->
<!-- <BTab title="ترجمه ها">-->
<!-- -->
<!-- </BTab>-->
<!-- </BTabs>-->
</div>
</div>
</div>
</div>
</template>

<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;
}

.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>

+ 200
- 95
src/components/modals/attribute/addAttribute.vue Dosyayı Görüntüle

@@ -11,7 +11,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel"> <h5 class="modal-title" id="exampleModalLabel">
اضافه کردن رنگ جدید
اضافه کردن ویژگی جدید
</h5> </h5>
<button <button
type="button" type="button"
@@ -21,77 +21,139 @@
></button> ></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="addAttribute">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">نام رنگ</label>

<input
v-model="colorName"
@input="clearError('colorName')"
type="text"
<Steppy
v-model:step="step"
:tabs="[
{ title: 'ثبت دسته بندی', isValid: true },
{ title: 'ترجمه ها', isValid: true },
]"
backText="قبلی"
nextText="بعدی"
doneText="ذخیره"
primaryColor1="#04A9F5"
circleSize="45"
>
<template #1>
<div class="form-group">
<label class="form-label">دسته بندی</label>
<select
v-model="categoryId"
class="form-control" class="form-control"
placeholder="نام رنگ"
:class="{ 'is-invalid': errors.colorName }"
placeholder="دسته بندی انتخاب کنید"
>
<option
v-for="category in categories"
:key="category?.id"
:value="category?.id"
>
{{ category?.translation?.title ?? 'بدون نام' }}
</option>
</select>
</div>

<!-- Submit Buttons -->
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>

<button @click="handlerSubmitCategory" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/> />
<small v-if="errors.colorName" class="text-danger">
{{ errors.colorName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب رنگ</label>
<div class="color-picker-wrapper">
<input
v-model="colorCode"
@input="clearError('colorCode')"
type="color"
class="form-control color-picker"
:class="{ 'is-invalid': errors.colorCode }"
/>
<span
v-if="colorCode"
class="color-display"
:style="{ backgroundColor: colorCode }"
></span>
</div>
<span> {{ colorCode }}</span>

<small v-if="errors.colorCode" class="text-danger">
{{ errors.colorCode }}
</small>
ذخیره
</button>
</div>
</template>

<template #2>
<form @submit.prevent="addAttribute">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">عنوان ویژگی</label>

<input
v-model="attrName"
@input="clearError('attrName')"
type="text"
class="form-control"
placeholder="عنوان ویژگی"
:class="{ 'is-invalid': errors.attrName }"
/>
<small v-if="errors.attrName" class="text-danger">
{{ errors.attrName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>

<button type="submit" class="btn btn-primary" :disabled="loadingAttr">
<span
v-if="loadingAttr"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
ذخیره
</button>
</div> </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>
</form>
</template>
</Steppy>
</div> </div>
</div> </div>
</div> </div>
@@ -103,8 +165,10 @@ import { ref, toRef, watch } from "vue";
import { toast } from "vue3-toastify"; import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css"; import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";
import {Steppy} from "vue3-steppy";


export default { export default {
components: {Steppy},
props: { props: {
attributeValues: { attributeValues: {
type: Array, type: Array,
@@ -112,11 +176,17 @@ export default {
}, },
}, },
setup(props, { emit }) { setup(props, { emit }) {
const colorName = ref();
const attrName = ref();
const colorCode = ref(); const colorCode = ref();
const localAttributeValues = toRef(props.attributeValues); const localAttributeValues = toRef(props.attributeValues);
const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
const loadingAttr = ref(false);
const categories = ref([]);
const categoryId = ref(null);
const categoryResponse = ref(null);
const step = ref(1);
const locale = ref('fa');


watch( watch(
() => props.attributeValues, () => props.attributeValues,
@@ -125,9 +195,9 @@ export default {


const validateForm = () => { const validateForm = () => {
errors.value = {}; errors.value = {};
if (!colorName.value)
errors.value.colorName = "وارد کردن نام رنگ ضروری می باشد";
if (!colorCode.value) errors.value.colorCode = "انتخاب رنگ ضروری می باشد";
// if (!attrName.value)
// errors.value.attrName = "وارد کردن نام ویژگی ضروری می باشد";
if (!categoryId.value) errors.value.categoryId = "انتخاب دسته بندی ضروری می باشد";


return Object.keys(errors.value).length === 0; return Object.keys(errors.value).length === 0;
}; };
@@ -136,7 +206,7 @@ export default {
errors.value[field] = ""; errors.value[field] = "";
}; };


const addAttribute = () => {
const handlerSubmitCategory = () => {
if (!validateForm()) { if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", { toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right", position: "top-right",
@@ -144,30 +214,21 @@ export default {
}); });
return; return;
} }

loading.value = true; loading.value = true;


const formData = new FormData();
console.log(localAttributeValues.value);
formData.append("title", colorName.value);
formData.append("code", colorCode.value);
console.log(localAttributeValues);
formData.append("attribute_id", 1);

ApiServiece.post(`admin/attribute-values`, formData)
.then(() => {
toast.success("!ویژگی با موفقیت اضافه شد", {
const params = {
category_id: categoryId.value,
}

ApiServiece.post(`admin/attributes`, params)
.then(({ data }) => {
toast.success(data?.message, {
position: "top-right", position: "top-right",
autoClose: 1000, autoClose: 1000,
}); });
})
.then(() => {
setTimeout(() => {
document.getElementById("close").click();
emit("attribute-updated");
colorName.value = "";
colorCode.value = "";
}, 500);
categoryResponse.value = data?.data?.id;
step.value++
}) })
.catch((error) => { .catch((error) => {
toast.error(`${error.response.data.message}`, { toast.error(`${error.response.data.message}`, {
@@ -180,13 +241,57 @@ export default {
}); });
}; };


const addAttribute = async () => {
try {
loadingAttr.value = true;

const params = {
title: attrName.value,
locale: locale.value,
}

const { data: { success, message } } = await ApiServiece.post(`admin/attributes/${categoryResponse.value}/translations`, params)

if (success) {
toast.success(message)
//document.getElementById("close").click();
emit("attribute-updated");
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loadingAttr.value = false;
}
}

const getAttributes = async () => {
try {
const { data: { success, data } } = await ApiServiece.get(`admin/categories`)

if (success) {
categories.value = data;
}
} catch (e) {
toast.error(e?.response?.data?.message)
}
}

getAttributes()

return { return {
errors, errors,
loading, loading,
clearError, clearError,
addAttribute,
colorName,
handlerSubmitCategory,
attrName,
colorCode, colorCode,
getAttributes,
categories,
loadingAttr,
categoryId,
step,
locale,
addAttribute
}; };
}, },
}; };


+ 311
- 125
src/components/modals/attribute/editAttribute.vue Dosyayı Görüntüle

@@ -19,77 +19,140 @@
></button> ></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="editAttribute">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">نام رنگ</label>

<input
v-model="localColorName"
@input="clearError('localColorName')"
type="text"
<BTabs>
<BTab title="دسته بندی">
<div class="form-group mt-2">
<label class="form-label">دسته بندی</label>
<select
v-model="categoryId"
class="form-control" class="form-control"
placeholder="نام رنگ"
:class="{ 'is-invalid': errors.colorName }"
placeholder="دسته بندی انتخاب کنید"
>
<option
v-for="category in categories"
:key="category?.id"
:value="category?.id"
>
{{ category?.translation?.title ?? 'بدون نام' }}
</option>
</select>
</div>

<!-- Submit Buttons -->
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<button
class="btn btn-secondary"
data-bs-dismiss="modal"
id="close"
>
بستن
</button>

<button @click="handlerSubmitCategory" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/> />
<small v-if="errors.localColorName" class="text-danger">
{{ errors.localColorName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب رنگ</label>
<div class="color-picker-wrapper">
<input
v-model="localColorCode"
@input="clearError('localColorCode')"
type="color"
class="form-control color-picker"
:class="{ 'is-invalid': errors.localColorCode }"
/>
<span
v-if="colorCode"
class="color-display"
:style="{ backgroundColor: localColorCode }"
></span>
</div>
<span> {{ localColorCode }}</span>

<small v-if="errors.localColorCode" class="text-danger">
{{ errors.localColorCode }}
</small>
ذخیره
</button>
</div>

</BTab>

<BTab title="ترجمه ها">
<form @submit.prevent="editAttribute" class="mt-4">

<BButton
:disabled="!findLocaleTranslation"
:loading="loadingDelete"
@click="handlerRemoveTranslation"
class="btn btn-sm rounded btn-danger d-block"
style="margin-right: auto"
>
حذف ترجمه {{ findLocaleTranslation }}
</BButton>

<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">عنوان ویژگی</label>

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

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
@change="handlerChangeLocale"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>
<div
class="d-flex justify-content-end gap-2"
style="margin-top: 20px"
>
<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"
/>
ذخیره
</button>
</div> </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="closeAttributeModal"
>
بستن
</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>
</form>
</BTab>
</BTabs>
</div> </div>
</div> </div>
</div> </div>
@@ -97,13 +160,15 @@
</template> </template>


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


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


export default { export default {
components: {BTabs},
props: { props: {
attributeValues: { attributeValues: {
type: Array, type: Array,
@@ -121,41 +186,64 @@ export default {
type: String, type: String,
Required: true, Required: true,
}, },
attrRow: {
type: Object,
required: true,
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const localColorName = ref();
const localTitle = ref();
const localColorCode = ref(); const localColorCode = ref();
const localId = toRef(props.id);
const localAttributeValues = toRef(props.attributeValues);
const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
const categoryId = ref(null);
const categories = ref([]);
const locale = ref('fa');
const loadingDelete = ref(false);


watch(
() => props.attributeValues,
(newVal) => (localAttributeValues.value = newVal)
);

watch(
() => props.code,
(newVal) => (localColorCode.value = newVal)
);
const attrRowModel = computed({
get: () => props.attrRow,
set: (newValue) => emit('update:categoryRow', newValue)
});


watch( watch(
() => props.title,
(newVal) => (localColorName.value = newVal)
);
() => props.attrRow,
(newVal) => {
localTitle.value = newVal?.translation?.title

locale.value = newVal?.translation?.locale

categoryId.value = newVal?.category?.id
}
)

const findLocaleTranslation = computed(() => {
const foundTranslation = attrRowModel.value?.translations?.find(
item => item?.locale === locale.value
);

if (foundTranslation) {
switch (foundTranslation?.locale) {
case "en":
return "انگلیسی";
case "fa":
return "فارسی";
case "ar":
return "عربی";
default:
return null;
}
}


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


const validateForm = () => { const validateForm = () => {
errors.value = {}; errors.value = {};
if (!localColorName.value)
errors.value.colorName = "وارد کردن نام رنگ ضروری می باشد";
if (!localColorCode.value)
errors.value.colorCode = "انتخاب رنگ ضروری می باشد";
if (!localTitle.value)
errors.value.colorName = "وارد کردن عنوان ویژگی ضروری می باشد";
if (!locale.value)
errors.value.colorCode = "انتخاب زبان ضروری می باشد";


return Object.keys(errors.value).length === 0; return Object.keys(errors.value).length === 0;
}; };
@@ -164,7 +252,7 @@ export default {
errors.value[field] = ""; errors.value[field] = "";
}; };


const editAttribute = () => {
const editAttribute = async () => {
if (!validateForm()) { if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", { toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right", position: "top-right",
@@ -172,47 +260,145 @@ export default {
}); });
return; return;
} }
loading.value = true;

const formData = new FormData();
console.log(localAttributeValues.value);
formData.append("title", localColorName.value);
formData.append("code", localColorCode.value);
formData.append("attribute_id", localAttributeValues.value[0].id);

ApiServiece.put(`admin/attribute-values/${localId.value}`, formData)
.then((resp) => {
console.log(resp);
toast.success("!ویژگی با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
document.getElementById("closeAttributeModal").click();
emit("attribute-updated");
}, 500);
})
.catch((error) => {
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;

try {
loading.value = true;

const params = { title: localTitle.value, locale: locale.value };

const existingTranslation = attrRowModel.value?.translations?.find(
t => t.locale === locale.value
);

const { data } = existingTranslation
? await ApiServiece.put(
`admin/attributes/${attrRowModel.value?.id}/translations/${existingTranslation?.id}`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
)
: await ApiServiece.post(
`admin/attributes/${attrRowModel.value?.id}/translations`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);

if (data?.success) {
const updatedCategory = props.attrRow;

if (existingTranslation) {
updatedCategory.translations = data?.data?.translations || [];
} else {
updatedCategory.translations = [
...(updatedCategory.translations || []),
...(data?.data?.translations || [])
];
}

attrRowModel.value = updatedCategory;

console.log(attrRowModel.value,'attrRowModel.value')

toast.success(data?.message)

emit("cat-updated");
}

} catch (error) {
console.log(error,'error error');
toast.error(`${error?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
}); });
} finally {
loading.value = false;
}
}; };


const handlerRemoveTranslation = async () => {
const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === locale.value);

if (findLocale) {
try {
loadingDelete.value = true;

const { data: { success, message, data } } = await ApiServiece.delete(
`admin/attributes/${attrRowModel.value?.id}/translations/${findLocale?.id}`
)

if (success) {
const updatedCategory = props.attrRow;

if (findLocale) {
updatedCategory.translations = data?.data?.translations || [];
} else {
updatedCategory.translations = [
...(updatedCategory.translations || []),
...(data?.data?.translations || [])
];
}

attrRowModel.value = updatedCategory;

localTitle.value = null

toast.success(message)
}
} catch (e) {
toast.error(e?.response?.data?.message)
}finally {
loadingDelete.value = false
}
}
}

const getAttributes = async () => {
try {
const { data: { success, data } } = await ApiServiece.get(`admin/categories`)

if (success) {
categories.value = data;
}
} catch (e) {
toast.error(e?.response?.data?.message)
}
}

const handlerChangeLocale = (e) => {
const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === e.target.value);

if (findLocale) {
localTitle.value = findLocale?.title
} else {
localTitle.value = undefined
}
}

getAttributes()

return { return {
errors, errors,
loading, loading,
clearError, clearError,
editAttribute, editAttribute,
localColorName,
localTitle,
localColorCode, localColorCode,
categoryId,
getAttributes,
categories,
attrRowModel,
handlerChangeLocale,
locale,
findLocaleTranslation,
handlerRemoveTranslation,
loadingDelete,
}; };
}, },
}; };


+ 298
- 0
src/components/modals/attribute/test.vue Dosyayı Görüntüle

@@ -0,0 +1,298 @@
<template>
<div
class="modal fade"
id="addAttribute"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<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="addAttribute">
<BRow class="g-3">
<!-- Brand Title -->
<BCol lg="6">
<div class="form-group">
<label class="form-label">نام رنگ</label>

<input
v-model="colorName"
@input="clearError('colorName')"
type="text"
class="form-control"
placeholder="نام رنگ"
:class="{ 'is-invalid': errors.colorName }"
/>
<small v-if="errors.colorName" class="text-danger">
{{ errors.colorName }}
</small>
</div>
</BCol>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب رنگ</label>
<div class="color-picker-wrapper">
<input
v-model="colorCode"
@input="clearError('colorCode')"
type="color"
class="form-control color-picker"
:class="{ 'is-invalid': errors.colorCode }"
/>
<span
v-if="colorCode"
class="color-display"
:style="{ backgroundColor: colorCode }"
></span>
</div>
<span> {{ colorCode }}</span>

<small v-if="errors.colorCode" class="text-danger">
{{ errors.colorCode }}
</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: {
attributeValues: {
type: Array,
Required: true,
},
},
setup(props, { emit }) {
const colorName = ref();
const colorCode = ref();
const localAttributeValues = toRef(props.attributeValues);
const errors = ref({});
const loading = ref(false);

watch(
() => props.attributeValues,
(newVal) => (localAttributeValues.value = newVal)
);

const validateForm = () => {
errors.value = {};
if (!colorName.value)
errors.value.colorName = "وارد کردن نام رنگ ضروری می باشد";
if (!colorCode.value) errors.value.colorCode = "انتخاب رنگ ضروری می باشد";

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

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

const addAttribute = () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
autoClose: 1000,
});
return;
}

loading.value = true;

const formData = new FormData();

formData.append("title", colorName.value);

formData.append("code", colorCode.value);

formData.append("attribute_id", 1);

ApiServiece.post(`admin/attribute-values`, formData)
.then(() => {
toast.success("!ویژگی با موفقیت اضافه شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
//document.getElementById("close").click();
emit("attribute-updated");
colorName.value = "";
colorCode.value = "";
}, 500);
})
.catch((error) => {
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;
});
};

return {
errors,
loading,
clearError,
addAttribute,
colorName,
colorCode,
};
},
};
</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>

+ 4
- 4
src/components/modals/blogCat/addBlogCat.vue Dosyayı Görüntüle

@@ -82,7 +82,6 @@
v-model="locale" v-model="locale"
class="form-control" class="form-control"
placeholder="انتخاب کنید" placeholder="انتخاب کنید"
@change="handlerChangeLocale"
> >
<option <option
key="fa" key="fa"
@@ -137,6 +136,7 @@
> >
بستن بستن
</button> </button>

<button type="submit" class="btn btn-primary" :disabled="loading"> <button type="submit" class="btn btn-primary" :disabled="loading">
<span <span
v-if="loading" v-if="loading"
@@ -201,7 +201,7 @@ export default {


const { data: { message, success, data } } = await ApiServiece.post( const { data: { message, success, data } } = await ApiServiece.post(
`admin/blog-categories`, `admin/blog-categories`,
{ title: title.value },
{ title: titleCat.value },
{ {
headers: { headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`, Authorization: `Bearer ${localStorage.getItem("token")}`,
@@ -246,10 +246,10 @@ export default {
}) })
.then(() => { .then(() => {
setTimeout(() => { setTimeout(() => {
document.getElementById("closeAddBlogCat").click();
//document.getElementById("closeAddBlogCat").click();
emit("cat-updated"); emit("cat-updated");
title.value = ""; title.value = "";
step.value = 1;
//step.value = 1;
}, 500); }, 500);
}) })
.catch((error) => { .catch((error) => {


+ 133
- 58
src/components/modals/blogCat/editBlogCat.vue Dosyayı Görüntüle

@@ -20,7 +20,6 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="editCat" class="mt-4"> <form @submit.prevent="editCat" class="mt-4">

<BButton <BButton
:disabled="!findLocaleTranslation" :disabled="!findLocaleTranslation"
:loading="loadingDelete" :loading="loadingDelete"
@@ -67,7 +66,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">عنوان دسته</label> <label class="form-label">عنوان دسته</label>
<input <input
v-model="title"
v-model="titleCat"
@input="clearError('title')" @input="clearError('title')"
type="text" type="text"
class="form-control" class="form-control"
@@ -112,8 +111,8 @@
</template> </template>


<script> <script>
import { iconData } from "../../../views/live-preview/icon/data";
import { ref, toRef, watch } from "vue";
import { iconData } from "@/views/live-preview/icon/data";
import {computed, ref, watch} from "vue";
import { toast } from "vue3-toastify"; import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css"; import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";
@@ -132,30 +131,31 @@ export default {
type: String, type: String,
Required: true, Required: true,
}, },
catRow: {
type: Object,
Required: true,
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const localTitle = toRef(props.title);
const localIcon = ref(props.icon);
const localId = toRef(props.id);
const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
const loadingDelete = ref(false);
const titleCat = ref(null); const titleCat = ref(null);
const title = ref(null);
const locale = ref("fa");


watch(
() => props.title,
(newVal) => (localTitle.value = newVal)
);
const categoryRowModel = computed({
get: () => props.catRow,
set: (newValue) => emit('update:categoryRow', newValue)
});


watch( watch(
() => props.icon,
(newVal) => (localIcon.value = newVal)
);
() => props.catRow,
(newVal) => {
titleCat.value = newVal?.translation?.title


watch(
() => props.id,
(newVal) => (localId.value = newVal)
);
locale.value = newVal?.translation?.locale
}
)


const clearError = (field) => { const clearError = (field) => {
errors.value[field] = ""; errors.value[field] = "";
@@ -163,17 +163,12 @@ export default {


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


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

const editCat = () => {
const editCat = async () => {
if (!validateForm()) { if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", { toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right", position: "top-right",
@@ -181,48 +176,128 @@ export default {
}); });
return; return;
} }
loading.value = true;

const formData = new FormData();
formData.append("title", localTitle.value);
formData.append("icon", localIcon.value);
ApiServiece.put(`admin/blog-categories/${localId.value}`, formData)
.then(() => {
toast.success("!دسته با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
document.getElementById("closeEditBlogCat").click();
emit("cat-updated");
}, 500);
})
.catch((error) => {
toast.error(`${error.response.data.message}`, {
position: "top-right",
autoClose: 1000,
});
})
.finally(() => {
loading.value = false;

try {
loading.value = true;

const params = {
title: titleCat.value,
locale: locale.value,
}

const existingTranslation = categoryRowModel.value?.translations?.find(
t => t.locale === locale.value
);

const { data } = existingTranslation
? await ApiServiece.put(
`admin/blog-categories/${categoryRowModel.value?.id}/translations/${existingTranslation?.id}`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
)
: await ApiServiece.post(
`admin/blog-categories/${categoryRowModel.value?.id}/translations`,
params,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);

console.log(data,'data data data')

toast.success("!دسته با موفقیت ویرایش شد", {
position: "top-right",
autoClose: 1000,
}); });

} catch (e) {
toast.error(`${e?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
})
} finally {
loading.value = false;
}
}; };


const handlerChangeLocale = (e) => {
const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === e.target.value);

if (findLocale) {
titleCat.value = findLocale?.title
} else {
titleCat.value = undefined
}
}

const findLocaleTranslation = computed(() => {
const foundTranslation = categoryRowModel.value?.translations?.find(
item => item?.locale === locale.value
);

if (foundTranslation) {
switch (foundTranslation?.locale) {
case "en":
return "انگلیسی";
case "fa":
return "فارسی";
case "ar":
return "عربی";
default:
return null;
}
}

return null;
});

const handlerRemoveTranslation = async () => {
const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === locale.value);

if (findLocale) {
try {
loadingDelete.value = true;

const { data: { success, message, data } } = await ApiServiece.delete(
`admin/blog-categories/${categoryRowModel.value?.id}/translations/${findLocale?.id}`
)

if (success) {
const updatedCategory = props.catRow

updatedCategory.translations = data?.translations

titleCat.value = null

categoryRowModel.value = updatedCategory

toast.success(message)
}
} catch (e) {
console.log(e)
}finally {
loadingDelete.value = false
}
}
}

return { return {
errors, errors,
loading, loading,
clearError, clearError,
editCat, editCat,
localTitle,
iconData, iconData,
setSelectedIcon,
localIcon,
localId,
title,
titleCat, titleCat,
locale,
handlerChangeLocale,
findLocaleTranslation,
handlerRemoveTranslation,
}; };
}, },
}; };


+ 249
- 0
src/components/modals/countries/CountriesModal.vue Dosyayı Görüntüle

@@ -0,0 +1,249 @@
<script setup>
import { ref, reactive, defineExpose, defineEmits } from "vue";
import ApiService from "@/services/ApiService";
import {toast} from "vue3-toastify";
import {BRow} from "bootstrap-vue-next";
import codeCountries from "@/utils/code-countries"

const initForm = {
title: null,
minimum_shipping_weight: null,
per_kilograms_cost: null,
status: 'in_active',
country_code: null,
}

const emit = defineEmits(['country-updated'])

const errors = ref({})

const loading = ref(false)

const form = reactive({...initForm})

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

const validateForm = () => {
errors.value = {};
if (!form.title) errors.value.title = "وارد کردن عنوان کشور ضروری می باشد";
if (!form.minimum_shipping_weight) errors.value.minimum_shipping_weight = "وارد کردن حداقل وزن حمل ونقل ضروری می باشد";
if (!form.per_kilograms_cost) errors.value.per_kilograms_cost = "وارد کردن هزینه ضروری می باشد";
if (!form.country_code) errors.value.country_code = "وارد کردن پیش شماره کشور ضروری می باشد";

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

const addCountry = async () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
autoClose: 1000,
});
return;
}

try {
loading.value = true

const url = form?.id ? `admin/country-configs/${form?.id}` : 'admin/country-configs'

const { data: { success, message } } = await ApiService[form?.id ? 'put' : 'post'](url, form)

if (success) {
toast.success(message)

Object.assign(form, initForm)

document.getElementById("closeModal").click();

emit("country-updated");
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loading.value = false
}
};

const closeModal = () => {
if (form?.id)
form.id = undefined

Object.assign(form, initForm)
}

defineExpose({ form })
</script>

<template>
<div
class="modal fade"
id="countriesModal"
tabindex="-1"
role="dialog"
data-bs-backdrop="static"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">
{{ form?.id ? 'ویرایش کشور' : 'اضافه کردن کشور' }}
</h5>
<button
type="button"
class="btn-close"
id="closeModal"
data-bs-dismiss="modal"
aria-label="Close"
@click="closeModal"
></button>
</div>

<div class="modal-body">
<form @submit.prevent="addCountry">
<BRow class="g-3">
<!-- Form fields -->

<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label">نام کشور</label>
<input
v-model="form.title"
type="text"
class="form-control"
placeholder="نام کشور را وارد نمایید"
/>
<small v-if="errors.title" class="text-danger">{{
errors.title
}}</small>
</div>
</BCol>

<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label">حداقل هزینه حمل و نقل</label>
<input
v-model="form.minimum_shipping_weight"
@input="clearError('minimum_shipping_weight')"
type="number"
class="form-control"
/>
<small v-if="errors.minimum_shipping_weight" class="text-danger">{{
errors.minimum_shipping_weight
}}</small>
</div>
</BCol>

<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label">هزینه هر کیلو گرم</label>
<input
v-model="form.per_kilograms_cost"
@input="clearError('per_kilograms_cost')"
type="number"
class="form-control"
/>
<small v-if="errors.per_kilograms_cost" class="text-danger">{{
errors.per_kilograms_cost
}}</small>
</div>
</BCol>

<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label">انتخاب پیش شماره</label>

<select
class="form-select"
v-model="form.country_code"
@change="clearError('country_code')"
>
<option v-for="code in codeCountries" :value="code?.dial_code" :key="code?.code">
{{ code?.name }}({{ code?.dial_code }})
</option>
</select>

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

<BCol class="col-lg-6">
<div class="form-group">
<label class="form-label">وضعیت</label>

<select
class="form-select"
v-model="form.status"
@change="clearError('status')"
>
<option value="active">فعال</option>
<option value="in_active">غیر فعال</option>
</select>

<small v-if="errors.status" class="text-danger">{{errors.status}}</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="addClose"
>
بستن
</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>

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

.modal-content {
padding: 1.5rem;
}

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

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

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

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

+ 22
- 1
src/components/modals/editUser.vue Dosyayı Görüntüle

@@ -84,6 +84,8 @@
}}</small> }}</small>
</div> </div>
</BCol> </BCol>

<ConfigCountriesSelect v-model:country-code="localCountryCode"/>
</BRow> </BRow>


<div <div
@@ -120,8 +122,10 @@ import ApiServiece from "@/services/ApiService";
import { ref, toRef, watch } from "vue"; import { ref, toRef, watch } from "vue";
import { toast } from "vue3-toastify"; import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css"; import "vue3-toastify/dist/index.css";
import ConfigCountriesSelect from "@/components/ConfigCountriesSelect.vue";


export default { export default {
components: {ConfigCountriesSelect},
props: { props: {
name: { name: {
type: String, type: String,
@@ -139,6 +143,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
countryCode: {
type: Number,
required: true,
},
}, },


setup(props, { emit }) { setup(props, { emit }) {
@@ -146,6 +154,7 @@ export default {
const localName = toRef(props.name); const localName = toRef(props.name);
const localMobile = toRef(props.mobile); const localMobile = toRef(props.mobile);
const localRole = toRef(props.role); const localRole = toRef(props.role);
const localCountryCode = toRef(props.countryCode);
const localId = toRef(props.id); const localId = toRef(props.id);
const errors = ref({}); const errors = ref({});
const loading = ref(false); const loading = ref(false);
@@ -168,12 +177,22 @@ export default {
(newVal) => (localRole.value = newVal) (newVal) => (localRole.value = newVal)
); );


watch(
() => props.countryCode,
(newVal) => {
localCountryCode.value = Number(newVal);
}
);


const validateForm = () => { const validateForm = () => {
errors.value = {}; errors.value = {};
if (!localMobile.value) if (!localMobile.value)
errors.value.localMobile = "وارد کردن موبایل ضروری می باشد"; errors.value.localMobile = "وارد کردن موبایل ضروری می باشد";
if (!localRole.value) if (!localRole.value)
errors.value.localMobile = "انتخاب کردن نقش کاربر ضروری می باشد"; errors.value.localMobile = "انتخاب کردن نقش کاربر ضروری می باشد";
if (!localCountryCode.value)
errors.value.localCountryCode = "انتخاب کردن پیش شماره ضروری می باشد";
if (password.value && password.value.length < 8) { if (password.value && password.value.length < 8) {
errors.value.password = " رمز عبور باید حداقل 8 کاراکتر باشد"; errors.value.password = " رمز عبور باید حداقل 8 کاراکتر باشد";
} }
@@ -202,12 +221,13 @@ export default {
if (password.value) { if (password.value) {
formData.append("password", password.value); formData.append("password", password.value);
} }
formData.append("country_config_id", localCountryCode.value)

ApiServiece.put(`admin/users/${localId.value}`, formData) ApiServiece.put(`admin/users/${localId.value}`, formData)
.then(() => { .then(() => {
toast.success("!کاربر با موفقیت ویرایش شد", { toast.success("!کاربر با موفقیت ویرایش شد", {
position: "top-right", position: "top-right",
autoClose: 1000, autoClose: 1000,
onClose: () => emit("user-updated"),
}); });
}) })
@@ -238,6 +258,7 @@ export default {
password, password,
editUser, editUser,
localRole, localRole,
localCountryCode,
}; };
}, },
}; };


+ 18
- 2
src/router/routes.js Dosyayı Görüntüle

@@ -23,6 +23,15 @@ export default [
}, },
component: () => import("../views/live-preview/pages/users/users.vue"), component: () => import("../views/live-preview/pages/users/users.vue"),
}, },
{
path: "/countries",
name: "countries",
meta: {
title: "کشورها",
requiresAuth: true,
},
component: () => import("../views/live-preview/pages/countries/countries.vue"),
},
{ {
path: "/brands", path: "/brands",
name: "brands", name: "brands",
@@ -51,6 +60,15 @@ export default [
component: () => component: () =>
import("../views/live-preview/pages/attributes/attributes.vue"), import("../views/live-preview/pages/attributes/attributes.vue"),
}, },
{
path: "/attributes-value/:id",
name: "attributes-value",
meta: {
title: "مقدار ویژگی",
requiresAuth: true,
},
component: () => import("../views/live-preview/pages/attributes-value/attributes-value.vue"),
},
{ {
path: "/blogCat", path: "/blogCat",
name: "blogCat", name: "blogCat",
@@ -290,9 +308,7 @@ export default [
}, },
component: () => import("../views/live-preview/pages/settings/setting.vue"), component: () => import("../views/live-preview/pages/settings/setting.vue"),
}, },

// Auth // Auth

{ {
path: "/otpLogin", path: "/otpLogin",
name: "otpLogin", name: "otpLogin",


+ 1214
- 0
src/utils/code-countries.js
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 463
- 0
src/views/live-preview/pages/attributes-value/attributes-value.vue Dosyayı Görüntüle

@@ -0,0 +1,463 @@
<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 { useRoute } from "vue-router"
import AddAttributeValue from "@/components/modals/attribute-value/addAttributeValue.vue";
import EditAttributeValue from "@/components/modals/attribute-value/editAttributeValue.vue";

export default {
name: "BORDER",
components: {
EditAttributeValue,
AddAttributeValue,
Layout,
},
setup() {
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 attributeCode = ref();
const attrRow = ref(null)
const route = useRoute()

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

searchTimeout = setTimeout(() => {
getAttributes();
page.value = 1;
}, 500);
};
watch(searchQuery, () => {
handleSearchChange();
});
const getAttributes = () => {
filterLoading.value = true;
ApiServiece.get(`admin/attribute-values?attribute_id=${route?.params?.id}&title=${encodeURIComponent(
searchQuery.value || ""
)}&code=${encodeURIComponent(searchQuery.value || "")}
&paginate=${paginate.value || 10}&page=${page.value || 1}`)
.then((resp) => {
filterLoading.value = false;
attributes.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page;
})
.catch(() => {
filterLoading.value = false;
});
};

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) {
end += 1 - start;
start = 1;
}
if (end > totalPages.value) {
start -= end - totalPages.value;
end = totalPages.value;
}
start = Math.max(start, 1);

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 = attr => {
attrRow.value = attr
}

const getAttributeValues = () => {
ApiServiece.get(`admin/attributes`).then((resp) => {
console.log(resp);
attributeValues.value = resp.data.data;
});
};

onMounted(() => {
getAttributes();
getAttributeValues();
});

return {
attributes,
convertToJalali,
handleAttributeUpdated,
editModalData,
deleteAttribute,
searchQuery,
filterLoading,
attributeId,
attributeCode,
attributeTitle,
attributeValues,
searchPage,
currentPage,
totalPages,
paginate,
page,
prevPage,
nextPage,
handlePageInput,
visiblePages,
attrRow
};
},
};
</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"
/>

</div>
<button
data-bs-toggle="modal"
data-bs-target="#addAttributeValue"
class="btn btn-light text-primary btn-sm px-3"
>
افزودن مقدار ویژگی
</button>
</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>{{ convertToJalali(attribute?.created_at) }}</td>
<td>{{ attribute?.translation?.title }}</td>
<td>
<span class="rounded-circle m-auto d-block" :style="{ background: attribute?.code, width: '20px', height: '20px' }" />
</td>
<td>
<button
@click="
editModalData(attribute)
"
data-bs-toggle="modal"
data-bs-target="#editAttributeValue"
class="btn btn-sm btn-outline-warning me-1"
>
ویرایش
</button>
<button
@click="deleteAttribute(attribute?.id, attribute?.translation?.title)"
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>
<AddAttributeValue
:attributeValues="attributeValues"
@attribute-updated="handleAttributeUpdated()"
/>
<edit-attribute-value
:attrRow="attrRow"
@attribute-updated="handleAttributeUpdated()"
/>
</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>

<!-- First page and leading dots -->
<li
v-if="visiblePages[0] > 1"
class="page-item"
@click="page = 1"
>
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="visiblePages[0] > 2" class="page-item disabled">
<span class="page-link">...</span>
</li>

<!-- Visible pages -->
<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>

<!-- Trailing dots and last page -->
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages - 1"
class="page-item disabled"
>
<span class="page-link">...</span>
</li>
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages"
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;
}
.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>

+ 34
- 33
src/views/live-preview/pages/attributes/attributes.vue Dosyayı Görüntüle

@@ -8,6 +8,7 @@ import "vue3-toastify/dist/index.css";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import addAttribute from "@/components/modals/attribute/addAttribute.vue"; import addAttribute from "@/components/modals/attribute/addAttribute.vue";
import editAttribute from "@/components/modals/attribute/editAttribute.vue"; import editAttribute from "@/components/modals/attribute/editAttribute.vue";
import router from "@/router";
export default { export default {
name: "BORDER", name: "BORDER",
components: { components: {
@@ -28,6 +29,8 @@ export default {
const attributeTitle = ref(); const attributeTitle = ref();
const attributeId = ref(); const attributeId = ref();
const attributeCode = ref(); const attributeCode = ref();
const attrRow = ref(null)

let searchTimeout = null; let searchTimeout = null;
const convertToJalali = (date) => { const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss") return moment(date, "YYYY-MM-DD HH:mm:ss")
@@ -48,7 +51,7 @@ export default {
const getAttributes = () => { const getAttributes = () => {
filterLoading.value = true; filterLoading.value = true;
ApiServiece.get( ApiServiece.get(
`admin/attribute-values?attribute_id=1&title=${encodeURIComponent(
`admin/attributes?attribute_id=1&title=${encodeURIComponent(
searchQuery.value || "" searchQuery.value || ""
)}&code=${encodeURIComponent(searchQuery.value || "")} )}&code=${encodeURIComponent(searchQuery.value || "")}
&paginate=${paginate.value || 10}&page=${page.value || 1} &paginate=${paginate.value || 10}&page=${page.value || 1}
@@ -56,11 +59,9 @@ export default {
) )
.then((resp) => { .then((resp) => {
filterLoading.value = false; filterLoading.value = false;
console.log(resp.data);
attributes.value = resp.data.data.data; attributes.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page; currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page; totalPages.value = resp.data.data.last_page;
console.log(attributes.value);
}) })
.catch(() => { .catch(() => {
filterLoading.value = false; filterLoading.value = false;
@@ -123,13 +124,14 @@ export default {
} }
return pages; return pages;
}); });

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


const deleteAttribute = (id, title) => { const deleteAttribute = (id, title) => {
Swal.fire({ Swal.fire({
text: `می خواهید رنگ ${title} را حذف کنید ؟`,
text: `می خواهید رنگ ${title ?? ''} را حذف کنید ؟`,
icon: "warning", icon: "warning",
showCancelButton: true, showCancelButton: true,
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
@@ -138,7 +140,7 @@ export default {
cancelButtonText: "خیر", cancelButtonText: "خیر",
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
ApiServiece.delete(`admin/attribute-values/${id}`)
ApiServiece.delete(`admin/attributes/${id}`)
.then(() => { .then(() => {
toast.success("!ویژگی با موفقیت حذف شد", { toast.success("!ویژگی با موفقیت حذف شد", {
position: "top-right", position: "top-right",
@@ -159,13 +161,9 @@ export default {
}); });
}; };


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

const editModalData = attr => {
attrRow.value = attr
}


const getAttributeValues = () => { const getAttributeValues = () => {
ApiServiece.get(`admin/attributes`).then((resp) => { ApiServiece.get(`admin/attributes`).then((resp) => {
@@ -174,10 +172,15 @@ export default {
}); });
}; };


const redirectToAttrValue = id => {
router.push(`/attributes-value/${id}`)
}

onMounted(() => { onMounted(() => {
getAttributes(); getAttributes();
getAttributeValues(); getAttributeValues();
}); });

return { return {
attributes, attributes,
convertToJalali, convertToJalali,
@@ -199,10 +202,13 @@ export default {
nextPage, nextPage,
handlePageInput, handlePageInput,
visiblePages, visiblePages,
attrRow,
redirectToAttrValue
}; };
}, },
}; };
</script> </script>

<template> <template>
<Layout> <Layout>
<BRow> <BRow>
@@ -227,7 +233,7 @@ export default {
data-bs-target="#addAttribute" data-bs-target="#addAttribute"
class="btn btn-light text-primary btn-sm px-3" class="btn btn-light text-primary btn-sm px-3"
> >
افزودن رنگ
افزودن ویژگی
</button> </button>
</div> </div>
<div v-if="!filterLoading" class="card-body table-border-style p-0"> <div v-if="!filterLoading" class="card-body table-border-style p-0">
@@ -235,46 +241,40 @@ export default {
<table class="table table-hover table-bordered m-0" dir="rtl"> <table class="table table-hover table-bordered m-0" dir="rtl">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>نام</th>
<th>رنگ</th>
<th>کد رنگ</th>
<th>تاریخ ایجاد</th> <th>تاریخ ایجاد</th>
<th>نام</th>
<th>دسته بندی</th>
<th>عملیات</th> <th>عملیات</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="attribute in attributes" :key="attribute.id"> <tr v-for="attribute in attributes" :key="attribute.id">
<td>{{ attribute.title }}</td>
<td
:style="{
backgroundColor: attribute.code,
textAlign: 'center',
}"
></td>
<td>{{ attribute.code }}</td>

<td>{{ convertToJalali(attribute?.created_at) }}</td> <td>{{ convertToJalali(attribute?.created_at) }}</td>
<td>{{ attribute?.translation?.title }}</td>
<td>{{ attribute?.category?.translation?.title }}</td>
<td> <td>
<!-- <button
<button
@click=" @click="
editModalData(
attribute?.id,
attribute?.title,
attribute.code
)
editModalData(attribute)
" "
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#editAttribute" data-bs-target="#editAttribute"
class="btn btn-sm btn-outline-warning me-1" class="btn btn-sm btn-outline-warning me-1"
> >
ویرایش ویرایش
</button> -->
</button>
<button <button
@click="deleteAttribute(attribute.id, attribute.title)"
@click="deleteAttribute(attribute?.id, attribute?.translation?.title)"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
> >
حذف حذف
</button> </button>
<button
@click="redirectToAttrValue(attribute?.id)"
class="btn btn-sm btn-outline-primary me-1"
>
مقدار ویژگی ها
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -296,6 +296,7 @@ export default {
:title="attributeTitle" :title="attributeTitle"
:code="attributeCode" :code="attributeCode"
:attributeValues="attributeValues" :attributeValues="attributeValues"
:attrRow="attrRow"
@attribute-updated="handleAttributeUpdated()" @attribute-updated="handleAttributeUpdated()"
/> />
</BRow> </BRow>


+ 459
- 355
src/views/live-preview/pages/banners/addBanner.vue Dosyayı Görüntüle

@@ -58,339 +58,395 @@
</div> </div>
</BCardHeader> </BCardHeader>
<BCardBody> <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_page">صفحه اصلی</option>
<option value="category">صفحه دسته</option>
<option value="special_page">صفحه فروش ویژه</option>
<option value="brand">صفحه برند</option>
<option value="blog_page">صفحه بلاگ</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedCatPage"
:isLoading="categoryPageSelectorLoader"
:options="formattedCategoriesPages"
@change="clearError('selectedCatPage')"
placeholder="دسته ای را انتخاب کنید"
@search="handleCategoryPageSearch"
/>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه برند</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedBrandPage"
:isLoading="brandSelectorLoader"
:options="formattedBrands"
@change="clearError(`selectedBrandPage`)"
placeholder="برندی را انتخاب کنید"
@search="handleBrandSearch"
/>
</div>
<small v-if="errors.selectedBrandPage" class="text-danger">
{{ errors.selectedBrandPage }}
</small>
</BCol>

<BCol v-if="pannel != 'wholesale' && pannel" 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="انتخاب صفحه فرود"

<Steppy
v-model:step="step"
:tabs="[
{ title: 'ایجاد بنر', isValid: true },
{ title: 'ترجمه ها', isValid: true },
]"
backText="قبلی"
nextText="بعدی"
doneText="ذخیره"
primaryColor1="#04A9F5"
circleSize="45"
>
<template #1>
<BRow class="g-3">
<BCol>
<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_page">صفحه اصلی</option>
<option value="category">صفحه دسته</option>
<option value="special_page">صفحه فروش ویژه</option>
<option value="brand">صفحه برند</option>
<option value="blog_page">صفحه بلاگ</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedCatPage"
:isLoading="categoryPageSelectorLoader"
:options="formattedCategoriesPages"
@change="clearError('selectedCatPage')"
placeholder="دسته ای را انتخاب کنید"
@search="handleCategoryPageSearch"
/>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه برند</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedBrandPage"
:isLoading="brandSelectorLoader"
:options="formattedBrands"
@change="clearError(`selectedBrandPage`)"
placeholder="برندی را انتخاب کنید"
@search="handleBrandSearch"
/>
</div>
<small v-if="errors.selectedBrandPage" class="text-danger">
{{ errors.selectedBrandPage }}
</small>
</BCol>

<BCol v-if="pannel != 'wholesale' && pannel" 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"
> >
<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>

<VueSelect
style="
<label for="token">صفحه کدام محصول</label>

<VueSelect
style="
--vs-min-height: 48px; --vs-min-height: 48px;
--vs-border-radius: 8px; --vs-border-radius: 8px;
margin-top: 7px; margin-top: 7px;
" "
v-model="selectedLandingProduct"
:isLoading="productSelectorLoader"
@change="clearError(`selectedLandingProduct`)"
:options="formattedProducts"
placeholder="محصولی را انتخاب کنید"
@search="handleSearch"
/>
<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>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedLandingCat"
@change="clearError('selectedLandingCat')"
:isLoading="categorySelectorLoader"
:options="formattedCategories"
placeholder="دسته ای را انتخاب کنید"
@search="handleCategorySearch"
/>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>
</BCol>

<BCol v-if="pageType === 'main_page' && 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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && 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
v-if="pageType === 'special_page' && 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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Slideshow</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol
v-if="pageType === 'blog_page' && 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="موقعیت بنر"
:value="(selectedLoc = 'A')"
v-model="selectedLandingProduct"
:isLoading="productSelectorLoader"
@change="clearError(`selectedLandingProduct`)"
:options="formattedProducts"
placeholder="محصولی را انتخاب کنید"
@search="handleSearch"
/>
<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>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedLandingCat"
@change="clearError('selectedLandingCat')"
:isLoading="categorySelectorLoader"
:options="formattedCategories"
placeholder="دسته ای را انتخاب کنید"
@search="handleCategorySearch"
/>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>
</BCol>

<BCol v-if="pageType === 'main_page' && 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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && 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
v-if="pageType === 'special_page' && pannel === 'web'"
md="6"
> >
<option value="A">A-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && 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="موقعیت بنر"
<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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Slideshow</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol
v-if="pageType === 'blog_page' && pannel === 'web'"
md="6"
> >
<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>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol md="6" v-if="pannel">
<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">
<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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && 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>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>
</BRow>

<button <button
type="submit"
class="btn btn-primary"
@click.prevent="submitForm"
:disabled="loading"
type="submit"
class="btn btn-primary mt-5"
@click.prevent="submitForm"
:disabled="loading"
> >
<span v-if="loading">
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
</span>
<span v-else>ایجاد</span>
</button>
</template>

<template #2>
<BRow>
<BCol cols="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>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<button
@click.prevent="submitTranslation"
:disabled="loadingFinally"
class="btn btn-primary mt-5"
>
<span v-if="loadingFinally">
<i class="fa fa-spinner fa-spin"></i> بارگذاری... <i class="fa fa-spinner fa-spin"></i> بارگذاری...
</span> </span>
<span v-else>ایجاد</span> <span v-else>ایجاد</span>
</button> </button>
</div>
</div>
</BCardFooter>
</template>
</Steppy>
</BCardBody>
</BCard> </BCard>
</BCol> </BCol>
</BRow> </BRow>
@@ -413,10 +469,12 @@ import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import Layout from "@/layout/custom.vue"; import Layout from "@/layout/custom.vue";
import {Steppy} from "vue3-steppy";


export default { export default {
name: "SAMPLE-PAGE", name: "SAMPLE-PAGE",
components: { components: {
Steppy,
Layout, Layout,
mainPageBanner, mainPageBanner,
catBanner, catBanner,
@@ -437,7 +495,7 @@ export default {
const selectedLandingCat = ref(); const selectedLandingCat = ref();
const selectedLandingProduct = ref(); const selectedLandingProduct = ref();
const selectedLoc = ref(); const selectedLoc = ref();
const pannel = ref();
const pannel = ref('web');
const image = ref(); const image = ref();
const imagePreview = ref(); const imagePreview = ref();
const productSelectorLoader = ref(false); const productSelectorLoader = ref(false);
@@ -446,6 +504,10 @@ export default {
const brandSelectorLoader = ref(false); const brandSelectorLoader = ref(false);
const loading = ref(false); const loading = ref(false);
const errors = ref({}); const errors = ref({});
const step = ref(1);
const locale = ref('fa');
const loadingFinally = ref(false);
const bannerId = ref(null);


const handleSearch = async (searchTerm) => { const handleSearch = async (searchTerm) => {
if (searchTerm.length < 3) return; if (searchTerm.length < 3) return;
@@ -465,10 +527,10 @@ export default {
}; };


const formattedBrands = computed(() => const formattedBrands = computed(() =>
Array.isArray(brands.value) // ✅ Check if products.value is an array
Array.isArray(brands.value)
? brands.value.map((brand) => ({ ? brands.value.map((brand) => ({
value: brand.id, value: brand.id,
label: brand.title,
label: brand?.translation?.title,
})) }))
: [] : []
); );
@@ -490,10 +552,10 @@ export default {
}; };


const formattedProducts = computed(() => const formattedProducts = computed(() =>
Array.isArray(products.value) // ✅ Check if products.value is an array
Array.isArray(products.value)
? products.value.map((product) => ({ ? products.value.map((product) => ({
value: product.id,
label: product.title,
value: product?.id,
label: product?.translation?.title,
})) }))
: [] : []
); );
@@ -516,9 +578,9 @@ export default {


const formattedCategories = computed(() => const formattedCategories = computed(() =>
Array.isArray(cats.value) Array.isArray(cats.value)
? cats.value.map((cat) => ({
value: cat.id,
label: cat.title,
? cats.value?.map((cat) => ({
value: cat?.id,
label: cat?.translation?.title,
})) }))
: [] : []
); );
@@ -542,8 +604,8 @@ export default {
const formattedCategoriesPages = computed(() => const formattedCategoriesPages = computed(() =>
Array.isArray(categoryPages.value) Array.isArray(categoryPages.value)
? categoryPages.value.map((categoryPage) => ({ ? categoryPages.value.map((categoryPage) => ({
value: categoryPage.id,
label: categoryPage.title,
value: categoryPage?.id,
label: categoryPage?.translation?.title,
})) }))
: [] : []
); );
@@ -593,7 +655,7 @@ export default {


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


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


return Object.keys(errors.value).length === 0; return Object.keys(errors.value).length === 0;
}; };
@@ -611,65 +673,72 @@ export default {
return; return;
} }
loading.value = true; loading.value = true;
const formData = new FormData();
formData.append("title", title.value);

const params = {}

params.title = title.value;


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


if (pageType.value === "brand") { if (pageType.value === "brand") {
formData.append("page_id", selectedBrandPage.value);
params.page_id = selectedBrandPage.value
} }


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


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


if (selectedLoc.value === "A") { if (selectedLoc.value === "A") {
formData.append("type", pageType.value === 'blog_page' ? "banner" :"slider");
params.type = pageType.value === 'blog_page' ? "banner" :"slider"
} }


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


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

params.location = "A"
} }


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

params.panel = pannel.value

if (pannel.value == "wholesale") { if (pannel.value == "wholesale") {
formData.append("page_type", "main_page");
params.page_type = 'main_page'
} }


if (pannel.value !== "wholesale") { if (pannel.value !== "wholesale") {
formData.append("page_type", pageType.value);
params.page_type = pageType.value
} }


formData.append("image", image.value);
formData.append("sort", 1);
params.sort = 1


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

bannerId.value = data?.data?.id;

step.value++

loading.value = false; loading.value = false;


resetForm(); resetForm();
@@ -683,6 +752,36 @@ export default {
}); });
}); });
}; };

const submitTranslation = async () => {
try {
loadingFinally.value = true;

const formData = new FormData();

formData.append("image", image.value);

formData.append("locale", locale.value);

const { data: { message, success } } = await ApiServiece.post(`admin/banners/${bannerId.value}/translations`,formData,{
headers: {
'content-type': 'multipart/form-data',
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})

if (success) {
toast.success(message)

//image.value = null
}
} catch (e) {
toast.error(e?.response?.data?.message)
} finally {
loadingFinally.value = false;
}
}

const resetForm = () => { const resetForm = () => {
title.value = ""; title.value = "";
selectedCatPage.value = ""; selectedCatPage.value = "";
@@ -729,6 +828,11 @@ export default {
brandSelectorLoader, brandSelectorLoader,
formattedBrands, formattedBrands,
handleBrandSearch, handleBrandSearch,
step,
locale,
submitTranslation,
loadingFinally,
bannerId
}; };
}, },
}; };


+ 3
- 3
src/views/live-preview/pages/banners/banners.vue Dosyayı Görüntüle

@@ -203,7 +203,7 @@ export default {
case 'category': case 'category':
return 'دسته ' + banner?.category_page?.title; return 'دسته ' + banner?.category_page?.title;
case 'brand': case 'brand':
return 'برند ' + banner?.brand_page?.title
return 'برند ' + banner?.brand_page?.translation?.title
case 'main_page': case 'main_page':
return 'صفحه اصلی' return 'صفحه اصلی'
case 'special_page': case 'special_page':
@@ -215,10 +215,10 @@ export default {


const setProductOrCategory = (banner) => { const setProductOrCategory = (banner) => {
if (banner?.category_id) { if (banner?.category_id) {
return `دسته<br>${banner?.category?.title}`;
return `دسته<br>${banner?.category?.translation?.title ?? 'ندارد'}`;
} }
if (banner?.product_id) { if (banner?.product_id) {
return `محصول<br>${banner?.product?.title}`;
return `محصول<br>${banner?.product?.translation?.title ?? 'ندارد'}`;
} }
return ""; return "";
}; };


+ 498
- 371
src/views/live-preview/pages/banners/editBanner.vue Dosyayı Görüntüle

@@ -58,350 +58,376 @@
</div> </div>
</BCardHeader> </BCardHeader>
<BCardBody> <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' && pannel" 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_page">صفحه اصلی</option>
<option value="category">صفحه دسته</option>
<option value="special_page">صفحه فروش ویژه</option>
<option value="brand">صفحه برند</option>
<option value="blog_page">صفحه بلاگ</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedCatPage"
@change="clearError('selectedCatPage')"
label="label"
:isLoading="categoryPageSelectorLoader"
:reduce="(option) => option.value"
:options="formattedCatPages"
@search="handleCategoryPageSearch"
placeholder="دسته ای را انتخاب کنید"
/>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه برند</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedBrandPage"
label="label"
:isLoading="brandSelectorLoader"
:reduce="(option) => option.value"
:options="formattedBrands"
@change="clearError(`selectedBrandPage`)"
placeholder="برندی را انتخاب کنید"
@search="handleBrandSearch"
/>
</div>
<small v-if="errors.selectedBrandPage" class="text-danger">
{{ errors.selectedBrandPage }}
</small>
</BCol>

<BCol v-if="pannel == 'web' && pannel" 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="انتخاب صفحه فرود"

<BTabs>
<BTab title="ویرایش بنر">
<BRow class="g-3 mt-2">
<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 v-if="pannel === 'web' && pannel" 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_page">صفحه اصلی</option>
<option value="category">صفحه دسته</option>
<option value="special_page">صفحه فروش ویژه</option>
<option value="brand">صفحه برند</option>
<option value="blog_page">صفحه بلاگ</option>
</select>
</div>
<small v-if="errors.pageType" class="text-danger">
{{ errors.pageType }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه دسته</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedCatPage"
@change="clearError('selectedCatPage')"
label="label"
:isLoading="categoryPageSelectorLoader"
:reduce="(option) => option.value"
:options="formattedCatPages"
@search="handleCategoryPageSearch"
placeholder="دسته ای را انتخاب کنید"
/>
</div>
<small v-if="errors.selectedCatPage" class="text-danger">
{{ errors.selectedCatPage }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && pannel === 'web'" md="6">
<div class="form-group">
<label class="form-label">صفحه برند</label>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedBrandPage"
label="label"
:isLoading="brandSelectorLoader"
:reduce="(option) => option.value"
:options="formattedBrands"
@change="clearError(`selectedBrandPage`)"
placeholder="برندی را انتخاب کنید"
@search="handleBrandSearch"
/>
</div>
<small v-if="errors.selectedBrandPage" class="text-danger">
{{ errors.selectedBrandPage }}
</small>
</BCol>

<BCol v-if="pannel == 'web' && pannel" 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"
> >
<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>

<VueSelect
style="
<label for="token">صفحه کدام محصول</label>

<VueSelect
style="
--vs-min-height: 48px; --vs-min-height: 48px;
--vs-border-radius: 8px; --vs-border-radius: 8px;
margin-top: 7px; margin-top: 7px;
" "
v-model="selectedLandingProduct"
:reduce="(option) => option.value"
label="label"
:isLoading="productSelectorLoader"
:options="formattedProducts"
@search="handleProductSearch"
placeholder="محصولی را انتخاب کنید"
/>
<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>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
:isLoading="categorySelectorLoader"
@change="clearError('selectedLandingCat')"
label="label"
v-model="selectedLandingCat"
:reduce="(option) => option.value"
:options="formattedCategories"
placeholder="دسته ای را انتخاب کنید"
@search="handleSearch"
/>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>
</BCol>

<BCol v-if="pageType === 'main_page' && 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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && 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
v-if="pageType === 'special_page' && 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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Slideshow</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && 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="موقعیت بنر"
v-model="selectedLandingProduct"
:reduce="(option) => option.value"
label="label"
:isLoading="productSelectorLoader"
:options="formattedProducts"
@search="handleProductSearch"
placeholder="محصولی را انتخاب کنید"
/>
<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>

<VueSelect
style="--vs-min-height: 48px; --vs-border-radius: 8px"
v-model="selectedLandingCat"
@change="clearError('selectedLandingCat')"
:isLoading="categorySelectorLoader"
:options="formattedCategories"
placeholder="دسته ای را انتخاب کنید"
@search="handleSearch"
/>
</div>
<small v-if="errors.selectedLandingCat" class="text-danger">
{{ errors.selectedLandingCat }}
</small>

</BCol>

<BCol v-if="pageType === 'main_page' && 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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'category' && 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
v-if="pageType === 'special_page' && pannel === 'web'"
md="6"
> >
<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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol
v-if="pageType === 'blog_page' && 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="موقعیت بنر"
:value="(selectedLoc = 'A')"
<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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Slideshow</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol v-if="pageType === 'brand' && 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>
<option value="J">J-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>

<BCol
v-if="pageType === 'blog_page' && pannel === 'web'"
md="6"
> >
<option value="A">A-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">
<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="موقعیت بنر"
:value="(selectedLoc = 'A')"
>
<option value="A">A-Banner</option>
</select>
</div>
<small v-if="errors.selectedLoc" class="text-danger">
{{ errors.selectedLoc }}
</small>
</BCol>
</BRow>

<button <button
type="submit"
class="btn btn-primary"
@click.prevent="submitForm"
:disabled="loading"
type="submit"
class="btn btn-primary mt-4"
@click.prevent="submitForm"
:disabled="loading"
> >
<span v-if="loading"> <span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> ویرایش... <i class="fa fa-spinner fa-spin"></i> ویرایش...
</span> </span>
<span v-else>ویرایش</span> <span v-else>ویرایش</span>
</button> </button>
</div>
</div>
</BCardFooter>
</BTab>

<BTab title="ترجمه ها" class="mt-4">
<BRow>
<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>

<BCol lg="6">
<div class="form-group">
<label class="form-label">انتخاب زبان</label>
<select
v-model="locale"
class="form-control"
placeholder="انتخاب کنید"
@change="handlerChangeLocale"
>
<option
key="fa"
value="fa"
>
فارسی
</option>
<option
key="en"
value="en"
>
انگلیسی
</option>
<option
key="ar"
value="ar"
>
عربی
</option>
</select>
</div>
</BCol>
</BRow>

<button
@click.prevent="editTranslationBanner"
:disabled="loading"
class="btn btn-primary mt-5"
>
<span v-if="loading">
<i class="fa fa-spinner fa-spin"></i> بارگذاری...
</span>
<span v-else>ایجاد</span>
</button>
</BTab>
</BTabs>
</BCardBody>
</BCard> </BCard>
</BCol> </BCol>
</BRow> </BRow>
@@ -425,10 +451,12 @@ import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService"; import ApiServiece from "@/services/ApiService";
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import Layout from "@/layout/custom.vue"; import Layout from "@/layout/custom.vue";
import {BTabs} from "bootstrap-vue-next";


export default { export default {
name: "SAMPLE-PAGE", name: "SAMPLE-PAGE",
components: { components: {
BTabs,
Layout, Layout,
mainPageBanner, mainPageBanner,
catBanner, catBanner,
@@ -449,26 +477,18 @@ export default {
const selectedLandingCat = ref(); const selectedLandingCat = ref();
const selectedLandingProduct = ref(); const selectedLandingProduct = ref();
const selectedLoc = ref(); const selectedLoc = ref();
const pannel = ref();
const pannel = ref('web');
const image = ref(); const image = ref();
const imagePreview = ref(); const imagePreview = ref();
const selectedBrandPage = ref(); const selectedBrandPage = ref();
const loading = ref(false); const loading = ref(false);
const errors = ref({}); const errors = ref({});

const categorySelectorLoader = ref(false); const categorySelectorLoader = ref(false);
const productSelectorLoader = ref(false); const productSelectorLoader = ref(false);
const categoryPageSelectorLoader = ref(false); const categoryPageSelectorLoader = ref(false);
const brandSelectorLoader = ref(false); const brandSelectorLoader = ref(false);

const formattedCategories = computed(() =>
Array.isArray(cats.value)
? cats.value.map((cat) => ({
value: cat.id,
label: cat.title,
}))
: []
);
const banner = ref(null)
const locale = ref('fa')


const handleSearch = async (searchTerm) => { const handleSearch = async (searchTerm) => {
if (searchTerm.length < 3) return; if (searchTerm.length < 3) return;
@@ -485,6 +505,15 @@ export default {
} }
}; };


const formattedCategories = computed(() =>
Array.isArray(cats.value)
? cats.value?.map((cat) => ({
value: cat?.id,
label: cat?.translation?.title || 'بدون عنوان',
}))
: []
);

const formattedProducts = computed(() => const formattedProducts = computed(() =>
Array.isArray(products.value) Array.isArray(products.value)
? products.value.map((product) => ({ ? products.value.map((product) => ({
@@ -512,8 +541,8 @@ export default {
const formattedCatPages = computed(() => const formattedCatPages = computed(() =>
Array.isArray(catPages.value) Array.isArray(catPages.value)
? catPages.value.map((catPage) => ({ ? catPages.value.map((catPage) => ({
value: catPage.id,
label: catPage.title,
value: catPage?.id,
label: catPage?.translation?.title ?? ''
})) }))
: [] : []
); );
@@ -537,7 +566,7 @@ export default {
Array.isArray(brands.value) Array.isArray(brands.value)
? brands.value.map((brand) => ({ ? brands.value.map((brand) => ({
value: brand.id, value: brand.id,
label: brand.title,
label: brand?.translation?.title ?? 'بدون عنوان',
})) }))
: [] : []
); );
@@ -592,11 +621,6 @@ export default {
if (!selectedLoc.value) if (!selectedLoc.value)
errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید"; errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید";


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

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

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


@@ -629,12 +653,12 @@ export default {
} }


title.value = data?.title; title.value = data?.title;
pannel.value = data?.panel;
imagePreview.value = data?.image;
imagePreview.value = data?.translation?.image;
selectedLoc.value = data?.location; selectedLoc.value = data?.location;
selectedLandingCat.value = data?.category_id; selectedLandingCat.value = data?.category_id;
selectedLandingProduct.value = data?.product_id; selectedLandingProduct.value = data?.product_id;
pageType.value = data.page_type; pageType.value = data.page_type;
banner.value = data;
if (data.page_id && pageType.value === "category") { if (data.page_id && pageType.value === "category") {
selectedCatPage.value = data?.page_id; selectedCatPage.value = data?.page_id;
} }
@@ -669,55 +693,58 @@ export default {
return; return;
} }
loading.value = true; loading.value = true;
const formData = new FormData();
formData.append("title", title.value);

const params = {}

params.title = title.value


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


if (pageType.value === "brand") { if (pageType.value === "brand") {
formData.append("page_id", selectedBrandPage.value);
params.page_id = selectedBrandPage.value
} }


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


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


if (selectedLoc.value === "A") { if (selectedLoc.value === "A") {
formData.append("type", pageType.value === 'blog_page' ? "banner" : "slider");
params.type = pageType.value === 'blog_page' ? "banner" : "slider"
} }


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


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

params.location = "A"
} }


formData.append("location", selectedLoc.value);
params.location = selectedLoc.value


formData.append("panel", pannel.value);
params.panel = pannel.value


if(pannel.value === 'web') if(pannel.value === 'web')
formData.append("page_type", pageType.value);
params.page_type = pageType.value


if (image.value) {
formData.append("image", image.value);
}
// if (image.value) {
// formData.append("image", image.value);
// }


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


ApiServiece.post(`admin/banners/${route.params.id}`, formData, {
params._method = 'put'

ApiServiece.post(`admin/banners/${route.params.id}`, params, {
headers: { headers: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`, Authorization: `Bearer ${localStorage.getItem("token")}`,
}, },
}) })
@@ -739,6 +766,103 @@ export default {
}); });
}; };


const handlerChangeLocale = (e) => {
const findLocale = banner.value?.translations?.find(item => item?.locale === e.target.value);

if (findLocale) {
image.value = findLocale?.image

imagePreview.value = findLocale?.image
} else {
image.value = undefined

imagePreview.value = undefined
}
}

const editTranslationBanner = async () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
autoClose: 1000,
});
return;
}

try {
loading.value = true;

const formData = new FormData();

if (image.value instanceof File) {
formData.append('image', image.value);
} else {
toast.error('لطفا تصویر جایگزین انتخاب کنید')

return;
}

formData.append('locale', locale.value);

const existingTranslation = banner.value?.translations?.find(
t => t.locale === locale.value
);

if (existingTranslation)
formData.append('_method', 'put')

const { data } = existingTranslation
? await ApiServiece.post(
`admin/banners/${banner.value?.id}/translations/${existingTranslation?.id}`,
formData,
{
headers: {
'content-type': 'multipart/form-data',
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
)
: await ApiServiece.post(
`admin/banners/${banner.value?.id}/translations`,
formData,
{
headers: {
'content-type': 'multipart/form-data',
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);

if (data?.success) {
// const updatedCategory = banner.value;
//
// if (existingTranslation) {
// updatedCategory.translations = data?.data?.translations || [];
// } else {
// updatedCategory.translations = [
// ...(updatedCategory.translations || []),
// ...(data?.data?.translations || [])
// ];
// }

banner.value = data?.data;

toast.success(data?.message)

//emit("cat-updated");
}

} catch (error) {
console.log(error,'error error');
toast.error(`${error?.response?.data?.message}`, {
position: "top-right",
autoClose: 1000,
});
} finally {
loading.value = false;
}
};

return { return {
cats, cats,
errors, errors,
@@ -771,6 +895,9 @@ export default {
brandSelectorLoader, brandSelectorLoader,
handleBrandSearch, handleBrandSearch,
selectedBrandPage, selectedBrandPage,
handlerChangeLocale,
locale,
editTranslationBanner
}; };
}, },
}; };


+ 10
- 6
src/views/live-preview/pages/blogCats/blogCat.vue Dosyayı Görüntüle

@@ -29,6 +29,7 @@ export default {
const cats = ref(); const cats = ref();
const catTitle = ref(); const catTitle = ref();
const catId = ref(); const catId = ref();
const catRow = ref();


const convertToJalali = (date) => { const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss") return moment(date, "YYYY-MM-DD HH:mm:ss")
@@ -151,11 +152,12 @@ export default {
}); });
}; };


const editModalData = (id, title, icon) => {
catId.value = id;
catTitle.value = title;
catIcon.value = icon;
};
const editModalData = (cat) => {
// catId.value = id;
// catTitle.value = title;
// catIcon.value = icon;
catRow.value = cat
}


watch(page, () => { watch(page, () => {
getCats(); getCats();
@@ -183,6 +185,7 @@ export default {
handlePageInput, handlePageInput,
searchPage, searchPage,
visiblePages, visiblePages,
catRow,
}; };
}, },
}; };
@@ -231,10 +234,10 @@ export default {


<td> <td>
<button <button
@click="editModalData(cat?.id, cat?.translation?.title)"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#editBlogCat" data-bs-target="#editBlogCat"
class="btn btn-sm btn-outline-warning me-1" class="btn btn-sm btn-outline-warning me-1"
@click="editModalData(cat)"
> >
ویرایش ویرایش
</button> </button>
@@ -261,6 +264,7 @@ export default {
:id="catId" :id="catId"
:title="catTitle" :title="catTitle"
:icon="catIcon" :icon="catIcon"
:catRow="catRow"
@cat-updated="handleCatUpdated()" @cat-updated="handleCatUpdated()"
/> />
<showDescription :desc="catDescription" /> <showDescription :desc="catDescription" />


+ 9
- 13
src/views/live-preview/pages/brands/brands.vue Dosyayı Görüntüle

@@ -30,7 +30,9 @@ export default {
const brandTitle = ref(); const brandTitle = ref();
const brandId = ref(); const brandId = ref();
const brandImage = ref(); const brandImage = ref();
const brandRow = ref()
let searchTimeout = null; let searchTimeout = null;

const convertToJalali = (date) => { const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss") return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa") .locale("fa")
@@ -103,7 +105,7 @@ export default {
}; };
const deleteBrand = (id, title) => { const deleteBrand = (id, title) => {
Swal.fire({ Swal.fire({
text: `می خواهید برند ${title} را حذف کنید؟`,
text: `می خواهید برند ${title ?? ''} را حذف کنید؟`,
icon: "warning", icon: "warning",
showCancelButton: true, showCancelButton: true,
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
@@ -131,11 +133,8 @@ export default {
}); });
}; };


const editModalData = (id, title, desc, img) => {
brandId.value = id;
brandTitle.value = title;
brandDescription.value = desc;
brandImage.value = img;
const editModalData = (brand) => {
brandRow.value = brand;
}; };


const descriptionModal = (desc) => { const descriptionModal = (desc) => {
@@ -194,6 +193,7 @@ export default {
page, page,
paginate, paginate,
handlePageInput, handlePageInput,
brandRow,
}; };
}, },
}; };
@@ -266,12 +266,7 @@ export default {
<td> <td>
<button <button
@click=" @click="
editModalData(
brand?.id,
brand?.title,
brand.description,
brand.image
)
editModalData(brand)
" "
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#editBrand" data-bs-target="#editBrand"
@@ -280,7 +275,7 @@ export default {
ویرایش ویرایش
</button> </button>
<button <button
@click="deleteBrand(brand.id, brand.title)"
@click="deleteBrand(brand.id, brand?.translation?.title)"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
> >
حذف حذف
@@ -303,6 +298,7 @@ export default {
:title="brandTitle" :title="brandTitle"
:description="brandDescription" :description="brandDescription"
:image="brandImage" :image="brandImage"
:brandRow="brandRow"
@brand-updated="handleBrandUpdated()" @brand-updated="handleBrandUpdated()"
/> />
<showDescription :desc="brandDescription" /> <showDescription :desc="brandDescription" />


+ 526
- 0
src/views/live-preview/pages/countries/countries.vue Dosyayı Görüntüle

@@ -0,0 +1,526 @@
<script>
import { debounce } from "lodash";
import Layout from "@/layout/custom.vue";
import { onMounted, ref, watch, computed } from "vue";
import Swal from "sweetalert2";
import ApiServiece from "@/services/ApiService";
import moment from "jalali-moment";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import CountriesModal from "@/components/modals/countries/CountriesModal.vue";
export default {
name: "SAMPLE-PAGE",
components: {
CountriesModal,
Layout,
},
setup() {
const selectedStatus = ref("");
const filterLoading = ref(false);
const searchPage = ref();
const currentPage = ref(1);
const totalPages = ref(1);
const paginate = ref(20);
const page = ref(1);
const searchQuery = ref("");
const countries = ref([]);
const userName = ref();
const userMobile = ref();
const userId = ref();
const userRole = ref();
const selectedRole = ref("");
const countryModalRef = ref(null);
const userProfile = JSON.parse(localStorage.getItem('user_profile'));

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

const getCountries = async () => {
try {
filterLoading.value = true;
const { data: { success, data } } = await ApiServiece.get('admin/country-configs', {
params: {
paginate: paginate.value,
page: currentPage.value,
}
})

if(success) {
countries.value = data?.data
currentPage.value = data?.current_page;
totalPages.value = data?.last_page;
}
} catch (e) {
toast.error(e?.response?.data?.message);
} finally {
filterLoading.value = false;
}
};

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

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

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) {
end += 1 - start;
start = 1;
}
if (end > totalPages.value) {
start -= end - totalPages.value;
end = totalPages.value;
}
start = Math.max(start, 1);

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

watch(selectedRole, () => {
getCountries();
});

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

watch(searchQuery, (newQuery) => {
debouncedSearch(newQuery);
});

const debouncedSearch = debounce(() => {
getCountries();
}, 1000);

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

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 modalData = (item) => {
Object.assign(countryModalRef.value.form, item)
};

const handleUserUpdated = () => {
getCountries();
};

const deleteUser = (id) => {
Swal.fire({
text: "آیا میخواهید این کاربر را حذف کنید؟",
icon: "warning",
showCancelButton: true,
confirmButtonText: "بله",
cancelButtonText: "خیر",
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
}).then((result) => {
if (result.isConfirmed) {
ApiServiece.delete(`admin/country-configs/${id}`)
.then((res) => {
toast.success(res?.data?.message , {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
getCountries();
})
.catch((err) => {
toast.error(err?.response?.data?.message, {
position: "top-right",
autoClose: 1000,
});
});
}
});
};

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

onMounted(() => {
getCountries();
});

return {
countries,
convertToJalali,
searchQuery,
modalData,
userName,
userMobile,
userId,
handleUserUpdated,
deleteUser,
unBlockUser,
userRole,
handlePageInput,
nextPage,
paginate,
totalPages,
currentPage,
prevPage,
visiblePages,
page,
searchPage,
selectedRole,
selectedStatus,
filterLoading,
userProfile,
getCountries,
countryModalRef
};
},
};
</script>

<template>
<Layout>
<BRow>
<div class="col-sm-12">
<div class="card table-card user-profile-list">
<div class="card-body">
<div class="filter-container">
<div class="search-filters d-flex align-items-center gap-3">
<!-- Search Input -->
<input
v-model="searchQuery"
type="text"
placeholder="جستجو..."
class="form-control form-control-sm d-inline-block me-2"
style="width: 250px; border-radius: 15px"
/>

<!-- User Role Selector -->
<select
class="form-select form-select-sm"
v-model="selectedRole"
style="width: 120px; border-radius: 15px"
>
<option value="" disabled selected>نقش کاربر</option>
<option value="">همه</option>
<option value="admin">فقط مدیران</option>
<option value="client">فقط مشتریان</option>
<option value="operator">فقط اپراتورها</option>
</select>

<select
class="form-select form-select-sm"
v-model="selectedStatus"
style="width: 120px; border-radius: 15px"
>
<option value="" disabled selected>وضعیت</option>
<option value="">همه</option>
<option value="0">فعال</option>
<option value="1">بلاک</option>
</select>
<!-- Add User Button -->
</div>
<button
v-if="userProfile?.role === 'admin'"
data-bs-toggle="modal"
data-bs-target="#countriesModal"
class="btn btn-add-user btn btn-light text-primary btn-sm px-3"
>
افزودن کشور
</button>
</div>

<div v-if="!filterLoading" class="table-responsive">
<table class="table table-hover" id="pc-dt-simple">
<thead>
<tr>
<th>پیش شماره</th>
<th>کشور</th>
<th>حداقل وزن حمل ونقل</th>
<th>هزینه هر کیلوگرم</th>
<th>وضعیت</th>
</tr>
</thead>
<tbody>
<tr v-for="country in countries" :key="country.id">
<td>
{{ country?.country_code }}
</td>
<td>
{{ country?.title }}
</td>
<td>
{{ country?.minimum_shipping_weight }}
</td>
<td>
{{ country?.per_kilograms_cost }}
</td>
<td>
<span v-if="!country?.deleted_at" class="badge bg-light-success">فعال</span>
<span v-else class="badge bg-light-danger">بلاک</span>
<div class="overlay-edit">
<ul class="list-inline mb-0">
<li class="list-inline-item m-0">
<a
@click="modalData(country)"
data-bs-toggle="modal"
data-bs-target="#countriesModal"
href="#"
class="avtar avtar-s btn btn-primary"
>
<i class="ti ti-pencil f-18"></i>
</a>
</li>
<li class="list-inline-item m-0">
<a
@click="deleteUser(country?.id)"
href="#"
class="avtar avtar-s btn bg-white btn-link-danger"
>
<i class="ti ti-trash f-18"></i>
</a>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div
v-else
class="filter-loader card table-card user-profile-list"
></div>
</div>
</div>
</div>
<CountriesModal ref="countryModalRef" @country-updated="getCountries" />
</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>

<!-- First page and leading dots -->
<li
v-if="visiblePages[0] > 1"
class="page-item"
@click="page = 1"
>
<a class="page-link" href="javascript:void(0)">1</a>
</li>
<li v-if="visiblePages[0] > 2" class="page-item disabled">
<span class="page-link">...</span>
</li>

<!-- Visible pages -->
<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>

<!-- Trailing dots and last page -->
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages - 1"
class="page-item disabled"
>
<span class="page-link">...</span>
</li>
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages"
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>
.filter-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px;
background-color: #fafafa;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.search-filters {
display: flex;
align-items: center;
}

.btn-add-user {
align-items: center;
padding: 10px 20px;
color: white;
font-size: 14px;
border-radius: 8px;
border: none;
transition: all 0.3s ease;
}

.btn-add-user i {
margin-right: 8px;
}

.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;
}
.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;
}
</style>

+ 3
- 3
src/views/live-preview/pages/discounts/addDiscount.vue Dosyayı Görüntüle

@@ -245,8 +245,8 @@ export default {
const formattedCategories = computed(() => const formattedCategories = computed(() =>
Array.isArray(categories.value) Array.isArray(categories.value)
? categories.value.map((category) => ({ ? categories.value.map((category) => ({
value: category.id,
label: category.title,
value: category?.id,
label: category?.translation?.title,
})) }))
: [] : []
); );
@@ -270,7 +270,7 @@ export default {
Array.isArray(products.value) Array.isArray(products.value)
? products.value.map((product) => ({ ? products.value.map((product) => ({
value: product.id, value: product.id,
label: product.title,
label: product?.translation?.title,
})) }))
: [] : []
); );


+ 14
- 11
src/views/live-preview/pages/discounts/editDiscount.vue Dosyayı Görüntüle

@@ -222,28 +222,28 @@ export default {


const formattedCategories = computed(() => const formattedCategories = computed(() =>
Array.isArray(categories.value) Array.isArray(categories.value)
? categories.value.map((category) => ({
value: category.id,
label: category.title,
? categories.value.filter(item => item?.translation).map((category) => ({
value: category?.id,
label: category?.translation?.title,
})) }))
: [] : []
); );


const formattedProducts = computed(() => const formattedProducts = computed(() =>
Array.isArray(products.value) Array.isArray(products.value)
? products.value.map((product) => ({
value: product.id,
label: product.title,
? products.value.filter(item => item?.translation).map((product) => ({
value: product?.id,
label: product?.translation?.title,
})) }))
: [] : []
); );


const handleSearch = async (searchTerm) => { const handleSearch = async (searchTerm) => {
if (searchTerm.length < 3) return;
if (searchTerm?.length < 3) return;
categorySelectorLoader.value = true; categorySelectorLoader.value = true;
try { try {
const response = await ApiServiece.get( const response = await ApiServiece.get(
`admin/categories?title=${searchTerm}`
`admin/categories?title=${searchTerm ?? ''}`
); );
categories.value = response.data.data; categories.value = response.data.data;
categorySelectorLoader.value = false; categorySelectorLoader.value = false;
@@ -254,11 +254,11 @@ export default {
}; };


const handleProductSearch = async (searchTerm) => { const handleProductSearch = async (searchTerm) => {
if (searchTerm.length < 3) return;
if (searchTerm?.length < 3) return;
productSelectorLoader.value = true; productSelectorLoader.value = true;
try { try {
const response = await ApiServiece.get( const response = await ApiServiece.get(
`admin/products?title=${searchTerm}`
`admin/products?title=${searchTerm ?? ''}`
); );
products.value = response.data.data; products.value = response.data.data;
productSelectorLoader.value = false; productSelectorLoader.value = false;
@@ -364,6 +364,9 @@ export default {


onMounted(() => { onMounted(() => {
getDiscount(); getDiscount();

handleProductSearch()
handleSearch()
}); });


const submitForm = () => { const submitForm = () => {
@@ -409,7 +412,7 @@ export default {
if (maxUsage.value) if (maxUsage.value)
formData.append("max_usage", maxUsage.value); formData.append("max_usage", maxUsage.value);


ApiServiece.post(`/admin/discounts`, formData)
ApiServiece.put(`/admin/discounts/${route.params.id || discount.value?.id}`, formData)
.then((resp) => { .then((resp) => {
loading.value = false; loading.value = false;
toast.success("!تخفیف با موفقیت ویرایش شد", { toast.success("!تخفیف با موفقیت ویرایش شد", {


+ 245
- 584
src/views/live-preview/pages/products/addProduct.vue
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 563
- 1399
src/views/live-preview/pages/products/editProduct.vue
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 15
- 54
src/views/live-preview/pages/products/products.vue Dosyayı Görüntüle

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


const restoreProduct = (id, title) => { const restoreProduct = (id, title) => {
Swal.fire({ Swal.fire({
text: `می خواهید محصول ${title} را بازیابی کنید؟`,
text: `می خواهید محصول ${title ?? ''} را بازیابی کنید؟`,
icon: "warning", icon: "warning",
showCancelButton: true, showCancelButton: true,
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
@@ -294,6 +294,7 @@ export default {
}, },
}; };
</script> </script>

<template> <template>
<Layout> <Layout>
<BRow> <BRow>
@@ -362,15 +363,11 @@ export default {
<tr> <tr>
<th>عکس</th> <th>عکس</th>
<th>عنوان</th> <th>عنوان</th>
<th>مدل</th>
<th>دسته بندی</th> <th>دسته بندی</th>
<th>برند</th> <th>برند</th>
<th>تعداد فروش</th> <th>تعداد فروش</th>
<th>برگزیده</th>
<th>تخفیف برگزیده</th>
<th>ویژه</th>
<th>قیمت عمده</th>
<th>قیمت تک</th>
<th>قیمت</th>
<th>وزن</th>
<th>وضعیت</th> <th>وضعیت</th>
<th>عملیات</th> <th>عملیات</th>
</tr> </tr>
@@ -386,58 +383,22 @@ export default {
</td> </td>
<td v-if="!product.image">ندارد</td> <td v-if="!product.image">ندارد</td>


<td>{{ product.title }}</td>
<td>{{ product?.translation?.title }}</td>


<td v-if="product.type == 1">تکی</td>
<td v-if="product.type == 2">عمده</td>
<td v-if="product.type == 3">تکی و عمده</td>


<td>{{ product?.category?.title }}</td>
<td>{{ product?.category?.translation?.title }}</td>


<td>{{ product?.brand?.title }}</td>
<td>{{ product?.brand?.translation?.title }}</td>

<td>{{ product?.sold_count }}</td>


<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">
%{{ formatWithCommas(product?.chosen_price) }}
</td>
<td v-if="!product.chosen_price">
<i
class="fas fa-times-circle status-icon unavailable"
></i>
</td>
<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>
</span>
{{ formatWithCommas(product.price) }}
</td> </td>


<td v-if="product?.wholesale_price">
{{ formatWithCommas(product.wholesale_price) }}
</td>
<td v-if="!product?.wholesale_price">ندارد</td>
<td v-if="product?.retail_price">
{{ formatWithCommas(product?.retail_price) }}
<td>
{{ product?.weight }}
</td> </td>
<td v-if="!product?.retail_price">ندارد</td>


<td v-if="!product.deleted_at"> <td v-if="!product.deleted_at">
<span class="badge bg-success text-white">فعال</span> <span class="badge bg-success text-white">فعال</span>
@@ -455,7 +416,7 @@ export default {
</router-link> </router-link>
<button <button
v-if="!product.deleted_at" v-if="!product.deleted_at"
@click="deleteProduct(product?.id, product?.title)"
@click="deleteProduct(product?.id, product?.translation?.title)"
class="btn btn-sm btn-outline-danger me-1" class="btn btn-sm btn-outline-danger me-1"
> >
حذف حذف
@@ -463,7 +424,7 @@ export default {


<button <button
v-else v-else
@click="restoreProduct(product?.id, product?.title)"
@click="restoreProduct(product?.id, product?.translation?.title)"
class="btn btn-sm btn-outline-success" class="btn btn-sm btn-outline-success"
> >
بازیابی بازیابی


+ 9
- 5
src/views/live-preview/pages/users/users.vue Dosyayı Görüntüle

@@ -30,6 +30,7 @@ export default {
const userMobile = ref(); const userMobile = ref();
const userId = ref(); const userId = ref();
const userRole = ref(); const userRole = ref();
const userCountryCode = ref();
const selectedRole = ref(""); const selectedRole = ref("");
const userProfile = JSON.parse(localStorage.getItem('user_profile')); const userProfile = JSON.parse(localStorage.getItem('user_profile'));


@@ -49,7 +50,6 @@ export default {
}&trashed=${selectedStatus.value || ""}` }&trashed=${selectedStatus.value || ""}`
) )
.then((resp) => { .then((resp) => {
console.log(resp.data.data);
users.value = resp.data.data.data; users.value = resp.data.data.data;
currentPage.value = resp.data.data.current_page; currentPage.value = resp.data.data.current_page;
totalPages.value = resp.data.data.last_page; totalPages.value = resp.data.data.last_page;
@@ -113,8 +113,7 @@ export default {
debouncedSearch(newQuery); debouncedSearch(newQuery);
}); });


const debouncedSearch = debounce((query) => {
console.log("Searching for:", query);
const debouncedSearch = debounce(() => {
getUsers(); getUsers();
}, 1000); }, 1000);


@@ -134,12 +133,14 @@ export default {
} }
} }


const modalData = (id, name, mobile, role) => {
const modalData = (id, name, mobile, role, config_id) => {
userName.value = name; userName.value = name;
userMobile.value = mobile; userMobile.value = mobile;
userId.value = id; userId.value = id;
userRole.value = role; userRole.value = role;
userCountryCode.value = config_id;
}; };

const handleUserUpdated = () => { const handleUserUpdated = () => {
getUsers(); getUsers();
}; };
@@ -237,6 +238,7 @@ export default {
selectedStatus, selectedStatus,
filterLoading, filterLoading,
userProfile, userProfile,
userCountryCode,
}; };
}, },
}; };
@@ -356,7 +358,8 @@ export default {
user.id, user.id,
user.name, user.name,
user.mobile, user.mobile,
user.role
user.role,
user?.country_config_id
) )
" "
data-bs-toggle="modal" data-bs-toggle="modal"
@@ -405,6 +408,7 @@ export default {
:mobile="userMobile" :mobile="userMobile"
:id="userId" :id="userId"
:role="userRole" :role="userRole"
:countryCode="userCountryCode"
/> />
<addUser @user-updated="handleUserUpdated" /> <addUser @user-updated="handleUserUpdated" />
</BRow> </BRow>


Yükleniyor…
İptal
Kaydet