from pathlib import Path
import sys

from docx import Document
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "docs" / "CRM_Complete_Project_Documentation_1405-05-22.docx"
SKILL_DIR = Path(
    r"C:\Users\AminSoroush\.gapcode\plugins\cache\gapgpt\documents\26.722.10000\skills\documents"
)
sys.path.insert(0, str(SKILL_DIR / "scripts"))
from fa_docx import set_font, set_rtl_paragraph, set_rtl_styles


NAVY = "12344D"
BLUE = "0277BD"
LIGHT_BLUE = "E1F5FE"
PALE_BLUE = "F4FAFD"
TEAL = "00796B"
LIGHT_TEAL = "E0F2F1"
GOLD = "F59E0B"
LIGHT_GOLD = "FFF8E1"
RED = "B71C1C"
LIGHT_RED = "FFEBEE"
GRAY = "607D8B"
LIGHT_GRAY = "F5F7F9"
INK = "263238"
WHITE = "FFFFFF"


def shade(cell, fill):
    tc_pr = cell._tc.get_or_add_tcPr()
    node = tc_pr.find(qn("w:shd"))
    if node is None:
        node = OxmlElement("w:shd")
        tc_pr.append(node)
    node.set(qn("w:fill"), fill)


def set_cell_margins(cell, top=90, start=120, bottom=90, end=120):
    tc_pr = cell._tc.get_or_add_tcPr()
    tc_mar = tc_pr.first_child_found_in("w:tcMar")
    if tc_mar is None:
        tc_mar = OxmlElement("w:tcMar")
        tc_pr.append(tc_mar)
    for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
        node = tc_mar.find(qn(f"w:{name}"))
        if node is None:
            node = OxmlElement(f"w:{name}")
            tc_mar.append(node)
        node.set(qn("w:w"), str(value))
        node.set(qn("w:type"), "dxa")


def set_table_geometry(table, widths, indent=120):
    table.autofit = False
    tbl_pr = table._tbl.tblPr
    tbl_w = tbl_pr.find(qn("w:tblW"))
    if tbl_w is None:
        tbl_w = OxmlElement("w:tblW")
        tbl_pr.append(tbl_w)
    tbl_w.set(qn("w:w"), str(sum(widths)))
    tbl_w.set(qn("w:type"), "dxa")

    tbl_ind = tbl_pr.find(qn("w:tblInd"))
    if tbl_ind is None:
        tbl_ind = OxmlElement("w:tblInd")
        tbl_pr.append(tbl_ind)
    tbl_ind.set(qn("w:w"), str(indent))
    tbl_ind.set(qn("w:type"), "dxa")

    grid = table._tbl.tblGrid
    for child in list(grid):
        grid.remove(child)
    for width in widths:
        col = OxmlElement("w:gridCol")
        col.set(qn("w:w"), str(width))
        grid.append(col)

    for row in table.rows:
        for cell, width in zip(row.cells, widths):
            tc_pr = cell._tc.get_or_add_tcPr()
            tc_w = tc_pr.find(qn("w:tcW"))
            if tc_w is None:
                tc_w = OxmlElement("w:tcW")
                tc_pr.append(tc_w)
            tc_w.set(qn("w:w"), str(width))
            tc_w.set(qn("w:type"), "dxa")
            set_cell_margins(cell)
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER


def mark_header_row(row):
    tr_pr = row._tr.get_or_add_trPr()
    node = tr_pr.find(qn("w:tblHeader"))
    if node is None:
        node = OxmlElement("w:tblHeader")
        tr_pr.append(node)
    node.set(qn("w:val"), "true")


def set_spacing(paragraph, before=0, after=6, line=1.18):
    fmt = paragraph.paragraph_format
    fmt.space_before = Pt(before)
    fmt.space_after = Pt(after)
    fmt.line_spacing = line


def add_run_text(paragraph, text, size=10.3, bold=False, color=INK, italic=False):
    run = paragraph.add_run(text)
    run.bold = bold
    run.italic = italic
    set_font(run, "Vazirmatn", size)
    run.font.color.rgb = RGBColor.from_string(color)
    return run


def add_para(doc, text, size=10.3, bold=False, color=INK, align=WD_ALIGN_PARAGRAPH.RIGHT, after=6, before=0):
    p = doc.add_paragraph()
    p.alignment = align
    add_run_text(p, text, size=size, bold=bold, color=color)
    set_rtl_paragraph(p, font="Vazirmatn", size=size)
    set_spacing(p, before=before, after=after)
    return p


def add_bullet(doc, text, level=0, color=INK):
    p = doc.add_paragraph(style="List Bullet")
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    p.paragraph_format.left_indent = Inches(0.25 + level * 0.18)
    p.paragraph_format.right_indent = Inches(0.10)
    p.paragraph_format.first_line_indent = Inches(-0.16)
    add_run_text(p, text, size=10.1, color=color)
    set_rtl_paragraph(p, font="Vazirmatn", size=10.1)
    set_spacing(p, after=3)
    return p


def add_numbered(doc, text):
    p = doc.add_paragraph(style="List Number")
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    p.paragraph_format.left_indent = Inches(0.38)
    p.paragraph_format.right_indent = Inches(0.10)
    p.paragraph_format.first_line_indent = Inches(-0.22)
    add_run_text(p, text, size=10.1)
    set_rtl_paragraph(p, font="Vazirmatn", size=10.1)
    set_spacing(p, after=4)
    return p


def add_heading(doc, text, level=1):
    p = doc.add_paragraph(style=f"Heading {level}")
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    color = BLUE if level in (1, 2) else NAVY
    size = {1: 16, 2: 13, 3: 11.5}[level]
    add_run_text(p, text, size=size, bold=True, color=color)
    set_rtl_paragraph(p, font="Vazirmatn", size=size)
    set_spacing(p, before={1: 16, 2: 11, 3: 7}[level], after={1: 7, 2: 5, 3: 3}[level])
    return p


def add_callout(doc, text, fill=LIGHT_BLUE, color=NAVY, label=None):
    table = doc.add_table(rows=1, cols=1)
    table.style = "Table Grid"
    set_table_geometry(table, [9360])
    cell = table.cell(0, 0)
    shade(cell, fill)
    cell.text = ""
    p = cell.paragraphs[0]
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    if label:
        add_run_text(p, f"{label}: ", size=10.3, bold=True, color=color)
    add_run_text(p, text, size=10.3, color=color)
    set_rtl_paragraph(p, font="Vazirmatn", size=10.3)
    set_spacing(p, before=0, after=0, line=1.18)
    set_cell_margins(cell, top=160, bottom=160)
    doc.add_paragraph()


def add_table(doc, headers, rows, widths, header_fill=BLUE, font_size=8.9, striped=True):
    table = doc.add_table(rows=1, cols=len(headers))
    table.style = "Table Grid"
    set_table_geometry(table, widths)
    mark_header_row(table.rows[0])

    for idx, value in enumerate(headers):
        cell = table.rows[0].cells[idx]
        cell.text = ""
        shade(cell, header_fill)
        p = cell.paragraphs[0]
        p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
        add_run_text(p, str(value), size=font_size, bold=True, color=WHITE)
        set_rtl_paragraph(p, font="Vazirmatn", size=font_size)
        set_spacing(p, after=0, line=1.08)

    for row_idx, row in enumerate(rows):
        cells = table.add_row().cells
        for idx, value in enumerate(row):
            cell = cells[idx]
            cell.text = ""
            if striped and row_idx % 2 == 0:
                shade(cell, LIGHT_GRAY)
            p = cell.paragraphs[0]
            p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
            add_run_text(p, str(value), size=font_size - 0.2)
            set_rtl_paragraph(p, font="Vazirmatn", size=font_size - 0.2)
            set_spacing(p, after=1, line=1.08)
    for row in table.rows:
        for cell in row.cells:
            set_cell_margins(cell)
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
    doc.add_paragraph()
    return table


def add_code_block(doc, text):
    table = doc.add_table(rows=1, cols=1)
    table.style = "Table Grid"
    set_table_geometry(table, [9360])
    cell = table.cell(0, 0)
    shade(cell, "F1F3F5")
    cell.text = ""
    p = cell.paragraphs[0]
    p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    p.paragraph_format.left_indent = Inches(0.08)
    r = p.add_run(text)
    r.font.name = "Consolas"
    r._element.rPr.rFonts.set(qn("w:ascii"), "Consolas")
    r._element.rPr.rFonts.set(qn("w:hAnsi"), "Consolas")
    r.font.size = Pt(8.5)
    r.font.color.rgb = RGBColor.from_string(INK)
    set_spacing(p, after=0, line=1.05)
    doc.add_paragraph()


def configure_document(doc):
    set_rtl_styles(doc, font="Vazirmatn")
    normal = doc.styles["Normal"]
    normal.font.name = "Vazirmatn"
    normal.font.size = Pt(10.3)
    normal.paragraph_format.space_after = Pt(6)
    normal.paragraph_format.line_spacing = 1.18
    for name, size, color in (
        ("Heading 1", 16, BLUE),
        ("Heading 2", 13, BLUE),
        ("Heading 3", 11.5, NAVY),
    ):
        style = doc.styles[name]
        style.font.name = "Vazirmatn"
        style.font.size = Pt(size)
        style.font.bold = True
        style.font.color.rgb = RGBColor.from_string(color)
        style.paragraph_format.keep_with_next = True

    section = doc.sections[0]
    section.page_width = Inches(8.5)
    section.page_height = Inches(11)
    section.top_margin = Inches(0.72)
    section.bottom_margin = Inches(0.70)
    section.left_margin = Inches(0.78)
    section.right_margin = Inches(0.78)
    section.header_distance = Inches(0.32)
    section.footer_distance = Inches(0.32)

    hp = section.header.paragraphs[0]
    hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    add_run_text(hp, "راهنمای جامع فنی و تست CRM آتش فراز", size=8.2, color=GRAY)
    set_rtl_paragraph(hp, font="Vazirmatn", size=8.2)
    set_spacing(hp, after=0)

    fp = section.footer.paragraphs[0]
    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
    add_run_text(fp, "نسخه بررسی داخلی | وضعیت پروژه تا ۲۲ مرداد ۱۴۰۵", size=8, color=GRAY)
    set_rtl_paragraph(fp, font="Vazirmatn", size=8)
    set_spacing(fp, after=0)


