From dfebf532cfd50db7c9632ea65308b324b3a96a42 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 8 Apr 2026 07:01:06 +0000 Subject: [PATCH] =?UTF-8?q?=D8=AA=D8=B3=D9=88=D9=8A=D9=82=20=D9=85=D8=AF?= =?UTF-8?q?=D8=B1=D8=B3=D8=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .perm_test_apache | 0 .perm_test_exec | 0 ...20260408110000-create-student-inquiries.js | 172 ++++++ backend/src/db/models/student_inquiries.js | 98 +++ backend/src/index.js | 3 +- backend/src/routes/student_inquiries.js | 282 +++++++++ frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/menuAside.ts | 6 + frontend/src/pages/index.tsx | 519 +++++++++++----- frontend/src/pages/school-marketing.tsx | 583 ++++++++++++++++++ 11 files changed, 1522 insertions(+), 147 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 backend/src/db/migrations/20260408110000-create-student-inquiries.js create mode 100644 backend/src/db/models/student_inquiries.js create mode 100644 backend/src/routes/student_inquiries.js create mode 100644 frontend/src/pages/school-marketing.tsx diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/db/migrations/20260408110000-create-student-inquiries.js b/backend/src/db/migrations/20260408110000-create-student-inquiries.js new file mode 100644 index 0000000..e02a7b9 --- /dev/null +++ b/backend/src/db/migrations/20260408110000-create-student-inquiries.js @@ -0,0 +1,172 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + const rows = await queryInterface.sequelize.query( + "SELECT to_regclass('public.student_inquiries') AS regclass_name;", + { + transaction, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (rows[0].regclass_name) { + await transaction.commit(); + return; + } + + await queryInterface.createTable( + 'student_inquiries', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + leadCode: { + type: Sequelize.DataTypes.STRING(32), + allowNull: false, + unique: true, + }, + studentName: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + guardianName: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + whatsappNumber: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + gradeLevel: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + campusPreference: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + interestTrack: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + referralSource: { + type: Sequelize.DataTypes.ENUM, + values: ['website', 'instagram', 'whatsapp', 'parent_referral', 'school_event', 'other'], + allowNull: false, + defaultValue: 'website', + }, + referralName: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + preferredContactTime: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + notes: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + status: { + type: Sequelize.DataTypes.ENUM, + values: ['new', 'contacted', 'demo_scheduled', 'application_started', 'enrolled', 'lost'], + allowNull: false, + defaultValue: 'new', + }, + meetingAt: { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }, + demoVideoUrl: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + counselorNotes: { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + lastContactedAt: { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }, + createdAt: { + type: Sequelize.DataTypes.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DataTypes.DATE, + allowNull: false, + }, + deletedAt: { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + allowNull: true, + references: { + key: 'id', + model: 'users', + }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + allowNull: true, + references: { + key: 'id', + model: 'users', + }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + const rows = await queryInterface.sequelize.query( + "SELECT to_regclass('public.student_inquiries') AS regclass_name;", + { + transaction, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (!rows[0].regclass_name) { + await transaction.commit(); + return; + } + + await queryInterface.dropTable('student_inquiries', { transaction }); + await queryInterface.sequelize.query( + 'DROP TYPE IF EXISTS "enum_student_inquiries_referralSource";', + { transaction }, + ); + await queryInterface.sequelize.query( + 'DROP TYPE IF EXISTS "enum_student_inquiries_status";', + { transaction }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, +}; diff --git a/backend/src/db/models/student_inquiries.js b/backend/src/db/models/student_inquiries.js new file mode 100644 index 0000000..880f743 --- /dev/null +++ b/backend/src/db/models/student_inquiries.js @@ -0,0 +1,98 @@ +module.exports = function (sequelize, DataTypes) { + const student_inquiries = sequelize.define( + 'student_inquiries', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + leadCode: { + type: DataTypes.STRING(32), + allowNull: false, + unique: true, + }, + studentName: { + type: DataTypes.TEXT, + allowNull: false, + }, + guardianName: { + type: DataTypes.TEXT, + allowNull: false, + }, + whatsappNumber: { + type: DataTypes.TEXT, + allowNull: false, + }, + gradeLevel: { + type: DataTypes.TEXT, + allowNull: false, + }, + campusPreference: { + type: DataTypes.TEXT, + allowNull: true, + }, + interestTrack: { + type: DataTypes.TEXT, + allowNull: true, + }, + referralSource: { + type: DataTypes.ENUM, + values: ['website', 'instagram', 'whatsapp', 'parent_referral', 'school_event', 'other'], + allowNull: false, + defaultValue: 'website', + }, + referralName: { + type: DataTypes.TEXT, + allowNull: true, + }, + preferredContactTime: { + type: DataTypes.TEXT, + allowNull: true, + }, + notes: { + type: DataTypes.TEXT, + allowNull: true, + }, + status: { + type: DataTypes.ENUM, + values: ['new', 'contacted', 'demo_scheduled', 'application_started', 'enrolled', 'lost'], + allowNull: false, + defaultValue: 'new', + }, + meetingAt: { + type: DataTypes.DATE, + allowNull: true, + }, + demoVideoUrl: { + type: DataTypes.TEXT, + allowNull: true, + }, + counselorNotes: { + type: DataTypes.TEXT, + allowNull: true, + }, + lastContactedAt: { + type: DataTypes.DATE, + allowNull: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + student_inquiries.associate = (db) => { + db.student_inquiries.belongsTo(db.users, { + as: 'createdBy', + }); + + db.student_inquiries.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return student_inquiries; +}; diff --git a/backend/src/index.js b/backend/src/index.js index 43460da..54a8483 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -6,7 +6,6 @@ const passport = require('passport'); const path = require('path'); const fs = require('fs'); const bodyParser = require('body-parser'); -const db = require('./db/models'); const config = require('./config'); const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); @@ -38,6 +37,7 @@ const audit_logsRoutes = require('./routes/audit_logs'); const app_settingsRoutes = require('./routes/app_settings'); const api_keysRoutes = require('./routes/api_keys'); +const studentInquiriesRoutes = require('./routes/student_inquiries'); const getBaseUrl = (url) => { @@ -114,6 +114,7 @@ app.use('/api/audit_logs', passport.authenticate('jwt', {session: false}), audit app.use('/api/app_settings', passport.authenticate('jwt', {session: false}), app_settingsRoutes); app.use('/api/api_keys', passport.authenticate('jwt', {session: false}), api_keysRoutes); +app.use('/api/student-inquiries', studentInquiriesRoutes); app.use( '/api/openai', diff --git a/backend/src/routes/student_inquiries.js b/backend/src/routes/student_inquiries.js new file mode 100644 index 0000000..204d584 --- /dev/null +++ b/backend/src/routes/student_inquiries.js @@ -0,0 +1,282 @@ +const express = require('express'); +const passport = require('passport'); +const { Op } = require('sequelize'); + +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const REFERRAL_SOURCES = ['website', 'instagram', 'whatsapp', 'parent_referral', 'school_event', 'other']; +const STATUSES = ['new', 'contacted', 'demo_scheduled', 'application_started', 'enrolled', 'lost']; + +function makeBadRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function cleanText(value) { + if (typeof value !== 'string') { + return ''; + } + + return value.trim(); +} + +function normalizeWhatsapp(value) { + return cleanText(value).replace(/[^\d+]/g, ''); +} + +function normalizeUrl(value) { + const url = cleanText(value); + + if (!url) { + return null; + } + + if (!/^https?:\/\//i.test(url)) { + throw makeBadRequest('Video URL must start with http:// or https://'); + } + + return url; +} + +function normalizeDate(value, fieldName) { + const raw = cleanText(value); + + if (!raw) { + return null; + } + + const parsed = new Date(raw); + if (Number.isNaN(parsed.getTime())) { + throw makeBadRequest(`${fieldName} is not a valid date`); + } + + return parsed; +} + +async function generateLeadCode() { + for (let index = 0; index < 5; index += 1) { + const suffix = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 90 + 10)}`; + const leadCode = `SM-${suffix}`; + const existing = await db.student_inquiries.findOne({ where: { leadCode } }); + + if (!existing) { + return leadCode; + } + } + + throw new Error('Could not generate a unique lead code'); +} + +function buildPublicPayload(data) { + const studentName = cleanText(data.studentName); + const guardianName = cleanText(data.guardianName); + const whatsappNumber = normalizeWhatsapp(data.whatsappNumber); + const gradeLevel = cleanText(data.gradeLevel); + const campusPreference = cleanText(data.campusPreference); + const interestTrack = cleanText(data.interestTrack); + const referralSource = cleanText(data.referralSource) || 'website'; + const referralName = cleanText(data.referralName); + const preferredContactTime = cleanText(data.preferredContactTime); + const notes = cleanText(data.notes); + + if (!studentName) { + throw makeBadRequest('Student name is required'); + } + + if (!guardianName) { + throw makeBadRequest('Guardian name is required'); + } + + if (!whatsappNumber || whatsappNumber.length < 8) { + throw makeBadRequest('Please enter a valid WhatsApp number'); + } + + if (!gradeLevel) { + throw makeBadRequest('Grade level is required'); + } + + if (!REFERRAL_SOURCES.includes(referralSource)) { + throw makeBadRequest('Referral source is invalid'); + } + + return { + studentName, + guardianName, + whatsappNumber, + gradeLevel, + campusPreference, + interestTrack, + referralSource, + referralName, + preferredContactTime, + notes, + }; +} + +function buildUpdatePayload(data) { + const payload = {}; + + if (Object.prototype.hasOwnProperty.call(data, 'status')) { + const status = cleanText(data.status); + if (!STATUSES.includes(status)) { + throw makeBadRequest('Status is invalid'); + } + payload.status = status; + } + + if (Object.prototype.hasOwnProperty.call(data, 'meetingAt')) { + payload.meetingAt = normalizeDate(data.meetingAt, 'Meeting date'); + } + + if (Object.prototype.hasOwnProperty.call(data, 'lastContactedAt')) { + payload.lastContactedAt = normalizeDate(data.lastContactedAt, 'Last contacted at'); + } + + if (Object.prototype.hasOwnProperty.call(data, 'demoVideoUrl')) { + payload.demoVideoUrl = normalizeUrl(data.demoVideoUrl); + } + + if (Object.prototype.hasOwnProperty.call(data, 'counselorNotes')) { + payload.counselorNotes = cleanText(data.counselorNotes); + } + + if (payload.status && payload.status !== 'new' && !payload.lastContactedAt) { + payload.lastContactedAt = new Date(); + } + + return payload; +} + +router.post( + '/public-submit', + wrapAsync(async (req, res) => { + const data = buildPublicPayload(req.body.data || {}); + + const inquiry = await db.student_inquiries.create({ + ...data, + leadCode: await generateLeadCode(), + }); + + res.status(200).send({ + id: inquiry.id, + leadCode: inquiry.leadCode, + status: inquiry.status, + }); + }), +); + +router.use(passport.authenticate('jwt', { session: false })); + +router.get( + '/summary', + wrapAsync(async (req, res) => { + const [total, newLeads, scheduled, enrolled, referrals] = await Promise.all([ + db.student_inquiries.count(), + db.student_inquiries.count({ where: { status: 'new' } }), + db.student_inquiries.count({ where: { status: 'demo_scheduled' } }), + db.student_inquiries.count({ where: { status: 'enrolled' } }), + db.student_inquiries.count({ + where: { + [Op.or]: [ + { referralSource: 'parent_referral' }, + { + referralName: { + [Op.not]: null, + }, + }, + ], + }, + }), + ]); + + res.status(200).send({ + total, + newLeads, + scheduled, + enrolled, + referrals, + }); + }), +); + +router.get( + '/', + wrapAsync(async (req, res) => { + const status = cleanText(req.query.status); + const q = cleanText(req.query.q); + const where = {}; + + if (status && status !== 'all') { + if (!STATUSES.includes(status)) { + throw makeBadRequest('Status filter is invalid'); + } + where.status = status; + } + + if (q) { + where[Op.or] = [ + { studentName: { [Op.iLike]: `%${q}%` } }, + { guardianName: { [Op.iLike]: `%${q}%` } }, + { whatsappNumber: { [Op.iLike]: `%${q}%` } }, + { leadCode: { [Op.iLike]: `%${q}%` } }, + { referralName: { [Op.iLike]: `%${q}%` } }, + ]; + } + + const rows = await db.student_inquiries.findAll({ + where, + order: [['createdAt', 'DESC']], + limit: 100, + }); + + res.status(200).send({ + rows, + count: rows.length, + }); + }), +); + +router.get( + '/:id', + wrapAsync(async (req, res) => { + const inquiry = await db.student_inquiries.findByPk(req.params.id); + + if (!inquiry) { + const notFound = new Error('Lead not found'); + notFound.code = 404; + throw notFound; + } + + res.status(200).send(inquiry); + }), +); + +router.put( + '/:id', + wrapAsync(async (req, res) => { + const inquiry = await db.student_inquiries.findByPk(req.params.id); + + if (!inquiry) { + const notFound = new Error('Lead not found'); + notFound.code = 404; + throw notFound; + } + + const data = buildUpdatePayload(req.body.data || {}); + + await inquiry.update({ + ...data, + updatedById: req.currentUser?.id || null, + }); + + res.status(200).send(true); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 64df84e..eab5f82 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -8,6 +8,12 @@ const menuAside: MenuAsideItem[] = [ label: 'Dashboard', }, + { + href: '/school-marketing', + label: 'التسويق المدرسي', + icon: icon.mdiBullhornOutline, + }, + { href: '/users/users-list', label: 'Users', diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index f4a7856..83078a1 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,401 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; +import { + mdiAccountPlus, + mdiCalendarClock, + mdiCheckCircleOutline, + mdiLogin, + mdiMessageTextOutline, + mdiOpenInNew, + mdiPlayCircleOutline, + mdiSchoolOutline, + mdiVideoOutline, + mdiViewDashboardOutline, + mdiWhatsapp, +} from '@mdi/js'; +import axios from 'axios'; import Head from 'next/head'; import Link from 'next/link'; +import React, { ReactElement } from 'react'; import BaseButton from '../components/BaseButton'; +import BaseIcon from '../components/BaseIcon'; import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; -import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; +import FormField from '../components/FormField'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import LayoutGuest from '../layouts/Guest'; +type LeadForm = { + studentName: string; + guardianName: string; + whatsappNumber: string; + gradeLevel: string; + campusPreference: string; + interestTrack: string; + referralSource: string; + referralName: string; + preferredContactTime: string; + notes: string; +}; -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('video'); - const [contentPosition, setContentPosition] = useState('right'); - const textColor = useAppSelector((state) => state.style.linkColor); +const initialForm: LeadForm = { + studentName: '', + guardianName: '', + whatsappNumber: '', + gradeLevel: '', + campusPreference: '', + interestTrack: '', + referralSource: 'website', + referralName: '', + preferredContactTime: '', + notes: '', +}; - const title = 'App Preview' +const featureCards = [ + { + icon: mdiAccountPlus, + title: 'تسجيل طالب جديد', + description: 'نموذج سريع يجمع بيانات الطالب وولي الأمر والمرحلة الدراسية في دقيقة واحدة.', + accent: 'from-[#8B5CF6] to-[#D946EF]', + }, + { + icon: mdiWhatsapp, + title: 'إحالات وواتساب', + description: 'تتبّع مصدر الإحالة وابدأ محادثة واتساب من لوحة الإدارة مباشرة.', + accent: 'from-[#10B981] to-[#22C55E]', + }, + { + icon: mdiCalendarClock, + title: 'جدولة وعرض فيديو', + description: 'سجّل الوقت الأنسب للتواصل وأرسل فيديو تعريفي أو حدّد موعد عرض.', + accent: 'from-[#F97316] to-[#FACC15]', + }, + { + icon: mdiCheckCircleOutline, + title: 'تحليلات أولية', + description: 'شاهد الطلبات الجديدة، العروض المجدولة، وعدد الإحالات في لمحة واحدة.', + accent: 'from-[#0EA5E9] to-[#14B8A6]', + }, +]; - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); +const steps = [ + { + icon: mdiSchoolOutline, + title: '1) ولي الأمر يرسل الطلب', + description: 'البيانات تصل إلى النظام مع رقم متابعة واضح ومصدر الإحالة.', + }, + { + icon: mdiMessageTextOutline, + title: '2) فريق القبول يتابع', + description: 'فتح واتساب، تحديث الحالة، وإضافة ملاحظات المتابعة في نفس الشاشة.', + }, + { + icon: mdiVideoOutline, + title: '3) جدولة وعرض', + description: 'إرسال فيديو تعريفي أو تحديد موعد عرض قبل الانتقال إلى مرحلة التسجيل.', + }, +]; - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); +export default function HomePage() { + const [formData, setFormData] = React.useState(initialForm); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const [successLead, setSuccessLead] = React.useState<{ leadCode: string; status: string } | null>(null); - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; + const updateField = (field: keyof LeadForm, value: string) => { + setFormData((current) => ({ ...current, [field]: value })); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setIsSubmitting(true); + setErrorMessage(''); + + try { + const response = await axios.post('/student-inquiries/public-submit', { + data: formData, + }); + setSuccessLead(response.data); + setFormData(initialForm); + } catch (error: any) { + setErrorMessage(error?.response?.data || error?.message || 'تعذر إرسال الطلب الآن'); + } finally { + setIsSubmitting(false); + } + }; return ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('نظام تسويق مدرسي')} - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+ +
+
+
+
+
SCHOOL MARKETING
+
نظام إدارة تسويق مدرسي عربي RTL
+
+
+ + + تسجيل الدخول + + + + واجهة الإدارة + +
- - - +
- - +
+
+
+ + أول نسخة عملية: تسجيل + إحالة + واتساب + متابعة +
+
+

+ اجعل رحلة التسجيل المدرسي أسرع، أوضح، وأقرب لأولياء الأمور. +

+

+ هذه الصفحة العامة تستقبل طلبات أولياء الأمور باللغة العربية وباتجاه RTL، ثم تنقلها مباشرة إلى لوحة + متابعة داخلية فيها إحالات، واتساب، فيديو، وجدولة وعرض تحليلات أولية لفريق القبول. +

+
+
+ + + ابدأ تسجيل الطالب + + + + دخول فريق القبول + +
+
+
+
سرعة الاستجابة
+
واتساب فوري
+
+
+
تحويل الإحالات
+
مصدر واضح
+
+
+
حجز العرض
+
جدولة سهلة
+
+
+
+ + +
+
+
+
رحلة القبول
+
من الطلب إلى الجدولة
+
+
+ +
+
+
+ {steps.map((step) => ( +
+
+
+ +
+
+
{step.title}
+
{step.description}
+
+
+
+ ))} +
+
+
+
+ +
+ {featureCards.map((feature) => ( + +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+
+ ))} +
+ +
+
+
+ + نموذج عملي جاهز للاستخدام الآن +
+
+

أرسل طلب التسجيل وسنتواصل عبر واتساب

+

+ املأ البيانات الأساسية، اذكر مصدر الإحالة إن وجد، وحدّد الوقت المفضل للتواصل. بعد الإرسال سيظهر رقم متابعة يمكن لفريقنا الرجوع إليه بسرعة. +

+
+
+
+
ما الذي يحدث بعد الإرسال؟
+
    +
  • • استلام الطلب في لوحة الإدارة مباشرة.
  • +
  • • بدء المحادثة عبر واتساب من رقم ولي الأمر.
  • +
  • • جدولة عرض أو مشاركة فيديو تعريفي حسب حالة الطالب.
  • +
+
+ {successLead ? ( +
+
+
+ +
+
+
تم استلام طلبكم بنجاح
+

+ رقم المتابعة: {successLead.leadCode} + {' '}— سنراجع الطلب ونتواصل معكم قريباً. +

+
+
+
+ ) : null} +
+
+ + +
+
+ + updateField('studentName', event.target.value)} + placeholder="مثال: أحمد محمد" + /> + + + updateField('guardianName', event.target.value)} + placeholder="مثال: محمد علي" + /> + +
+ +
+ + updateField('whatsappNumber', event.target.value)} + placeholder="9665xxxxxxx" + /> + + + + +
+ +
+ + updateField('campusPreference', event.target.value)} + placeholder="مثال: فرع شمال الرياض" + /> + + + updateField('interestTrack', event.target.value)} + placeholder="مثال: STEM / تحفيظ / لغات" + /> + +
+ +
+ + + + + updateField('referralName', event.target.value)} + placeholder="اختياري" + /> + +
+ + + updateField('preferredContactTime', event.target.value)} + placeholder="مثال: بعد 5 مساءً" + /> + + + +