تسويق مدرسة

This commit is contained in:
Flatlogic Bot 2026-04-08 07:01:06 +00:00
parent 2f3a401454
commit dfebf532cf
11 changed files with 1522 additions and 147 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

View File

@ -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;
}
},
};

View File

@ -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;
};

View File

@ -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',

View File

@ -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;

View File

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

View File

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

View File

@ -8,6 +8,12 @@ const menuAside: MenuAsideItem[] = [
label: 'Dashboard',
},
{
href: '/school-marketing',
label: 'التسويق المدرسي',
icon: icon.mdiBullhornOutline,
},
{
href: '/users/users-list',
label: 'Users',

View File

@ -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) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
export default function HomePage() {
const [formData, setFormData] = React.useState<LeadForm>(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 (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
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 (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('نظام تسويق مدرسي')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
<main
dir="rtl"
className="min-h-screen bg-[#F8FAFC] text-slate-900"
style={{ fontFamily: 'Tajawal, Inter, sans-serif' }}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your App Preview app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(circle_at_top,_rgba(139,92,246,0.28),_transparent_55%),radial-gradient(circle_at_top_left,_rgba(14,165,233,0.18),_transparent_45%),linear-gradient(180deg,_#EEF2FF_0%,_#F8FAFC_60%)]" />
<div className="mx-auto flex max-w-7xl flex-col gap-16 px-6 py-6 lg:px-10">
<header className="rounded-[28px] border border-white/80 bg-white/80 px-5 py-4 shadow-lg shadow-slate-200/60 backdrop-blur">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm font-semibold tracking-[0.3em] text-violet-600">SCHOOL MARKETING</div>
<div className="mt-1 text-lg font-bold text-slate-900">نظام إدارة تسويق مدرسي عربي RTL</div>
</div>
<div className="flex flex-wrap items-center gap-3">
<Link href="/login" className="inline-flex items-center gap-2 rounded-full border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-violet-200 hover:text-violet-700">
<BaseIcon path={mdiLogin} size={18} />
تسجيل الدخول
</Link>
<Link href="/school-marketing" className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800">
<BaseIcon path={mdiViewDashboardOutline} size={18} />
واجهة الإدارة
</Link>
</div>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</header>
</BaseButtons>
</CardBox>
<section className="grid items-center gap-8 lg:grid-cols-[1.15fr_0.85fr]">
<div className="space-y-6">
<div className="inline-flex items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-4 py-2 text-sm font-medium text-violet-700">
<BaseIcon path={mdiCheckCircleOutline} size={18} />
أول نسخة عملية: تسجيل + إحالة + واتساب + متابعة
</div>
<div className="space-y-4">
<h1 className="text-4xl font-black leading-tight text-slate-950 md:text-6xl">
اجعل رحلة التسجيل المدرسي أسرع، أوضح، وأقرب لأولياء الأمور.
</h1>
<p className="max-w-2xl text-base leading-8 text-slate-600 md:text-lg">
هذه الصفحة العامة تستقبل طلبات أولياء الأمور باللغة العربية وباتجاه RTL، ثم تنقلها مباشرة إلى لوحة
متابعة داخلية فيها إحالات، واتساب، فيديو، وجدولة وعرض تحليلات أولية لفريق القبول.
</p>
</div>
<div className="flex flex-wrap gap-3">
<a href="#lead-form" className="inline-flex items-center gap-2 rounded-full bg-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-violet-200 transition hover:bg-violet-700">
<BaseIcon path={mdiAccountPlus} size={18} />
ابدأ تسجيل الطالب
</a>
<Link href="/login" className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-violet-200 hover:text-violet-700">
<BaseIcon path={mdiOpenInNew} size={18} />
دخول فريق القبول
</Link>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-[24px] border border-white/80 bg-white/80 p-4 shadow-lg shadow-slate-200/40">
<div className="text-xs text-slate-500">سرعة الاستجابة</div>
<div className="mt-2 text-2xl font-bold">واتساب فوري</div>
</div>
<div className="rounded-[24px] border border-white/80 bg-white/80 p-4 shadow-lg shadow-slate-200/40">
<div className="text-xs text-slate-500">تحويل الإحالات</div>
<div className="mt-2 text-2xl font-bold">مصدر واضح</div>
</div>
<div className="rounded-[24px] border border-white/80 bg-white/80 p-4 shadow-lg shadow-slate-200/40">
<div className="text-xs text-slate-500">حجز العرض</div>
<div className="mt-2 text-2xl font-bold">جدولة سهلة</div>
</div>
</div>
</div>
<CardBox className="overflow-hidden border-0 bg-gradient-to-br from-[#111827] via-[#1E1B4B] to-[#312E81] text-white shadow-2xl shadow-indigo-200/60">
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-indigo-200">رحلة القبول</div>
<div className="mt-2 text-3xl font-bold">من الطلب إلى الجدولة</div>
</div>
<div className="rounded-full border border-white/10 bg-white/10 p-3">
<BaseIcon path={mdiPlayCircleOutline} size={32} />
</div>
</div>
<div className="space-y-3">
{steps.map((step) => (
<div key={step.title} className="rounded-[24px] border border-white/10 bg-white/10 p-4 backdrop-blur">
<div className="flex items-start gap-3">
<div className="rounded-2xl bg-white/10 p-3">
<BaseIcon path={step.icon} size={22} />
</div>
<div>
<div className="font-semibold">{step.title}</div>
<div className="mt-1 text-sm leading-6 text-indigo-100">{step.description}</div>
</div>
</div>
</div>
))}
</div>
</div>
</CardBox>
</section>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{featureCards.map((feature) => (
<CardBox key={feature.title} className="border-0 bg-white/90 shadow-lg shadow-slate-200/60 backdrop-blur">
<div className="space-y-4">
<div className={`inline-flex rounded-2xl bg-gradient-to-br p-3 text-white ${feature.accent}`}>
<BaseIcon path={feature.icon} size={24} />
</div>
<div>
<h2 className="text-xl font-bold text-slate-900">{feature.title}</h2>
<p className="mt-2 text-sm leading-7 text-slate-600">{feature.description}</p>
</div>
</div>
</CardBox>
))}
</section>
<section id="lead-form" className="grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
<div className="space-y-4">
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-700">
<BaseIcon path={mdiMessageTextOutline} size={18} />
نموذج عملي جاهز للاستخدام الآن
</div>
<div>
<h2 className="text-3xl font-bold text-slate-950 md:text-4xl">أرسل طلب التسجيل وسنتواصل عبر واتساب</h2>
<p className="mt-3 max-w-xl text-base leading-8 text-slate-600">
املأ البيانات الأساسية، اذكر مصدر الإحالة إن وجد، وحدّد الوقت المفضل للتواصل. بعد الإرسال سيظهر رقم متابعة يمكن لفريقنا الرجوع إليه بسرعة.
</p>
</div>
<div className="grid gap-3">
<div className="rounded-[24px] border border-slate-200 bg-white p-4">
<div className="text-sm font-semibold text-slate-900">ما الذي يحدث بعد الإرسال؟</div>
<ul className="mt-3 space-y-3 text-sm leading-7 text-slate-600">
<li> استلام الطلب في لوحة الإدارة مباشرة.</li>
<li> بدء المحادثة عبر واتساب من رقم ولي الأمر.</li>
<li> جدولة عرض أو مشاركة فيديو تعريفي حسب حالة الطالب.</li>
</ul>
</div>
{successLead ? (
<div className="rounded-[28px] border border-emerald-200 bg-emerald-50 p-5 shadow-sm">
<div className="flex items-start gap-3">
<div className="rounded-2xl bg-emerald-500 p-3 text-white">
<BaseIcon path={mdiCheckCircleOutline} size={22} />
</div>
<div>
<div className="text-lg font-bold text-emerald-900">تم استلام طلبكم بنجاح</div>
<p className="mt-2 text-sm leading-7 text-emerald-800">
رقم المتابعة: <span className="font-black">{successLead.leadCode}</span>
{' '} سنراجع الطلب ونتواصل معكم قريباً.
</p>
</div>
</div>
</div>
) : null}
</div>
</div>
<CardBox className="border-0 bg-white/95 shadow-2xl shadow-slate-200/60">
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="اسم الطالب">
<input
value={formData.studentName}
onChange={(event) => updateField('studentName', event.target.value)}
placeholder="مثال: أحمد محمد"
/>
</FormField>
<FormField label="اسم ولي الأمر">
<input
value={formData.guardianName}
onChange={(event) => updateField('guardianName', event.target.value)}
placeholder="مثال: محمد علي"
/>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="رقم واتساب" icons={[mdiWhatsapp]}>
<input
value={formData.whatsappNumber}
onChange={(event) => updateField('whatsappNumber', event.target.value)}
placeholder="9665xxxxxxx"
/>
</FormField>
<FormField label="المرحلة الدراسية">
<select value={formData.gradeLevel} onChange={(event) => updateField('gradeLevel', event.target.value)}>
<option value="">اختر المرحلة</option>
<option value="رياض أطفال">رياض أطفال</option>
<option value="ابتدائي">ابتدائي</option>
<option value="متوسط">متوسط</option>
<option value="ثانوي">ثانوي</option>
</select>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="الفرع المفضل">
<input
value={formData.campusPreference}
onChange={(event) => updateField('campusPreference', event.target.value)}
placeholder="مثال: فرع شمال الرياض"
/>
</FormField>
<FormField label="القسم أو البرنامج">
<input
value={formData.interestTrack}
onChange={(event) => updateField('interestTrack', event.target.value)}
placeholder="مثال: STEM / تحفيظ / لغات"
/>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="مصدر الإحالة">
<select value={formData.referralSource} onChange={(event) => updateField('referralSource', event.target.value)}>
<option value="website">الموقع</option>
<option value="instagram">إنستغرام</option>
<option value="whatsapp">واتساب</option>
<option value="parent_referral">إحالة من ولي أمر</option>
<option value="school_event">فعالية مدرسية</option>
<option value="other">أخرى</option>
</select>
</FormField>
<FormField label="اسم المحيل أو الملاحظة المرجعية">
<input
value={formData.referralName}
onChange={(event) => updateField('referralName', event.target.value)}
placeholder="اختياري"
/>
</FormField>
</div>
<FormField label="أفضل وقت للتواصل">
<input
value={formData.preferredContactTime}
onChange={(event) => updateField('preferredContactTime', event.target.value)}
placeholder="مثال: بعد 5 مساءً"
/>
</FormField>
<FormField label="ملاحظات إضافية" hasTextareaHeight>
<textarea
value={formData.notes}
onChange={(event) => updateField('notes', event.target.value)}
placeholder="أي معلومات تساعد فريق القبول على تجهيز الرد المناسب"
/>
</FormField>
{errorMessage ? (
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{errorMessage}
</div>
) : null}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-slate-500">سنستخدم البيانات فقط للتواصل بخصوص التسجيل المدرسي.</div>
<BaseButton
type="submit"
color="info"
icon={mdiAccountPlus}
label={isSubmitting ? 'جاري الإرسال...' : 'إرسال طلب التسجيل'}
disabled={isSubmitting}
/>
</div>
</form>
</CardBox>
</section>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
</main>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
HomePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,583 @@
import {
mdiBullhornOutline,
mdiCalendarClock,
mdiCheckCircleOutline,
mdiMagnify,
mdiMessageTextOutline,
mdiOpenInNew,
mdiSchoolOutline,
mdiVideoOutline,
mdiWhatsapp,
} from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import FormField from '../components/FormField';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
type Inquiry = {
id: string;
leadCode: string;
studentName: string;
guardianName: string;
whatsappNumber: string;
gradeLevel: string;
campusPreference?: string | null;
interestTrack?: string | null;
referralSource: string;
referralName?: string | null;
preferredContactTime?: string | null;
notes?: string | null;
status: string;
meetingAt?: string | null;
demoVideoUrl?: string | null;
counselorNotes?: string | null;
lastContactedAt?: string | null;
createdAt: string;
updatedAt: string;
};
type Summary = {
total: number;
newLeads: number;
scheduled: number;
enrolled: number;
referrals: number;
};
const statusOptions = [
{ value: 'all', label: 'كل الحالات' },
{ value: 'new', label: 'جديد' },
{ value: 'contacted', label: 'تم التواصل' },
{ value: 'demo_scheduled', label: 'عرض مجدول' },
{ value: 'application_started', label: 'بدأ التسجيل' },
{ value: 'enrolled', label: 'تم القبول' },
{ value: 'lost', label: 'غير مهتم' },
];
const statusLabels: Record<string, string> = {
new: 'جديد',
contacted: 'تم التواصل',
demo_scheduled: 'عرض مجدول',
application_started: 'بدأ التسجيل',
enrolled: 'تم القبول',
lost: 'غير مهتم',
};
const referralLabels: Record<string, string> = {
website: 'الموقع',
instagram: 'إنستغرام',
whatsapp: 'واتساب',
parent_referral: 'إحالة ولي أمر',
school_event: 'فعالية مدرسية',
other: 'أخرى',
};
const emptySummary: Summary = {
total: 0,
newLeads: 0,
scheduled: 0,
enrolled: 0,
referrals: 0,
};
function formatDate(value?: string | null) {
if (!value) {
return '—';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return date.toLocaleString('ar-EG', {
dateStyle: 'medium',
timeStyle: 'short',
});
}
function toDateTimeLocal(value?: string | null) {
if (!value) {
return '';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
const pad = (number: number) => number.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
export default function SchoolMarketingPage() {
const { currentUser } = useAppSelector((state) => state.auth);
const [summary, setSummary] = React.useState<Summary>(emptySummary);
const [inquiries, setInquiries] = React.useState<Inquiry[]>([]);
const [selectedId, setSelectedId] = React.useState<string>('');
const [selectedInquiry, setSelectedInquiry] = React.useState<Inquiry | null>(null);
const [statusFilter, setStatusFilter] = React.useState('all');
const [search, setSearch] = React.useState('');
const [loading, setLoading] = React.useState(true);
const [detailLoading, setDetailLoading] = React.useState(false);
const [saving, setSaving] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const [saveMessage, setSaveMessage] = React.useState('');
const [formState, setFormState] = React.useState({
status: 'new',
meetingAt: '',
demoVideoUrl: '',
counselorNotes: '',
});
const loadSummary = React.useCallback(async () => {
const response = await axios.get('/student-inquiries/summary');
setSummary(response.data || emptySummary);
}, []);
const loadInquiries = React.useCallback(async () => {
setLoading(true);
setErrorMessage('');
try {
const response = await axios.get('/student-inquiries', {
params: {
status: statusFilter,
q: search,
},
});
const rows = Array.isArray(response.data?.rows) ? response.data.rows : [];
setInquiries(rows);
if (!rows.length) {
setSelectedId('');
setSelectedInquiry(null);
return;
}
const nextId = rows.some((item: Inquiry) => item.id === selectedId) ? selectedId : rows[0].id;
setSelectedId(nextId);
} catch (error: any) {
setErrorMessage(error?.response?.data || error?.message || 'تعذر تحميل الطلبات');
} finally {
setLoading(false);
}
}, [search, selectedId, statusFilter]);
const loadInquiryDetails = React.useCallback(async (id: string) => {
if (!id) {
return;
}
setDetailLoading(true);
setErrorMessage('');
try {
const response = await axios.get(`/student-inquiries/${id}`);
const inquiry = response.data as Inquiry;
setSelectedInquiry(inquiry);
setFormState({
status: inquiry.status,
meetingAt: toDateTimeLocal(inquiry.meetingAt),
demoVideoUrl: inquiry.demoVideoUrl || '',
counselorNotes: inquiry.counselorNotes || '',
});
} catch (error: any) {
setErrorMessage(error?.response?.data || error?.message || 'تعذر تحميل التفاصيل');
} finally {
setDetailLoading(false);
}
}, []);
React.useEffect(() => {
const timeoutId = window.setTimeout(() => {
loadSummary().catch((error: any) => {
setErrorMessage(error?.response?.data || error?.message || 'تعذر تحميل الملخص');
});
loadInquiries().catch((error: any) => {
setErrorMessage(error?.response?.data || error?.message || 'تعذر تحميل الطلبات');
});
}, 250);
return () => window.clearTimeout(timeoutId);
}, [loadInquiries, loadSummary]);
React.useEffect(() => {
if (!selectedId) {
return;
}
loadInquiryDetails(selectedId).catch((error: any) => {
setErrorMessage(error?.response?.data || error?.message || 'تعذر تحميل التفاصيل');
});
}, [loadInquiryDetails, selectedId]);
const selectedWhatsappLink = React.useMemo(() => {
if (!selectedInquiry?.whatsappNumber) {
return '';
}
return `https://wa.me/${selectedInquiry.whatsappNumber.replace(/[^\d]/g, '')}`;
}, [selectedInquiry?.whatsappNumber]);
const handleSave = async () => {
if (!selectedInquiry) {
return;
}
setSaving(true);
setSaveMessage('');
setErrorMessage('');
try {
await axios.put(`/student-inquiries/${selectedInquiry.id}`, {
data: {
status: formState.status,
meetingAt: formState.meetingAt,
demoVideoUrl: formState.demoVideoUrl,
counselorNotes: formState.counselorNotes,
},
});
setSaveMessage('تم حفظ المتابعة بنجاح');
await Promise.all([loadSummary(), loadInquiries(), loadInquiryDetails(selectedInquiry.id)]);
} catch (error: any) {
setErrorMessage(error?.response?.data || error?.message || 'تعذر حفظ التحديثات');
} finally {
setSaving(false);
}
};
const stats = [
{
label: 'كل الطلبات',
value: summary.total,
accent: 'from-[#8B5CF6] to-[#D946EF]',
icon: mdiBullhornOutline,
},
{
label: 'طلبات جديدة',
value: summary.newLeads,
accent: 'from-[#0EA5E9] to-[#14B8A6]',
icon: mdiSchoolOutline,
},
{
label: 'عروض مجدولة',
value: summary.scheduled,
accent: 'from-[#F97316] to-[#FACC15]',
icon: mdiCalendarClock,
},
{
label: 'إحالات',
value: summary.referrals,
accent: 'from-[#10B981] to-[#22C55E]',
icon: mdiCheckCircleOutline,
},
];
return (
<>
<Head>
<title>{getPageTitle('School Marketing')}</title>
</Head>
<SectionMain>
<div dir="rtl" className="space-y-6 text-right" style={{ fontFamily: 'Tajawal, Inter, sans-serif' }}>
<SectionTitleLineWithButton icon={mdiBullhornOutline} title="لوحة التسويق المدرسي" main>
<div className="text-sm text-slate-500">
{currentUser?.firstName ? `مرحباً ${currentUser.firstName}` : 'متابعة يومية للطلاب المحتملين'}
</div>
</SectionTitleLineWithButton>
<CardBox className="overflow-hidden border-0 bg-gradient-to-br from-[#111827] via-[#1E1B4B] to-[#312E81] text-white shadow-2xl">
<div className="grid gap-6 lg:grid-cols-[1.4fr_1fr]">
<div className="space-y-4">
<div className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/10 px-4 py-2 text-sm text-slate-100">
<BaseIcon path={mdiMessageTextOutline} size={18} />
متابعة واتساب + إحالات + مواعيد عرض
</div>
<div>
<h2 className="text-3xl font-bold leading-tight md:text-4xl">من أول رسالة إلى القبول النهائي</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-200 md:text-base">
هذه أول نسخة عملية لمسار القبول: استلام الطلب من الصفحة العامة، تجميع الإحالات، جدولة العرض،
وتوثيق المتابعة في مكان واحد.
</p>
</div>
</div>
<div className="grid gap-3 rounded-[28px] border border-white/10 bg-white/10 p-4 backdrop-blur">
<div className="rounded-2xl bg-white/10 p-4">
<div className="text-xs text-slate-300">الطلاب المحتملون اليوم</div>
<div className="mt-2 text-3xl font-semibold">{summary.newLeads}</div>
</div>
<div className="rounded-2xl bg-white/10 p-4">
<div className="text-xs text-slate-300">المسجلون فعلياً</div>
<div className="mt-2 text-3xl font-semibold">{summary.enrolled}</div>
</div>
<div className="rounded-2xl bg-white/10 p-4 text-sm leading-6 text-slate-200">
استخدم البحث والتصفية على اليسار، ثم حدّث حالة الطالب وأضف رابط الفيديو أو موعد العرض من لوحة التفاصيل.
</div>
</div>
</div>
</CardBox>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{stats.map((item) => (
<CardBox key={item.label} className="border-0 bg-white/90 shadow-lg shadow-slate-200/70 dark:bg-dark-900">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-slate-500">{item.label}</div>
<div className="mt-2 text-3xl font-bold text-slate-900 dark:text-white">{item.value}</div>
</div>
<div className={`rounded-2xl bg-gradient-to-br p-3 text-white ${item.accent}`}>
<BaseIcon path={item.icon} size={26} />
</div>
</div>
</CardBox>
))}
</div>
<div className="grid gap-6 xl:grid-cols-[420px_1fr]">
<CardBox className="border-0 bg-white/90 shadow-lg shadow-slate-200/70 dark:bg-dark-900">
<div className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-end">
<div className="flex-1">
<FormField label="بحث سريع" icons={[mdiMagnify]}>
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="اسم الطالب أو ولي الأمر أو رقم واتساب"
/>
</FormField>
</div>
<div className="w-full md:w-44">
<FormField label="الحالة">
<select value={statusFilter} onChange={(event) => setStatusFilter(event.target.value)}>
{statusOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</FormField>
</div>
</div>
{errorMessage ? (
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{errorMessage}
</div>
) : null}
{loading ? (
<div className="rounded-3xl border border-dashed border-slate-200 bg-slate-50 px-5 py-12 text-center text-slate-500">
جاري تحميل الطلبات...
</div>
) : null}
{!loading && !inquiries.length ? (
<div className="rounded-3xl border border-dashed border-slate-200 bg-slate-50 px-5 py-12 text-center text-slate-500">
لا توجد طلبات تطابق الفلتر الحالي. جرّب إزالة البحث أو اطلب تسجيل طالب جديد من الصفحة الرئيسية.
</div>
) : null}
<div className="max-h-[720px] space-y-3 overflow-y-auto pr-1">
{inquiries.map((item) => {
const active = item.id === selectedId;
return (
<button
key={item.id}
type="button"
onClick={() => {
setSelectedId(item.id);
setSaveMessage('');
}}
className={`w-full rounded-[24px] border p-4 text-right transition ${
active
? 'border-[#7C3AED] bg-violet-50 shadow-md shadow-violet-100'
: 'border-slate-200 bg-slate-50 hover:border-violet-200 hover:bg-white'
}`}
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="font-semibold text-slate-900 dark:text-white">{item.studentName}</div>
<div className="mt-1 text-xs text-slate-500">{item.guardianName} {item.gradeLevel}</div>
</div>
<span className="rounded-full bg-slate-900 px-3 py-1 text-[11px] text-white">
{statusLabels[item.status] || item.status}
</span>
</div>
<div className="mt-3 text-xs text-slate-500">{item.leadCode}</div>
<div className="mt-2 text-xs text-slate-500">{referralLabels[item.referralSource] || 'أخرى'} {formatDate(item.createdAt)}</div>
</button>
);
})}
</div>
</div>
</CardBox>
<CardBox className="border-0 bg-white/90 shadow-lg shadow-slate-200/70 dark:bg-dark-900">
{!selectedInquiry || detailLoading ? (
<div className="flex min-h-[420px] items-center justify-center rounded-[28px] border border-dashed border-slate-200 bg-slate-50 px-6 text-center text-slate-500">
{detailLoading ? 'جاري تحميل التفاصيل...' : 'اختر طالباً من القائمة لعرض بطاقة المتابعة'}
</div>
) : (
<div className="space-y-6">
<div className="flex flex-col gap-4 border-b border-slate-200 pb-5 md:flex-row md:items-start md:justify-between">
<div>
<div className="inline-flex items-center gap-2 rounded-full bg-violet-100 px-3 py-1 text-xs font-semibold text-violet-700">
<BaseIcon path={mdiSchoolOutline} size={15} />
{selectedInquiry.leadCode}
</div>
<h3 className="mt-3 text-2xl font-bold text-slate-900 dark:text-white">{selectedInquiry.studentName}</h3>
<p className="mt-1 text-sm text-slate-500">ولي الأمر: {selectedInquiry.guardianName}</p>
</div>
<div className="flex flex-wrap gap-2">
<BaseButton
color="success"
icon={mdiWhatsapp}
label="واتساب"
onClick={() => {
if (selectedWhatsappLink) {
window.open(selectedWhatsappLink, '_blank', 'noopener,noreferrer');
}
}}
/>
<BaseButton
color="info"
icon={mdiOpenInNew}
label="فتح الفيديو"
onClick={() => {
if (formState.demoVideoUrl) {
window.open(formState.demoVideoUrl, '_blank', 'noopener,noreferrer');
}
}}
disabled={!formState.demoVideoUrl}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-[24px] bg-slate-50 p-4">
<div className="text-xs text-slate-500">رقم واتساب</div>
<div className="mt-1 font-semibold text-slate-900 dark:text-white">{selectedInquiry.whatsappNumber}</div>
</div>
<div className="rounded-[24px] bg-slate-50 p-4">
<div className="text-xs text-slate-500">المرحلة الدراسية</div>
<div className="mt-1 font-semibold text-slate-900 dark:text-white">{selectedInquiry.gradeLevel}</div>
</div>
<div className="rounded-[24px] bg-slate-50 p-4">
<div className="text-xs text-slate-500">القسم / البرنامج</div>
<div className="mt-1 font-semibold text-slate-900 dark:text-white">{selectedInquiry.interestTrack || 'لم يحدد بعد'}</div>
</div>
<div className="rounded-[24px] bg-slate-50 p-4">
<div className="text-xs text-slate-500">مصدر الإحالة</div>
<div className="mt-1 font-semibold text-slate-900 dark:text-white">
{referralLabels[selectedInquiry.referralSource] || 'أخرى'}
{selectedInquiry.referralName ? `${selectedInquiry.referralName}` : ''}
</div>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-[24px] border border-slate-200 p-4">
<div className="text-xs text-slate-500">أفضل وقت للتواصل</div>
<div className="mt-1 text-sm font-medium text-slate-900 dark:text-white">
{selectedInquiry.preferredContactTime || 'غير محدد'}
</div>
</div>
<div className="rounded-[24px] border border-slate-200 p-4">
<div className="text-xs text-slate-500">آخر تواصل</div>
<div className="mt-1 text-sm font-medium text-slate-900 dark:text-white">
{formatDate(selectedInquiry.lastContactedAt)}
</div>
</div>
</div>
<div className="rounded-[24px] border border-slate-200 p-4">
<div className="text-xs text-slate-500">ملاحظات ولي الأمر</div>
<div className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-700 dark:text-slate-200">
{selectedInquiry.notes || 'لا توجد ملاحظات مضافة من النموذج.'}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="حالة المتابعة">
<select
value={formState.status}
onChange={(event) => setFormState((current) => ({ ...current, status: event.target.value }))}
>
{statusOptions
.filter((option) => option.value !== 'all')
.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</FormField>
<FormField label="موعد العرض أو المكالمة" icons={[mdiCalendarClock]}>
<input
type="datetime-local"
value={formState.meetingAt}
onChange={(event) => setFormState((current) => ({ ...current, meetingAt: event.target.value }))}
/>
</FormField>
</div>
<FormField label="رابط الفيديو التعريفي" icons={[mdiVideoOutline]}>
<input
type="url"
value={formState.demoVideoUrl}
onChange={(event) => setFormState((current) => ({ ...current, demoVideoUrl: event.target.value }))}
placeholder="https://..."
/>
</FormField>
<FormField label="ملاحظات فريق القبول" hasTextareaHeight>
<textarea
value={formState.counselorNotes}
onChange={(event) => setFormState((current) => ({ ...current, counselorNotes: event.target.value }))}
placeholder="أضف نتيجة المكالمة، الخطوات القادمة، أو سبب الاهتمام"
/>
</FormField>
{saveMessage ? (
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
{saveMessage}
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4">
<div className="text-xs text-slate-500">
تم إنشاء الطلب في {formatDate(selectedInquiry.createdAt)}
</div>
<BaseButton
color="info"
icon={mdiCheckCircleOutline}
label={saving ? 'جاري الحفظ...' : 'حفظ التحديثات'}
onClick={handleSave}
disabled={saving}
/>
</div>
</div>
)}
</CardBox>
</div>
</div>
</SectionMain>
</>
);
}
SchoolMarketingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};