# API Documentation: Admin Dashboard Changes

This document covers recent changes and requirements for the Admin Dashboard, specifically around Catalog management (Categories, Services) and the new Provider Registration features.

📝 **See Also:** [Postman Examples for Category Questions](./POSTMAN_CATEGORY_QUESTIONS_EXAMPLES.md) - Complete form-data examples with correct keys for bulk editing.

---

## ⚠️ Deprecated Features

### Languages CRUD (Removed)
The **Languages** CRUD module has been removed from the admin dashboard. The application now uses a **fixed locale system** with only two supported languages:
- Arabic (`ar`)
- English (`en`)

**Reason for Removal:**
- The system is designed to operate in Arabic and English only
- Dynamic language management was unnecessary complexity
- Locale configuration is now handled at the application level via `config/translatable.php`

**Impact:**
- ❌ No admin endpoints for creating/updating/deleting languages
- ❌ **`languages` field removed from provider profile** (array of language IDs)
- ❌ Providers can no longer specify spoken languages (e.g., English, French, German)
- ✅ All translations continue to work with `ar` and `en` locales
- ✅ Translation files remain in `lang/ar.json` and `lang/en.json`
- ✅ Database `translations` tables continue to use `locale` column with `ar`/`en` values

**Why `languages` field was removed:**
- Without a Languages CRUD, there's no way to manage the languages list
- Language IDs would reference a non-existent or static table
- Simplifies provider profile management
- If needed in the future, can be re-implemented with a fixed enum of common languages

If you need to add language management in the future, it must be done at the code level by:
1. Creating an enum of supported spoken languages (e.g., `SpokenLanguageEnum`)
2. Using a JSON field or many-to-many relationship
3. Updating provider forms to include language selection
4. Adding validation rules for the language field

---

## 1. Category Creation (`POST /api/dashboard/admin/categories`)

When creating a **Main Category** (`level = 0`), you **must** include the `icon`, the `questions` array, and the exam configuration fields. Subcategories (`level = 1`) do **not** take an icon or questions, as they inherit the exam rules of their parent category.

### ⚠️ Required Fields for Main Categories (level = 0)

These four fields are **required** for every main category and control the exam rules **specifically for that category**. Each specialty can have different rules:

| Field | Type | Description | Default |
|---|---|---|---|
| `exam_passing_score` | `integer` | Minimum correct answers to pass (e.g. 18 out of 20) | — |
| `exam_time_minutes` | `integer` | Time limit in minutes for the exam | — |
| `exam_max_attempts` | `integer` | Max total attempts allowed before the provider is blocked | 3 |
| `exam_cooldown_days` | `integer` | Days the provider must wait between failed attempts | 14 |
| `questions` | `array` | The exam questions array (minimum **20** questions) | — |
| `icon` | `integer` | Media ID for the category icon | — |

> These are **NOT** global settings. They are stored directly on the `categories` table. Subcategories do not use these fields.

### Request Body (Example)
```json
{
    "parent_id": null,
    "level": 0,
    "is_active": true,
    "ar": { "name": "القانونية" },
    "en": { "name": "Legal" },
    "exam_passing_score": 18,
    "exam_time_minutes": 10,
    "exam_max_attempts": 3,
    "exam_cooldown_days": 14,
    "questions": [
        {
            "order": 1,
            "is_active": true,
            "ar": { "question": "السؤال الأول؟" },
            "en": { "question": "First question?" },
            "options": [
                {
                    "order": 1,
                    "is_correct": true,
                    "ar": { "answer": "إجابة 1" },
                    "en": { "answer": "Answer 1" }
                }
            ]
        }
    ]
}
```

### 📝 Managing Individual Questions After Creation
While you must submit at least 20 questions during the initial category creation/update, there are also individual endpoints to manage questions independently (`categories/{category}/questions/{question}`).

**Why use them?**
- **Fixing Typos:** Use `PUT /categories/{category}/questions/{question}` to fix a typo without resending a massive array of all 20+ questions.
- **Adding Questions:** Use `POST /categories/{category}/questions` to add a 21st or 22nd question cleanly.

**⚠️ Question Validation Rules:**

#### **Creation with Options:**
- ✅ Must include at least **2 options**
- ✅ Must have **exactly 1 correct option** (`is_correct: true`)
- ❌ Validation fails if less than 2 options or not exactly 1 correct option