def title_page(doc):
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    set_spacing(p, before=40, after=5)
    add_run_text(p, "مستند جامع پروژه CRM آتش فراز", size=26, bold=True, color=NAVY)
    set_rtl_paragraph(p, font="Vazirmatn", size=26)

    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    add_run_text(p, "معماری، دیتابیس، پنل مدیریت، پنل بازاریاب و برنامه تست", size=14, color=BLUE)
    set_rtl_paragraph(p, font="Vazirmatn", size=14)
    set_spacing(p, after=20)

    add_callout(
        doc,
        "این سند بر اساس مطالعه فایل‌های PHP، SQL، قالب‌های مشترک، اسکریپت‌های اصلی پنل بازاریاب و ساختار دارایی‌های پروژه تهیه شده است. هدف آن این است که یک فرد فنی یا تستر بتواند بدون حدس‌زدن، سامانه را راه‌اندازی، بررسی و سناریوهای اصلی را اجرا کند.",
        fill=LIGHT_BLUE,
        color=NAVY,
        label="هدف سند",
    )

    add_table(
        doc,
        ["مشخصه", "مقدار"],
        [
            ("نام سامانه", "سامانه اختصاصی بازرگانی آتش فراز"),
            ("نوع پروژه", "CRM و مدیریت مشتری، بازاریاب و توزیع"),
            ("فناوری اصلی", "PHP سنتی، PDO، MySQL، HTML/CSS/JavaScript"),
            ("زبان رابط", "فارسی، راست‌به‌چپ، تاریخ شمسی در نمایش"),
            ("پایگاه داده پیش‌فرض", "syspakhshdb"),
            ("نشانی محلی پیشنهادی", "http://atashfaraz.localhost"),
            ("نسخه درج‌شده در تنظیمات", "0.1"),
            ("تاریخ بررسی", "۲۲ مرداد ۱۴۰۵ / ۱۳ اوت ۲۰۲۶"),
        ],
        [2500, 6860],
        header_fill=TEAL,
    )

    add_callout(
        doc,
        "در متن سند، عبارت «فعال» یعنی در کد مسیر و پردازش متناظر وجود دارد؛ عبارت «نیازمند بررسی» یعنی از روی ارجاع، ساختار یا رفتار فعلی احتمال نقص، ناسازگاری یا ریسک وجود دارد و باید در تست تأیید شود.",
        fill=LIGHT_GOLD,
        color="6D4C00",
        label="راهنمای خواندن",
    )
    doc.add_page_break()


def toc(doc):
    add_heading(doc, "فهرست فصل‌ها", 1)
    chapters = [
        "۱. خلاصه اجرایی و دامنه بررسی",
        "۲. معماری و فناوری پروژه",
        "۳. ساختار فایل‌ها و قراردادهای مشترک",
        "۴. معماری احراز هویت و دسترسی",
        "۵. مدل کامل دیتابیس و روابط",
        "۶. پنل مدیریت",
        "۷. پنل بازاریاب",
        "۸. جریان‌های کاری و قواعد کسب‌وکار",
        "۹. راهنمای راه‌اندازی و داده‌های تست",
        "۱۰. برنامه تست جامع",
        "۱۱. بررسی‌های امنیتی و کیفیتی",
        "۱۲. نواقص، ریسک‌ها و موارد نیازمند تصمیم",
        "۱۳. نقشه مسیرها و فایل‌ها",
        "۱۴. جمع‌بندی عملیاتی",
    ]
    for chapter in chapters:
        add_bullet(doc, chapter)
    add_callout(
        doc,
        "برای تست بهتر، فصل ۹ را ابتدا اجرا کنید، سپس فصل‌های ۶ و ۷ را به‌ترتیب بخوانید و در پایان فصل ۱۰ را به‌صورت چک‌لیست اجرا کنید.",
        fill=PALE_BLUE,
        color=BLUE,
        label="ترتیب پیشنهادی مطالعه",
    )
    doc.add_page_break()


def chapter_scope(doc):
    add_heading(doc, "۱. خلاصه اجرایی و دامنه بررسی", 1)
    add_para(
        doc,
        "پروژه یک CRM سبک و اختصاصی برای مدیریت مشتریان، بازاریاب‌ها و بخشی از عملیات توزیع است. کد بدون فریم‌ورک نوشته شده و هر صفحه PHP بخشی از منطق دریافت داده، اعتبارسنجی، واکشی از دیتابیس و نمایش را هم‌زمان انجام می‌دهد. این روش برای پروژه کوچک و کنترل‌شده قابل استفاده است، اما باعث می‌شود مرز بین لایه نمایش، منطق کسب‌وکار و دسترسی به دیتابیس در برخی فایل‌ها کم‌رنگ باشد."
    )
    add_table(
        doc,
        ["حوزه بررسی", "وضعیت پوشش", "نتیجه"],
        [
            ("PHP و منطق سمت سرور", "بررسی فایل‌های اجرایی پروژه", "مسیرهای login، CRUD، API داخلی و پردازش‌های اصلی مستند شده‌اند."),
            ("SQL و دیتابیس", "بررسی 001_initial_schema، 002_required_data و 003_test_data", "جداول، کلیدها، وضعیت‌ها، داده‌های پایه و داده تست استخراج شده‌اند."),
            ("پنل مدیریت", "بررسی ماژول‌های مشتری، کاربران، نقش‌ها، مکان، ناوگان، زون و لاگ", "مسیرهای صفحه، دسترسی و رفتار عملیاتی ثبت شده است."),
            ("پنل بازاریاب", "بررسی تمام فایل‌های marketer و اسکریپت مشترک", "احراز هویت مستقل، مالکیت مشتری و پیگیری‌ها شرح داده شده است."),
            ("قالب و دارایی‌های رابط", "بررسی قالب‌های مشترک و CSS/JS اختصاصی", "ساختار layout، RTL، تاریخ شمسی و AJAX شرح داده شده است."),
            ("فایل‌های vendor", "فهرست‌برداری و بررسی وابستگی‌های مورد استفاده", "جزئیات همه پلاگین‌های غیرمرتبط با منطق کسب‌وکار در این سند تکرار نشده است."),
        ],
        [2350, 2700, 4310],
        header_fill=BLUE,
    )
    add_heading(doc, "۱.۱. نتیجه کلی", 2)
    for text in [
        "مرکز داده مشتری جدول customers است و برای پنل بازاریاب جدول موازی وجود ندارد.",
        "بازاریاب با visitor_id در customers شناخته می‌شود و تاریخچه تغییر مالک در customer_assignments ثبت می‌گردد.",
        "چرخه CRM از customer_statuses، customer_status_history، customer_activities و follow_ups تشکیل شده است.",
        "پنل مدیریت و پنل بازاریاب از یک users، roles و permissions مشترک استفاده می‌کنند، اما sessionهای جدا دارند.",
        "بخش ناوگان از vehicles، تخصیص راننده به ماشین و تخصیص ماشین به زون استفاده می‌کند.",
        "زیرساخت برای ثبت سفارش و حسابداری کامل هنوز در مخزن حاضر نیست؛ accounting_id فقط به‌عنوان شناسه نگهداری می‌شود.",
    ]:
        add_bullet(doc, text)


def chapter_architecture(doc):
    add_heading(doc, "۲. معماری و فناوری پروژه", 1)
    add_heading(doc, "۲.۱. پشته فنی", 2)
    add_table(
        doc,
        ["لایه", "فناوری/فایل", "نقش"],
        [
            ("وب‌سرور", "Apache / VirtualHost", "vhost.txt دامنه محلی atashfaraz.localhost را به ریشه پروژه وصل می‌کند."),
            ("Backend", "PHP سنتی", "صفحات وب، endpointهای POST و خروجی‌های JSON."),
            ("Database", "MySQL / MariaDB با PDO", "اتصال به syspakhshdb با utf8mb4 و prepared statement."),
            ("Frontend مدیریت", "Bootstrap RTL، jQuery، DataTables، SweetAlert، MetisMenu", "پوسته دسکتاپ و جدول‌های مدیریتی."),
            ("Frontend بازاریاب", "CSS اختصاصی marketer-panel.css و JS marketer-panel.js", "رابط موبایل‌محور و فرم‌های AJAX."),
            ("تقویم", "JDF و Persian Datepicker", "نمایش شمسی و تبدیل تاریخ شمسی/میلادی."),
            ("فونت", "IranSans، Estedad، Yekan و فونت‌های پروژه", "نمایش فارسی در پوسته‌های مختلف."),
            ("فایل و تصویر", "uploads/shop_images", "ذخیره تصویر فروشگاه مشتری با نام تصادفی."),
        ],
        [1800, 2800, 4760],
    )
    add_heading(doc, "۲.۲. الگوی اجرای درخواست", 2)
    add_numbered(doc, "کاربر یک URL صفحه یا endpoint را فراخوانی می‌کند.")
    add_numbered(doc, "صفحه session را آغاز می‌کند و فایل‌های تنظیمات، اتصال دیتابیس و توابع مشترک را وارد می‌کند.")
    add_numbered(doc, "در صفحات مدیریت، دسترسی با hasPermission یا requirePermission کنترل می‌شود.")
    add_numbered(doc, "در پنل بازاریاب، _bootstrap.php نشست ATASH_MARKETER_SESSION، helperها، CSRF و layout را آماده می‌کند.")
    add_numbered(doc, "داده از MySQL با PDO خوانده یا در تراکنش نوشته می‌شود.")
    add_numbered(doc, "صفحات مدیریت معمولاً با redirect و query string پیام می‌دهند؛ پنل بازاریاب endpointهای عملیاتی را JSON پاسخ می‌دهد.")
    add_numbered(doc, "تاریخ‌ها در دیتابیس میلادی هستند و هنگام نمایش به شمسی تبدیل می‌شوند.")
    add_code_block(
        doc,
        "Browser\n  ├─ /pages/login.php          → session اصلی مدیریت\n  ├─ /modules/...               → صفحات پنل مدیریت\n  └─ /marketer/login.php        → session مستقل بازاریاب\n                                      ↓\n                              core/function.php\n                                      ↓\n                                PDO / MySQL\n                                      ↓\n                    customers + CRM + users + fleet + logs",
    )
    add_heading(doc, "۲.۳. تنظیمات اتصال", 2)
    add_table(
        doc,
        ["پارامتر", "مقدار فعلی در کد", "ملاحظات تست/استقرار"],
        [
            ("Host", "localhost", "برای سرور واقعی باید از تنظیمات محیطی استفاده شود."),
            ("Database", "syspakhshdb", "در ADMIN/sql.php نیز همین نام استفاده شده است."),
            ("Username", "root", "برای تولید ناامن است؛ کاربر محدود دیتابیس ایجاد شود."),
            ("Password", "خالی", "ریسک جدی در محیط غیرمحلی."),
            ("Charset", "utf8mb4", "برای فارسی مناسب است."),
            ("Timezone", "Asia/Tehran", "در برخی فایل‌ها به‌صورت ASIA/TEHRAN نیز نوشته شده؛ یکسان‌سازی توصیه می‌شود."),
        ],
        [2200, 3000, 4160],
        header_fill=TEAL,
    )


