diff --git a/backend/src/routes/marketing_plans.js b/backend/src/routes/marketing_plans.js
index 21eed09..2d9e308 100644
--- a/backend/src/routes/marketing_plans.js
+++ b/backend/src/routes/marketing_plans.js
@@ -3,6 +3,10 @@ const express = require('express');
const Marketing_plansService = require('../services/marketing_plans');
const Marketing_plansDBApi = require('../db/api/marketing_plans');
+const Plan_channelsDBApi = require('../db/api/plan_channels');
+const Campaign_tasksDBApi = require('../db/api/campaign_tasks');
+const db = require('../db/models');
+const ValidationError = require('../services/notifications/errors/validation');
const wrapAsync = require('../helpers').wrapAsync;
@@ -95,6 +99,112 @@ router.post('/', wrapAsync(async (req, res) => {
res.status(200).send(payload);
}));
+const allowedWorkflowChannels = ['social', 'ads', 'email', 'seo'];
+
+const channelCopy = {
+ social: {
+ label: 'المحتوى الاجتماعي',
+ objective: 'بناء حضور مستمر وجدولة منشورات الحملة',
+ task: 'اكتب تقويم منشورات الأسبوع الأول',
+ },
+ ads: {
+ label: 'الإعلانات المدفوعة',
+ objective: 'إطلاق حملة مدفوعة برسالة واضحة وميزانية مضبوطة',
+ task: 'جهّز مسودة الإعلان والجمهور المستهدف',
+ },
+ email: {
+ label: 'البريد الإلكتروني',
+ objective: 'تحويل الجمهور المهتم عبر رسالة بريدية مركزة',
+ task: 'اكتب أول رسالة بريدية للحملة',
+ },
+ seo: {
+ label: 'تحسين محركات البحث',
+ objective: 'رفع قابلية اكتشاف المشروع عبر كلمات مفتاحية مناسبة',
+ task: 'اختر 5 كلمات مفتاحية وصفحة هبوط مناسبة',
+ },
+};
+
+/**
+ * Create the first complete marketing planning slice: plan + channel budgets + starter tasks.
+ */
+router.post('/workflow', wrapAsync(async (req, res) => {
+ const data = req.body.data || {};
+ const selectedChannels = Array.isArray(data.selected_channels)
+ ? data.selected_channels.filter((channel) => allowedWorkflowChannels.includes(channel))
+ : [];
+ const totalBudget = Number(data.total_budget);
+
+ if (!data.project_name || !data.marketing_goal || !data.target_audience || !Number.isFinite(totalBudget) || totalBudget <= 0 || !selectedChannels.length) {
+ throw new ValidationError('errors.validation.message');
+ }
+
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ const marketingPlan = await Marketing_plansDBApi.create(
+ {
+ project_name: data.project_name.trim(),
+ marketing_goal: data.marketing_goal.trim(),
+ target_audience: data.target_audience.trim(),
+ total_budget: totalBudget,
+ status: data.status || 'draft',
+ notes: data.notes || 'تم إنشاؤها من معمل خطوات التسويق.',
+ },
+ { currentUser: req.currentUser, transaction },
+ );
+
+ const baseAllocation = Number((totalBudget / selectedChannels.length).toFixed(2));
+ let assignedBudget = 0;
+ const channels = [];
+ const tasks = [];
+
+ for (let index = 0; index < selectedChannels.length; index += 1) {
+ const channel = selectedChannels[index];
+ const allocatedBudget = index === selectedChannels.length - 1
+ ? Number((totalBudget - assignedBudget).toFixed(2))
+ : baseAllocation;
+
+ assignedBudget += allocatedBudget;
+
+ const planChannel = await Plan_channelsDBApi.create(
+ {
+ channel,
+ allocated_budget: allocatedBudget,
+ objective: channelCopy[channel].objective,
+ marketing_plan: marketingPlan.id,
+ },
+ { currentUser: req.currentUser, transaction },
+ );
+
+ const campaignTask = await Campaign_tasksDBApi.create(
+ {
+ title: channelCopy[channel].task,
+ description: `مهمة بداية لقناة ${channelCopy[channel].label} ضمن خطة ${data.project_name.trim()}.`,
+ stage: index === 0 ? 'doing' : 'to_do',
+ channel,
+ priority: index + 1,
+ marketing_plan: marketingPlan.id,
+ },
+ { currentUser: req.currentUser, transaction },
+ );
+
+ channels.push(planChannel);
+ tasks.push(campaignTask);
+ }
+
+ await transaction.commit();
+
+ res.status(200).send({
+ plan: marketingPlan,
+ channels,
+ tasks,
+ });
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+}));
+
/**
* @swagger
* /api/budgets/bulk-import:
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 97ca4db..c7269d8 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -32,6 +32,12 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
+ {
+ href: '/marketing-studio',
+ label: 'خطوات التسويق',
+ icon: icon.mdiChartTimelineVariant,
+ permissions: 'READ_MARKETING_PLANS'
+ },
{
href: '/marketing_plans/marketing_plans-list',
label: 'Marketing plans',
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 51f9e0b..e7abec2 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,101 @@
+import React, { ReactElement } from 'react'
+import Head from 'next/head'
+import Link from 'next/link'
+import LayoutGuest from '../layouts/Guest'
+import { getPageTitle } from '../config'
-import React, { useEffect, useState } from 'react';
-import type { ReactElement } from 'react';
-import Head from 'next/head';
-import Link from 'next/link';
-import BaseButton from '../components/BaseButton';
-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 { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
-
-
-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('image');
- const [contentPosition, setContentPosition] = useState('left');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'خطوات التسويق'
-
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
-
- const imageBlock = (image) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
- };
+const IndexPage = () => {
+ const channels = ['التواصل الاجتماعي', 'الإعلانات', 'البريد الإلكتروني', 'SEO']
return (
-
+ <>
-
{getPageTitle('Starter Page')}
+
{getPageTitle('خطوات التسويق')}
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
+
+
+
+ تطبيق عربي حديث لبناء خطط التسويق خطوة بخطوة
+
+
+
+ خطط حملتك، وزّع ميزانيتك، وتابع التنفيذ في مكان واحد.
+
+
+ صُمم خطوات التسويق للمسوقين وأصحاب المشاريع الصغيرة: نموذج خطة واضح، جدول خطط محفوظ، كانبان مهام، ولوحة توزيع ميزانية حسب القنوات.
+
+
+
+
+ ابدأ بناء خطة
+
+
+ تسجيل الدخول
+
+
+
+ {channels.map((channel) => (
+
+ {channel}
+
+ ))}
+
+
-
- );
+
+
+
+
+
+
+
+
لوحة الخطة
+
إطلاق متجر العطور
+
+
قيد التنفيذ
+
+
+ {[['الإعلانات', '42%', '#F97316'], ['التواصل', '28%', '#7C3AED'], ['البريد', '18%', '#0891B2'], ['SEO', '12%', '#16A34A']].map(([label, value, color]) => (
+
+ ))}
+
+
+
+ {['للعمل', 'قيد التنفيذ', 'تم'].map((stage, index) => (
+
+ ))}
+
+
+
+
+
+
+ >
+ )
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return {page};
-};
+IndexPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+export default IndexPage
diff --git a/frontend/src/pages/marketing-studio.tsx b/frontend/src/pages/marketing-studio.tsx
new file mode 100644
index 0000000..435f38a
--- /dev/null
+++ b/frontend/src/pages/marketing-studio.tsx
@@ -0,0 +1,483 @@
+import React, { ReactElement, useEffect, useMemo, useState } from 'react'
+import Head from 'next/head'
+import axios from 'axios'
+import * as icon from '@mdi/js'
+import BaseButton from '../components/BaseButton'
+import CardBox from '../components/CardBox'
+import SectionMain from '../components/SectionMain'
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import { getPageTitle } from '../config'
+
+type MarketingPlan = {
+ id: string
+ project_name: string
+ marketing_goal: string
+ target_audience: string
+ total_budget: string | number
+ status: 'draft' | 'in_progress' | 'completed' | 'archived'
+ createdAt?: string
+}
+
+type PlanChannel = {
+ id: string
+ channel: ChannelKey
+ allocated_budget: string | number
+ objective?: string
+ marketing_plan?: MarketingPlan
+}
+
+type CampaignTask = {
+ id: string
+ title: string
+ description?: string
+ stage: StageKey
+ channel: ChannelKey | 'general'
+ priority?: number
+ marketing_plan?: MarketingPlan
+}
+
+type ChannelKey = 'social' | 'ads' | 'email' | 'seo'
+type StageKey = 'to_do' | 'doing' | 'done'
+
+const channels: { key: ChannelKey; label: string; accent: string }[] = [
+ { key: 'social', label: 'التواصل الاجتماعي', accent: '#7C3AED' },
+ { key: 'ads', label: 'الإعلانات', accent: '#F97316' },
+ { key: 'email', label: 'البريد الإلكتروني', accent: '#0891B2' },
+ { key: 'seo', label: 'SEO', accent: '#16A34A' },
+]
+
+const statusLabels: Record = {
+ draft: 'مسودة',
+ in_progress: 'قيد التنفيذ',
+ completed: 'مكتملة',
+ archived: 'مؤرشفة',
+}
+
+const stageLabels: Record = {
+ to_do: 'للعمل',
+ doing: 'قيد التنفيذ',
+ done: 'تم',
+}
+
+const defaultForm = {
+ project_name: '',
+ marketing_goal: '',
+ target_audience: '',
+ total_budget: '',
+ selected_channels: ['social', 'ads'] as ChannelKey[],
+}
+
+const formatBudget = (value: string | number) =>
+ new Intl.NumberFormat('ar', { maximumFractionDigits: 0 }).format(Number(value || 0))
+
+const MarketingStudio = () => {
+ const [form, setForm] = useState(defaultForm)
+ const [plans, setPlans] = useState([])
+ const [planChannels, setPlanChannels] = useState([])
+ const [tasks, setTasks] = useState([])
+ const [selectedPlanId, setSelectedPlanId] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [feedback, setFeedback] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
+
+ const selectedPlan = useMemo(
+ () => plans.find((plan) => plan.id === selectedPlanId) || plans[0],
+ [plans, selectedPlanId],
+ )
+
+ const visibleTasks = useMemo(() => {
+ if (!selectedPlan) return tasks
+ return tasks.filter((task) => task.marketing_plan?.id === selectedPlan.id)
+ }, [selectedPlan, tasks])
+
+ const visibleChannels = useMemo(() => {
+ if (!selectedPlan) return planChannels
+ return planChannels.filter((channel) => channel.marketing_plan?.id === selectedPlan.id)
+ }, [selectedPlan, planChannels])
+
+ const loadWorkspace = async (planToSelect?: string) => {
+ setLoading(true)
+ setFeedback(null)
+
+ try {
+ const [plansResponse, channelsResponse, tasksResponse] = await Promise.all([
+ axios.get('marketing_plans?limit=50&page=0'),
+ axios.get('plan_channels?limit=200&page=0'),
+ axios.get('campaign_tasks?limit=200&page=0'),
+ ])
+
+ const loadedPlans = plansResponse.data.rows || []
+ setPlans(loadedPlans)
+ setPlanChannels(channelsResponse.data.rows || [])
+ setTasks(tasksResponse.data.rows || [])
+
+ if (planToSelect) {
+ setSelectedPlanId(planToSelect)
+ } else if (!selectedPlanId && loadedPlans[0]) {
+ setSelectedPlanId(loadedPlans[0].id)
+ }
+ } catch (error) {
+ console.error('Failed to load marketing workspace', error)
+ setFeedback({ type: 'error', text: 'تعذر تحميل مساحة العمل. تحقق من الاتصال أو الصلاحيات.' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ loadWorkspace()
+ }, [])
+
+ const toggleChannel = (channel: ChannelKey) => {
+ setForm((current) => ({
+ ...current,
+ selected_channels: current.selected_channels.includes(channel)
+ ? current.selected_channels.filter((item) => item !== channel)
+ : [...current.selected_channels, channel],
+ }))
+ }
+
+ const submitPlan = async (event: React.FormEvent) => {
+ event.preventDefault()
+ const budget = Number(form.total_budget)
+
+ if (!form.project_name.trim() || !form.marketing_goal.trim() || !form.target_audience.trim() || !budget || budget <= 0 || !form.selected_channels.length) {
+ setFeedback({ type: 'error', text: 'أكمل اسم المشروع والهدف والجمهور والميزانية واختر قناة واحدة على الأقل.' })
+ return
+ }
+
+ setSaving(true)
+ setFeedback(null)
+
+ try {
+ const response = await axios.post('marketing_plans/workflow', {
+ data: {
+ ...form,
+ total_budget: budget,
+ status: 'draft',
+ },
+ })
+
+ setForm(defaultForm)
+ setFeedback({ type: 'success', text: 'تم إنشاء الخطة مع تقسيم الميزانية ومهام البداية.' })
+ await loadWorkspace(response.data.plan.id)
+ } catch (error) {
+ console.error('Failed to create marketing plan workflow', error)
+ setFeedback({ type: 'error', text: 'تعذر إنشاء الخطة. راجع البيانات وحاول مرة أخرى.' })
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const updatePlanStatus = async (plan: MarketingPlan, status: MarketingPlan['status']) => {
+ try {
+ await axios.put(`marketing_plans/${plan.id}`, { id: plan.id, data: { status } })
+ await loadWorkspace(plan.id)
+ } catch (error) {
+ console.error('Failed to update plan status', error)
+ setFeedback({ type: 'error', text: 'لم يتم تحديث حالة الخطة.' })
+ }
+ }
+
+ const moveTask = async (task: CampaignTask, stage: StageKey) => {
+ try {
+ await axios.put(`campaign_tasks/${task.id}`, {
+ id: task.id,
+ data: { stage, completed_at: stage === 'done' ? new Date().toISOString() : null },
+ })
+ await loadWorkspace(selectedPlan?.id)
+ } catch (error) {
+ console.error('Failed to move campaign task', error)
+ setFeedback({ type: 'error', text: 'تعذر نقل المهمة بين الأعمدة.' })
+ }
+ }
+
+ const totalVisibleBudget = visibleChannels.reduce((sum, channel) => sum + Number(channel.allocated_budget || 0), 0)
+
+ return (
+ <>
+
+ {getPageTitle('خطوات التسويق')}
+
+
+
+
+
+
+
+ معمل التخطيط التسويقي خطوة بخطوة
+
+
+
خطوات التسويق
+
+ أنشئ خطة تسويق واضحة، وزّع الميزانية تلقائياً على القنوات، ثم تابع مهام التنفيذ على لوحة كانبان واحدة.
+
+
+
+
+
{plans.length}
+
خطط محفوظة
+
+
+
{tasks.length}
+
مهام حملة
+
+
+
{formatBudget(totalVisibleBudget)}
+
ميزانية معروضة
+
+
+
+
+
الخطة النشطة
+ {selectedPlan ? (
+
+
{selectedPlan.project_name}
+
{selectedPlan.marketing_goal}
+
+ {statusLabels[selectedPlan.status]}
+ {formatBudget(selectedPlan.total_budget)} ر.س
+
+
+ ) : (
+
ابدأ بإنشاء أول خطة لتظهر تفاصيلها هنا.
+ )}
+
+
+
+
+
+
+
+
+ {feedback && (
+
+ {feedback.text}
+
+ )}
+
+
+
+
+
+
+
+
+
+
٢. الخطط المحفوظة
+
جدول الحالة
+
+ {loading &&
تحميل...}
+
+
+ {plans.length ? (
+
+
+
+
+ | المشروع |
+ الجمهور |
+ الميزانية |
+ الحالة |
+
+
+
+ {plans.map((plan) => (
+ setSelectedPlanId(plan.id)}
+ >
+ | {plan.project_name} |
+ {plan.target_audience} |
+ {formatBudget(plan.total_budget)} ر.س |
+
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+ لا توجد خطط بعد. املأ النموذج لإنشاء أول خطة تسويقية.
+
+ )}
+
+
+
+
+
+
+
٣. لوحة الميزانية
+
توزيع الميزانية حسب القناة
+
+ {visibleChannels.length ? (
+
+ {visibleChannels.map((channel) => {
+ const meta = channels.find((item) => item.key === channel.channel)
+ const percent = totalVisibleBudget ? Math.round((Number(channel.allocated_budget || 0) / totalVisibleBudget) * 100) : 0
+ return (
+
+
+ {meta?.label || channel.channel}
+ {formatBudget(channel.allocated_budget)} ر.س · {percent}%
+
+
+ {channel.objective &&
{channel.objective}
}
+
+ )
+ })}
+
+ ) : (
+
+ اختر خطة تحتوي على قنوات لتظهر لوحة توزيع الميزانية.
+
+ )}
+
+
+
+
+
٤. كانبان التنفيذ
+
مهام الحملة
+
+
+ {(Object.keys(stageLabels) as StageKey[]).map((stage) => (
+
+
+
{stageLabels[stage]}
+
+ {visibleTasks.filter((task) => task.stage === stage).length}
+
+
+
+ {visibleTasks.filter((task) => task.stage === stage).map((task) => {
+ const meta = channels.find((item) => item.key === task.channel)
+ return (
+
+
+
{task.title}
+
+
+ {task.description &&
{task.description}
}
+
+
+ )
+ })}
+ {!visibleTasks.filter((task) => task.stage === stage).length && (
+
+ لا توجد مهام في هذا العمود.
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
+
+MarketingStudio.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+export default MarketingStudio