feat(dealix): GTM polish, CRM/AI APIs, launch verification hardening

- Add integrations CRM and AI routing APIs; Salesforce OAuth refresh; lead CRM metadata
- Marketer hub, settings CRM UI, OS views; premium landing and strategy_summary differentiators
- Docs: API-MAP, product guide, competitive matrix, launch simulation, AGENT-MAP LLM routing
- Sync script: strategy legal + competitive matrix to public; pytest DB isolation (.pytest_dealix.sqlite)
- Tests: CRM status and AI routing smoke; check_go_live_gate UTF-8 stdout on Windows
- Alembic migrations for strategic deal links and lead company/sector/city

Made-with: Cursor
This commit is contained in:
Sami Assiri 2026-04-13 05:08:39 +03:00
parent c114ac34ae
commit 07557c4be9
61 changed files with 4118 additions and 399 deletions

View File

@ -49,6 +49,7 @@ coverage/
# Local SQLite / CI DB artifacts (never commit) # Local SQLite / CI DB artifacts (never commit)
backend/*.db backend/*.db
backend/ci_*.db backend/ci_*.db
backend/.pytest_dealix.sqlite
# Playwright / E2E output # Playwright / E2E output
frontend/test-results/ frontend/test-results/

View File

@ -0,0 +1,40 @@
"""Add lead_id and sales_deal_id to strategic_deals.
Revision ID: 20260413_0002
Revises: 20260403_0001
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "20260413_0002"
down_revision: Union[str, None] = "20260403_0001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name if bind else ""
uid = sa.Uuid(as_uuid=True)
if dialect == "sqlite":
with op.batch_alter_table("strategic_deals") as batch:
batch.add_column(sa.Column("lead_id", uid, nullable=True))
batch.add_column(sa.Column("sales_deal_id", uid, nullable=True))
else:
op.add_column("strategic_deals", sa.Column("lead_id", uid, nullable=True))
op.add_column("strategic_deals", sa.Column("sales_deal_id", uid, nullable=True))
def downgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name if bind else ""
if dialect == "sqlite":
with op.batch_alter_table("strategic_deals") as batch:
batch.drop_column("sales_deal_id")
batch.drop_column("lead_id")
else:
op.drop_column("strategic_deals", "sales_deal_id")
op.drop_column("strategic_deals", "lead_id")

View File

@ -0,0 +1,43 @@
"""Add company_name, sector, city to leads.
Revision ID: 20260413_0003
Revises: 20260413_0002
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "20260413_0003"
down_revision: Union[str, None] = "20260413_0002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name if bind else ""
if dialect == "sqlite":
with op.batch_alter_table("leads") as batch:
batch.add_column(sa.Column("company_name", sa.String(255), nullable=True))
batch.add_column(sa.Column("sector", sa.String(100), nullable=True))
batch.add_column(sa.Column("city", sa.String(100), nullable=True))
else:
op.add_column("leads", sa.Column("company_name", sa.String(255), nullable=True))
op.add_column("leads", sa.Column("sector", sa.String(100), nullable=True))
op.add_column("leads", sa.Column("city", sa.String(100), nullable=True))
def downgrade() -> None:
bind = op.get_bind()
dialect = bind.dialect.name if bind else ""
if dialect == "sqlite":
with op.batch_alter_table("leads") as batch:
batch.drop_column("city")
batch.drop_column("sector")
batch.drop_column("company_name")
else:
op.drop_column("leads", "city")
op.drop_column("leads", "sector")
op.drop_column("leads", "company_name")

View File

@ -0,0 +1,116 @@
"""Tenant-level LLM routing policy (no API keys exposed)."""
from __future__ import annotations
from typing import Any, Dict, Literal
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_role
from app.config import get_settings
from app.database import get_db
from app.models.tenant import Tenant
from app.models.user import User
router = APIRouter(prefix="/ai", tags=["AI — routing"])
TaskKey = Literal["discovery", "negotiation", "compliance", "strategy_summary", "embeddings"]
class TaskRoute(BaseModel):
provider: str = Field(..., description="groq | openai | anthropic | etc.")
model: str = Field(..., description="Model id for that provider")
class RoutingMap(BaseModel):
discovery: TaskRoute | None = None
negotiation: TaskRoute | None = None
compliance: TaskRoute | None = None
strategy_summary: TaskRoute | None = None
embeddings: TaskRoute | None = None
def _defaults_from_settings() -> Dict[str, Dict[str, str]]:
s = get_settings()
primary = (s.LLM_PRIMARY_PROVIDER or "groq").lower()
if primary == "openai":
default_model = s.OPENAI_MODEL
else:
default_model = s.GROQ_MODEL
return {
"discovery": {"provider": primary, "model": default_model},
"negotiation": {"provider": primary, "model": default_model},
"compliance": {"provider": primary, "model": s.OPENAI_MINI_MODEL if primary == "openai" else s.GROQ_FAST_MODEL},
"strategy_summary": {"provider": primary, "model": default_model},
"embeddings": {"provider": "openai", "model": s.EMBEDDING_MODEL},
}
def _available_providers() -> list[str]:
s = get_settings()
out = []
if s.GROQ_API_KEY:
out.append("groq")
if s.OPENAI_API_KEY:
out.append("openai")
if s.ANTHROPIC_API_KEY:
out.append("anthropic")
if s.DEEPSEEK_API_KEY:
out.append("deepseek")
if s.GOOGLE_API_KEY:
out.append("google")
if s.ZAI_API_KEY:
out.append("zai")
return out
def _merge_routing(tenant_settings: dict | None) -> Dict[str, Dict[str, str]]:
base = _defaults_from_settings()
custom = (tenant_settings or {}).get("llm_routing") or {}
for k, v in custom.items():
if isinstance(v, dict) and v.get("provider") and v.get("model"):
base[k] = {"provider": str(v["provider"]), "model": str(v["model"])}
return base
@router.get("/routing")
async def get_ai_routing(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id))
tenant = r.scalar_one_or_none()
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
return {
"effective": _merge_routing(tenant.settings),
"available_providers": _available_providers(),
"note_ar": "المفاتيح تبقى في الخادم فقط — الواجهة ترى أسماء المزودين والنماذج فقط.",
}
@router.put("/routing", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def put_ai_routing(
body: RoutingMap,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id))
tenant = r.scalar_one_or_none()
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
patch: Dict[str, Any] = {}
data = body.model_dump(exclude_none=True)
for task, spec in data.items():
if isinstance(spec, dict):
patch[task] = spec
base = dict(tenant.settings or {})
lr = dict(base.get("llm_routing") or {})
lr.update(patch)
base["llm_routing"] = lr
tenant.settings = base
await db.flush()
return {"status": "ok", "effective": _merge_routing(base)}

View File

@ -0,0 +1,226 @@
"""CRM integrations API — Salesforce & HubSpot sync (JWT)."""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_role
from app.config import get_settings
from app.database import get_db
from app.models.tenant import Tenant
from app.models.user import User
from app.services.crm_sync_service import CRMSyncService
from app.services.lead_service import LeadService
from app.services.operations_hub import upsert_connector_status
router = APIRouter(prefix="/integrations/crm", tags=["Integrations — CRM"])
settings = get_settings()
class TenantCRMUpdate(BaseModel):
"""Store non-secret CRM overrides on tenant.settings['crm'] (encrypt at rest in production)."""
salesforce: dict | None = None
hubspot: dict | None = None
async def _tenant(db: AsyncSession, tenant_id: UUID) -> Tenant:
r = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
t = r.scalar_one_or_none()
if not t:
raise HTTPException(status_code=404, detail="Tenant not found")
return t
@router.get("/status")
async def crm_status(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Which CRM keys are present (env vs tenant) — no secrets returned."""
tenant = await _tenant(db, current_user.tenant_id)
crm = (tenant.settings or {}).get("crm") or {}
sf_t = (crm.get("salesforce") or {})
hs_t = (crm.get("hubspot") or {})
return {
"salesforce": {
"env_refresh_configured": bool(
settings.SALESFORCE_CLIENT_ID
and settings.SALESFORCE_CLIENT_SECRET
and settings.SALESFORCE_REFRESH_TOKEN
),
"tenant_refresh_override": bool(sf_t.get("refresh_token")),
"domain": sf_t.get("domain") or settings.SALESFORCE_DOMAIN or "login.salesforce.com",
},
"hubspot": {
"env_token_configured": bool(settings.HUBSPOT_API_KEY),
"tenant_token_override": bool(hs_t.get("private_app_token") or hs_t.get("access_token")),
},
"docs": {
"integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md",
"api_map": "docs/API-MAP.md",
},
}
@router.put("/tenant-settings", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def put_tenant_crm_settings(
body: TenantCRMUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant = await _tenant(db, current_user.tenant_id)
base = dict(tenant.settings or {})
crm = dict(base.get("crm") or {})
if body.salesforce is not None:
crm["salesforce"] = {**(crm.get("salesforce") or {}), **body.salesforce}
if body.hubspot is not None:
crm["hubspot"] = {**(crm.get("hubspot") or {}), **body.hubspot}
base["crm"] = crm
tenant.settings = base
await db.flush()
return {"status": "ok"}
@router.post("/salesforce/test", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_test(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
try:
creds = await svc._get_crm_credentials(str(current_user.tenant_id), "salesforce")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e)[:500]) from e
if not creds:
raise HTTPException(status_code=400, detail="Salesforce not configured (refresh token + client id/secret)")
probe = await svc.salesforce_identity_probe(creds)
ok = bool(probe.get("ok"))
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_salesforce",
status="ok" if ok else "error",
success=ok,
last_error=None if ok else str(probe.get("detail") or probe)[:500],
)
return probe
@router.post("/salesforce/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_push_lead(
lead_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "salesforce")
if res.get("status") == "success" and res.get("salesforce_id"):
ls = LeadService(db)
await ls.merge_lead_extra_metadata(
str(current_user.tenant_id),
str(lead_id),
{"salesforce_lead_id": res["salesforce_id"]},
)
await upsert_connector_status(
db, current_user.tenant_id, "crm_salesforce", status="ok", success=True,
)
elif res.get("status") == "error":
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_salesforce",
status="error",
last_error=str(res.get("message", res))[:500],
)
return res
class PullBody(BaseModel):
since: str | None = Field(None, description="Ignored for Salesforce MVP; reserved for SOQL")
@router.post("/salesforce/pull-leads", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_pull_leads(
body: PullBody | None = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = body
svc = CRMSyncService(db)
res = await svc.full_sync(str(current_user.tenant_id), "salesforce")
if res.get("status") == "completed":
await upsert_connector_status(
db, current_user.tenant_id, "crm_salesforce", status="ok", success=True,
)
return res
@router.post("/hubspot/test", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_test(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
creds = await svc._get_crm_credentials(str(current_user.tenant_id), "hubspot")
if not creds:
raise HTTPException(status_code=400, detail="HubSpot token not configured")
probe = await svc.hubspot_identity_probe(creds.get("api_key", ""))
ok = bool(probe.get("ok"))
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_hubspot",
status="ok" if ok else "error",
success=ok,
last_error=None if ok else str(probe.get("detail") or probe)[:500],
)
return probe
@router.post("/hubspot/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_push_lead(
lead_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "hubspot")
if res.get("status") == "success" and res.get("hubspot_id"):
ls = LeadService(db)
await ls.merge_lead_extra_metadata(
str(current_user.tenant_id),
str(lead_id),
{"hubspot_contact_id": res["hubspot_id"]},
)
await upsert_connector_status(
db, current_user.tenant_id, "crm_hubspot", status="ok", success=True,
)
elif res.get("status") == "error":
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_hubspot",
status="error",
last_error=str(res.get("message", res))[:500],
)
return res
@router.post("/hubspot/pull-contacts", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_pull_contacts(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.full_sync(str(current_user.tenant_id), "hubspot")
if res.get("status") == "completed":
await upsert_connector_status(
db, current_user.tenant_id, "crm_hubspot", status="ok", success=True,
)
return res

View File

@ -108,6 +108,7 @@ def _demo_snapshot() -> Dict[str, Any]:
"audit_events_24h": 0, "audit_events_24h": 0,
"connectors": [ "connectors": [
{"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None},
{"connector_key": "crm_hubspot", "display_name_ar": "HubSpot CRM", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None},
{"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None},
{"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None},
{"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None},
@ -154,6 +155,9 @@ async def operations_snapshot(
pending = await count_pending_approvals(db, user.tenant_id) pending = await count_pending_approvals(db, user.tenant_id)
ev = await count_events_since(db, user.tenant_id, 24) ev = await count_events_since(db, user.tenant_id, 24)
aud = await count_audits_since(db, user.tenant_id, 24) aud = await count_audits_since(db, user.tenant_id, 24)
from app.services.integration_probe import probe_and_persist_crm_connectors
await probe_and_persist_crm_connectors(db, user.tenant_id)
connectors = await list_integration_connectors(db, user.tenant_id) connectors = await list_integration_connectors(db, user.tenant_id)
tenant_id_str = str(user.tenant_id) tenant_id_str = str(user.tenant_id)
esc = await refresh_pending_escalations(db, user.tenant_id) esc = await refresh_pending_escalations(db, user.tenant_id)

View File

@ -25,6 +25,8 @@ from app.api.v1 import customer_onboarding as customer_onboarding_router
from app.api.v1 import sales_os as sales_os_router from app.api.v1 import sales_os as sales_os_router
from app.api.v1 import operations as operations_router from app.api.v1 import operations as operations_router
from app.api.v1 import proposals as proposals_router from app.api.v1 import proposals as proposals_router
from app.api.v1 import integrations_crm as integrations_crm_router
from app.api.v1 import ai_routing as ai_routing_router
api_router = APIRouter() api_router = APIRouter()
@ -58,6 +60,8 @@ api_router.include_router(value_proposition_router.router)
api_router.include_router(customer_onboarding_router.router) api_router.include_router(customer_onboarding_router.router)
api_router.include_router(sales_os_router.router) api_router.include_router(sales_os_router.router)
api_router.include_router(operations_router.router) api_router.include_router(operations_router.router)
api_router.include_router(integrations_crm_router.router)
api_router.include_router(ai_routing_router.router)
api_router.include_router(analytics.router, tags=["Analytics & AI"]) api_router.include_router(analytics.router, tags=["Analytics & AI"])
api_router.include_router(webhooks.router, tags=["Webhooks"]) api_router.include_router(webhooks.router, tags=["Webhooks"])
api_router.include_router(prospecting.router, prefix="/prospecting", tags=["Prospecting"]) api_router.include_router(prospecting.router, prefix="/prospecting", tags=["Prospecting"])

View File

@ -24,6 +24,11 @@ from app.services.strategic_deals.company_profiler import CompanyProfiler
from app.services.strategic_deals.deal_matcher import DealMatcher from app.services.strategic_deals.deal_matcher import DealMatcher
from app.services.strategic_deals.deal_negotiator import DealNegotiator, NegotiationStrategy from app.services.strategic_deals.deal_negotiator import DealNegotiator, NegotiationStrategy
from app.services.strategic_deals.deal_agent import DealAgent from app.services.strategic_deals.deal_agent import DealAgent
from app.services.strategic_deals.operating_modes import OperatingMode, ModeEnforcer
from app.services.strategic_deals.deal_taxonomy import DealTaxonomyService
from app.services.dealix_os.vertical_playbooks import get_playbook, list_playbook_ids
from app.services.dealix_os.partner_archetypes import list_archetypes, archetype_for_deal_type
from app.services.dealix_os.policy_engine import evaluate_action, suggested_playbook_for_industry
router = APIRouter(prefix="/strategic-deals", tags=["Strategic Deals"]) router = APIRouter(prefix="/strategic-deals", tags=["Strategic Deals"])
@ -93,6 +98,8 @@ class DealCreate(Schema):
proposed_terms: dict = {} proposed_terms: dict = {}
estimated_value_sar: Optional[float] = None estimated_value_sar: Optional[float] = None
channel: str = "whatsapp" channel: str = "whatsapp"
lead_id: Optional[UUID] = None
sales_deal_id: Optional[UUID] = None
class DealResponse(Schema): class DealResponse(Schema):
@ -117,6 +124,8 @@ class DealResponse(Schema):
negotiation_history: list = [] negotiation_history: list = []
notes: Optional[str] = None notes: Optional[str] = None
notes_ar: Optional[str] = None notes_ar: Optional[str] = None
lead_id: Optional[UUID] = None
sales_deal_id: Optional[UUID] = None
created_at: datetime created_at: datetime
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
closed_at: Optional[datetime] = None closed_at: Optional[datetime] = None
@ -161,9 +170,44 @@ class BarterScanRequest(Schema):
profile_id: UUID profile_id: UUID
class OperatingModeSet(Schema):
mode: int = Field(..., ge=0, le=4, description="OperatingMode 04")
class PolicyEvaluateRequest(Schema):
channel: str = "whatsapp"
action: str = "send_custom_message"
deal_value_sar: float = 0.0
industry: Optional[str] = None
class DealLinksUpdate(Schema):
lead_id: Optional[UUID] = None
sales_deal_id: Optional[UUID] = None
# ── Profile Endpoints ──────────────────────────────────────────────────────── # ── Profile Endpoints ────────────────────────────────────────────────────────
@router.get("/profiles", response_model=list[ProfileResponse])
async def list_profiles(
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List company profiles for the tenant. | عرض ملفات الشركات"""
q = (
select(CompanyProfile)
.where(CompanyProfile.tenant_id == current_user.tenant_id)
.order_by(CompanyProfile.created_at.desc())
.offset((page - 1) * per_page)
.limit(per_page)
)
result = await db.execute(q)
return [ProfileResponse.model_validate(p) for p in result.scalars().all()]
@router.post("/profiles", response_model=ProfileResponse, status_code=201) @router.post("/profiles", response_model=ProfileResponse, status_code=201)
async def create_profile( async def create_profile(
data: ProfileCreate, data: ProfileCreate,
@ -339,6 +383,8 @@ async def create_deal(
channel=data.channel, channel=data.channel,
ai_confidence=0.0, ai_confidence=0.0,
negotiation_history=[], negotiation_history=[],
lead_id=data.lead_id,
sales_deal_id=data.sales_deal_id,
) )
db.add(deal) db.add(deal)
await db.flush() await db.flush()
@ -373,6 +419,313 @@ async def list_deals(
return [DealResponse.model_validate(d) for d in result.scalars().all()] return [DealResponse.model_validate(d) for d in result.scalars().all()]
@router.get("/operating-model")
async def get_operating_model(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Current AI operating mode and all mode definitions. | وضع التشغيل والأوصاف"""
mode = await ModeEnforcer.get_current_mode(str(current_user.tenant_id), db)
policy = ModeEnforcer.get_mode_policy(mode)
return {
"current": {
"mode": mode.value,
"name": mode.name,
"label_ar": policy.label_ar,
"description_ar": policy.description_ar,
"auto_send": policy.auto_send,
"auto_negotiate": policy.auto_negotiate,
"max_auto_commitment_sar": policy.max_auto_commitment_sar,
"allowed_channels": policy.allowed_channels,
},
"modes": ModeEnforcer.get_all_modes(),
"roles_ar": [
{"id": "owner", "label": "المالك", "scope": "تغيير وضع التشغيل، الالتزامات الكبرى"},
{"id": "revops", "label": "عمليات الإيرادات", "scope": "القمع، السياسات، التقارير"},
{"id": "partner_manager", "label": "مدير شراكات", "scope": "مسار الشراكات والتفاوض"},
{"id": "compliance", "label": "الامتثال", "scope": "الموافقات الحساسة والقطاعات المنظمة"},
],
"sla_hints_ar": {
"response_window": "الرد على العملاء المؤهلين خلال ٢٤–٤٨ ساعة عمل",
"followup_cap": "حد أقصى ٣ متابعات تلقائية ثم تصعيد بشري",
"opt_out": "احترام طلب التوقف فوراً وتسجيله في السجل",
},
}
@router.put("/operating-model")
async def set_operating_model(
data: OperatingModeSet,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Set tenant operating mode (stored on first company profile). | تعيين وضع التشغيل"""
try:
om = OperatingMode(data.mode)
except ValueError:
raise HTTPException(status_code=400, detail="وضع تشغيل غير صالح | Invalid mode")
try:
await ModeEnforcer.set_mode(str(current_user.tenant_id), om, db)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"status": "ok", "mode": om.value, "name": om.name}
@router.get("/taxonomy/deal-types")
async def taxonomy_deal_types():
"""Full 15-type partnership taxonomy for UI. | تصنيف أنواع الصفقات"""
return [t.model_dump() for t in DealTaxonomyService.get_all_types()]
@router.get("/taxonomy/deal-types/{type_id}")
async def taxonomy_deal_type_detail(type_id: str):
spec = DealTaxonomyService.get_deal_type(type_id)
if not spec:
raise HTTPException(status_code=404, detail="نوع غير معروف | Unknown type")
return spec.model_dump()
@router.get("/partner-archetypes")
async def partner_archetypes():
"""Map DB deal_type values to operational archetypes. | أنماط الشراكات التشغيلية"""
return {"archetypes": list_archetypes()}
@router.get("/playbooks")
async def playbooks_list():
"""Vertical playbooks (sector defaults). | قوالب قطاعية"""
return {
"ids": list_playbook_ids(),
"items": [get_playbook(i) for i in list_playbook_ids()],
}
@router.get("/playbooks/{playbook_id}")
async def playbook_detail(playbook_id: str):
pb = get_playbook(playbook_id)
if not pb:
raise HTTPException(status_code=404, detail="playbook not found")
return pb
@router.post("/policy/evaluate")
async def policy_evaluate(
data: PolicyEvaluateRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Graded policy: auto_execute | approval_required | blocked."""
result = await evaluate_action(
tenant_id=current_user.tenant_id,
channel=data.channel,
action=data.action,
deal_value_sar=data.deal_value_sar,
industry=data.industry,
db=db,
)
sp = suggested_playbook_for_industry(data.industry)
result["suggested_playbook_id"] = sp
return result
@router.get("/identity/graph")
async def identity_graph(
profile_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Counts and links for one company profile (light account graph)."""
pr = await db.execute(
select(CompanyProfile).where(
CompanyProfile.id == profile_id,
CompanyProfile.tenant_id == current_user.tenant_id,
)
)
profile = pr.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
deals_init = await db.execute(
select(func.count()).select_from(StrategicDeal).where(
StrategicDeal.tenant_id == current_user.tenant_id,
StrategicDeal.initiator_profile_id == profile_id,
)
)
deals_tgt = await db.execute(
select(func.count()).select_from(StrategicDeal).where(
StrategicDeal.tenant_id == current_user.tenant_id,
StrategicDeal.target_profile_id == profile_id,
)
)
matches_a = await db.execute(
select(func.count()).select_from(DealMatch).where(
DealMatch.tenant_id == current_user.tenant_id,
DealMatch.company_a_id == profile_id,
)
)
matches_b = await db.execute(
select(func.count()).select_from(DealMatch).where(
DealMatch.tenant_id == current_user.tenant_id,
DealMatch.company_b_id == profile_id,
)
)
linked_leads = await db.execute(
select(func.count()).select_from(StrategicDeal).where(
StrategicDeal.tenant_id == current_user.tenant_id,
(StrategicDeal.initiator_profile_id == profile_id)
| (StrategicDeal.target_profile_id == profile_id),
StrategicDeal.lead_id.isnot(None),
)
)
linked_sales = await db.execute(
select(func.count()).select_from(StrategicDeal).where(
StrategicDeal.tenant_id == current_user.tenant_id,
(StrategicDeal.initiator_profile_id == profile_id)
| (StrategicDeal.target_profile_id == profile_id),
StrategicDeal.sales_deal_id.isnot(None),
)
)
return {
"profile_id": str(profile_id),
"company_name": profile.company_name,
"suggested_playbook_id": suggested_playbook_for_industry(profile.industry),
"archetype_hint": archetype_for_deal_type("partnership"),
"counts": {
"strategic_deals_as_initiator": deals_init.scalar() or 0,
"strategic_deals_as_target": deals_tgt.scalar() or 0,
"matches_as_party_a": matches_a.scalar() or 0,
"matches_as_party_b": matches_b.scalar() or 0,
"deals_with_lead_link": linked_leads.scalar() or 0,
"deals_with_sales_deal_link": linked_sales.scalar() or 0,
},
}
@router.get("/governance/snapshot")
async def governance_snapshot(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""North-star style KPIs + policy posture for dashboards."""
mode = await ModeEnforcer.get_current_mode(str(current_user.tenant_id), db)
policy = ModeEnforcer.get_mode_policy(mode)
tenant_id = current_user.tenant_id
total_deals = (await db.execute(
select(func.count()).select_from(StrategicDeal).where(StrategicDeal.tenant_id == tenant_id)
)).scalar() or 0
hist_rows = (
await db.execute(
select(StrategicDeal.negotiation_history).where(StrategicDeal.tenant_id == tenant_id)
)
).all()
deals_with_history = sum(
1 for (h,) in hist_rows if isinstance(h, list) and len(h) > 0
)
return {
"operating_mode": {"value": mode.value, "name": mode.name, "label_ar": policy.label_ar},
"north_star_hints_ar": {
"touch_to_meeting": "تقليل الزمن من أول لمسة إلى اجتماع مؤهل",
"stage_conversion": "تحسين تحويل المراحل في القمع",
"partner_attribution": "مساهمة الشراكات في خط الأنابيب",
},
"governance_kpis": {
"auto_send_enabled": policy.auto_send,
"auto_negotiate_enabled": policy.auto_negotiate,
"max_auto_commitment_sar": policy.max_auto_commitment_sar,
"strategic_deals_total": total_deals,
"deals_with_negotiation_rounds": deals_with_history,
},
}
@router.get("/growth/checklist")
async def growth_ma_checklist():
"""Light M&A / expansion checklist (human decisions required)."""
return {
"disclaimer_ar": "قائمة إرشادية فقط — لا تغني عن مستشار قانوني أو مالي.",
"phases": [
{
"id": "thesis",
"title_ar": "أطروحة الاستثمار",
"items_ar": [
"تحديد القطاع والجغرافيا والحجم المستهدف",
"ربط الصفقة بأهداف الشركة الاستراتيجية (٣–٥ نقاط)",
],
},
{
"id": "screen",
"title_ar": "فرز أولي",
"items_ar": [
"تطبيق معايير إقصاء واضحة (حجم، نمو، تركيز)",
"تسجيل مصادر البيانات لكل هدف",
],
},
{
"id": "dd_lite",
"title_ar": "عناية واجبة خفيفة",
"items_ar": [
"المالية: إيرادات، هامش، تدفقات",
"التقنية والمنتج: نضج، ديون تقنية، IP",
"العملاء: تركيز، انحراف، مخاطر تجميع",
],
},
{
"id": "approval",
"title_ar": "موافقة وإغلاق داخلي",
"items_ar": [
"لجنة استثمار / مجلس إدارة حسب الحوكمة",
"توثيق الشروط الرئيسية قبل أي التزام",
],
},
],
}
@router.get("/agent-quality/snapshot")
async def agent_quality_snapshot(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Proxy metrics for QA / improvement loop (extend with real message logs later)."""
tenant_id = current_user.tenant_id
total = (await db.execute(
select(func.count()).select_from(StrategicDeal).where(StrategicDeal.tenant_id == tenant_id)
)).scalar() or 0
with_hist = (await db.execute(
select(StrategicDeal.negotiation_history).where(StrategicDeal.tenant_id == tenant_id)
)).all()
rounds = 0
for row in with_hist:
h = row[0] or []
if isinstance(h, list):
rounds += len(h)
avg_rounds = (rounds / total) if total else 0.0
high_conf = (await db.execute(
select(func.count()).select_from(StrategicDeal).where(
StrategicDeal.tenant_id == tenant_id,
StrategicDeal.ai_confidence >= 0.7,
)
)).scalar() or 0
return {
"labels_ar": {
"negotiation_depth": "عمق جولات التفاوض المسجّل",
"high_confidence_deals": "صفقات بثقة نموذج مرتفعة",
},
"strategic_deals_total": total,
"negotiation_rounds_total": rounds,
"avg_negotiation_rounds_per_deal": round(avg_rounds, 2),
"deals_high_ai_confidence": high_conf,
"loop_hints_ar": [
"اربط هذه المؤشرات لاحقاً بردود العملاء الفعلية ومعدلات التحويل",
"استخدم وضع «مسودات» عند ارتفاع معدل التصعيد",
],
}
@router.get("/{deal_id}", response_model=DealResponse) @router.get("/{deal_id}", response_model=DealResponse)
async def get_deal( async def get_deal(
deal_id: UUID, deal_id: UUID,
@ -392,6 +745,32 @@ async def get_deal(
return DealResponse.model_validate(deal) return DealResponse.model_validate(deal)
@router.patch("/{deal_id}/links", response_model=DealResponse)
async def patch_deal_links(
deal_id: UUID,
data: DealLinksUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Link strategic deal to CRM lead and/or sales deal. | ربط الصفقة بعميل محتمل أو صفقة مبيعات"""
result = await db.execute(
select(StrategicDeal).where(
StrategicDeal.id == deal_id,
StrategicDeal.tenant_id == current_user.tenant_id,
)
)
deal = result.scalar_one_or_none()
if not deal:
raise HTTPException(status_code=404, detail="الصفقة غير موجودة | Deal not found")
if data.lead_id is not None:
deal.lead_id = data.lead_id
if data.sales_deal_id is not None:
deal.sales_deal_id = data.sales_deal_id
await db.flush()
await db.refresh(deal)
return DealResponse.model_validate(deal)
# ── Negotiation ────────────────────────────────────────────────────────────── # ── Negotiation ──────────────────────────────────────────────────────────────

View File

@ -25,6 +25,33 @@ async def strategy_summary() -> dict:
"Measurable self-improvement loops when enabled", "Measurable self-improvement loops when enabled",
"OpenClaw-style durable flows + revision posture (see openclaw-config.yaml)", "OpenClaw-style durable flows + revision posture (see openclaw-config.yaml)",
], ],
"differentiators_verifiable": [
{
"id": "api_surface",
"title_ar": "مسارات API موثقة ومفتوحة للتحقق",
"evidence": "docs/API-MAP.md + OpenAPI /docs + scripts/verify_frontend_openapi_paths.py",
},
{
"id": "crm_sync",
"title_ar": "تكامل Salesforce/HubSpot مع اختبار ودفع وسحب",
"evidence": "POST /api/v1/integrations/crm/*/test|push-lead|pull-*",
},
{
"id": "llm_routing",
"title_ar": "توجيه نماذج حسب المهمة دون كشف مفاتيح",
"evidence": "GET/PUT /api/v1/ai/routing",
},
{
"id": "arabic_os",
"title_ar": "واجهة عربية ومسارات Dealix OS في منتج واحد",
"evidence": "dashboard hubs + docs/DEALIX_OS_PRODUCT_GUIDE_AR.md",
},
{
"id": "competitive_doc",
"title_ar": "مصفوفة تنافسية صريحة بدون أرقام غير مثبتة",
"evidence": "docs/COMPETITIVE_MATRIX_AR.md (نسخة ويب /strategy/COMPETITIVE_MATRIX_AR.md بعد المزامنة)",
},
],
"competitive_moat": { "competitive_moat": {
"durable_runtime": "OpenClaw 2026.4.2 pattern — checkpoints, retries, bounded plugins", "durable_runtime": "OpenClaw 2026.4.2 pattern — checkpoints, retries, bounded plugins",
"self_improvement": "6-phase loop: signals → diagnose → experiments → A/B → governance → promote/rollback", "self_improvement": "6-phase loop: signals → diagnose → experiments → A/B → governance → promote/rollback",
@ -90,6 +117,7 @@ async def strategy_summary() -> dict:
"full_markdown_web": "/strategy/DEALIX_NEXT_LEVEL_MASTER_PLAN_AR.md", "full_markdown_web": "/strategy/DEALIX_NEXT_LEVEL_MASTER_PLAN_AR.md",
"ultimate_execution_ar": "/strategy/ULTIMATE_EXECUTION_MASTER_AR.md", "ultimate_execution_ar": "/strategy/ULTIMATE_EXECUTION_MASTER_AR.md",
"integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md", "integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md",
"competitive_matrix_ar": "/strategy/COMPETITIVE_MATRIX_AR.md",
"investor_html": "/dealix-marketing/investor/00-investor-dealix-full-ar.html", "investor_html": "/dealix-marketing/investor/00-investor-dealix-full-ar.html",
}, },
"repo_paths": { "repo_paths": {
@ -98,4 +126,26 @@ async def strategy_summary() -> dict:
"ultimate_doc": "salesflow-saas/docs/ULTIMATE_EXECUTION_MASTER_AR.md", "ultimate_doc": "salesflow-saas/docs/ULTIMATE_EXECUTION_MASTER_AR.md",
"integration_master": "salesflow-saas/docs/INTEGRATION_MASTER_AR.md", "integration_master": "salesflow-saas/docs/INTEGRATION_MASTER_AR.md",
}, },
"dealix_os_three_pillars": {
"sales": {
"label_ar": "محرك المبيعات",
"focus": "اكتشاف، قمع، قنوات، إغلاق مع حوكمة إرسال",
"primary_api_surface": "leads, pipeline, inbox, agents",
},
"partnerships": {
"label_ar": "شراكات استراتيجية",
"focus": "ملفات B2B، مطابقة، تفاوض، Partnership Studio",
"primary_api_surface": "/api/v1/strategic-deals",
},
"growth": {
"label_ar": "نمو واستعداد استحواذ",
"focus": "ذكاء استراتيجي، قوائم مهام، قرار بشري للالتزامات الكبرى",
"primary_api_surface": "autonomous_core, strategy_summary, growth/checklist",
},
"governance": {
"label_ar": "حوكمة وثقة",
"focus": "أوضاع تشغيل، سياسات متدرجة، go-live، سجلات",
"primary_api_surface": "operating-model, policy/evaluate, go-live-gate",
},
},
} }