def chapter_structure(doc):
    add_heading(doc, "۳. ساختار فایل‌ها و قراردادهای مشترک", 1)
    add_table(
        doc,
        ["مسیر", "محتوا", "نقطه ورود/نقش"],
        [
            ("app_config/", "db_settings.php و app_settings.php", "اتصال PDO و متغیرهای عمومی سامانه."),
            ("core/", "function.php", "توابع مشترک، permission، لاگ، تبدیل تاریخ و helperهای lookup."),
            ("templates/", "layout_start/end، navbar، sidebar، header، footer", "پوسته پنل مدیریت."),
            ("pages/", "login.php و logout.php", "احراز هویت و خروج پنل مدیریت."),
            ("modules/customers/", "CRUD مشتری، پروفایل، پیگیری و تاریخچه بازاریاب", "هسته CRM."),
            ("modules/customerscat/", "CRUD دسته‌بندی مشتری", "مدیریت گروه‌های مشتری."),
            ("modules/users/", "CRUD کاربر و پروفایل", "مدیریت کاربران، راننده و بازاریاب."),
            ("modules/roles/", "CRUD نقش و دسترسی", "مدیریت RBAC."),
            ("modules/locations/", "CRUD استان و شهر", "داده‌های جغرافیایی."),
            ("modules/vehicles/", "CRUD ماشین، تخصیص راننده و تاریخچه", "ناوگان پخش."),
            ("modules/zones/", "CRUD زون، تخصیص ماشین و مشتریان زون", "مسیر/ناحیه توزیع."),
            ("modules/logs/", "صفحه و API DataTables", "حسابرسی رخدادها."),
            ("modules/adminsetting/", "تغییر رمز پنل مدیریت", "امنیت حساب کاربری."),
            ("marketer/", "پنل موبایل بازاریاب", "مشتریان خود، ثبت مشتری و پیگیری."),
            ("ADMIN/sql/", "schema، داده پایه و داده تست", "نصب دیتابیس."),
            ("ADMIN/sql.php", "اجرای فایل SQL و حذف جدول‌ها", "ابزار نصب/ریست؛ حساس و نیازمند حفاظت."),
            ("assets/", "CSS، JS، فونت، پلاگین و تصاویر", "وابستگی‌های frontend."),
            ("uploads/shop_images/", "تصاویر آپلودی فروشگاه", "فایل‌های runtime؛ باید backup و محدودسازی شوند."),
        ],
        [2100, 3100, 4160],
    )
    add_heading(doc, "۳.۱. قرارداد layout پنل مدیریت", 2)
    for text in [
        "هر صفحه معمولاً متغیرهای custom_css و custom_js را تعریف می‌کند.",
        "متغیرهای menuname و pagename برای active شدن منوی sidebar استفاده می‌شوند.",
        "layout_start.php session را بررسی و header، navbar و sidebar را باز می‌کند.",
        "layout_end.php footer، modal جست‌وجوی مشتری و footer_js را وارد می‌کند.",
        "برخی فایل‌ها به $_SERVER['DOCUMENT_ROOT'] متکی هستند؛ اجرای پروژه در subdirectory بدون VirtualHost ممکن است مسیرها را خراب کند.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۳.۲. قرارداد پنل بازاریاب", 2)
    add_table(
        doc,
        ["فایل/تابع", "رفتار"],
        [
            ("marketer/_bootstrap.php", "session جدا، اتصال، escape، JSON، login/permission، CSRF، تاریخ و layout."),
            ("marketer_page_start/end", "قالب مشترک، topbar، تاریخ شمسی، bottom navigation و بارگذاری JS."),
            ("marketer_json", "پاسخ JSON با UTF-8 و status code."),
            ("marketer_check_csrf", "بررسی hash_equals برای فرم‌های تغییردهنده."),
            ("marketer_refresh_next_followup", "کمترین due_at پیگیری pending را در customers.next_follow_up_at ذخیره می‌کند."),
            ("assets/js/marketer-panel.js", "فرم‌های data-marketer-ajax-form را با fetch ارسال و redirect/alert را مدیریت می‌کند."),
        ],
        [3300, 6060],
        header_fill=TEAL,
    )


def chapter_auth(doc):
    add_heading(doc, "۴. معماری احراز هویت و دسترسی", 1)
    add_heading(doc, "۴.۱. ورود پنل مدیریت", 2)
    add_numbered(doc, "pages/login.php نام کاربری را در users جست‌وجو می‌کند.")
    add_numbered(doc, "password_verify رمز bcrypt را بررسی می‌کند.")
    add_numbered(doc, "کاربر باید status برابر active داشته باشد؛ pending، blocked و inactive پیام متفاوت دارند.")
    add_numbered(doc, "session_regenerate_id(true) اجرا می‌شود.")
    add_numbered(doc, "نقش‌ها از user_roles و permissions از user_roles → role_permissions → permissions خوانده می‌شوند.")
    add_numbered(doc, "در session کلیدهای user_id، roles، role_slugs، permissions و permission_slugs ذخیره می‌شوند.")
    add_numbered(doc, "last_login_at به‌روزرسانی و رخداد ورود در logs ثبت می‌شود.")
    add_callout(
        doc,
        "در کد فعلی ورود اصلی صرفاً به active بودن کاربر و صحت رمز متکی است و در خود login.php مجوز panel.login را enforce نمی‌کند. تست باید مشخص کند آیا این رفتار عمدی است یا باید اصلاح شود.",
        fill=LIGHT_RED,
        color=RED,
        label="یافته مهم",
    )
    add_heading(doc, "۴.۲. ورود پنل بازاریاب", 2)
    add_numbered(doc, "marketer/login.php فقط کاربری را می‌پذیرد که role با slug=visitor داشته باشد.")
    add_numbered(doc, "همان کاربر باید permission با slug=marketer.panel.login داشته باشد.")
    add_numbered(doc, "status کاربر باید active باشد.")
    add_numbered(doc, "session با نام ATASH_MARKETER_SESSION ساخته و permission slugها در marketer_permission_slugs ذخیره می‌شوند.")
    add_numbered(doc, "CSRF token مستقل برای session بازاریاب ساخته می‌شود.")
    add_heading(doc, "۴.۳. نقش‌ها و دسترسی‌های پایه", 2)
    add_table(
        doc,
        ["نقش", "slug", "وضعیت پیش‌فرض در SQL", "کاربرد"],
        [
            ("مدیر سیستم", "superadmin", "تمام دسترسی‌های پایه و مدیریتی", "مدیریت کامل سامانه."),
            ("کارشناس سیستم", "system_expert", "رکورد نقش وجود دارد؛ دسترسی‌ها باید بررسی/تخصیص داده شوند.", "مدیریت عملیاتی CRM در نظر گرفته شده."),
            ("ناظر", "supervisor", "رکورد نقش وجود دارد؛ دسترسی پایه کامل در seed نیست.", "نقش سازمانی پیشنهادی."),
            ("حسابدار", "accountant", "رکورد نقش وجود دارد؛ دسترسی‌ها باید تنظیم شوند.", "نقش سازمانی پیشنهادی."),
            ("کمک حسابدار", "assistant_accountant", "رکورد نقش وجود دارد؛ دسترسی‌ها باید تنظیم شوند.", "نقش سازمانی پیشنهادی."),
            ("انباردار", "warehouse_keeper", "رکورد نقش وجود دارد؛ دسترسی‌ها باید تنظیم شوند.", "نقش سازمانی پیشنهادی."),
            ("بازاریاب", "visitor", "permissionهای پنل بازاریاب", "مشاهده مشتری خود، ثبت مشتری و پیگیری."),
            ("راننده", "driver", "رکورد نقش؛ دسترسی‌های صفحه‌ای باید تنظیم شوند.", "انتساب به ماشین و عملیات توزیع."),
        ],
        [1900, 1900, 2550, 3010],
    )
    add_heading(doc, "۴.۴. مدل RBAC", 2)
    add_code_block(
        doc,
        "users ──< user_roles >── roles ──< role_permissions >── permissions\n\n"
        "session اصلی:\n"
        "  user_id + role_slugs + permission_slugs\n\n"
        "session بازاریاب:\n"
        "  marketer_user_id + marketer_permission_slugs + marketer_csrf",
    )


