const { createApp } = Vue createApp({ data() { return { brand: "san", acc: "9", country: null, complaintEndpoint: "submit-complaint.php", contactEndpoint: "contact.php", version: "2.2", errorIcon: "", englishDigits: { '٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4', '٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9' }, forms: { personal: [ { id: 0, name: "fullName", val: "", errorMsg: 'يرجى إدخال الاسم الكامل (كلمتان على الأقل، كل كلمة لا تقل عن 3 أحرف).', error: null, }, { id: 1, name: "age", val: "", errorMsg: 'يرجى إدخال عمر صحيح (رقمان بين 19 و 98).', error: null, }, { id: 2, name: "phone", val: "", draft: "", cleaned: "", errorMsg: "رجاءً تأكد من ادخال رقم الهاتف صحيح", error: null, }, ], complaint: [ { id: 0, name: "description", val: "", errorMsg: "الرجاء كتابة وصف مختصر للواقعة", error: null, }, { id: 1, name: "company", val: "", errorMsg: null, error: false, }, { id: 2, name: "total", val: "", errorMsg: null, error: false, } ], contact: [ { id: 0, name: "fullName", val: "", }, { id: 1, name: "email", val: "", }, { id: 2, name: "desc", val: "", }, ] }, contactSubmitted: false, active: { form: null, index: null }, userCountry: "SA", correctPhoneLength: 9, countryDialCode: "966", allowNext: false, steps: [ { id: 1, label: "نوع الشكوى" }, { id: 2, label: "تفاصيل الشكوى" }, { id: 3, label: "البيانات الشخصية" }, { id: 4, label: "استلام الشكوى" } ], currentStep: 1, totalSteps: 4, categories: [ { id: "ecommerce", title: "شكوى على متجر إلكتروني", examples: "بلاغ تجاري على متجر إلكتروني، بلاغات حماية المستهلك المتعلقة بالشراء عبر الإنترنت.", icon: `` }, { id: "shipping", title: "شكاوى شركات الشحن والتوصيل", examples: "بلاغات تخص شركات التوصيل.", icon: `` }, { id: "individuals", title: "شكاوى أفراد أو جهات خاصة", examples: "شكاوى تخص تعاملات خاصة أو مهنية.", icon: `` }, { id: "telecom", title: "شكاوى شركات الاتصالات", examples: "مشاكل الاتصالات، الفواتير، إيقاف الخدمة بدون سبب، سوء معاملة الموظفين.", icon: `` }, { id: "restaurants", title: "شكاوى مطاعم ومتاجر", examples: "شكاوى متعلقة بالمطاعم والمتاجر مثل الغش التجاري، الأسعار غير الواضحة، النظافة، أو سوء الخدمة.", icon: `` }, { id: "healthcare", title: "شكاوى خدمات صحية", examples: "مواعيد، سوء معاملة، أخطاء إدارية.", icon: `` }, { id: "education", title: "شكاوى تعليمية", examples: "شكاوى تعليمية تتعلق بالخدمة أو المحتوى.", icon: `` }, { id: "internet", title: "شكاوى الإنترنت", examples: "بلاغات تخص مزودي خدمة الإنترنت.", icon: `` }, { id: "insurance", title: "شكاوى شركات التأمين", examples: "مشاكل في المطالبات التأمينية أو تأخير الإجراءات.", icon: `` }, { id: "travel", title: "شكاوى الطيران والسفر", examples: "شكاوى تخص الرحلات الجوية وخدمات المسافرين.", icon: `` }, { id: "public", title: "شكاوى خدمات عامة", examples: "شكاوى تتعلق بالخدمات العامة مثل الكهرباء والمياه والصرف الصحي والنظافة.", icon: `` }, { id: "finance", title: "شكاوى الخدمات المالية", examples: "سحب مبالغ بدون إذن، رفض معاملات، رسوم غير مبررة.", icon: `` }, { id: "other", title: "شكاوى أخرى", examples: "في حال لم تجد القسم المناسب.", icon: `` }, ], startY: 0, scrollup: null, scrolldown: null, lastScrollPosition: 0, showNavbar: null, bodyScroll: true, headerActive: null, showMenu: false, showSearch: false, selectedCategory: null, submitError: "", complaintNo: "", activeCatId: null, mainTitle: "", isLoading: false, } }, methods: { handleScroll() { var scrollY = window.scrollY this.startY = scrollY; if (this.showMenu) this.showMenu = false; if (scrollY < 0) return if (Math.abs(scrollY - this.lastScrollPosition) < 60) return this.showNavbar = scrollY < this.lastScrollPosition; this.lastScrollPosition = scrollY }, headerClasses() { return { scrollup: this.showNavbar && this.startY !== 0, scrolldown: this.showNavbar == false, preventScroll: this.bodyScroll == false, closed: this.headerActive == false, active: this.headerActive } }, setActiveField(formName, index) { this.active.form = formName this.active.index = index }, clearActiveField() { this.active.form = null this.active.index = null }, fieldClasses(formName, index) { const field = this.forms[formName][index] const hasVal = (field.val ?? "").toString().length > 1 const isActive = this.active.form === formName && this.active.index === index return { active: isActive, hasVal, done: field.error === false && hasVal, required: field.error === true } }, complaintInput() { const field = this.forms.complaint[0] const len = field.val.length if (len > 300) { field.error = true field.errorMsg = 'وصف الواقعة طويل جدًا، يجب ألا يتجاوز 300 حرف.' } else { field.error = len >= 50 ? false : null } }, complaintBlur() { const field = this.forms.complaint[0] const len = field.val.length if (len < 50) { field.error = true field.errorMsg = 'يرجى كتابة وصف لا يقل عن 50 حرفًا.' } this.clearActiveField(); }, fullNameInput() { const field = this.forms.personal[0] const isValid = this.validateFullName(field.val) field.error = isValid ? false : null }, fullNameBlur() { const field = this.forms.personal[0] const isValid = this.validateFullName(field.val) field.error = isValid ? false : true this.clearActiveField() }, validateFullName(value) { const words = value.trim().split(/\s+/).filter(Boolean) return words.length >= 2 && words.every(w => w.length >= 3) }, ageInput() { const field = this.forms.personal[1] const normalized = String(field.val).replace(/[٠-٩۰-۹]/g, (d) => this.englishDigits[d]) const cleaned = normalized.replace(/\D/g, '') field.val = cleaned const age = Number(cleaned) if (age > 98) { field.error = true return } const isValid = this.validateAge(cleaned) field.error = isValid ? false : null }, ageBlur() { const field = this.forms.personal[1] const isValid = this.validateAge(field.val) field.error = isValid ? false : true this.clearActiveField() }, validateAge(value) { const cleaned = value.replace(/\D/g, '') if (!/^\d{2}$/.test(cleaned)) return false const age = Number(cleaned) return age > 18 && age < 99 }, phoneInput() { const field = this.forms.personal[2] field.draft = field.draft.replace(/[٠١٢٣٤٥٦٧٨٩]/g, s => this.englishDigits[s]) let phone = field.draft .replace(/\D/g, '') .replace(/^0+/, ''); if (phone.startsWith(this.countryDialCode)) { phone = phone.slice(this.countryDialCode.length).replace(/^0+/, ''); } const phoneValid = phone.length === this.correctPhoneLength; field.error = phoneValid ? false : (phone.length < this.correctPhoneLength ? null : true); field.cleaned = phone; field.val = "0" + phone; }, phoneBlur() { const field = this.forms.personal[2] field.error = field.cleaned.length !== this.correctPhoneLength; this.clearActiveField() }, nextStep() { this.isLoading = true; setTimeout(() => { this.currentStep += 1 this.isLoading = false; this.$nextTick(() => { this.$refs.scrollElm?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }) }, 700) }, selectCategory(category) { this.selectedCategory = category.title; this.nextStep(); }, async contactUsSubmit() { this.isLoading = true; this.contactSubmitted = false; try { const fullName = this.forms.contact[0].val; const email = this.forms.contact[1].val; const description = this.forms.contact[2].val; const payload = { fullName, email, description }; const res = await fetch(this.contactEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const json = await res.json().catch(() => ({})); if (!res.ok || json.ok !== true) throw new Error(json.error || "submit_failed"); this.contactSubmitted = true; return json; } finally { this.isLoading = false; } }, buildPayload() { const width = window.innerWidth const device = width <= 768 ? "Mobile" : width <= 1024 ? "Tablet" : "Desktop" return { selectedCategory: this.selectedCategory, personal: { fullName: this.forms.personal[0].val, age: this.forms.personal[1].val, phone: this.forms.personal[2].val, }, complaint: { description: this.forms.complaint[0].val, entity: this.forms.complaint[1].val, amount: this.forms.complaint[2].val, complaintNo: this.complaintNo, }, meta: { device, deviceDimensions: `${window.innerWidth} x ${window.innerHeight}`, owner: "OM", } } }, async sendToComplaintEndpoint(payload) { const res = await fetch(this.complaintEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) const json = await res.json().catch(() => ({})) if (!res.ok || json.ok !== true) { const error = new Error(json.error || "submit_failed") error.payload = json throw error } return json }, getSubmitErrorMessage(err) { const key = err?.message || "submit_failed" const map = { empty_request_body: "الطلب فارغ.", invalid_json: "بيانات الطلب غير صحيحة.", missing_required_fields: "يرجى تعبئة جميع الحقول المطلوبة.", invalid_full_name: "الاسم الكامل غير صحيح.", invalid_age: "العمر غير صحيح.", invalid_phone: "رقم الجوال غير صحيح.", invalid_description: "وصف الشكوى غير صحيح.", invalid_email: "البريد الإلكتروني غير صحيح.", message_too_short: "نص الرسالة قصير جدًا.", missing_to_email_in_config: "إعدادات البريد غير مكتملة.", mail_send_failed: "فشل إرسال الرسالة. حاول مرة أخرى.", submit_failed: "فشل إرسال النموذج.", } const readable = map[key] || "حدث خطأ غير متوقع." return `${readable}` }, generateComplaintNo() { const now = Date.now().toString() const last7 = now.slice(-7) return `C-${last7}` }, getMainCat() { this.mainTitle = null this.activeCatId = null }, async formSubmit() { this.isLoading = true; this.submitError = ""; try { const payload = this.buildPayload() await this.sendToComplaintEndpoint(payload) this.currentStep += 1 this.$nextTick(() => { this.$refs.scrollElm?.scrollIntoView({ behavior: "smooth", block: "start" }) }) } catch (e) { this.submitError = this.getSubmitErrorMessage(e) } finally { this.isLoading = false; } }, }, computed: { canSubmitComplaint() { return this.forms.complaint.every(f => f.error === false) }, canSubmitPersonal() { return this.forms.personal.every(f => f.error === false) } }, mounted() { this.getMainCat(); this.complaintNo = this.generateComplaintNo(); window.addEventListener('scroll', this.handleScroll); this.lastScrollPosition = window.scrollY; console.log(this.version); } }).mount('#app')