# Maaal Holding API — Documentation

REST API powering the **Maaal Holding** dashboard (admin CMS) and the public website, reverse-engineered from the two Figma design files.

- **Framework:** Laravel 12 (PHP 8.3)
- **Auth:** Laravel Sanctum (Bearer personal-access tokens)
- **DB:** MySQL / MariaDB
- **Base URL:** `http://localhost:8000/api` (dev) — every route below is relative to `/api`.
- **Localization:** content is bilingual (Arabic primary). Always send/receive **UTF-8**.

---

## 1. Response envelope

Every endpoint returns the same JSON shape:

```json
{
  "success": true,
  "message": "تمت العملية بنجاح",
  "data": { },
  "errors": null,
  "meta": null
}
```

| key | meaning |
|---|---|
| `success` | `true` for 2xx, `false` for errors |
| `message` | human-readable Arabic message |
| `data` | the payload — object, array, or `null` |
| `errors` | validation errors map (`{ "field": ["..."] }`) or `null` |
| `meta` | pagination block for list endpoints, else `null` |

**Paginated `meta`:**
```json
"meta": { "current_page": 1, "per_page": 15, "total": 42, "last_page": 3, "from": 1, "to": 15 }
```

---

## 2. Status codes & errors

| code | when |
|---|---|
| 200 | OK |
| 201 | Created |
| 401 | Not authenticated (missing/invalid token) |
| 403 | Authenticated but lacks permission / inactive account |
| 404 | Resource not found |
| 422 | Validation failed (see `errors`) |
| 500 | Server error |

Example validation error (422):
```json
{
  "success": false,
  "message": "البيانات المُدخلة غير صحيحة.",
  "data": null,
  "errors": { "title": ["The title field is required."] },
  "meta": null
}
```

---

## 3. Conventions for list endpoints

All admin `index` endpoints accept:

| query param | description |
|---|---|
| `search` | free-text search on the entity's main columns |
| `is_active` | `true` / `false` |
| `sort_by` | column to sort by (default `order` or `created_at`) |
| `sort_dir` | `asc` / `desc` |
| `per_page` | page size (default 15) |
| `page` | page number |

Entity-specific filters are noted per module.

---

## 4. Authentication

### POST `/auth/login`
Accepts email, username, or mobile number in `login`.

```bash
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "login": "admin@maaal.com", "password": "Password@123", "device_name": "web" }'
```

**200**
```json
{
  "success": true,
  "message": "تم تسجيل الدخول بنجاح",
  "data": {
    "token": "1|Xnq8....",
    "token_type": "Bearer",
    "user": {
      "id": 1, "username": "admin", "full_name": "مدير النظام",
      "email": "admin@maaal.com", "role_id": 1,
      "role": { "id": 1, "name": "مسؤول", "slug": "super_admin" },
      "permissions": ["users.view", "users.create", "..."]
    }
  }
}
```

Use the token on every protected request:
```
Authorization: Bearer 1|Xnq8....
```

### GET `/auth/me`  *(auth)*
Returns the current user with role + permissions.

### POST `/auth/logout`  *(auth)*
Revokes the current token.

### Password reset (OTP)
| method | path | body |
|---|---|---|
| POST | `/auth/forgot-password` | `{ identifier, channel: "email"\|"sms" }` |
| POST | `/auth/verify-otp` | `{ identifier, code, purpose: "password_reset" }` |
| POST | `/auth/reset-password` | `{ identifier, code, password, password_confirmation }` |

> In local/dev the OTP is written to `storage/logs/laravel.log` (no SMS/mail gateway configured).

---

## 5. Profile  *(auth — prefix `/profile`)*

| method | path | body |
|---|---|---|
| GET | `/profile` | — |
| PUT | `/profile` | `username, first_name, last_name, name, gender, language, date_of_birth, linkedin_url, twitter_url` |
| POST | `/profile/avatar` | multipart `avatar` (image) |
| POST | `/profile/change-password` | `current_password, password, password_confirmation` |
| POST | `/profile/email/request-otp` | `{ email }` → sends OTP to new email |
| POST | `/profile/email/confirm` | `{ email, code }` |
| POST | `/profile/mobile/request-otp` | `{ mobile_number }` |
| POST | `/profile/mobile/confirm` | `{ mobile_number, mobile_country_code, code }` |

---

## 6. Public website endpoints  *(no auth — prefix `/api`)*