def chapter_database(doc):
    add_heading(doc, "۵. مدل کامل دیتابیس و روابط", 1)
    add_callout(
        doc,
        "ترتیب نصب رسمی فایل‌ها: 001_initial_schema.sql سپس 002_required_data.sql و در صورت نیاز 003_test_data.sql. اجرای دوباره 001 روی دیتابیس موجود به‌دلیل ALTERهای بدون شرط می‌تواند خطا بدهد؛ برای نصب تکراری، دیتابیس را پاک یا migration را idempotent کنید.",
        fill=LIGHT_GOLD,
        color="6D4C00",
        label="ترتیب نصب",
    )
    add_heading(doc, "۵.۱. نقشه روابط", 2)
    add_code_block(
        doc,
        "provinces_list 1 ──< provinces_cities\n"
        "customers_cat 1 ──< customers\n"
        "zones 1 ──< customers\n"
        "users 1 ──< customers (created_by / visitor_id)\n"
        "users N ──N roles        via user_roles\n"
        "roles N ──N permissions   via role_permissions\n"
        "vehicles N ──N users      via vehicle_driver_assignments (history)\n"
        "zones N ──N vehicles      via zone_vehicle_assignments (history)\n"
        "zones N ──N users         via zone_drivers (legacy/direct driver history)\n"
        "customers 1 ──< customer_assignments\n"
        "customers 1 ──< customer_status_history\n"
        "customers 1 ──< customer_activities\n"
        "customers 1 ──< follow_ups\n"
        "users 1 ──< follow_ups (assigned_to / created_by / completed_by)\n"
        "users 1 ──< logs (nullable)",
    )
    add_heading(doc, "۵.۲. جدول‌های جغرافیا و دسته‌بندی", 2)
    add_table(
        doc,
        ["جدول", "ستون‌های مهم", "کلید/قید", "رفتار حذف"],
        [
            ("provinces_list", "id، name، ordered", "PK روی id", "حذف استان، شهرهای وابسته را CASCADE می‌کند؛ UI قبل از حذف وجود شهر/مشتری را می‌سنجد."),
            ("provinces_cities", "id، prov_id، name، ordered", "FK prov_id → provinces_list.id", "با حذف استان حذف می‌شود."),
            ("customers_cat", "id، name، descri، status، soft_deleted", "PK؛ soft delete مفهومی", "به‌دلیل FK مشتری، دسته دارای مشتری حذف نمی‌شود."),
        ],
        [2100, 2850, 2300, 2110],
    )
    add_heading(doc, "۵.۳. کاربران و RBAC", 2)
    add_table(
        doc,
        ["جدول", "ستون‌های مهم", "روابط"],
        [
            ("users", "username یکتا، password، full_name، mobile یکتا، status، last_login_at", "مرجع نقش، مشتری، راننده، لاگ و پیگیری."),
            ("roles", "name یکتا، slug یکتا، description", "N به N با users و permissions."),
            ("permissions", "name، slug یکتا، description", "از طریق role_permissions به نقش متصل می‌شود."),
            ("user_roles", "user_id، role_id", "uq_user_role؛ حذف کاربر/نقش CASCADE."),
            ("role_permissions", "role_id، permission_id", "uq_role_permission؛ حذف نقش/مجوز CASCADE."),
            ("logs", "user_id nullable، log_type، log_event، ip_address، created_at", "user_id با ON DELETE SET NULL."),
        ],
        [1900, 3150, 4310],
    )
    add_heading(doc, "۵.۴. ناوگان و توزیع", 2)
    add_table(
        doc,
        ["جدول", "فیلدهای مهم", "قواعد عملیاتی"],
        [
            ("zones", "name، area_name، description، status", "نام در process یکتا بررسی می‌شود؛ status عددی ۰/۱."),
            ("vehicles", "vehicle_code یکتا، plate_number یکتا، vehicle_type، ownership_type، status", "وضعیت active/inactive/maintenance."),
            ("vehicle_driver_assignments", "vehicle_id، driver_id، assigned_from، assigned_to، status", "فقط یک تخصیص active فعلی از طریق process نگهداری می‌شود."),
            ("zone_vehicle_assignments", "zone_id، vehicle_id، assigned_from، assigned_to، status", "یک ماشین نمی‌تواند هم‌زمان در دو زون فعال باشد."),
            ("zone_drivers", "zone_id، driver_id، is_primary، status، assigned_from", "جدول قدیمی/سازگاری؛ UI جدید بیشتر از تخصیص ماشین استفاده می‌کند."),
        ],
        [2400, 3200, 3760],
    )
    add_heading(doc, "۵.۵. جدول customers", 2)
    add_table(
        doc,
        ["دسته اطلاعات", "ستون‌ها", "قاعده/توضیح"],
        [
            ("هویتی", "shop_name، owner_name، national_code، economic_code، birthday، shop_image", "owner_name و mobile در منطق برنامه ضروری هستند؛ national_code الگوریتمی بررسی می‌شود."),
            ("تماس", "mobile، phone، alternative_mobile، phone_verified_at، password", "mobile اصلی یکتا و با 09 + 9 رقم اعتبارسنجی می‌شود."),
            ("مکان", "province_id، city_id، address، postal_code، latitude، longitude", "city باید با province انتخاب‌شده هم‌خوان باشد."),
            ("دسته و وضعیت", "customer_category_id، status، profile_status، lifecycle_status_id", "status وضعیت دسترسی است؛ lifecycle_status وضعیت CRM است."),
            ("مالکیت CRM", "visitor_id، created_by، lead_source، first_contact_at، first_purchase_at، last_activity_at، next_follow_up_at، lost_at، lost_reason", "visitor_id مالک فعلی و customer_assignments تاریخچه است."),
            ("فروش و توزیع", "zone_id، visit_days، delivery_note، payment_type، credit_limit، settlement_days، customer_grade", "در پنل بازاریاب فقط بخشی از این فیلدها قابل ثبت است."),
            ("تجهیزات/یادداشت", "has_company_equipment، equipment_note، notes", "بخش توزیع/اطلاعات تکمیلی."),
        ],
        [1900, 3650, 3810],
    )
    add_heading(doc, "۵.۶. وضعیت‌های CRM", 2)
    add_table(
        doc,
        ["slug", "عنوان", "رنگ seed", "رفتار فعلی"],
        [
            ("lead", "سرنخ", "secondary", "برای توسعه چرخه اولیه."),
            ("potential", "مشتری بالقوه", "info", "وضعیت پیش‌فرض ثبت مشتری از هر دو پنل."),
            ("customer", "مشتری واقعی", "success", "هنگام انتخاب، first_purchase_at در ویرایش پر می‌شود."),
            ("inactive", "کم‌فعال / نیازمند پیگیری", "warning", "فعلاً تغییر دستی؛ job خودکار وجود ندارد."),
            ("lost", "از دست رفته", "danger", "دلیل از دست رفتن اجباری و lost_at ثبت می‌شود."),
        ],
        [1800, 2900, 1800, 2860],
    )
    add_heading(doc, "۵.۷. فعالیت و پیگیری", 2)
    add_table(
        doc,
        ["جدول", "فیلدهای مهم", "چرخه"],
        [
            ("customer_status_history", "old_status_id، new_status_id، changed_by، reason، created_at", "با ثبت مشتری یک رکورد اولیه و با تغییر lifecycle یک رکورد جدید."),
            ("customer_activities", "performed_by، activity_type، activity_at، result، note", "ثبت ویزیت اولیه و فعالیت حاصل از complete پیگیری."),
            ("follow_ups", "assigned_to، due_at، follow_up_type، status، result، note، completed_at، completed_by، created_by", "pending → done یا cancelled در پنل مدیریت؛ پنل بازاریاب فقط pending → done."),
        ],
        [2500, 3500, 3560],
    )
    add_heading(doc, "۵.۸. قواعد حذف و یکپارچگی", 2)
    for text in [
        "customers_cat دارای مشتری قابل حذف نیست؛ FK نیز ON DELETE RESTRICT است.",
        "حذف استان در سطح DB می‌تواند شهرها را cascade کند، اما process برنامه وجود شهر/مشتری را بررسی و حذف را منع می‌کند.",
        "حذف کاربر از نظر دیتابیس روی برخی روابط CASCADE و روی برخی RESTRICT یا SET NULL است؛ قبل از حذف کاربر اثر آن بررسی شود.",
        "حذف مشتری، assignment، status history، activity و followupهای وابسته را به‌صورت cascade حذف می‌کند؛ در محیط واقعی بهتر است حذف سخت محدود شود.",
        "تاریخچه تخصیص‌ها با پایان دادن رکورد قبلی و درج رکورد جدید حفظ می‌شود؛ قید یکتایی فعال در سطح دیتابیس وجود ندارد و process باید مرجع باشد.",
    ]:
        add_bullet(doc, text)


def chapter_admin(doc):
    add_heading(doc, "۶. پنل مدیریت", 1)
    add_callout(
        doc,
        "پنل مدیریت از مسیر pages/login.php وارد می‌شود و صفحات اصلی در modules قرار دارند. نمایش منو با permission انجام می‌شود، اما برای تست باید هم پنهان شدن منو و هم جلوگیری سمت سرور با URL مستقیم بررسی شود.",
        fill=LIGHT_BLUE,
        color=NAVY,
        label="اصل تست پنل مدیریت",
    )
    add_heading(doc, "۶.۱. داشبورد", 2)
    add_table(
        doc,
        ["مسیر", "مجوز", "رفتار فعلی", "تست"],
        [
            ("/modules/dashboard/", "dashboard.view", "صفحه پایه؛ بخشی از خروجی فعلی چاپ session و متن تست permission است.", "ورود با کاربر مجاز/غیرمجاز و بررسی عدم نمایش اطلاعات session."),
        ],
        [2500, 1900, 3000, 1960],
    )
    add_heading(doc, "۶.۲. مدیریت مشتریان", 2)
    add_table(
        doc,
        ["صفحه", "مجوز اصلی", "قابلیت"],
        [
            ("modules/customers/index.php", "customers.modules", "صفحه عملیات مشتریان و لینک‌های دسترسی‌محور."),
            ("modules/customers/list.php", "customers.fulllist", "لیست کامل، فیلتر دسته‌بندی، DataTables و export Excel/PDF."),
            ("modules/customers/add.php", "customers.add", "فرم کامل مشتری؛ دسته، هویت، عکس، تماس، مکان، مالی، توزیع و CRM."),
            ("modules/customers/edit.php", "مجوزهای جزئی", "فرم بخش‌بندی‌شده با قفل کردن فیلدها بر اساس permission."),
            ("modules/customers/profile.php", "customers.profile", "نمایش مشخصات، تاریخچه CRM، assignment، activities و followups."),
            ("modules/customers/followups.php", "customers.followups.view/manage", "لیست، فیلتر، ایجاد، انجام و لغو پیگیری."),
            ("modules/customers/visitor_change_list.php", "customers.visitor.assignments.log.view", "تاریخچه تخصیص بازاریاب به مشتری."),
            ("modules/customers/process_customer.php", "customers.modules + جزئی", "endpoint JSON برای شهرها، ثبت مشتری و ویرایش مشتری."),
            ("modules/customers/followups_process.php", "customers.modules + manage", "endpoint JSON برای ساخت، انجام و لغو پیگیری."),
        ],
        [3100, 2400, 3860],
    )
    add_heading(doc, "۶.۳. فرم ثبت/ویرایش مشتری", 2)
    add_table(
        doc,
        ["بخش فرم", "فیلدهای شاخص", "اعتبارسنجی/رفتار"],
        [
            ("عمومی", "accounting_id، category، shop_name، owner_name، national/economic code، birthday، shop_image", "کد ملی الگوریتمی؛ تصویر فقط JPG/PNG/WEBP و حداکثر ۵MB."),
            ("تماس", "mobile، alternative_mobile، phone، password", "mobile اصلی یکتا و الگوی 09xxxxxxxxx."),
            ("مکان", "province، city، address، postal_code، latitude، longitude", "شهر با استان بررسی می‌شود؛ شهرها AJAX هستند."),
            ("تخصیص", "zone_id، visitor_id، visit_days، delivery_note", "بازاریاب باید فعال و دارای role visitor باشد؛ بازاریاب عادی فقط خودش را می‌گیرد."),
            ("فروش", "payment_type، credit_limit، settlement_days، customer_grade", "cash/credit/cheque/both و رتبه A/B/C."),
            ("تجهیزات", "has_company_equipment، equipment_note، notes", "برای اطلاعات امانی و توضیحات توزیع."),
            ("CRM", "lifecycle_status_id، customer_status، lost_reason، status_reason", "lost بدون دلیل پذیرفته نمی‌شود؛ تغییر وضعیت history و log می‌سازد."),
        ],
        [1800, 3850, 3710],
    )
    add_heading(doc, "۶.۴. دسته‌بندی مشتریان", 2)
    for text in [
        "دسته عمومی با id=1 یا نام عمومی ثابت تلقی می‌شود و قابل ویرایش/حذف نیست.",
        "دسته‌بندی نام تکراری فعال/soft_deleted نشده نمی‌پذیرد.",
        "حذف دسته فقط وقتی مجاز است که مشتری وابسته نداشته باشد.",
        "status دسته ۱ فعال و ۰ غیرفعال است؛ در فرم ثبت، دسته غیرفعال برای مشتری جدید قابل انتخاب نیست.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۶.۵. کاربران", 2)
    add_table(
        doc,
        ["صفحه", "رفتار"],
        [
            ("users/index.php", "فهرست کاربران، نقش‌ها، وضعیت، آخرین ورود و عملیات."),
            ("users/add.php", "ایجاد کاربر با username، رمز حداقل ۶ کاراکتر، mobile، email، status و نقش."),
            ("users/edit.php", "ویرایش اطلاعات، رمز اختیاری و جایگزینی نقش."),
            ("users/profile.php", "پروفایل، نقش‌ها، آخرین ورود، تاریخ ایجاد و تعداد مشتری ثبت‌شده."),
            ("users/process.php", "اعتبارسنجی، یکتایی username/mobile، activate/deactivate و ثبت log."),
        ],
        [2600, 6760],
    )
    add_heading(doc, "۶.۶. نقش‌ها و دسترسی‌ها", 2)
    for text in [
        "roles/index.php تعداد کاربران و permissionهای هر نقش را نمایش می‌دهد.",
        "roles/add.php و roles/edit.php امکان انتخاب permissionها را دارند.",
        "نقش superadmin قابل حذف نیست و slug آن نباید تغییر کند.",
        "حذف نقش دارای کاربر مجاز نیست.",
        "ذخیره permissionهای نقش ابتدا role_permissions را پاک و سپس انتخاب‌های جدید را درج می‌کند؛ این عملیات در تراکنش انجام می‌شود.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۶.۷. استان‌ها و شهرها", 2)
    for text in [
        "استان‌ها با ordered نزولی و سپس نام مرتب می‌شوند.",
        "هر شهر به یک استان وابسته است؛ نام شهر در یک استان تکراری نیست.",
        "استان دارای شهر یا مشتری حذف نمی‌شود.",
        "شهر دارای مشتری حذف نمی‌شود.",
        "تغییر استان یک شهر باید در تست اثر آن بر مشتری‌های قبلی بررسی شود؛ process فعلی اجازه تغییر می‌دهد.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۶.۸. ماشین‌ها و زون‌ها", 2)
    add_table(
        doc,
        ["ماژول", "صفحات", "عملیات اصلی"],
        [
            ("vehicles", "index/add/edit/profile/assignments/process", "تعریف ماشین، مالکیت، وضعیت، تخصیص راننده، پایان تخصیص و تاریخچه."),
            ("zones", "index/add/edit/profile/assignments/process", "تعریف زون، ناحیه، وضعیت، تخصیص ماشین، مشتریان زون و تاریخچه."),
            ("fleet_helpers.php", "helper مشترک", "برچسب وضعیت، مالکیت، ارقام فارسی و escape."),
        ],
        [1800, 3200, 4360],
    )
    add_heading(doc, "۶.۹. لاگ سیستم", 2)
    for text in [
        "modules/logs/index.php جدول خالی اولیه را می‌سازد و DataTables از AJAX تغذیه می‌شود.",
        "modules/logs/ajax/get_logs_list.php جست‌وجو، sort امن با whitelist ستون‌ها و pagination را پیاده می‌کند.",
        "کاربر حذف‌شده به‌صورت سیستم/مهمان نمایش داده می‌شود.",
        "رخدادهای authentication، مشتری، دسته، کاربر، نقش، location، vehicles، zones و followups در بخش‌های مختلف ثبت می‌شوند.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۶.۱۰. تغییر رمز و ابزار SQL", 2)
    add_table(
        doc,
        ["بخش", "رفتار", "هشدار"],
        [
            ("adminsetting/adminpass.php", "تغییر رمز با رمز فعلی، حداقل ۶ کاراکتر و جلوگیری از تکرار رمز.", "CSRF در این فرم مشاهده نشد؛ در تست امنیتی بررسی شود."),
            ("ADMIN/sql.php", "اجرای فایل تکی/چند فایل SQL با CSRF و حذف تمام جدول‌های دیتابیس.", "هیچ احراز هویت مدیریتی در فایل دیده نشد؛ انتشار عمومی آن بسیار خطرناک است."),
        ],
        [2500, 3900, 2960],
        header_fill=RED,
    )


