| @@ -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 | <span class="pc-mtext">کاربران</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li class="pc-item" :class="{ active: this.$route.path === '/countries' }"> | |||||
| <router-link to="/countries" class="pc-link"> | |||||
| <span class="pc-micon"> | |||||
| <i class="ph-duotone ph-user-circle"></i> | |||||
| </span> | |||||
| <span class="pc-mtext">کشورها</span> | |||||
| </router-link> | |||||
| </li> | |||||
| <li | <li | ||||
| class="pc-item" | class="pc-item" | ||||
| :class="{ | :class="{ | ||||
| @@ -280,6 +290,7 @@ export default { | |||||
| <span class="pc-mtext">بنر ها</span></router-link | <span class="pc-mtext">بنر ها</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li class="pc-item" :class="{ active: this.$route.path === '/brands' }"> | <li class="pc-item" :class="{ active: this.$route.path === '/brands' }"> | ||||
| <router-link to="/brands" class="pc-link"> | <router-link to="/brands" class="pc-link"> | ||||
| <span class="pc-micon"> | <span class="pc-micon"> | ||||
| @@ -300,17 +311,17 @@ export default { | |||||
| <span class="pc-mtext">ویژگی ها</span></router-link | <span class="pc-mtext">ویژگی ها</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li | |||||
| class="pc-item" | |||||
| :class="{ active: this.$route.path === '/idenities' }" | |||||
| > | |||||
| <router-link to="/idenities" class="pc-link"> | |||||
| <span class="pc-micon"> | |||||
| <i class="ph-duotone ph-barcode"></i> | |||||
| </span> | |||||
| <span class="pc-mtext">مشخصات</span></router-link | |||||
| > | |||||
| </li> | |||||
| <!-- <li--> | |||||
| <!-- class="pc-item"--> | |||||
| <!-- :class="{ active: this.$route.path === '/idenities' }"--> | |||||
| <!-- >--> | |||||
| <!-- <router-link to="/idenities" class="pc-link">--> | |||||
| <!-- <span class="pc-micon">--> | |||||
| <!-- <i class="ph-duotone ph-barcode"></i>--> | |||||
| <!-- </span>--> | |||||
| <!-- <span class="pc-mtext">مشخصات</span></router-link--> | |||||
| <!-- >--> | |||||
| <!-- </li>--> | |||||
| <li | <li | ||||
| class="pc-item" | class="pc-item" | ||||
| :class="{ | :class="{ | ||||
| @@ -327,6 +338,7 @@ export default { | |||||
| <span class="pc-mtext">بلاگ ها</span></router-link | <span class="pc-mtext">بلاگ ها</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li | <li | ||||
| class="pc-item" | class="pc-item" | ||||
| :class="{ | :class="{ | ||||
| @@ -343,6 +355,7 @@ export default { | |||||
| <span class="pc-mtext">محصولات</span></router-link | <span class="pc-mtext">محصولات</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li | <li | ||||
| class="pc-item" | class="pc-item" | ||||
| :class="{ | :class="{ | ||||
| @@ -360,50 +373,47 @@ export default { | |||||
| > | > | ||||
| </li> | </li> | ||||
| <!-- سفاراشات --> | |||||
| <li class="pc-item pc-hasmenu"> | |||||
| <BLink | |||||
| class="pc-link" | |||||
| data-bs-toggle="collapse" | |||||
| href="#collapse-orders" | |||||
| role="button" | |||||
| aria-expanded="false" | |||||
| aria-controls="collapse-orders" | |||||
| > | |||||
| <span class="pc-micon"> | |||||
| <i class="ph-duotone ph-shopping-cart"></i> | |||||
| </span> | |||||
| <span class="pc-mtext">سفارشات</span> | |||||
| <span class="pc-arrow"> | |||||
| <ChevronDownIcon /> | |||||
| </span> | |||||
| </BLink> | |||||
| <div class="collapse" id="collapse-orders"> | |||||
| <ul class="pc-submenu"> | |||||
| <li | |||||
| class="pc-item" | |||||
| :class="{ | |||||
| <li | |||||
| class="pc-item" | |||||
| :class="{ | |||||
| active: | active: | ||||
| this.$route.path === '/orders' || | this.$route.path === '/orders' || | ||||
| this.$route.name === 'singleOrder', | this.$route.name === 'singleOrder', | ||||
| }" | }" | ||||
| > | |||||
| <router-link to="/orders" class="pc-link"> | |||||
| <span class="pc-mtext">سفارشات</span></router-link | |||||
| > | |||||
| </li> | |||||
| <li | |||||
| class="pc-item" | |||||
| :class="{ active: this.$route.path === '/approvedOrders' }" | |||||
| > | |||||
| <router-link to="/approvedOrders" class="pc-link"> | |||||
| <span class="pc-mtext">آیتم های تایید شده(عمده)</span></router-link | |||||
| > | |||||
| </li> | |||||
| </ul> | |||||
| </div> | |||||
| > | |||||
| <router-link to="/orders" class="pc-link"> | |||||
| <span class="pc-micon"> | |||||
| <i class="ph-duotone ph-shopping-cart"></i> | |||||
| </span> | |||||
| <span class="pc-mtext">سفارشات</span> | |||||
| </router-link> | |||||
| </li> | </li> | ||||
| <!-- سفاراشات --> | |||||
| <!-- <li class="pc-item pc-hasmenu">--> | |||||
| <!-- <BLink--> | |||||
| <!-- class="pc-link"--> | |||||
| <!-- data-bs-toggle="collapse"--> | |||||
| <!-- href="#collapse-orders"--> | |||||
| <!-- role="button"--> | |||||
| <!-- aria-expanded="false"--> | |||||
| <!-- aria-controls="collapse-orders"--> | |||||
| <!-- >--> | |||||
| <!-- <span class="pc-micon">--> | |||||
| <!-- <i class="ph-duotone ph-shopping-cart"></i>--> | |||||
| <!-- </span>--> | |||||
| <!-- <span class="pc-mtext">سفارشات</span>--> | |||||
| <!-- <span class="pc-arrow">--> | |||||
| <!-- <ChevronDownIcon />--> | |||||
| <!-- </span>--> | |||||
| <!-- </BLink>--> | |||||
| <!-- <div class="collapse" id="collapse-orders">--> | |||||
| <!-- <ul class="pc-submenu">--> | |||||
| <!-- --> | |||||
| <!-- </ul>--> | |||||
| <!-- </div>--> | |||||
| <!-- </li>--> | |||||
| <!-- دسته ها --> | <!-- دسته ها --> | ||||
| <!-- <li class="pc-item pc-hasmenu"> | <!-- <li class="pc-item pc-hasmenu"> | ||||
| <BLink | <BLink | ||||
| @@ -455,6 +465,7 @@ export default { | |||||
| <span class="pc-mtext">نظرات</span></router-link | <span class="pc-mtext">نظرات</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li | <li | ||||
| class="pc-item" | class="pc-item" | ||||
| :class="{ | :class="{ | ||||
| @@ -469,6 +480,7 @@ export default { | |||||
| <span class="pc-mtext">پرسش و پاسخ</span></router-link | <span class="pc-mtext">پرسش و پاسخ</span></router-link | ||||
| > | > | ||||
| </li> | </li> | ||||
| <li class="pc-item pc-hasmenu"> | <li class="pc-item pc-hasmenu"> | ||||
| <BLink | <BLink | ||||
| class="pc-link" | class="pc-link" | ||||
| @@ -21,105 +21,166 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="addBrand"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان برند</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان برند" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'ثبت دسته بندی', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| > | |||||
| <template #1> | |||||
| <!-- Brand Image Upload --> | <!-- Brand Image Upload --> | ||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر برند</label> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر برند</label> | |||||
| <input | |||||
| <input | |||||
| type="file" | type="file" | ||||
| accept="image/*" | accept="image/*" | ||||
| @change="handleImageChange" | @change="handleImageChange" | ||||
| class="form-control" | class="form-control" | ||||
| :class="{ 'is-invalid': errors.image }" | :class="{ 'is-invalid': errors.image }" | ||||
| /> | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | :src="imagePreview" | ||||
| alt="Image Preview" | alt="Image Preview" | ||||
| class="img-fluid rounded shadow-sm Image-Preview" | class="img-fluid rounded shadow-sm Image-Preview" | ||||
| /> | |||||
| <button | |||||
| /> | |||||
| <button | |||||
| type="button" | type="button" | ||||
| @click="removeImage()" | @click="removeImage()" | ||||
| class="delete-btn" | class="delete-btn" | ||||
| > | |||||
| <i class="fa fa-trash f-16"></i> | |||||
| </button> | |||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات برند</label> | |||||
| <textarea | |||||
| v-model="description" | |||||
| @input="clearError('description')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات برند" | |||||
| :class="{ 'is-invalid': errors.description }" | |||||
| ></textarea> | |||||
| <small v-if="errors.description" class="text-danger"> | |||||
| {{ errors.description }} | |||||
| </small> | |||||
| > | |||||
| <i class="fa fa-trash f-16"></i> | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | > | ||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <button :disabled="loadingStep" @click="handlerAddBrand" class="btn btn-primary"> | |||||
| <span | |||||
| v-if="loadingStep" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </template> | |||||
| <template #2> | |||||
| <form @submit.prevent="addBrand"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان برند</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان برند" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات برند</label> | |||||
| <textarea | |||||
| v-model="description" | |||||
| @input="clearError('description')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات برند" | |||||
| :class="{ 'is-invalid': errors.description }" | |||||
| ></textarea> | |||||
| <small v-if="errors.description" class="text-danger"> | |||||
| {{ errors.description }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | <span | ||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | ></span> | ||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </template> | |||||
| </Steppy> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -131,13 +192,19 @@ import { ref } from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import {Steppy} from "vue3-steppy"; | |||||
| export default { | export default { | ||||
| components: {Steppy}, | |||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const image = ref(null); | const image = ref(null); | ||||
| const imagePreview = ref(null); | const imagePreview = ref(null); | ||||
| const description = ref(); | const description = ref(); | ||||
| const title = ref(); | const title = ref(); | ||||
| const loadingStep = ref(false); | |||||
| const step = ref(1); | |||||
| const brandId = ref(null); | |||||
| const locale = ref('fa'); | |||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| @@ -150,8 +217,6 @@ export default { | |||||
| if (fileInput) { | if (fileInput) { | ||||
| fileInput.value = ""; | fileInput.value = ""; | ||||
| } | } | ||||
| console.log(image.value); | |||||
| }; | }; | ||||
| const handleImageChange = (event) => { | const handleImageChange = (event) => { | ||||
| const file = event.target.files[0]; | const file = event.target.files[0]; | ||||
| @@ -204,14 +269,14 @@ export default { | |||||
| } | } | ||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| formData.append("description", description.value); | |||||
| formData.append("image", image.value); | |||||
| console.log(image.value); | |||||
| ApiServiece.post(`admin/brands`, formData, { | |||||
| const params = { | |||||
| title: title.value, | |||||
| description: description.value, | |||||
| locale: locale.value, | |||||
| } | |||||
| ApiServiece.post(`admin/brands/${brandId.value}/translations`, params, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| @@ -224,11 +289,9 @@ export default { | |||||
| }) | }) | ||||
| .then(() => { | .then(() => { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| document.getElementById("close").click(); | |||||
| emit("brand-updated"); | emit("brand-updated"); | ||||
| title.value = ""; | title.value = ""; | ||||
| description.value = ""; | description.value = ""; | ||||
| image.value = null; | |||||
| imagePreview.value = null; | imagePreview.value = null; | ||||
| }, 500); | }, 500); | ||||
| }) | }) | ||||
| @@ -244,6 +307,44 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const handlerAddBrand = async () => { | |||||
| if (!image.value) { | |||||
| toast.error('تصویر برند نمی تواند خالی باشد.') | |||||
| return | |||||
| } | |||||
| try { | |||||
| loadingStep.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("image", image.value); | |||||
| const { data: { message, success, data } } = await ApiServiece.post( | |||||
| `admin/brands`, | |||||
| formData, | |||||
| { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| brandId.value = data?.id | |||||
| step.value++ | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingStep.value = false; | |||||
| } | |||||
| } | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| @@ -255,6 +356,11 @@ export default { | |||||
| removeImage, | removeImage, | ||||
| title, | title, | ||||
| description, | description, | ||||
| loadingStep, | |||||
| step, | |||||
| handlerAddBrand, | |||||
| brandId, | |||||
| locale | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -20,99 +20,157 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="editBrand"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان برند</label> | |||||
| <input | |||||
| v-model="localTitle" | |||||
| @input="clearError('localTitle')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان برند" | |||||
| :class="{ 'is-invalid': errors.localTitle }" | |||||
| /> | |||||
| <small v-if="errors.localTitle" class="text-danger"> | |||||
| {{ errors.localTitle }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <!-- Brand Image Upload --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر برند</label> | |||||
| <BTabs> | |||||
| <BTab title="بارگذاری تصویر برند"> | |||||
| <div class="form-group mt-4"> | |||||
| <label class="form-label">تصویر برند</label> | |||||
| <input | |||||
| <input | |||||
| type="file" | type="file" | ||||
| @input="clearError('localImage')" | @input="clearError('localImage')" | ||||
| accept="image/*" | accept="image/*" | ||||
| @change="handleImageChange" | @change="handleImageChange" | ||||
| class="form-control" | class="form-control" | ||||
| :class="{ 'is-invalid': errors.localImage }" | :class="{ 'is-invalid': errors.localImage }" | ||||
| /> | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | :src="imagePreview" | ||||
| alt="Image Preview" | alt="Image Preview" | ||||
| class="img-fluid rounded shadow-sm Image-Preview" | class="img-fluid rounded shadow-sm Image-Preview" | ||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.localImage" class="text-danger"> | |||||
| {{ errors.localImage }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات برند</label> | |||||
| <textarea | |||||
| v-model="localDesc" | |||||
| @input="clearError('localDesc')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات برند" | |||||
| :class="{ 'is-invalid': errors.localDesc }" | |||||
| ></textarea> | |||||
| <small v-if="errors.localDesc" class="text-danger"> | |||||
| {{ errors.localDesc }} | |||||
| </small> | |||||
| /> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeEdit" | |||||
| <small v-if="errors.localImage" class="text-danger"> | |||||
| {{ errors.localImage }} | |||||
| </small> | |||||
| </div> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | > | ||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| <button :disabled="loadingImage" @click="editImageBrand" class="btn btn-primary"> | |||||
| <span | |||||
| v-if="loadingImage" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </BTab> | |||||
| <BTab title="ترجمه ها"> | |||||
| <form @submit.prevent="editBrand" class="mt-4"> | |||||
| <BButton | |||||
| :disabled="!findLocaleTranslation" | |||||
| :loading="loadingDelete" | |||||
| @click="handlerRemoveTranslation" | |||||
| class="btn btn-sm rounded btn-danger d-block" | |||||
| style="margin-right: auto" | |||||
| > | |||||
| حذف ترجمه {{ findLocaleTranslation }} | |||||
| </BButton> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان برند</label> | |||||
| <input | |||||
| v-model="localTitle" | |||||
| @input="clearError('localTitle')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان برند" | |||||
| :class="{ 'is-invalid': errors.localTitle }" | |||||
| /> | |||||
| <small v-if="errors.localTitle" class="text-danger"> | |||||
| {{ errors.localTitle }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات برند</label> | |||||
| <textarea | |||||
| v-model="localDesc" | |||||
| @input="clearError('localDesc')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات برند" | |||||
| :class="{ 'is-invalid': errors.localDesc }" | |||||
| ></textarea> | |||||
| <small v-if="errors.localDesc" class="text-danger"> | |||||
| {{ errors.localDesc }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeEdit" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </BTab> | |||||
| </BTabs> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -120,13 +178,15 @@ | |||||
| </template> | </template> | ||||
| <script> | <script> | ||||
| import { ref, toRef, watch } from "vue"; | |||||
| import {computed, ref, toRef, watch} from "vue"; | |||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import {BTabs} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| components: {BTabs}, | |||||
| props: { | props: { | ||||
| id: { | id: { | ||||
| type: String, | type: String, | ||||
| @@ -144,6 +204,10 @@ export default { | |||||
| type: String, | type: String, | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| brandRow: { | |||||
| type: Object, | |||||
| required: true, | |||||
| }, | |||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| @@ -152,9 +216,50 @@ export default { | |||||
| const localDesc = toRef(props.description); | const localDesc = toRef(props.description); | ||||
| const localImage = toRef(props.image); | const localImage = toRef(props.image); | ||||
| const image = ref(null); | const image = ref(null); | ||||
| const localId = toRef(props.id); | |||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const loadingImage = ref(false); | |||||
| const locale = ref('fa'); | |||||
| const loadingDelete = ref(false); | |||||
| const brandRowModel = computed({ | |||||
| get: () => props.brandRow, | |||||
| set: (newValue) => emit('update:categoryRow', newValue) | |||||
| }); | |||||
| watch( | |||||
| () => props.brandRow, | |||||
| (newVal) => { | |||||
| localTitle.value = newVal?.translation?.title | |||||
| localDesc.value = newVal?.translation?.description | |||||
| locale.value = newVal?.translation?.locale | |||||
| imagePreview.value = newVal?.image | |||||
| } | |||||
| ) | |||||
| const findLocaleTranslation = computed(() => { | |||||
| const foundTranslation = brandRowModel.value?.translations?.find( | |||||
| item => item?.locale === locale.value | |||||
| ); | |||||
| if (foundTranslation) { | |||||
| switch (foundTranslation?.locale) { | |||||
| case "en": | |||||
| return "انگلیسی"; | |||||
| case "fa": | |||||
| return "فارسی"; | |||||
| case "ar": | |||||
| return "عربی"; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }); | |||||
| const handleImageChange = (event) => { | const handleImageChange = (event) => { | ||||
| const file = event.target.files[0]; | const file = event.target.files[0]; | ||||
| @@ -178,27 +283,6 @@ export default { | |||||
| } | } | ||||
| }; | }; | ||||
| watch( | |||||
| () => props.title, | |||||
| (newVal) => (localTitle.value = newVal) | |||||
| ); | |||||
| watch( | |||||
| () => props.description, | |||||
| (newVal) => (localDesc.value = newVal) | |||||
| ); | |||||
| watch( | |||||
| () => props.image, | |||||
| (newVal) => { | |||||
| localImage.value = newVal; | |||||
| imagePreview.value = newVal; | |||||
| } | |||||
| ); | |||||
| watch( | |||||
| () => props.id, | |||||
| (newVal) => (localId.value = newVal) | |||||
| ); | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!localTitle.value) | if (!localTitle.value) | ||||
| @@ -215,7 +299,7 @@ export default { | |||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| }; | }; | ||||
| const editBrand = () => { | |||||
| const editBrand = async () => { | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -223,47 +307,140 @@ export default { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("title", localTitle.value); | |||||
| formData.append("description", localDesc.value); | |||||
| try { | |||||
| loading.value = true; | |||||
| const params = { title: localTitle.value, description: localDesc.value, locale: locale.value }; | |||||
| const existingTranslation = brandRowModel.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| const { data } = existingTranslation | |||||
| ? await ApiServiece.put( | |||||
| `admin/brands/${brandRowModel.value?.id}/translations/${existingTranslation?.id}`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| : await ApiServiece.post( | |||||
| `admin/brands/${brandRowModel.value?.id}/translations`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ); | |||||
| if (data?.success) { | |||||
| const updatedBrands = brandRowModel.value ? {...brandRowModel.value} : null; | |||||
| if (updatedBrands) { | |||||
| updatedBrands.translations = [...(updatedBrands.translations || []), data?.data]; | |||||
| brandRowModel.value = updatedBrands; | |||||
| } | |||||
| toast.success(data?.message) | |||||
| if (image.value) { | |||||
| formData.append("image", image.value); | |||||
| } | |||||
| formData.append("_method", "put"); | |||||
| ApiServiece.post(`admin/brands/${localId.value}`, formData, { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| .then(() => { | |||||
| toast.success("!برند با موفقیت ویرایش شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| document.getElementById("closeEdit").click(); | |||||
| emit("brand-updated"); | |||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| console.error(error); | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| emit("cat-updated"); | |||||
| }, 500) | |||||
| } | |||||
| } catch (error) { | |||||
| toast.error(`${error?.response?.data?.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | }); | ||||
| } finally { | |||||
| loading.value = false; | |||||
| } | |||||
| }; | }; | ||||
| const editImageBrand = async () => { | |||||
| try { | |||||
| loadingImage.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("image", image.value); | |||||
| formData.append("_method", 'put'); | |||||
| const { data: { message, success } } = await ApiServiece.post( | |||||
| `admin/brands/${brandRowModel.value?.id}`, | |||||
| formData, | |||||
| { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingImage.value = false; | |||||
| } | |||||
| } | |||||
| const handlerRemoveTranslation = async () => { | |||||
| const findLocale = brandRowModel.value?.translations?.find(item => item?.locale === locale.value); | |||||
| if (findLocale) { | |||||
| try { | |||||
| loadingDelete.value = true; | |||||
| const { data: { success, message, data } } = await ApiServiece.delete( | |||||
| `admin/brands/${brandRowModel.value?.id}/translations/${findLocale?.id}` | |||||
| ) | |||||
| if (success) { | |||||
| const updatedBrands = brandRowModel.value ? {...brandRowModel.value} : null; | |||||
| if (updatedBrands) { | |||||
| updatedBrands.translations = [...(updatedBrands.translations || []), data]; | |||||
| brandRowModel.value = updatedBrands; | |||||
| } | |||||
| localTitle.value = null | |||||
| localDesc.value = null | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e) | |||||
| }finally { | |||||
| loadingDelete.value = false | |||||
| } | |||||
| } | |||||
| } | |||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = brandRowModel.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| localTitle.value = findLocale?.title | |||||
| localDesc.value = findLocale?.description | |||||
| } else { | |||||
| localTitle.value = undefined | |||||
| localDesc.value = undefined | |||||
| } | |||||
| } | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| @@ -274,6 +451,13 @@ export default { | |||||
| localImage, | localImage, | ||||
| handleImageChange, | handleImageChange, | ||||
| imagePreview, | imagePreview, | ||||
| locale, | |||||
| loadingImage, | |||||
| editImageBrand, | |||||
| findLocaleTranslation, | |||||
| handlerRemoveTranslation, | |||||
| loadingDelete, | |||||
| handlerChangeLocale | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -103,6 +103,8 @@ | |||||
| }}</small> | }}</small> | ||||
| </div> | </div> | ||||
| </BCol> | </BCol> | ||||
| <ConfigCountriesSelect v-model:country-code="selectedCountryCode"/> | |||||
| </BRow> | </BRow> | ||||
| <!-- Submit Buttons --> | <!-- Submit Buttons --> | ||||
| @@ -137,20 +139,22 @@ | |||||
| <script> | <script> | ||||
| import { ref } from "vue"; | import { ref } from "vue"; | ||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import ConfigCountriesSelect from "@/components/ConfigCountriesSelect.vue"; | |||||
| export default { | export default { | ||||
| components: {ConfigCountriesSelect}, | |||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const name = ref(); | const name = ref(); | ||||
| const mobile = ref(); | const mobile = ref(); | ||||
| const password = ref(); | const password = ref(); | ||||
| const repeatPassword = ref(); | const repeatPassword = ref(); | ||||
| const role = ref(); | const role = ref(); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const selectedCountryCode = ref(''); | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| @@ -165,7 +169,7 @@ export default { | |||||
| errors.value.password = "رمز عبور باید حداقل 8 کاراکتر باشد"; | errors.value.password = "رمز عبور باید حداقل 8 کاراکتر باشد"; | ||||
| } else if (repeatPassword.value.length < 8) { | } else if (repeatPassword.value.length < 8) { | ||||
| errors.value.repeatPassword = | errors.value.repeatPassword = | ||||
| "تکرار رمز عبور باید حداقل 8 کاراکتر باشد"; | |||||
| "تکرار رمز عبور باید حداقل 8 کاراکتر باشد"; | |||||
| } | } | ||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -187,38 +191,40 @@ export default { | |||||
| const formData = new FormData(); | const formData = new FormData(); | ||||
| formData.append("mobile", mobile.value); | formData.append("mobile", mobile.value); | ||||
| if (name.value) | if (name.value) | ||||
| formData.append("name", name.value); | |||||
| formData.append("name", name.value); | |||||
| formData.append("role", role.value); | formData.append("role", role.value); | ||||
| formData.append("password", password.value); | formData.append("password", password.value); | ||||
| formData.append("password_confirmation", repeatPassword.value); | formData.append("password_confirmation", repeatPassword.value); | ||||
| formData.append("country_config_id", selectedCountryCode.value); | |||||
| ApiServiece.post(`admin/users`, formData) | ApiServiece.post(`admin/users`, formData) | ||||
| .then(() => { | |||||
| toast.success("!کاربر با موفقیت اضافه شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| mobile.value = ""; | |||||
| name.value = ""; | |||||
| role.value = ""; | |||||
| password.value = ""; | |||||
| repeatPassword.value = ""; | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | |||||
| document.getElementById("addClose").click(); | |||||
| emit("user-updated"); | |||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| .then(() => { | |||||
| toast.success("!کاربر با موفقیت اضافه شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| mobile.value = ""; | |||||
| name.value = ""; | |||||
| role.value = ""; | |||||
| password.value = ""; | |||||
| repeatPassword.value = ""; | |||||
| selectedCountryCode.value = ""; | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | |||||
| document.getElementById("addClose").click(); | |||||
| emit("user-updated"); | |||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| }); | }); | ||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| }); | |||||
| }; | }; | ||||
| return { | return { | ||||
| @@ -231,6 +237,7 @@ export default { | |||||
| password, | password, | ||||
| repeatPassword, | repeatPassword, | ||||
| role, | role, | ||||
| selectedCountryCode, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -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-content"> | ||||
| <div class="modal-header"> | <div class="modal-header"> | ||||
| <h5 class="modal-title" id="exampleModalLabel"> | <h5 class="modal-title" id="exampleModalLabel"> | ||||
| اضافه کردن رنگ جدید | |||||
| اضافه کردن ویژگی جدید | |||||
| </h5> | </h5> | ||||
| <button | <button | ||||
| type="button" | type="button" | ||||
| @@ -21,77 +21,139 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="addAttribute"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نام رنگ</label> | |||||
| <input | |||||
| v-model="colorName" | |||||
| @input="clearError('colorName')" | |||||
| type="text" | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'ثبت دسته بندی', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| > | |||||
| <template #1> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">دسته بندی</label> | |||||
| <select | |||||
| v-model="categoryId" | |||||
| class="form-control" | class="form-control" | ||||
| placeholder="نام رنگ" | |||||
| :class="{ 'is-invalid': errors.colorName }" | |||||
| placeholder="دسته بندی انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| v-for="category in categories" | |||||
| :key="category?.id" | |||||
| :value="category?.id" | |||||
| > | |||||
| {{ category?.translation?.title ?? 'بدون نام' }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button @click="handlerSubmitCategory" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | /> | ||||
| <small v-if="errors.colorName" class="text-danger"> | |||||
| {{ errors.colorName }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب رنگ</label> | |||||
| <div class="color-picker-wrapper"> | |||||
| <input | |||||
| v-model="colorCode" | |||||
| @input="clearError('colorCode')" | |||||
| type="color" | |||||
| class="form-control color-picker" | |||||
| :class="{ 'is-invalid': errors.colorCode }" | |||||
| /> | |||||
| <span | |||||
| v-if="colorCode" | |||||
| class="color-display" | |||||
| :style="{ backgroundColor: colorCode }" | |||||
| ></span> | |||||
| </div> | |||||
| <span> {{ colorCode }}</span> | |||||
| <small v-if="errors.colorCode" class="text-danger"> | |||||
| {{ errors.colorCode }} | |||||
| </small> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </template> | |||||
| <template #2> | |||||
| <form @submit.prevent="addAttribute"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان ویژگی</label> | |||||
| <input | |||||
| v-model="attrName" | |||||
| @input="clearError('attrName')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان ویژگی" | |||||
| :class="{ 'is-invalid': errors.attrName }" | |||||
| /> | |||||
| <small v-if="errors.attrName" class="text-danger"> | |||||
| {{ errors.attrName }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loadingAttr"> | |||||
| <span | |||||
| v-if="loadingAttr" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </form> | |||||
| </template> | |||||
| </Steppy> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -103,8 +165,10 @@ import { ref, toRef, watch } from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import {Steppy} from "vue3-steppy"; | |||||
| export default { | export default { | ||||
| components: {Steppy}, | |||||
| props: { | props: { | ||||
| attributeValues: { | attributeValues: { | ||||
| type: Array, | type: Array, | ||||
| @@ -112,11 +176,17 @@ export default { | |||||
| }, | }, | ||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const colorName = ref(); | |||||
| const attrName = ref(); | |||||
| const colorCode = ref(); | const colorCode = ref(); | ||||
| const localAttributeValues = toRef(props.attributeValues); | const localAttributeValues = toRef(props.attributeValues); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const loadingAttr = ref(false); | |||||
| const categories = ref([]); | |||||
| const categoryId = ref(null); | |||||
| const categoryResponse = ref(null); | |||||
| const step = ref(1); | |||||
| const locale = ref('fa'); | |||||
| watch( | watch( | ||||
| () => props.attributeValues, | () => props.attributeValues, | ||||
| @@ -125,9 +195,9 @@ export default { | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!colorName.value) | |||||
| errors.value.colorName = "وارد کردن نام رنگ ضروری می باشد"; | |||||
| if (!colorCode.value) errors.value.colorCode = "انتخاب رنگ ضروری می باشد"; | |||||
| // if (!attrName.value) | |||||
| // errors.value.attrName = "وارد کردن نام ویژگی ضروری می باشد"; | |||||
| if (!categoryId.value) errors.value.categoryId = "انتخاب دسته بندی ضروری می باشد"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -136,7 +206,7 @@ export default { | |||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| }; | }; | ||||
| const addAttribute = () => { | |||||
| const handlerSubmitCategory = () => { | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -144,30 +214,21 @@ export default { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| console.log(localAttributeValues.value); | |||||
| formData.append("title", colorName.value); | |||||
| formData.append("code", colorCode.value); | |||||
| console.log(localAttributeValues); | |||||
| formData.append("attribute_id", 1); | |||||
| ApiServiece.post(`admin/attribute-values`, formData) | |||||
| .then(() => { | |||||
| toast.success("!ویژگی با موفقیت اضافه شد", { | |||||
| const params = { | |||||
| category_id: categoryId.value, | |||||
| } | |||||
| ApiServiece.post(`admin/attributes`, params) | |||||
| .then(({ data }) => { | |||||
| toast.success(data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | |||||
| document.getElementById("close").click(); | |||||
| emit("attribute-updated"); | |||||
| colorName.value = ""; | |||||
| colorCode.value = ""; | |||||
| }, 500); | |||||
| categoryResponse.value = data?.data?.id; | |||||
| step.value++ | |||||
| }) | }) | ||||
| .catch((error) => { | .catch((error) => { | ||||
| toast.error(`${error.response.data.message}`, { | toast.error(`${error.response.data.message}`, { | ||||
| @@ -180,13 +241,57 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const addAttribute = async () => { | |||||
| try { | |||||
| loadingAttr.value = true; | |||||
| const params = { | |||||
| title: attrName.value, | |||||
| locale: locale.value, | |||||
| } | |||||
| const { data: { success, message } } = await ApiServiece.post(`admin/attributes/${categoryResponse.value}/translations`, params) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| //document.getElementById("close").click(); | |||||
| emit("attribute-updated"); | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingAttr.value = false; | |||||
| } | |||||
| } | |||||
| const getAttributes = async () => { | |||||
| try { | |||||
| const { data: { success, data } } = await ApiServiece.get(`admin/categories`) | |||||
| if (success) { | |||||
| categories.value = data; | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } | |||||
| } | |||||
| getAttributes() | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| clearError, | clearError, | ||||
| addAttribute, | |||||
| colorName, | |||||
| handlerSubmitCategory, | |||||
| attrName, | |||||
| colorCode, | colorCode, | ||||
| getAttributes, | |||||
| categories, | |||||
| loadingAttr, | |||||
| categoryId, | |||||
| step, | |||||
| locale, | |||||
| addAttribute | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -19,77 +19,140 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="editAttribute"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نام رنگ</label> | |||||
| <input | |||||
| v-model="localColorName" | |||||
| @input="clearError('localColorName')" | |||||
| type="text" | |||||
| <BTabs> | |||||
| <BTab title="دسته بندی"> | |||||
| <div class="form-group mt-2"> | |||||
| <label class="form-label">دسته بندی</label> | |||||
| <select | |||||
| v-model="categoryId" | |||||
| class="form-control" | class="form-control" | ||||
| placeholder="نام رنگ" | |||||
| :class="{ 'is-invalid': errors.colorName }" | |||||
| placeholder="دسته بندی انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| v-for="category in categories" | |||||
| :key="category?.id" | |||||
| :value="category?.id" | |||||
| > | |||||
| {{ category?.translation?.title ?? 'بدون نام' }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button @click="handlerSubmitCategory" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | /> | ||||
| <small v-if="errors.localColorName" class="text-danger"> | |||||
| {{ errors.localColorName }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب رنگ</label> | |||||
| <div class="color-picker-wrapper"> | |||||
| <input | |||||
| v-model="localColorCode" | |||||
| @input="clearError('localColorCode')" | |||||
| type="color" | |||||
| class="form-control color-picker" | |||||
| :class="{ 'is-invalid': errors.localColorCode }" | |||||
| /> | |||||
| <span | |||||
| v-if="colorCode" | |||||
| class="color-display" | |||||
| :style="{ backgroundColor: localColorCode }" | |||||
| ></span> | |||||
| </div> | |||||
| <span> {{ localColorCode }}</span> | |||||
| <small v-if="errors.localColorCode" class="text-danger"> | |||||
| {{ errors.localColorCode }} | |||||
| </small> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </BTab> | |||||
| <BTab title="ترجمه ها"> | |||||
| <form @submit.prevent="editAttribute" class="mt-4"> | |||||
| <BButton | |||||
| :disabled="!findLocaleTranslation" | |||||
| :loading="loadingDelete" | |||||
| @click="handlerRemoveTranslation" | |||||
| class="btn btn-sm rounded btn-danger d-block" | |||||
| style="margin-right: auto" | |||||
| > | |||||
| حذف ترجمه {{ findLocaleTranslation }} | |||||
| </BButton> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان ویژگی</label> | |||||
| <input | |||||
| v-model="localTitle" | |||||
| @input="clearError('attrName')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان ویژگی" | |||||
| :class="{ 'is-invalid': errors.attrName }" | |||||
| /> | |||||
| <small v-if="errors.attrName" class="text-danger"> | |||||
| {{ errors.attrName }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeAttributeModal" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </form> | |||||
| </BTab> | |||||
| </BTabs> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -97,13 +160,15 @@ | |||||
| </template> | </template> | ||||
| <script> | <script> | ||||
| import { ref, toRef, watch } from "vue"; | |||||
| import {computed, ref, watch} from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import {BTabs} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| components: {BTabs}, | |||||
| props: { | props: { | ||||
| attributeValues: { | attributeValues: { | ||||
| type: Array, | type: Array, | ||||
| @@ -121,41 +186,64 @@ export default { | |||||
| type: String, | type: String, | ||||
| Required: true, | Required: true, | ||||
| }, | }, | ||||
| attrRow: { | |||||
| type: Object, | |||||
| required: true, | |||||
| }, | |||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const localColorName = ref(); | |||||
| const localTitle = ref(); | |||||
| const localColorCode = ref(); | const localColorCode = ref(); | ||||
| const localId = toRef(props.id); | |||||
| const localAttributeValues = toRef(props.attributeValues); | |||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const categoryId = ref(null); | |||||
| const categories = ref([]); | |||||
| const locale = ref('fa'); | |||||
| const loadingDelete = ref(false); | |||||
| watch( | |||||
| () => props.attributeValues, | |||||
| (newVal) => (localAttributeValues.value = newVal) | |||||
| ); | |||||
| watch( | |||||
| () => props.code, | |||||
| (newVal) => (localColorCode.value = newVal) | |||||
| ); | |||||
| const attrRowModel = computed({ | |||||
| get: () => props.attrRow, | |||||
| set: (newValue) => emit('update:categoryRow', newValue) | |||||
| }); | |||||
| watch( | watch( | ||||
| () => props.title, | |||||
| (newVal) => (localColorName.value = newVal) | |||||
| ); | |||||
| () => props.attrRow, | |||||
| (newVal) => { | |||||
| localTitle.value = newVal?.translation?.title | |||||
| locale.value = newVal?.translation?.locale | |||||
| categoryId.value = newVal?.category?.id | |||||
| } | |||||
| ) | |||||
| const findLocaleTranslation = computed(() => { | |||||
| const foundTranslation = attrRowModel.value?.translations?.find( | |||||
| item => item?.locale === locale.value | |||||
| ); | |||||
| if (foundTranslation) { | |||||
| switch (foundTranslation?.locale) { | |||||
| case "en": | |||||
| return "انگلیسی"; | |||||
| case "fa": | |||||
| return "فارسی"; | |||||
| case "ar": | |||||
| return "عربی"; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| watch( | |||||
| () => props.id, | |||||
| (newVal) => (localId.value = newVal) | |||||
| ); | |||||
| return null; | |||||
| }); | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!localColorName.value) | |||||
| errors.value.colorName = "وارد کردن نام رنگ ضروری می باشد"; | |||||
| if (!localColorCode.value) | |||||
| errors.value.colorCode = "انتخاب رنگ ضروری می باشد"; | |||||
| if (!localTitle.value) | |||||
| errors.value.colorName = "وارد کردن عنوان ویژگی ضروری می باشد"; | |||||
| if (!locale.value) | |||||
| errors.value.colorCode = "انتخاب زبان ضروری می باشد"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -164,7 +252,7 @@ export default { | |||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| }; | }; | ||||
| const editAttribute = () => { | |||||
| const editAttribute = async () => { | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -172,47 +260,145 @@ export default { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | |||||
| const formData = new FormData(); | |||||
| console.log(localAttributeValues.value); | |||||
| formData.append("title", localColorName.value); | |||||
| formData.append("code", localColorCode.value); | |||||
| formData.append("attribute_id", localAttributeValues.value[0].id); | |||||
| ApiServiece.put(`admin/attribute-values/${localId.value}`, formData) | |||||
| .then((resp) => { | |||||
| console.log(resp); | |||||
| toast.success("!ویژگی با موفقیت ویرایش شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | |||||
| document.getElementById("closeAttributeModal").click(); | |||||
| emit("attribute-updated"); | |||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| try { | |||||
| loading.value = true; | |||||
| const params = { title: localTitle.value, locale: locale.value }; | |||||
| const existingTranslation = attrRowModel.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| const { data } = existingTranslation | |||||
| ? await ApiServiece.put( | |||||
| `admin/attributes/${attrRowModel.value?.id}/translations/${existingTranslation?.id}`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| : await ApiServiece.post( | |||||
| `admin/attributes/${attrRowModel.value?.id}/translations`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ); | |||||
| if (data?.success) { | |||||
| const updatedCategory = props.attrRow; | |||||
| if (existingTranslation) { | |||||
| updatedCategory.translations = data?.data?.translations || []; | |||||
| } else { | |||||
| updatedCategory.translations = [ | |||||
| ...(updatedCategory.translations || []), | |||||
| ...(data?.data?.translations || []) | |||||
| ]; | |||||
| } | |||||
| attrRowModel.value = updatedCategory; | |||||
| console.log(attrRowModel.value,'attrRowModel.value') | |||||
| toast.success(data?.message) | |||||
| emit("cat-updated"); | |||||
| } | |||||
| } catch (error) { | |||||
| console.log(error,'error error'); | |||||
| toast.error(`${error?.response?.data?.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | }); | ||||
| } finally { | |||||
| loading.value = false; | |||||
| } | |||||
| }; | }; | ||||
| const handlerRemoveTranslation = async () => { | |||||
| const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === locale.value); | |||||
| if (findLocale) { | |||||
| try { | |||||
| loadingDelete.value = true; | |||||
| const { data: { success, message, data } } = await ApiServiece.delete( | |||||
| `admin/attributes/${attrRowModel.value?.id}/translations/${findLocale?.id}` | |||||
| ) | |||||
| if (success) { | |||||
| const updatedCategory = props.attrRow; | |||||
| if (findLocale) { | |||||
| updatedCategory.translations = data?.data?.translations || []; | |||||
| } else { | |||||
| updatedCategory.translations = [ | |||||
| ...(updatedCategory.translations || []), | |||||
| ...(data?.data?.translations || []) | |||||
| ]; | |||||
| } | |||||
| attrRowModel.value = updatedCategory; | |||||
| localTitle.value = null | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| }finally { | |||||
| loadingDelete.value = false | |||||
| } | |||||
| } | |||||
| } | |||||
| const getAttributes = async () => { | |||||
| try { | |||||
| const { data: { success, data } } = await ApiServiece.get(`admin/categories`) | |||||
| if (success) { | |||||
| categories.value = data; | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } | |||||
| } | |||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = attrRowModel.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| localTitle.value = findLocale?.title | |||||
| } else { | |||||
| localTitle.value = undefined | |||||
| } | |||||
| } | |||||
| getAttributes() | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| clearError, | clearError, | ||||
| editAttribute, | editAttribute, | ||||
| localColorName, | |||||
| localTitle, | |||||
| localColorCode, | localColorCode, | ||||
| categoryId, | |||||
| getAttributes, | |||||
| categories, | |||||
| attrRowModel, | |||||
| handlerChangeLocale, | |||||
| locale, | |||||
| findLocaleTranslation, | |||||
| handlerRemoveTranslation, | |||||
| loadingDelete, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -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" | v-model="locale" | ||||
| class="form-control" | class="form-control" | ||||
| placeholder="انتخاب کنید" | placeholder="انتخاب کنید" | ||||
| @change="handlerChangeLocale" | |||||
| > | > | ||||
| <option | <option | ||||
| key="fa" | key="fa" | ||||
| @@ -137,6 +136,7 @@ | |||||
| > | > | ||||
| بستن | بستن | ||||
| </button> | </button> | ||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | <button type="submit" class="btn btn-primary" :disabled="loading"> | ||||
| <span | <span | ||||
| v-if="loading" | v-if="loading" | ||||
| @@ -201,7 +201,7 @@ export default { | |||||
| const { data: { message, success, data } } = await ApiServiece.post( | const { data: { message, success, data } } = await ApiServiece.post( | ||||
| `admin/blog-categories`, | `admin/blog-categories`, | ||||
| { title: title.value }, | |||||
| { title: titleCat.value }, | |||||
| { | { | ||||
| headers: { | headers: { | ||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| @@ -246,10 +246,10 @@ export default { | |||||
| }) | }) | ||||
| .then(() => { | .then(() => { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| document.getElementById("closeAddBlogCat").click(); | |||||
| //document.getElementById("closeAddBlogCat").click(); | |||||
| emit("cat-updated"); | emit("cat-updated"); | ||||
| title.value = ""; | title.value = ""; | ||||
| step.value = 1; | |||||
| //step.value = 1; | |||||
| }, 500); | }, 500); | ||||
| }) | }) | ||||
| .catch((error) => { | .catch((error) => { | ||||
| @@ -20,7 +20,6 @@ | |||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="editCat" class="mt-4"> | <form @submit.prevent="editCat" class="mt-4"> | ||||
| <BButton | <BButton | ||||
| :disabled="!findLocaleTranslation" | :disabled="!findLocaleTranslation" | ||||
| :loading="loadingDelete" | :loading="loadingDelete" | ||||
| @@ -67,7 +66,7 @@ | |||||
| <div class="form-group"> | <div class="form-group"> | ||||
| <label class="form-label">عنوان دسته</label> | <label class="form-label">عنوان دسته</label> | ||||
| <input | <input | ||||
| v-model="title" | |||||
| v-model="titleCat" | |||||
| @input="clearError('title')" | @input="clearError('title')" | ||||
| type="text" | type="text" | ||||
| class="form-control" | class="form-control" | ||||
| @@ -112,8 +111,8 @@ | |||||
| </template> | </template> | ||||
| <script> | <script> | ||||
| import { iconData } from "../../../views/live-preview/icon/data"; | |||||
| import { ref, toRef, watch } from "vue"; | |||||
| import { iconData } from "@/views/live-preview/icon/data"; | |||||
| import {computed, ref, watch} from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| @@ -132,30 +131,31 @@ export default { | |||||
| type: String, | type: String, | ||||
| Required: true, | Required: true, | ||||
| }, | }, | ||||
| catRow: { | |||||
| type: Object, | |||||
| Required: true, | |||||
| }, | |||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const localTitle = toRef(props.title); | |||||
| const localIcon = ref(props.icon); | |||||
| const localId = toRef(props.id); | |||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const loadingDelete = ref(false); | |||||
| const titleCat = ref(null); | const titleCat = ref(null); | ||||
| const title = ref(null); | |||||
| const locale = ref("fa"); | |||||
| watch( | |||||
| () => props.title, | |||||
| (newVal) => (localTitle.value = newVal) | |||||
| ); | |||||
| const categoryRowModel = computed({ | |||||
| get: () => props.catRow, | |||||
| set: (newValue) => emit('update:categoryRow', newValue) | |||||
| }); | |||||
| watch( | watch( | ||||
| () => props.icon, | |||||
| (newVal) => (localIcon.value = newVal) | |||||
| ); | |||||
| () => props.catRow, | |||||
| (newVal) => { | |||||
| titleCat.value = newVal?.translation?.title | |||||
| watch( | |||||
| () => props.id, | |||||
| (newVal) => (localId.value = newVal) | |||||
| ); | |||||
| locale.value = newVal?.translation?.locale | |||||
| } | |||||
| ) | |||||
| const clearError = (field) => { | const clearError = (field) => { | ||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| @@ -163,17 +163,12 @@ export default { | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!localTitle.value) | |||||
| if (!titleCat.value) | |||||
| errors.value.localTitle = "وارد کردن عنوان ضروری می باشد"; | errors.value.localTitle = "وارد کردن عنوان ضروری می باشد"; | ||||
| if (!localIcon.value) errors.value.icon = "انتخاب آیکن ضروری است"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| const setSelectedIcon = (icon) => { | |||||
| localIcon.value = icon; | |||||
| }; | |||||
| const editCat = () => { | |||||
| const editCat = async () => { | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -181,48 +176,128 @@ export default { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("title", localTitle.value); | |||||
| formData.append("icon", localIcon.value); | |||||
| ApiServiece.put(`admin/blog-categories/${localId.value}`, formData) | |||||
| .then(() => { | |||||
| toast.success("!دسته با موفقیت ویرایش شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | |||||
| document.getElementById("closeEditBlogCat").click(); | |||||
| emit("cat-updated"); | |||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| try { | |||||
| loading.value = true; | |||||
| const params = { | |||||
| title: titleCat.value, | |||||
| locale: locale.value, | |||||
| } | |||||
| const existingTranslation = categoryRowModel.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| const { data } = existingTranslation | |||||
| ? await ApiServiece.put( | |||||
| `admin/blog-categories/${categoryRowModel.value?.id}/translations/${existingTranslation?.id}`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| : await ApiServiece.post( | |||||
| `admin/blog-categories/${categoryRowModel.value?.id}/translations`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ); | |||||
| console.log(data,'data data data') | |||||
| toast.success("!دسته با موفقیت ویرایش شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | }); | ||||
| } catch (e) { | |||||
| toast.error(`${e?.response?.data?.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }) | |||||
| } finally { | |||||
| loading.value = false; | |||||
| } | |||||
| }; | }; | ||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| titleCat.value = findLocale?.title | |||||
| } else { | |||||
| titleCat.value = undefined | |||||
| } | |||||
| } | |||||
| const findLocaleTranslation = computed(() => { | |||||
| const foundTranslation = categoryRowModel.value?.translations?.find( | |||||
| item => item?.locale === locale.value | |||||
| ); | |||||
| if (foundTranslation) { | |||||
| switch (foundTranslation?.locale) { | |||||
| case "en": | |||||
| return "انگلیسی"; | |||||
| case "fa": | |||||
| return "فارسی"; | |||||
| case "ar": | |||||
| return "عربی"; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }); | |||||
| const handlerRemoveTranslation = async () => { | |||||
| const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === locale.value); | |||||
| if (findLocale) { | |||||
| try { | |||||
| loadingDelete.value = true; | |||||
| const { data: { success, message, data } } = await ApiServiece.delete( | |||||
| `admin/blog-categories/${categoryRowModel.value?.id}/translations/${findLocale?.id}` | |||||
| ) | |||||
| if (success) { | |||||
| const updatedCategory = props.catRow | |||||
| updatedCategory.translations = data?.translations | |||||
| titleCat.value = null | |||||
| categoryRowModel.value = updatedCategory | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e) | |||||
| }finally { | |||||
| loadingDelete.value = false | |||||
| } | |||||
| } | |||||
| } | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| clearError, | clearError, | ||||
| editCat, | editCat, | ||||
| localTitle, | |||||
| iconData, | iconData, | ||||
| setSelectedIcon, | |||||
| localIcon, | |||||
| localId, | |||||
| title, | |||||
| titleCat, | titleCat, | ||||
| locale, | |||||
| handlerChangeLocale, | |||||
| findLocaleTranslation, | |||||
| handlerRemoveTranslation, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -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> | }}</small> | ||||
| </div> | </div> | ||||
| </BCol> | </BCol> | ||||
| <ConfigCountriesSelect v-model:country-code="localCountryCode"/> | |||||
| </BRow> | </BRow> | ||||
| <div | <div | ||||
| @@ -120,8 +122,10 @@ import ApiServiece from "@/services/ApiService"; | |||||
| import { ref, toRef, watch } from "vue"; | import { ref, toRef, watch } from "vue"; | ||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ConfigCountriesSelect from "@/components/ConfigCountriesSelect.vue"; | |||||
| export default { | export default { | ||||
| components: {ConfigCountriesSelect}, | |||||
| props: { | props: { | ||||
| name: { | name: { | ||||
| type: String, | type: String, | ||||
| @@ -139,6 +143,10 @@ export default { | |||||
| type: String, | type: String, | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| countryCode: { | |||||
| type: Number, | |||||
| required: true, | |||||
| }, | |||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| @@ -146,6 +154,7 @@ export default { | |||||
| const localName = toRef(props.name); | const localName = toRef(props.name); | ||||
| const localMobile = toRef(props.mobile); | const localMobile = toRef(props.mobile); | ||||
| const localRole = toRef(props.role); | const localRole = toRef(props.role); | ||||
| const localCountryCode = toRef(props.countryCode); | |||||
| const localId = toRef(props.id); | const localId = toRef(props.id); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| @@ -168,12 +177,22 @@ export default { | |||||
| (newVal) => (localRole.value = newVal) | (newVal) => (localRole.value = newVal) | ||||
| ); | ); | ||||
| watch( | |||||
| () => props.countryCode, | |||||
| (newVal) => { | |||||
| localCountryCode.value = Number(newVal); | |||||
| } | |||||
| ); | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!localMobile.value) | if (!localMobile.value) | ||||
| errors.value.localMobile = "وارد کردن موبایل ضروری می باشد"; | errors.value.localMobile = "وارد کردن موبایل ضروری می باشد"; | ||||
| if (!localRole.value) | if (!localRole.value) | ||||
| errors.value.localMobile = "انتخاب کردن نقش کاربر ضروری می باشد"; | errors.value.localMobile = "انتخاب کردن نقش کاربر ضروری می باشد"; | ||||
| if (!localCountryCode.value) | |||||
| errors.value.localCountryCode = "انتخاب کردن پیش شماره ضروری می باشد"; | |||||
| if (password.value && password.value.length < 8) { | if (password.value && password.value.length < 8) { | ||||
| errors.value.password = " رمز عبور باید حداقل 8 کاراکتر باشد"; | errors.value.password = " رمز عبور باید حداقل 8 کاراکتر باشد"; | ||||
| } | } | ||||
| @@ -202,12 +221,13 @@ export default { | |||||
| if (password.value) { | if (password.value) { | ||||
| formData.append("password", password.value); | formData.append("password", password.value); | ||||
| } | } | ||||
| formData.append("country_config_id", localCountryCode.value) | |||||
| ApiServiece.put(`admin/users/${localId.value}`, formData) | ApiServiece.put(`admin/users/${localId.value}`, formData) | ||||
| .then(() => { | .then(() => { | ||||
| toast.success("!کاربر با موفقیت ویرایش شد", { | toast.success("!کاربر با موفقیت ویرایش شد", { | ||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| onClose: () => emit("user-updated"), | |||||
| }); | }); | ||||
| }) | }) | ||||
| @@ -238,6 +258,7 @@ export default { | |||||
| password, | password, | ||||
| editUser, | editUser, | ||||
| localRole, | localRole, | ||||
| localCountryCode, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -23,6 +23,15 @@ export default [ | |||||
| }, | }, | ||||
| component: () => import("../views/live-preview/pages/users/users.vue"), | component: () => import("../views/live-preview/pages/users/users.vue"), | ||||
| }, | }, | ||||
| { | |||||
| path: "/countries", | |||||
| name: "countries", | |||||
| meta: { | |||||
| title: "کشورها", | |||||
| requiresAuth: true, | |||||
| }, | |||||
| component: () => import("../views/live-preview/pages/countries/countries.vue"), | |||||
| }, | |||||
| { | { | ||||
| path: "/brands", | path: "/brands", | ||||
| name: "brands", | name: "brands", | ||||
| @@ -51,6 +60,15 @@ export default [ | |||||
| component: () => | component: () => | ||||
| import("../views/live-preview/pages/attributes/attributes.vue"), | import("../views/live-preview/pages/attributes/attributes.vue"), | ||||
| }, | }, | ||||
| { | |||||
| path: "/attributes-value/:id", | |||||
| name: "attributes-value", | |||||
| meta: { | |||||
| title: "مقدار ویژگی", | |||||
| requiresAuth: true, | |||||
| }, | |||||
| component: () => import("../views/live-preview/pages/attributes-value/attributes-value.vue"), | |||||
| }, | |||||
| { | { | ||||
| path: "/blogCat", | path: "/blogCat", | ||||
| name: "blogCat", | name: "blogCat", | ||||
| @@ -290,9 +308,7 @@ export default [ | |||||
| }, | }, | ||||
| component: () => import("../views/live-preview/pages/settings/setting.vue"), | component: () => import("../views/live-preview/pages/settings/setting.vue"), | ||||
| }, | }, | ||||
| // Auth | // Auth | ||||
| { | { | ||||
| path: "/otpLogin", | path: "/otpLogin", | ||||
| name: "otpLogin", | name: "otpLogin", | ||||
| @@ -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 Swal from "sweetalert2"; | ||||
| import addAttribute from "@/components/modals/attribute/addAttribute.vue"; | import addAttribute from "@/components/modals/attribute/addAttribute.vue"; | ||||
| import editAttribute from "@/components/modals/attribute/editAttribute.vue"; | import editAttribute from "@/components/modals/attribute/editAttribute.vue"; | ||||
| import router from "@/router"; | |||||
| export default { | export default { | ||||
| name: "BORDER", | name: "BORDER", | ||||
| components: { | components: { | ||||
| @@ -28,6 +29,8 @@ export default { | |||||
| const attributeTitle = ref(); | const attributeTitle = ref(); | ||||
| const attributeId = ref(); | const attributeId = ref(); | ||||
| const attributeCode = ref(); | const attributeCode = ref(); | ||||
| const attrRow = ref(null) | |||||
| let searchTimeout = null; | let searchTimeout = null; | ||||
| const convertToJalali = (date) => { | const convertToJalali = (date) => { | ||||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | return moment(date, "YYYY-MM-DD HH:mm:ss") | ||||
| @@ -48,7 +51,7 @@ export default { | |||||
| const getAttributes = () => { | const getAttributes = () => { | ||||
| filterLoading.value = true; | filterLoading.value = true; | ||||
| ApiServiece.get( | ApiServiece.get( | ||||
| `admin/attribute-values?attribute_id=1&title=${encodeURIComponent( | |||||
| `admin/attributes?attribute_id=1&title=${encodeURIComponent( | |||||
| searchQuery.value || "" | searchQuery.value || "" | ||||
| )}&code=${encodeURIComponent(searchQuery.value || "")} | )}&code=${encodeURIComponent(searchQuery.value || "")} | ||||
| &paginate=${paginate.value || 10}&page=${page.value || 1} | &paginate=${paginate.value || 10}&page=${page.value || 1} | ||||
| @@ -56,11 +59,9 @@ export default { | |||||
| ) | ) | ||||
| .then((resp) => { | .then((resp) => { | ||||
| filterLoading.value = false; | filterLoading.value = false; | ||||
| console.log(resp.data); | |||||
| attributes.value = resp.data.data.data; | attributes.value = resp.data.data.data; | ||||
| currentPage.value = resp.data.data.current_page; | currentPage.value = resp.data.data.current_page; | ||||
| totalPages.value = resp.data.data.last_page; | totalPages.value = resp.data.data.last_page; | ||||
| console.log(attributes.value); | |||||
| }) | }) | ||||
| .catch(() => { | .catch(() => { | ||||
| filterLoading.value = false; | filterLoading.value = false; | ||||
| @@ -123,13 +124,14 @@ export default { | |||||
| } | } | ||||
| return pages; | return pages; | ||||
| }); | }); | ||||
| watch(page, () => { | watch(page, () => { | ||||
| getAttributes(); | getAttributes(); | ||||
| }); | }); | ||||
| const deleteAttribute = (id, title) => { | const deleteAttribute = (id, title) => { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید رنگ ${title} را حذف کنید ؟`, | |||||
| text: `می خواهید رنگ ${title ?? ''} را حذف کنید ؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -138,7 +140,7 @@ export default { | |||||
| cancelButtonText: "خیر", | cancelButtonText: "خیر", | ||||
| }).then((result) => { | }).then((result) => { | ||||
| if (result.isConfirmed) { | if (result.isConfirmed) { | ||||
| ApiServiece.delete(`admin/attribute-values/${id}`) | |||||
| ApiServiece.delete(`admin/attributes/${id}`) | |||||
| .then(() => { | .then(() => { | ||||
| toast.success("!ویژگی با موفقیت حذف شد", { | toast.success("!ویژگی با موفقیت حذف شد", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -159,13 +161,9 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const editModalData = (id, title, code) => { | |||||
| attributeId.value = id; | |||||
| attributeTitle.value = title; | |||||
| attributeCode.value = code; | |||||
| }; | |||||
| const editModalData = attr => { | |||||
| attrRow.value = attr | |||||
| } | |||||
| const getAttributeValues = () => { | const getAttributeValues = () => { | ||||
| ApiServiece.get(`admin/attributes`).then((resp) => { | ApiServiece.get(`admin/attributes`).then((resp) => { | ||||
| @@ -174,10 +172,15 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const redirectToAttrValue = id => { | |||||
| router.push(`/attributes-value/${id}`) | |||||
| } | |||||
| onMounted(() => { | onMounted(() => { | ||||
| getAttributes(); | getAttributes(); | ||||
| getAttributeValues(); | getAttributeValues(); | ||||
| }); | }); | ||||
| return { | return { | ||||
| attributes, | attributes, | ||||
| convertToJalali, | convertToJalali, | ||||
| @@ -199,10 +202,13 @@ export default { | |||||
| nextPage, | nextPage, | ||||
| handlePageInput, | handlePageInput, | ||||
| visiblePages, | visiblePages, | ||||
| attrRow, | |||||
| redirectToAttrValue | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| </script> | </script> | ||||
| <template> | <template> | ||||
| <Layout> | <Layout> | ||||
| <BRow> | <BRow> | ||||
| @@ -227,7 +233,7 @@ export default { | |||||
| data-bs-target="#addAttribute" | data-bs-target="#addAttribute" | ||||
| class="btn btn-light text-primary btn-sm px-3" | class="btn btn-light text-primary btn-sm px-3" | ||||
| > | > | ||||
| افزودن رنگ | |||||
| افزودن ویژگی | |||||
| </button> | </button> | ||||
| </div> | </div> | ||||
| <div v-if="!filterLoading" class="card-body table-border-style p-0"> | <div v-if="!filterLoading" class="card-body table-border-style p-0"> | ||||
| @@ -235,46 +241,40 @@ export default { | |||||
| <table class="table table-hover table-bordered m-0" dir="rtl"> | <table class="table table-hover table-bordered m-0" dir="rtl"> | ||||
| <thead class="table-light"> | <thead class="table-light"> | ||||
| <tr> | <tr> | ||||
| <th>نام</th> | |||||
| <th>رنگ</th> | |||||
| <th>کد رنگ</th> | |||||
| <th>تاریخ ایجاد</th> | <th>تاریخ ایجاد</th> | ||||
| <th>نام</th> | |||||
| <th>دسته بندی</th> | |||||
| <th>عملیات</th> | <th>عملیات</th> | ||||
| </tr> | </tr> | ||||
| </thead> | </thead> | ||||
| <tbody> | <tbody> | ||||
| <tr v-for="attribute in attributes" :key="attribute.id"> | <tr v-for="attribute in attributes" :key="attribute.id"> | ||||
| <td>{{ attribute.title }}</td> | |||||
| <td | |||||
| :style="{ | |||||
| backgroundColor: attribute.code, | |||||
| textAlign: 'center', | |||||
| }" | |||||
| ></td> | |||||
| <td>{{ attribute.code }}</td> | |||||
| <td>{{ convertToJalali(attribute?.created_at) }}</td> | <td>{{ convertToJalali(attribute?.created_at) }}</td> | ||||
| <td>{{ attribute?.translation?.title }}</td> | |||||
| <td>{{ attribute?.category?.translation?.title }}</td> | |||||
| <td> | <td> | ||||
| <!-- <button | |||||
| <button | |||||
| @click=" | @click=" | ||||
| editModalData( | |||||
| attribute?.id, | |||||
| attribute?.title, | |||||
| attribute.code | |||||
| ) | |||||
| editModalData(attribute) | |||||
| " | " | ||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| data-bs-target="#editAttribute" | data-bs-target="#editAttribute" | ||||
| class="btn btn-sm btn-outline-warning me-1" | class="btn btn-sm btn-outline-warning me-1" | ||||
| > | > | ||||
| ویرایش | ویرایش | ||||
| </button> --> | |||||
| </button> | |||||
| <button | <button | ||||
| @click="deleteAttribute(attribute.id, attribute.title)" | |||||
| @click="deleteAttribute(attribute?.id, attribute?.translation?.title)" | |||||
| class="btn btn-sm btn-outline-danger" | class="btn btn-sm btn-outline-danger" | ||||
| > | > | ||||
| حذف | حذف | ||||
| </button> | </button> | ||||
| <button | |||||
| @click="redirectToAttrValue(attribute?.id)" | |||||
| class="btn btn-sm btn-outline-primary me-1" | |||||
| > | |||||
| مقدار ویژگی ها | |||||
| </button> | |||||
| </td> | </td> | ||||
| </tr> | </tr> | ||||
| </tbody> | </tbody> | ||||
| @@ -296,6 +296,7 @@ export default { | |||||
| :title="attributeTitle" | :title="attributeTitle" | ||||
| :code="attributeCode" | :code="attributeCode" | ||||
| :attributeValues="attributeValues" | :attributeValues="attributeValues" | ||||
| :attrRow="attrRow" | |||||
| @attribute-updated="handleAttributeUpdated()" | @attribute-updated="handleAttributeUpdated()" | ||||
| /> | /> | ||||
| </BRow> | </BRow> | ||||
| @@ -58,339 +58,395 @@ | |||||
| </div> | </div> | ||||
| </BCardHeader> | </BCardHeader> | ||||
| <BCardBody> | <BCardBody> | ||||
| <BRow class="g-3"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بنر" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">پنل نمایش</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="pannel" | |||||
| @change="clearError('pannel')" | |||||
| :class="{ 'is-invalid': errors.pannel }" | |||||
| placeholder="انتخاب پنل" | |||||
| > | |||||
| <option value="wholesale">پنل عمده فروشی</option> | |||||
| <option value="web">وب سایت و اپلیکیشن</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pannel" class="text-danger"> | |||||
| {{ errors.pannel }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نمایش در</label> | |||||
| <select | |||||
| v-model="pageType" | |||||
| :class="{ 'is-invalid': errors.pageType }" | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| placeholder="انتخاب صفحه" | |||||
| @change="clearError('pageType')" | |||||
| > | |||||
| <option value="main_page">صفحه اصلی</option> | |||||
| <option value="category">صفحه دسته</option> | |||||
| <option value="special_page">صفحه فروش ویژه</option> | |||||
| <option value="brand">صفحه برند</option> | |||||
| <option value="blog_page">صفحه بلاگ</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pageType" class="text-danger"> | |||||
| {{ errors.pageType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedCatPage" | |||||
| :isLoading="categoryPageSelectorLoader" | |||||
| :options="formattedCategoriesPages" | |||||
| @change="clearError('selectedCatPage')" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleCategoryPageSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedCatPage" class="text-danger"> | |||||
| {{ errors.selectedCatPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه برند</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedBrandPage" | |||||
| :isLoading="brandSelectorLoader" | |||||
| :options="formattedBrands" | |||||
| @change="clearError(`selectedBrandPage`)" | |||||
| placeholder="برندی را انتخاب کنید" | |||||
| @search="handleBrandSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedBrandPage" class="text-danger"> | |||||
| {{ errors.selectedBrandPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel != 'wholesale' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب صفحه فرود</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="landingType" | |||||
| @change="clearError('landingType')" | |||||
| :class="{ 'is-invalid': errors.landingType }" | |||||
| placeholder="انتخاب صفحه فرود" | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'ایجاد بنر', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| > | |||||
| <template #1> | |||||
| <BRow class="g-3"> | |||||
| <BCol> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بنر" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <!-- <BCol md="6">--> | |||||
| <!-- <div class="form-group">--> | |||||
| <!-- <label class="form-label">پنل نمایش</label>--> | |||||
| <!-- <select--> | |||||
| <!-- class="form-select"--> | |||||
| <!-- aria-label="Default select example"--> | |||||
| <!-- v-model="pannel"--> | |||||
| <!-- @change="clearError('pannel')"--> | |||||
| <!-- :class="{ 'is-invalid': errors.pannel }"--> | |||||
| <!-- placeholder="انتخاب پنل"--> | |||||
| <!-- >--> | |||||
| <!-- <option value="wholesale">پنل عمده فروشی</option>--> | |||||
| <!-- <option value="web">وب سایت و اپلیکیشن</option>--> | |||||
| <!-- </select>--> | |||||
| <!-- </div>--> | |||||
| <!-- <small v-if="errors.pannel" class="text-danger">--> | |||||
| <!-- {{ errors.pannel }}--> | |||||
| <!-- </small>--> | |||||
| <!-- </BCol>--> | |||||
| <BCol v-if="pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نمایش در</label> | |||||
| <select | |||||
| v-model="pageType" | |||||
| :class="{ 'is-invalid': errors.pageType }" | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| placeholder="انتخاب صفحه" | |||||
| @change="clearError('pageType')" | |||||
| > | |||||
| <option value="main_page">صفحه اصلی</option> | |||||
| <option value="category">صفحه دسته</option> | |||||
| <option value="special_page">صفحه فروش ویژه</option> | |||||
| <option value="brand">صفحه برند</option> | |||||
| <option value="blog_page">صفحه بلاگ</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pageType" class="text-danger"> | |||||
| {{ errors.pageType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedCatPage" | |||||
| :isLoading="categoryPageSelectorLoader" | |||||
| :options="formattedCategoriesPages" | |||||
| @change="clearError('selectedCatPage')" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleCategoryPageSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedCatPage" class="text-danger"> | |||||
| {{ errors.selectedCatPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه برند</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedBrandPage" | |||||
| :isLoading="brandSelectorLoader" | |||||
| :options="formattedBrands" | |||||
| @change="clearError(`selectedBrandPage`)" | |||||
| placeholder="برندی را انتخاب کنید" | |||||
| @search="handleBrandSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedBrandPage" class="text-danger"> | |||||
| {{ errors.selectedBrandPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel != 'wholesale' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب صفحه فرود</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="landingType" | |||||
| @change="clearError('landingType')" | |||||
| :class="{ 'is-invalid': errors.landingType }" | |||||
| placeholder="انتخاب صفحه فرود" | |||||
| > | |||||
| <option value="product">صفحه محصولات</option> | |||||
| <option value="cat">صفحه دسته ها</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.landingType" class="text-danger"> | |||||
| {{ errors.landingType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="landingType === 'product'" | |||||
| sm="6" | |||||
| class="mt-3" | |||||
| style="margin-top: 30px" | |||||
| > | > | ||||
| <option value="product">صفحه محصولات</option> | |||||
| <option value="cat">صفحه دسته ها</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.landingType" class="text-danger"> | |||||
| {{ errors.landingType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="landingType === 'product'" | |||||
| sm="6" | |||||
| class="mt-3" | |||||
| style="margin-top: 30px" | |||||
| > | |||||
| <label for="token">صفحه کدام محصول</label> | |||||
| <VueSelect | |||||
| style=" | |||||
| <label for="token">صفحه کدام محصول</label> | |||||
| <VueSelect | |||||
| style=" | |||||
| --vs-min-height: 48px; | --vs-min-height: 48px; | ||||
| --vs-border-radius: 8px; | --vs-border-radius: 8px; | ||||
| margin-top: 7px; | margin-top: 7px; | ||||
| " | " | ||||
| v-model="selectedLandingProduct" | |||||
| :isLoading="productSelectorLoader" | |||||
| @change="clearError(`selectedLandingProduct`)" | |||||
| :options="formattedProducts" | |||||
| placeholder="محصولی را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| <small v-if="errors.selectedLandingProduct" class="text-danger"> | |||||
| {{ errors.selectedLandingProduct }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="landingType === 'cat'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه کدام دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedLandingCat" | |||||
| @change="clearError('selectedLandingCat')" | |||||
| :isLoading="categorySelectorLoader" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleCategorySearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedLandingCat" class="text-danger"> | |||||
| {{ errors.selectedLandingCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'main_page' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه اصلی</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه دسته ها</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'special_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه فروش ویژه</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Slideshow</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'blog_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه بلاگ</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| v-model="selectedLandingProduct" | |||||
| :isLoading="productSelectorLoader" | |||||
| @change="clearError(`selectedLandingProduct`)" | |||||
| :options="formattedProducts" | |||||
| placeholder="محصولی را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| <small v-if="errors.selectedLandingProduct" class="text-danger"> | |||||
| {{ errors.selectedLandingProduct }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="landingType === 'cat'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه کدام دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedLandingCat" | |||||
| @change="clearError('selectedLandingCat')" | |||||
| :isLoading="categorySelectorLoader" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleCategorySearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedLandingCat" class="text-danger"> | |||||
| {{ errors.selectedLandingCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'main_page' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه اصلی</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه دسته ها</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'special_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | > | ||||
| <option value="A">A-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه برند</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه فروش ویژه</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Slideshow</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'blog_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | > | ||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6" v-if="pannel"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بنر</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageUpload" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </BCardBody> | |||||
| <BCardFooter> | |||||
| <div class="d-flex justify-content-center"> | |||||
| <div class="text-center"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه بلاگ</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه برند</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <button | <button | ||||
| type="submit" | |||||
| class="btn btn-primary" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| type="submit" | |||||
| class="btn btn-primary mt-5" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| > | > | ||||
| <span v-if="loading"> | |||||
| <span v-if="loading"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | |||||
| </span> | |||||
| <span v-else>ایجاد</span> | |||||
| </button> | |||||
| </template> | |||||
| <template #2> | |||||
| <BRow> | |||||
| <BCol cols="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بنر</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageUpload" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <button | |||||
| @click.prevent="submitTranslation" | |||||
| :disabled="loadingFinally" | |||||
| class="btn btn-primary mt-5" | |||||
| > | |||||
| <span v-if="loadingFinally"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | <i class="fa fa-spinner fa-spin"></i> بارگذاری... | ||||
| </span> | </span> | ||||
| <span v-else>ایجاد</span> | <span v-else>ایجاد</span> | ||||
| </button> | </button> | ||||
| </div> | |||||
| </div> | |||||
| </BCardFooter> | |||||
| </template> | |||||
| </Steppy> | |||||
| </BCardBody> | |||||
| </BCard> | </BCard> | ||||
| </BCol> | </BCol> | ||||
| </BRow> | </BRow> | ||||
| @@ -413,10 +469,12 @@ import "vue3-toastify/dist/index.css"; | |||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import { ref, computed } from "vue"; | import { ref, computed } from "vue"; | ||||
| import Layout from "@/layout/custom.vue"; | import Layout from "@/layout/custom.vue"; | ||||
| import {Steppy} from "vue3-steppy"; | |||||
| export default { | export default { | ||||
| name: "SAMPLE-PAGE", | name: "SAMPLE-PAGE", | ||||
| components: { | components: { | ||||
| Steppy, | |||||
| Layout, | Layout, | ||||
| mainPageBanner, | mainPageBanner, | ||||
| catBanner, | catBanner, | ||||
| @@ -437,7 +495,7 @@ export default { | |||||
| const selectedLandingCat = ref(); | const selectedLandingCat = ref(); | ||||
| const selectedLandingProduct = ref(); | const selectedLandingProduct = ref(); | ||||
| const selectedLoc = ref(); | const selectedLoc = ref(); | ||||
| const pannel = ref(); | |||||
| const pannel = ref('web'); | |||||
| const image = ref(); | const image = ref(); | ||||
| const imagePreview = ref(); | const imagePreview = ref(); | ||||
| const productSelectorLoader = ref(false); | const productSelectorLoader = ref(false); | ||||
| @@ -446,6 +504,10 @@ export default { | |||||
| const brandSelectorLoader = ref(false); | const brandSelectorLoader = ref(false); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const step = ref(1); | |||||
| const locale = ref('fa'); | |||||
| const loadingFinally = ref(false); | |||||
| const bannerId = ref(null); | |||||
| const handleSearch = async (searchTerm) => { | const handleSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | if (searchTerm.length < 3) return; | ||||
| @@ -465,10 +527,10 @@ export default { | |||||
| }; | }; | ||||
| const formattedBrands = computed(() => | const formattedBrands = computed(() => | ||||
| Array.isArray(brands.value) // ✅ Check if products.value is an array | |||||
| Array.isArray(brands.value) | |||||
| ? brands.value.map((brand) => ({ | ? brands.value.map((brand) => ({ | ||||
| value: brand.id, | value: brand.id, | ||||
| label: brand.title, | |||||
| label: brand?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -490,10 +552,10 @@ export default { | |||||
| }; | }; | ||||
| const formattedProducts = computed(() => | const formattedProducts = computed(() => | ||||
| Array.isArray(products.value) // ✅ Check if products.value is an array | |||||
| Array.isArray(products.value) | |||||
| ? products.value.map((product) => ({ | ? products.value.map((product) => ({ | ||||
| value: product.id, | |||||
| label: product.title, | |||||
| value: product?.id, | |||||
| label: product?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -516,9 +578,9 @@ export default { | |||||
| const formattedCategories = computed(() => | const formattedCategories = computed(() => | ||||
| Array.isArray(cats.value) | Array.isArray(cats.value) | ||||
| ? cats.value.map((cat) => ({ | |||||
| value: cat.id, | |||||
| label: cat.title, | |||||
| ? cats.value?.map((cat) => ({ | |||||
| value: cat?.id, | |||||
| label: cat?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -542,8 +604,8 @@ export default { | |||||
| const formattedCategoriesPages = computed(() => | const formattedCategoriesPages = computed(() => | ||||
| Array.isArray(categoryPages.value) | Array.isArray(categoryPages.value) | ||||
| ? categoryPages.value.map((categoryPage) => ({ | ? categoryPages.value.map((categoryPage) => ({ | ||||
| value: categoryPage.id, | |||||
| label: categoryPage.title, | |||||
| value: categoryPage?.id, | |||||
| label: categoryPage?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -593,7 +655,7 @@ export default { | |||||
| if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید"; | if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید"; | ||||
| if (!image.value) errors.value.image = "عکس بنر را وارد نمایید"; | |||||
| // if (!image.value) errors.value.image = "عکس بنر را وارد نمایید"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -611,65 +673,72 @@ export default { | |||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| const params = {} | |||||
| params.title = title.value; | |||||
| if (pageType.value === "category") { | if (pageType.value === "category") { | ||||
| formData.append("page_id", selectedCatPage.value); | |||||
| params.page_id = selectedCatPage.value | |||||
| } | } | ||||
| if (pageType.value === "brand") { | if (pageType.value === "brand") { | ||||
| formData.append("page_id", selectedBrandPage.value); | |||||
| params.page_id = selectedBrandPage.value | |||||
| } | } | ||||
| if (landingType.value === "product" && pannel.value != "wholesale") { | |||||
| formData.append("product_id", selectedLandingProduct.value); | |||||
| if (landingType.value === "product") { | |||||
| params.page_id = selectedLandingProduct.value | |||||
| } | } | ||||
| if (landingType.value === "cat" && pannel.value != "wholesale") { | |||||
| formData.append("category_id", selectedLandingCat.value); | |||||
| if (landingType.value === "cat") { | |||||
| params.category_id = selectedLandingCat.value | |||||
| } | } | ||||
| if (selectedLoc.value === "A") { | if (selectedLoc.value === "A") { | ||||
| formData.append("type", pageType.value === 'blog_page' ? "banner" :"slider"); | |||||
| params.type = pageType.value === 'blog_page' ? "banner" :"slider" | |||||
| } | } | ||||
| if (selectedLoc.value !== "A") { | if (selectedLoc.value !== "A") { | ||||
| formData.append("type", "banner"); | |||||
| params.type = "banner" | |||||
| } | } | ||||
| if (pannel.value === "wholesale") { | if (pannel.value === "wholesale") { | ||||
| formData.append("type", "slider"); | |||||
| formData.append("location", "A"); | |||||
| params.type = "slider" | |||||
| params.location = "A" | |||||
| } | } | ||||
| if (selectedLoc.value) { | if (selectedLoc.value) { | ||||
| formData.append("location", selectedLoc.value); | |||||
| params.location = selectedLoc.value | |||||
| } | } | ||||
| formData.append("panel", pannel.value); | |||||
| params.panel = pannel.value | |||||
| if (pannel.value == "wholesale") { | if (pannel.value == "wholesale") { | ||||
| formData.append("page_type", "main_page"); | |||||
| params.page_type = 'main_page' | |||||
| } | } | ||||
| if (pannel.value !== "wholesale") { | if (pannel.value !== "wholesale") { | ||||
| formData.append("page_type", pageType.value); | |||||
| params.page_type = pageType.value | |||||
| } | } | ||||
| formData.append("image", image.value); | |||||
| formData.append("sort", 1); | |||||
| params.sort = 1 | |||||
| ApiServiece.post(`admin/banners`, formData, { | |||||
| ApiServiece.post(`admin/banners`, params, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| .then((resp) => { | |||||
| toast.success("!بنر با موفقیت اضافه شد", { | |||||
| .then(({ data }) => { | |||||
| toast.success(data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| console.log(resp); | |||||
| bannerId.value = data?.data?.id; | |||||
| step.value++ | |||||
| loading.value = false; | loading.value = false; | ||||
| resetForm(); | resetForm(); | ||||
| @@ -683,6 +752,36 @@ export default { | |||||
| }); | }); | ||||
| }); | }); | ||||
| }; | }; | ||||
| const submitTranslation = async () => { | |||||
| try { | |||||
| loadingFinally.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("image", image.value); | |||||
| formData.append("locale", locale.value); | |||||
| const { data: { message, success } } = await ApiServiece.post(`admin/banners/${bannerId.value}/translations`,formData,{ | |||||
| headers: { | |||||
| 'content-type': 'multipart/form-data', | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| //image.value = null | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingFinally.value = false; | |||||
| } | |||||
| } | |||||
| const resetForm = () => { | const resetForm = () => { | ||||
| title.value = ""; | title.value = ""; | ||||
| selectedCatPage.value = ""; | selectedCatPage.value = ""; | ||||
| @@ -729,6 +828,11 @@ export default { | |||||
| brandSelectorLoader, | brandSelectorLoader, | ||||
| formattedBrands, | formattedBrands, | ||||
| handleBrandSearch, | handleBrandSearch, | ||||
| step, | |||||
| locale, | |||||
| submitTranslation, | |||||
| loadingFinally, | |||||
| bannerId | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -203,7 +203,7 @@ export default { | |||||
| case 'category': | case 'category': | ||||
| return 'دسته ' + banner?.category_page?.title; | return 'دسته ' + banner?.category_page?.title; | ||||
| case 'brand': | case 'brand': | ||||
| return 'برند ' + banner?.brand_page?.title | |||||
| return 'برند ' + banner?.brand_page?.translation?.title | |||||
| case 'main_page': | case 'main_page': | ||||
| return 'صفحه اصلی' | return 'صفحه اصلی' | ||||
| case 'special_page': | case 'special_page': | ||||
| @@ -215,10 +215,10 @@ export default { | |||||
| const setProductOrCategory = (banner) => { | const setProductOrCategory = (banner) => { | ||||
| if (banner?.category_id) { | if (banner?.category_id) { | ||||
| return `دسته<br>${banner?.category?.title}`; | |||||
| return `دسته<br>${banner?.category?.translation?.title ?? 'ندارد'}`; | |||||
| } | } | ||||
| if (banner?.product_id) { | if (banner?.product_id) { | ||||
| return `محصول<br>${banner?.product?.title}`; | |||||
| return `محصول<br>${banner?.product?.translation?.title ?? 'ندارد'}`; | |||||
| } | } | ||||
| return ""; | return ""; | ||||
| }; | }; | ||||
| @@ -58,350 +58,376 @@ | |||||
| </div> | </div> | ||||
| </BCardHeader> | </BCardHeader> | ||||
| <BCardBody> | <BCardBody> | ||||
| <BRow class="g-3"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بنر" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">پنل نمایش</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="pannel" | |||||
| @change="clearError('pannel')" | |||||
| :class="{ 'is-invalid': errors.pannel }" | |||||
| placeholder="انخاب پنل" | |||||
| > | |||||
| <option value="wholesale">پنل عمده فروشی</option> | |||||
| <option value="web">وب سایت و اپلیکیشن</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pannel" class="text-danger"> | |||||
| {{ errors.pannel }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel === 'web' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نمایش در</label> | |||||
| <select | |||||
| v-model="pageType" | |||||
| :class="{ 'is-invalid': errors.pageType }" | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| @change="clearError('pageType')" | |||||
| placeholder="انخاب صفحه اصلی" | |||||
| > | |||||
| <option value="main_page">صفحه اصلی</option> | |||||
| <option value="category">صفحه دسته</option> | |||||
| <option value="special_page">صفحه فروش ویژه</option> | |||||
| <option value="brand">صفحه برند</option> | |||||
| <option value="blog_page">صفحه بلاگ</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pageType" class="text-danger"> | |||||
| {{ errors.pageType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedCatPage" | |||||
| @change="clearError('selectedCatPage')" | |||||
| label="label" | |||||
| :isLoading="categoryPageSelectorLoader" | |||||
| :reduce="(option) => option.value" | |||||
| :options="formattedCatPages" | |||||
| @search="handleCategoryPageSearch" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedCatPage" class="text-danger"> | |||||
| {{ errors.selectedCatPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه برند</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedBrandPage" | |||||
| label="label" | |||||
| :isLoading="brandSelectorLoader" | |||||
| :reduce="(option) => option.value" | |||||
| :options="formattedBrands" | |||||
| @change="clearError(`selectedBrandPage`)" | |||||
| placeholder="برندی را انتخاب کنید" | |||||
| @search="handleBrandSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedBrandPage" class="text-danger"> | |||||
| {{ errors.selectedBrandPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel == 'web' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب صفحه فرود</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="landingType" | |||||
| @change="clearError('landingType')" | |||||
| :class="{ 'is-invalid': errors.landingType }" | |||||
| placeholder="انتخاب صفحه فرود" | |||||
| <BTabs> | |||||
| <BTab title="ویرایش بنر"> | |||||
| <BRow class="g-3 mt-2"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بنر" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel === 'web' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نمایش در</label> | |||||
| <select | |||||
| v-model="pageType" | |||||
| :class="{ 'is-invalid': errors.pageType }" | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| @change="clearError('pageType')" | |||||
| placeholder="انخاب صفحه اصلی" | |||||
| > | |||||
| <option value="main_page">صفحه اصلی</option> | |||||
| <option value="category">صفحه دسته</option> | |||||
| <option value="special_page">صفحه فروش ویژه</option> | |||||
| <option value="brand">صفحه برند</option> | |||||
| <option value="blog_page">صفحه بلاگ</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.pageType" class="text-danger"> | |||||
| {{ errors.pageType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedCatPage" | |||||
| @change="clearError('selectedCatPage')" | |||||
| label="label" | |||||
| :isLoading="categoryPageSelectorLoader" | |||||
| :reduce="(option) => option.value" | |||||
| :options="formattedCatPages" | |||||
| @search="handleCategoryPageSearch" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedCatPage" class="text-danger"> | |||||
| {{ errors.selectedCatPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه برند</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedBrandPage" | |||||
| label="label" | |||||
| :isLoading="brandSelectorLoader" | |||||
| :reduce="(option) => option.value" | |||||
| :options="formattedBrands" | |||||
| @change="clearError(`selectedBrandPage`)" | |||||
| placeholder="برندی را انتخاب کنید" | |||||
| @search="handleBrandSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedBrandPage" class="text-danger"> | |||||
| {{ errors.selectedBrandPage }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pannel == 'web' && pannel" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب صفحه فرود</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="landingType" | |||||
| @change="clearError('landingType')" | |||||
| :class="{ 'is-invalid': errors.landingType }" | |||||
| placeholder="انتخاب صفحه فرود" | |||||
| > | |||||
| <option value="product">صفحه محصولات</option> | |||||
| <option value="cat">صفحه دسته ها</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.landingType" class="text-danger"> | |||||
| {{ errors.landingType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="landingType === 'product'" | |||||
| sm="6" | |||||
| class="mt-3" | |||||
| style="margin-top: 30px" | |||||
| > | > | ||||
| <option value="product">صفحه محصولات</option> | |||||
| <option value="cat">صفحه دسته ها</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.landingType" class="text-danger"> | |||||
| {{ errors.landingType }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="landingType === 'product'" | |||||
| sm="6" | |||||
| class="mt-3" | |||||
| style="margin-top: 30px" | |||||
| > | |||||
| <label for="token">صفحه کدام محصول</label> | |||||
| <VueSelect | |||||
| style=" | |||||
| <label for="token">صفحه کدام محصول</label> | |||||
| <VueSelect | |||||
| style=" | |||||
| --vs-min-height: 48px; | --vs-min-height: 48px; | ||||
| --vs-border-radius: 8px; | --vs-border-radius: 8px; | ||||
| margin-top: 7px; | margin-top: 7px; | ||||
| " | " | ||||
| v-model="selectedLandingProduct" | |||||
| :reduce="(option) => option.value" | |||||
| label="label" | |||||
| :isLoading="productSelectorLoader" | |||||
| :options="formattedProducts" | |||||
| @search="handleProductSearch" | |||||
| placeholder="محصولی را انتخاب کنید" | |||||
| /> | |||||
| <small v-if="errors.selectedLandingProduct" class="text-danger"> | |||||
| {{ errors.selectedLandingProduct }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="landingType === 'cat'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه کدام دسته </label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| :isLoading="categorySelectorLoader" | |||||
| @change="clearError('selectedLandingCat')" | |||||
| label="label" | |||||
| v-model="selectedLandingCat" | |||||
| :reduce="(option) => option.value" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedLandingCat" class="text-danger"> | |||||
| {{ errors.selectedLandingCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'main_page' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه اصلی</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه دسته ها</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'special_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه فروش ویژه</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Slideshow</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه برند</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| v-model="selectedLandingProduct" | |||||
| :reduce="(option) => option.value" | |||||
| label="label" | |||||
| :isLoading="productSelectorLoader" | |||||
| :options="formattedProducts" | |||||
| @search="handleProductSearch" | |||||
| placeholder="محصولی را انتخاب کنید" | |||||
| /> | |||||
| <small v-if="errors.selectedLandingProduct" class="text-danger"> | |||||
| {{ errors.selectedLandingProduct }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="landingType === 'cat'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">صفحه کدام دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| v-model="selectedLandingCat" | |||||
| @change="clearError('selectedLandingCat')" | |||||
| :isLoading="categorySelectorLoader" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.selectedLandingCat" class="text-danger"> | |||||
| {{ errors.selectedLandingCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'main_page' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه اصلی</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'category' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه دسته ها</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'special_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | > | ||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'blog_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه بلاگ</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه فروش ویژه</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Slideshow</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol v-if="pageType === 'brand' && pannel === 'web'" md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه برند</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| > | |||||
| <option value="A">َA-Slideshow</option> | |||||
| <option value="B">B-Banner</option> | |||||
| <option value="C">C-Banner</option> | |||||
| <option value="D">D-Banner</option> | |||||
| <option value="E">E-Banner</option> | |||||
| <option value="F">F-Banner</option> | |||||
| <option value="G">G-Banner</option> | |||||
| <option value="H">H-Banner</option> | |||||
| <option value="I">I-Banner</option> | |||||
| <option value="J">J-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol | |||||
| v-if="pageType === 'blog_page' && pannel === 'web'" | |||||
| md="6" | |||||
| > | > | ||||
| <option value="A">A-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بنر</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageUpload" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.imagePreview }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.imagePreview" class="text-danger"> | |||||
| {{ errors.imagePreview }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </BCardBody> | |||||
| <BCardFooter> | |||||
| <div class="d-flex justify-content-center"> | |||||
| <div class="text-center"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">موقعیت در صفحه بلاگ</label> | |||||
| <select | |||||
| class="form-select" | |||||
| aria-label="Default select example" | |||||
| v-model="selectedLoc" | |||||
| @change="clearError('selectedLoc')" | |||||
| :class="{ 'is-invalid': errors.selectedLoc }" | |||||
| placeholder="موقعیت بنر" | |||||
| :value="(selectedLoc = 'A')" | |||||
| > | |||||
| <option value="A">A-Banner</option> | |||||
| </select> | |||||
| </div> | |||||
| <small v-if="errors.selectedLoc" class="text-danger"> | |||||
| {{ errors.selectedLoc }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <button | <button | ||||
| type="submit" | |||||
| class="btn btn-primary" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| type="submit" | |||||
| class="btn btn-primary mt-4" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| > | > | ||||
| <span v-if="loading"> | <span v-if="loading"> | ||||
| <i class="fa fa-spinner fa-spin"></i> ویرایش... | <i class="fa fa-spinner fa-spin"></i> ویرایش... | ||||
| </span> | </span> | ||||
| <span v-else>ویرایش</span> | <span v-else>ویرایش</span> | ||||
| </button> | </button> | ||||
| </div> | |||||
| </div> | |||||
| </BCardFooter> | |||||
| </BTab> | |||||
| <BTab title="ترجمه ها" class="mt-4"> | |||||
| <BRow> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بنر</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageUpload" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.imagePreview }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.imagePreview" class="text-danger"> | |||||
| {{ errors.imagePreview }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <button | |||||
| @click.prevent="editTranslationBanner" | |||||
| :disabled="loading" | |||||
| class="btn btn-primary mt-5" | |||||
| > | |||||
| <span v-if="loading"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | |||||
| </span> | |||||
| <span v-else>ایجاد</span> | |||||
| </button> | |||||
| </BTab> | |||||
| </BTabs> | |||||
| </BCardBody> | |||||
| </BCard> | </BCard> | ||||
| </BCol> | </BCol> | ||||
| </BRow> | </BRow> | ||||
| @@ -425,10 +451,12 @@ import "vue3-toastify/dist/index.css"; | |||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import { ref, onMounted, computed } from "vue"; | import { ref, onMounted, computed } from "vue"; | ||||
| import Layout from "@/layout/custom.vue"; | import Layout from "@/layout/custom.vue"; | ||||
| import {BTabs} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| name: "SAMPLE-PAGE", | name: "SAMPLE-PAGE", | ||||
| components: { | components: { | ||||
| BTabs, | |||||
| Layout, | Layout, | ||||
| mainPageBanner, | mainPageBanner, | ||||
| catBanner, | catBanner, | ||||
| @@ -449,26 +477,18 @@ export default { | |||||
| const selectedLandingCat = ref(); | const selectedLandingCat = ref(); | ||||
| const selectedLandingProduct = ref(); | const selectedLandingProduct = ref(); | ||||
| const selectedLoc = ref(); | const selectedLoc = ref(); | ||||
| const pannel = ref(); | |||||
| const pannel = ref('web'); | |||||
| const image = ref(); | const image = ref(); | ||||
| const imagePreview = ref(); | const imagePreview = ref(); | ||||
| const selectedBrandPage = ref(); | const selectedBrandPage = ref(); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const categorySelectorLoader = ref(false); | const categorySelectorLoader = ref(false); | ||||
| const productSelectorLoader = ref(false); | const productSelectorLoader = ref(false); | ||||
| const categoryPageSelectorLoader = ref(false); | const categoryPageSelectorLoader = ref(false); | ||||
| const brandSelectorLoader = ref(false); | const brandSelectorLoader = ref(false); | ||||
| const formattedCategories = computed(() => | |||||
| Array.isArray(cats.value) | |||||
| ? cats.value.map((cat) => ({ | |||||
| value: cat.id, | |||||
| label: cat.title, | |||||
| })) | |||||
| : [] | |||||
| ); | |||||
| const banner = ref(null) | |||||
| const locale = ref('fa') | |||||
| const handleSearch = async (searchTerm) => { | const handleSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | if (searchTerm.length < 3) return; | ||||
| @@ -485,6 +505,15 @@ export default { | |||||
| } | } | ||||
| }; | }; | ||||
| const formattedCategories = computed(() => | |||||
| Array.isArray(cats.value) | |||||
| ? cats.value?.map((cat) => ({ | |||||
| value: cat?.id, | |||||
| label: cat?.translation?.title || 'بدون عنوان', | |||||
| })) | |||||
| : [] | |||||
| ); | |||||
| const formattedProducts = computed(() => | const formattedProducts = computed(() => | ||||
| Array.isArray(products.value) | Array.isArray(products.value) | ||||
| ? products.value.map((product) => ({ | ? products.value.map((product) => ({ | ||||
| @@ -512,8 +541,8 @@ export default { | |||||
| const formattedCatPages = computed(() => | const formattedCatPages = computed(() => | ||||
| Array.isArray(catPages.value) | Array.isArray(catPages.value) | ||||
| ? catPages.value.map((catPage) => ({ | ? catPages.value.map((catPage) => ({ | ||||
| value: catPage.id, | |||||
| label: catPage.title, | |||||
| value: catPage?.id, | |||||
| label: catPage?.translation?.title ?? '' | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -537,7 +566,7 @@ export default { | |||||
| Array.isArray(brands.value) | Array.isArray(brands.value) | ||||
| ? brands.value.map((brand) => ({ | ? brands.value.map((brand) => ({ | ||||
| value: brand.id, | value: brand.id, | ||||
| label: brand.title, | |||||
| label: brand?.translation?.title ?? 'بدون عنوان', | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -592,11 +621,6 @@ export default { | |||||
| if (!selectedLoc.value) | if (!selectedLoc.value) | ||||
| errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید"; | errors.value.selectedLoc = "موقعیت بنر را انتخاب کنید"; | ||||
| if (!pannel.value) errors.value.pannel = "پنل نمایش بنر را انتخاب کنید"; | |||||
| if (!imagePreview.value) | |||||
| errors.value.imagePreview = "عکس بنر را وارد نمایید"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -629,12 +653,12 @@ export default { | |||||
| } | } | ||||
| title.value = data?.title; | title.value = data?.title; | ||||
| pannel.value = data?.panel; | |||||
| imagePreview.value = data?.image; | |||||
| imagePreview.value = data?.translation?.image; | |||||
| selectedLoc.value = data?.location; | selectedLoc.value = data?.location; | ||||
| selectedLandingCat.value = data?.category_id; | selectedLandingCat.value = data?.category_id; | ||||
| selectedLandingProduct.value = data?.product_id; | selectedLandingProduct.value = data?.product_id; | ||||
| pageType.value = data.page_type; | pageType.value = data.page_type; | ||||
| banner.value = data; | |||||
| if (data.page_id && pageType.value === "category") { | if (data.page_id && pageType.value === "category") { | ||||
| selectedCatPage.value = data?.page_id; | selectedCatPage.value = data?.page_id; | ||||
| } | } | ||||
| @@ -669,55 +693,58 @@ export default { | |||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| const params = {} | |||||
| params.title = title.value | |||||
| if (pageType.value === "category") { | if (pageType.value === "category") { | ||||
| formData.append("page_id", selectedCatPage.value); | |||||
| params.page_id = selectedCatPage.value | |||||
| } | } | ||||
| if (pageType.value === "brand") { | if (pageType.value === "brand") { | ||||
| formData.append("page_id", selectedBrandPage.value); | |||||
| params.page_id = selectedBrandPage.value | |||||
| } | } | ||||
| if (landingType.value === "product") { | if (landingType.value === "product") { | ||||
| formData.append("product_id", selectedLandingProduct.value); | |||||
| params.product_id = selectedLandingProduct.value | |||||
| } | } | ||||
| if (landingType.value === "cat") { | if (landingType.value === "cat") { | ||||
| formData.append("category_id", selectedLandingCat.value); | |||||
| params.category_id = selectedLandingCat.value | |||||
| } | } | ||||
| if (selectedLoc.value === "A") { | if (selectedLoc.value === "A") { | ||||
| formData.append("type", pageType.value === 'blog_page' ? "banner" : "slider"); | |||||
| params.type = pageType.value === 'blog_page' ? "banner" : "slider" | |||||
| } | } | ||||
| if (selectedLoc.value !== "A") { | if (selectedLoc.value !== "A") { | ||||
| formData.append("type", "banner"); | |||||
| params.type = "banner" | |||||
| } | } | ||||
| if (pannel.value === "wholesale") { | if (pannel.value === "wholesale") { | ||||
| formData.append("type", "slider"); | |||||
| formData.append("location", "A"); | |||||
| params.type = "slider" | |||||
| params.location = "A" | |||||
| } | } | ||||
| formData.append("location", selectedLoc.value); | |||||
| params.location = selectedLoc.value | |||||
| formData.append("panel", pannel.value); | |||||
| params.panel = pannel.value | |||||
| if(pannel.value === 'web') | if(pannel.value === 'web') | ||||
| formData.append("page_type", pageType.value); | |||||
| params.page_type = pageType.value | |||||
| if (image.value) { | |||||
| formData.append("image", image.value); | |||||
| } | |||||
| // if (image.value) { | |||||
| // formData.append("image", image.value); | |||||
| // } | |||||
| formData.append("sort", 1); | |||||
| formData.append("_method", "put"); | |||||
| params.sort = 1 | |||||
| ApiServiece.post(`admin/banners/${route.params.id}`, formData, { | |||||
| params._method = 'put' | |||||
| ApiServiece.post(`admin/banners/${route.params.id}`, params, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| @@ -739,6 +766,103 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = banner.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| image.value = findLocale?.image | |||||
| imagePreview.value = findLocale?.image | |||||
| } else { | |||||
| image.value = undefined | |||||
| imagePreview.value = undefined | |||||
| } | |||||
| } | |||||
| const editTranslationBanner = async () => { | |||||
| if (!validateForm()) { | |||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| return; | |||||
| } | |||||
| try { | |||||
| loading.value = true; | |||||
| const formData = new FormData(); | |||||
| if (image.value instanceof File) { | |||||
| formData.append('image', image.value); | |||||
| } else { | |||||
| toast.error('لطفا تصویر جایگزین انتخاب کنید') | |||||
| return; | |||||
| } | |||||
| formData.append('locale', locale.value); | |||||
| const existingTranslation = banner.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| if (existingTranslation) | |||||
| formData.append('_method', 'put') | |||||
| const { data } = existingTranslation | |||||
| ? await ApiServiece.post( | |||||
| `admin/banners/${banner.value?.id}/translations/${existingTranslation?.id}`, | |||||
| formData, | |||||
| { | |||||
| headers: { | |||||
| 'content-type': 'multipart/form-data', | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| : await ApiServiece.post( | |||||
| `admin/banners/${banner.value?.id}/translations`, | |||||
| formData, | |||||
| { | |||||
| headers: { | |||||
| 'content-type': 'multipart/form-data', | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ); | |||||
| if (data?.success) { | |||||
| // const updatedCategory = banner.value; | |||||
| // | |||||
| // if (existingTranslation) { | |||||
| // updatedCategory.translations = data?.data?.translations || []; | |||||
| // } else { | |||||
| // updatedCategory.translations = [ | |||||
| // ...(updatedCategory.translations || []), | |||||
| // ...(data?.data?.translations || []) | |||||
| // ]; | |||||
| // } | |||||
| banner.value = data?.data; | |||||
| toast.success(data?.message) | |||||
| //emit("cat-updated"); | |||||
| } | |||||
| } catch (error) { | |||||
| console.log(error,'error error'); | |||||
| toast.error(`${error?.response?.data?.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| } finally { | |||||
| loading.value = false; | |||||
| } | |||||
| }; | |||||
| return { | return { | ||||
| cats, | cats, | ||||
| errors, | errors, | ||||
| @@ -771,6 +895,9 @@ export default { | |||||
| brandSelectorLoader, | brandSelectorLoader, | ||||
| handleBrandSearch, | handleBrandSearch, | ||||
| selectedBrandPage, | selectedBrandPage, | ||||
| handlerChangeLocale, | |||||
| locale, | |||||
| editTranslationBanner | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -29,6 +29,7 @@ export default { | |||||
| const cats = ref(); | const cats = ref(); | ||||
| const catTitle = ref(); | const catTitle = ref(); | ||||
| const catId = ref(); | const catId = ref(); | ||||
| const catRow = ref(); | |||||
| const convertToJalali = (date) => { | const convertToJalali = (date) => { | ||||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | return moment(date, "YYYY-MM-DD HH:mm:ss") | ||||
| @@ -151,11 +152,12 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const editModalData = (id, title, icon) => { | |||||
| catId.value = id; | |||||
| catTitle.value = title; | |||||
| catIcon.value = icon; | |||||
| }; | |||||
| const editModalData = (cat) => { | |||||
| // catId.value = id; | |||||
| // catTitle.value = title; | |||||
| // catIcon.value = icon; | |||||
| catRow.value = cat | |||||
| } | |||||
| watch(page, () => { | watch(page, () => { | ||||
| getCats(); | getCats(); | ||||
| @@ -183,6 +185,7 @@ export default { | |||||
| handlePageInput, | handlePageInput, | ||||
| searchPage, | searchPage, | ||||
| visiblePages, | visiblePages, | ||||
| catRow, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -231,10 +234,10 @@ export default { | |||||
| <td> | <td> | ||||
| <button | <button | ||||
| @click="editModalData(cat?.id, cat?.translation?.title)" | |||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| data-bs-target="#editBlogCat" | data-bs-target="#editBlogCat" | ||||
| class="btn btn-sm btn-outline-warning me-1" | class="btn btn-sm btn-outline-warning me-1" | ||||
| @click="editModalData(cat)" | |||||
| > | > | ||||
| ویرایش | ویرایش | ||||
| </button> | </button> | ||||
| @@ -261,6 +264,7 @@ export default { | |||||
| :id="catId" | :id="catId" | ||||
| :title="catTitle" | :title="catTitle" | ||||
| :icon="catIcon" | :icon="catIcon" | ||||
| :catRow="catRow" | |||||
| @cat-updated="handleCatUpdated()" | @cat-updated="handleCatUpdated()" | ||||
| /> | /> | ||||
| <showDescription :desc="catDescription" /> | <showDescription :desc="catDescription" /> | ||||
| @@ -30,7 +30,9 @@ export default { | |||||
| const brandTitle = ref(); | const brandTitle = ref(); | ||||
| const brandId = ref(); | const brandId = ref(); | ||||
| const brandImage = ref(); | const brandImage = ref(); | ||||
| const brandRow = ref() | |||||
| let searchTimeout = null; | let searchTimeout = null; | ||||
| const convertToJalali = (date) => { | const convertToJalali = (date) => { | ||||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | return moment(date, "YYYY-MM-DD HH:mm:ss") | ||||
| .locale("fa") | .locale("fa") | ||||
| @@ -103,7 +105,7 @@ export default { | |||||
| }; | }; | ||||
| const deleteBrand = (id, title) => { | const deleteBrand = (id, title) => { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید برند ${title} را حذف کنید؟`, | |||||
| text: `می خواهید برند ${title ?? ''} را حذف کنید؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -131,11 +133,8 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const editModalData = (id, title, desc, img) => { | |||||
| brandId.value = id; | |||||
| brandTitle.value = title; | |||||
| brandDescription.value = desc; | |||||
| brandImage.value = img; | |||||
| const editModalData = (brand) => { | |||||
| brandRow.value = brand; | |||||
| }; | }; | ||||
| const descriptionModal = (desc) => { | const descriptionModal = (desc) => { | ||||
| @@ -194,6 +193,7 @@ export default { | |||||
| page, | page, | ||||
| paginate, | paginate, | ||||
| handlePageInput, | handlePageInput, | ||||
| brandRow, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -266,12 +266,7 @@ export default { | |||||
| <td> | <td> | ||||
| <button | <button | ||||
| @click=" | @click=" | ||||
| editModalData( | |||||
| brand?.id, | |||||
| brand?.title, | |||||
| brand.description, | |||||
| brand.image | |||||
| ) | |||||
| editModalData(brand) | |||||
| " | " | ||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| data-bs-target="#editBrand" | data-bs-target="#editBrand" | ||||
| @@ -280,7 +275,7 @@ export default { | |||||
| ویرایش | ویرایش | ||||
| </button> | </button> | ||||
| <button | <button | ||||
| @click="deleteBrand(brand.id, brand.title)" | |||||
| @click="deleteBrand(brand.id, brand?.translation?.title)" | |||||
| class="btn btn-sm btn-outline-danger" | class="btn btn-sm btn-outline-danger" | ||||
| > | > | ||||
| حذف | حذف | ||||
| @@ -303,6 +298,7 @@ export default { | |||||
| :title="brandTitle" | :title="brandTitle" | ||||
| :description="brandDescription" | :description="brandDescription" | ||||
| :image="brandImage" | :image="brandImage" | ||||
| :brandRow="brandRow" | |||||
| @brand-updated="handleBrandUpdated()" | @brand-updated="handleBrandUpdated()" | ||||
| /> | /> | ||||
| <showDescription :desc="brandDescription" /> | <showDescription :desc="brandDescription" /> | ||||
| @@ -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(() => | const formattedCategories = computed(() => | ||||
| Array.isArray(categories.value) | Array.isArray(categories.value) | ||||
| ? categories.value.map((category) => ({ | ? categories.value.map((category) => ({ | ||||
| value: category.id, | |||||
| label: category.title, | |||||
| value: category?.id, | |||||
| label: category?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -270,7 +270,7 @@ export default { | |||||
| Array.isArray(products.value) | Array.isArray(products.value) | ||||
| ? products.value.map((product) => ({ | ? products.value.map((product) => ({ | ||||
| value: product.id, | value: product.id, | ||||
| label: product.title, | |||||
| label: product?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -222,28 +222,28 @@ export default { | |||||
| const formattedCategories = computed(() => | const formattedCategories = computed(() => | ||||
| Array.isArray(categories.value) | Array.isArray(categories.value) | ||||
| ? categories.value.map((category) => ({ | |||||
| value: category.id, | |||||
| label: category.title, | |||||
| ? categories.value.filter(item => item?.translation).map((category) => ({ | |||||
| value: category?.id, | |||||
| label: category?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| const formattedProducts = computed(() => | const formattedProducts = computed(() => | ||||
| Array.isArray(products.value) | Array.isArray(products.value) | ||||
| ? products.value.map((product) => ({ | |||||
| value: product.id, | |||||
| label: product.title, | |||||
| ? products.value.filter(item => item?.translation).map((product) => ({ | |||||
| value: product?.id, | |||||
| label: product?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| const handleSearch = async (searchTerm) => { | const handleSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | |||||
| if (searchTerm?.length < 3) return; | |||||
| categorySelectorLoader.value = true; | categorySelectorLoader.value = true; | ||||
| try { | try { | ||||
| const response = await ApiServiece.get( | const response = await ApiServiece.get( | ||||
| `admin/categories?title=${searchTerm}` | |||||
| `admin/categories?title=${searchTerm ?? ''}` | |||||
| ); | ); | ||||
| categories.value = response.data.data; | categories.value = response.data.data; | ||||
| categorySelectorLoader.value = false; | categorySelectorLoader.value = false; | ||||
| @@ -254,11 +254,11 @@ export default { | |||||
| }; | }; | ||||
| const handleProductSearch = async (searchTerm) => { | const handleProductSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | |||||
| if (searchTerm?.length < 3) return; | |||||
| productSelectorLoader.value = true; | productSelectorLoader.value = true; | ||||
| try { | try { | ||||
| const response = await ApiServiece.get( | const response = await ApiServiece.get( | ||||
| `admin/products?title=${searchTerm}` | |||||
| `admin/products?title=${searchTerm ?? ''}` | |||||
| ); | ); | ||||
| products.value = response.data.data; | products.value = response.data.data; | ||||
| productSelectorLoader.value = false; | productSelectorLoader.value = false; | ||||
| @@ -364,6 +364,9 @@ export default { | |||||
| onMounted(() => { | onMounted(() => { | ||||
| getDiscount(); | getDiscount(); | ||||
| handleProductSearch() | |||||
| handleSearch() | |||||
| }); | }); | ||||
| const submitForm = () => { | const submitForm = () => { | ||||
| @@ -409,7 +412,7 @@ export default { | |||||
| if (maxUsage.value) | if (maxUsage.value) | ||||
| formData.append("max_usage", maxUsage.value); | formData.append("max_usage", maxUsage.value); | ||||
| ApiServiece.post(`/admin/discounts`, formData) | |||||
| ApiServiece.put(`/admin/discounts/${route.params.id || discount.value?.id}`, formData) | |||||
| .then((resp) => { | .then((resp) => { | ||||
| loading.value = false; | loading.value = false; | ||||
| toast.success("!تخفیف با موفقیت ویرایش شد", { | toast.success("!تخفیف با موفقیت ویرایش شد", { | ||||
| @@ -114,7 +114,7 @@ export default { | |||||
| }); | }); | ||||
| const deleteProduct = (id, title) => { | const deleteProduct = (id, title) => { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید محصول ${title} را حذف کنید؟`, | |||||
| text: `می خواهید محصول ${title ?? ''} را حذف کنید؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -146,7 +146,7 @@ export default { | |||||
| const restoreProduct = (id, title) => { | const restoreProduct = (id, title) => { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید محصول ${title} را بازیابی کنید؟`, | |||||
| text: `می خواهید محصول ${title ?? ''} را بازیابی کنید؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -294,6 +294,7 @@ export default { | |||||
| }, | }, | ||||
| }; | }; | ||||
| </script> | </script> | ||||
| <template> | <template> | ||||
| <Layout> | <Layout> | ||||
| <BRow> | <BRow> | ||||
| @@ -362,15 +363,11 @@ export default { | |||||
| <tr> | <tr> | ||||
| <th>عکس</th> | <th>عکس</th> | ||||
| <th>عنوان</th> | <th>عنوان</th> | ||||
| <th>مدل</th> | |||||
| <th>دسته بندی</th> | <th>دسته بندی</th> | ||||
| <th>برند</th> | <th>برند</th> | ||||
| <th>تعداد فروش</th> | <th>تعداد فروش</th> | ||||
| <th>برگزیده</th> | |||||
| <th>تخفیف برگزیده</th> | |||||
| <th>ویژه</th> | |||||
| <th>قیمت عمده</th> | |||||
| <th>قیمت تک</th> | |||||
| <th>قیمت</th> | |||||
| <th>وزن</th> | |||||
| <th>وضعیت</th> | <th>وضعیت</th> | ||||
| <th>عملیات</th> | <th>عملیات</th> | ||||
| </tr> | </tr> | ||||
| @@ -386,58 +383,22 @@ export default { | |||||
| </td> | </td> | ||||
| <td v-if="!product.image">ندارد</td> | <td v-if="!product.image">ندارد</td> | ||||
| <td>{{ product.title }}</td> | |||||
| <td>{{ product?.translation?.title }}</td> | |||||
| <td v-if="product.type == 1">تکی</td> | |||||
| <td v-if="product.type == 2">عمده</td> | |||||
| <td v-if="product.type == 3">تکی و عمده</td> | |||||
| <td>{{ product?.category?.title }}</td> | |||||
| <td>{{ product?.category?.translation?.title }}</td> | |||||
| <td>{{ product?.brand?.title }}</td> | |||||
| <td>{{ product?.brand?.translation?.title }}</td> | |||||
| <td>{{ product?.sold_count }}</td> | |||||
| <td>{{ product.sold_count }}</td> | |||||
| <td> | |||||
| <span v-if="product.is_chosen == 0"> | |||||
| <i | |||||
| class="fas fa-times-circle status-icon unavailable" | |||||
| ></i> | |||||
| </span> | |||||
| <span v-if="product.is_chosen == 1"> | |||||
| <i | |||||
| class="fas fa-check-circle status-icon available" | |||||
| ></i> | |||||
| </span> | |||||
| </td> | |||||
| <td v-if="product?.chosen_price"> | |||||
| %{{ formatWithCommas(product?.chosen_price) }} | |||||
| </td> | |||||
| <td v-if="!product.chosen_price"> | |||||
| <i | |||||
| class="fas fa-times-circle status-icon unavailable" | |||||
| ></i> | |||||
| </td> | |||||
| <td> | <td> | ||||
| <span v-if="product.is_special == 0"> | |||||
| <i | |||||
| class="fas fa-times-circle status-icon unavailable" | |||||
| ></i> | |||||
| </span> | |||||
| <span v-if="product.is_special == 1"> | |||||
| <i | |||||
| class="fas fa-check-circle status-icon available" | |||||
| ></i> | |||||
| </span> | |||||
| {{ formatWithCommas(product.price) }} | |||||
| </td> | </td> | ||||
| <td v-if="product?.wholesale_price"> | |||||
| {{ formatWithCommas(product.wholesale_price) }} | |||||
| </td> | |||||
| <td v-if="!product?.wholesale_price">ندارد</td> | |||||
| <td v-if="product?.retail_price"> | |||||
| {{ formatWithCommas(product?.retail_price) }} | |||||
| <td> | |||||
| {{ product?.weight }} | |||||
| </td> | </td> | ||||
| <td v-if="!product?.retail_price">ندارد</td> | |||||
| <td v-if="!product.deleted_at"> | <td v-if="!product.deleted_at"> | ||||
| <span class="badge bg-success text-white">فعال</span> | <span class="badge bg-success text-white">فعال</span> | ||||
| @@ -455,7 +416,7 @@ export default { | |||||
| </router-link> | </router-link> | ||||
| <button | <button | ||||
| v-if="!product.deleted_at" | v-if="!product.deleted_at" | ||||
| @click="deleteProduct(product?.id, product?.title)" | |||||
| @click="deleteProduct(product?.id, product?.translation?.title)" | |||||
| class="btn btn-sm btn-outline-danger me-1" | class="btn btn-sm btn-outline-danger me-1" | ||||
| > | > | ||||
| حذف | حذف | ||||
| @@ -463,7 +424,7 @@ export default { | |||||
| <button | <button | ||||
| v-else | v-else | ||||
| @click="restoreProduct(product?.id, product?.title)" | |||||
| @click="restoreProduct(product?.id, product?.translation?.title)" | |||||
| class="btn btn-sm btn-outline-success" | class="btn btn-sm btn-outline-success" | ||||
| > | > | ||||
| بازیابی | بازیابی | ||||
| @@ -30,6 +30,7 @@ export default { | |||||
| const userMobile = ref(); | const userMobile = ref(); | ||||
| const userId = ref(); | const userId = ref(); | ||||
| const userRole = ref(); | const userRole = ref(); | ||||
| const userCountryCode = ref(); | |||||
| const selectedRole = ref(""); | const selectedRole = ref(""); | ||||
| const userProfile = JSON.parse(localStorage.getItem('user_profile')); | const userProfile = JSON.parse(localStorage.getItem('user_profile')); | ||||
| @@ -49,7 +50,6 @@ export default { | |||||
| }&trashed=${selectedStatus.value || ""}` | }&trashed=${selectedStatus.value || ""}` | ||||
| ) | ) | ||||
| .then((resp) => { | .then((resp) => { | ||||
| console.log(resp.data.data); | |||||
| users.value = resp.data.data.data; | users.value = resp.data.data.data; | ||||
| currentPage.value = resp.data.data.current_page; | currentPage.value = resp.data.data.current_page; | ||||
| totalPages.value = resp.data.data.last_page; | totalPages.value = resp.data.data.last_page; | ||||
| @@ -113,8 +113,7 @@ export default { | |||||
| debouncedSearch(newQuery); | debouncedSearch(newQuery); | ||||
| }); | }); | ||||
| const debouncedSearch = debounce((query) => { | |||||
| console.log("Searching for:", query); | |||||
| const debouncedSearch = debounce(() => { | |||||
| getUsers(); | getUsers(); | ||||
| }, 1000); | }, 1000); | ||||
| @@ -134,12 +133,14 @@ export default { | |||||
| } | } | ||||
| } | } | ||||
| const modalData = (id, name, mobile, role) => { | |||||
| const modalData = (id, name, mobile, role, config_id) => { | |||||
| userName.value = name; | userName.value = name; | ||||
| userMobile.value = mobile; | userMobile.value = mobile; | ||||
| userId.value = id; | userId.value = id; | ||||
| userRole.value = role; | userRole.value = role; | ||||
| userCountryCode.value = config_id; | |||||
| }; | }; | ||||
| const handleUserUpdated = () => { | const handleUserUpdated = () => { | ||||
| getUsers(); | getUsers(); | ||||
| }; | }; | ||||
| @@ -237,6 +238,7 @@ export default { | |||||
| selectedStatus, | selectedStatus, | ||||
| filterLoading, | filterLoading, | ||||
| userProfile, | userProfile, | ||||
| userCountryCode, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -356,7 +358,8 @@ export default { | |||||
| user.id, | user.id, | ||||
| user.name, | user.name, | ||||
| user.mobile, | user.mobile, | ||||
| user.role | |||||
| user.role, | |||||
| user?.country_config_id | |||||
| ) | ) | ||||
| " | " | ||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| @@ -405,6 +408,7 @@ export default { | |||||
| :mobile="userMobile" | :mobile="userMobile" | ||||
| :id="userId" | :id="userId" | ||||
| :role="userRole" | :role="userRole" | ||||
| :countryCode="userCountryCode" | |||||
| /> | /> | ||||
| <addUser @user-updated="handleUserUpdated" /> | <addUser @user-updated="handleUserUpdated" /> | ||||
| </BRow> | </BRow> | ||||