#### **Activation:**
When trying to activate a question (either via `PUT` update or `PATCH /status`):
- ✅ The question must have at least **2 active options**
- ✅ The question must have **exactly 1 active correct option**
- ❌ Cannot activate if these conditions are not met

#### **Deletion:**
- ✅ Can only delete if it won't drop active questions below **20** for the category
- ❌ **Error:** "Cannot delete this question. A category must have at least 20 active questions for the exam."

#### **Disabling:**
- ✅ Can only disable if it won't drop active questions below **20** for the category
- ❌ **Error:** "Cannot disable this question. A category must have at least 20 active questions for the exam."

---

### 📝 Managing Individual Options
Similarly, individual endpoints exist to manage options independently: `category-questions/{question}/options/{option}`.

**⚠️ Option Validation Rules:**

#### **Correct Answer Enforcement (Handled Automatically):**
The system **automatically enforces exactly ONE correct answer** per question:
- ✅ When you mark an option as correct → all other options are automatically unmarked
- ✅ When you try to unmark the only correct option → it's prevented
- ✅ Works across all creation/update operations

#### **Deletion:**
- ✅ Can delete if at least **2 active options** will remain
- ✅ Cannot delete the **only correct option**
- ❌ **Error:** "Cannot delete this option. A multiple-choice question must have at least 2 active options."
- ❌ **Error:** "Cannot delete the only correct option. Please mark another option as correct first."

#### **Disabling:**
- ✅ Can disable if at least **2 active options** will remain
- ✅ Cannot disable the **only active correct option**
- ❌ **Error:** "Cannot disable this option. A multiple-choice question must have at least 2 active options."
- ❌ **Error:** "Cannot deactivate the only active correct option. Please mark another active option as correct first."

#### **Update/Create:**
- ✅ When marking `is_correct: true` → automatically unmarks all other options
- ✅ Ensures data integrity without manual checks

---

## 2. Service Creation (`POST /api/dashboard/admin/services`)

When creating a new service, you **must** include the `form_fields` array. A service cannot be created without its dynamic form fields defined. The backend will auto-generate `field_key` strings — do **not** send `field_key` manually.

Also note that you must define the `price_from` and `price_to` bounds, which control the pricing limits for providers.

### ⚠️ Service Pricing Limits

Services have two pricing boundary fields that control what providers can charge:

| Field | Type | Description |
|---|---|---|
| `price_from` | `float` | Minimum price a provider can set for this service |
| `price_to` | `float` | Maximum price a provider can set for this service |

**Enforcement:**
- ✅ When providers update their service prices, the system validates against these bounds
- ✅ When negotiating prices, the agreed price must fall within these limits
- ❌ **Error:** "Price must be at least :min for this service."
- ❌ **Error:** "Price must not exceed :max for this service."

**Example:**
```json
{
    "price_from": 100,
    "price_to": 500
}
```
This means providers can charge between 100 SAR and 500 SAR for this service.

### ⚠️ Form Field Keys (Complete Reference)

Each object in the `form_fields` array supports the following keys:

| Key | Type | Required | Description |
|---|---|---|---|
| `type` / `field_type` | `string` | Yes | Field type (e.g. `text`, `select`, `textarea`, `file`, `date`). Either alias works. |
| `is_required` | `boolean` | No | Whether this field is mandatory in the order form |
| `is_active` | `boolean` | No | Whether this field is visible |
| `is_repeatable` | `boolean` | No | Whether the field can be added multiple times by the user |
| `order` | `integer` | No | Display order |
| `options` | `array` | No | For `select` type only — array of option objects: `{"value": "val", "label": {"ar": "...", "en": "..."}}` |
| `ar.label` | `string` | Yes | Arabic label |
| `ar.placeholder` | `string` | No | Arabic placeholder text |
| `ar.help_text` | `string` | No | Arabic helper/hint text shown below the field |
| `en.label` | `string` | Yes | English label |
| `en.placeholder` | `string` | No | English placeholder text |
| `en.help_text` | `string` | No | English helper/hint text |