View File

@ -11,6 +11,9 @@ class Lead(BaseModel):
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True) tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
company_name = Column(String(255), nullable=True)
sector = Column(String(100), nullable=True)
city = Column(String(100), nullable=True)
phone = Column(String(20)) phone = Column(String(20))
email = Column(String(255)) email = Column(String(255))
source = Column(String(100)) # whatsapp, website, referral, social, phone source = Column(String(100)) # whatsapp, website, referral, social, phone

View File

@ -182,6 +182,10 @@ class StrategicDeal(TenantModel):
closed_at = Column(DateTime(timezone=True), nullable=True) closed_at = Column(DateTime(timezone=True), nullable=True)
# Links to core CRM entities (Sales OS — optional)
lead_id = Column(UUID(as_uuid=True), ForeignKey("leads.id"), nullable=True, index=True)
sales_deal_id = Column(UUID(as_uuid=True), ForeignKey("deals.id"), nullable=True, index=True)
# Relationships # Relationships
initiator_profile = relationship( initiator_profile = relationship(
"CompanyProfile", back_populates="initiated_deals", "CompanyProfile", back_populates="initiated_deals",
@ -191,6 +195,8 @@ class StrategicDeal(TenantModel):
"CompanyProfile", back_populates="targeted_deals", "CompanyProfile", back_populates="targeted_deals",
foreign_keys=[target_profile_id], foreign_keys=[target_profile_id],
) )
lead = relationship("Lead", foreign_keys=[lead_id])
sales_deal = relationship("Deal", foreign_keys=[sales_deal_id])
# ── Deal Match ─────────────────────────────────────────────────────────────── # ── Deal Match ───────────────────────────────────────────────────────────────

View File