| method | path | description |
|---|---|---|
| GET | `/home` | aggregated homepage: settings + services + featured works + latest articles + partners + testimonials |
| GET | `/home/partners` | active partners |
| GET | `/home/testimonials` | active testimonials |
| GET | `/about` | about-page singleton + active info cards |
| GET | `/services` | active services (paginated) |
| GET | `/services/{slug}` | service detail |
| GET | `/services/{slug}/works` | works under a service |
| GET | `/works` | active works (`?service_id=`) |
| GET | `/works/{slug}` | work detail |
| GET | `/blog` | published articles (`?search=`, `?category=`, `?featured=1`) |
| GET | `/blog/{slug}` | article detail (increments views) + related |
| GET | `/studies` | published studies (`?year=`, `?month=`, `?category=`, `?search=`) |
| GET | `/studies/{slug}` | study detail (increments views) |
| POST | `/studies/{id}/download` | increments downloads, returns document link |
| GET | `/contact/enquiry-types` | active enquiry types for the form dropdown |
| POST | `/contact` | submit the contact form (creates a ticket) |
| GET | `/company-settings` | public company/contact info |
| GET | `/menus/{location}` | menu (with nested items) by location, e.g. `header` |
| GET | `/social-links` | active social media links |

### Example — submit contact form
```bash
curl -X POST http://localhost:8000/api/contact \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{ "name":"محمد", "email":"m@example.com", "country_dial_code":"+966",
        "phone":"500000000", "enquiry_type_id":2, "message":"أرغب بطلب استشارة" }'
```
**201**
```json
{ "success": true, "message": "تم استلام رسالتك بنجاح، سنتواصل معك قريباً.",
  "data": { "ticket_number": "TK-20260617-DKEFAP" }, "errors": null, "meta": null }
```

---

## 7. Admin dashboard endpoints  *(auth — prefix `/api/admin`)*

> All admin routes require `Authorization: Bearer <token>`. Attach `permission:<name>` middleware
> per-route to enforce granular access; the seeded **super_admin** role holds all permissions.

### 7.1 Dashboard
| method | path | description |
|---|---|---|
| GET | `/dashboard/stats` | KPI cards (project/contact/consultation request counts + MoM %) and global totals |
| GET | `/dashboard/requests-chart?type=consultation&year=2026` | 12 monthly buckets for the bar chart |
| GET | `/dashboard/recent?type=tickets&limit=5` | recent records for a tab |

### 7.2 Users  (`/users`)
Full CRUD + `POST /users/{user}/toggle-status`.
Filters: `search, role_id, language, is_active, scope=all|admins|team`.

**Create:**
```bash
curl -X POST http://localhost:8000/api/admin/users \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json; charset=utf-8" \
  -d '{ "username":"editor1", "first_name":"سارة", "last_name":"العتيبي",
        "gender":"female", "language":"ar", "role_id":2,
        "email":"sara@maaal.com", "mobile_number":"512345678",
        "password":"Secret@123", "password_confirmation":"Secret@123" }'
```

### 7.3 Roles & Permissions
| method | path | notes |
|---|---|---|
| CRUD | `/roles` | + `POST /roles/{role}/toggle-status` |
| — | `POST/PUT /roles` body | `name, language, is_active, permissions: [id,...]` (synced to the role) |
| GET | `/permissions` | all permissions grouped by module (for the permission matrix) |

### 7.4 Content modules (full CRUD + actions)

| module | base path | extra actions |
|---|---|---|
| Services | `/services` | `toggle-status`, `duplicate`, `reorder` |
| Works (portfolio) | `/works` | `toggle-status`, `duplicate`, `reorder` |
| Articles (news/blog) | `/articles` | `toggle-status`, `toggle-featured`, `publish`, `schedule`, `{id}/restore` |
| Categories | `/categories` | `toggle-status`, `reorder` |
| Tags | `/tags` | `toggle-status` |
| Study categories | `/study-categories` | `toggle-status` |
| Studies | `/studies` | `toggle-status`, `toggle-featured`, `download` |
| Partners | `/partners` | `toggle-status`, `reorder` |
| Testimonials | `/testimonials` | `toggle-status`, `reorder` |
| Enquiry types | `/enquiry-types` | `toggle-status`, `reorder` |
| Menus | `/menus` | `toggle-status` |
| Menu items | `/menu-items` | `reorder` |
| Widgets | `/widgets` | `toggle-status`, `reorder` |
| Social links | `/social-media-links` | `toggle-status`, `reorder` |
| Fonts | `/fonts` | `toggle-status`, `set-default` |
| Maintenance modes | `/maintenance-modes` | `toggle-status` |
| About info cards | `/about-info-cards` | `toggle-status`, `reorder` |