### Request Body (Example)
```json
{
    "subcategory_id": 1,
    "is_active": true,
    "price_from": 100,
    "price_to": 500,
    "is_integrated_case": false,
    "is_agency_service": false,
    "ar": { "title": "خدمة صياغة العقود" },
    "en": { "title": "Contract Drafting Service" },
    "form_fields": [
        {
            "type": "text",
            "is_required": true,
            "is_active": true,
            "is_repeatable": false,
            "order": 1,
            "ar": {
                "label": "الاسم الكامل",
                "placeholder": "أدخل اسمك الكامل",
                "help_text": null
            },
            "en": {
                "label": "Full Name",
                "placeholder": "Enter your full name",
                "help_text": null
            }
        },
        {
            "type": "select",
            "is_required": true,
            "is_active": true,
            "is_repeatable": false,
            "order": 2,
            "options": [
                {
                    "value": "option_1",
                    "label": { "ar": "خيار 1", "en": "Option 1" }
                }
            ],
            "ar": { "label": "نوع العقد", "placeholder": null, "help_text": null },
            "en": { "label": "Contract Type", "placeholder": null, "help_text": null }
        },
        {
            "type": "file",
            "is_required": false,
            "is_active": true,
            "is_repeatable": true,
            "order": 3,
            "options": [],
            "ar": { "label": "المستندات الداعمة", "placeholder": null, "help_text": "يمكنك إرفاق أكثر من ملف" },
            "en": { "label": "Supporting Documents", "placeholder": null, "help_text": "You can attach multiple files" }
        }
    ]
}
```

### Response — What a form field looks like when returned

There are **two versions** of the form field resource depending on who is consuming it:

#### Admin Dashboard (`/api/dashboard/admin/services/{id}`)
```json
{
    "id": 5,
    "field_key": "full_name",
    "field_type": "text",
    "is_required": true,
    "is_active": true,
    "is_repeatable": false,
    "order": 1,
    "options": [],
    "ar": {
        "label": "الاسم الكامل",
        "placeholder": "أدخل اسمك الكامل",
        "help_text": null
    },
    "en": {
        "label": "Full Name",
        "placeholder": "Enter your full name",
        "help_text": null
    },
    "created_at": 1727175600,
    "updated_at": 1727175600
}
```

#### Provider / Client App (`/api/general/catalog/services/{id}`)
> Uses the current request locale — returns the translated values flat (no `ar`/`en` wrapper).
```json
{
    "id": 5,
    "field_key": "full_name",
    "field_type": "text",
    "label": "Full Name",
    "placeholder": "Enter your full name",
    "help_text": null,
    "is_required": true,
    "is_repeatable": false,
    "options": []
}
```
> **Note:** The General resource omits `is_active`, `order`, `created_at`, `updated_at` — those are admin-only concerns.



## 3. Provider Management & Requests

The admin dashboard now fully handles the review, creation, and approval of providers according to strict requirements.

### Admin Reviewing Pending Providers
When viewing a provider request (`GET /api/dashboard/admin/providers/{id}`), the resource includes:
- **`exam_latest_attempt`**: Returns the `score`, `total_questions`, `passed`, and `attempt_number`. You do not need to parse all the questions; the system handles the strict pass/fail logic.
- **Documents array**: `wages_certificate` and `other_licenses` are now arrays of media objects.
- **Provider details**: `degree_grade` (excellent/very_good), `degree_specialty` (law/sharia), `specialty_experience_years`, `gpa_or_percentage`, etc.

### Admin Creating a Provider directly
When creating a provider directly from the dashboard (`POST /api/dashboard/admin/providers`), the admin payload expects all the same fields as the provider's step 1-4, except:
- **Documents**: `cv`, `intro_video` are optional (nullable) during creation.
- **Pricing**: Admins can pre-set the prices of the services they assign to the provider in the `services.*.price` array.

### Admin Updating a Provider
Admins can update most provider fields via `PUT /api/dashboard/admin/providers/{id}`. The following fields can be updated:

**Basic Information:**
- `full_name`, `email`, `phone`, `phone_code`
- `allow_notifications`, `is_available`
- `national_id_type`, `national_id`, `nationality`

**Professional Details:**
- `professional_license_number`, `license_expiration_date`, `licensing_authority`
- `degree_specialty`, `degree_grade`, `gpa_or_percentage`
- `years_of_experience`, `specialty_experience_years`
- `has_criminal_record`

**Location & Bio:**
- `brief`, `address`, `lat`, `lng`, `city_id`, `district_id`

**Media (IDs or Arrays):**
- Single: `avatar`, `cv`, `intro_video`, `national_id_doc`, `professional_license_doc`, `university_degree_doc`, `criminal_record_certificate`
- Arrays: `wages_certificate`, `other_licenses`

**Services & Team:**
- `services` (array with service_id, price, provider_percentage)
- `category_id`, `subcategory_id` (for changing specialty - requires re-exam)