def chapter_marketer(doc):
    add_heading(doc, "۷. پنل بازاریاب", 1)
    add_callout(
        doc,
        "پنل بازاریاب رابطی مستقل و موبایل‌محور است. این پنل از همان جدول customers استفاده می‌کند اما با شرط visitor_id فقط مالک فعلی مشتری را می‌بیند. تغییر مالک، تغییر CRM و ویرایش کامل مشتری در این پنل وجود ندارد.",
        fill=LIGHT_TEAL,
        color=TEAL,
        label="اصل مالکیت",
    )
    add_heading(doc, "۷.۱. مسیرها و مجوزها", 2)
    add_table(
        doc,
        ["مسیر", "مجوز", "قابلیت"],
        [
            ("/marketer/login.php", "marketer.panel.login + role visitor", "ورود مستقل بازاریاب."),
            ("/marketer/index.php", "marketer.panel.access", "شمارنده مشتری من، پیگیری باز و پیگیری امروز."),
            ("/marketer/customers.php", "marketer.customers.view", "لیست مشتریان اختصاص‌یافته با جست‌وجوی نام/فروشگاه/موبایل."),
            ("/marketer/customer.php?id=...", "marketer.customers.view", "پروفایل مشتری خود، پیگیری‌ها و ۲۰ فعالیت آخر."),
            ("/marketer/customer_create.php", "marketer.customers.create", "ثبت مشتری پایه."),
            ("/marketer/customer_process.php", "marketer.customers.create + CSRF", "ثبت مشتری و ساخت assignment/history/activity."),
            ("/marketer/cities.php", "marketer.customers.create + CSRF", "AJAX شهرهای استان."),
            ("/marketer/followups.php", "marketer.followups.view", "لیست pendingهای اختصاص‌یافته."),
            ("/marketer/followup_process.php", "marketer.followups.manage + CSRF", "ثبت و انجام پیگیری؛ لغو عمداً ممنوع."),
            ("/marketer/password.php", "marketer.panel.access", "تغییر رمز شخصی."),
            ("/marketer/logout.php", "session", "پاک‌سازی نشست بازاریاب."),
        ],
        [3100, 2700, 3560],
    )
    add_heading(doc, "۷.۲. داشبورد بازاریاب", 2)
    add_table(
        doc,
        ["کارت/بخش", "منبع داده", "توضیح"],
        [
            ("مشتری من", "COUNT customers WHERE visitor_id", "تعداد مشتریان مالک فعلی."),
            ("پیگیری باز", "COUNT follow_ups WHERE assigned_to AND pending", "وظایف تعیین‌تکلیف‌نشده."),
            ("پیگیری امروز", "DATE(due_at)=CURDATE()", "با تاریخ ذخیره‌شده میلادی/زمان سرور."),
            ("ثبت مشتری", "permission marketer.customers.create", "لینک ثبت مشتری پایه."),
            ("پیگیری‌های پیش‌رو", "۵ followup اول بر اساس due_at", "دسترسی مستقیم به پروفایل مشتری."),
        ],
        [2400, 3400, 3560],
    )
    add_heading(doc, "۷.۳. ثبت مشتری جدید", 2)
    add_table(
        doc,
        ["بخش فرم", "فیلدها", "رفتار سمت سرور"],
        [
            ("اطلاعات اصلی", "owner_name، shop_name، mobile، alternative_mobile، category، grade", "owner_name/category/mobile معتبر الزامی؛ grade فقط A/B/C."),
            ("موقعیت", "province، city، zone، postal_code، address", "city باید متعلق به province باشد؛ zone باید فعال باشد."),
            ("یادداشت", "notes", "در customers.notes ذخیره می‌شود."),
            ("تخصیص خودکار", "visitor_id = session user", "بازاریاب نمی‌تواند مالک دیگری انتخاب کند."),
            ("رخدادهای پس از ثبت", "customer_assignments، status_history، customer_activities، logs", "یک زنجیره اولیه برای CRM ساخته می‌شود."),
        ],
        [1900, 3350, 4110],
    )
    add_heading(doc, "۷.۴. پروفایل مشتری", 2)
    for text in [
        "اطلاعات هویتی، موبایل، دسته، استان/شهر و آدرس را نمایش می‌دهد.",
        "پیگیری‌های مشتری را به ترتیب pending، done، cancelled و due_at نشان می‌دهد.",
        "آخرین ۲۰ activity را نمایش می‌دهد.",
        "اگر permission مدیریت پیگیری وجود داشته باشد، فرم ثبت پیگیری فعال می‌شود.",
        "هیچ فرم ویرایش عمومی مشتری در پنل بازاریاب وجود ندارد.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۷.۵. پیگیری‌های بازاریاب", 2)
    add_table(
        doc,
        ["عملیات", "مجاز؟", "اثر"],
        [
            ("ثبت پیگیری", "بله، روی مشتری خودش", "INSERT follow_ups با assigned_to خود بازاریاب."),
            ("انجام پیگیری", "بله، فقط پیگیری اختصاص‌یافته به خودش", "pending → done، activity جدید، last_activity_at و next_follow_up_at."),
            ("لغو پیگیری", "خیر", "endpoint عمداً خطا می‌دهد و تلاش را در logs ثبت می‌کند."),
            ("مشاهده مشتری دیگر", "خیر", "query profile شرط c.visitor_id = session user دارد."),
            ("تغییر بازاریاب", "خیر", "باید از پنل مدیریت انجام شود."),
        ],
        [2300, 2500, 4560],
        header_fill=TEAL,
    )
    add_heading(doc, "۷.۶. طراحی رابط", 2)
    for text in [
        "در موبایل bottom navigation ثابت و در نمایشگر بزرگ‌تر درون صفحه قرار می‌گیرد.",
        "فرم‌ها با data-marketer-ajax-form بدون refresh ارسال می‌شوند.",
        "CSRF از data-csrf روی .marketer-main تأمین می‌شود.",
        "رنگ‌بندی فعلی بین دو تعریف در marketer-panel.css تغییر کرده است؛ در انتهای فایل متغیرهای جدیدتر دوباره تعریف شده‌اند و باید در مرورگر نهایی بررسی شود.",
        "پنل از فونت IranSans FaNum و تاریخ شمسی استفاده می‌کند.",
    ]:
        add_bullet(doc, text)


