mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
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:
parent
c114ac34ae
commit
07557c4be9
1
salesflow-saas/.gitignore
vendored
1
salesflow-saas/.gitignore
vendored
@ -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/
|
||||||
|
|||||||
@ -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")
|
||||||
@ -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")
|
||||||
116
salesflow-saas/backend/app/api/v1/ai_routing.py
Normal file
116
salesflow-saas/backend/app/api/v1/ai_routing.py
Normal 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)}
|
||||||
226
salesflow-saas/backend/app/api/v1/integrations_crm.py
Normal file
226
salesflow-saas/backend/app/api/v1/integrations_crm.py
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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"])
|
||||||
|
|||||||
@ -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 0–4")
|
||||||
|
|
||||||
|
|
||||||
|
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 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
14
salesflow-saas/backend/app/services/dealix_os/__init__.py
Normal file
14
salesflow-saas/backend/app/services/dealix_os/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
@ -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)
|
||||||
110
salesflow-saas/backend/app/services/dealix_os/policy_engine.py
Normal file
110
salesflow-saas/backend/app/services/dealix_os/policy_engine.py
Normal 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
|
||||||
@ -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)
|
||||||
73
salesflow-saas/backend/app/services/integration_probe.py
Normal file
73
salesflow-saas/backend/app/services/integration_probe.py
Normal 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],
|
||||||
|
)
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"},
|
||||||
|
|||||||
53
salesflow-saas/backend/app/services/salesforce_oauth.py
Normal file
53
salesflow-saas/backend/app/services/salesforce_oauth.py
Normal 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,
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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 (0–4) 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) |
|
||||||
|
|||||||
35
salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md
Normal file
35
salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md
Normal 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)
|
||||||
39
salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md
Normal file
39
salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md
Normal 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` على الصفقة الاستراتيجية).
|
||||||
|
- **نمو:** ذكاء استراتيجي وقوائم مهام؛ الالتزامات القانونية والمالية الكبرى **بشرية**.
|
||||||
|
- **حوكمة:** أوضاع تشغيل (0–4)، [`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).
|
||||||
34
salesflow-saas/docs/ENTERPRISE_ROADMAP.md
Normal file
34
salesflow-saas/docs/ENTERPRISE_ROADMAP.md
Normal 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 (0–90 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 (90–180 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 (180–365 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`
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
34
salesflow-saas/docs/LAUNCH_SIMULATION.md
Normal file
34
salesflow-saas/docs/LAUNCH_SIMULATION.md
Normal 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.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
> "نبني أول نظام تجاري ذكي مصمم للسعودية"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*صنع بحب في السعودية 🇸🇦*
|
|
||||||
@ -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 |
@ -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 |
@ -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)
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
# قواعد المسوقين بالعمولة - Dealix
|
||||||
|
|
||||||
|
## ما هو مسموح
|
||||||
|
- التعريف بنفسك كمستشار مبيعات في Dealix
|
||||||
|
- مشاركة المواد التسويقية المعتمدة
|
||||||
|
- التواصل مع العملاء المحتملين بطريقة مهنية
|
||||||
|
- استخدام السكربتات والبرزنتيشنات المقدمة
|
||||||
|
- العمل في أي وقت ومن أي مكان
|
||||||
|
- إحالة مسوقين آخرين
|
||||||
|
|
||||||
|
## ما هو محظور
|
||||||
|
- انتحال صفة مؤسس أو مدير الشركة
|
||||||
|
- تقديم وعود غير مكتوبة في السياسات الرسمية
|
||||||
|
- إرسال رسائل جماعية مزعجة (spam)
|
||||||
|
- مشاركة بيانات عملاء آخرين
|
||||||
|
- تسجيل عملاء وهميين أو مكررين
|
||||||
|
- التلاعب في بيانات الإحالة
|
||||||
|
- إساءة استخدام اسم الشركة
|
||||||
|
- مخالفة سياسات التواصل المعتمدة
|
||||||
|
|
||||||
|
## العقوبات
|
||||||
|
| المخالفة | العقوبة |
|
||||||
|
|---------|--------|
|
||||||
|
| مخالفة أولى بسيطة | تحذير كتابي |
|
||||||
|
| مخالفة ثانية | تجميد العمولات 30 يوم + تحذير نهائي |
|
||||||
|
| مخالفة ثالثة أو مخالفة جسيمة | إنهاء العلاقة فوراً |
|
||||||
|
| احتيال مثبت | إنهاء + استرجاع عمولات + إجراء قانوني |
|
||||||
|
|
||||||
|
## حقوق المسوق
|
||||||
|
- الاطلاع على كشف العمولات الشهري
|
||||||
|
- الاعتراض على أي حساب خاطئ
|
||||||
|
- الحصول على التدريب والأدوات المحدثة
|
||||||
|
- الترقية للتوظيف عند تحقيق الأهداف
|
||||||
|
- إنهاء العلاقة في أي وقت مع استلام العمولات المستحقة
|
||||||
@ -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 أيام عمل
|
||||||
|
- القرار نهائي مع حق الاستئناف مرة واحدة
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# سياسة الموافقة والاشتراك - Dealix
|
||||||
|
|
||||||
|
## مبدأ عام
|
||||||
|
لا يتم التواصل مع أي شخص عبر أي قناة إلا بموافقته المسبقة الصريحة.
|
||||||
|
|
||||||
|
## أنواع الموافقة
|
||||||
|
|
||||||
|
### واتساب
|
||||||
|
- يجب الحصول على opt-in صريح قبل إرسال أي رسالة
|
||||||
|
- الموافقة تُسجل مع التاريخ والمصدر
|
||||||
|
- حق الانسحاب (opt-out) يُنفذ فوراً
|
||||||
|
- يُستخدم فقط قوالب معتمدة من Meta للرسائل الأولى
|
||||||
|
|
||||||
|
### البريد الإلكتروني
|
||||||
|
- رابط إلغاء الاشتراك إلزامي في كل رسالة
|
||||||
|
- opt-in عند التسجيل أو تعبئة نموذج
|
||||||
|
- لا يُرسل أكثر من 3 رسائل بدون رد قبل التوقف
|
||||||
|
|
||||||
|
### الرسائل النصية (SMS)
|
||||||
|
- موافقة مسبقة مطلوبة
|
||||||
|
- تُستخدم فقط للإشعارات الضرورية
|
||||||
|
|
||||||
|
### المكالمات الصوتية
|
||||||
|
- لا يُتصل بدون سبب مشروع (عميل محتمل مسجل)
|
||||||
|
- تسجيل المكالمات يتطلب إبلاغ وموافقة
|
||||||
|
|
||||||
|
## سجل الموافقة
|
||||||
|
- كل موافقة تُسجل بـ: التاريخ، القناة، المصدر، IP (إن توفر)
|
||||||
|
- كل انسحاب يُنفذ خلال 24 ساعة كحد أقصى
|
||||||
|
- السجلات تُحفظ 36 شهر
|
||||||
|
|
||||||
|
## حقوق صاحب البيانات
|
||||||
|
- الوصول لسجل الموافقات الخاص به
|
||||||
|
- سحب الموافقة في أي وقت لأي قناة
|
||||||
|
- تقديم شكوى في حال مخالفة
|
||||||
@ -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 ساعة
|
||||||
|
- توثيق الخرق والإجراءات المتخذة
|
||||||
@ -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 يوم.
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# سياسة الاسترجاع والضمان الذهبي - Dealix
|
||||||
|
|
||||||
|
## الضمان الذهبي (30 يوم)
|
||||||
|
|
||||||
|
### الوعد
|
||||||
|
إذا استخدمت Dealix لمدة 30 يوم ولم تشهد تحسناً في عمليات مبيعاتك، نسترجع لك المبلغ كاملاً.
|
||||||
|
|
||||||
|
### شروط الأهلية
|
||||||
|
1. استخدام المنصة لمدة 14 يوم متواصل على الأقل
|
||||||
|
2. إدخال 20 عميل محتمل كحد أدنى
|
||||||
|
3. إرسال 50 رسالة كحد أدنى عبر المنصة
|
||||||
|
4. حضور جلسة التدريب الأولية
|
||||||
|
|
||||||
|
### الاستثناءات
|
||||||
|
- الحسابات المجمدة بسبب مخالفات
|
||||||
|
- الاستخدام لمرة واحدة فقط لكل شركة
|
||||||
|
- لا يسري على الاشتراكات التجريبية المجانية
|
||||||
|
|
||||||
|
### إجراءات الطلب
|
||||||
|
1. تقديم طلب الاسترجاع عبر الدعم الفني
|
||||||
|
2. إشعار استلام خلال 24 ساعة
|
||||||
|
3. مراجعة الاستخدام خلال 3 أيام عمل
|
||||||
|
4. القرار خلال 5 أيام عمل
|
||||||
|
5. التنفيذ خلال 7 أيام عمل من الموافقة
|
||||||
|
|
||||||
|
### طريقة الاسترجاع
|
||||||
|
- نفس طريقة الدفع الأصلية
|
||||||
|
- تحويل بنكي إذا تعذرت الطريقة الأصلية
|
||||||
@ -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. القانون الحاكم
|
||||||
|
تخضع هذه الشروط لأنظمة المملكة العربية السعودية والجهات القضائية المختصة في الرياض.
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 }),
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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">تغيير وضع التشغيل (0–4)</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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">
|
||||||
“Dealix غيّر طريقة عمل فريق المبيعات عندنا بالكامل. من أول شهر زادت مبيعاتنا ٤٠٪ وصار عندنا رؤية واضحة لكل صفقة.”
|
“Dealix جمع لنا المسار والقنوات في مكان واحد؛ صار عندنا رؤية أوضح لكل صفقة وتقليل تشتيت بين الأدوات.”
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold">عبدالله الشمري</p>
|
<p className="font-bold">عبدالله الشمري</p>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user