**Registration Phase:**
- `current_registration_phase` (admin can manually advance or revert phases)

### Provider Self-Update (After Approval)
After a provider is approved, they can update their own profile via `PUT /api/provider/profile`. **Limited fields:**

**Allowed:**
- `full_name`, `allow_notifications`, `is_available`
- `brief`, `address`, `lat`, `lng`, `city_id`, `district_id`
- `avatar`, `cv`, `intro_video` (media IDs)
- `other_licenses` (arrays of media IDs)

**Locked (Cannot be changed by provider):**
- ❌ `national_id`, `national_id_type`, `nationality`
- ❌ `professional_license_number`, `license_expiration_date`
- ❌ `degree_specialty`, `degree_grade`, `gpa_or_percentage`
- ❌ `category_id`, `subcategory_id`
- ❌ Core registration documents
- ❌ `locale` (language preference is fixed at application level)

**Rationale:** Identity and credential fields are locked to prevent fraud and maintain data integrity. Only admins can modify these through the dashboard after proper verification. Language preference is determined by the user's device/browser settings.

#### How to upload and send Media (Attachments)
To upload files for the provider, you must first use the general attachments endpoint (`POST /api/general/attachments`) with the following form-data:
- `model`: `User`
- `media_key`: The specific key for the document (e.g., `national_id_doc`, `wages_certificate`, `other_licenses`).
- `file`: The binary file.

The endpoint will return a media `id`. You then send this `id` in the provider creation/update request:
- For single files (e.g., `"national_id_doc": 5`)
- For multiple files (e.g., `"wages_certificate": [12, 13]`, `"other_licenses": [14, 15]`)

### Global Settings (Provider Registration)
The system settings (`settings` table) include important keys for the registration cycle:
- `provider_rejection_cooldown_months`: (Default: 3) Determines how many months a rejected provider must wait before they can apply again from scratch.
- `exam_duration_minutes`: **Note** — this setting acts as a **fallback**. The primary source of truth for exam duration is `exam_time_minutes` on the category itself. This global setting is only used if the category value is somehow unset.

---

## 4. Technical Implementation Notes

### Validation Architecture

The validation system uses a **layered approach** for maximum data integrity:

#### **Service Layer** (`CategoryQuestionService`, `CategoryQuestionOptionService`, etc.)
- Handles business rules (minimum counts, relationships, constraints)
- Validates bulk operations
- Wrapped in database transactions
- Examples:
  - Ensures 20 active questions minimum per category
  - Validates provider service prices are within `price_from` and `price_to` bounds
  - Prevents deletion of services with assigned providers

#### **Model Layer** (`CategoryQuestionOption` model events)
- Handles data integrity rules
- Enforces "exactly one correct answer" automatically
- Works across all entry points (API, seeding, direct DB operations)
- Prevents invalid state changes

#### **Benefits:**
- ✅ No duplicate validation logic
- ✅ Works regardless of how data is created (nested creation, direct API calls)
- ✅ Automatic enforcement via Eloquent events
- ✅ Clear error messages in Arabic and English

### Error Messages & Translations

All validation error messages use Laravel's `__()` translation helper and are fully translated in both Arabic and English:

**Question Validations:**
- `"A multiple-choice question must have at least 2 options."`
- `"A question must have exactly one correct option."`
- `"Cannot activate this question. It must have at least 2 active options."`
- `"Cannot activate this question. It must have exactly one active correct option."`
- `"Cannot delete this question. A category must have at least :min active questions for the exam."`
- `"Cannot disable this question. A category must have at least :min active questions for the exam."`

**Option Validations:**
- `"Cannot delete this option. A multiple-choice question must have at least 2 active options."`
- `"Cannot disable this option. A multiple-choice question must have at least 2 active options."`
- `"Cannot delete the only correct option. Please mark another option as correct first."`
- `"Cannot deactivate the only active correct option. Please mark another active option as correct first."`

All messages are stored in `lang/ar.json` and automatically translated based on the `Accept-Language` header or user locale.

### Database Transactions

All create/update/delete operations are wrapped in database transactions to ensure:
- ✅ Atomicity: All changes succeed or all fail
- ✅ Consistency: Data integrity maintained
- ✅ Isolation: Concurrent requests don't interfere
- ✅ Rollback on validation failures

Example flow:
```php
DB::transaction(function () {
    // 1. Create question
    // 2. Create options
    // 3. Validate rules
    // 4. Commit or rollback
});
```