def chapter_workflows(doc):
    add_heading(doc, "۸. جریان‌های کاری و قواعد کسب‌وکار", 1)
    add_heading(doc, "۸.۱. ثبت مشتری از پنل مدیریت", 2)
    add_numbered(doc, "کاربر مجاز فرم را باز می‌کند.")
    add_numbered(doc, "دسته فعال و اطلاعات پایه انتخاب می‌شود.")
    add_numbered(doc, "موبایل یکتا و سایر فیلدها اعتبارسنجی می‌شوند.")
    add_numbered(doc, "بازاریاب فعال با role visitor باید انتخاب شود؛ اگر کاربر خودش visitor باشد، visitor_id به session او محدود می‌شود.")
    add_numbered(doc, "تصویر با MIME واقعی بررسی و در uploads/shop_images ذخیره می‌شود.")
    add_numbered(doc, "تراکنش customers، customer_assignments، status_history، activity و log را ثبت می‌کند.")
    add_numbered(doc, "وضعیت اولیه lifecycle_status_id=2 یعنی potential است.")
    add_heading(doc, "۸.۲. ویرایش مشتری و کنترل permission جزئی", 2)
    add_table(
        doc,
        ["permission", "بخش قابل تغییر"],
        [
            ("customers.edit.general", "نام فروشگاه، مالک، اطلاعات هویتی و تصویر."),
            ("customers.edit.contact", "موبایل دوم، تلفن، رمز مشتری و اطلاعات تماس."),
            ("customers.edit.location", "استان، شهر، آدرس، کدپستی و مختصات."),
            ("customers.accounting.edit", "شناسه حسابداری."),
            ("customers.sales.edit", "روش پرداخت، سقف اعتبار، مهلت تسویه و grade."),
            ("customers.distribution.edit", "زون، روزهای ویزیت، تجهیزات و راهنمای توزیع."),
            ("customers.edit.assignment", "تغییر visitor و بستن/ایجاد assignment history."),
            ("customers.edit.crm", "lifecycle، status دسترسی، lost reason و status history."),
        ],
        [3100, 6260],
        header_fill=TEAL,
    )
    add_heading(doc, "۸.۳. ثبت و انجام پیگیری", 2)
    add_code_block(
        doc,
        "create:\n"
        "  validate customer + owner + due_at + type\n"
        "  INSERT follow_ups(status=pending)\n"
        "  next_follow_up_at = MIN(pending.due_at)\n"
        "\n"
        "complete:\n"
        "  UPDATE follow_ups(status=done, completed_at, completed_by)\n"
        "  INSERT customer_activities\n"
        "  UPDATE customers.last_activity_at\n"
        "  refresh next_follow_up_at\n"
        "\n"
        "cancel (مدیریت):\n"
        "  UPDATE follow_ups(status=cancelled)\n"
        "  refresh next_follow_up_at",
    )
    add_heading(doc, "۸.۴. چرخه وضعیت مشتری", 2)
    add_table(
        doc,
        ["انتقال", "محرک فعلی", "اثر داده‌ای"],
        [
            ("NULL → potential", "ثبت مشتری", "customer_status_history اولیه."),
            ("potential → customer", "انتخاب دستی در ویرایش", "first_purchase_at اگر خالی باشد پر می‌شود."),
            ("هر وضعیت → lost", "انتخاب lifecycle lost", "lost_reason اجباری و lost_at ثبت می‌شود."),
            ("lost → وضعیت دیگر", "ویرایش دستی", "lost_at و lost_reason پاک می‌شوند."),
            ("هر وضعیت → inactive", "ویرایش دستی", "اتوماسیون زمانی فعلی وجود ندارد."),
        ],
        [2400, 3100, 3860],
    )
    add_heading(doc, "۸.۵. تخصیص راننده و ماشین", 2)
    add_numbered(doc, "ماشین باید active باشد.")
    add_numbered(doc, "راننده باید کاربر active با role driver باشد.")
    add_numbered(doc, "در تخصیص جدید، رکورد active قبلی ended می‌شود و رکورد جدید درج می‌گردد.")
    add_numbered(doc, "ماشین فعال نمی‌تواند هم‌زمان در زون دیگری تخصیص فعال داشته باشد.")
    add_numbered(doc, "تاریخچه در profile و assignments قابل مشاهده است.")


def chapter_setup(doc):
    add_heading(doc, "۹. راهنمای راه‌اندازی و داده‌های تست", 1)
    add_heading(doc, "۹.۱. پیش‌نیازها", 2)
    for text in [
        "Apache با فعال بودن mod_rewrite/AllowOverride مطابق vhost.txt.",
        "PHP با PDO MySQL و توابع فایل/تصویر مورد استفاده.",
        "MySQL یا MariaDB با پشتیبانی utf8mb4 و foreign key.",
        "تنظیم DocumentRoot روی F:/13-Localhost/atashfaraz یا مسیر معادل.",
        "دامنه محلی atashfaraz.localhost در hosts ویندوز.",
        "دسترسی نوشتن به uploads/shop_images.",
    ]:
        add_bullet(doc, text)
    add_heading(doc, "۹.۲. نصب دیتابیس با ابزار پروژه", 2)
    add_numbered(doc, "دیتابیس خالی syspakhshdb را بسازید.")
    add_numbered(doc, "به /ADMIN/sql.php بروید؛ فقط در محیط محلی و پس از حفاظت دسترسی.")
    add_numbered(doc, "001_initial_schema.sql را اجرا کنید.")
    add_numbered(doc, "002_required_data.sql را اجرا کنید.")
    add_numbered(doc, "برای محیط تست 003_test_data.sql را اجرا کنید.")
    add_numbered(doc, "پس از نصب، login مدیریت و بازاریاب را با سناریوهای فصل ۱۰ بررسی کنید.")
    add_code_block(
        doc,
        "ترتیب صحیح:\n"
        "  001_initial_schema.sql\n"
        "  002_required_data.sql\n"
        "  003_test_data.sql  (اختیاری برای تست)\n",
    )
    add_heading(doc, "۹.۳. کاربران seed و رمز تست", 2)
    add_table(
        doc,
        ["username", "نقش", "رمز seed", "کاربرد تست"],
        [
            ("admin", "مدیر سیستم", "password", "تست کامل پنل مدیریت."),
            ("expert_1", "کارشناس سیستم", "password", "تست رفتار نقش بدون permissionهای تخصیص‌یافته پیش‌فرض."),
            ("expert_2", "کارشناس سیستم", "password", "تست کاربر دوم."),
            ("visitor_1", "بازاریاب", "password", "مالک مشتریان ۱ و ۲."),
            ("visitor_2", "بازاریاب", "password", "مالک مشتریان ۳ و ۴."),
            ("driver_1", "راننده", "password", "راننده ماشین ۱ و زون ۱."),
            ("driver_2", "راننده", "password", "راننده ماشین ۲ و زون ۲/۳."),
        ],
        [2200, 2200, 1800, 3160],
        header_fill=TEAL,
    )
    add_callout(
        doc,
        "رمزهای seed فقط برای محیط توسعه هستند. در اولین نصب واقعی باید admin و تمام حساب‌های نمونه حذف یا رمز آن‌ها تغییر داده شود.",
        fill=LIGHT_RED,
        color=RED,
        label="امنیت",
    )
    add_heading(doc, "۹.۴. داده‌های تست نمونه", 2)
    add_table(
        doc,
        ["حوزه", "نمونه‌های درج‌شده"],
        [
            ("جغرافیا", "استان گیلان و مازندران و شهرهای پایه."),
            ("دسته مشتری", "عمومی، ویژه، تستی."),
            ("زون", "۴ مسیر نمونه تهران غرب/شرق/شمال/جنوب."),
            ("ماشین", "VAN-001 و ISUZU-001."),
            ("مشتری", "۴ مشتری با visitor، zone، grade و payment متفاوت."),
            ("assignment", "تخصیص بازاریاب مشتری و راننده/ماشین/زون."),
        ],
        [2300, 7060],
    )
    add_callout(
        doc,
        "در 003_test_data.sql بعضی آدرس‌های متنی با استان/شهر انتخاب‌شده هم‌خوان نیستند؛ این موضوع برای تست constraint برنامه مفید است اما برای داده نمایشی واقعی باید اصلاح شود.",
        fill=LIGHT_GOLD,
        color="6D4C00",
        label="نکته داده تست",
    )


