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')