@ -3,12 +3,15 @@ CRM Sync Service — Bidirectional sync with Salesforce, HubSpot, and generic CR
""" """
import uuid import uuid
from datetime import datetime, timezone
from typing import Optional from typing import Optional
import httpx import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings from app.config import get_settings
from app.models.tenant import Tenant
from app.services.salesforce_oauth import refresh_salesforce_access_token
settings = get_settings() settings = get_settings()
@ -32,9 +35,13 @@ class CRMSyncService:
if not access_token or not instance_url: if not access_token or not instance_url:
return {"status": "error", "message": "Invalid Salesforce credentials"} return {"status": "error", "message": "Invalid Salesforce credentials"}
fn = (lead.get("full_name") or lead.get("name") or "").strip()
parts = fn.split() if fn else []
first = parts[0] if parts else "Unknown"
last = parts[-1] if len(parts) > 1 else "."
sf_lead = { sf_lead = {
"FirstName": lead.get("full_name", "").split()[0] if lead.get("full_name") else "", "FirstName": first,
"LastName": lead.get("full_name", "").split()[-1] if lead.get("full_name") else "Unknown", "LastName": last,
"Phone": lead.get("phone", ""), "Phone": lead.get("phone", ""),
"Email": lead.get("email", ""), "Email": lead.get("email", ""),
"Company": lead.get("company_name", "Unknown"), "Company": lead.get("company_name", "Unknown"),
@ -65,10 +72,11 @@ class CRMSyncService:
access_token = credentials.get("access_token") access_token = credentials.get("access_token")
instance_url = credentials.get("instance_url") instance_url = credentials.get("instance_url")
query = "SELECT Id, FirstName, LastName, Phone, Email, Company, Industry, City, Rating FROM Lead" # SOQL: avoid injecting raw `since` without proper quoting — use full window + LIMIT
if since: query = (
query += f" WHERE LastModifiedDate > {since}" "SELECT Id, FirstName, LastName, Phone, Email, Company, Industry, City, Rating "
query += " ORDER BY LastModifiedDate DESC LIMIT 100" "FROM Lead ORDER BY LastModifiedDate DESC LIMIT 100"
)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.get( response = await client.get(
@ -100,8 +108,14 @@ class CRMSyncService:
"""Push a contact from Dealix to HubSpot.""" """Push a contact from Dealix to HubSpot."""
hs_contact = { hs_contact = {
"properties": { "properties": {
"firstname": lead.get("full_name", "").split()[0] if lead.get("full_name") else "", "firstname": (
"lastname": lead.get("full_name", "").split()[-1] if lead.get("full_name") else "", ((lead.get("full_name") or lead.get("name") or "").split() or [""])[0]
),
"lastname": (
((lead.get("full_name") or lead.get("name") or "").split() or ["", "."])[-1]
if len((lead.get("full_name") or lead.get("name") or "").split()) > 1
else "."
),
"phone": lead.get("phone", ""), "phone": lead.get("phone", ""),
"email": lead.get("email", ""), "email": lead.get("email", ""),
"company": lead.get("company_name", ""), "company": lead.get("company_name", ""),
@ -209,11 +223,19 @@ class CRMSyncService:
for ext_lead in external_leads: for ext_lead in external_leads:
try: try:
em = (ext_lead.get("email") or "").strip()
if em:
existing = await lead_svc.get_lead_by_email(tenant_id, em)
if existing:
continue
name = ext_lead.get("full_name") or "Unknown"
if not name.strip():
name = "Unknown"
await lead_svc.create_lead( await lead_svc.create_lead(
tenant_id=tenant_id, tenant_id=tenant_id,
full_name=ext_lead["full_name"], full_name=name,
phone=ext_lead.get("phone", ""), phone=ext_lead.get("phone", ""),
email=ext_lead.get("email", ""), email=em,
company_name=ext_lead.get("company_name", ""), company_name=ext_lead.get("company_name", ""),
sector=ext_lead.get("sector", ""), sector=ext_lead.get("sector", ""),
city=ext_lead.get("city", ""), city=ext_lead.get("city", ""),
@ -236,19 +258,72 @@ class CRMSyncService:
# ── Helpers ─────────────────────────────────── # ── Helpers ───────────────────────────────────
async def _get_crm_credentials(self, tenant_id: str, provider: str) -> Optional[dict]: async def _get_crm_credentials(self, tenant_id: str, provider: str) -> Optional[dict]:
"""Get CRM credentials from tenant settings.""" """Resolve CRM credentials: tenant.settings.crm overrides global env."""
# In production, this would fetch from encrypted tenant settings tid = uuid.UUID(tenant_id)
result = await self.db.execute(select(Tenant).where(Tenant.id == tid))
tenant = result.scalar_one_or_none()
tset = dict(tenant.settings or {}) if tenant else {}
crm = dict(tset.get("crm") or {})
if provider == "salesforce": if provider == "salesforce":
return { sf = dict(crm.get("salesforce") or {})
"access_token": settings.SALESFORCE_CLIENT_SECRET, client_id = (sf.get("client_id") or settings.SALESFORCE_CLIENT_ID or "").strip()
"instance_url": "", client_secret = (sf.get("client_secret") or settings.SALESFORCE_CLIENT_SECRET or "").strip()
} if settings.SALESFORCE_CLIENT_ID else None refresh_token = (sf.get("refresh_token") or settings.SALESFORCE_REFRESH_TOKEN or "").strip()
elif provider == "hubspot": domain_host = (sf.get("domain") or settings.SALESFORCE_DOMAIN or "login.salesforce.com").strip()
return { if not client_id or not client_secret or not refresh_token:
"api_key": settings.HUBSPOT_API_KEY, return None
} if settings.HUBSPOT_API_KEY else None try:
tok = await refresh_salesforce_access_token(
domain_host=domain_host,
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
)
return {
"access_token": tok["access_token"],
"instance_url": tok["instance_url"],
}
except Exception:
return None
if provider == "hubspot":
hs = dict(crm.get("hubspot") or {})
token = (hs.get("private_app_token") or hs.get("access_token") or settings.HUBSPOT_API_KEY or "").strip()
if not token:
return None
return {"api_key": token}
return None return None
async def salesforce_identity_probe(self, credentials: dict) -> dict:
"""Lightweight Salesforce API probe (limits resource)."""
access_token = credentials.get("access_token")
instance_url = credentials.get("instance_url")
if not access_token or not instance_url:
return {"ok": False, "error": "missing_token_or_instance"}
async with httpx.AsyncClient() as client:
r = await client.get(
f"{instance_url}/services/data/{settings.SALESFORCE_API_VERSION}/limits",
headers={"Authorization": f"Bearer {access_token}"},
timeout=25.0,
)
if r.status_code == 200:
return {"ok": True, "status_code": r.status_code}
return {"ok": False, "status_code": r.status_code, "detail": r.text[:300]}
async def hubspot_identity_probe(self, api_key: str) -> dict:
async with httpx.AsyncClient() as client:
r = await client.get(
"https://api.hubapi.com/crm/v3/objects/contacts",
params={"limit": 1},
headers={"Authorization": f"Bearer {api_key}"},
timeout=25.0,
)
if r.status_code == 200:
return {"ok": True, "status_code": r.status_code}
return {"ok": False, "status_code": r.status_code, "detail": r.text[:300]}
@staticmethod @staticmethod
def _score_to_sf_rating(score: int) -> str: def _score_to_sf_rating(score: int) -> str:
if score >= 80: if score >= 80:

View File

@ -0,0 +1,14 @@
from app.services.dealix_os.vertical_playbooks import VERTICAL_PLAYBOOKS, get_playbook, list_playbook_ids
from app.services.dealix_os.partner_archetypes import ARCHETYPE_MAP, list_archetypes, archetype_for_deal_type
from app.services.dealix_os.policy_engine import evaluate_action, suggested_playbook_for_industry
__all__ = [
"VERTICAL_PLAYBOOKS",
"get_playbook",
"list_playbook_ids",
"ARCHETYPE_MAP",
"list_archetypes",
"archetype_for_deal_type",
"evaluate_action",
"suggested_playbook_for_industry",
]

View File

@ -0,0 +1,77 @@
"""
Maps core deal_type values (DB enum) to partnership archetypes and taxonomy hints.
يربط نوع الصفقة المخزّن بأنماط شراكة تشغيلية أوضح للواجهة والوكلاء.
"""
from typing import Any
# Strategic deal deal_type column uses DealType enum strings; taxonomy uses richer IDs.
ARCHETYPE_MAP: dict[str, dict[str, Any]] = {
"partnership": {
"archetype_id": "strategic_alliance",
"label_ar": "شراكة عامة / تحالف",
"label_en": "General partnership / alliance",
"description_ar": "تعاون مرن قد يتطور إلى تسويق مشترك أو بيع مشترك.",
"default_taxonomy_ids": ["strategic_alliance", "co_marketing", "co_selling"],
"negotiation_focus_ar": ["الأهداف المشتركة", "مدة التعاون", "مشاركة العملاء المحتملين"],
},
"distribution": {
"archetype_id": "channel",
"label_ar": "قناة توزيع",
"label_en": "Channel / distribution",
"description_ar": "الوصول لعملاء الشريك عبر شبكة بيع أو توزيع.",
"default_taxonomy_ids": ["channel_partnership", "reseller"],
"negotiation_focus_ar": ["المنطقة", "الهامش", "أهداف الأداء", "التدريب"],
},
"franchise": {
"archetype_id": "expansion",
"label_ar": "امتياز / توسع علامة",
"label_en": "Franchise / brand expansion",
"description_ar": "تكرار نموذج عمل تحت علامة موحّدة مع حوكمة أعلى.",
"default_taxonomy_ids": ["strategic_alliance", "joint_venture"],
"negotiation_focus_ar": ["الرسوم", "الامتثال للعلامة", "الأداء الإقليمي"],
},
"jv": {
"archetype_id": "jv",
"label_ar": "مشروع مشترك (JV)",
"label_en": "Joint venture",
"description_ar": "كيان أو مشروع مشترك مع مساهمات ورقابة من الطرفين.",
"default_taxonomy_ids": ["joint_venture", "tender_consortium"],
"negotiation_focus_ar": ["نسب الملكية", "الحوكمة", "الخروج", "توزيع الأرباح"],
},
"referral": {
"archetype_id": "referral",
"label_ar": "إحالة وعمولة",
"label_en": "Referral",
"description_ar": "إحالة عملاء مؤهلين مقابل عمولة أو مقايضة قيمة.",
"default_taxonomy_ids": ["referral_partnership"],
"negotiation_focus_ar": ["نسبة العمولة", "التتبع", "تعريف العميل المؤهل"],
},
"acquisition": {
"archetype_id": "corp_dev",
"label_ar": "نمو واستحواذ (مساعد قرار)",
"label_en": "Growth / M&A assist",
"description_ar": "استكشاف وتأهيل أهداف؛ القرار النهائي والعناية الواجبة بشرية.",
"default_taxonomy_ids": ["acquisition_scouting", "investment_intro"],
"negotiation_focus_ar": ["معايير الهدف", "الخصومية", "نطاق الفحص المبدئي"],
},
"barter": {
"archetype_id": "barter",
"label_ar": "مقايضة / تبادل خدمات",
"label_en": "Barter / service exchange",
"description_ar": "تبادل قيمة بدون تدفق نقدي كامل أو مع دفعات جزئية.",
"default_taxonomy_ids": ["service_barter"],
"negotiation_focus_ar": ["تعادل القيمة", "نطاق التسليم", "مدة الالتزام"],
},
}
def list_archetypes() -> list[dict[str, Any]]:
return [
{"deal_type": k, **v}
for k, v in ARCHETYPE_MAP.items()
]
def archetype_for_deal_type(deal_type: str) -> dict[str, Any] | None:
return ARCHETYPE_MAP.get(deal_type)

View File

@ -0,0 +1,110 @@
"""
Graded policy evaluation: auto_execute | approval_required | blocked.
محرك سياسات متدرج مرتبط بوضع التشغيل والقطاع والقناة والقيمة.
"""
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.strategic_deals.operating_modes import ModeEnforcer, OperatingMode, MODE_POLICIES
from app.services.dealix_os.vertical_playbooks import VERTICAL_PLAYBOOKS
REGULATED_SECTORS = frozenset(
{"healthcare", "pharma", "medical", "finance", "banking", "insurance", "real_estate"}
)
def _normalize_industry(industry: str | None) -> str:
if not industry:
return ""
return industry.strip().lower().replace(" ", "_")
async def evaluate_action(
*,
tenant_id,
channel: str,
action: str,
deal_value_sar: float,
industry: str | None,
db: AsyncSession,
) -> dict:
mode = await ModeEnforcer.get_current_mode(str(tenant_id), db)
policy = MODE_POLICIES.get(mode)
if not policy:
return {
"level": "blocked",
"reason_ar": "وضع تشغيل غير معرّف",
"mode": mode.value,
"mode_name": mode.name,
}
ind = _normalize_industry(industry)
is_regulated = any(s in ind for s in REGULATED_SECTORS) or ind in REGULATED_SECTORS
channel_ok, ch_msg = await ModeEnforcer.check_channel(mode, channel)
if not channel_ok:
return {
"level": "blocked",
"reason_ar": ch_msg,
"mode": mode.value,
"mode_name": mode.name,
"channel": channel,
"action": action,
}
action_ok, act_msg = await ModeEnforcer.check_action(mode, action, deal_value_sar, db)
if not action_ok:
return {
"level": "approval_required",
"reason_ar": act_msg,
"mode": mode.value,
"mode_name": mode.name,
"channel": channel,
"action": action,
}
# Extra gates: regulated + high-touch channels
if is_regulated and channel == "whatsapp" and policy.auto_send:
return {
"level": "approval_required",
"reason_ar": "قطاع حساس: يُفضّل موافقة بشرية قبل إرسال واتساب.",
"mode": mode.value,
"mode_name": mode.name,
"channel": channel,
"action": action,
}
if action in {"send_custom_message", "run_outreach_campaign"} and deal_value_sar > policy.max_auto_commitment_sar > 0:
return {
"level": "approval_required",
"reason_ar": "قيمة الصفقة تتجاوز حد الالتزام التلقائي لوضع التشغيل الحالي.",
"mode": mode.value,
"mode_name": mode.name,
"channel": channel,
"action": action,
}
return {
"level": "auto_execute",
"reason_ar": "مسموح ضمن السياسات الحالية.",
"mode": mode.value,
"mode_name": mode.name,
"channel": channel,
"action": action,
}
def suggested_playbook_for_industry(industry: str | None) -> str | None:
"""Pick a default vertical playbook id from coarse industry string."""
ind = _normalize_industry(industry)
if not ind:
return None
if any(x in ind for x in ("عقار", "real", "estate", "property")):
return "real_estate"
if any(x in ind for x in ("صح", "health", "medical", "clinic", "hospital")):
return "healthcare"
if any(x in ind for x in ("saas", "software", "tech", "برمج")):
return "saas_b2b"
if any(x in ind for x in ("consult", "legal", "محاسب", "service")):
return "professional_services"
return None

View File

@ -0,0 +1,61 @@
"""
Vertical playbooks sector defaults for ICP, channels, and governance hints.
طبقات إعداد قطاعية تغذي الوكلاء والواجهة دون تعديل كود لكل عميل.
"""
from typing import Any
VERTICAL_PLAYBOOKS: dict[str, dict[str, Any]] = {
"real_estate": {
"id": "real_estate",
"label_ar": "العقار والتطوير",
"label_en": "Real estate & development",
"icp_hints_ar": ["مطورون، وكلاء عقاريون، صناديق عقارية، إدارة أملاك"],
"primary_channels": ["whatsapp", "email"],
"objection_patterns_ar": ["العمولة", "حصرية الحي", "زمن البيع", "الترخيص"],
"approval_value_threshold_sar": 250_000,
"suggested_deal_types": ["channel_partnership", "referral_partnership", "co_marketing"],
"compliance_notes_ar": ["تأكد من إعلانات الهيئة العقارية عند الاقتضاء", "لا التزامات تسعير نهائي بدون موافقة"],
},
"healthcare": {
"id": "healthcare",
"label_ar": "الرعاية الصحية",
"label_en": "Healthcare",
"icp_hints_ar": ["مستشفيات، عيادات، موردو أجهزة، منصات صحية"],
"primary_channels": ["email", "in_person"],
"objection_patterns_ar": ["الترخيص SFDA", "خصوصية البيانات", "التكامل مع أنظمة المستشفى"],
"approval_value_threshold_sar": 100_000,
"suggested_deal_types": ["subcontracting", "white_label", "strategic_alliance"],
"compliance_notes_ar": ["قطاع حساس — افتراض موافقة بشرية قبل إرسال واتساب", "تجنب ادعاءات علاجية في المحتوى الآلي"],
},
"saas_b2b": {
"id": "saas_b2b",
"label_ar": "SaaS وB2B تقني",
"label_en": "B2B SaaS / technology",
"icp_hints_ar": ["شركات برمجيات، شركاء تكامل، موزعون، استشاريو تحول رقمي"],
"primary_channels": ["email", "linkedin", "whatsapp"],
"objection_patterns_ar": ["الأمان", "SLA", "التكامل", "التسعير حسب المقعد"],
"approval_value_threshold_sar": 150_000,
"suggested_deal_types": ["reseller", "co_selling", "white_label", "channel_partnership"],
"compliance_notes_ar": ["مراجعات DPA عند مشاركة بيانات عملاء"],
},
"professional_services": {
"id": "professional_services",
"label_ar": "الخدمات المهنية",
"label_en": "Professional services",
"icp_hints_ar": ["محاسبة، قانون، استشارات إدارية، تدريب"],
"primary_channels": ["email", "linkedin"],
"objection_patterns_ar": ["نطاق الخدمة", "الاستشاري المسؤول", "السرية المهنية"],
"approval_value_threshold_sar": 80_000,
"suggested_deal_types": ["referral_partnership", "co_selling", "capability_gap_fill"],
"compliance_notes_ar": ["لا تقديم استشارة قانونية/مالية كأمر تنفيذي آلي"],
},
}
def list_playbook_ids() -> list[str]:
return list(VERTICAL_PLAYBOOKS.keys())
def get_playbook(playbook_id: str) -> dict[str, Any] | None:
return VERTICAL_PLAYBOOKS.get(playbook_id)

View File

@ -0,0 +1,73 @@
"""Probe external integrations and persist status on IntegrationSyncState."""
from __future__ import annotations
import logging
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.crm_sync_service import CRMSyncService
from app.services.operations_hub import upsert_connector_status
logger = logging.getLogger("dealix.integration_probe")
async def probe_and_persist_crm_connectors(db: AsyncSession, tenant_id: UUID) -> None:
"""Update crm_salesforce / crm_hubspot connector rows from live probes (best-effort)."""
svc = CRMSyncService(db)
tid = str(tenant_id)
# Salesforce
try:
creds = await svc._get_crm_credentials(tid, "salesforce")
if not creds:
await upsert_connector_status(
db, tenant_id, "crm_salesforce", status="unknown",
)
else:
probe = await svc.salesforce_identity_probe(creds)
if probe.get("ok"):
await upsert_connector_status(
db, tenant_id, "crm_salesforce", status="ok", success=True,
)
else:
await upsert_connector_status(
db,
tenant_id,
"crm_salesforce",
status="error",
last_error=str(probe.get("detail") or probe)[:500],
)
except Exception as e:
logger.exception("Salesforce probe failed")
await upsert_connector_status(
db, tenant_id, "crm_salesforce", status="error", last_error=str(e)[:500],
)
# HubSpot
try:
creds = await svc._get_crm_credentials(tid, "hubspot")
if not creds:
await upsert_connector_status(
db, tenant_id, "crm_hubspot", status="unknown",
)
else:
probe = await svc.hubspot_identity_probe(creds.get("api_key", ""))
if probe.get("ok"):
await upsert_connector_status(
db, tenant_id, "crm_hubspot", status="ok", success=True,
)
else:
await upsert_connector_status(
db,
tenant_id,
"crm_hubspot",
status="error",
last_error=str(probe.get("detail") or probe)[:500],
)
except Exception as e:
logger.exception("HubSpot probe failed")
await upsert_connector_status(
db, tenant_id, "crm_hubspot", status="error", last_error=str(e)[:500],
)

View File

@ -39,12 +39,12 @@ class LeadService:
lead = Lead( lead = Lead(
id=uuid.uuid4(), id=uuid.uuid4(),
tenant_id=uuid.UUID(tenant_id), tenant_id=uuid.UUID(tenant_id),
full_name=full_name, name=full_name,
phone=phone, phone=phone,
email=email, email=email,
company_name=company_name, company_name=company_name or None,
sector=sector, sector=sector or None,
city=city, city=city or None,
source=source, source=source,
status="new", status="new",
score=0, score=0,
@ -156,6 +156,47 @@ class LeadService:
await self.db.flush() await self.db.flush()
return self._to_dict(lead) return self._to_dict(lead)
async def get_lead_by_email(self, tenant_id: str, email: str) -> Optional[dict]:
from app.models.lead import Lead
if not (email or "").strip():
return None
normalized = email.strip().lower()
result = await self.db.execute(
select(Lead)
.where(
Lead.tenant_id == uuid.UUID(tenant_id),
func.lower(Lead.email) == normalized,
)
.limit(1)
)
lead = result.scalars().first()
return self._to_dict(lead) if lead else None
async def merge_lead_extra_metadata(
self, tenant_id: str, lead_id: str, crm_patch: dict
) -> Optional[dict]:
"""Merge into extra_metadata.crm (e.g. salesforce_lead_id, hubspot_contact_id)."""
from app.models.lead import Lead
result = await self.db.execute(
select(Lead).where(
Lead.id == uuid.UUID(lead_id),
Lead.tenant_id == uuid.UUID(tenant_id),
)
)
lead = result.scalar_one_or_none()
if not lead:
return None
base = dict(lead.extra_metadata or {})
crm = dict(base.get("crm") or {})
crm.update(crm_patch)
base["crm"] = crm
lead.extra_metadata = base
lead.updated_at = datetime.now(timezone.utc)
await self.db.flush()
return self._to_dict(lead)
async def get_lead_by_phone(self, tenant_id: str, phone: str) -> Optional[dict]: async def get_lead_by_phone(self, tenant_id: str, phone: str) -> Optional[dict]:
from app.models.lead import Lead from app.models.lead import Lead
@ -371,6 +412,7 @@ class LeadService:
def _to_dict(lead) -> dict: def _to_dict(lead) -> dict:
if not lead: if not lead:
return {} return {}
em = lead.extra_metadata if getattr(lead, "extra_metadata", None) else {}
return { return {
"id": str(lead.id), "id": str(lead.id),
"tenant_id": str(lead.tenant_id), "tenant_id": str(lead.tenant_id),
@ -378,15 +420,16 @@ class LeadService:
"source": lead.source, "source": lead.source,
"status": lead.status, "status": lead.status,
"score": lead.score, "score": lead.score,
"full_name": lead.full_name, "full_name": lead.name,
"phone": lead.phone, "phone": lead.phone,
"email": lead.email, "email": lead.email,
"company_name": lead.company_name, "company_name": getattr(lead, "company_name", None) or em.get("company_name", ""),
"sector": lead.sector, "sector": getattr(lead, "sector", None) or em.get("sector", ""),
"city": lead.city, "city": getattr(lead, "city", None) or em.get("city", ""),
"notes": lead.notes, "notes": lead.notes,
"qualified_at": lead.qualified_at.isoformat() if lead.qualified_at else None, "extra_metadata": dict(em) if em else {},
"converted_at": lead.converted_at.isoformat() if lead.converted_at else None, "qualified_at": lead.qualified_at.isoformat() if getattr(lead, "qualified_at", None) else None,
"converted_at": lead.converted_at.isoformat() if getattr(lead, "converted_at", None) else None,
"created_at": lead.created_at.isoformat() if lead.created_at else None, "created_at": lead.created_at.isoformat() if lead.created_at else None,
"updated_at": lead.updated_at.isoformat() if lead.updated_at else None, "updated_at": lead.updated_at.isoformat() if lead.updated_at else None,
} }

View File

@ -58,6 +58,7 @@ async def count_pending_approvals(db: AsyncSession, tenant_id: UUID) -> int:
_DEFAULT_CONNECTORS: List[Dict[str, str]] = [ _DEFAULT_CONNECTORS: List[Dict[str, str]] = [
{"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown"}, {"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown"},
{"connector_key": "crm_hubspot", "display_name_ar": "HubSpot CRM", "status": "unknown"},
{"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown"}, {"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown"},
{"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown"}, {"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown"},
{"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown"}, {"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown"},

View File

@ -0,0 +1,53 @@
"""Salesforce OAuth2 refresh-token flow for REST API calls."""
from __future__ import annotations
import logging
import httpx
logger = logging.getLogger("dealix.salesforce_oauth")
def salesforce_token_url(domain_host: str) -> str:
d = (domain_host or "login.salesforce.com").strip().rstrip("/")
if d.startswith("http://") or d.startswith("https://"):
return f"{d}/services/oauth2/token"
return f"https://{d}/services/oauth2/token"
async def refresh_salesforce_access_token(
*,
domain_host: str,
client_id: str,
client_secret: str,
refresh_token: str,
) -> dict:
"""
Exchange refresh token for access_token + instance_url.
Returns dict with access_token, instance_url, and optional issued fields.
"""
url = salesforce_token_url(domain_host)
async with httpx.AsyncClient() as client:
response = await client.post(
url,
data={
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=45.0,
)
if response.status_code != 200:
logger.warning("Salesforce token refresh failed: %s", response.text[:500])
response.raise_for_status()
data = response.json()
instance = (data.get("instance_url") or "").rstrip("/")
if not data.get("access_token") or not instance:
raise ValueError("Salesforce token response missing access_token or instance_url")
return {
"access_token": data["access_token"],
"instance_url": instance,
}

View File

@ -1,9 +1,19 @@
import asyncio import asyncio
import os import os
from pathlib import Path
# JWT-based API tests require this gate to be off (production may set .env). # JWT-based API tests require this gate to be off (production may set .env).
os.environ["DEALIX_INTERNAL_API_TOKEN"] = "" os.environ["DEALIX_INTERNAL_API_TOKEN"] = ""
# Fresh SQLite schema per pytest session — avoids stale ./dealix.db missing new columns.
_backend_root = Path(__file__).resolve().parent.parent
_test_sqlite = _backend_root / ".pytest_dealix.sqlite"
try:
_test_sqlite.unlink(missing_ok=True)
except OSError:
pass
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///" + _test_sqlite.resolve().as_posix()
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from httpx import AsyncClient, ASGITransport from httpx import AsyncClient, ASGITransport

View File

@ -0,0 +1,80 @@
"""Smoke tests for CRM status and AI routing (JWT bypass via dependency override)."""
import uuid
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from app.api.deps import get_current_user
from app.database import async_session
from app.main import app
from app.models.tenant import Tenant
from app.models.user import User
@pytest_asyncio.fixture
async def owner_user_id():
suffix = uuid.uuid4().hex[:10]
async with async_session() as db:
tenant = Tenant(
name=f"IntTest {suffix}",
slug=f"inttest-{suffix}",
email=f"tenant-{suffix}@example.com",
)
db.add(tenant)
await db.flush()
user = User(
tenant_id=tenant.id,
email=f"owner-{suffix}@example.com",
password_hash="$2b$12$dummyNotForLoginxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
full_name="Owner",
role="owner",
)
db.add(user)
await db.commit()
uid = str(user.id)
yield uid
def _user_override(user_id: str):
async def _dep():
async with async_session() as db:
row = (await db.execute(select(User).where(User.id == user_id))).scalar_one()
return row
return _dep
@pytest.mark.asyncio
async def test_integrations_crm_status_shape(owner_user_id):
app.dependency_overrides[get_current_user] = _user_override(owner_user_id)
try:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/api/v1/integrations/crm/status")
assert r.status_code == 200, r.text
data = r.json()
assert "salesforce" in data and "hubspot" in data
assert "env_refresh_configured" in data["salesforce"]
assert "docs" in data
finally:
app.dependency_overrides.pop(get_current_user, None)
@pytest.mark.asyncio
async def test_ai_routing_shape(owner_user_id):
app.dependency_overrides[get_current_user] = _user_override(owner_user_id)
try:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/api/v1/ai/routing")
assert r.status_code == 200, r.text
data = r.json()
assert "effective" in data
assert "available_providers" in data
assert isinstance(data["effective"], dict)
assert "note_ar" in data
finally:
app.dependency_overrides.pop(get_current_user, None)

View File

@ -231,6 +231,17 @@ Action Handler Human Handoff
Log to ai_conversations Log to ai_conversations
``` ```
## LLM routing policy (per tenant)
وكلاء الجدول أعلاه يستهلكون نماذج LLM عبر طبقة التطبيق. **اختيار المزود والنموذج لكل فئة مهمة** (مثل استكشاف، تفاوض، امتثال، ملخص استراتيجي، تضمينات) يُخزَّن في `tenant.settings["llm_routing"]` ويُعرض ويُحدَّث عبر واجهة موحّدة:
| Method | Path | ملاحظة |
|--------|------|--------|
| GET | `/api/v1/ai/routing` | خريطة فعّالة + قائمة `available_providers` (بدون مفاتيح API) |
| PUT | `/api/v1/ai/routing` | تحديث جزئي لسياسة المستأجر (صلاحيات owner / manager / admin) |
تفاصيل الحقول والمسارات المجاورة: [`API-MAP.md`](API-MAP.md) (قسم AI routing). عند إضافة وكيل جديد، اربط نوع مهمته بأقرب مفتاح في سياسة التوجيه حتى يبقى السلوك قابلاً للضبط من لوحة واحدة.
## Agent Configuration ## Agent Configuration
Each agent is defined in `ai-agents/` with: Each agent is defined in `ai-agents/` with:

View File

@ -272,3 +272,62 @@ All routes are prefixed with `/api/v1`. Authentication is required unless marked
| GET | `/health` | Basic health check `[public]` | | GET | `/health` | Basic health check `[public]` |
| GET | `/health/ready` | Readiness (DB + Redis) `[public]` | | GET | `/health/ready` | Readiness (DB + Redis) `[public]` |
| GET | `/health/version` | App version `[public]` | | GET | `/health/version` | App version `[public]` |
## Strategic Deals (Dealix OS / B2B)
Prefix: `/strategic-deals`. Company profiles, matches, deals, negotiation, governance.
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/strategic-deals/profiles` | List tenant company profiles (paginated) |
| POST | `/strategic-deals/profiles` | Create company profile |
| PUT | `/strategic-deals/profiles/{id}/enrich` | AI-enrich profile |
| POST | `/strategic-deals/profiles/{id}/analyze-needs` | Needs analysis |
| GET | `/strategic-deals/matches` | List AI matches |
| POST | `/strategic-deals/matches/{id}/approve` | Approve match |
| POST | `/strategic-deals/scan` | Discovery scan |
| POST | `/strategic-deals/barter-scan` | Barter chain scan |
| POST | `/strategic-deals` | Create strategic deal (`lead_id`, `sales_deal_id` optional) |
| GET | `/strategic-deals` | List deals (filters: status, deal_type, profile_id) |
| GET | `/strategic-deals/operating-model` | Current OS mode + all modes + roles/SLA hints |
| PUT | `/strategic-deals/operating-model` | Set operating mode (04) on tenant profile |
| GET | `/strategic-deals/taxonomy/deal-types` | 15-type partnership taxonomy |
| GET | `/strategic-deals/taxonomy/deal-types/{type_id}` | One taxonomy type |
| GET | `/strategic-deals/partner-archetypes` | Map `deal_type` → operational archetypes |
| GET | `/strategic-deals/playbooks` | Vertical sector playbooks |
| GET | `/strategic-deals/playbooks/{id}` | One playbook |
| POST | `/strategic-deals/policy/evaluate` | Graded policy: auto_execute / approval_required / blocked |
| GET | `/strategic-deals/identity/graph` | Light account graph (`profile_id` query) |
| GET | `/strategic-deals/governance/snapshot` | Governance KPIs + operating mode |
| GET | `/strategic-deals/growth/checklist` | M&A-style checklist (guidance) |
| GET | `/strategic-deals/agent-quality/snapshot` | Agent quality proxy metrics |
| GET | `/strategic-deals/{deal_id}` | Deal detail |
| PATCH | `/strategic-deals/{deal_id}/links` | Link CRM `lead_id` / `sales_deal_id` |
| PUT | `/strategic-deals/{deal_id}/negotiate` | Negotiation round |
| POST | `/strategic-deals/{deal_id}/outreach` | Outreach |
| POST | `/strategic-deals/{deal_id}/proposal` | Generate proposal |
| POST | `/strategic-deals/{deal_id}/term-sheet` | Term sheet |
| GET | `/strategic-deals/analytics/overview` | Deal analytics |
## Integrations (CRM)
Prefix: `/integrations`. Salesforce / HubSpot sync and health (JWT).
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/integrations/crm/status` | Config presence + last probe (no secrets) |
| POST | `/integrations/crm/salesforce/test` | Probe Salesforce token/API |
| POST | `/integrations/crm/salesforce/push-lead/{lead_id}` | Push one lead to Salesforce |
| POST | `/integrations/crm/salesforce/pull-leads` | Pull leads (optional `since`) |
| POST | `/integrations/crm/hubspot/test` | Probe HubSpot API |
| POST | `/integrations/crm/hubspot/push-lead/{lead_id}` | Push one contact to HubSpot |
| POST | `/integrations/crm/hubspot/pull-contacts` | Pull contacts page |
## AI routing (tenant)
Prefix: `/ai`. Model routing policy per task category (no API keys in response).
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/ai/routing` | Effective routing map for current tenant |
| PUT | `/ai/routing` | Update tenant `settings.llm_routing` (owner/manager) |

View File

@ -0,0 +1,35 @@
# مصفوفة تنافسية — Dealix مقابل منصات الإيرادات والـ CRM
هذا المستند يقارن **قدرات المنتج كما تظهر في المستودع** (واجهة، API، تكاملات، حوكمة) مع فئات أدوات السوق الشائعة. لا تُذكر أرقام سوق أو معدلات اعتماد غير مثبتة.
## الصفوف: محاور Dealix
| المحور | Dealix (ما يُثبت في الكود) |
|--------|---------------------------|
| حوكمة وإرسال | مسارات سياسات، موافقات، سجلات؛ تكامل مع `go-live-gate` ووثائق التشغيل |
| واتساب والقنوات المحلية | مسارات بريد/واتساب في التكاملات؛ تجربة عربية RTL في الواجهة |
| شراكات B2B | `strategic-deals`، Partnership Studio، هوية وصفقات استراتيجية |
| نظام تشغيل ثلاثي | أعمدة المبيعات / الشراكات / النمو في الداشبورد و`strategy_summary` |
| تكاملات CRM | Salesforce (OAuth + refresh + push/pull)، HubSpot (token + push/pull)، حالة في `integrations/crm` و`operations` |
| ذكاء متعدد المزودين | `GET/PUT /api/v1/ai/routing` — نماذج حسب نوع المهمة دون كشف مفاتيح |
## الأعمدة: فئات منافسين (مرجعية)
| الفئة | مثال مرجعي | ملاحظة مقارنة |
|-------|------------|----------------|
| Salesforce Sales Cloud | منصة CRM عالمية واسعة | Dealix لا يستبدل كل وحدات Salesforce؛ يُغطى **التنسيق والمزامنة** مع Leads/Contacts حسب التكامل الحالي |
| HubSpot CRM | تسويق وCRM مدمج | نفس نمط **الوجهة/المصدر** عبر API HubSpot في الخدمة الموحدة |
| منصات Revenue / GTM orchestration | أدلة مشتري عامة (Fullcast، Revenue.io، إلخ) | Dealix يركّز على **سوق سعودي أولاً**، حوكمة، ومسارات شراكات B2B ضمن منتج واحد |
## فروقات قابلة للإثبات (بدون أرقام ادعائية)
1. **شفافية تقنية:** مسارات API موثقة في `docs/API-MAP.md` ومطابقة OpenAPI عبر `scripts/verify_frontend_openapi_paths.py`.
2. **تكامل CRM قابل للتشغيل:** نقاط `test` و`push` و`pull` تحت `/api/v1/integrations/crm/`.
3. **سياسة نماذج:** توجيه المهام (`discovery`, `negotiation`, …) عبر `/api/v1/ai/routing` مع بقاء الأسرار في الخادم.
4. **قصة منتج موحدة:** `docs/DEALIX_OS_PRODUCT_GUIDE_AR.md` و`/strategy/summary` يربطان الواجهة بالوثائق.
## روابط
- [دليل التكاملات](INTEGRATION_MASTER_AR.md)
- [خريطة API](API-MAP.md)
- [قائمة الإطلاق](LAUNCH_CHECKLIST.md)

View File

@ -0,0 +1,39 @@
# دليل منتج Dealix OS — مسارات المستخدمين
يربط هذا الملف بين **واجهة الداشبورد**، **واجهات البرمجة**، والوثائق التفصيلية. للتكاملات والبيئة راجع [`INTEGRATION_MASTER_AR.md`](INTEGRATION_MASTER_AR.md) و[`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md).
## 1) لمن هذا المنتج؟
| الجمهور | المحور في الداشبورد | مرجع تقني |
|--------|----------------------|-----------|
| الإدارة والمالك | المنصة والحوكمة | [`strategic_deals` operating-model](API-MAP.md)، [`go-live-gate`](../backend/app/services/go_live_matrix.py) |
| فريق المبيعات | محرك المبيعات | Leads، Pipeline، Inbox، الوكلاء |
| المسوقون والشركاء | مركز المسوق + المسوقين والموظفين | [`/affiliates/program`](../backend/app/api/v1/affiliates.py)، قسم `marketer-hub` |
| الشراكات B2B | الشراكات الاستراتيجية | [`/strategic-deals`](API-MAP.md)، Partnership Studio |
| الاستراتيجية والنمو | النمو والاستراتيجية | Intelligence، Growth checklist، [`autonomous_core`](../backend/app/services/autonomous_core.py) |
## 2) الثلاثة أعمدة + الحوكمة
- **مبيعات:** اكتشاف، قمع، قنوات، إغلاق — مع موافقات عند الإرسال الحساس.
- **شراكات:** ملفات شركات، مطابقة، تفاوض، ربط اختياري بـ CRM (`lead_id` / `sales_deal_id` على الصفقة الاستراتيجية).
- **نمو:** ذكاء استراتيجي وقوائم مهام؛ الالتزامات القانونية والمالية الكبرى **بشرية**.
- **حوكمة:** أوضاع تشغيل (04)، [`policy/evaluate`](API-MAP.md)، سجلات وتكاملات.
مصدر نصي موحّد للرؤية: [`strategy/summary`](../backend/app/api/v1/strategy_summary.py) (`dealix_os_three_pillars`).
## 3) وثائق يجب أن يعرفها فريق التنفيذ
- خريطة الوكلاء: [`AGENT-MAP.md`](AGENT-MAP.md)
- نموذج البيانات: [`DATA-MODEL.md`](DATA-MODEL.md)
- مسار Enterprise: [`ENTERPRISE_ROADMAP.md`](ENTERPRISE_ROADMAP.md)
- مصفوفة تنافسية: [`COMPETITIVE_MATRIX_AR.md`](COMPETITIVE_MATRIX_AR.md)
## 4) فحص تناغم الفرونت مع الـ API
من جذر `salesflow-saas`:
```bash
py -3 scripts/verify_frontend_openapi_paths.py
```
يُنصح بتشغيله قبل الإصدار؛ راجع أيضاً بنود [`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md).

View File

@ -0,0 +1,34 @@
# Dealix Enterprise roadmap
Concise path from pilot to enterprise-grade deployment. This document complements `INTEGRATION_MASTER_AR.md` and the in-app go-live gate.
## Phase 1 — Tenant isolation and audit (090 days)
- Enforce tenant scoping on every strategic-deals and CRM read/write (verify RLS or equivalent on PostgreSQL).
- Structured audit log for: operating mode changes, outbound sends, policy blocks, and human approvals.
- Backup and restore runbook per tenant (or per region).
## Phase 2 — Identity and access (90180 days)
- SSO (SAML/OIDC) for owner and compliance roles.
- SCIM or admin API for user lifecycle (optional).
- Role matrix aligned with UI: owner, RevOps, partner manager, compliance — mapped to API scopes.
## Phase 3 — Data governance (180365 days)
- Data retention policies per tenant and per channel (PDPL-aware).
- Export and delete workflows for subject requests.
- Encryption at rest review; field-level encryption for highly sensitive notes if required.
## Phase 4 — Scale and SLOs
- Per-tenant rate limits on agent and strategic-deals endpoints.
- Observability: RED metrics on API, agent latency histograms, error budgets.
- Multi-region readiness assessment (data residency constraints).
## Dependencies in this repo
- Strategic deals and OS endpoints: `backend/app/api/v1/strategic_deals.py`
- Operating modes: `backend/app/services/strategic_deals/operating_modes.py`
- Policy evaluation: `backend/app/services/dealix_os/policy_engine.py`
- Frontend hubs: `frontend/src/app/dashboard/page.tsx`

View File

@ -7,7 +7,10 @@
- [ ] `cd frontend && npm run lint && npm run build` (أو تُغطّى بواسطة `verify-launch.ps1`). - [ ] `cd frontend && npm run lint && npm run build` (أو تُغطّى بواسطة `verify-launch.ps1`).
- [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `npm run build`). - [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `npm run build`).
- [ ] **E2E (Playwright):** بعد `npm run build`، حرّر المنفذ **3000** ثم من `frontend/`: `CI=true npm run test:e2e`. إن ظهر «port already in use» أو timeout على `webServer`: من جذر `salesflow-saas` شغّل `.\scripts\kill-port-3000.ps1` ثم أعد المحاولة. - [ ] **E2E (Playwright):** بعد `npm run build`، حرّر المنفذ **3000** ثم من `frontend/`: `CI=true npm run test:e2e`. إن ظهر «port already in use» أو timeout على `webServer`: من جذر `salesflow-saas` شغّل `.\scripts\kill-port-3000.ps1` ثم أعد المحاولة.
- [ ] (اختياري) من جذر `salesflow-saas`: `py -3 scripts/verify_frontend_openapi_paths.py` (أو `python3 scripts/...`) — يطابق مسارات `/api/v1` في الفرونت مع OpenAPI (حرفيًا وفي قوالب مثل `` `${base}/api/v1/...` ``). - [ ] (موصى به) من جذر `salesflow-saas`: `py -3 scripts/verify_frontend_openapi_paths.py` (أو `python3 scripts/...`) — يطابق مسارات `/api/v1` في الفرونت مع OpenAPI (حرفيًا وفي قوالب مثل `` `${base}/api/v1/...` ``). يشمل استدعاءات `strategic-deals` و`integrations/crm` و`ai/routing` عند إضافتها في الواجهة.
- [ ] مراجعة [`docs/API-MAP.md`](API-MAP.md) مقابل OpenAPI الفعلي (`/docs` على الخادم) بعد أي إصدار يضيف مسارات جديدة.
- [ ] قراءة سريعة لـ [`docs/DEALIX_OS_PRODUCT_GUIDE_AR.md`](DEALIX_OS_PRODUCT_GUIDE_AR.md) للتأكد من توافق قصة المنتج مع الداشبورد.
- [ ] (موصى به) تشغيل سيناريو محاكاة الإطلاق في [`docs/LAUNCH_SIMULATION.md`](LAUNCH_SIMULATION.md) وتسجيل نتيجة `go-live-gate` لكل بيئة.
## 2. الخادم (API) ## 2. الخادم (API)

View File

@ -0,0 +1,34 @@
# محاكاة إطلاق Dealix (Staging → جاهزية)
وثيقة قصيرة لتشغيل **سيناريو إطلاق** يدوياً والتسجيل في سجل التشغيل. تكمّل [`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md).
## 1. التحضير
1. فرع كود محدث؛ قاعدة بيانات متوافقة مع آخر هجرات Alembic.
2. نسخ متغيرات البيئة من الأمثلة (`backend/.env`، `frontend/.env.local` / `NEXT_PUBLIC_API_URL`).
3. من جذر `salesflow-saas`:
`node scripts/sync-marketing-to-public.cjs`
## 2. البناء والتحقق
1. `.\verify-launch.ps1` (أو pytest + lint + build يدوياً كما في قائمة الإطلاق).
2. `py -3 scripts/verify_frontend_openapi_paths.py`
3. تشغيل API: `py -3 -m uvicorn app.main:app --host 127.0.0.1 --port 8000` من `backend/`.
## 3. بوابة go-live
1. استدعاء `GET /api/v1/autonomous-foundation/integrations/go-live-gate` (مع JWT إن لزم حسب البيئة).
2. تسجيل: `launch_allowed`، `blocked_reasons`، و`blocking` في ملاحظات الإصدار.
## 4. فحوص تكامل (رملي حيث أمكن)
| النظام | فعل مقترح | نتيجة متوقعة |
|--------|-----------|---------------|
| CRM Salesforce | `POST /api/v1/integrations/crm/salesforce/test` | `ok: true` أو رسالة خطأ واضحة |
| CRM HubSpot | `POST /api/v1/integrations/crm/hubspot/test` | كما فوق |
| واجهة | تبويب الإعدادات → تكاملات؛ مركز المسوق في الداشبورد | تحميل الحالة بدون أخطاء console حرجة |
## 5. الخاتمة
- وثّق التاريخ، البيئة (staging)، ونسخة الـ commit.
- أي فشل: أضف بنداً في `LAUNCH_CHECKLIST` أو issue مع `blocked_reasons` المنسوخة من الـ API.

View File

@ -9,6 +9,8 @@
http://localhost:3000/dealix-presentations/ http://localhost:3000/dealix-presentations/
http://localhost:3000/resources http://localhost:3000/resources
http://localhost:3000/strategy http://localhost:3000/strategy
http://localhost:3000/strategy/legal/ (وثائق قانونية بعد المزامنة)
http://localhost:3000/strategy/COMPETITIVE_MATRIX_AR.md
لتحديث النسخ بعد تعديل الملفات الأصلية: لتحديث النسخ بعد تعديل الملفات الأصلية:
node scripts/sync-marketing-to-public.cjs node scripts/sync-marketing-to-public.cjs

View File

@ -1,217 +0,0 @@
# Dealix — Investor Pitch Deck
# ديلكس — عرض المستثمرين
---
## Slide 1: Cover
# Dealix | ديلكس
### نظام المبيعات الذكي للسعودية
### The Smart Sales System for Saudi Arabia
**AI-Powered Revenue + Partnership + Strategic Deal OS**
---
## Slide 2: Problem | المشكلة
### الشركات السعودية تخسر ملايين بسبب:
| المشكلة | الأثر |
|---------|-------|
| **٧٠٪ من العملاء المحتملين يضيعون** | بسبب عدم المتابعة |
| **فوضى الواتساب** | رسائل ضايعة، ما تعرف مين رد |
| **لا يوجد CRM عربي ذكي** | الأنظمة الأجنبية مو مصممة للسوق السعودي |
| **فرص شراكات ضائعة** | لا يوجد نظام يكتشف ويفاوض الصفقات تلقائياً |
| **غرامات PDPL** | حتى ٥ مليون ر.س لكل مخالفة |
**السوق يحتاج نظام مبني للسعودية، عربي أولاً، ذكي فعلاً.**
---
## Slide 3: Solution | الحل
### Dealix = 4 طبقات متكاملة
```
Layer 3: Strategic Growth OS
→ رصد استحواذات، خريطة شركاء، محاكاة ROI
Layer 2: Deal Exchange OS
→ مطابقة شركاء، تبادل خدمات، غرف صفقات
Layer 1: Sales OS
→ ليدات، تواصل واتساب ذكي، عروض أسعار، pipeline
Layer 0: Core Platform
→ Company Twin، حوكمة، PDPL، ذاكرة تجارية
```
**أول منصة سعودية تجمع بين المبيعات والشراكات والنمو الاستراتيجي.**
---
## Slide 4: Market | السوق
### سوق CRM السعودي
| المقياس | الرقم |
|---------|-------|
| **حجم السوق ٢٠٢٤** | $652M |
| **المتوقع ٢٠٣٣** | $1.46B |
| **معدل النمو** | 9.4% CAGR |
| **سوق AI السعودي ٢٠٢٦** | $680M |
| **المتوقع ٢٠٣١** | $2.8B (32.9% CAGR) |
| **عدد الشركات المسجلة** | 1.2M+ |
| **استخدام WhatsApp** | 85%+ (30M+ مستخدم) |
### رؤية 2030 تدفع التحول الرقمي
- استثمار Salesforce $500M في الرياض
- AWS + Humain: 150,000 AI accelerator في الرياض
- SDAIA تقود حوكمة الذكاء الاصطناعي
---
## Slide 5: Product | المنتج
### ميزات رئيسية
| الميزة | الوصف |
|--------|-------|
| 🤖 **AI عربي** | NLP يفهم اللهجة السعودية + تقييم ذكي للعملاء |
| 📱 **واتساب ذكي** | بوت يبيع ويدعم ويتفاوض بالعربي |
| 🔄 **صفقات استراتيجية** | مطابقة شركاء + تبادل خدمات + مفاوض AI |
| 📊 **Pipeline بصري** | Kanban مع drag-and-drop |
| 💰 **عروض أسعار** | CPQ مع ضريبة القيمة المضافة تلقائياً |
| 🛡️ **PDPL مدمج** | موافقات + حقوق بيانات + audit trail |
| 📈 **تنبؤات مبيعات** | AI forecasting بالعربي |
| 🌍 **ثنائي اللغة** | عربي/إنجليزي بتبديل فوري |
---
## Slide 6: Technology | التقنية
### البنية التقنية
- **Backend**: FastAPI + Python 3.12 (async)
- **Frontend**: Next.js 15 + React 19 + TypeScript
- **Database**: PostgreSQL 16 + Redis + pgvector
- **AI**: Groq + OpenAI + Arabic NLP (camel-tools)
- **Orchestration**: Hermes + OpenClaw 2026.4.11
- **Channels**: WhatsApp Business API + Email + LinkedIn
- **Infrastructure**: Docker + Nginx + Celery
### AI Stack
- Arabic NLP with Saudi dialect detection
- AI Lead Scoring (0-100, 4 dimensions)
- Conversation Intelligence (Arabic dialogue analysis)
- Autonomous Sales Agent (WhatsApp qualification bot)
- Strategic Deal Negotiator (multi-round Arabic negotiation)
---
## Slide 7: Business Model | نموذج العمل
### SaaS Subscription
| الباقة | السعر/شهر | المستهدف |
|--------|-----------|----------|
| **Starter** | 59 ر.س | شركات صغيرة (1-3 مستخدمين) |
| **Professional** | 149 ر.س | شركات متوسطة (1-10 مستخدمين) |
| **Enterprise** | 225 ر.س | شركات كبيرة (لا محدود) |
### مصادر إيراد إضافية
- **Success Fees**: 1-3% من قيمة الصفقات المكتملة عبر Deal Exchange
- **Marketplace**: عمولة على شراكات ناجحة
- **API Access**: للمطورين والتكاملات
- **Premium Support**: دعم مخصص للمؤسسات
---
## Slide 8: Go-to-Market | خطة الذهاب للسوق
### Phase A: Real Estate (الآن)
- 20,000+ وكالة عقارية في الرياض
- متوسط قيمة الصفقة: 500K-5M ر.س
- WhatsApp = قناة التواصل الأساسية
### Phase B: Healthcare (شهر 3-6)
- عيادات وأسنان ومراكز طبية
- حجوزات + متابعة مرضى
### Phase C: Contracting (شهر 6-12)
- مقاولات وخدمات
- عروض أسعار + متابعة مشاريع
### استراتيجية الاكتساب
- Cold outreach (email-first)
- Content marketing (Arabic SEO)
- WhatsApp viral (referral program)
- LinkedIn thought leadership
- Saudi Chamber partnerships
---
## Slide 9: Competition | المنافسة
### لماذا Dealix يتفوق
| الميزة | Dealix | Salesforce | Zoho | HubSpot |
|--------|--------|------------|------|---------|
| عربي أولاً | ✅ | ❌ | ⚠️ | ❌ |
| واتساب مدمج | ✅ | ❌ | ⚠️ | ❌ |
| AI عربي | ✅ | ❌ | ❌ | ❌ |
| PDPL مدمج | ✅ | ❌ | ❌ | ❌ |
| صفقات استراتيجية | ✅ | ❌ | ❌ | ❌ |
| السعر/مستخدم/شهر | 59 ر.س | 656 ر.س | 52 ر.س | 562 ر.س |
| سيرفر سعودي | ✅ | ❌ | ✅ | ❌ |
**لا يوجد منافس يجمع: AI عربي + WhatsApp أساسي + PDPL + صفقات استراتيجية**
---
## Slide 10: Revenue Projections | التوقعات المالية
| الفترة | عملاء | MRR | ARR |
|--------|-------|-----|-----|
| شهر 1 | 5 | 745 ر.س | - |
| شهر 3 | 25 | 3,725 ر.س | - |
| شهر 6 | 100 | 14,900 ر.س | 178K ر.س |
| سنة 1 | 300 | 44,700 ر.س | 536K ر.س |
| سنة 2 | 1,000 | 149,000 ر.س | 1.79M ر.س |
| سنة 3 | 5,000 | 745,000 ر.س | 8.94M ر.س |
---
## Slide 11: Ask | الطلب
### نبحث عن: جولة Pre-Seed / Seed
| البند | المبلغ |
|-------|--------|
| **المطلوب** | 2-5M ر.س |
| **التقييم** | 15-25M ر.س (pre-money) |
| **الاستخدام** | التطوير (40%) + التسويق (30%) + التشغيل (20%) + احتياطي (10%) |
### المعالم (12 شهر)
- 1,000 عميل مدفوع
- 1.79M ر.س ARR
- 3 قطاعات مغطاة
- Deal Exchange OS مُطلق
- فريق 8-12 شخص
---
## Slide 12: Contact | تواصل
### Dealix | ديلكس
- **الموقع**: dealix.sa
- **الإيميل**: invest@dealix.sa
- **الواتساب**: +966 5X XXX XXXX
- **LinkedIn**: linkedin.com/company/dealix-sa
> "نبني أول نظام تجاري ذكي مصمم للسعودية"
---
*صنع بحب في السعودية 🇸🇦*

View File

@ -1,13 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs> <defs>
<linearGradient id="fg" x1="0%" y1="100%" x2="100%" y2="0%"> <linearGradient id="bg" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#0F4C81" /> <stop offset="0%" stop-color="#0b1220" />
<stop offset="100%" style="stop-color:#00BFA6" /> <stop offset="100%" stop-color="#0d9488" />
</linearGradient>
<linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#14b8a6" />
<stop offset="100%" stop-color="#f59e0b" />
</linearGradient> </linearGradient>
</defs> </defs>
<rect rx="6" width="32" height="32" fill="url(#fg)" /> <rect x="2" y="2" width="60" height="60" rx="16" fill="url(#bg)" />
<path d="M10 6 L10 26 L16 26 C22 26, 25 21, 25 16 C25 11, 22 6, 16 6 Z" <path d="M14 37 C22 27, 29 24, 35 26" fill="none" stroke="#5eead4" stroke-width="6" stroke-linecap="round" />
fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M50 27 C42 37, 35 40, 29 38" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round" />
<path d="M24 12 L24 5 M24 5 L21 8 M24 5 L27 8" <circle cx="32" cy="32" r="3.4" fill="#ffffff" />
fill="none" stroke="#FF6B35" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /> <path d="M20 48 H44 V22" fill="none" stroke="url(#g)" stroke-width="4" stroke-linecap="round" />
<path d="M38 28 L44 22 L50 28" fill="none" stroke="url(#g)" stroke-width="4" stroke-linecap="round" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,20 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs> <defs>
<linearGradient id="grad" x1="0%" y1="100%" x2="100%" y2="0%"> <linearGradient id="bg" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#0F4C81;stop-opacity:1" /> <stop offset="0%" stop-color="#0b1220" />
<stop offset="100%" style="stop-color:#00BFA6;stop-opacity:1" /> <stop offset="100%" stop-color="#0d9488" />
</linearGradient> </linearGradient>
<linearGradient id="arrow" x1="0%" y1="100%" x2="0%" y2="0%"> <linearGradient id="deal" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#00BFA6;stop-opacity:1" /> <stop offset="0%" stop-color="#5eead4" />
<stop offset="100%" style="stop-color:#FF6B35;stop-opacity:1" /> <stop offset="100%" stop-color="#14b8a6" />
</linearGradient>
<linearGradient id="growth" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#14b8a6" />
<stop offset="100%" stop-color="#f59e0b" />
</linearGradient> </linearGradient>
</defs> </defs>
<!-- Background circle -->
<circle cx="100" cy="100" r="92" fill="url(#grad)" /> <rect x="8" y="8" width="240" height="240" rx="56" fill="url(#bg)" />
<!-- D letter -->
<path d="M70 45 L70 155 L105 155 C140 155, 155 130, 155 100 C155 70, 140 45, 105 45 Z" <!-- Two hands approaching (strategic deal / handshake) -->
fill="none" stroke="white" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" /> <path d="M58 146 C80 118, 106 106, 126 112 C136 115, 144 123, 154 129"
<!-- Upward arrow --> fill="none" stroke="url(#deal)" stroke-width="20" stroke-linecap="round" />
<path d="M148 85 L148 40 M148 40 L132 56 M148 40 L164 56" <path d="M198 110 C176 138, 150 150, 130 144 C120 141, 112 133, 102 127"
fill="none" stroke="url(#arrow)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" /> fill="none" stroke="#ffffff" stroke-opacity="0.9" stroke-width="20" stroke-linecap="round" />
<!-- Deal node -->
<circle cx="128" cy="128" r="12" fill="#ffffff" />
<circle cx="128" cy="128" r="7" fill="#0d9488" />
<!-- Revenue growth arrow -->
<path d="M82 186 L172 186 L172 96" fill="none" stroke="url(#growth)" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
<path d="M154 112 L172 94 L190 112" fill="none" stroke="url(#growth)" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,35 @@
# مصفوفة تنافسية — Dealix مقابل منصات الإيرادات والـ CRM
هذا المستند يقارن **قدرات المنتج كما تظهر في المستودع** (واجهة، API، تكاملات، حوكمة) مع فئات أدوات السوق الشائعة. لا تُذكر أرقام سوق أو معدلات اعتماد غير مثبتة.
## الصفوف: محاور Dealix
| المحور | Dealix (ما يُثبت في الكود) |
|--------|---------------------------|
| حوكمة وإرسال | مسارات سياسات، موافقات، سجلات؛ تكامل مع `go-live-gate` ووثائق التشغيل |
| واتساب والقنوات المحلية | مسارات بريد/واتساب في التكاملات؛ تجربة عربية RTL في الواجهة |
| شراكات B2B | `strategic-deals`، Partnership Studio، هوية وصفقات استراتيجية |
| نظام تشغيل ثلاثي | أعمدة المبيعات / الشراكات / النمو في الداشبورد و`strategy_summary` |
| تكاملات CRM | Salesforce (OAuth + refresh + push/pull)، HubSpot (token + push/pull)، حالة في `integrations/crm` و`operations` |
| ذكاء متعدد المزودين | `GET/PUT /api/v1/ai/routing` — نماذج حسب نوع المهمة دون كشف مفاتيح |
## الأعمدة: فئات منافسين (مرجعية)
| الفئة | مثال مرجعي | ملاحظة مقارنة |
|-------|------------|----------------|
| Salesforce Sales Cloud | منصة CRM عالمية واسعة | Dealix لا يستبدل كل وحدات Salesforce؛ يُغطى **التنسيق والمزامنة** مع Leads/Contacts حسب التكامل الحالي |
| HubSpot CRM | تسويق وCRM مدمج | نفس نمط **الوجهة/المصدر** عبر API HubSpot في الخدمة الموحدة |
| منصات Revenue / GTM orchestration | أدلة مشتري عامة (Fullcast، Revenue.io، إلخ) | Dealix يركّز على **سوق سعودي أولاً**، حوكمة، ومسارات شراكات B2B ضمن منتج واحد |
## فروقات قابلة للإثبات (بدون أرقام ادعائية)
1. **شفافية تقنية:** مسارات API موثقة في `docs/API-MAP.md` ومطابقة OpenAPI عبر `scripts/verify_frontend_openapi_paths.py`.
2. **تكامل CRM قابل للتشغيل:** نقاط `test` و`push` و`pull` تحت `/api/v1/integrations/crm/`.
3. **سياسة نماذج:** توجيه المهام (`discovery`, `negotiation`, …) عبر `/api/v1/ai/routing` مع بقاء الأسرار في الخادم.
4. **قصة منتج موحدة:** `docs/DEALIX_OS_PRODUCT_GUIDE_AR.md` و`/strategy/summary` يربطان الواجهة بالوثائق.
## روابط
- [دليل التكاملات](INTEGRATION_MASTER_AR.md)
- [خريطة API](API-MAP.md)
- [قائمة الإطلاق](LAUNCH_CHECKLIST.md)

View File

@ -0,0 +1,34 @@
# قواعد المسوقين بالعمولة - Dealix
## ما هو مسموح
- التعريف بنفسك كمستشار مبيعات في Dealix
- مشاركة المواد التسويقية المعتمدة
- التواصل مع العملاء المحتملين بطريقة مهنية
- استخدام السكربتات والبرزنتيشنات المقدمة
- العمل في أي وقت ومن أي مكان
- إحالة مسوقين آخرين
## ما هو محظور
- انتحال صفة مؤسس أو مدير الشركة
- تقديم وعود غير مكتوبة في السياسات الرسمية
- إرسال رسائل جماعية مزعجة (spam)
- مشاركة بيانات عملاء آخرين
- تسجيل عملاء وهميين أو مكررين
- التلاعب في بيانات الإحالة
- إساءة استخدام اسم الشركة
- مخالفة سياسات التواصل المعتمدة
## العقوبات
| المخالفة | العقوبة |
|---------|--------|
| مخالفة أولى بسيطة | تحذير كتابي |
| مخالفة ثانية | تجميد العمولات 30 يوم + تحذير نهائي |
| مخالفة ثالثة أو مخالفة جسيمة | إنهاء العلاقة فوراً |
| احتيال مثبت | إنهاء + استرجاع عمولات + إجراء قانوني |
## حقوق المسوق
- الاطلاع على كشف العمولات الشهري
- الاعتراض على أي حساب خاطئ
- الحصول على التدريب والأدوات المحدثة
- الترقية للتوظيف عند تحقيق الأهداف
- إنهاء العلاقة في أي وقت مع استلام العمولات المستحقة

View File

@ -0,0 +1,37 @@
# سياسة العمولات - Dealix
## هيكل العمولات
| الباقة | السعر | نسبة العمولة (مسوق حر) | نسبة العمولة (موظف) |
|--------|-------|----------------------|-------------------|
| أساسي | 299 ر.س | 15% (~45 ر.س) | 20% (~60 ر.س) |
| احترافي | 699 ر.س | 20% (~140 ر.س) | 25% (~175 ر.س) |
| مؤسسات | 1,499 ر.س | 25% (~375 ر.س) | 30% (~450 ر.س) |
## دورة حياة العمولة
1. **مسودة (Draft)**: عند تسجيل الصفقة
2. **معلقة (Pending)**: بعد تأكيد الصفقة
3. **معتمدة (Approved)**: بعد تأكيد دفع العميل
4. **مجمدة (Held)**: في حال وجود نزاع أو مراجعة
5. **مدفوعة (Paid)**: بعد التحويل للمسوق
6. **مسترجعة (Clawback)**: إذا استرجع العميل خلال فترة الضمان
## المكافآت الشهرية
- 5 شركات/شهر = 500 ر.س بونس
- 10 شركات/شهر = 1,500 ر.س بونس + أهلية التوظيف
- 15+ شركات/شهر = 3,000 ر.س بونس
## موعد الدفع
- تُحسب العمولات في بداية كل شهر ميلادي
- تُدفع خلال أول 10 أيام عمل من الشهر التالي
- الحد الأدنى للصرف: 100 ر.س
## قواعد الإسناد (Attribution)
- العميل يُنسب للمسوق الأول الذي سجله في النظام
- صلاحية الإسناد: 90 يوم من تاريخ التسجيل
- في حال تعدد المسوقين: الأولوية للأول مع إمكانية التقسيم بقرار إداري
## النزاعات
- يحق للمسوق الاعتراض خلال 14 يوم من نشر كشف العمولات
- المراجعة خلال 5 أيام عمل
- القرار نهائي مع حق الاستئناف مرة واحدة

View File

@ -0,0 +1,35 @@
# سياسة الموافقة والاشتراك - Dealix
## مبدأ عام
لا يتم التواصل مع أي شخص عبر أي قناة إلا بموافقته المسبقة الصريحة.
## أنواع الموافقة
### واتساب
- يجب الحصول على opt-in صريح قبل إرسال أي رسالة
- الموافقة تُسجل مع التاريخ والمصدر
- حق الانسحاب (opt-out) يُنفذ فوراً
- يُستخدم فقط قوالب معتمدة من Meta للرسائل الأولى
### البريد الإلكتروني
- رابط إلغاء الاشتراك إلزامي في كل رسالة
- opt-in عند التسجيل أو تعبئة نموذج
- لا يُرسل أكثر من 3 رسائل بدون رد قبل التوقف
### الرسائل النصية (SMS)
- موافقة مسبقة مطلوبة
- تُستخدم فقط للإشعارات الضرورية
### المكالمات الصوتية
- لا يُتصل بدون سبب مشروع (عميل محتمل مسجل)
- تسجيل المكالمات يتطلب إبلاغ وموافقة
## سجل الموافقة
- كل موافقة تُسجل بـ: التاريخ، القناة، المصدر، IP (إن توفر)
- كل انسحاب يُنفذ خلال 24 ساعة كحد أقصى
- السجلات تُحفظ 36 شهر
## حقوق صاحب البيانات
- الوصول لسجل الموافقات الخاص به
- سحب الموافقة في أي وقت لأي قناة
- تقديم شكوى في حال مخالفة

View File

@ -0,0 +1,36 @@
# حماية البيانات - Dealix
## الامتثال لنظام حماية البيانات الشخصية (PDPL)
### المبادئ الأساسية
1. **تقليل البيانات**: نجمع فقط البيانات الضرورية لتقديم الخدمة
2. **تحديد الغرض**: كل بيان يُجمع لغرض محدد ومعلن
3. **الشفافية**: سياسات واضحة ومتاحة للجميع
4. **الأمان**: حماية تقنية وتنظيمية مناسبة
5. **المساءلة**: سجلات تدقيق لكل عملية
### التدابير التقنية
- **التشفير**: TLS 1.3 للنقل، AES-256 للتخزين
- **النسخ الاحتياطي**: يومي، مشفر، خارج الموقع
- **الصلاحيات**: نظام أدوار وصلاحيات (RBAC)
- **المراقبة**: رصد مستمر للوصول غير المصرح
- **التدقيق**: سجل لكل عملية وصول أو تعديل
### حقوق أصحاب البيانات
- **الوصول**: طلب نسخة من بياناتك خلال 30 يوم
- **التصحيح**: طلب تعديل بيانات غير صحيحة
- **الحذف**: طلب حذف بياناتك (حق النسيان)
- **النقل**: طلب نقل بياناتك بصيغة قابلة للقراءة
- **الاعتراض**: الاعتراض على أي معالجة
### تقديم طلب
1. أرسل طلبك عبر: privacy@dealix.sa
2. حدد نوع الطلب (وصول/تصحيح/حذف/نقل)
3. إشعار استلام خلال 48 ساعة
4. تنفيذ خلال 30 يوم كحد أقصى
### الإبلاغ عن خرق
في حال اكتشاف خرق للبيانات:
- إبلاغ الجهات المختصة خلال 72 ساعة
- إبلاغ المتضررين خلال 72 ساعة
- توثيق الخرق والإجراءات المتخذة

View File

@ -0,0 +1,57 @@
# سياسة الخصوصية - Dealix (ديل اي اكس)
**تاريخ السريان:** 2026-03-31
**آخر تحديث:** 2026-03-31
## 1. مقدمة
تلتزم Dealix (ديل اي اكس) بحماية خصوصية مستخدميها وعملائها وفقاً لنظام حماية البيانات الشخصية (PDPL) في المملكة العربية السعودية.
## 2. البيانات التي نجمعها
- **بيانات الحساب**: الاسم، البريد الإلكتروني، رقم الجوال، اسم الشركة، القطاع
- **بيانات الاستخدام**: سجلات الدخول، الصفحات المزارة، الإجراءات المنفذة
- **بيانات التواصل**: سجلات المحادثات، الرسائل، المكالمات (بموافقة مسبقة)
- **بيانات الدفع**: معلومات الفوترة (تُعالج عبر مزود دفع آمن)
## 3. كيف نستخدم بياناتك
- تقديم وتحسين خدماتنا
- التواصل معك بخصوص حسابك
- إرسال إشعارات الخدمة والتحديثات
- تحليل الاستخدام لتحسين المنصة
- الامتثال للمتطلبات القانونية
## 4. مشاركة البيانات
لا نبيع بياناتك الشخصية. قد نشاركها مع:
- مزودي الخدمة (استضافة، بريد، رسائل) بعقود حماية
- الجهات الرسمية عند الطلب القانوني
- شركاء الأعمال بموافقتك المسبقة
## 5. حقوقك
لك الحق في:
- الوصول لبياناتك الشخصية
- تصحيح بياناتك
- حذف بياناتك
- الاعتراض على المعالجة
- نقل بياناتك
- سحب الموافقة في أي وقت
## 6. أمن البيانات
- تشفير كامل للبيانات أثناء النقل والتخزين (TLS 1.3 + AES-256)
- نسخ احتياطية يومية مشفرة
- صلاحيات وصول محددة حسب الدور
- مراقبة أمنية مستمرة
- سجلات تدقيق لكل عملية وصول
## 7. الاحتفاظ بالبيانات
- بيانات الحساب: طوال مدة الاشتراك + 12 شهر بعد الإلغاء
- سجلات التواصل: 24 شهر
- سجلات التدقيق: 36 شهر
- بيانات الدفع: حسب المتطلبات الضريبية
## 8. ملفات الارتباط (Cookies)
نستخدم ملفات ارتباط ضرورية لتشغيل المنصة وتحسين تجربتك.
## 9. التواصل
لأي استفسارات تتعلق بالخصوصية: privacy@dealix.sa
## 10. التعديلات
نحتفظ بحق تعديل هذه السياسة مع إشعار مسبق 30 يوم.

View File

@ -0,0 +1,28 @@
# سياسة الاسترجاع والضمان الذهبي - Dealix
## الضمان الذهبي (30 يوم)
### الوعد
إذا استخدمت Dealix لمدة 30 يوم ولم تشهد تحسناً في عمليات مبيعاتك، نسترجع لك المبلغ كاملاً.
### شروط الأهلية
1. استخدام المنصة لمدة 14 يوم متواصل على الأقل
2. إدخال 20 عميل محتمل كحد أدنى
3. إرسال 50 رسالة كحد أدنى عبر المنصة
4. حضور جلسة التدريب الأولية
### الاستثناءات
- الحسابات المجمدة بسبب مخالفات
- الاستخدام لمرة واحدة فقط لكل شركة
- لا يسري على الاشتراكات التجريبية المجانية
### إجراءات الطلب
1. تقديم طلب الاسترجاع عبر الدعم الفني
2. إشعار استلام خلال 24 ساعة
3. مراجعة الاستخدام خلال 3 أيام عمل
4. القرار خلال 5 أيام عمل
5. التنفيذ خلال 7 أيام عمل من الموافقة
### طريقة الاسترجاع
- نفس طريقة الدفع الأصلية
- تحويل بنكي إذا تعذرت الطريقة الأصلية

View File

@ -0,0 +1,54 @@
# شروط الخدمة - Dealix (ديل اي اكس)
**تاريخ السريان:** 2026-03-31
## 1. التعريفات
- **المنصة**: منصة Dealix لأتمتة المبيعات بالذكاء الاصطناعي
- **المشترك**: الشركة أو الفرد المسجل في المنصة
- **الخدمات**: جميع المميزات والأدوات المتاحة حسب الباقة المختارة
## 2. الخدمات المقدمة
- إدارة العملاء المحتملين والمتابعة التلقائية
- تكامل واتساب بزنس وإيميل ورسائل نصية
- خط أنابيب المبيعات وعروض الأسعار
- تقارير وتحليلات ولوحات تحكم
- وكلاء ذكاء اصطناعي للتواصل التلقائي
## 3. الباقات والأسعار
- أساسي: 299 ر.س/شهر | احترافي: 699 ر.س/شهر | مؤسسات: 1,499 ر.س/شهر
- جميع الأسعار بالريال السعودي وتشمل ضريبة القيمة المضافة
- الدفع شهري أو سنوي مقدماً
## 4. التجربة المجانية
- 14 يوم بكل المميزات بدون بطاقة ائتمان
- تنتهي تلقائياً بدون أي التزام
## 5. حقوق المشترك
- الوصول الكامل لمميزات الباقة المختارة
- الدعم الفني حسب مستوى الباقة
- ملكية كاملة لبياناته وحق تصديرها
- الإلغاء في أي وقت
## 6. التزامات المشترك
- عدم استخدام المنصة لأغراض غير قانونية
- عدم مشاركة بيانات الدخول
- الالتزام بسياسات التواصل مع العملاء
- عدم إرسال رسائل مزعجة (spam)
- الحفاظ على سرية بيانات عملائه
## 7. الضمان الذهبي
- 30 يوم استرجاع كامل بشروط محددة في سياسة الاسترجاع
- يسري على جميع الباقات المدفوعة
## 8. المسؤولية
- Dealix غير مسؤولة عن خسائر ناتجة عن سوء استخدام المنصة
- المسؤولية القصوى لا تتجاوز قيمة الاشتراك الشهري
- لا نضمن نتائج مبيعات محددة
## 9. الإلغاء
- يحق للمشترك الإلغاء في أي وقت
- الخدمة تستمر حتى نهاية الفترة المدفوعة
- لا توجد رسوم إلغاء
## 10. القانون الحاكم
تخضع هذه الشروط لأنظمة المملكة العربية السعودية والجهات القضائية المختصة في الرياض.

View File

@ -1,7 +1,8 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useRequireAuth } from "@/contexts/auth-context"; import { useRequireAuth } from "@/contexts/auth-context";
import { useRouter } from "next/navigation";
import { import {
BarChart3, BarChart3,
Users, Users,
@ -28,6 +29,13 @@ import {
UserCheck, UserCheck,
TrendingUp, TrendingUp,
Crosshair, Crosshair,
SlidersHorizontal,
Activity,
BookMarked,
Handshake,
Share2,
Rocket,
Megaphone,
} from "lucide-react"; } from "lucide-react";
import { DashboardView } from "../../components/dealix/dashboard-view"; import { DashboardView } from "../../components/dealix/dashboard-view";
@ -51,6 +59,15 @@ import { FullOpsView } from "../../components/dealix/full-ops-view";
import { PipelineKanban } from "../../components/dealix/pipeline-kanban"; import { PipelineKanban } from "../../components/dealix/pipeline-kanban";
import { UnifiedInbox } from "../../components/dealix/unified-inbox"; import { UnifiedInbox } from "../../components/dealix/unified-inbox";
import { LeadScoreCard } from "../../components/dealix/lead-score-card"; import { LeadScoreCard } from "../../components/dealix/lead-score-card";
import { GoLiveReadinessCard } from "../../components/dealix/go-live-readiness-card";
import { OperatingModelView } from "../../components/dealix/operating-model-view";
import { PartnershipStudioView } from "../../components/dealix/partnership-studio-view";
import { GrowthPlaybookView } from "../../components/dealix/growth-playbook-view";
import { GovernanceMetricsView } from "../../components/dealix/governance-metrics-view";
import { IdentityGraphView } from "../../components/dealix/identity-graph-view";
import { VerticalPlaybooksView } from "../../components/dealix/vertical-playbooks-view";
import { AgentQualityView } from "../../components/dealix/agent-quality-view";
import { MarketerHubView } from "../../components/dealix/marketer-hub-view";
const dashboardLeadScoreDemo = { const dashboardLeadScoreDemo = {
score: 82, score: 82,
@ -63,9 +80,69 @@ const dashboardLeadScoreDemo = {
recommendation: "عميل واعد — تابع خلال ٢٤ ساعة", recommendation: "عميل واعد — تابع خلال ٢٤ ساعة",
}; };
const HUB_ORDER = ["platform", "sales", "partnerships", "growth"] as const;
type HubId = (typeof HUB_ORDER)[number];
const HUB_LABELS: Record<HubId, string> = {
platform: "المنصة والحوكمة",
sales: "محرك المبيعات",
partnerships: "الشراكات الاستراتيجية",
growth: "النمو والاستراتيجية",
};
const NAV_ITEMS = [
{ id: "overview", label: "لوحة القيادة والمراقبة", icon: BarChart3, hub: "platform" as const },
{ id: "go-live", label: "جاهزية الإطلاق", icon: ShieldCheck, hub: "platform" as const },
{ id: "operating-model", label: "نموذج التشغيل OS", icon: SlidersHorizontal, hub: "platform" as const },
{ id: "governance-metrics", label: "الحوكمة والمؤشرات", icon: ShieldCheck, hub: "platform" as const },
{ id: "agent-quality", label: "جودة الوكلاء", icon: Activity, hub: "platform" as const },
{ id: "onboarding", label: "تأهيل المسوق", icon: BookOpen, hub: "platform" as const },
{ id: "agreements", label: "الاتفاقيات واHR", icon: FileSignature, hub: "platform" as const },
{ id: "guarantee", label: "الضمان الذهبي", icon: ShieldCheck, hub: "platform" as const },
{ id: "leads", label: "توليد العملاء — AI", icon: Target, hub: "sales" as const },
{ id: "pipeline", label: "مسار الصفقات", icon: Target, hub: "sales" as const },
{ id: "inbox", label: "صندوق الوارد الموحد", icon: Bell, hub: "sales" as const },
{ id: "scoring", label: "تقييم العملاء AI", icon: Zap, hub: "sales" as const },
{ id: "scripts", label: "سكربتات المبيعات", icon: Phone, hub: "sales" as const },
{ id: "presentations", label: "البرزنتيشنات القطاعية", icon: MonitorPlay, hub: "sales" as const },
{ id: "properties", label: "إدارة المخزون العقاري", icon: Building2, hub: "sales" as const },
{ id: "marketer-hub", label: "مركز المسوق", icon: Megaphone, hub: "sales" as const },
{ id: "affiliates", label: "المسوقين والموظفين", icon: Users, hub: "sales" as const },
{ id: "agents", label: "الوكلاء الأذكياء", icon: BrainCircuit, hub: "sales" as const },
{ id: "revenue", label: "المالية والتحصيل", icon: DollarSign, hub: "sales" as const },
{ id: "sales-os", label: "دفتر العمولة (Sales OS)", icon: Receipt, hub: "sales" as const },
{ id: "full-ops", label: "التشغيل الشامل (Full Ops)", icon: Layers, hub: "sales" as const },
{ id: "analytics", label: "التحليلات ونبض السوق", icon: BarChart3, hub: "sales" as const },
{ id: "knowledge", label: "الذكاء والمعرفة", icon: Brain, hub: "sales" as const },
{ id: "partnership-studio", label: "Partnership Studio", icon: Handshake, hub: "partnerships" as const },
{ id: "identity-graph", label: "طبقة الكيان الموحّد", icon: Share2, hub: "partnerships" as const },
{ id: "vertical-playbooks", label: "Playbooks قطاعية", icon: BookMarked, hub: "partnerships" as const },
{ id: "growth-playbook", label: "نمو واستعداد استحواذ", icon: Rocket, hub: "growth" as const },
{ id: "intelligence", label: "الذكاء المستقل — Manus", icon: BrainCircuit, hub: "growth" as const },
{ id: "business-impact", label: "القيمة للشركات", icon: LineChart, hub: "growth" as const },
{ id: "customer-journey", label: "مسار التشغيل مع العميل", icon: ClipboardList, hub: "growth" as const },
] as const;
type DashboardTabId = (typeof NAV_ITEMS)[number]["id"];
export default function DashboardPage() { export default function DashboardPage() {
const auth = useRequireAuth(); const auth = useRequireAuth();
const [activeTab, setActiveTab] = useState("overview"); const router = useRouter();
const allowedTabs = useMemo(() => new Set<DashboardTabId>(NAV_ITEMS.map((n) => n.id)), []);
const [activeTab, setActiveTabState] = useState<DashboardTabId>("overview");
useEffect(() => {
const requested = new URLSearchParams(window.location.search).get("section") || "overview";
if (allowedTabs.has(requested as DashboardTabId)) {
setActiveTabState(requested as DashboardTabId);
}
}, [allowedTabs]);
const setActiveTab = (tab: DashboardTabId) => {
const next = new URLSearchParams(window.location.search);
next.set("section", tab);
setActiveTabState(tab);
router.push(`/dashboard?${next.toString()}`);
};
if (auth.loading) { if (auth.loading) {
return ( return (
@ -78,36 +155,28 @@ export default function DashboardPage() {
return null; return null;
} }
const NAV_ITEMS = [
{ id: "overview", label: "لوحة القيادة والمراقبة", icon: BarChart3 },
{ id: "business-impact", label: "القيمة للشركات", icon: LineChart },
{ id: "customer-journey", label: "مسار التشغيل مع العميل", icon: ClipboardList },
{ id: "intelligence", label: "الذكاء المستقل — Manus", icon: BrainCircuit },
{ id: "leads", label: "توليد العملاء — AI", icon: Target },
{ id: "properties", label: "إدارة المخزون العقاري", icon: Building2 },
{ id: "affiliates", label: "المسوقين والموظفين", icon: Users },
{ id: "agents", label: "الوكلاء الأذكياء", icon: BrainCircuit },
{ id: "revenue", label: "المالية والتحصيل", icon: DollarSign },
{ id: "sales-os", label: "دفتر العمولة (Sales OS)", icon: Receipt },
{ id: "full-ops", label: "التشغيل الشامل (Full Ops)", icon: Layers },
{ id: "analytics", label: "التحليلات ونبض السوق", icon: BarChart3 },
{ id: "knowledge", label: "الذكاء والمعرفة", icon: Brain },
{ id: "presentations", label: "البرزنتيشنات القطاعية", icon: MonitorPlay },
{ id: "scripts", label: "سكربتات المبيعات", icon: Phone },
{ id: "agreements", label: "الاتفاقيات واHR", icon: FileSignature },
{ id: "guarantee", label: "الضمان الذهبي", icon: ShieldCheck },
{ id: "pipeline", label: "مسار الصفقات", icon: Target },
{ id: "inbox", label: "صندوق الوارد الموحد", icon: Bell },
{ id: "scoring", label: "تقييم العملاء AI", icon: Zap },
{ id: "onboarding", label: "تأهيل المسوق", icon: BookOpen },
];
const renderContent = () => { const renderContent = () => {
switch (activeTab) { switch (activeTab) {
case "overview": case "overview":
return <DashboardView />; return <DashboardView />;
case "business-impact": case "business-impact":
return <BusinessImpactView />; return <BusinessImpactView />;
case "go-live":
return <GoLiveReadinessCard />;
case "operating-model":
return <OperatingModelView />;
case "governance-metrics":
return <GovernanceMetricsView />;
case "agent-quality":
return <AgentQualityView />;
case "partnership-studio":
return <PartnershipStudioView />;
case "identity-graph":
return <IdentityGraphView />;
case "vertical-playbooks":
return <VerticalPlaybooksView />;
case "growth-playbook":
return <GrowthPlaybookView />;
case "customer-journey": case "customer-journey":
return <CustomerOnboardingJourneyView />; return <CustomerOnboardingJourneyView />;
case "intelligence": case "intelligence":
@ -116,6 +185,8 @@ export default function DashboardPage() {
return <LeadGeneratorView />; return <LeadGeneratorView />;
case "properties": case "properties":
return <PropertiesView />; return <PropertiesView />;
case "marketer-hub":
return <MarketerHubView />;
case "affiliates": case "affiliates":
return <AffiliatesView />; return <AffiliatesView />;
case "agents": case "agents":
@ -165,27 +236,35 @@ export default function DashboardPage() {
</div> </div>
</div> </div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto"> <nav className="flex-1 p-2 space-y-2 overflow-y-auto">
{NAV_ITEMS.map((item) => ( {HUB_ORDER.map((hub) => (
<button <div key={hub} className="space-y-0.5">
key={item.id} <p className="px-3 pt-2 pb-1 text-[10px] uppercase tracking-wider text-muted-foreground/90 font-bold">
type="button" {HUB_LABELS[hub]}
onClick={() => setActiveTab(item.id)} </p>
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 ${ {NAV_ITEMS.filter((item) => item.hub === hub).map((item) => (
activeTab === item.id <button
? "bg-primary/10 text-primary font-bold border border-primary/20 shadow-sm" key={item.id}
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground font-medium" type="button"
}`} onClick={() => setActiveTab(item.id)}
> className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 ${
<item.icon className={`w-5 h-5 ${activeTab === item.id ? "text-primary" : "opacity-70"}`} /> activeTab === item.id
<span>{item.label}</span> ? "bg-primary/10 text-primary font-bold border border-primary/20 shadow-sm"
</button> : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground font-medium"
}`}
>
<item.icon className={`w-5 h-5 shrink-0 ${activeTab === item.id ? "text-primary" : "opacity-70"}`} />
<span className="text-sm text-right leading-snug">{item.label}</span>
</button>
))}
</div>
))} ))}
</nav> </nav>
<div className="p-4 mt-auto border-t border-border/50 bg-secondary/10"> <div className="p-4 mt-auto border-t border-border/50 bg-secondary/10">
<button <button
type="button" type="button"
onClick={() => router.push("/settings")}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-muted-foreground hover:bg-secondary/50 transition-all font-medium" className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-muted-foreground hover:bg-secondary/50 transition-all font-medium"
> >
<Settings className="w-5 h-5" /> <Settings className="w-5 h-5" />
@ -236,25 +315,23 @@ export default function DashboardPage() {
<div className="flex-1 w-full max-w-[1600px] mx-auto pb-24 lg:pb-0">{renderContent()}</div> <div className="flex-1 w-full max-w-[1600px] mx-auto pb-24 lg:pb-0">{renderContent()}</div>
<nav className="lg:hidden fixed bottom-6 left-1/2 -translate-x-1/2 w-[90%] max-w-md bg-card/80 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl flex items-center justify-around py-4 px-4 z-50"> <nav className="lg:hidden fixed bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl bg-card/80 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl py-3 px-3 z-50 overflow-x-auto">
{[ <div className="flex items-center gap-2 min-w-max">
{ id: "overview", icon: BarChart3 }, {NAV_ITEMS.map((item) => (
{ id: "agents", icon: BrainCircuit },
{ id: "presentations", icon: MonitorPlay },
{ id: "scripts", icon: Phone },
].map((item) => (
<button <button
key={item.id} key={item.id}
type="button" type="button"
onClick={() => setActiveTab(item.id)} onClick={() => setActiveTab(item.id)}
className={`flex flex-col items-center gap-1 transition-all ${ className={`flex flex-col items-center gap-1 px-3 py-1.5 rounded-xl transition-all ${
activeTab === item.id ? "text-primary scale-110" : "text-muted-foreground opacity-60" activeTab === item.id ? "text-primary bg-primary/10" : "text-muted-foreground opacity-70"
}`} }`}
aria-label={item.label}
> >
<item.icon className="w-6 h-6" /> <item.icon className="w-5 h-5" />
{activeTab === item.id && <span className="w-1 h-1 bg-primary rounded-full" />} <span className="text-[10px] whitespace-nowrap">{item.label}</span>
</button> </button>
))} ))}
</div>
</nav> </nav>
</main> </main>
</div> </div>

View File

@ -83,6 +83,9 @@
/* Luxury Glassmorphism & High-End Effects */ /* Luxury Glassmorphism & High-End Effects */
@layer utilities { @layer utilities {
.glass-card {
@apply bg-card/60 backdrop-blur-2xl border border-border/50 rounded-2xl shadow-xl transition-all duration-300 hover:border-primary/30;
}
.glass-premium { .glass-premium {
@apply bg-white/5 dark:bg-black/40 backdrop-blur-2xl border border-white/10 dark:border-white/5 shadow-[0_8px_32px_0_rgba(0,0,0,0.37)]; @apply bg-white/5 dark:bg-black/40 backdrop-blur-2xl border border-white/10 dark:border-white/5 shadow-[0_8px_32px_0_rgba(0,0,0,0.37)];
} }
@ -99,6 +102,10 @@
/* Animations & Scrollbar */ /* Animations & Scrollbar */
@layer utilities { @layer utilities {
.dealix-section-header {
@apply border-b border-border/50 pb-4;
}
.animate-float { .animate-float {
animation: float 6s ease-in-out infinite; animation: float 6s ease-in-out infinite;
} }

View File

@ -1,5 +1,5 @@
import HeroLanding from "../../components/dealix/hero-landing"; import { PremiumLanding } from "../../components/dealix/premium-landing";
export default function LandingPage() { export default function LandingPage() {
return <HeroLanding />; return <PremiumLanding />;
} }

View File

@ -13,6 +13,11 @@ export const metadata: Metadata = {
title: "Dealix — نظام تشغيل الإيرادات B2B", title: "Dealix — نظام تشغيل الإيرادات B2B",
description: description:
"اكتشاف، تأهيل، قنوات متعددة، وتحليلات — مع حوكمة وذاكرة. سوق سعودي.", "اكتشاف، تأهيل، قنوات متعددة، وتحليلات — مع حوكمة وذاكرة. سوق سعودي.",
icons: {
icon: "/favicon.svg",
shortcut: "/favicon.svg",
apple: "/favicon.svg",
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@ -1,8 +1,10 @@
'use client'; 'use client';
import { useState, type ReactNode } from 'react'; import { useState, useEffect, useCallback, type ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useI18n } from '@/i18n'; import { useI18n } from '@/i18n';
import { apiFetch } from '@/lib/api-client';
import { getAccessToken, getStoredUser } from '@/lib/auth-storage';
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Types */ /* Types */
@ -355,6 +357,42 @@ function BillingTab({ label }: { label: L }) {
</div> </div>
</Section> </Section>
<Section title={label('سياسات التسعير المؤسسي', 'Enterprise Pricing Controls')} onSave={() => {}} label={label}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label={label('نموذج التسعير', 'Pricing Model')}>
<SelectInput
defaultValue="hybrid"
options={[
{ value: 'seat', label: label('حسب عدد المستخدمين', 'Per seat') },
{ value: 'volume', label: label('حسب حجم العملاء', 'Volume based') },
{ value: 'hybrid', label: label('هجين (موصى به)', 'Hybrid (recommended)') },
]}
/>
</Field>
<Field label={label('عملة الفوترة', 'Billing Currency')}>
<SelectInput
defaultValue="SAR"
options={[
{ value: 'SAR', label: 'SAR' },
{ value: 'USD', label: 'USD' },
]}
/>
</Field>
<Field label={label('الحد الأدنى للمقاعد', 'Minimum Seats')}>
<TextInput defaultValue="10" dir="ltr" />
</Field>
<Field label={label('خصم الشراكات الاستراتيجية (%)', 'Strategic Partnership Discount (%)')}>
<TextInput defaultValue="12" dir="ltr" />
</Field>
</div>
<p className="text-xs text-slate-500 mt-2">
{label(
'هذه الحقول تمثل ضوابط تسعير على مستوى الشركة ويمكن تعديلها لاحقًا حسب سياسة كل قطاع.',
'These values are company-level pricing controls and can be tuned per vertical later.'
)}
</p>
</Section>
{/* Invoice history */} {/* Invoice history */}
<Section title={label('سجل الفواتير', 'Invoice History')} label={label}> <Section title={label('سجل الفواتير', 'Invoice History')} label={label}>
<div className="space-y-2"> <div className="space-y-2">
@ -375,34 +413,277 @@ function BillingTab({ label }: { label: L }) {
); );
} }
type CrmStatusPayload = {
salesforce: {
env_refresh_configured: boolean;
tenant_refresh_override: boolean;
domain: string;
};
hubspot: {
env_token_configured: boolean;
tenant_token_override: boolean;
};
docs: { integration_master_ar: string; api_map: string };
};
type AiRoutingPayload = {
effective: Record<string, { provider: string; model: string }>;
available_providers: string[];
note_ar: string;
};
function IntegrationsTab({ label }: { label: L }) { function IntegrationsTab({ label }: { label: L }) {
const integrations = [ const [crm, setCrm] = useState<CrmStatusPayload | null>(null);
{ name: 'WhatsApp', icon: '💬', connected: true, descAr: 'متصل — رقم +966 50 XXX XXXX', descEn: 'Connected — +966 50 XXX XXXX' }, const [routing, setRouting] = useState<AiRoutingPayload | null>(null);
{ name: label('البريد SMTP', 'Email SMTP'), icon: '📧', connected: false, descAr: 'غير متصل', descEn: 'Not connected' }, const [leadId, setLeadId] = useState('');
]; const [busy, setBusy] = useState<string | null>(null);
const [lastResult, setLastResult] = useState<string | null>(null);
const [noToken, setNoToken] = useState(false);
const user = typeof window !== 'undefined' ? getStoredUser() : null;
const canOps =
user && ['owner', 'manager', 'admin'].includes((user.role || '').toLowerCase());
const load = useCallback(async () => {
if (!getAccessToken()) {
setNoToken(true);
setCrm(null);
setRouting(null);
return;
}
setNoToken(false);
const r = await apiFetch('/api/v1/integrations/crm/status');
if (r.ok) setCrm((await r.json()) as CrmStatusPayload);
const ar = await apiFetch('/api/v1/ai/routing');
if (ar.ok) setRouting((await ar.json()) as AiRoutingPayload);
}, []);
useEffect(() => {
void load();
}, [load]);
const postAction = async (path: string, body?: object) => {
setBusy(path);
setLastResult(null);
try {
const r = await apiFetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body ?? {}),
});
const text = await r.text();
let msg = text;
try {
msg = JSON.stringify(JSON.parse(text), null, 2);
} catch {
/* raw */
}
setLastResult(`${r.status} ${r.statusText}\n${msg.slice(0, 4000)}`);
if (r.ok) void load();
} finally {
setBusy(null);
}
};
const pushSf = () => {
const id = leadId.trim();
if (!/^[0-9a-f-]{36}$/i.test(id)) {
setLastResult(label('معرّف العميل المحتمل غير صالح (UUID)', 'Invalid lead id (UUID)'));
return;
}
void postAction(`/api/v1/integrations/crm/salesforce/push-lead/${id}`);
};
const pushHs = () => {
const id = leadId.trim();
if (!/^[0-9a-f-]{36}$/i.test(id)) {
setLastResult(label('معرّف العميل المحتمل غير صالح (UUID)', 'Invalid lead id (UUID)'));
return;
}
void postAction(`/api/v1/integrations/crm/hubspot/push-lead/${id}`);
};
const sfReady =
crm &&
(crm.salesforce.env_refresh_configured || crm.salesforce.tenant_refresh_override);
const hsReady = crm && (crm.hubspot.env_token_configured || crm.hubspot.tenant_token_override);
return ( return (
<> <>
<Section title={label('التكاملات', 'Integrations')} label={label}> <Section title={label('تكاملات CRM', 'CRM integrations')} label={label}>
<div className="space-y-3"> <div className="dealix-section-header space-y-2">
{integrations.map((intg, i) => ( <p className="text-sm text-slate-400">
<div key={i} className="flex items-center justify-between p-4 rounded-xl bg-white/[0.03] border border-white/5"> {label(
<div className="flex items-center gap-3"> 'حالة التهيئة من البيئة وإعدادات المستأجر — دون عرض أسرار.',
<span className="text-2xl">{intg.icon}</span> 'Configuration hints from env and tenant settings — no secrets shown.',
<div> )}
<p className="text-sm font-medium text-white">{intg.name}</p> </p>
<p className="text-xs text-slate-500">{label(intg.descAr, intg.descEn)}</p> <a
</div> href="/strategy/INTEGRATION_MASTER_AR.md"
</div> target="_blank"
<span className={`text-xs font-semibold px-3 py-1 rounded-full border ${intg.connected ? 'text-emerald-400 bg-emerald-400/10 border-emerald-400/30' : 'text-slate-400 bg-white/5 border-white/10'}`}> rel="noopener noreferrer"
{intg.connected ? label('متصل', 'Connected') : label('غير متصل', 'Disconnected')} className="text-sm font-semibold text-primary hover:underline"
</span> >
</div> {label('دليل التكاملات (Markdown)', 'Integration master (Markdown)')}
))} </a>
</div> </div>
{noToken && (
<p className="text-sm text-amber-400/90">
{label(
'سجّل الدخول من صفحة تسجيل الدخول لتحميل حالة التكاملات.',
'Sign in from the login page to load integration status.',
)}
</p>
)}
{crm && (
<div className="grid gap-4 sm:grid-cols-2">
<div className="p-4 rounded-xl bg-white/[0.03] border border-white/10">
<h3 className="text-sm font-bold text-white mb-2">Salesforce</h3>
<ul className="text-xs text-slate-400 space-y-1">
<li>
{label('بيئة:', 'Env:')}{' '}
{crm.salesforce.env_refresh_configured ? label('مهيأ', 'OK') : label('غير مهيأ', 'Missing')}
</li>
<li>
{label('مستأجر:', 'Tenant:')}{' '}
{crm.salesforce.tenant_refresh_override ? label('يوجد override', 'Override') : label('افتراضي', 'Default')}
</li>
<li className="break-all">domain: {crm.salesforce.domain}</li>
</ul>
<p className="text-xs mt-2 text-slate-500">
{sfReady
? label('جاهز لمحاولة الاختبار', 'Ready to test')
: label('أضف refresh token وعميل OAuth', 'Add OAuth client + refresh token')}
</p>
</div>
<div className="p-4 rounded-xl bg-white/[0.03] border border-white/10">
<h3 className="text-sm font-bold text-white mb-2">HubSpot</h3>
<ul className="text-xs text-slate-400 space-y-1">
<li>
{label('بيئة:', 'Env:')}{' '}
{crm.hubspot.env_token_configured ? label('مهيأ', 'OK') : label('غير مهيأ', 'Missing')}
</li>
<li>
{label('مستأجر:', 'Tenant:')}{' '}
{crm.hubspot.tenant_token_override ? label('يوجد رمز', 'Token set') : label('افتراضي', 'Default')}
</li>
</ul>
<p className="text-xs mt-2 text-slate-500">
{hsReady
? label('جاهز لمحاولة الاختبار', 'Ready to test')
: label('أضف مفتاح/رمز HubSpot', 'Add HubSpot token')}
</p>
</div>
</div>
)}
{canOps && crm && (
<div className="pt-4 border-t border-white/10 space-y-3">
<h3 className="text-sm font-semibold text-white">
{label('اختبار ومزامنة', 'Test & sync')}
</h3>
<div className="flex flex-wrap gap-2">
<button
type="button"
disabled={!!busy || !sfReady}
onClick={() => void postAction('/api/v1/integrations/crm/salesforce/test')}
className="px-3 py-2 rounded-lg bg-primary/20 text-primary border border-primary/30 text-xs font-semibold disabled:opacity-40"
>
{busy === '/api/v1/integrations/crm/salesforce/test' ? '…' : 'SF test'}
</button>
<button
type="button"
disabled={!!busy || !sfReady}
onClick={() => void postAction('/api/v1/integrations/crm/salesforce/pull-leads')}
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-semibold disabled:opacity-40"
>
{busy === '/api/v1/integrations/crm/salesforce/pull-leads' ? '…' : 'SF pull'}
</button>
<button
type="button"
disabled={!!busy || !hsReady}
onClick={() => void postAction('/api/v1/integrations/crm/hubspot/test')}
className="px-3 py-2 rounded-lg bg-primary/20 text-primary border border-primary/30 text-xs font-semibold disabled:opacity-40"
>
{busy === '/api/v1/integrations/crm/hubspot/test' ? '…' : 'HS test'}
</button>
<button
type="button"
disabled={!!busy || !hsReady}
onClick={() => void postAction('/api/v1/integrations/crm/hubspot/pull-contacts')}
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-semibold disabled:opacity-40"
>
{busy === '/api/v1/integrations/crm/hubspot/pull-contacts' ? '…' : 'HS pull'}
</button>
</div>
<Field label={label('معرّف عميل محتمل (UUID) لدفع واحد', 'Lead UUID for single push')}>
<input
type="text"
value={leadId}
onChange={(e) => setLeadId(e.target.value)}
dir="ltr"
placeholder="00000000-0000-0000-0000-000000000000"
className="w-full px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-white placeholder-slate-500 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
</Field>
<div className="flex flex-wrap gap-2">
<button
type="button"
disabled={!!busy || !sfReady}
onClick={pushSf}
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-semibold disabled:opacity-40"
>
SF push lead
</button>
<button
type="button"
disabled={!!busy || !hsReady}
onClick={pushHs}
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-semibold disabled:opacity-40"
>
HS push lead
</button>
</div>
</div>
)}
{!canOps && !noToken && (
<p className="text-xs text-slate-500">
{label(
'عمليات الاختبار والدفع تتطلب دور مالك أو مدير أو مسؤول.',
'Test and push operations require owner, manager, or admin role.',
)}
</p>
)}
{lastResult && (
<pre
dir="ltr"
className="mt-4 p-3 rounded-xl bg-black/40 border border-white/10 text-[11px] text-slate-300 overflow-x-auto whitespace-pre-wrap"
>
{lastResult}
</pre>
)}
</Section> </Section>
{/* API Key */} {routing && (
<Section title={label('توجيه نماذج الذكاء', 'LLM routing')} label={label}>
<p className="text-xs text-slate-500 mb-3">{routing.note_ar}</p>
<p className="text-xs text-slate-500 mb-2">
{label('المزودون المتاحون حسب مفاتيح الخادم:', 'Available providers (server keys):')}{' '}
{routing.available_providers.join(', ') || '—'}
</p>
<pre
dir="ltr"
className="p-3 rounded-xl bg-black/30 border border-white/10 text-[11px] text-slate-300 overflow-x-auto"
>
{JSON.stringify(routing.effective, null, 2)}
</pre>
</Section>
)}
<Section title={label('مفتاح API', 'API Key')} label={label}> <Section title={label('مفتاح API', 'API Key')} label={label}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@ -412,7 +693,10 @@ function IntegrationsTab({ label }: { label: L }) {
dir="ltr" dir="ltr"
className="flex-1 px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-slate-400 text-sm font-mono" className="flex-1 px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-slate-400 text-sm font-mono"
/> />
<button className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-sm text-slate-300 transition-all"> <button
type="button"
className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-sm text-slate-300 transition-all"
>
{label('نسخ', 'Copy')} {label('نسخ', 'Copy')}
</button> </button>
</div> </div>

View File

@ -0,0 +1,79 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { Activity, LineChart } from "lucide-react";
type Snap = {
strategic_deals_total: number;
negotiation_rounds_total: number;
avg_negotiation_rounds_per_deal: number;
deals_high_ai_confidence: number;
labels_ar: Record<string, string>;
loop_hints_ar: string[];
};
export function AgentQualityView() {
const [data, setData] = useState<Snap | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let on = true;
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/agent-quality/snapshot");
if (!on) return;
if (!r.ok) {
setErr(`تعذر التحميل (${r.status})`);
return;
}
setData(await r.json());
})();
return () => {
on = false;
};
}, []);
if (err) {
return <div className="glass-card p-6 m-4 text-destructive">{err}</div>;
}
if (!data) {
return <div className="glass-card p-6 m-4">جارٍ التحميل</div>;
}
return (
<div className="p-4 md:p-8 max-w-4xl mx-auto space-y-6">
<div>
<h1 className="text-2xl font-bold">حلقة جودة الوكلاء</h1>
<p className="text-sm text-muted-foreground mt-1">مؤشرات مساعدة من مسار الصفقات الاستراتيجية توسع لاحقاً بسجلات الرسائل والتحويل.</p>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="glass-card p-5 space-y-1">
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Activity className="w-4 h-4" />
{data.labels_ar.negotiation_depth}
</div>
<p className="text-2xl font-bold">{data.negotiation_rounds_total}</p>
<p className="text-xs text-muted-foreground">متوسط {data.avg_negotiation_rounds_per_deal} جولة / صفقة</p>
</div>
<div className="glass-card p-5 space-y-1">
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<LineChart className="w-4 h-4" />
{data.labels_ar.high_confidence_deals}
</div>
<p className="text-2xl font-bold">{data.deals_high_ai_confidence}</p>
<p className="text-xs text-muted-foreground">من أصل {data.strategic_deals_total} صفقة</p>
</div>
</div>
<div className="glass-card p-6 space-y-2">
<h2 className="font-semibold text-sm">تحسين مستمر</h2>
<ul className="list-disc pr-5 text-sm text-muted-foreground space-y-1">
{data.loop_hints_ar.map((h, i) => (
<li key={i}>{h}</li>
))}
</ul>
</div>
</div>
);
}

View File

@ -17,6 +17,31 @@ function useIsMobile() {
return mobile; return mobile;
} }
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return reduced;
}
function StaticHandshakeMark() {
return (
<div className="relative h-full w-full rounded-3xl border border-white/20 bg-gradient-to-br from-teal-500/25 to-cyan-500/20">
<div className="absolute inset-0 rounded-3xl bg-[radial-gradient(circle_at_50%_50%,rgba(45,212,191,0.35),transparent_60%)]" />
<svg viewBox="0 0 120 120" className="h-full w-full p-8">
<path d="M18 72 C35 52, 48 46, 60 50" fill="none" stroke="#5eead4" strokeWidth="9" strokeLinecap="round" />
<path d="M102 48 C85 68, 72 74, 60 70" fill="none" stroke="#ffffff" strokeWidth="9" strokeLinecap="round" />
<circle cx="60" cy="60" r="5" fill="#ffffff" />
</svg>
</div>
);
}
function HandShape({ position, rotation, color }: { function HandShape({ position, rotation, color }: {
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
@ -164,27 +189,33 @@ interface DealixLogo3DProps {
function DealixLogo3D({ size = 300, className }: DealixLogo3DProps) { function DealixLogo3D({ size = 300, className }: DealixLogo3DProps) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const prefersReducedMotion = usePrefersReducedMotion();
const lowSpec = isMobile || prefersReducedMotion;
return ( return (
<div <div
className={clsx('relative', className)} className={clsx('relative', className)}
style={{ width: size, height: size }} style={{ width: size, height: size }}
> >
{lowSpec ? (
<StaticHandshakeMark />
) : (
<Suspense fallback={<LoadingShimmer />}> <Suspense fallback={<LoadingShimmer />}>
<Canvas <Canvas
camera={{ position: [0, 0, 3], fov: 40 }} camera={{ position: [0, 0, 3], fov: 40 }}
dpr={isMobile ? 1 : [1, 2]} dpr={[1, 1.5]}
gl={{ alpha: true, antialias: !isMobile }} gl={{ alpha: true, antialias: true }}
style={{ background: 'transparent' }} style={{ background: 'transparent' }}
> >
<ambientLight intensity={0.4} /> <ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={1} color="#ffffff" /> <directionalLight position={[5, 5, 5]} intensity={1} color="#ffffff" />
<pointLight position={[0, 0, 2]} intensity={0.8} color="#14b8a6" /> <pointLight position={[0, 0, 2]} intensity={0.8} color="#14b8a6" />
<Float speed={1.5} rotationIntensity={0.2} floatIntensity={0.3}> <Float speed={1.5} rotationIntensity={0.2} floatIntensity={0.3}>
<HandshakeScene isMobile={isMobile} /> <HandshakeScene isMobile={lowSpec} />
</Float> </Float>
</Canvas> </Canvas>
</Suspense> </Suspense>
)}
{/* Ambient glow behind the canvas */} {/* Ambient glow behind the canvas */}
<div className="absolute inset-0 -z-10 rounded-full bg-teal-500/10 blur-3xl" /> <div className="absolute inset-0 -z-10 rounded-full bg-teal-500/10 blur-3xl" />

View File

@ -0,0 +1,73 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
type GatePayload = {
launch_allowed?: boolean;
readiness_percent_total?: number;
blocked_reasons?: string[];
};
export function GoLiveReadinessCard() {
const [state, setState] = useState<{
loading: boolean;
error: string | null;
data: GatePayload | null;
}>({ loading: true, error: null, data: null });
useEffect(() => {
let active = true;
const load = async () => {
try {
const r = await apiFetch("/api/v1/autonomous-foundation/integrations/go-live-gate", {
cache: "no-store",
});
const json = await r.json();
if (!active) return;
setState({ loading: false, error: null, data: json });
} catch (err) {
if (!active) return;
setState({
loading: false,
error: err instanceof Error ? err.message : "failed_to_load",
data: null,
});
}
};
load();
return () => {
active = false;
};
}, []);
if (state.loading) {
return <div className="glass-card p-6">جارٍ تحميل حالة الإطلاق</div>;
}
if (state.error) {
return <div className="glass-card p-6 text-destructive">تعذر جلب go-live-gate: {state.error}</div>;
}
const ok = Boolean(state.data?.launch_allowed);
const reasons = (state.data?.blocked_reasons || []).slice(0, 5);
return (
<div className="glass-card p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold">جاهزية الإطلاق التجاري</h3>
<span className={`text-xs px-2 py-1 rounded-full ${ok ? "bg-green-500/20 text-green-300" : "bg-amber-500/20 text-amber-300"}`}>
{ok ? "جاهز للإطلاق" : "يحتاج استكمال"}
</span>
</div>
<p className="text-sm text-muted-foreground">
readiness_percent_total: {state.data?.readiness_percent_total ?? "-"}%
</p>
{!ok && reasons.length > 0 && (
<ul className="list-disc pr-5 text-sm space-y-1 text-muted-foreground">
{reasons.map((reason) => (
<li key={reason}>{reason}</li>
))}
</ul>
)}
</div>
);
}

View File

@ -0,0 +1,103 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { GoLiveReadinessCard } from "./go-live-readiness-card";
import { Target, Shield, TrendingUp } from "lucide-react";
type GovSnapshot = {
operating_mode: { value: number; name: string; label_ar: string };
north_star_hints_ar: Record<string, string>;
governance_kpis: {
auto_send_enabled: boolean;
auto_negotiate_enabled: boolean;
max_auto_commitment_sar: number;
strategic_deals_total: number;
deals_with_negotiation_rounds: number;
};
};
export function GovernanceMetricsView() {
const [snap, setSnap] = useState<GovSnapshot | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let on = true;
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/governance/snapshot");
if (!on) return;
if (!r.ok) {
setErr(`تعذر جلب المؤشرات (${r.status})`);
return;
}
setSnap(await r.json());
})();
return () => {
on = false;
};
}, []);
return (
<div className="p-4 md:p-8 max-w-5xl mx-auto space-y-6">
<div>
<h1 className="text-2xl font-bold">الحوكمة والمؤشرات</h1>
<p className="text-sm text-muted-foreground mt-1">Go-live، وضع التشغيل، ومؤشرات موحّدة لثلاثة محاور المنتج.</p>
</div>
<GoLiveReadinessCard />
{err && <div className="text-destructive text-sm">{err}</div>}
{snap && (
<>
<div className="glass-card p-6 space-y-4">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-primary" />
<h2 className="font-semibold">وضع التشغيل والحدود</h2>
</div>
<p className="text-sm">
<span className="font-medium">{snap.operating_mode.label_ar}</span>
<span className="text-muted-foreground"> ({snap.operating_mode.name})</span>
</p>
<div className="grid sm:grid-cols-2 gap-3 text-sm">
<div className="rounded-lg bg-secondary/30 p-3">
إرسال تلقائي: {snap.governance_kpis.auto_send_enabled ? "مفعّل" : "غير مفعّل"}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
تفاوض تلقائي: {snap.governance_kpis.auto_negotiate_enabled ? "مفعّل" : "غير مفعّل"}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
حد الالتزام التلقائي: {snap.governance_kpis.max_auto_commitment_sar?.toLocaleString("ar-SA")} ر.س
</div>
<div className="rounded-lg bg-secondary/30 p-3">
صفقات استراتيجية: {snap.governance_kpis.strategic_deals_total}
</div>
<div className="rounded-lg bg-secondary/30 p-3 sm:col-span-2">
صفقات بجولات تفاوض مسجّلة: {snap.governance_kpis.deals_with_negotiation_rounds}
</div>
</div>
</div>
<div className="glass-card p-6 space-y-3">
<div className="flex items-center gap-2">
<Target className="w-5 h-5 text-primary" />
<h2 className="font-semibold">تلميحات North Star</h2>
</div>
<ul className="list-disc pr-5 text-sm text-muted-foreground space-y-1">
{Object.entries(snap.north_star_hints_ar).map(([k, v]) => (
<li key={k}>{v}</li>
))}
</ul>
</div>
</>
)}
<div className="glass-card p-6 flex gap-3 items-start">
<TrendingUp className="w-5 h-5 shrink-0 text-muted-foreground" />
<p className="text-xs text-muted-foreground">
اربط هذه اللوحة ببيانات القمع والقنوات الفعلية عند تفعيل التكاملات؛ المؤشرات الحالية تعكس طبقة الصفقات الاستراتيجية والحوكمة.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import Link from "next/link";
import { Rocket, CheckCircle2, ExternalLink } from "lucide-react";
type Phase = {
id: string;
title_ar: string;
items_ar: string[];
};
export function GrowthPlaybookView() {
const [phases, setPhases] = useState<Phase[]>([]);
const [done, setDone] = useState<Record<string, boolean>>({});
const [disclaimer, setDisclaimer] = useState("");
useEffect(() => {
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/growth/checklist");
if (!r.ok) return;
const j = await r.json();
setPhases(j.phases || []);
setDisclaimer(j.disclaimer_ar || "");
})();
}, []);
const toggle = (key: string) => {
setDone((d) => ({ ...d, [key]: !d[key] }));
};
return (
<div className="p-4 md:p-8 max-w-4xl mx-auto space-y-6">
<div className="flex items-start gap-3">
<Rocket className="w-8 h-8 text-primary shrink-0" />
<div>
<h1 className="text-2xl font-bold">نمو واستعداد استحواذ / توسع</h1>
<p className="text-sm text-muted-foreground mt-1">
مساعد قرار وتنظيم مهام الموافقات والعناية الواجبة الكاملة تبقى بشرية.
</p>
</div>
</div>
<div className="glass-card p-4 text-xs text-muted-foreground border border-amber-500/20 bg-amber-500/5">
{disclaimer}
</div>
<Link
href="/dashboard?section=intelligence"
className="inline-flex items-center gap-2 text-sm text-primary hover:underline"
>
<ExternalLink className="w-4 h-4" />
فتح لوحة الذكاء الاستراتيجي (Manus) للسيناريوهات والتقارير
</Link>
<div className="space-y-4">
{phases.map((ph) => (
<div key={ph.id} className="glass-card p-5 space-y-3">
<h2 className="font-semibold">{ph.title_ar}</h2>
<ul className="space-y-2">
{ph.items_ar.map((item, idx) => {
const key = `${ph.id}-${idx}`;
const checked = !!done[key];
return (
<li key={key} className="flex items-start gap-2 text-sm">
<button
type="button"
onClick={() => toggle(key)}
className="mt-0.5 text-primary shrink-0"
aria-pressed={checked}
>
<CheckCircle2 className={`w-5 h-5 ${checked ? "opacity-100" : "opacity-30"}`} />
</button>
<span className={checked ? "line-through text-muted-foreground" : ""}>{item}</span>
</li>
);
})}
</ul>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,119 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { Share2 } from "lucide-react";
type Profile = {
id: string;
company_name: string;
industry?: string | null;
};
type GraphPayload = {
profile_id: string;
company_name: string;
suggested_playbook_id: string | null;
counts: {
strategic_deals_as_initiator: number;
strategic_deals_as_target: number;
matches_as_party_a: number;
matches_as_party_b: number;
deals_with_lead_link: number;
deals_with_sales_deal_link: number;
};
};
export function IdentityGraphView() {
const [profiles, setProfiles] = useState<Profile[]>([]);
const [pid, setPid] = useState<string>("");
const [graph, setGraph] = useState<GraphPayload | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/profiles");
if (!r.ok) return;
const list = (await r.json()) as Profile[];
setProfiles(list);
setPid((p) => p || list[0]?.id || "");
})();
}, []);
useEffect(() => {
if (!pid) return;
void (async () => {
setErr(null);
const r = await apiFetch(`/api/v1/strategic-deals/identity/graph?profile_id=${encodeURIComponent(pid)}`);
if (!r.ok) {
setErr("تعذر تحميل الرسم البياني للكيان");
setGraph(null);
return;
}
setGraph(await r.json());
})();
}, [pid]);
return (
<div className="p-4 md:p-8 max-w-4xl mx-auto space-y-6">
<div className="flex items-start gap-3">
<Share2 className="w-8 h-8 text-primary shrink-0" />
<div>
<h1 className="text-2xl font-bold">طبقة الكيان الموحّد</h1>
<p className="text-sm text-muted-foreground mt-1">
ملف شركة واحد صفقات استراتيجية، مطابقات، وروابط CRM (خفيفة).
</p>
</div>
</div>
<div className="glass-card p-4">
<label className="text-xs text-muted-foreground block mb-2">ملف الشركة</label>
<select
className="w-full bg-secondary/50 border border-border rounded-xl px-3 py-2 text-sm"
value={pid}
onChange={(e) => setPid(e.target.value)}
>
<option value=""> اختر </option>
{profiles.map((p) => (
<option key={p.id} value={p.id}>
{p.company_name}
</option>
))}
</select>
</div>
{err && <div className="text-destructive text-sm">{err}</div>}
{graph && (
<div className="glass-card p-6 space-y-4">
<h2 className="font-semibold">{graph.company_name}</h2>
{graph.suggested_playbook_id && (
<p className="text-xs text-muted-foreground">
Playbook مقترح: <span className="text-foreground">{graph.suggested_playbook_id}</span>
</p>
)}
<div className="grid sm:grid-cols-2 gap-3 text-sm">
<div className="rounded-lg bg-secondary/30 p-3">
صفقات كمبادر: {graph.counts.strategic_deals_as_initiator}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
صفقات كهدف: {graph.counts.strategic_deals_as_target}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
مطابقات (طرف أ): {graph.counts.matches_as_party_a}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
مطابقات (طرف ب): {graph.counts.matches_as_party_b}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
صفقات مربوطة بـ lead: {graph.counts.deals_with_lead_link}
</div>
<div className="rounded-lg bg-secondary/30 p-3">
صفقات مربوطة بصفقة مبيعات: {graph.counts.deals_with_sales_deal_link}
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getApiBaseUrl } from "@/lib/api-base"; import { getApiBaseUrl } from "@/lib/api-base";
import { apiFetch } from "@/lib/api-client";
interface AgentStatus { interface AgentStatus {
role: string; role: string;
@ -39,8 +40,7 @@ export function IntelligenceDashboard() {
const fetchAgentStatus = async () => { const fetchAgentStatus = async () => {
try { try {
const base = getApiBaseUrl().replace(/\/$/, ""); const res = await apiFetch("/api/v1/agents/status", { cache: "no-store" });
const res = await fetch(`${base}/api/v1/agents/status`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setAgents(data.agents || []); setAgents(data.agents || []);
@ -50,8 +50,7 @@ export function IntelligenceDashboard() {
const fetchHealth = async () => { const fetchHealth = async () => {
try { try {
const base = getApiBaseUrl().replace(/\/$/, ""); const res = await apiFetch("/api/v1/intelligence/health", { cache: "no-store" });
const res = await fetch(`${base}/api/v1/intelligence/health`);
if (res.ok) setHealth(await res.json()); if (res.ok) setHealth(await res.json());
} catch {} } catch {}
}; };
@ -60,8 +59,7 @@ export function IntelligenceDashboard() {
if (!leadForm.contact_name || !leadForm.company_name) return; if (!leadForm.contact_name || !leadForm.company_name) return;
setLoading(true); setLoading(true);
try { try {
const base = getApiBaseUrl().replace(/\/$/, ""); const res = await apiFetch("/api/v1/intelligence/run-pipeline", {
const res = await fetch(`${base}/api/v1/intelligence/run-pipeline`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: `lead_${Date.now()}`, ...leadForm }), body: JSON.stringify({ id: `lead_${Date.now()}`, ...leadForm }),

View File

@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { getApiBaseUrl } from "@/lib/api-base"; import { getApiBaseUrl } from "@/lib/api-base";
import { apiFetch } from "@/lib/api-client";
export function LeadGeneratorView() { export function LeadGeneratorView() {
const [sector, setSector] = useState("تقنية المعلومات"); const [sector, setSector] = useState("تقنية المعلومات");
@ -27,8 +28,7 @@ export function LeadGeneratorView() {
setLoading(true); setLoading(true);
setLeads([]); setLeads([]);
try { try {
const base = getApiBaseUrl().replace(/\/$/, ""); const res = await apiFetch(`/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, {
const res = await fetch(`${base}/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, {
method: "POST" method: "POST"
}); });
if (res.ok) { if (res.ok) {
@ -56,8 +56,7 @@ export function LeadGeneratorView() {
const runPipeline = async (lead: any) => { const runPipeline = async (lead: any) => {
setPipelineRunning(lead.company_name); setPipelineRunning(lead.company_name);
try { try {
const base = getApiBaseUrl().replace(/\/$/, ""); const res = await apiFetch("/api/v1/dealix/full-power", {
const res = await fetch(`${base}/api/v1/dealix/full-power`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -0,0 +1,171 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import {
BookOpen,
FileText,
Gavel,
Megaphone,
Route,
Sparkles,
Target,
ExternalLink,
} from "lucide-react";
type ProgramPayload = {
title_ar?: string;
journey_ar?: { step: number; title: string; detail_ar: string }[];
};
const LEGAL_LINKS = [
{ href: "/strategy/legal/affiliate-rules-ar.md", label: "قواعد المسوقين بالعمولة", summary: "مسموح ومحظور، عقوبات، حقوق المسوق." },
{ href: "/strategy/legal/commission-policy-ar.md", label: "سياسة العمولات", summary: "هيكل العمولات والمستحقات." },
{ href: "/strategy/legal/consent-policy-ar.md", label: "سياسة الموافقة", summary: "التواصل والموافقات المطلوبة." },
] as const;
const PLAYBOOK_STEPS = [
{
phase: "تأهيل",
items: [
{ label: "تعرّف على البرنامج والعمولة", section: "affiliates" as const },
{ label: "اقرأ القواعد والموافقة", section: null },
],
},
{
phase: "محتوى وجاهزية",
items: [
{ label: "البرزنتيشنات القطاعية", section: "presentations" as const },
{ label: "سكربتات المكالمات والواتساب", section: "scripts" as const },
],
},
{
phase: "توليد ومتابعة",
items: [
{ label: "توليد عملاء محتملين", section: "leads" as const },
{ label: "صندوق الوارد الموحّد", section: "inbox" as const },
],
},
{
phase: "شراكات متقدمة",
items: [{ label: "Partnership Studio", section: "partnership-studio" as const }],
},
];
function dash(section: string) {
return `/dashboard?section=${section}`;
}
export function MarketerHubView() {
const [program, setProgram] = useState<ProgramPayload | null>(null);
const load = useCallback(async () => {
const r = await apiFetch("/api/v1/affiliates/program");
if (r.ok) setProgram(await r.json());
}, []);
useEffect(() => {
void load();
}, [load]);
return (
<div className="p-4 md:p-8 max-w-5xl mx-auto space-y-8 animate-in fade-in duration-300">
<header className="dealix-section-header space-y-2">
<div className="flex items-center gap-3">
<Megaphone className="w-9 h-9 text-primary shrink-0" />
<div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">مركز المسوق</h1>
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">
مسار واحد: قواعد البرنامج، الأدوات في Dealix، والوثائق الرسمية بدون تشتيت بين الشاشات.
</p>
</div>
</div>
</header>
<section className="glass-card p-6 space-y-4">
<div className="flex items-center gap-2 text-primary">
<Sparkles className="w-5 h-5" />
<h2 className="font-semibold text-lg">برنامج العمولة (مختصر مباشر من API)</h2>
</div>
{program?.title_ar && <p className="text-sm font-medium">{program.title_ar}</p>}
<ul className="space-y-2 text-sm text-muted-foreground">
{(program?.journey_ar || []).slice(0, 4).map((s) => (
<li key={s.step} className="flex gap-2">
<span className="text-primary font-mono text-xs pt-0.5">{s.step}</span>
<span>
<span className="text-foreground font-medium">{s.title}</span> {s.detail_ar}
</span>
</li>
))}
{!program?.journey_ar?.length && <li>جارٍ التحميل أو لا توجد بيانات برنامج بعد.</li>}
</ul>
<Link
href={dash("affiliates")}
className="inline-flex items-center gap-2 text-sm text-primary hover:underline"
>
<Target className="w-4 h-4" />
فتح لوحة المسوقين والترتيب الكامل
</Link>
</section>
<section className="glass-card p-6 space-y-4">
<div className="flex items-center gap-2">
<Route className="w-5 h-5 text-primary" />
<h2 className="font-semibold text-lg">Playbook المسار</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{PLAYBOOK_STEPS.map((block) => (
<div key={block.phase} className="rounded-xl border border-border/60 bg-secondary/20 p-4 space-y-2">
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{block.phase}</p>
<ul className="space-y-2">
{block.items.map((it) => (
<li key={it.label}>
{it.section ? (
<Link href={dash(it.section)} className="text-sm text-primary hover:underline inline-flex items-center gap-1">
{it.label}
<ExternalLink className="w-3 h-3 opacity-70" />
</Link>
) : (
<span className="text-sm text-muted-foreground">{it.label}</span>
)}
</li>
))}
</ul>
</div>
))}
</div>
</section>
<section className="glass-card p-6 space-y-4">
<div className="flex items-center gap-2">
<Gavel className="w-5 h-5 text-muted-foreground" />
<h2 className="font-semibold text-lg">الوثائق القانونية (ملخص + الملف الكامل)</h2>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{LEGAL_LINKS.map((doc) => (
<a
key={doc.href}
href={doc.href}
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-xl border border-border/50 hover:bg-secondary/40 transition-colors"
>
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 text-primary shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium">{doc.label}</p>
<p className="text-xs text-muted-foreground mt-1">{doc.summary}</p>
</div>
</div>
</a>
))}
</div>
<p className="text-xs text-muted-foreground flex items-center gap-2">
<BookOpen className="w-4 h-4" />
للربط الشامل بالبيئة والتكاملات راجع وثيقة INTEGRATION_MASTER في المستودع أو مسار الاستراتيجية العامة.
</p>
</section>
</div>
);
}

View File

@ -0,0 +1,161 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { Shield, Users, Clock, Gavel } from "lucide-react";
type ModeRow = {
mode: number;
name: string;
label_ar: string;
description_ar: string;
auto_send: boolean;
auto_negotiate: boolean;
max_auto_commitment_sar: number;
allowed_channels: string[];
};
type OperatingPayload = {
current: ModeRow & { name: string };
modes: ModeRow[];
roles_ar: { id: string; label: string; scope: string }[];
sla_hints_ar: Record<string, string>;
};
export function OperatingModelView() {
const [data, setData] = useState<OperatingPayload | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setError(null);
const r = await apiFetch("/api/v1/strategic-deals/operating-model");
if (!r.ok) {
setError(`تعذر التحميل (${r.status})`);
setLoading(false);
return;
}
setData(await r.json());
setLoading(false);
}, []);
useEffect(() => {
void load();
}, [load]);
const setMode = async (mode: number) => {
setSaving(true);
setError(null);
const r = await apiFetch("/api/v1/strategic-deals/operating-model", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
setSaving(false);
if (!r.ok) {
const j = await r.json().catch(() => ({}));
setError((j as { detail?: string }).detail || "فشل الحفظ");
return;
}
await load();
};
if (loading) {
return <div className="glass-card p-6 m-4">جارٍ تحميل نموذج التشغيل</div>;
}
if (error && !data) {
return <div className="glass-card p-6 m-4 text-destructive">{error}</div>;
}
if (!data) return null;
return (
<div className="p-4 md:p-8 max-w-5xl mx-auto space-y-6 animate-in fade-in duration-300">
<div>
<h1 className="text-2xl font-bold">نموذج تشغيل Dealix OS</h1>
<p className="text-sm text-muted-foreground mt-1">
أدوار، حلقات قرار، وحدود أتمتة الذكاء الاصطناعي مربوطة بوضع التشغيل في الخادم.
</p>
</div>
{error && <div className="text-sm text-amber-500">{error}</div>}
<div className="glass-card p-6 space-y-4">
<div className="flex items-center gap-2 text-primary">
<Shield className="w-5 h-5" />
<h2 className="font-semibold">الوضع الحالي</h2>
</div>
<p className="text-lg font-medium">{data.current.label_ar}</p>
<p className="text-sm text-muted-foreground">{data.current.description_ar}</p>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 rounded-full bg-secondary">
إرسال تلقائي: {data.current.auto_send ? "نعم" : "لا"}
</span>
<span className="px-2 py-1 rounded-full bg-secondary">
تفاوض تلقائي: {data.current.auto_negotiate ? "نعم" : "لا"}
</span>
<span className="px-2 py-1 rounded-full bg-secondary">
حد الالتزام التلقائي: {data.current.max_auto_commitment_sar?.toLocaleString("ar-SA")} ر.س
</span>
</div>
</div>
<div className="glass-card p-6 space-y-3">
<h2 className="font-semibold">تغيير وضع التشغيل (04)</h2>
<div className="grid sm:grid-cols-2 gap-2">
{data.modes.map((m) => (
<button
key={m.mode}
type="button"
disabled={saving}
onClick={() => void setMode(m.mode)}
className={`text-right p-3 rounded-xl border transition-colors ${
data.current.mode === m.mode
? "border-primary bg-primary/10"
: "border-border hover:bg-secondary/40"
}`}
>
<div className="font-medium">{m.label_ar}</div>
<div className="text-xs text-muted-foreground line-clamp-2">{m.description_ar}</div>
</button>
))}
</div>
</div>
<div className="glass-card p-6 space-y-3">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-muted-foreground" />
<h2 className="font-semibold">أدوار مقترحة</h2>
</div>
<ul className="space-y-2 text-sm">
{data.roles_ar.map((r) => (
<li key={r.id} className="border-b border-border/50 pb-2 last:border-0">
<span className="font-medium">{r.label}</span>
<span className="text-muted-foreground"> {r.scope}</span>
</li>
))}
</ul>
</div>
<div className="glass-card p-6 space-y-3">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-muted-foreground" />
<h2 className="font-semibold">إرشادات SLA داخلية</h2>
</div>
<ul className="list-disc pr-5 text-sm text-muted-foreground space-y-1">
{Object.values(data.sla_hints_ar).map((t, i) => (
<li key={i}>{t}</li>
))}
</ul>
</div>
<div className="glass-card p-6 flex gap-3 items-start">
<Gavel className="w-5 h-5 shrink-0 text-muted-foreground mt-0.5" />
<p className="text-xs text-muted-foreground">
الالتزامات القانونية والمالية النهائية تبقى بشرية؛ الوضع الاستراتيجي يسمح بأتمتة أوسع مع تصعيد إلزامي عند الحدود.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,540 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { Handshake, Plus, RefreshCw } from "lucide-react";
type Profile = {
id: string;
company_name: string;
industry?: string | null;
capabilities?: string[];
needs?: string[];
};
type StrategicDeal = {
id: string;
deal_title: string;
deal_type: string;
status: string;
estimated_value_sar?: number | null;
lead_id?: string | null;
sales_deal_id?: string | null;
};
type MatchRow = {
id: string;
match_score: number;
company_b_name?: string | null;
status: string;
deal_type_suggested?: string | null;
};
const DEAL_TYPES = [
"partnership",
"distribution",
"franchise",
"jv",
"referral",
"acquisition",
"barter",
];
export function PartnershipStudioView() {
const [tab, setTab] = useState<"profiles" | "deals" | "matches" | "policy" | "archetypes">("profiles");
const [profiles, setProfiles] = useState<Profile[]>([]);
const [deals, setDeals] = useState<StrategicDeal[]>([]);
const [matches, setMatches] = useState<MatchRow[]>([]);
const [selProfile, setSelProfile] = useState<string>("");
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [newProfile, setNewProfile] = useState({
company_name: "",
industry: "",
capabilities: "",
needs: "",
});
const [newDeal, setNewDeal] = useState({
deal_title: "",
deal_type: "partnership",
target_company_name: "",
estimated_value_sar: "",
channel: "whatsapp",
lead_id: "",
sales_deal_id: "",
});
const [policy, setPolicy] = useState({
channel: "whatsapp",
action: "send_custom_message",
deal_value_sar: "0",
industry: "",
});
const [policyResult, setPolicyResult] = useState<Record<string, unknown> | null>(null);
const [archetypes, setArchetypes] = useState<Record<string, unknown>[]>([]);
const loadProfiles = useCallback(async () => {
const r = await apiFetch("/api/v1/strategic-deals/profiles");
if (r.ok) {
const list = (await r.json()) as Profile[];
setProfiles(list);
setSelProfile((prev) => prev || list[0]?.id || "");
}
}, []);
const loadDeals = useCallback(async () => {
if (!selProfile) return;
const r = await apiFetch(
`/api/v1/strategic-deals?profile_id=${encodeURIComponent(selProfile)}&per_page=50`,
);
if (r.ok) setDeals(await r.json());
}, [selProfile]);
const loadMatches = useCallback(async () => {
if (!selProfile) return;
const r = await apiFetch(
`/api/v1/strategic-deals/matches?profile_id=${encodeURIComponent(selProfile)}&per_page=50`,
);
if (r.ok) setMatches(await r.json());
}, [selProfile]);
useEffect(() => {
void loadProfiles();
}, [loadProfiles]);
useEffect(() => {
void loadDeals();
void loadMatches();
}, [loadDeals, loadMatches]);
useEffect(() => {
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/partner-archetypes");
if (r.ok) {
const j = await r.json();
setArchetypes(j.archetypes || []);
}
})();
}, []);
const refresh = async () => {
setLoading(true);
setMsg(null);
await loadProfiles();
await loadDeals();
await loadMatches();
setLoading(false);
};
const createProfile = async () => {
setMsg(null);
const caps = newProfile.capabilities.split(",").map((s) => s.trim()).filter(Boolean);
const needs = newProfile.needs.split(",").map((s) => s.trim()).filter(Boolean);
const r = await apiFetch("/api/v1/strategic-deals/profiles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
company_name: newProfile.company_name,
industry: newProfile.industry || undefined,
capabilities: caps,
needs,
}),
});
if (!r.ok) {
const e = await r.json().catch(() => ({}));
setMsg((e as { detail?: string }).detail || "فشل إنشاء الملف");
return;
}
setNewProfile({ company_name: "", industry: "", capabilities: "", needs: "" });
await loadProfiles();
setMsg("تم إنشاء ملف الشركة.");
};
const createDeal = async () => {
if (!selProfile) {
setMsg("اختر ملف شركة مبادر.");
return;
}
setMsg(null);
const body: Record<string, unknown> = {
initiator_profile_id: selProfile,
deal_title: newDeal.deal_title,
deal_type: newDeal.deal_type,
channel: newDeal.channel,
target_company_name: newDeal.target_company_name || undefined,
estimated_value_sar: newDeal.estimated_value_sar
? Number(newDeal.estimated_value_sar)
: undefined,
};
if (newDeal.lead_id.trim()) body.lead_id = newDeal.lead_id.trim();
if (newDeal.sales_deal_id.trim()) body.sales_deal_id = newDeal.sales_deal_id.trim();
const r = await apiFetch("/api/v1/strategic-deals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) {
const e = await r.json().catch(() => ({}));
setMsg((e as { detail?: string }).detail || "فشل إنشاء الصفقة");
return;
}
setNewDeal((d) => ({
...d,
deal_title: "",
target_company_name: "",
estimated_value_sar: "",
lead_id: "",
sales_deal_id: "",
}));
await loadDeals();
setMsg("تم إنشاء الصفقة الاستراتيجية.");
};
const evaluatePolicy = async () => {
setMsg(null);
const r = await apiFetch("/api/v1/strategic-deals/policy/evaluate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
channel: policy.channel,
action: policy.action,
deal_value_sar: Number(policy.deal_value_sar) || 0,
industry: policy.industry || undefined,
}),
});
if (!r.ok) {
setMsg("فشل تقييم السياسة");
return;
}
setPolicyResult(await r.json());
};
const patchDealLinks = async (dealId: string, leadId: string, salesId: string) => {
const body: Record<string, string> = {};
if (leadId.trim()) body.lead_id = leadId.trim();
if (salesId.trim()) body.sales_deal_id = salesId.trim();
if (!Object.keys(body).length) return;
const r = await apiFetch(`/api/v1/strategic-deals/${dealId}/links`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (r.ok) await loadDeals();
};
return (
<div className="p-4 md:p-8 max-w-6xl mx-auto space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<Handshake className="w-8 h-8 text-primary" />
<div>
<h1 className="text-2xl font-bold">Partnership Studio</h1>
<p className="text-sm text-muted-foreground">ملفات، صفقات، مطابقات، سياسات، وأنماط شراكة عبر REST الموحّد.</p>
</div>
</div>
<button
type="button"
onClick={() => void refresh()}
disabled={loading}
className="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-border text-sm"
>
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
تحديث
</button>
</div>
{msg && <div className="text-sm text-primary">{msg}</div>}
<div className="flex flex-wrap gap-2 border-b border-border pb-2">
{(
[
["profiles", "الملفات"],
["deals", "الصفقات"],
["matches", "المطابقات"],
["policy", "محرك السياسات"],
["archetypes", "أنماط الشراكة"],
] as const
).map(([id, label]) => (
<button
key={id}
type="button"
onClick={() => setTab(id)}
className={`px-3 py-1.5 rounded-lg text-sm ${
tab === id ? "bg-primary/15 text-primary font-medium" : "text-muted-foreground"
}`}
>
{label}
</button>
))}
</div>
<div className="glass-card p-4 flex flex-wrap gap-3 items-center">
<span className="text-sm text-muted-foreground">ملف المبادر النشط:</span>
<select
className="bg-secondary/50 border border-border rounded-lg px-2 py-1 text-sm min-w-[200px]"
value={selProfile}
onChange={(e) => setSelProfile(e.target.value)}
>
{profiles.map((p) => (
<option key={p.id} value={p.id}>
{p.company_name}
</option>
))}
</select>
</div>
{tab === "profiles" && (
<div className="grid md:grid-cols-2 gap-6">
<div className="glass-card p-5 space-y-3">
<h2 className="font-semibold flex items-center gap-2">
<Plus className="w-4 h-4" /> ملف شركة جديد
</h2>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="اسم الشركة *"
value={newProfile.company_name}
onChange={(e) => setNewProfile({ ...newProfile, company_name: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="القطاع"
value={newProfile.industry}
onChange={(e) => setNewProfile({ ...newProfile, industry: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="قدرات (مفصولة بفاصلة)"
value={newProfile.capabilities}
onChange={(e) => setNewProfile({ ...newProfile, capabilities: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="احتياجات (مفصولة بفاصلة)"
value={newProfile.needs}
onChange={(e) => setNewProfile({ ...newProfile, needs: e.target.value })}
/>
<button
type="button"
onClick={() => void createProfile()}
className="w-full py-2 rounded-xl bg-primary text-primary-foreground text-sm font-medium"
>
إنشاء
</button>
</div>
<div className="glass-card p-5 space-y-2 max-h-[400px] overflow-y-auto">
<h2 className="font-semibold mb-2">الملفات الحالية</h2>
{profiles.map((p) => (
<div key={p.id} className="text-sm border-b border-border/50 pb-2">
<div className="font-medium">{p.company_name}</div>
<div className="text-xs text-muted-foreground">{p.industry || "—"}</div>
</div>
))}
{!profiles.length && <p className="text-sm text-muted-foreground">لا توجد ملفات بعد.</p>}
</div>
</div>
)}
{tab === "deals" && (
<div className="space-y-6">
<div className="glass-card p-5 space-y-3">
<h2 className="font-semibold">صفقة استراتيجية جديدة</h2>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="عنوان الصفقة *"
value={newDeal.deal_title}
onChange={(e) => setNewDeal({ ...newDeal, deal_title: e.target.value })}
/>
<div className="grid sm:grid-cols-2 gap-2">
<select
className="bg-secondary/50 border border-border rounded-lg px-2 py-2 text-sm"
value={newDeal.deal_type}
onChange={(e) => setNewDeal({ ...newDeal, deal_type: e.target.value })}
>
{DEAL_TYPES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<select
className="bg-secondary/50 border border-border rounded-lg px-2 py-2 text-sm"
value={newDeal.channel}
onChange={(e) => setNewDeal({ ...newDeal, channel: e.target.value })}
>
<option value="whatsapp">whatsapp</option>
<option value="email">email</option>
<option value="linkedin">linkedin</option>
</select>
</div>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="اسم الشركة المستهدفة"
value={newDeal.target_company_name}
onChange={(e) => setNewDeal({ ...newDeal, target_company_name: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="قيمة تقديرية (ر.س)"
value={newDeal.estimated_value_sar}
onChange={(e) => setNewDeal({ ...newDeal, estimated_value_sar: e.target.value })}
/>
<div className="grid sm:grid-cols-2 gap-2">
<input
className="bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="ربط lead_id (اختياري)"
value={newDeal.lead_id}
onChange={(e) => setNewDeal({ ...newDeal, lead_id: e.target.value })}
/>
<input
className="bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="ربط sales_deal_id (اختياري)"
value={newDeal.sales_deal_id}
onChange={(e) => setNewDeal({ ...newDeal, sales_deal_id: e.target.value })}
/>
</div>
<button
type="button"
onClick={() => void createDeal()}
className="px-4 py-2 rounded-xl bg-primary text-primary-foreground text-sm"
>
إنشاء صفقة
</button>
</div>
<div className="glass-card p-5 space-y-3 overflow-x-auto">
<h2 className="font-semibold">الصفقات</h2>
<table className="w-full text-sm text-right min-w-[640px]">
<thead>
<tr className="text-muted-foreground border-b border-border">
<th className="py-2">العنوان</th>
<th className="py-2">النوع</th>
<th className="py-2">الحالة</th>
<th className="py-2">ربط CRM</th>
</tr>
</thead>
<tbody>
{deals.map((d) => (
<tr key={d.id} className="border-b border-border/40">
<td className="py-2">{d.deal_title}</td>
<td className="py-2">{d.deal_type}</td>
<td className="py-2">{d.status}</td>
<td className="py-2">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
lead: {d.lead_id || "—"} | sales: {d.sales_deal_id || "—"}
</span>
<div className="flex gap-1 flex-wrap">
<input
id={`lead-${d.id}`}
className="w-28 bg-secondary/50 border border-border rounded px-1 py-0.5 text-xs"
placeholder="lead uuid"
/>
<input
id={`sales-${d.id}`}
className="w-28 bg-secondary/50 border border-border rounded px-1 py-0.5 text-xs"
placeholder="deal uuid"
/>
<button
type="button"
className="text-xs text-primary"
onClick={() => {
const li = document.getElementById(`lead-${d.id}`) as HTMLInputElement;
const si = document.getElementById(`sales-${d.id}`) as HTMLInputElement;
void patchDealLinks(d.id, li?.value || "", si?.value || "");
}}
>
حفظ الربط
</button>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
{!deals.length && <p className="text-sm text-muted-foreground">لا صفقات لهذا الملف.</p>}
</div>
</div>
)}
{tab === "matches" && (
<div className="glass-card p-5 space-y-2">
<h2 className="font-semibold">مطابقات مقترحة</h2>
{matches.map((m) => (
<div key={m.id} className="text-sm border-b border-border/50 py-2 flex justify-between gap-2">
<div>
<div className="font-medium">{m.company_b_name || "طرف ب"}</div>
<div className="text-xs text-muted-foreground">
score {m.match_score.toFixed(2)} · {m.deal_type_suggested || "—"} · {m.status}
</div>
</div>
</div>
))}
{!matches.length && <p className="text-sm text-muted-foreground">لا مطابقات بعد جرّب فحص الاكتشاف من الـ API.</p>}
</div>
)}
{tab === "policy" && (
<div className="glass-card p-5 space-y-4 max-w-xl">
<h2 className="font-semibold">تقييم سياسة الإجراء</h2>
<select
className="w-full bg-secondary/50 border border-border rounded-lg px-2 py-2 text-sm"
value={policy.channel}
onChange={(e) => setPolicy({ ...policy, channel: e.target.value })}
>
<option value="whatsapp">whatsapp</option>
<option value="email">email</option>
<option value="linkedin">linkedin</option>
</select>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="action (مثلاً send_custom_message)"
value={policy.action}
onChange={(e) => setPolicy({ ...policy, action: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="قيمة الصفقة ر.س"
value={policy.deal_value_sar}
onChange={(e) => setPolicy({ ...policy, deal_value_sar: e.target.value })}
/>
<input
className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm"
placeholder="قطاع (لاختبار القطاعات الحساسة)"
value={policy.industry}
onChange={(e) => setPolicy({ ...policy, industry: e.target.value })}
/>
<button
type="button"
onClick={() => void evaluatePolicy()}
className="px-4 py-2 rounded-xl bg-primary text-primary-foreground text-sm"
>
تقييم
</button>
{policyResult && (
<pre className="text-xs bg-secondary/40 p-3 rounded-lg overflow-auto whitespace-pre-wrap">
{JSON.stringify(policyResult, null, 2)}
</pre>
)}
</div>
)}
{tab === "archetypes" && (
<div className="glass-card p-5 space-y-3 max-h-[560px] overflow-y-auto">
<h2 className="font-semibold">من نوع الصفقة إلى النمط التشغيلي</h2>
{archetypes.map((a, i) => (
<div key={i} className="text-sm border-b border-border/40 pb-3">
<div className="font-medium">{(a as { deal_type?: string }).deal_type}</div>
<div className="text-muted-foreground">{(a as { label_ar?: string }).label_ar}</div>
<div className="text-xs mt-1">{(a as { description_ar?: string }).description_ar}</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -2,6 +2,8 @@
import { useRef } from "react"; import { useRef } from "react";
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import Link from "next/link";
import { DealixLogo3D } from "./dealix-3d-logo";
import { import {
Zap, Zap,
MessageSquare, MessageSquare,
@ -85,6 +87,35 @@ const steps = [
{ num: "٣", title: "ابدأ البيع", desc: "دع الذكاء الاصطناعي يساعدك في إتمام المزيد من الصفقات" }, { num: "٣", title: "ابدأ البيع", desc: "دع الذكاء الاصطناعي يساعدك في إتمام المزيد من الصفقات" },
]; ];
/** فروقات يمكن التحقق منها في المستودع والوثائق — بدون أرقام سوق غير مثبتة */
const provenDifferentiators = [
{
icon: FileText,
title: "مسارات API موثقة",
desc: "خريطة API في المستودع ومطابقة OpenAPI عبر سكربت التحقق من الفرونت.",
},
{
icon: ShieldCheck,
title: "حوكمة وإطلاق منضبط",
desc: "بوابة go-live وفحوص تكامل موثقة في قائمة الإطلاق وليس مجرد وعود في الواجهة.",
},
{
icon: MessageSquare,
title: "قنوات محلية أولاً",
desc: "تجربة عربية RTL وواتساب ضمن مسار المنتج، وليس قالباً أمريكياً مترجماً فقط.",
},
{
icon: Building2,
title: "شراكات B2B في نفس المنصة",
desc: "مسارات الصفقات الاستراتيجية وPartnership Studio بجانب محرك المبيعات.",
},
{
icon: BrainCircuit,
title: "توجيه نماذج حسب المهمة",
desc: "سياسة LLM لكل نوع عمل دون إرسال مفاتيح إلى المتصفح.",
},
] as const;
const pricingPlans = [ const pricingPlans = [
{ {
name: "Starter", name: "Starter",
@ -116,25 +147,6 @@ const pricingPlans = [
}, },
]; ];
/* ───────────── 3D Logo placeholder ───────────── */
function DealixLogo3D() {
return (
<motion.div
animate={{ y: [0, -12, 0] }}
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut" }}
className="relative w-48 h-48 md:w-64 md:h-64"
>
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-teal-500 via-emerald-500 to-cyan-400 opacity-80 blur-2xl animate-pulse" />
<div className="relative w-full h-full rounded-3xl bg-gradient-to-br from-teal-500 via-emerald-500 to-cyan-400 flex items-center justify-center shadow-2xl shadow-teal-500/30 border border-white/20">
<div className="text-center">
<Zap className="w-12 h-12 md:w-16 md:h-16 text-black mx-auto" />
<span className="text-xl md:text-2xl font-black text-black tracking-tighter mt-2 block">DEALIX</span>
</div>
</div>
</motion.div>
);
}
/* ───────────── main component ───────────── */ /* ───────────── main component ───────────── */
export function PremiumLanding() { export function PremiumLanding() {
return ( return (
@ -156,11 +168,12 @@ export function PremiumLanding() {
{/* ═══════════ NAV ═══════════ */} {/* ═══════════ NAV ═══════════ */}
<nav className="relative z-10 flex items-center justify-between px-6 md:px-12 py-5 max-w-7xl mx-auto"> <nav className="relative z-10 flex items-center justify-between px-6 md:px-12 py-5 max-w-7xl mx-auto">
<button className="px-5 py-2 rounded-xl bg-teal-500 text-black font-bold text-sm hover:bg-teal-400 transition-colors shadow-lg shadow-teal-500/20"> <Link href="/register" className="px-5 py-2 rounded-xl bg-teal-500 text-black font-bold text-sm hover:bg-teal-400 transition-colors shadow-lg shadow-teal-500/20">
ابدأ مجاناً ابدأ مجاناً
</button> </Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-white/60"> <div className="hidden md:flex items-center gap-8 text-sm font-medium text-white/60">
<a href="#pricing" className="hover:text-white transition-colors">الأسعار</a> <a href="#pricing" className="hover:text-white transition-colors">الأسعار</a>
<a href="#differentiators" className="hover:text-white transition-colors">لماذا Dealix</a>
<a href="#features" className="hover:text-white transition-colors">المميزات</a> <a href="#features" className="hover:text-white transition-colors">المميزات</a>
<a href="#how" className="hover:text-white transition-colors">كيف يعمل</a> <a href="#how" className="hover:text-white transition-colors">كيف يعمل</a>
</div> </div>
@ -177,7 +190,7 @@ export function PremiumLanding() {
<div className="flex flex-col-reverse md:flex-row items-center gap-12 md:gap-8"> <div className="flex flex-col-reverse md:flex-row items-center gap-12 md:gap-8">
{/* left: 3D logo */} {/* left: 3D logo */}
<motion.div variants={fadeUp} custom={2} className="shrink-0"> <motion.div variants={fadeUp} custom={2} className="shrink-0">
<DealixLogo3D /> <DealixLogo3D size={280} />
</motion.div> </motion.div>
{/* right: text */} {/* right: text */}
@ -187,26 +200,27 @@ export function PremiumLanding() {
custom={0} custom={0}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-black leading-tight mb-6" className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-black leading-tight mb-6"
> >
نظام المبيعات الذكي نظام تشغيل الإيرادات
<br /> <br />
<span className="text-teal-400">للسعودية</span> <span className="text-teal-400">للشركات والشراكات</span>
</motion.h1> </motion.h1>
<motion.p <motion.p
variants={fadeUp} variants={fadeUp}
custom={1} custom={1}
className="text-lg md:text-xl text-white/60 mb-8 max-w-xl leading-relaxed" className="text-lg md:text-xl text-white/60 mb-8 max-w-xl leading-relaxed"
> >
وحّد فريق مبيعاتك مع واتساب، أتمت المتابعة بالذكاء الاصطناعي، وتابع كل صفقة من البداية للإغلاق منصة واحدة تدير دورة البيع كاملة: توليد العملاء، التفاوض، الإغلاق، إدارة الشركاء،
وتشغيل القنوات الذكية عبر واتساب وإيميل ولينكدإن.
</motion.p> </motion.p>
<motion.div variants={fadeUp} custom={2} className="flex flex-wrap gap-4"> <motion.div variants={fadeUp} custom={2} className="flex flex-wrap gap-4">
<button className="px-8 py-4 rounded-2xl bg-teal-500 text-black font-black text-base hover:bg-teal-400 transition-all shadow-xl shadow-teal-500/25 flex items-center gap-2"> <Link href="/register" className="px-8 py-4 rounded-2xl bg-teal-500 text-black font-black text-base hover:bg-teal-400 transition-all shadow-xl shadow-teal-500/25 flex items-center gap-2">
ابدأ مجاناً ابدأ مجاناً
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
</button> </Link>
<button className="px-8 py-4 rounded-2xl bg-white/5 border border-white/10 font-bold text-base hover:bg-white/10 transition-all flex items-center gap-2"> <Link href="/dashboard" className="px-8 py-4 rounded-2xl bg-white/5 border border-white/10 font-bold text-base hover:bg-white/10 transition-all flex items-center gap-2">
<Play className="w-4 h-4" /> <Play className="w-4 h-4" />
شاهد العرض استكشف المنصة
</button> </Link>
</motion.div> </motion.div>
</div> </div>
</div> </div>
@ -215,18 +229,18 @@ export function PremiumLanding() {
<motion.div <motion.div
variants={fadeUp} variants={fadeUp}
custom={3} custom={3}
className="mt-20 grid grid-cols-3 gap-4 max-w-2xl mx-auto" className="mt-20 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-3xl mx-auto"
> >
{[ {[
{ label: "شركة سعودية", value: "+٥٠٠" }, { label: "اللغة والسياق", value: "عربي · سعودي أولاً" },
{ label: "رضا العملاء", value: "٩٥٪" }, { label: "تكامل CRM", value: "Salesforce · HubSpot عبر API" },
{ label: "صفقة مغلقة", value: "+١٠٠٠" }, { label: "الشفافية", value: "وثائق + مصفوفة تنافسية" },
].map((s, i) => ( ].map((s, i) => (
<div <div
key={i} key={i}
className="text-center py-4 px-3 rounded-2xl bg-white/[0.03] border border-white/[0.06]" className="text-center py-4 px-3 rounded-2xl bg-white/[0.03] border border-white/[0.06]"
> >
<p className="text-2xl md:text-3xl font-black text-teal-400">{s.value}</p> <p className="text-sm md:text-base font-bold text-teal-400 leading-snug">{s.value}</p>
<p className="text-xs text-white/40 font-medium mt-1">{s.label}</p> <p className="text-xs text-white/40 font-medium mt-1">{s.label}</p>
</div> </div>
))} ))}
@ -286,6 +300,40 @@ export function PremiumLanding() {
</div> </div>
</Section> </Section>
{/* ═══════════ PROVEN DIFFERENTIATORS ═══════════ */}
<Section id="differentiators" className="max-w-7xl mx-auto px-6 md:px-12 py-20">
<motion.h2 variants={fadeUp} className="text-3xl md:text-4xl font-black text-center mb-4">
فروقات يمكن إثباتها لا أرقام وهمية
</motion.h2>
<motion.p variants={fadeUp} custom={1} className="text-center text-white/50 mb-4 max-w-2xl mx-auto">
ما يلي مربوط بمسارات الكود والوثائق في المستودع؛ راجع المصفوفة التفصيلية للمقارنة مع فئات الأدوات العالمية.
</motion.p>
<motion.p variants={fadeUp} custom={2} className="text-center mb-10">
<a
href="/strategy/COMPETITIVE_MATRIX_AR.md"
className="text-sm font-semibold text-teal-400 hover:text-teal-300 underline underline-offset-4"
>
فتح مصفوفة تنافسية (Markdown)
</a>
</motion.p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{provenDifferentiators.map((d, i) => (
<motion.div
key={d.title}
variants={fadeUp}
custom={i}
className="rounded-2xl bg-white/[0.03] backdrop-blur-xl border border-white/[0.08] p-6 text-right hover:border-teal-500/25 transition-all"
>
<div className="w-11 h-11 rounded-xl bg-teal-500/10 flex items-center justify-center mb-4 mr-auto">
<d.icon className="w-5 h-5 text-teal-400" />
</div>
<h3 className="font-bold text-base mb-2">{d.title}</h3>
<p className="text-sm text-white/50 leading-relaxed">{d.desc}</p>
</motion.div>
))}
</div>
</Section>
{/* ═══════════ HOW IT WORKS ═══════════ */} {/* ═══════════ HOW IT WORKS ═══════════ */}
<Section id="how" className="max-w-4xl mx-auto px-6 md:px-12 py-20"> <Section id="how" className="max-w-4xl mx-auto px-6 md:px-12 py-20">
<motion.h2 variants={fadeUp} className="text-3xl md:text-4xl font-black text-center mb-14"> <motion.h2 variants={fadeUp} className="text-3xl md:text-4xl font-black text-center mb-14">
@ -342,7 +390,7 @@ export function PremiumLanding() {
))} ))}
</div> </div>
<p className="text-lg text-white/80 leading-relaxed mb-6"> <p className="text-lg text-white/80 leading-relaxed mb-6">
&ldquo;Dealix غيّر طريقة عمل فريق المبيعات عندنا بالكامل. من أول شهر زادت مبيعاتنا ٤٠٪ وصار عندنا رؤية واضحة لكل صفقة.&rdquo; &ldquo;Dealix جمع لنا المسار والقنوات في مكان واحد؛ صار عندنا رؤية أوضح لكل صفقة وتقليل تشتيت بين الأدوات.&rdquo;
</p> </p>
<div> <div>
<p className="font-bold">عبدالله الشمري</p> <p className="font-bold">عبدالله الشمري</p>

View File

@ -0,0 +1,106 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api-client";
import { BookOpen } from "lucide-react";
type Playbook = {
id: string;
label_ar: string;
label_en: string;
primary_channels: string[];
approval_value_threshold_sar: number;
compliance_notes_ar: string[];
};
export function VerticalPlaybooksView() {
const [items, setItems] = useState<Playbook[]>([]);
const [sel, setSel] = useState<string | null>(null);
const [detail, setDetail] = useState<Playbook | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
void (async () => {
const r = await apiFetch("/api/v1/strategic-deals/playbooks");
if (!r.ok) {
setErr("تعذر تحميل القوالب القطاعية");
return;
}
const j = (await r.json()) as { items: Playbook[] };
setItems(j.items || []);
})();
}, []);
useEffect(() => {
if (!sel) {
setDetail(null);
return;
}
void (async () => {
const r = await apiFetch(`/api/v1/strategic-deals/playbooks/${sel}`);
if (r.ok) setDetail(await r.json());
})();
}, [sel]);
return (
<div className="p-4 md:p-8 max-w-5xl mx-auto space-y-6">
<div className="flex items-start gap-3">
<BookOpen className="w-8 h-8 text-primary shrink-0" />
<div>
<h1 className="text-2xl font-bold">Playbooks قطاعية</h1>
<p className="text-sm text-muted-foreground mt-1">
قنوات أولى، عتبات موافقة، وملاحظات امتثال تغذي الوكلاء والسياسات دون تخصيص كود لكل عميل.
</p>
</div>
</div>
{err && <div className="text-destructive text-sm">{err}</div>}
<div className="grid md:grid-cols-2 gap-4">
<div className="glass-card p-4 space-y-2 max-h-[480px] overflow-y-auto">
{items.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setSel(p.id)}
className={`w-full text-right p-3 rounded-xl border transition-colors ${
sel === p.id ? "border-primary bg-primary/10" : "border-border hover:bg-secondary/40"
}`}
>
<div className="font-medium">{p.label_ar}</div>
<div className="text-xs text-muted-foreground">{p.label_en}</div>
</button>
))}
</div>
<div className="glass-card p-5 space-y-3 min-h-[200px]">
{!detail ? (
<p className="text-sm text-muted-foreground">اختر قطاعاً لعرض التفاصيل.</p>
) : (
<>
<h2 className="font-semibold">{detail.label_ar}</h2>
<p className="text-xs text-muted-foreground">عتبة موافقة مقترحة: {detail.approval_value_threshold_sar?.toLocaleString("ar-SA")} ر.س</p>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">قنوات أولى</p>
<div className="flex flex-wrap gap-1">
{detail.primary_channels?.map((c) => (
<span key={c} className="text-xs px-2 py-0.5 rounded-full bg-secondary">
{c}
</span>
))}
</div>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">امتثال</p>
<ul className="list-disc pr-4 text-sm text-muted-foreground space-y-1">
{detail.compliance_notes_ar?.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@ -21,6 +21,12 @@ from pathlib import Path
def main() -> int: def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
saas = Path(__file__).resolve().parent.parent saas = Path(__file__).resolve().parent.parent
backend = saas / "backend" backend = saas / "backend"
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./go_live_gate_cli.db") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./go_live_gate_cli.db")

View File

@ -67,6 +67,29 @@ if (fs.existsSync(SRC_INTEGRATION)) {
console.warn("SKIP INTEGRATION_MASTER doc (missing):", SRC_INTEGRATION); console.warn("SKIP INTEGRATION_MASTER doc (missing):", SRC_INTEGRATION);
} }
const SRC_LEGAL = path.join(ROOT, "docs", "legal");
const DEST_LEGAL = path.join(DEST_STRATEGY_DIR, "legal");
if (fs.existsSync(SRC_LEGAL)) {
fs.mkdirSync(DEST_LEGAL, { recursive: true });
for (const f of fs.readdirSync(SRC_LEGAL)) {
if (f.endsWith(".md")) {
fs.copyFileSync(path.join(SRC_LEGAL, f), path.join(DEST_LEGAL, f));
}
}
console.log("OK:", DEST_LEGAL);
} else {
console.warn("SKIP legal docs (missing):", SRC_LEGAL);
}
const SRC_COMPETITIVE = path.join(ROOT, "docs", "COMPETITIVE_MATRIX_AR.md");
if (fs.existsSync(SRC_COMPETITIVE)) {
fs.mkdirSync(DEST_STRATEGY_DIR, { recursive: true });
fs.copyFileSync(SRC_COMPETITIVE, path.join(DEST_STRATEGY_DIR, "COMPETITIVE_MATRIX_AR.md"));
console.log("OK:", path.join(DEST_STRATEGY_DIR, "COMPETITIVE_MATRIX_AR.md"));
} else {
console.warn("SKIP competitive matrix (missing):", SRC_COMPETITIVE);
}
const readme = path.join(DEST_MARKETING, "LOCAL-ONLY-NEXT.txt"); const readme = path.join(DEST_MARKETING, "LOCAL-ONLY-NEXT.txt");
fs.writeFileSync( fs.writeFileSync(
readme, readme,
@ -82,6 +105,8 @@ fs.writeFileSync(
" http://localhost:3000/dealix-presentations/", " http://localhost:3000/dealix-presentations/",
" http://localhost:3000/resources", " http://localhost:3000/resources",
" http://localhost:3000/strategy", " http://localhost:3000/strategy",
" http://localhost:3000/strategy/legal/ (وثائق قانونية بعد المزامنة)",
" http://localhost:3000/strategy/COMPETITIVE_MATRIX_AR.md",
"", "",
"لتحديث النسخ بعد تعديل الملفات الأصلية:", "لتحديث النسخ بعد تعديل الملفات الأصلية:",
" node scripts/sync-marketing-to-public.cjs", " node scripts/sync-marketing-to-public.cjs",