def chapter_testing(doc):
    add_heading(doc, "۱۰. برنامه تست جامع", 1)
    add_callout(
        doc,
        "هر تست را با شماره، نقش، URL، داده ورودی، نتیجه مورد انتظار و نتیجه واقعی ثبت کنید. تست مجوز را هم با پنهان شدن منو و هم با ورود مستقیم به URL انجام دهید.",
        fill=LIGHT_BLUE,
        color=NAVY,
        label="روش ثبت نتیجه",
    )
    add_heading(doc, "۱۰.۱. تست دودویی راه‌اندازی", 2)
    smoke = [
        "دامنه atashfaraz.localhost باز می‌شود و خطای 500/مسیر مطلق دیده نمی‌شود.",
        "اتصال PDO به syspakhshdb برقرار است.",
        "صفحه login مدیریت باز می‌شود.",
        "ورود admin با password موفق و last_login_at تغییر می‌کند.",
        "خروج مدیریت session را می‌بندد.",
        "ورود visitor_1 به /marketer/login.php موفق است.",
        "پنل بازاریاب در موبایل و دسکتاپ بدون overflow اساسی نمایش داده می‌شود.",
        "استان و شهر در فرم بازاریاب با AJAX کار می‌کند.",
        "تصویر فروشگاه در uploads/shop_images ذخیره می‌شود.",
        "لاگ‌های ایجاد مشتری و پیگیری در modules/logs نمایش داده می‌شوند.",
    ]
    for item in smoke:
        add_bullet(doc, "□ " + item)
    add_heading(doc, "۱۰.۲. تست ورود و نشست", 2)
    add_table(
        doc,
        ["کد", "سناریو", "نتیجه مورد انتظار"],
        [
            ("AUTH-01", "username/password خالی در مدیریت", "خطای اعتبارسنجی و عدم query مؤثر."),
            ("AUTH-02", "رمز اشتباه مدیریت", "عدم ورود و ثبت log شکست."),
            ("AUTH-03", "کاربر pending/blocked/inactive", "عدم ورود با پیام وضعیت مربوط."),
            ("AUTH-04", "ورود موفق مدیریت", "session regenerate، نقش و permission در session."),
            ("AUTH-05", "خروج مدیریت", "پاک شدن session و redirect به login."),
            ("AUTH-06", "بازاریاب بدون role visitor", "رد شدن در login بازاریاب."),
            ("AUTH-07", "visitor بدون marketer.panel.login", "رد شدن در login بازاریاب."),
            ("AUTH-08", "ورود موفق بازاریاب", "ساخت session مستقل و CSRF."),
            ("AUTH-09", "باز کردن URL پنل بدون session", "redirect به login متناظر."),
            ("AUTH-10", "باز کردن صفحه مدیریت با session بازاریاب", "نباید مجوز پنل مدیریت تلقی شود."),
        ],
        [1200, 4000, 4160],
    )
    add_heading(doc, "۱۰.۳. تست CRUD مشتری", 2)
    add_table(
        doc,
        ["کد", "سناریو", "بررسی"],
        [
            ("CUS-01", "ثبت با owner/mobile/category معتبر", "رکورد customers، assignment، history، activity و log ایجاد شود."),
            ("CUS-02", "ثبت با موبایل تکراری", "خطا و عدم ایجاد رکورد دوم."),
            ("CUS-03", "موبایل با رقم فارسی", "در صورت ورودی پنل مناسب، رفتار normalize بررسی شود."),
            ("CUS-04", "alternative_mobile نامعتبر", "خطا."),
            ("CUS-05", "کد ملی نامعتبر/تکراری", "خطا."),
            ("CUS-06", "کد اقتصادی غیر ۱۲ رقمی", "خطا."),
            ("CUS-07", "کد پستی غیر ۱۰ رقمی", "خطا."),
            ("CUS-08", "city متعلق به province دیگر", "خطا."),
            ("CUS-09", "visitor غیرفعال یا بدون role", "خطا."),
            ("CUS-10", "آپلود فایل غیرتصویری/بیش از ۵MB", "خطا و فایل نباید در مقصد بماند."),
            ("CUS-11", "ویرایش توسط visitor روی مشتری دیگری", "رد دسترسی."),
            ("CUS-12", "ویرایش با permission جزئی", "فیلدهای خارج از permission نباید تغییر کنند."),
            ("CUS-13", "تغییر visitor", "assignment قبلی ended و جدید active."),
            ("CUS-14", "تغییر lifecycle به lost بدون دلیل", "خطا."),
            ("CUS-15", "تغییر lifecycle به customer", "first_purchase_at پر شود."),
            ("CUS-16", "حذف/تعویض تصویر", "فایل قبلی در صورت امکان حذف و فایل جدید ثبت شود."),
        ],
        [1200, 3950, 4210],
    )
    add_heading(doc, "۱۰.۴. تست پیگیری و فعالیت", 2)
    add_table(
        doc,
        ["کد", "سناریو", "نتیجه مورد انتظار"],
        [
            ("FUP-01", "ثبت pending توسط مدیر برای مشتری و کاربر فعال", "رکورد pending و next_follow_up_at."),
            ("FUP-02", "ثبت توسط visitor برای مشتری خودش", "موفق."),
            ("FUP-03", "ثبت visitor برای مشتری دیگری", "رد مالکیت."),
            ("FUP-04", "تاریخ شمسی نامعتبر", "خطا."),
            ("FUP-05", "نوع followup نامعتبر", "خطا."),
            ("FUP-06", "complete توسط assigned_to", "done، activity، last_activity_at و refresh موعد بعد."),
            ("FUP-07", "complete توسط کاربر دیگر بدون others.manage", "رد."),
            ("FUP-08", "cancel توسط مدیر", "cancelled و refresh موعد بعد."),
            ("FUP-09", "cancel توسط بازاریاب", "رد عمدی و log تلاش."),
            ("FUP-10", "تعیین تکلیف دوباره همان پیگیری", "خطا و عدم activity تکراری."),
        ],
        [1200, 3950, 4210],
    )
    add_heading(doc, "۱۰.۵. تست پنل بازاریاب", 2)
    add_table(
        doc,
        ["کد", "سناریو", "نتیجه مورد انتظار"],
        [
            ("MKT-01", "لیست مشتریان visitor_1", "فقط visitor_id همان کاربر."),
            ("MKT-02", "جست‌وجوی owner/shop/mobile", "نتایج فقط در محدوده مالک."),
            ("MKT-03", "پروفایل id مشتری دیگر", "404/عدم نمایش."),
            ("MKT-04", "ثبت مشتری پایه", "مالکیت خودکار و سه رکورد CRM اولیه."),
            ("MKT-05", "duplicate mobile مشتری خودش", "پیام مشتری تکراری."),
            ("MKT-06", "duplicate mobile مشتری دیگری", "پیام شماره متعلق به مشتری دیگر."),
            ("MKT-07", "انتخاب استان و city", "فقط شهرهای استان."),
            ("MKT-08", "ارسال بدون یا با CSRF اشتباه", "419 و عدم تغییر داده."),
            ("MKT-09", "ثبت followup با تاریخ شمسی", "تبدیل صحیح و نمایش شمسی."),
            ("MKT-10", "انجام followup", "redirect به لیست و ثبت activity."),
            ("MKT-11", "لغو followup", "رد و log."),
            ("MKT-12", "تغییر رمز با رمز فعلی اشتباه", "خطا."),
            ("MKT-13", "تغییر رمز موفق و ورود مجدد", "رمز جدید معتبر، قدیمی نامعتبر."),
        ],
        [1200, 3950, 4210],
    )
    add_heading(doc, "۱۰.۶. تست RBAC و پنل مدیریت", 2)
    add_table(
        doc,
        ["کد", "نقش/مجوز", "موارد"],
        [
            ("RBAC-01", "superadmin", "همه منوهای seed و عملیات مجاز."),
            ("RBAC-02", "visitor", "عدم مشاهده منوهای users/roles/fleet مگر permission دستی."),
            ("RBAC-03", "بدون customers.fulllist", "عدم نمایش لیست کامل و رد URL."),
            ("RBAC-04", "بدون customers.edit.assignment", "عدم تغییر visitor حتی با POST دست‌کاری‌شده."),
            ("RBAC-05", "بدون customers.followups.others.manage", "فقط assigned خودش."),
            ("RBAC-06", "بدون vehicles.driver.assign", "عدم نمایش/رد endpoint تخصیص."),
            ("RBAC-07", "بدون locations.delete", "دکمه حذف و endpoint هر دو مسدود."),
            ("RBAC-08", "کاربر فعال بدون permission panel.login", "رفتار واقعی login و تصمیم اصلاحی ثبت شود."),
        ],
        [1200, 3000, 5160],
    )
    add_heading(doc, "۱۰.۷. تست ناوگان و زون", 2)
    for item in [
        "کد ماشین و پلاک تکراری رد شود.",
        "ماشین شخصی بدون owner_name رد شود.",
        "راننده غیر driver یا inactive رد شود.",
        "تخصیص راننده جدید، قبلی را ended کند.",
        "پایان تخصیص بدون رکورد active خطا دهد.",
        "ماشین در زون دوم هم‌زمان قابل تخصیص نباشد.",
        "تخصیص ماشین غیرفعال به زون رد شود.",
        "تاریخچه راننده و زون در profile و assignments یکسان باشد.",
        "تعداد مشتری زون با customers.zone_id سازگار باشد.",
    ]:
        add_bullet(doc, "□ " + item)
    add_heading(doc, "۱۰.۸. تست لاگ و حسابرسی", 2)
    for item in [
        "ورود موفق و ناموفق مدیریت و بازاریاب log داشته باشد.",
        "ثبت، ویرایش و تغییر visitor مشتری log داشته باشد.",
        "تغییر lifecycle با reason در history و log دیده شود.",
        "ثبت، انجام و لغو پیگیری log داشته باشد.",
        "تلاش لغو توسط بازاریاب log شود.",
        "IP در محیط واقعی مقدار معتبر داشته باشد.",
        "جست‌وجوی لاگ با نام کاربر، نوع، شرح و IP نتیجه درست بدهد.",
        "مرتب‌سازی DataTables فقط روی ستون‌های whitelist انجام شود.",
    ]:
        add_bullet(doc, "□ " + item)


def chapter_security(doc):
    add_heading(doc, "۱۱. بررسی‌های امنیتی و کیفیتی", 1)
    add_heading(doc, "۱۱.۱. موارد مثبت موجود", 2)
    for text in [
        "اتصال دیتابیس با PDO و emulate prepares=false است.",
        "رمزها با password_hash و password_verify مدیریت می‌شوند.",
        "session_regenerate_id در ورود استفاده شده است.",
        "پنل بازاریاب CSRF دارد و درخواست‌های تغییردهنده را بررسی می‌کند.",
        "خروجی‌های پنل بازاریاب با marketer_e escape می‌شوند.",
        "پارامترهای اصلی SQL با prepared statement ارسال می‌شوند.",
        "مرتب‌سازی لاگ با whitelist ستون‌ها کنترل شده است.",
        "فایل تصویر با extension و MIME واقعی بررسی می‌شود.",
    ]:
        add_bullet(doc, text, color=TEAL)
    add_heading(doc, "۱۱.۲. ریسک‌ها و آزمون‌های امنیتی اجباری", 2)
    add_table(
        doc,
        ["ریسک", "شاهد کد", "تست/اقدام اصلاحی"],
        [
            ("ADMIN/sql.php بدون auth", "فایل session و CSRF دارد اما requirePermission یا login ندارد.", "در production مسدود/حذف؛ دسترسی فقط local یا admin."),
            ("CSRF ناقص در پنل مدیریت", "فرم‌های users، roles، locations، vehicles، zones و adminpass توکن ندارند.", "POST از سایت دیگر/توکن جعلی تست و CSRF سراسری اضافه شود."),
            ("رمز دیتابیس root خالی", "db_settings.php و ADMIN/sql.php.", "کاربر کم‌دسترسی و secret محیطی."),
            ("مسیرهای مطلق و DocumentRoot", "layout و AJAXها به / و $_SERVER['DOCUMENT_ROOT'] متکی‌اند.", "استقرار در root واقعی و تست subdirectory."),
            ("حذف سخت مشتری", "FKهای وابسته CASCADE هستند.", "محدود کردن DELETE و soft delete واقعی."),
            ("آپلود در web root", "uploads/shop_images داخل ریشه عمومی.", "هدر/پسوند/دسترسی و جلوگیری از اجرای script."),
            ("نمایش خطای فنی", "برخی JSONها e->getMessage() را برمی‌گردانند.", "در production پیام عمومی و log داخلی."),
            ("عدم قید یکتایی فعال assignment", "unique فقط در برخی جداول روی زوج اصلی نیست.", "قفل منطقی/transaction و گزارش duplicate فعال."),
        ],
        [2200, 3200, 4160],
        header_fill=RED,
    )
    add_heading(doc, "۱۱.۳. کیفیت و نگهداری", 2)
    for text in [
        "منطق مشابه در processهای customers و marketer تکرار شده و تغییرات آینده ممکن است واگرا شوند.",
        "نام‌گذاری وضعیت‌ها و متن‌ها در بعضی فایل‌ها تکرار شده؛ enum/constant مرکزی مفید است.",
        "دو نسخه متغیرهای CSS در marketer-panel.css وجود دارد و آخرین تعریف برنده می‌شود.",
        "برخی لینک‌ها و assetها به مسیرهایی اشاره می‌کنند که در مخزن فعلی موجود نیستند.",
        "وجود فایل‌های قدیمی و vendorهای متعدد نگهداری frontend را دشوار می‌کند.",
        "تست خودکار PHP و migration versioning در پروژه دیده نشد.",
    ]:
        add_bullet(doc, text)


