Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
5c6edae417 ل2 2026-08-06 15:43:30 +00:00
6 changed files with 691 additions and 159 deletions

View File

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

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

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

View File

@ -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) => (
<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>
);
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 IndexPage = () => {
const channels = ['التواصل الاجتماعي', 'الإعلانات', 'البريد الإلكتروني', 'SEO']
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`}
>
{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!"/>
<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>
<main dir="rtl" className="min-h-screen overflow-hidden bg-[#F8FAFC] text-slate-950">
<section className="relative isolate px-6 py-6 lg:px-12">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_10%_10%,rgba(16,185,129,0.20),transparent_28%),radial-gradient(circle_at_85%_5%,rgba(249,115,22,0.18),transparent_24%)]" />
<nav className="mx-auto flex max-w-7xl items-center justify-between rounded-full border border-white/80 bg-white/80 px-5 py-3 shadow-xl shadow-slate-200/70 backdrop-blur">
<Link href="/" className="text-xl font-black text-emerald-700">خطوات التسويق</Link>
<div className="flex items-center gap-3 text-sm font-semibold">
<Link href="/marketing-studio" className="hidden rounded-full px-4 py-2 text-slate-600 transition hover:bg-slate-100 md:inline-flex">مساحة العمل</Link>
<Link href="/login" className="rounded-full bg-slate-950 px-5 py-2 text-white shadow-lg shadow-slate-300 transition hover:bg-emerald-700">دخول الإدارة</Link>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</nav>
</BaseButtons>
</CardBox>
</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 className="mx-auto grid max-w-7xl items-center gap-10 py-16 lg:grid-cols-[1.05fr_0.95fr] lg:py-24">
<div className="space-y-8">
<span className="inline-flex rounded-full border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-bold text-emerald-700">
تطبيق عربي حديث لبناء خطط التسويق خطوة بخطوة
</span>
<div className="space-y-5">
<h1 className="max-w-4xl text-5xl font-black leading-[1.12] tracking-tight md:text-7xl">
خطط حملتك، وزّع ميزانيتك، وتابع التنفيذ في مكان واحد.
</h1>
<p className="max-w-2xl text-lg leading-9 text-slate-600">
صُمم <strong>خطوات التسويق</strong> للمسوقين وأصحاب المشاريع الصغيرة: نموذج خطة واضح، جدول خطط محفوظ، كانبان مهام، ولوحة توزيع ميزانية حسب القنوات.
</p>
</div>
<div className="flex flex-wrap gap-3">
<Link href="/marketing-studio" className="rounded-full bg-emerald-600 px-7 py-4 font-bold text-white shadow-xl shadow-emerald-200 transition hover:-translate-y-0.5 hover:bg-emerald-700">
ابدأ بناء خطة
</Link>
<Link href="/login" className="rounded-full border border-slate-200 bg-white px-7 py-4 font-bold text-slate-700 transition hover:-translate-y-0.5 hover:border-emerald-300">
تسجيل الدخول
</Link>
</div>
<div className="grid max-w-2xl grid-cols-2 gap-3 sm:grid-cols-4">
{channels.map((channel) => (
<div key={channel} className="rounded-2xl border border-white bg-white/80 p-4 text-center text-sm font-bold text-slate-600 shadow-lg shadow-slate-200/60">
{channel}
</div>
))}
</div>
</div>
</div>
);
<div className="relative">
<div className="absolute -left-8 top-10 h-36 w-36 rounded-full bg-orange-300/40 blur-3xl" />
<div className="absolute -right-8 bottom-10 h-44 w-44 rounded-full bg-emerald-300/40 blur-3xl" />
<div className="relative rounded-[2.5rem] border border-white bg-white/85 p-5 shadow-2xl shadow-slate-300/70 backdrop-blur">
<div className="rounded-[2rem] bg-slate-950 p-5 text-white">
<div className="mb-6 flex items-center justify-between">
<div>
<p className="text-sm text-emerald-200">لوحة الخطة</p>
<h2 className="text-2xl font-black">إطلاق متجر العطور</h2>
</div>
<span className="rounded-full bg-emerald-400 px-3 py-1 text-xs font-bold text-emerald-950">قيد التنفيذ</span>
</div>
<div className="space-y-4">
{[['الإعلانات', '42%', '#F97316'], ['التواصل', '28%', '#7C3AED'], ['البريد', '18%', '#0891B2'], ['SEO', '12%', '#16A34A']].map(([label, value, color]) => (
<div key={label}>
<div className="mb-2 flex justify-between text-sm text-slate-300"><span>{label}</span><span>{value}</span></div>
<div className="h-3 rounded-full bg-white/10"><div className="h-3 rounded-full" style={{ width: value, backgroundColor: color }} /></div>
</div>
))}
</div>
</div>
<div className="mt-4 grid grid-cols-3 gap-3">
{['للعمل', 'قيد التنفيذ', 'تم'].map((stage, index) => (
<div key={stage} className="rounded-3xl bg-slate-50 p-3">
<p className="mb-3 text-xs font-bold text-slate-500">{stage}</p>
<div className="space-y-2">
<div className="h-16 rounded-2xl bg-white p-3 shadow-sm"><div className="h-2 w-16 rounded bg-slate-200" /><div className="mt-3 h-2 w-10 rounded bg-slate-100" /></div>
{index === 1 && <div className="h-16 rounded-2xl bg-emerald-50 p-3 shadow-sm"><div className="h-2 w-14 rounded bg-emerald-300" /><div className="mt-3 h-2 w-12 rounded bg-emerald-100" /></div>}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
</main>
</>
)
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
IndexPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>
}
export default IndexPage

View File

@ -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<MarketingPlan['status'], string> = {
draft: 'مسودة',
in_progress: 'قيد التنفيذ',
completed: 'مكتملة',
archived: 'مؤرشفة',
}
const stageLabels: Record<StageKey, string> = {
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<MarketingPlan[]>([])
const [planChannels, setPlanChannels] = useState<PlanChannel[]>([])
const [tasks, setTasks] = useState<CampaignTask[]>([])
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 (
<>
<Head>
<title>{getPageTitle('خطوات التسويق')}</title>
</Head>
<SectionMain>
<div dir="rtl" className="space-y-6 font-sans text-slate-900 dark:text-slate-100">
<div className="overflow-hidden rounded-[2rem] bg-[#111827] shadow-2xl shadow-emerald-900/20">
<div className="grid gap-8 bg-[radial-gradient(circle_at_top_left,_rgba(20,184,166,0.22),_transparent_32%),linear-gradient(135deg,_#111827,_#0F172A_52%,_#064E3B)] p-8 text-white lg:grid-cols-[1.15fr_0.85fr]">
<div className="space-y-5">
<span className="inline-flex rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm text-emerald-100 backdrop-blur">
معمل التخطيط التسويقي خطوة بخطوة
</span>
<div>
<h1 className="text-4xl font-black leading-tight md:text-5xl">خطوات التسويق</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-emerald-50/90">
أنشئ خطة تسويق واضحة، وزّع الميزانية تلقائياً على القنوات، ثم تابع مهام التنفيذ على لوحة كانبان واحدة.
</p>
</div>
<div className="grid grid-cols-3 gap-3 text-center sm:max-w-xl">
<div className="rounded-2xl bg-white/10 p-4 backdrop-blur">
<div className="text-3xl font-black">{plans.length}</div>
<div className="text-xs text-emerald-100">خطط محفوظة</div>
</div>
<div className="rounded-2xl bg-white/10 p-4 backdrop-blur">
<div className="text-3xl font-black">{tasks.length}</div>
<div className="text-xs text-emerald-100">مهام حملة</div>
</div>
<div className="rounded-2xl bg-white/10 p-4 backdrop-blur">
<div className="text-3xl font-black">{formatBudget(totalVisibleBudget)}</div>
<div className="text-xs text-emerald-100">ميزانية معروضة</div>
</div>
</div>
</div>
<div className="rounded-[1.5rem] border border-white/15 bg-white/10 p-5 backdrop-blur">
<p className="mb-3 text-sm text-emerald-100">الخطة النشطة</p>
{selectedPlan ? (
<div className="space-y-4">
<h2 className="text-2xl font-bold">{selectedPlan.project_name}</h2>
<p className="leading-7 text-white/80">{selectedPlan.marketing_goal}</p>
<div className="flex flex-wrap gap-2">
<span className="rounded-full bg-emerald-400 px-3 py-1 text-sm font-semibold text-emerald-950">{statusLabels[selectedPlan.status]}</span>
<span className="rounded-full bg-white/15 px-3 py-1 text-sm text-white">{formatBudget(selectedPlan.total_budget)} ر.س</span>
</div>
</div>
) : (
<p className="leading-7 text-white/75">ابدأ بإنشاء أول خطة لتظهر تفاصيلها هنا.</p>
)}
</div>
</div>
</div>
<SectionTitleLineWithButton icon={icon.mdiChartTimelineVariant} title="مساحة التخطيط" main>
<BaseButton href="/marketing_plans/marketing_plans-list" label="إدارة CRUD" color="light" />
</SectionTitleLineWithButton>
{feedback && (
<div className={`rounded-2xl border p-4 text-sm ${feedback.type === 'success' ? 'border-emerald-200 bg-emerald-50 text-emerald-800' : 'border-rose-200 bg-rose-50 text-rose-800'}`}>
{feedback.text}
</div>
)}
<div className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<form onSubmit={submitPlan} className="space-y-5">
<div>
<p className="text-sm font-semibold text-emerald-600">١. أنشئ خطة جديدة</p>
<h2 className="mt-1 text-2xl font-black">بيانات المشروع التسويقي</h2>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold">اسم المشروع</span>
<input
className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:bg-dark-800"
value={form.project_name}
onChange={(event) => setForm({ ...form, project_name: event.target.value })}
placeholder="مثال: إطلاق متجر العطور"
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold">الهدف التسويقي</span>
<textarea
className="min-h-[92px] w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:bg-dark-800"
value={form.marketing_goal}
onChange={(event) => setForm({ ...form, marketing_goal: event.target.value })}
placeholder="زيادة المبيعات بنسبة 20% خلال 60 يوماً"
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold">الجمهور المستهدف</span>
<input
className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:bg-dark-800"
value={form.target_audience}
onChange={(event) => setForm({ ...form, target_audience: event.target.value })}
placeholder="رواد أعمال، أمهات، طلاب..."
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold">الميزانية الإجمالية</span>
<input
type="number"
min="1"
className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-100 dark:bg-dark-800"
value={form.total_budget}
onChange={(event) => setForm({ ...form, total_budget: event.target.value })}
placeholder="15000"
/>
</label>
<div>
<span className="mb-3 block text-sm font-semibold">القنوات المختارة</span>
<div className="grid grid-cols-2 gap-3">
{channels.map((channel) => {
const active = form.selected_channels.includes(channel.key)
return (
<button
key={channel.key}
type="button"
onClick={() => toggleChannel(channel.key)}
className={`rounded-2xl border px-4 py-3 text-right text-sm font-semibold transition ${active ? 'border-emerald-500 bg-emerald-50 text-emerald-800 shadow-lg shadow-emerald-100' : 'border-slate-200 bg-white text-slate-600 hover:border-emerald-300 dark:bg-dark-800'}`}
>
{channel.label}
</button>
)
})}
</div>
</div>
<BaseButton
type="submit"
label={saving ? 'جارٍ إنشاء الخطة...' : 'إنشاء الخطة والمهام'}
color="success"
className="w-full rounded-2xl py-3 text-base font-bold"
disabled={saving}
/>
</form>
</CardBox>
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5 flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold text-cyan-600">٢. الخطط المحفوظة</p>
<h2 className="mt-1 text-2xl font-black">جدول الحالة</h2>
</div>
{loading && <span className="rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-500">تحميل...</span>}
</div>
{plans.length ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-100 text-right text-sm">
<thead>
<tr className="text-slate-500">
<th className="px-3 py-3 font-semibold">المشروع</th>
<th className="px-3 py-3 font-semibold">الجمهور</th>
<th className="px-3 py-3 font-semibold">الميزانية</th>
<th className="px-3 py-3 font-semibold">الحالة</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{plans.map((plan) => (
<tr
key={plan.id}
className={`cursor-pointer transition hover:bg-emerald-50/60 ${selectedPlan?.id === plan.id ? 'bg-emerald-50/80' : ''}`}
onClick={() => setSelectedPlanId(plan.id)}
>
<td className="px-3 py-4 font-bold text-slate-900 dark:text-slate-100">{plan.project_name}</td>
<td className="max-w-[210px] truncate px-3 py-4 text-slate-500">{plan.target_audience}</td>
<td className="px-3 py-4 font-semibold">{formatBudget(plan.total_budget)} ر.س</td>
<td className="px-3 py-4">
<select
className="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs focus:border-emerald-500 focus:outline-none dark:bg-dark-800"
value={plan.status || 'draft'}
onChange={(event) => updatePlanStatus(plan, event.target.value as MarketingPlan['status'])}
onClick={(event) => event.stopPropagation()}
>
{Object.entries(statusLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="rounded-3xl border border-dashed border-slate-200 bg-slate-50 p-8 text-center text-slate-500">
لا توجد خطط بعد. املأ النموذج لإنشاء أول خطة تسويقية.
</div>
)}
</CardBox>
</div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5">
<p className="text-sm font-semibold text-orange-500">٣. لوحة الميزانية</p>
<h2 className="mt-1 text-2xl font-black">توزيع الميزانية حسب القناة</h2>
</div>
{visibleChannels.length ? (
<div className="space-y-4">
{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 (
<div key={channel.id}>
<div className="mb-2 flex items-center justify-between text-sm">
<span className="font-bold">{meta?.label || channel.channel}</span>
<span className="text-slate-500">{formatBudget(channel.allocated_budget)} ر.س · {percent}%</span>
</div>
<div className="h-3 overflow-hidden rounded-full bg-slate-100">
<div className="h-full rounded-full" style={{ width: `${percent}%`, backgroundColor: meta?.accent || '#10B981' }} />
</div>
{channel.objective && <p className="mt-2 text-xs leading-5 text-slate-500">{channel.objective}</p>}
</div>
)
})}
</div>
) : (
<div className="rounded-3xl border border-dashed border-slate-200 bg-slate-50 p-8 text-center text-slate-500">
اختر خطة تحتوي على قنوات لتظهر لوحة توزيع الميزانية.
</div>
)}
</CardBox>
<CardBox className="border-0 shadow-xl shadow-slate-200/70 dark:shadow-none">
<div className="mb-5">
<p className="text-sm font-semibold text-violet-600">٤. كانبان التنفيذ</p>
<h2 className="mt-1 text-2xl font-black">مهام الحملة</h2>
</div>
<div className="grid gap-4 lg:grid-cols-3">
{(Object.keys(stageLabels) as StageKey[]).map((stage) => (
<div key={stage} className="min-h-[260px] rounded-3xl bg-slate-50 p-4 dark:bg-dark-800">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-black">{stageLabels[stage]}</h3>
<span className="rounded-full bg-white px-3 py-1 text-xs text-slate-500 shadow-sm dark:bg-dark-900">
{visibleTasks.filter((task) => task.stage === stage).length}
</span>
</div>
<div className="space-y-3">
{visibleTasks.filter((task) => task.stage === stage).map((task) => {
const meta = channels.find((item) => item.key === task.channel)
return (
<div key={task.id} className="rounded-2xl border border-slate-100 bg-white p-4 shadow-sm dark:border-dark-700 dark:bg-dark-900">
<div className="mb-3 flex items-start justify-between gap-2">
<h4 className="font-bold leading-6">{task.title}</h4>
<span className="h-3 w-3 flex-none rounded-full" style={{ backgroundColor: meta?.accent || '#64748B' }} />
</div>
{task.description && <p className="mb-3 text-xs leading-5 text-slate-500">{task.description}</p>}
<select
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs focus:border-emerald-500 focus:outline-none dark:bg-dark-800"
value={task.stage}
onChange={(event) => moveTask(task, event.target.value as StageKey)}
>
{Object.entries(stageLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
)
})}
{!visibleTasks.filter((task) => task.stage === stage).length && (
<div className="rounded-2xl border border-dashed border-slate-200 p-5 text-center text-xs text-slate-400">
لا توجد مهام في هذا العمود.
</div>
)}
</div>
</div>
))}
</div>
</CardBox>
</div>
</div>
</SectionMain>
</>
)
}
MarketingStudio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_MARKETING_PLANS">{page}</LayoutAuthenticated>
}
export default MarketingStudio