fix(config): align Configuration pages with platform logic

Audit of the admin Configuration section (FAQ Manager, Notification Rules,
Approval Config) surfaced three contract mismatches and one missing schema.

FAQ
  * Backend `encoach.faq.category.audience` / `encoach.faq.item.audience`
    rejected the `both` and `entity` values emitted by the FAQ Manager
    UI. Widened the Selection so the UI's full vocabulary round-trips.
  * FAQ item `video_url` was already accepted by the UI but silently
    dropped by the controller; now persisted via a new `video_url`
    field on `encoach.faq.item` and serialized on list/get responses.

Notification Rules
  * Added `days_before`, `frequency`, `channel`, `entity_id` to
    `encoach.notification.rule`. These are the exact fields the admin
    form collects; prior to this change they were serialized into the
    JSON body, ignored by the controller, and omitted from responses,
    leaving the Active switch permanently off and the table columns
    blank.
  * Controller now translates `active` <-> `is_active` both ways so the
    new frontend contract and legacy callers coexist. Missing required
    fields return HTTP 400 instead of 500.

Approval Workflows
  * The controller had been hand-rolling raw SQL against three tables
    (`encoach_approval_workflow`, `_stage`, `_request`) that no Odoo
    model declared, so the tables never existed. List() was guarded and
    returned empty; create() would 500 with "relation does not exist".
  * Introduced real ORM models in `encoach_exam_template/models/approval.py`
    plus access rights, which auto-provision the tables on -u.
  * Rewrote the controller to use ORM, added PATCH, and emitted both
    `items` and `results` in the list envelope so the frontend's
    PaginatedResponse reader and legacy callers both work. Step
    payloads now carry `max_days`, `auto_escalate`, and
    `notification_email` end to end.
  * Frontend `approvalsService` and `ApprovalWorkflowConfig` updated to
    send the full stage shape + `allow_bypass`, tolerate both envelope
    keys, and validate at least one approver before submit.

Schema delta applied via `./run.sh -u encoach_exam_template,encoach_lms_api`.
Verified with new `test_config_flows.py`: 24/24 passing. Regression runs
on `test_support_flows.py` (29/29) and `test_training_flows.py` (26/26)
remain green.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-19 10:58:43 +04:00
parent 7f23127e44
commit d940db075e
11 changed files with 841 additions and 259 deletions

View File

@@ -1,30 +1,66 @@
import { api } from "@/lib/api-client";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
import type { PaginationParams, ApiSuccessResponse } from "@/types";
export interface ApprovalStage {
id?: number;
/** 1-based UI position; serialized alongside sequence for compatibility. */
order: number;
sequence?: number;
approver_id: number;
approver_name: string;
max_days?: number;
auto_escalate?: boolean;
notification_email?: string;
status?: "pending" | "approved" | "rejected" | "skipped";
comment?: string;
acted_at?: string | null;
}
export interface ApprovalWorkflow {
id: number;
name: string;
entity_id: number;
entity_name: string;
steps: { order: number; approver_id: number; approver_name: string }[];
status: "active" | "draft";
type?: string;
entity_id: number | null;
entity_name: string | null;
steps: ApprovalStage[];
status: "draft" | "active" | "archived";
allow_bypass?: boolean;
stage_count?: number;
request_count?: number;
created_at: string;
}
export interface ApprovalWorkflowListResponse {
items?: ApprovalWorkflow[];
results?: ApprovalWorkflow[];
total: number;
}
export const approvalsService = {
async list(params?: PaginationParams): Promise<PaginatedResponse<ApprovalWorkflow>> {
return api.get<PaginatedResponse<ApprovalWorkflow>>("/approval-workflows", params as Record<string, string | number | boolean | undefined>);
async list(params?: PaginationParams & { status?: string; entity_id?: number }): Promise<ApprovalWorkflowListResponse> {
return api.get<ApprovalWorkflowListResponse>(
"/approval-workflows",
params as Record<string, string | number | boolean | undefined>,
);
},
async create(data: Partial<ApprovalWorkflow>): Promise<ApprovalWorkflow> {
async get(id: number): Promise<ApprovalWorkflow> {
return api.get<ApprovalWorkflow>(`/approval-workflows/${id}`);
},
async create(data: Partial<ApprovalWorkflow> & { steps?: Partial<ApprovalStage>[]; bypass_enabled?: boolean }): Promise<ApprovalWorkflow> {
return api.post<ApprovalWorkflow>("/approval-workflows", data);
},
async update(id: number, data: Partial<ApprovalWorkflow>): Promise<ApprovalWorkflow> {
async update(id: number, data: Partial<ApprovalWorkflow> & { steps?: Partial<ApprovalStage>[]; bypass_enabled?: boolean }): Promise<ApprovalWorkflow> {
return api.patch<ApprovalWorkflow>(`/approval-workflows/${id}`, data);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/approval-workflows/${id}`);
},
async listUsers(search?: string): Promise<{ items: { id: number; name: string; login: string; email?: string }[]; total: number }> {
return api.get("/approval-users", search ? { search } : undefined);
},
};