def chapter_findings(doc):
    add_heading(doc, "۱۲. نواقص، ریسک‌ها و موارد نیازمند تصمیم", 1)
    add_table(
        doc,
        ["اولویت", "مورد", "اثر", "پیشنهاد"],
        [
            ("بحرانی", "ADMIN/sql.php قابل دسترسی بدون احراز هویت", "اجرای SQL یا حذف کل دیتابیس", "حذف از web root یا افزودن auth قوی و محدودیت IP."),
            ("بالا", "رمز دیتابیس root خالی", "دسترسی کامل در صورت compromise", "secret محیطی و کاربر محدود."),
            ("بالا", "CSRF در اکثر فرم‌های مدیریت", "تغییر داده با درخواست جعلی", "helper مرکزی CSRF برای تمام POSTها."),
            ("بالا", "ارجاع به modules/reports و modules/support بدون فایل", "لینک 404 در sidebar", "ماژول بسازید یا لینک را حذف/غیرفعال کنید."),
            ("بالا", "ارجاع به includes/search_1.php وجود ندارد", "جست‌وجوی modal کار نمی‌کند", "endpoint را ایجاد یا modal را حذف کنید."),
            ("بالا", "ارجاع به modules/forms/add.php وجود ندارد", "دکمه سریع header خراب", "ماژول forms را ایجاد یا لینک را حذف کنید."),
            ("متوسط", "حذف مشتری به ajax/customer_delete_ajax.php اشاره می‌کند ولی فایل در tree دیده نشد", "حذف مشتری احتمالاً 404 است", "endpoint امن با permission و soft delete."),
            ("متوسط", "داشبورد هنوز خروجی print_r session/متن تست دارد", "نشت اطلاعات و ظاهر غیرنهایی", "پاک‌سازی قبل از production."),
            ("متوسط", "001_initial_schema.sql idempotent کامل نیست", "اجرای دوباره خطا می‌دهد", "migrationهای نسخه‌دار یا IF NOT EXISTS برای ALTER."),
            ("متوسط", "داده تست استان/آدرس ناسازگار", "گزارش‌های غلط در تست نمایشی", "داده seed را هماهنگ کنید."),
            ("کم", "مرز PHP/HTML/SQL در فایل‌های بزرگ", "تست و تغییر سخت‌تر", "تفکیک service/repository/view در فاز refactor."),
        ],
        [1250, 2850, 2800, 2460],
        header_fill=RED,
    )
    add_heading(doc, "۱۲.۱. ارجاع‌های شکسته شناسایی‌شده", 2)
    add_table(
        doc,
        ["ارجاع", "محل ارجاع", "وضعیت در مخزن"],
        [
            ("/modules/reports", "templates/sidebar.php", "پوشه modules/reports وجود ندارد."),
            ("/modules/support", "templates/sidebar.php", "پوشه modules/support وجود ندارد."),
            ("/includes/search_1.php", "templates/footer_js.php", "پوشه includes و فایل endpoint وجود ندارد."),
            ("/modules/forms/add.php", "templates/header_navbar.php", "پوشه modules/forms وجود ندارد."),
            ("ajax/customer_delete_ajax.php", "modules/customers/list.php", "در فهرست فایل‌های فعلی دیده نشد."),
        ],
        [3000, 3100, 3260],
    )
    add_heading(doc, "۱۲.۲. تصمیم‌های کسب‌وکاری باز", 2)
    for text in [
        "چه رویدادی lifecycle_status را از potential به customer تغییر می‌دهد: ثبت سفارش، تأیید مدیر یا ورود دستی؟",
        "آیا بازاریاب دفتر باید بتواند پیگیری بازاریاب دیگر را انجام دهد؟",
        "آیا بازاریاب باید بتواند status CRM یا فقط followup را تغییر دهد؟",
        "آیا حذف مشتری باید ممنوع و به soft delete تبدیل شود؟",
        "تعریف دقیق استفاده از accounting_id و سرویس تست اتصال چیست؟",
        "منطق inactive شدن بعد از سه ماه بر اساس last_activity_at یا last_purchase چه باشد؟",
        "آیا یک ماشین می‌تواند در چند زون به‌صورت هم‌زمان باشد یا فقط انتقال تاریخی مجاز است؟",
    ]:
        add_bullet(doc, "□ " + text)


def chapter_routes(doc):
    add_heading(doc, "۱۳. نقشه مسیرها و فایل‌ها", 1)
    add_heading(doc, "۱۳.۱. مسیرهای عمومی", 2)
    add_table(
        doc,
        ["URL", "فایل", "احراز/خروجی"],
        [
            ("/pages/login.php", "pages/login.php", "ورود مدیریت؛ HTML."),
            ("/pages/logout.php", "pages/logout.php", "خروج مدیریت."),
            ("/marketer/login.php", "marketer/login.php", "ورود بازاریاب؛ HTML."),
            ("/marketer/logout.php", "marketer/logout.php", "خروج بازاریاب."),
            ("/ADMIN/sql.php", "ADMIN/sql.php", "اجرای SQL؛ خطرناک و باید حفاظت شود."),
        ],
        [2600, 2900, 3860],
    )
    add_heading(doc, "۱۳.۲. مسیرهای مشتری و CRM", 2)
    add_table(
        doc,
        ["مسیر", "کارکرد"],
        [
            ("/modules/customers/", "redirect به list.php."),
            ("/modules/customers/list.php", "لیست مشتری."),
            ("/modules/customers/add.php", "ثبت مشتری."),
            ("/modules/customers/edit.php?id=N", "ویرایش مشتری."),
            ("/modules/customers/profile.php?id=N", "پروفایل مشتری."),
            ("/modules/customers/followups.php", "لیست و مدیریت پیگیری."),
            ("/modules/customers/visitor_change_list.php", "تاریخچه بازاریاب."),
            ("/modules/customers/process_customer.php", "POST/JSON ثبت، ویرایش و شهرها."),
            ("/modules/customers/followups_process.php", "POST/JSON پیگیری."),
            ("/modules/customerscat/", "دسته‌بندی مشتری."),
        ],
        [3300, 6060],
    )
    add_heading(doc, "۱۳.۳. مسیرهای مدیریت سیستم و توزیع", 2)
    add_table(
        doc,
        ["ماژول", "صفحات"],
        [
            ("users", "index.php، add.php، edit.php، profile.php، process.php"),
            ("roles", "index.php، add.php، edit.php، process.php"),
            ("locations", "index.php، province_add.php، province_edit.php، city_add.php، city_edit.php، process.php"),
            ("vehicles", "index.php، add.php، edit.php، profile.php، assignments.php، process.php"),
            ("zones", "index.php، add.php، edit.php، profile.php، assignments.php، process.php"),
            ("logs", "index.php، ajax/get_logs_list.php، js/logs_list.js"),
            ("adminsetting", "adminpass.php، adminpass_process.php"),
        ],
        [2200, 7160],
    )
    add_heading(doc, "۱۳.۴. مسیرهای پنل بازاریاب", 2)
    add_table(
        doc,
        ["فایل", "نوع", "شرح"],
        [
            ("_bootstrap.php", "shared", "session و helper."),
            ("index.php", "page", "داشبورد."),
            ("customers.php", "page", "مشتریان من."),
            ("customer.php", "page", "پروفایل مشتری."),
            ("customer_create.php", "page", "فرم ثبت."),
            ("customer_process.php", "endpoint", "ثبت مشتری."),
            ("cities.php", "endpoint", "شهرها."),
            ("followups.php", "page", "پیگیری‌های من."),
            ("followup_process.php", "endpoint", "ثبت/انجام پیگیری."),
            ("password.php / password_process.php", "page/endpoint", "تغییر رمز."),
        ],
        [2900, 1600, 4860],
    )


def chapter_summary(doc):
    add_heading(doc, "۱۴. جمع‌بندی عملیاتی", 1)
    add_para(
        doc,
        "پروژه از نظر هسته CRM، مدل مشتری، مالکیت بازاریاب، پیگیری و تاریخچه، پایه قابل استفاده‌ای دارد. مسیر پیشنهادی برای بهره‌برداری ایمن این است که ابتدا موارد بحرانی فصل ۱۲ اصلاح شوند، سپس migration و داده seed پایدار شوند، بعد تست‌های فصل ۱۰ در یک محیط تمیز اجرا و مستند گردند."
    )
    add_heading(doc, "۱۴.۱. ترتیب اولویت اصلاح", 2)
    for item in [
        "بستن یا حفاظت کامل ADMIN/sql.php.",
        "حذف root بدون رمز و انتقال secrets به تنظیمات محیطی.",
        "افزودن CSRF به تمام POSTهای پنل مدیریت.",
        "رفع مسیرهای شکسته reports، support، forms، search و حذف مشتری.",
        "پاک‌سازی داشبورد و خروجی‌های debug.",
        "اصلاح idempotency فایل‌های SQL و ایجاد نسخه‌بندی migration.",
        "تعریف سیاست حذف مشتری و assignmentهای فعال در سطح دیتابیس.",
        "تکمیل تست‌های RBAC و قرارداد کسب‌وکاری lifecycle.",
        "سپس refactor تدریجی منطق تکراری مشتری/پیگیری به helper یا service مشترک.",
    ]:
        add_numbered(doc, item)
    add_heading(doc, "۱۴.۲. چک‌لیست تحویل نسخه تست", 2)
    for item in [
        "□ دیتابیس از صفر نصب شد.",
        "□ داده تست پاک و دوباره درج شد.",
        "□ admin، visitor_1، visitor_2 و driverها تست شدند.",
        "□ تمام URLهای مجاز و غیرمجاز بررسی شدند.",
        "□ ثبت مشتری، ویرایش، تصویر، assignment و CRM تست شد.",
        "□ ثبت/انجام/لغو پیگیری و next_follow_up_at بررسی شد.",
        "□ لاگ‌های مهم قابل جست‌وجو و قابل تطبیق با عملیات هستند.",
        "□ ابزار SQL از دسترس عمومی خارج است.",
        "□ خطاهای PHP و 404های منابع در مرورگر باقی نمانده‌اند.",
        "□ backup و rollback دیتابیس قبل از استقرار تهیه شده است.",
    ]:
        add_bullet(doc, item)
    add_callout(
        doc,
        "این سند وضعیت کد موجود را توصیف می‌کند، نه الزاماً وضعیت مطلوب کسب‌وکار. هرجا رفتار فعلی با تصمیم سازمانی متفاوت است، تصمیم رسمی باید به permission، migration، تست و سپس کد تبدیل شود.",
        fill=LIGHT_BLUE,
        color=NAVY,
        label="نتیجه نهایی",
    )


def build():
    doc = Document()
    configure_document(doc)
    title_page(doc)
    toc(doc)
    chapter_scope(doc)
    chapter_architecture(doc)
    chapter_structure(doc)
    chapter_auth(doc)
    chapter_database(doc)
    chapter_admin(doc)
    chapter_marketer(doc)
    chapter_workflows(doc)
    chapter_setup(doc)
    chapter_testing(doc)
    chapter_security(doc)
    chapter_findings(doc)
    chapter_routes(doc)
    chapter_summary(doc)

    doc.core_properties.title = "مستند جامع پروژه CRM آتش فراز"
    doc.core_properties.subject = "معماری، دیتابیس، پنل مدیریت، پنل بازاریاب و تست"
    doc.core_properties.author = "Atash Faraz CRM"
    doc.core_properties.comments = "تهیه‌شده بر اساس وضعیت کد پروژه در ۲۲ مرداد ۱۴۰۵"
    OUT.parent.mkdir(parents=True, exist_ok=True)
    doc.save(OUT)
    print(OUT)


if __name__ == "__main__":
    build()