Every CRUD base path supports:
`GET /{base}` (list) · `POST /{base}` (create) · `GET /{base}/{id}` (show) · `PUT /{base}/{id}` (update) · `DELETE /{base}/{id}` (soft delete).

Reorder bodies: `POST /{base}/reorder  { "items": [ { "id": 1, "order": 0 }, { "id": 2, "order": 1 } ] }`.

### 7.5 Articles — example create
```bash
curl -X POST http://localhost:8000/api/admin/articles \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json; charset=utf-8" \
  -d '{ "title":"ارتفاع مؤشر السوق", "category_id":1, "tags":[1,2],
        "status":"published", "publish_type":"immediate",
        "excerpt":"ملخص الخبر", "body":"النص الكامل للخبر" }'
```
Schedule instead: `POST /articles/{id}/schedule { "published_at": "2026-07-01 09:00:00" }`.

### 7.6 Tickets  (`/tickets`)
CRUD + workflow actions:
| method | path | body |
|---|---|---|
| POST | `/tickets/{ticket}/reply` | `{ body, channel: "dashboard"\|"gmail"\|"whatsapp"\|"email" }` |
| POST | `/tickets/{ticket}/status` | `{ status: "new"\|"open"\|"replied"\|"closed" }` |
| POST | `/tickets/{ticket}/assign` | `{ assigned_to: <user_id> }` |
| POST | `/tickets/{ticket}/read` | — (mark read) |

Filters: `search, status, priority, channel, type, enquiry_type_id, assigned_to, is_read`.

### 7.7 Settings (singletons — `GET` to read, `PUT` to update)

| path | screen |
|---|---|
| `/company-settings` | بيانات الشركة |
| `/home-settings` | إعدادات الصفحة الرئيسية |
| `/about` | صفحة من نحن (singleton) |
| `/site-settings` | Custom CSS/HTML · Cache · Cookies · SMTP |
| `/tracking-settings` | Google Analytics · SEO · Sitemap (+ `POST /tracking-settings/generate-sitemap`) |
| `/site-font-settings` | الخطوط المستخدمة في الموقع |

**Update example:**
```bash
curl -X PUT http://localhost:8000/api/admin/company-settings \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json; charset=utf-8" \
  -d '{ "name":"ماال القابضة", "company_email":"info@maaal.com",
        "company_phone":"920000000", "city":"الرياض",
        "map_lat":24.7136, "map_lng":46.6753 }'
```

---

## 8. Seeded data (after `php artisan migrate:fresh --seed`)

| item | value |
|---|---|
| Admin user | `admin@maaal.com` / `Password@123` (role `super_admin`) |
| Roles | `super_admin` (84 perms), `editor`, `data_entry` |
| Permissions | 84 (24 modules × view/create/edit/delete or view/edit) |
| Enquiry types | 5 (general, consultation, project, support, partnership) |
| Settings | company / home / about / site / tracking singletons initialised |

---

## 9. Running locally

```bash
# 1. configure DB in .env (DB_DATABASE=maaal_holding, DB_USERNAME=root, DB_PASSWORD=)
php artisan migrate:fresh --seed     # build schema + seed
php artisan serve                    # http://localhost:8000

# 2. authenticate
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"login":"admin@maaal.com","password":"Password@123"}'
```

For file uploads (avatars, logos, images) run `php artisan storage:link` once so `storage/app/public` is web-accessible at `/storage`.

---

## 10. Entity reference

The full database schema — every table, column, type and relationship — is documented in
[`API_DATA_MODEL.md`](API_DATA_MODEL.md). Summary of the 30+ tables:

**Auth & access:** users, roles, permissions, permission_role, verification_codes, personal_access_tokens
**Content:** services, works, articles, categories, tags, article_tag, studies, study_categories, partners, testimonials
**Support:** tickets, ticket_replies, enquiry_types
**Site building:** menus, menu_items, widgets, social_media_links, fonts, site_font_settings, maintenance_modes
**Settings (singletons):** company_settings, home_settings, about_page, about_info_cards, site_settings, tracking_settings
