| @@ -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/ | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -264,6 +264,16 @@ export default { | |||
| <span class="pc-mtext">کاربران</span></router-link | |||
| > | |||
| </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 | |||
| class="pc-item" | |||
| :class="{ | |||
| @@ -280,6 +290,7 @@ export default { | |||
| <span class="pc-mtext">بنر ها</span></router-link | |||
| > | |||
| </li> | |||
| <li class="pc-item" :class="{ active: this.$route.path === '/brands' }"> | |||
| <router-link to="/brands" class="pc-link"> | |||
| <span class="pc-micon"> | |||
| @@ -300,17 +311,17 @@ export default { | |||
| <span class="pc-mtext">ویژگی ها</span></router-link | |||
| > | |||
| </li> | |||
| <li | |||
| class="pc-item" | |||
| :class="{ active: this.$route.path === '/idenities' }" | |||
| > | |||
| <router-link to="/idenities" class="pc-link"> | |||
| <span class="pc-micon"> | |||
| <i class="ph-duotone ph-barcode"></i> | |||
| </span> | |||
| <span class="pc-mtext">مشخصات</span></router-link | |||
| > | |||
| </li> | |||
| <!-- <li--> | |||
| <!-- class="pc-item"--> | |||
| <!-- :class="{ active: this.$route.path === '/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="{ | |||
| @@ -327,6 +338,7 @@ export default { | |||
| <span class="pc-mtext">بلاگ ها</span></router-link | |||
| > | |||
| </li> | |||
| <li | |||
| class="pc-item" | |||
| :class="{ | |||
| @@ -343,6 +355,7 @@ export default { | |||
| <span class="pc-mtext">محصولات</span></router-link | |||
| > | |||
| </li> | |||
| <li | |||
| class="pc-item" | |||
| :class="{ | |||
| @@ -360,50 +373,47 @@ export default { | |||
| > | |||
| </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: | |||
| this.$route.path === '/orders' || | |||
| 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 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"> | |||
| <BLink | |||
| @@ -455,6 +465,7 @@ export default { | |||
| <span class="pc-mtext">نظرات</span></router-link | |||
| > | |||
| </li> | |||
| <li | |||
| class="pc-item" | |||
| :class="{ | |||
| @@ -469,6 +480,7 @@ export default { | |||
| <span class="pc-mtext">پرسش و پاسخ</span></router-link | |||
| > | |||
| </li> | |||
| <li class="pc-item pc-hasmenu"> | |||
| <BLink | |||
| class="pc-link" | |||
| @@ -21,105 +21,166 @@ | |||
| ></button> | |||
| </div> | |||
| <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 --> | |||
| <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" | |||
| accept="image/*" | |||
| @change="handleImageChange" | |||
| class="form-control" | |||
| :class="{ 'is-invalid': errors.image }" | |||
| /> | |||
| /> | |||
| <div v-if="imagePreview" class="mt-2"> | |||
| <img | |||
| <div v-if="imagePreview" class="mt-2"> | |||
| <img | |||
| :src="imagePreview" | |||
| alt="Image Preview" | |||
| class="img-fluid rounded shadow-sm Image-Preview" | |||
| /> | |||
| <button | |||
| /> | |||
| <button | |||
| type="button" | |||
| @click="removeImage()" | |||
| 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> | |||
| </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 | |||
| 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> | |||
| ذخیره | |||
| </button> | |||
| </div> | |||
| </form> | |||
| ذخیره | |||
| </button> | |||
| </div> | |||
| </form> | |||
| </template> | |||
| </Steppy> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -131,13 +192,19 @@ import { ref } from "vue"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import {Steppy} from "vue3-steppy"; | |||
| export default { | |||
| components: {Steppy}, | |||
| setup(props, { emit }) { | |||
| const image = ref(null); | |||
| const imagePreview = ref(null); | |||
| const description = ref(); | |||
| const title = ref(); | |||
| const loadingStep = ref(false); | |||
| const step = ref(1); | |||
| const brandId = ref(null); | |||
| const locale = ref('fa'); | |||
| const errors = ref({}); | |||
| const loading = ref(false); | |||
| @@ -150,8 +217,6 @@ export default { | |||
| if (fileInput) { | |||
| fileInput.value = ""; | |||
| } | |||
| console.log(image.value); | |||
| }; | |||
| const handleImageChange = (event) => { | |||
| const file = event.target.files[0]; | |||
| @@ -204,14 +269,14 @@ export default { | |||
| } | |||
| 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: { | |||
| "content-type": "multipart", | |||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||
| }, | |||
| }) | |||
| @@ -224,11 +289,9 @@ export default { | |||
| }) | |||
| .then(() => { | |||
| setTimeout(() => { | |||
| document.getElementById("close").click(); | |||
| emit("brand-updated"); | |||
| title.value = ""; | |||
| description.value = ""; | |||
| image.value = null; | |||
| imagePreview.value = null; | |||
| }, 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 { | |||
| errors, | |||
| loading, | |||
| @@ -255,6 +356,11 @@ export default { | |||
| removeImage, | |||
| title, | |||
| description, | |||
| loadingStep, | |||
| step, | |||
| handlerAddBrand, | |||
| brandId, | |||
| locale | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -20,99 +20,157 @@ | |||
| ></button> | |||
| </div> | |||
| <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" | |||
| @input="clearError('localImage')" | |||
| accept="image/*" | |||
| @change="handleImageChange" | |||
| class="form-control" | |||
| :class="{ 'is-invalid': errors.localImage }" | |||
| /> | |||
| /> | |||
| <div v-if="imagePreview" class="mt-2"> | |||
| <img | |||
| <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.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> | |||
| </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> | |||
| @@ -120,13 +178,15 @@ | |||
| </template> | |||
| <script> | |||
| import { ref, toRef, watch } from "vue"; | |||
| import {computed, ref, toRef, watch} from "vue"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import {BTabs} from "bootstrap-vue-next"; | |||
| export default { | |||
| components: {BTabs}, | |||
| props: { | |||
| id: { | |||
| type: String, | |||
| @@ -144,6 +204,10 @@ export default { | |||
| type: String, | |||
| required: true, | |||
| }, | |||
| brandRow: { | |||
| type: Object, | |||
| required: true, | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| @@ -152,9 +216,50 @@ export default { | |||
| const localDesc = toRef(props.description); | |||
| const localImage = toRef(props.image); | |||
| const image = ref(null); | |||
| const localId = toRef(props.id); | |||
| const errors = ref({}); | |||
| 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 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 = () => { | |||
| errors.value = {}; | |||
| if (!localTitle.value) | |||
| @@ -215,7 +299,7 @@ export default { | |||
| errors.value[field] = ""; | |||
| }; | |||
| const editBrand = () => { | |||
| const editBrand = async () => { | |||
| if (!validateForm()) { | |||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | |||
| position: "top-right", | |||
| @@ -223,47 +307,140 @@ export default { | |||
| }); | |||
| 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(() => { | |||
| 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 { | |||
| errors, | |||
| loading, | |||
| @@ -274,6 +451,13 @@ export default { | |||
| localImage, | |||
| handleImageChange, | |||
| imagePreview, | |||
| locale, | |||
| loadingImage, | |||
| editImageBrand, | |||
| findLocaleTranslation, | |||
| handlerRemoveTranslation, | |||
| loadingDelete, | |||
| handlerChangeLocale | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -103,6 +103,8 @@ | |||
| }}</small> | |||
| </div> | |||
| </BCol> | |||
| <ConfigCountriesSelect v-model:country-code="selectedCountryCode"/> | |||
| </BRow> | |||
| <!-- Submit Buttons --> | |||
| @@ -137,20 +139,22 @@ | |||
| <script> | |||
| import { ref } from "vue"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import ConfigCountriesSelect from "@/components/ConfigCountriesSelect.vue"; | |||
| export default { | |||
| components: {ConfigCountriesSelect}, | |||
| setup(props, { emit }) { | |||
| const name = ref(); | |||
| const mobile = ref(); | |||
| const password = ref(); | |||
| const repeatPassword = ref(); | |||
| const role = ref(); | |||
| const errors = ref({}); | |||
| const loading = ref(false); | |||
| const selectedCountryCode = ref(''); | |||
| const validateForm = () => { | |||
| errors.value = {}; | |||
| @@ -165,7 +169,7 @@ export default { | |||
| errors.value.password = "رمز عبور باید حداقل 8 کاراکتر باشد"; | |||
| } else if (repeatPassword.value.length < 8) { | |||
| errors.value.repeatPassword = | |||
| "تکرار رمز عبور باید حداقل 8 کاراکتر باشد"; | |||
| "تکرار رمز عبور باید حداقل 8 کاراکتر باشد"; | |||
| } | |||
| return Object.keys(errors.value).length === 0; | |||
| }; | |||
| @@ -187,38 +191,40 @@ export default { | |||
| const formData = new FormData(); | |||
| formData.append("mobile", mobile.value); | |||
| if (name.value) | |||
| formData.append("name", name.value); | |||
| formData.append("name", name.value); | |||
| formData.append("role", role.value); | |||
| formData.append("password", password.value); | |||
| formData.append("password_confirmation", repeatPassword.value); | |||
| formData.append("country_config_id", selectedCountryCode.value); | |||
| 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 { | |||
| @@ -231,6 +237,7 @@ export default { | |||
| password, | |||
| repeatPassword, | |||
| role, | |||
| selectedCountryCode, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -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> | |||
| @@ -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>--> | |||
| <!-- <!– 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 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> | |||
| @@ -11,7 +11,7 @@ | |||
| <div class="modal-content"> | |||
| <div class="modal-header"> | |||
| <h5 class="modal-title" id="exampleModalLabel"> | |||
| اضافه کردن رنگ جدید | |||
| اضافه کردن ویژگی جدید | |||
| </h5> | |||
| <button | |||
| type="button" | |||
| @@ -21,77 +21,139 @@ | |||
| ></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" | |||
| <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" | |||
| 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> | |||
| </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> | |||
| @@ -103,8 +165,10 @@ import { ref, toRef, watch } from "vue"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import {Steppy} from "vue3-steppy"; | |||
| export default { | |||
| components: {Steppy}, | |||
| props: { | |||
| attributeValues: { | |||
| type: Array, | |||
| @@ -112,11 +176,17 @@ export default { | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| const colorName = ref(); | |||
| const attrName = ref(); | |||
| const colorCode = ref(); | |||
| const localAttributeValues = toRef(props.attributeValues); | |||
| const errors = ref({}); | |||
| 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( | |||
| () => props.attributeValues, | |||
| @@ -125,9 +195,9 @@ export default { | |||
| const validateForm = () => { | |||
| 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; | |||
| }; | |||
| @@ -136,7 +206,7 @@ export default { | |||
| errors.value[field] = ""; | |||
| }; | |||
| const addAttribute = () => { | |||
| const handlerSubmitCategory = () => { | |||
| if (!validateForm()) { | |||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | |||
| position: "top-right", | |||
| @@ -144,30 +214,21 @@ export default { | |||
| }); | |||
| return; | |||
| } | |||
| 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", | |||
| 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) => { | |||
| 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 { | |||
| errors, | |||
| loading, | |||
| clearError, | |||
| addAttribute, | |||
| colorName, | |||
| handlerSubmitCategory, | |||
| attrName, | |||
| colorCode, | |||
| getAttributes, | |||
| categories, | |||
| loadingAttr, | |||
| categoryId, | |||
| step, | |||
| locale, | |||
| addAttribute | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -19,77 +19,140 @@ | |||
| ></button> | |||
| </div> | |||
| <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" | |||
| 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> | |||
| </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> | |||
| @@ -97,13 +160,15 @@ | |||
| </template> | |||
| <script> | |||
| import { ref, toRef, watch } from "vue"; | |||
| import {computed, ref, watch} from "vue"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import {BTabs} from "bootstrap-vue-next"; | |||
| export default { | |||
| components: {BTabs}, | |||
| props: { | |||
| attributeValues: { | |||
| type: Array, | |||
| @@ -121,41 +186,64 @@ export default { | |||
| type: String, | |||
| Required: true, | |||
| }, | |||
| attrRow: { | |||
| type: Object, | |||
| required: true, | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| const localColorName = ref(); | |||
| const localTitle = ref(); | |||
| const localColorCode = ref(); | |||
| const localId = toRef(props.id); | |||
| const localAttributeValues = toRef(props.attributeValues); | |||
| const errors = ref({}); | |||
| 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( | |||
| () => 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 = () => { | |||
| 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; | |||
| }; | |||
| @@ -164,7 +252,7 @@ export default { | |||
| errors.value[field] = ""; | |||
| }; | |||
| const editAttribute = () => { | |||
| const editAttribute = async () => { | |||
| if (!validateForm()) { | |||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | |||
| position: "top-right", | |||
| @@ -172,47 +260,145 @@ export default { | |||
| }); | |||
| 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 { | |||
| errors, | |||
| loading, | |||
| clearError, | |||
| editAttribute, | |||
| localColorName, | |||
| localTitle, | |||
| localColorCode, | |||
| categoryId, | |||
| getAttributes, | |||
| categories, | |||
| attrRowModel, | |||
| handlerChangeLocale, | |||
| locale, | |||
| findLocaleTranslation, | |||
| handlerRemoveTranslation, | |||
| loadingDelete, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -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> | |||
| @@ -82,7 +82,6 @@ | |||
| v-model="locale" | |||
| class="form-control" | |||
| placeholder="انتخاب کنید" | |||
| @change="handlerChangeLocale" | |||
| > | |||
| <option | |||
| key="fa" | |||
| @@ -137,6 +136,7 @@ | |||
| > | |||
| بستن | |||
| </button> | |||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||
| <span | |||
| v-if="loading" | |||
| @@ -201,7 +201,7 @@ export default { | |||
| const { data: { message, success, data } } = await ApiServiece.post( | |||
| `admin/blog-categories`, | |||
| { title: title.value }, | |||
| { title: titleCat.value }, | |||
| { | |||
| headers: { | |||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||
| @@ -246,10 +246,10 @@ export default { | |||
| }) | |||
| .then(() => { | |||
| setTimeout(() => { | |||
| document.getElementById("closeAddBlogCat").click(); | |||
| //document.getElementById("closeAddBlogCat").click(); | |||
| emit("cat-updated"); | |||
| title.value = ""; | |||
| step.value = 1; | |||
| //step.value = 1; | |||
| }, 500); | |||
| }) | |||
| .catch((error) => { | |||
| @@ -20,7 +20,6 @@ | |||
| </div> | |||
| <div class="modal-body"> | |||
| <form @submit.prevent="editCat" class="mt-4"> | |||
| <BButton | |||
| :disabled="!findLocaleTranslation" | |||
| :loading="loadingDelete" | |||
| @@ -67,7 +66,7 @@ | |||
| <div class="form-group"> | |||
| <label class="form-label">عنوان دسته</label> | |||
| <input | |||
| v-model="title" | |||
| v-model="titleCat" | |||
| @input="clearError('title')" | |||
| type="text" | |||
| class="form-control" | |||
| @@ -112,8 +111,8 @@ | |||
| </template> | |||
| <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 "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| @@ -132,30 +131,31 @@ export default { | |||
| type: String, | |||
| Required: true, | |||
| }, | |||
| catRow: { | |||
| type: Object, | |||
| Required: true, | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| const localTitle = toRef(props.title); | |||
| const localIcon = ref(props.icon); | |||
| const localId = toRef(props.id); | |||
| const errors = ref({}); | |||
| const loading = ref(false); | |||
| const loadingDelete = ref(false); | |||
| 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( | |||
| () => 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) => { | |||
| errors.value[field] = ""; | |||
| @@ -163,17 +163,12 @@ export default { | |||
| const validateForm = () => { | |||
| errors.value = {}; | |||
| if (!localTitle.value) | |||
| if (!titleCat.value) | |||
| errors.value.localTitle = "وارد کردن عنوان ضروری می باشد"; | |||
| if (!localIcon.value) errors.value.icon = "انتخاب آیکن ضروری است"; | |||
| return Object.keys(errors.value).length === 0; | |||
| }; | |||
| const setSelectedIcon = (icon) => { | |||
| localIcon.value = icon; | |||
| }; | |||
| const editCat = () => { | |||
| const editCat = async () => { | |||
| if (!validateForm()) { | |||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | |||
| position: "top-right", | |||
| @@ -181,48 +176,128 @@ export default { | |||
| }); | |||
| 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 { | |||
| errors, | |||
| loading, | |||
| clearError, | |||
| editCat, | |||
| localTitle, | |||
| iconData, | |||
| setSelectedIcon, | |||
| localIcon, | |||
| localId, | |||
| title, | |||
| titleCat, | |||
| locale, | |||
| handlerChangeLocale, | |||
| findLocaleTranslation, | |||
| handlerRemoveTranslation, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -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> | |||
| @@ -84,6 +84,8 @@ | |||
| }}</small> | |||
| </div> | |||
| </BCol> | |||
| <ConfigCountriesSelect v-model:country-code="localCountryCode"/> | |||
| </BRow> | |||
| <div | |||
| @@ -120,8 +122,10 @@ import ApiServiece from "@/services/ApiService"; | |||
| import { ref, toRef, watch } from "vue"; | |||
| import { toast } from "vue3-toastify"; | |||
| import "vue3-toastify/dist/index.css"; | |||
| import ConfigCountriesSelect from "@/components/ConfigCountriesSelect.vue"; | |||
| export default { | |||
| components: {ConfigCountriesSelect}, | |||
| props: { | |||
| name: { | |||
| type: String, | |||
| @@ -139,6 +143,10 @@ export default { | |||
| type: String, | |||
| required: true, | |||
| }, | |||
| countryCode: { | |||
| type: Number, | |||
| required: true, | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| @@ -146,6 +154,7 @@ export default { | |||
| const localName = toRef(props.name); | |||
| const localMobile = toRef(props.mobile); | |||
| const localRole = toRef(props.role); | |||
| const localCountryCode = toRef(props.countryCode); | |||
| const localId = toRef(props.id); | |||
| const errors = ref({}); | |||
| const loading = ref(false); | |||
| @@ -168,12 +177,22 @@ export default { | |||
| (newVal) => (localRole.value = newVal) | |||
| ); | |||
| watch( | |||
| () => props.countryCode, | |||
| (newVal) => { | |||
| localCountryCode.value = Number(newVal); | |||
| } | |||
| ); | |||
| const validateForm = () => { | |||
| errors.value = {}; | |||
| if (!localMobile.value) | |||
| errors.value.localMobile = "وارد کردن موبایل ضروری می باشد"; | |||
| if (!localRole.value) | |||
| errors.value.localMobile = "انتخاب کردن نقش کاربر ضروری می باشد"; | |||
| if (!localCountryCode.value) | |||
| errors.value.localCountryCode = "انتخاب کردن پیش شماره ضروری می باشد"; | |||
| if (password.value && password.value.length < 8) { | |||
| errors.value.password = " رمز عبور باید حداقل 8 کاراکتر باشد"; | |||
| } | |||
| @@ -202,12 +221,13 @@ export default { | |||
| if (password.value) { | |||
| formData.append("password", password.value); | |||
| } | |||
| formData.append("country_config_id", localCountryCode.value) | |||
| ApiServiece.put(`admin/users/${localId.value}`, formData) | |||
| .then(() => { | |||
| toast.success("!کاربر با موفقیت ویرایش شد", { | |||
| position: "top-right", | |||
| autoClose: 1000, | |||
| onClose: () => emit("user-updated"), | |||
| }); | |||
| }) | |||
| @@ -238,6 +258,7 @@ export default { | |||
| password, | |||
| editUser, | |||
| localRole, | |||
| localCountryCode, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -23,6 +23,15 @@ export default [ | |||
| }, | |||
| 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", | |||
| name: "brands", | |||
| @@ -51,6 +60,15 @@ export default [ | |||
| component: () => | |||
| 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", | |||
| name: "blogCat", | |||
| @@ -290,9 +308,7 @@ export default [ | |||
| }, | |||
| component: () => import("../views/live-preview/pages/settings/setting.vue"), | |||
| }, | |||
| // Auth | |||
| { | |||
| path: "/otpLogin", | |||
| name: "otpLogin", | |||
| @@ -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> | |||
| @@ -8,6 +8,7 @@ import "vue3-toastify/dist/index.css"; | |||
| import Swal from "sweetalert2"; | |||
| import addAttribute from "@/components/modals/attribute/addAttribute.vue"; | |||
| import editAttribute from "@/components/modals/attribute/editAttribute.vue"; | |||
| import router from "@/router"; | |||
| export default { | |||
| name: "BORDER", | |||
| components: { | |||
| @@ -28,6 +29,8 @@ export default { | |||
| const attributeTitle = ref(); | |||
| const attributeId = ref(); | |||
| const attributeCode = ref(); | |||
| const attrRow = ref(null) | |||
| let searchTimeout = null; | |||
| const convertToJalali = (date) => { | |||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | |||
| @@ -48,7 +51,7 @@ export default { | |||
| const getAttributes = () => { | |||
| filterLoading.value = true; | |||
| ApiServiece.get( | |||
| `admin/attribute-values?attribute_id=1&title=${encodeURIComponent( | |||
| `admin/attributes?attribute_id=1&title=${encodeURIComponent( | |||
| searchQuery.value || "" | |||
| )}&code=${encodeURIComponent(searchQuery.value || "")} | |||
| &paginate=${paginate.value || 10}&page=${page.value || 1} | |||
| @@ -56,11 +59,9 @@ export default { | |||
| ) | |||
| .then((resp) => { | |||
| filterLoading.value = false; | |||
| console.log(resp.data); | |||
| attributes.value = resp.data.data.data; | |||
| currentPage.value = resp.data.data.current_page; | |||
| totalPages.value = resp.data.data.last_page; | |||
| console.log(attributes.value); | |||
| }) | |||
| .catch(() => { | |||
| filterLoading.value = false; | |||
| @@ -123,13 +124,14 @@ export default { | |||
| } | |||
| return pages; | |||
| }); | |||
| watch(page, () => { | |||
| getAttributes(); | |||
| }); | |||
| const deleteAttribute = (id, title) => { | |||
| Swal.fire({ | |||
| text: `می خواهید رنگ ${title} را حذف کنید ؟`, | |||
| text: `می خواهید رنگ ${title ?? ''} را حذف کنید ؟`, | |||
| icon: "warning", | |||
| showCancelButton: true, | |||
| confirmButtonColor: "#3085d6", | |||
| @@ -138,7 +140,7 @@ export default { | |||
| cancelButtonText: "خیر", | |||
| }).then((result) => { | |||
| if (result.isConfirmed) { | |||
| ApiServiece.delete(`admin/attribute-values/${id}`) | |||
| ApiServiece.delete(`admin/attributes/${id}`) | |||
| .then(() => { | |||
| toast.success("!ویژگی با موفقیت حذف شد", { | |||
| 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 = () => { | |||
| ApiServiece.get(`admin/attributes`).then((resp) => { | |||
| @@ -174,10 +172,15 @@ export default { | |||
| }); | |||
| }; | |||
| const redirectToAttrValue = id => { | |||
| router.push(`/attributes-value/${id}`) | |||
| } | |||
| onMounted(() => { | |||
| getAttributes(); | |||
| getAttributeValues(); | |||
| }); | |||
| return { | |||
| attributes, | |||
| convertToJalali, | |||
| @@ -199,10 +202,13 @@ export default { | |||
| nextPage, | |||
| handlePageInput, | |||
| visiblePages, | |||
| attrRow, | |||
| redirectToAttrValue | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <template> | |||
| <Layout> | |||
| <BRow> | |||
| @@ -227,7 +233,7 @@ export default { | |||
| data-bs-target="#addAttribute" | |||
| class="btn btn-light text-primary btn-sm px-3" | |||
| > | |||
| افزودن رنگ | |||
| افزودن ویژگی | |||
| </button> | |||
| </div> | |||
| <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"> | |||
| <thead class="table-light"> | |||
| <tr> | |||
| <th>نام</th> | |||
| <th>رنگ</th> | |||
| <th>کد رنگ</th> | |||
| <th>تاریخ ایجاد</th> | |||
| <th>نام</th> | |||
| <th>دسته بندی</th> | |||
| <th>عملیات</th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| <tr v-for="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>{{ attribute?.translation?.title }}</td> | |||
| <td>{{ attribute?.category?.translation?.title }}</td> | |||
| <td> | |||
| <!-- <button | |||
| <button | |||
| @click=" | |||
| editModalData( | |||
| attribute?.id, | |||
| attribute?.title, | |||
| attribute.code | |||
| ) | |||
| editModalData(attribute) | |||
| " | |||
| data-bs-toggle="modal" | |||
| data-bs-target="#editAttribute" | |||
| class="btn btn-sm btn-outline-warning me-1" | |||
| > | |||
| ویرایش | |||
| </button> --> | |||
| </button> | |||
| <button | |||
| @click="deleteAttribute(attribute.id, attribute.title)" | |||
| @click="deleteAttribute(attribute?.id, attribute?.translation?.title)" | |||
| class="btn btn-sm btn-outline-danger" | |||
| > | |||
| حذف | |||
| </button> | |||
| <button | |||
| @click="redirectToAttrValue(attribute?.id)" | |||
| class="btn btn-sm btn-outline-primary me-1" | |||
| > | |||
| مقدار ویژگی ها | |||
| </button> | |||
| </td> | |||
| </tr> | |||
| </tbody> | |||
| @@ -296,6 +296,7 @@ export default { | |||
| :title="attributeTitle" | |||
| :code="attributeCode" | |||
| :attributeValues="attributeValues" | |||
| :attrRow="attrRow" | |||
| @attribute-updated="handleAttributeUpdated()" | |||
| /> | |||
| </BRow> | |||
| @@ -58,339 +58,395 @@ | |||
| </div> | |||
| </BCardHeader> | |||
| <BCardBody> | |||
| <BRow class="g-3"> | |||
| <BCol md="6"> | |||
| <div class="form-group"> | |||
| <label class="form-label">عنوان</label> | |||
| <input | |||
| type="text" | |||
| v-model="title" | |||
| class="form-control" | |||
| placeholder="عنوان بنر" | |||
| :class="{ 'is-invalid': errors.title }" | |||
| @input="clearError('title')" | |||
| /> | |||
| </div> | |||
| <small v-if="errors.title" class="text-danger"> | |||
| {{ errors.title }} | |||
| </small> | |||
| </BCol> | |||
| <BCol md="6"> | |||
| <div class="form-group"> | |||
| <label class="form-label">پنل نمایش</label> | |||
| <select | |||
| class="form-select" | |||
| aria-label="Default select example" | |||
| v-model="pannel" | |||
| @change="clearError('pannel')" | |||
| :class="{ 'is-invalid': errors.pannel }" | |||
| placeholder="انتخاب پنل" | |||
| > | |||
| <option value="wholesale">پنل عمده فروشی</option> | |||
| <option value="web">وب سایت و اپلیکیشن</option> | |||
| </select> | |||
| </div> | |||
| <small v-if="errors.pannel" class="text-danger"> | |||
| {{ errors.pannel }} | |||
| </small> | |||
| </BCol> | |||
| <BCol v-if="pannel === 'web'" md="6"> | |||
| <div class="form-group"> | |||
| <label class="form-label">نمایش در</label> | |||
| <select | |||
| v-model="pageType" | |||
| :class="{ 'is-invalid': errors.pageType }" | |||
| class="form-select" | |||
| aria-label="Default select example" | |||
| placeholder="انتخاب صفحه" | |||
| @change="clearError('pageType')" | |||
| > | |||
| <option value="main_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-border-radius: 8px; | |||
| 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 | |||
| 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> بارگذاری... | |||
| </span> | |||
| <span v-else>ایجاد</span> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </BCardFooter> | |||
| </template> | |||
| </Steppy> | |||
| </BCardBody> | |||
| </BCard> | |||
| </BCol> | |||
| </BRow> | |||
| @@ -413,10 +469,12 @@ import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import { ref, computed } from "vue"; | |||
| import Layout from "@/layout/custom.vue"; | |||
| import {Steppy} from "vue3-steppy"; | |||
| export default { | |||
| name: "SAMPLE-PAGE", | |||
| components: { | |||
| Steppy, | |||
| Layout, | |||
| mainPageBanner, | |||
| catBanner, | |||
| @@ -437,7 +495,7 @@ export default { | |||
| const selectedLandingCat = ref(); | |||
| const selectedLandingProduct = ref(); | |||
| const selectedLoc = ref(); | |||
| const pannel = ref(); | |||
| const pannel = ref('web'); | |||
| const image = ref(); | |||
| const imagePreview = ref(); | |||
| const productSelectorLoader = ref(false); | |||
| @@ -446,6 +504,10 @@ export default { | |||
| const brandSelectorLoader = ref(false); | |||
| const loading = ref(false); | |||
| const errors = ref({}); | |||
| const step = ref(1); | |||
| const locale = ref('fa'); | |||
| const loadingFinally = ref(false); | |||
| const bannerId = ref(null); | |||
| const handleSearch = async (searchTerm) => { | |||
| if (searchTerm.length < 3) return; | |||
| @@ -465,10 +527,10 @@ export default { | |||
| }; | |||
| const formattedBrands = computed(() => | |||
| Array.isArray(brands.value) // ✅ Check if products.value is an array | |||
| Array.isArray(brands.value) | |||
| ? brands.value.map((brand) => ({ | |||
| value: brand.id, | |||
| label: brand.title, | |||
| label: brand?.translation?.title, | |||
| })) | |||
| : [] | |||
| ); | |||
| @@ -490,10 +552,10 @@ export default { | |||
| }; | |||
| const formattedProducts = computed(() => | |||
| Array.isArray(products.value) // ✅ Check if products.value is an array | |||
| Array.isArray(products.value) | |||
| ? 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(() => | |||
| 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(() => | |||
| Array.isArray(categoryPages.value) | |||
| ? 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 (!image.value) errors.value.image = "عکس بنر را وارد نمایید"; | |||
| // if (!image.value) errors.value.image = "عکس بنر را وارد نمایید"; | |||
| return Object.keys(errors.value).length === 0; | |||
| }; | |||
| @@ -611,65 +673,72 @@ export default { | |||
| return; | |||
| } | |||
| loading.value = true; | |||
| const formData = new FormData(); | |||
| formData.append("title", title.value); | |||
| const params = {} | |||
| params.title = title.value; | |||
| if (pageType.value === "category") { | |||
| formData.append("page_id", selectedCatPage.value); | |||
| params.page_id = selectedCatPage.value | |||
| } | |||
| 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") { | |||
| formData.append("type", pageType.value === 'blog_page' ? "banner" :"slider"); | |||
| params.type = pageType.value === 'blog_page' ? "banner" :"slider" | |||
| } | |||
| if (selectedLoc.value !== "A") { | |||
| formData.append("type", "banner"); | |||
| params.type = "banner" | |||
| } | |||
| if (pannel.value === "wholesale") { | |||
| formData.append("type", "slider"); | |||
| formData.append("location", "A"); | |||
| params.type = "slider" | |||
| params.location = "A" | |||
| } | |||
| 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") { | |||
| formData.append("page_type", "main_page"); | |||
| params.page_type = 'main_page' | |||
| } | |||
| 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: { | |||
| "content-type": "multipart", | |||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||
| }, | |||
| }) | |||
| .then((resp) => { | |||
| toast.success("!بنر با موفقیت اضافه شد", { | |||
| .then(({ data }) => { | |||
| toast.success(data?.message, { | |||
| position: "top-right", | |||
| autoClose: 1000, | |||
| }); | |||
| console.log(resp); | |||
| bannerId.value = data?.data?.id; | |||
| step.value++ | |||
| loading.value = false; | |||
| 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 = () => { | |||
| title.value = ""; | |||
| selectedCatPage.value = ""; | |||
| @@ -729,6 +828,11 @@ export default { | |||
| brandSelectorLoader, | |||
| formattedBrands, | |||
| handleBrandSearch, | |||
| step, | |||
| locale, | |||
| submitTranslation, | |||
| loadingFinally, | |||
| bannerId | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -203,7 +203,7 @@ export default { | |||
| case 'category': | |||
| return 'دسته ' + banner?.category_page?.title; | |||
| case 'brand': | |||
| return 'برند ' + banner?.brand_page?.title | |||
| return 'برند ' + banner?.brand_page?.translation?.title | |||
| case 'main_page': | |||
| return 'صفحه اصلی' | |||
| case 'special_page': | |||
| @@ -215,10 +215,10 @@ export default { | |||
| const setProductOrCategory = (banner) => { | |||
| if (banner?.category_id) { | |||
| return `دسته<br>${banner?.category?.title}`; | |||
| return `دسته<br>${banner?.category?.translation?.title ?? 'ندارد'}`; | |||
| } | |||
| if (banner?.product_id) { | |||
| return `محصول<br>${banner?.product?.title}`; | |||
| return `محصول<br>${banner?.product?.translation?.title ?? 'ندارد'}`; | |||
| } | |||
| return ""; | |||
| }; | |||
| @@ -58,350 +58,376 @@ | |||
| </div> | |||
| </BCardHeader> | |||
| <BCardBody> | |||
| <BRow class="g-3"> | |||
| <BCol md="6"> | |||
| <div class="form-group"> | |||
| <label class="form-label">عنوان</label> | |||
| <input | |||
| type="text" | |||
| v-model="title" | |||
| class="form-control" | |||
| placeholder="عنوان بنر" | |||
| :class="{ 'is-invalid': errors.title }" | |||
| @input="clearError('title')" | |||
| /> | |||
| </div> | |||
| <small v-if="errors.title" class="text-danger"> | |||
| {{ errors.title }} | |||
| </small> | |||
| </BCol> | |||
| <BCol md="6"> | |||
| <div class="form-group"> | |||
| <label class="form-label">پنل نمایش</label> | |||
| <select | |||
| class="form-select" | |||
| aria-label="Default select example" | |||
| v-model="pannel" | |||
| @change="clearError('pannel')" | |||
| :class="{ 'is-invalid': errors.pannel }" | |||
| placeholder="انخاب پنل" | |||
| > | |||
| <option value="wholesale">پنل عمده فروشی</option> | |||
| <option value="web">وب سایت و اپلیکیشن</option> | |||
| </select> | |||
| </div> | |||
| <small v-if="errors.pannel" class="text-danger"> | |||
| {{ errors.pannel }} | |||
| </small> | |||
| </BCol> | |||
| <BCol v-if="pannel === 'web' && 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-border-radius: 8px; | |||
| 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 | |||
| 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"> | |||
| <i class="fa fa-spinner fa-spin"></i> ویرایش... | |||
| </span> | |||
| <span v-else>ویرایش</span> | |||
| </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> | |||
| </BCol> | |||
| </BRow> | |||
| @@ -425,10 +451,12 @@ import "vue3-toastify/dist/index.css"; | |||
| import ApiServiece from "@/services/ApiService"; | |||
| import { ref, onMounted, computed } from "vue"; | |||
| import Layout from "@/layout/custom.vue"; | |||
| import {BTabs} from "bootstrap-vue-next"; | |||
| export default { | |||
| name: "SAMPLE-PAGE", | |||
| components: { | |||
| BTabs, | |||
| Layout, | |||
| mainPageBanner, | |||
| catBanner, | |||
| @@ -449,26 +477,18 @@ export default { | |||
| const selectedLandingCat = ref(); | |||
| const selectedLandingProduct = ref(); | |||
| const selectedLoc = ref(); | |||
| const pannel = ref(); | |||
| const pannel = ref('web'); | |||
| const image = ref(); | |||
| const imagePreview = ref(); | |||
| const selectedBrandPage = ref(); | |||
| const loading = ref(false); | |||
| const errors = ref({}); | |||
| const categorySelectorLoader = ref(false); | |||
| const productSelectorLoader = ref(false); | |||
| const categoryPageSelectorLoader = 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) => { | |||
| 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(() => | |||
| Array.isArray(products.value) | |||
| ? products.value.map((product) => ({ | |||
| @@ -512,8 +541,8 @@ export default { | |||
| const formattedCatPages = computed(() => | |||
| Array.isArray(catPages.value) | |||
| ? 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) | |||
| ? brands.value.map((brand) => ({ | |||
| value: brand.id, | |||
| label: brand.title, | |||
| label: brand?.translation?.title ?? 'بدون عنوان', | |||
| })) | |||
| : [] | |||
| ); | |||
| @@ -592,11 +621,6 @@ export default { | |||
| if (!selectedLoc.value) | |||
| errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید"; | |||
| if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید"; | |||
| if (!imagePreview.value) | |||
| errors.value.imagePreview = "عکس بنر را وارد نمایید"; | |||
| return Object.keys(errors.value).length === 0; | |||
| }; | |||
| @@ -629,12 +653,12 @@ export default { | |||
| } | |||
| title.value = data?.title; | |||
| pannel.value = data?.panel; | |||
| imagePreview.value = data?.image; | |||
| imagePreview.value = data?.translation?.image; | |||
| selectedLoc.value = data?.location; | |||
| selectedLandingCat.value = data?.category_id; | |||
| selectedLandingProduct.value = data?.product_id; | |||
| pageType.value = data.page_type; | |||
| banner.value = data; | |||
| if (data.page_id && pageType.value === "category") { | |||
| selectedCatPage.value = data?.page_id; | |||
| } | |||
| @@ -669,55 +693,58 @@ export default { | |||
| return; | |||
| } | |||
| loading.value = true; | |||
| const formData = new FormData(); | |||
| formData.append("title", title.value); | |||
| const params = {} | |||
| params.title = title.value | |||
| if (pageType.value === "category") { | |||
| formData.append("page_id", selectedCatPage.value); | |||
| params.page_id = selectedCatPage.value | |||
| } | |||
| if (pageType.value === "brand") { | |||
| formData.append("page_id", selectedBrandPage.value); | |||
| params.page_id = selectedBrandPage.value | |||
| } | |||
| if (landingType.value === "product") { | |||
| formData.append("product_id", selectedLandingProduct.value); | |||
| params.product_id = selectedLandingProduct.value | |||
| } | |||
| if (landingType.value === "cat") { | |||
| formData.append("category_id", selectedLandingCat.value); | |||
| params.category_id = selectedLandingCat.value | |||
| } | |||
| 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") { | |||
| formData.append("type", "banner"); | |||
| params.type = "banner" | |||
| } | |||
| 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') | |||
| 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: { | |||
| "content-type": "multipart", | |||
| 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 { | |||
| cats, | |||
| errors, | |||
| @@ -771,6 +895,9 @@ export default { | |||
| brandSelectorLoader, | |||
| handleBrandSearch, | |||
| selectedBrandPage, | |||
| handlerChangeLocale, | |||
| locale, | |||
| editTranslationBanner | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -29,6 +29,7 @@ export default { | |||
| const cats = ref(); | |||
| const catTitle = ref(); | |||
| const catId = ref(); | |||
| const catRow = ref(); | |||
| const convertToJalali = (date) => { | |||
| 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, () => { | |||
| getCats(); | |||
| @@ -183,6 +185,7 @@ export default { | |||
| handlePageInput, | |||
| searchPage, | |||
| visiblePages, | |||
| catRow, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -231,10 +234,10 @@ export default { | |||
| <td> | |||
| <button | |||
| @click="editModalData(cat?.id, cat?.translation?.title)" | |||
| data-bs-toggle="modal" | |||
| data-bs-target="#editBlogCat" | |||
| class="btn btn-sm btn-outline-warning me-1" | |||
| @click="editModalData(cat)" | |||
| > | |||
| ویرایش | |||
| </button> | |||
| @@ -261,6 +264,7 @@ export default { | |||
| :id="catId" | |||
| :title="catTitle" | |||
| :icon="catIcon" | |||
| :catRow="catRow" | |||
| @cat-updated="handleCatUpdated()" | |||
| /> | |||
| <showDescription :desc="catDescription" /> | |||
| @@ -30,7 +30,9 @@ export default { | |||
| const brandTitle = ref(); | |||
| const brandId = ref(); | |||
| const brandImage = ref(); | |||
| const brandRow = ref() | |||
| let searchTimeout = null; | |||
| const convertToJalali = (date) => { | |||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | |||
| .locale("fa") | |||
| @@ -103,7 +105,7 @@ export default { | |||
| }; | |||
| const deleteBrand = (id, title) => { | |||
| Swal.fire({ | |||
| text: `می خواهید برند ${title} را حذف کنید؟`, | |||
| text: `می خواهید برند ${title ?? ''} را حذف کنید؟`, | |||
| icon: "warning", | |||
| showCancelButton: true, | |||
| 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) => { | |||
| @@ -194,6 +193,7 @@ export default { | |||
| page, | |||
| paginate, | |||
| handlePageInput, | |||
| brandRow, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -266,12 +266,7 @@ export default { | |||
| <td> | |||
| <button | |||
| @click=" | |||
| editModalData( | |||
| brand?.id, | |||
| brand?.title, | |||
| brand.description, | |||
| brand.image | |||
| ) | |||
| editModalData(brand) | |||
| " | |||
| data-bs-toggle="modal" | |||
| data-bs-target="#editBrand" | |||
| @@ -280,7 +275,7 @@ export default { | |||
| ویرایش | |||
| </button> | |||
| <button | |||
| @click="deleteBrand(brand.id, brand.title)" | |||
| @click="deleteBrand(brand.id, brand?.translation?.title)" | |||
| class="btn btn-sm btn-outline-danger" | |||
| > | |||
| حذف | |||
| @@ -303,6 +298,7 @@ export default { | |||
| :title="brandTitle" | |||
| :description="brandDescription" | |||
| :image="brandImage" | |||
| :brandRow="brandRow" | |||
| @brand-updated="handleBrandUpdated()" | |||
| /> | |||
| <showDescription :desc="brandDescription" /> | |||
| @@ -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> | |||
| @@ -245,8 +245,8 @@ export default { | |||
| const formattedCategories = computed(() => | |||
| Array.isArray(categories.value) | |||
| ? 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) | |||
| ? products.value.map((product) => ({ | |||
| value: product.id, | |||
| label: product.title, | |||
| label: product?.translation?.title, | |||
| })) | |||
| : [] | |||
| ); | |||
| @@ -222,28 +222,28 @@ export default { | |||
| const formattedCategories = computed(() => | |||
| 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(() => | |||
| 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) => { | |||
| if (searchTerm.length < 3) return; | |||
| if (searchTerm?.length < 3) return; | |||
| categorySelectorLoader.value = true; | |||
| try { | |||
| const response = await ApiServiece.get( | |||
| `admin/categories?title=${searchTerm}` | |||
| `admin/categories?title=${searchTerm ?? ''}` | |||
| ); | |||
| categories.value = response.data.data; | |||
| categorySelectorLoader.value = false; | |||
| @@ -254,11 +254,11 @@ export default { | |||
| }; | |||
| const handleProductSearch = async (searchTerm) => { | |||
| if (searchTerm.length < 3) return; | |||
| if (searchTerm?.length < 3) return; | |||
| productSelectorLoader.value = true; | |||
| try { | |||
| const response = await ApiServiece.get( | |||
| `admin/products?title=${searchTerm}` | |||
| `admin/products?title=${searchTerm ?? ''}` | |||
| ); | |||
| products.value = response.data.data; | |||
| productSelectorLoader.value = false; | |||
| @@ -364,6 +364,9 @@ export default { | |||
| onMounted(() => { | |||
| getDiscount(); | |||
| handleProductSearch() | |||
| handleSearch() | |||
| }); | |||
| const submitForm = () => { | |||
| @@ -409,7 +412,7 @@ export default { | |||
| if (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) => { | |||
| loading.value = false; | |||
| toast.success("!تخفیف با موفقیت ویرایش شد", { | |||
| @@ -114,7 +114,7 @@ export default { | |||
| }); | |||
| const deleteProduct = (id, title) => { | |||
| Swal.fire({ | |||
| text: `می خواهید محصول ${title} را حذف کنید؟`, | |||
| text: `می خواهید محصول ${title ?? ''} را حذف کنید؟`, | |||
| icon: "warning", | |||
| showCancelButton: true, | |||
| confirmButtonColor: "#3085d6", | |||
| @@ -146,7 +146,7 @@ export default { | |||
| const restoreProduct = (id, title) => { | |||
| Swal.fire({ | |||
| text: `می خواهید محصول ${title} را بازیابی کنید؟`, | |||
| text: `می خواهید محصول ${title ?? ''} را بازیابی کنید؟`, | |||
| icon: "warning", | |||
| showCancelButton: true, | |||
| confirmButtonColor: "#3085d6", | |||
| @@ -294,6 +294,7 @@ export default { | |||
| }, | |||
| }; | |||
| </script> | |||
| <template> | |||
| <Layout> | |||
| <BRow> | |||
| @@ -362,15 +363,11 @@ export default { | |||
| <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> | |||
| </tr> | |||
| @@ -386,58 +383,22 @@ export default { | |||
| </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> | |||
| <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 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 v-if="!product?.retail_price">ندارد</td> | |||
| <td v-if="!product.deleted_at"> | |||
| <span class="badge bg-success text-white">فعال</span> | |||
| @@ -455,7 +416,7 @@ export default { | |||
| </router-link> | |||
| <button | |||
| 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" | |||
| > | |||
| حذف | |||
| @@ -463,7 +424,7 @@ export default { | |||
| <button | |||
| v-else | |||
| @click="restoreProduct(product?.id, product?.title)" | |||
| @click="restoreProduct(product?.id, product?.translation?.title)" | |||
| class="btn btn-sm btn-outline-success" | |||
| > | |||
| بازیابی | |||
| @@ -30,6 +30,7 @@ export default { | |||
| const userMobile = ref(); | |||
| const userId = ref(); | |||
| const userRole = ref(); | |||
| const userCountryCode = ref(); | |||
| const selectedRole = ref(""); | |||
| const userProfile = JSON.parse(localStorage.getItem('user_profile')); | |||
| @@ -49,7 +50,6 @@ export default { | |||
| }&trashed=${selectedStatus.value || ""}` | |||
| ) | |||
| .then((resp) => { | |||
| console.log(resp.data.data); | |||
| users.value = resp.data.data.data; | |||
| currentPage.value = resp.data.data.current_page; | |||
| totalPages.value = resp.data.data.last_page; | |||
| @@ -113,8 +113,7 @@ export default { | |||
| debouncedSearch(newQuery); | |||
| }); | |||
| const debouncedSearch = debounce((query) => { | |||
| console.log("Searching for:", query); | |||
| const debouncedSearch = debounce(() => { | |||
| getUsers(); | |||
| }, 1000); | |||
| @@ -134,12 +133,14 @@ export default { | |||
| } | |||
| } | |||
| const modalData = (id, name, mobile, role) => { | |||
| const modalData = (id, name, mobile, role, config_id) => { | |||
| userName.value = name; | |||
| userMobile.value = mobile; | |||
| userId.value = id; | |||
| userRole.value = role; | |||
| userCountryCode.value = config_id; | |||
| }; | |||
| const handleUserUpdated = () => { | |||
| getUsers(); | |||
| }; | |||
| @@ -237,6 +238,7 @@ export default { | |||
| selectedStatus, | |||
| filterLoading, | |||
| userProfile, | |||
| userCountryCode, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -356,7 +358,8 @@ export default { | |||
| user.id, | |||
| user.name, | |||
| user.mobile, | |||
| user.role | |||
| user.role, | |||
| user?.country_config_id | |||
| ) | |||
| " | |||
| data-bs-toggle="modal" | |||
| @@ -405,6 +408,7 @@ export default { | |||
| :mobile="userMobile" | |||
| :id="userId" | |||
| :role="userRole" | |||
| :countryCode="userCountryCode" | |||
| /> | |||
| <addUser @user-updated="handleUserUpdated" /> | |||
| </BRow> | |||