Commit f90db6f4 authored by Administrator's avatar Administrator

Update 18 files via Son of Anton

parent 7537971e
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Forms\Models\FormSchema;
class FormBuilderController extends Controller
{
public function index(Request $request): Response
{
$schemas = FormSchema::allActive();
return $this->view('Forms.Views.builder', ['schemas' => $schemas, 'schema' => null]);
}
public function edit(Request $request, string $id): Response
{
$schema = FormSchema::find((int) $id);
if (!$schema) {
return $this->redirect('/forms/builder')->withError('النموذج غير موجود');
}
$schemas = FormSchema::allActive();
return $this->view('Forms.Views.builder', ['schemas' => $schemas, 'schema' => $schema]);
}
public function update(Request $request, string $id): Response
{
$schema = FormSchema::find((int) $id);
if (!$schema) {
return $this->redirect('/forms/builder')->withError('النموذج غير موجود');
}
$schemaJson = trim((string) $request->post('schema_json', ''));
$decoded = json_decode($schemaJson, true);
if ($decoded === null && $schemaJson !== 'null') {
return $this->redirect("/forms/builder/{$id}")->withError('JSON غير صالح');
}
$newVersion = (int) $schema->version + 1;
$schema->update([
'schema_json' => $schemaJson,
'name_ar' => trim((string) $request->post('name_ar', $schema->name_ar)),
'form_fee' => $request->post('form_fee', $schema->form_fee),
'version' => $newVersion,
]);
return $this->redirect('/forms/builder')->withSuccess('تم تحديث النموذج — الإصدار ' . $newVersion);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Forms\Models\FormSchema;
use App\Modules\Forms\Models\FormSubmission;
use App\Modules\Forms\Services\FormRenderer;
use App\Modules\Forms\Services\FormValidator;
class FormController extends Controller
{
public function index(Request $request): Response
{
$schemas = FormSchema::allActive();
return $this->view('Forms.Views.submissions', [
'schemas' => $schemas,
'rows' => [],
'filters' => ['form_code' => '', 'status' => '', 'search' => '', 'date_from' => '', 'date_to' => ''],
'pagination' => ['last_page' => 1, 'current_page' => 1],
]);
}
public function submissions(Request $request): Response
{
$filters = [
'form_code' => $request->get('form_code', ''),
'status' => $request->get('status', ''),
'search' => trim((string) $request->get('q', '')),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = FormSubmission::search($filters, 25, $page);
$schemas = FormSchema::allActive();
return $this->view('Forms.Views.submissions', [
'schemas' => $schemas,
'rows' => $result['data'],
'filters' => $filters,
'pagination' => $result['pagination'],
]);
}
public function render(Request $request, string $code): Response
{
$schema = FormSchema::findByCode($code);
if (!$schema) {
return $this->redirect('/forms')->withError('نموذج غير موجود');
}
return $this->view('Forms.Views.render', [
'schema' => $schema,
'formHtml' => FormRenderer::render($schema),
'data' => [],
'errors' => [],
]);
}
public function submit(Request $request, string $code): Response
{
$schema = FormSchema::findByCode($code);
if (!$schema) {
return $this->redirect('/forms')->withError('نموذج غير موجود');
}
$submittedData = $request->all();
unset($submittedData['_csrf_token']);
$validation = FormValidator::validate($schema, $submittedData);
if (!empty($validation['errors'])) {
return $this->view('Forms.Views.render', [
'schema' => $schema,
'formHtml' => FormRenderer::render($schema, $submittedData, $validation['errors']),
'data' => $submittedData,
'errors' => $validation['errors'],
]);
}
$employee = App::getInstance()->currentEmployee();
$formNumber = FormSubmission::generateFormNumber($code);
$expiresAt = null;
if ($schema->validity_days) {
$expiresAt = date('Y-m-d H:i:s', time() + ($schema->validity_days * 86400));
}
$submission = FormSubmission::create([
'form_schema_id' => (int) $schema->id,
'schema_version' => (int) $schema->version,
'form_number' => $formNumber,
'submitted_data_json' => json_encode($submittedData, JSON_UNESCAPED_UNICODE),
'status' => 'submitted',
'submitted_by_employee_id' => $employee ? (int) $employee->id : null,
'expires_at' => $expiresAt,
'notes' => $submittedData['notes'] ?? null,
]);
$eventData = [
'submission_id' => (int) $submission->id,
'form_code' => $code,
'form_number' => $formNumber,
'data' => $submittedData,
];
EventBus::dispatch('form.submitted', $eventData);
return $this->redirect('/forms/submissions')->withSuccess('تم تقديم النموذج بنجاح — رقم: ' . $formNumber);
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$submission = $db->selectOne(
"SELECT sub.*, fs.form_code, fs.name_ar as schema_name_ar, fs.schema_json, e.full_name_ar as employee_name
FROM form_submissions sub
JOIN form_schemas fs ON fs.id = sub.form_schema_id
LEFT JOIN employees e ON e.id = sub.submitted_by_employee_id
WHERE sub.id = ?",
[(int) $id]
);
if (!$submission) {
return $this->redirect('/forms/submissions')->withError('النموذج غير موجود');
}
$schema = new FormSchema([
'id' => $submission['form_schema_id'],
'schema_json' => $submission['schema_json'],
]);
$data = json_decode($submission['submitted_data_json'] ?? '{}', true) ?? [];
return $this->view('Forms.Views.render', [
'schema' => $schema,
'formHtml' => FormRenderer::render($schema, $data, null, true),
'data' => $data,
'errors' => [],
'submission' => $submission,
'readOnly' => true,
]);
}
public function printForm(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$submission = $db->selectOne(
"SELECT sub.*, fs.form_code, fs.name_ar as schema_name_ar, fs.schema_json
FROM form_submissions sub
JOIN form_schemas fs ON fs.id = sub.form_schema_id
WHERE sub.id = ?",
[(int) $id]
);
if (!$submission) {
return $this->redirect('/forms/submissions')->withError('النموذج غير موجود');
}
$schema = new FormSchema([
'id' => $submission['form_schema_id'],
'schema_json' => $submission['schema_json'],
]);
$data = json_decode($submission['submitted_data_json'] ?? '{}', true) ?? [];
return $this->view('Forms.Views.print', [
'schema' => $schema,
'formHtml' => FormRenderer::render($schema, $data, null, true),
'submission' => $submission,
]);
}
public function updateStatus(Request $request, string $id): Response
{
$newStatus = trim((string) $request->post('status', ''));
$validStatuses = ['draft', 'submitted', 'under_review', 'approved', 'rejected', 'expired'];
if (!in_array($newStatus, $validStatuses)) {
return $this->redirect('/forms/submissions/' . $id)->withError('حالة غير صالحة');
}
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$db->update('form_submissions', [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
'updated_by' => $employee ? (int) $employee->id : null,
], '`id` = ?', [(int) $id]);
return $this->redirect('/forms/submissions/' . $id)->withSuccess('تم تحديث الحالة');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Models;
use App\Core\Model;
use App\Core\App;
class FormSchema extends Model
{
protected static string $table = 'form_schemas';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'form_code', 'name_ar', 'name_en', 'form_fee', 'validity_days',
'schema_json', 'version', 'published_at', 'is_active',
];
public function getSchema(): array
{
return json_decode($this->schema_json ?? '{"sections":[]}', true) ?? ['sections' => []];
}
public function getSections(): array
{
$schema = $this->getSchema();
$sections = $schema['sections'] ?? [];
usort($sections, fn($a, $b) => ($a['order'] ?? 999) <=> ($b['order'] ?? 999));
return $sections;
}
public static function findByCode(string $code): ?static
{
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT * FROM form_schemas WHERE form_code = ? AND is_active = 1", [$code]);
if (!$row) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM form_schemas WHERE is_active = 1 ORDER BY name_ar");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class FormSubmission extends Model
{
protected static string $table = 'form_submissions';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'form_schema_id', 'schema_version', 'form_number', 'submitted_data_json',
'status', 'submitted_by_employee_id', 'member_id', 'expires_at',
'fee_receipt_number', 'notes',
];
public function getData(): array
{
return json_decode($this->submitted_data_json ?? '{}', true) ?? [];
}
public static function generateFormNumber(string $formCode): string
{
$db = App::getInstance()->db();
$prefix = strtoupper(substr($formCode, 0, 3));
$year = date('Y');
$pattern = $prefix . '-' . $year . '-%';
$last = $db->selectOne(
"SELECT form_number FROM form_submissions WHERE form_number LIKE ? ORDER BY id DESC LIMIT 1",
[$pattern]
);
if ($last) {
$parts = explode('-', $last['form_number']);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $prefix . '-' . $year . '-' . str_pad((string) $seq, 5, '0', STR_PAD_LEFT);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['form_code'])) {
$where .= ' AND fs.form_code = ?';
$params[] = $filters['form_code'];
}
if (!empty($filters['status'])) {
$where .= ' AND sub.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['search'])) {
$where .= ' AND (sub.form_number LIKE ? OR sub.notes LIKE ?)';
$s = '%' . $filters['search'] . '%';
$params[] = $s;
$params[] = $s;
}
if (!empty($filters['date_from'])) {
$where .= ' AND sub.created_at >= ?';
$params[] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$where .= ' AND sub.created_at <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM form_submissions sub JOIN form_schemas fs ON fs.id = sub.form_schema_id WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT sub.*, fs.form_code, fs.name_ar as schema_name_ar, e.full_name_ar as employee_name
FROM form_submissions sub
JOIN form_schemas fs ON fs.id = sub.form_schema_id
LEFT JOIN employees e ON e.id = sub.submitted_by_employee_id
WHERE {$where}
ORDER BY sub.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$pagination = Pagination::paginate($total, $perPage, $page);
return ['data' => $rows, 'pagination' => $pagination];
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/forms', 'Forms\Controllers\FormController@index', ['auth'], 'forms.view'],
['GET', '/forms/submissions', 'Forms\Controllers\FormController@submissions', ['auth'], 'forms.view'],
['GET', '/forms/submissions/{id:\d+}', 'Forms\Controllers\FormController@show', ['auth'], 'forms.view'],
['POST', '/forms/submissions/{id:\d+}/status', 'Forms\Controllers\FormController@updateStatus', ['auth', 'csrf'], 'forms.view'],
['GET', '/forms/render/{code}', 'Forms\Controllers\FormController@render', ['auth'], 'forms.view'],
['POST', '/forms/submit/{code}', 'Forms\Controllers\FormController@submit', ['auth', 'csrf'], 'forms.view'],
['GET', '/forms/print/{id:\d+}', 'Forms\Controllers\FormController@printForm', ['auth'], 'forms.view'],
['GET', '/forms/builder', 'Forms\Controllers\FormBuilderController@index', ['auth'], 'forms.edit_schema'],
['GET', '/forms/builder/{id:\d+}', 'Forms\Controllers\FormBuilderController@edit', ['auth'], 'forms.edit_schema'],
['POST', '/forms/builder/{id:\d+}', 'Forms\Controllers\FormBuilderController@update', ['auth', 'csrf'], 'forms.edit_schema'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Services;
use App\Core\App;
use App\Modules\Forms\Models\FormSchema;
final class FormRenderer
{
public static function render(FormSchema $schema, ?array $existingData = null, ?array $errors = null, bool $readOnly = false): string
{
$sections = $schema->getSections();
$data = $existingData ?? [];
$errors = $errors ?? [];
$html = '';
foreach ($sections as $section) {
$sectionKey = $section['key'] ?? '';
$sectionLabel = $section['label_ar'] ?? '';
$visibleWhen = $section['visible_when'] ?? null;
$repeatable = $section['repeatable'] ?? false;
$visAttr = '';
if ($visibleWhen) {
$visAttr = ' data-visible-when=\'' . htmlspecialchars(json_encode($visibleWhen), ENT_QUOTES) . '\'';
$visAttr .= ' style="display:none;"';
}
$html .= '<div class="card" style="margin-bottom:20px;" id="section-' . e($sectionKey) . '"' . $visAttr . '>';
$html .= '<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">' . e($sectionLabel) . '</h3></div>';
$html .= '<div style="padding:20px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">';
$fields = $section['fields'] ?? [];
usort($fields, fn($a, $b) => ($a['order'] ?? 999) <=> ($b['order'] ?? 999));
foreach ($fields as $field) {
$html .= self::renderField($field, $data, $errors, $readOnly);
}
$html .= '</div></div></div>';
}
return $html;
}
public static function renderField(array $field, array $data, array $errors, bool $readOnly): string
{
$key = $field['key'] ?? '';
$type = $field['type'] ?? 'text';
$labelAr = $field['label_ar'] ?? $key;
$required = $field['required'] ?? false;
$editable = $field['editable'] ?? true;
$value = $data[$key] ?? ($field['default_value'] ?? '');
$fieldErrors = $errors[$key] ?? [];
$helpText = $field['help_text_ar'] ?? '';
$options = $field['options'] ?? [];
$visibleWhen = $field['visible_when'] ?? null;
$autoParse = $field['auto_parse'] ?? false;
$populates = $field['populates'] ?? [];
$computedFrom = $field['computed_from'] ?? '';
$width = $field['width'] ?? 'half';
$colStyle = match ($width) {
'full' => 'grid-column:1/-1;',
'quarter' => '',
'third' => '',
default => '',
};
if ($width === 'full') {
$colStyle = 'grid-column:1/-1;';
}
$disabled = ($readOnly || !$editable) ? ' disabled' : '';
$requiredMark = $required ? ' <span style="color:#DC2626;">*</span>' : '';
$errorClass = !empty($fieldErrors) ? ' border-color:#DC2626;' : '';
$visAttr = '';
if ($visibleWhen) {
$visAttr = ' data-visible-when=\'' . htmlspecialchars(json_encode($visibleWhen), ENT_QUOTES) . '\' style="display:none;' . $colStyle . '"';
} else {
$visAttr = ' style="' . $colStyle . '"';
}
$dataAttrs = '';
if ($autoParse) {
$dataAttrs .= ' data-auto-parse="true"';
}
if (!empty($populates)) {
$dataAttrs .= ' data-populates=\'' . htmlspecialchars(json_encode($populates), ENT_QUOTES) . '\'';
}
$html = '<div class="form-group"' . $visAttr . ' id="field-wrap-' . e($key) . '">';
if ($type !== 'hidden' && $type !== 'checkbox') {
$html .= '<label class="form-label" for="field-' . e($key) . '">' . e($labelAr) . $requiredMark . '</label>';
}
switch ($type) {
case 'text':
case 'passport':
case 'phone':
case 'phone_eg':
case 'email':
$inputType = ($type === 'email') ? 'email' : (($type === 'phone' || $type === 'phone_eg') ? 'tel' : 'text');
$html .= '<input type="' . $inputType . '" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input" style="' . $errorClass . '"' . $disabled . $dataAttrs . '>';
break;
case 'national_id':
$html .= '<input type="text" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input nid-input" maxlength="14" pattern="\\d{14}" style="direction:ltr;text-align:left;' . $errorClass . '"' . $disabled . $dataAttrs . ' data-nid-parser="true">';
break;
case 'number':
case 'decimal':
$step = ($type === 'decimal') ? '0.01' : '1';
$html .= '<input type="number" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" step="' . $step . '" class="form-input" style="' . $errorClass . '"' . $disabled . '>';
break;
case 'currency':
$html .= '<div style="position:relative;"><input type="text" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input" style="direction:ltr;text-align:left;padding-left:40px;' . $errorClass . '" disabled>';
$html .= '<span style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#6B7280;font-size:12px;">ج.م</span></div>';
break;
case 'date':
$html .= '<input type="date" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input" style="' . $errorClass . '"' . $disabled . '>';
break;
case 'select':
$html .= '<select id="field-' . e($key) . '" name="' . e($key) . '" class="form-select" style="' . $errorClass . '"' . $disabled . '>';
$html .= '<option value="">-- اختر --</option>';
foreach ($options as $opt) {
$optVal = $opt['value'] ?? '';
$optLabel = $opt['label_ar'] ?? $optVal;
$selected = ((string) $value === (string) $optVal) ? ' selected' : '';
$html .= '<option value="' . e($optVal) . '"' . $selected . '>' . e($optLabel) . '</option>';
}
$html .= '</select>';
break;
case 'select_dynamic':
$dataSource = $field['data_source'] ?? '';
$html .= '<select id="field-' . e($key) . '" name="' . e($key) . '" class="form-select" style="' . $errorClass . '"' . $disabled . ' data-source="' . e($dataSource) . '">';
$html .= '<option value="">-- اختر --</option>';
$dynamicOptions = self::loadDynamicOptions($dataSource);
foreach ($dynamicOptions as $opt) {
$selected = ((string) $value === (string) $opt['value']) ? ' selected' : '';
$html .= '<option value="' . e($opt['value']) . '"' . $selected . '>' . e($opt['label']) . '</option>';
}
$html .= '</select>';
break;
case 'textarea':
$html .= '<textarea id="field-' . e($key) . '" name="' . e($key) . '" class="form-textarea" rows="3" style="' . $errorClass . '"' . $disabled . '>' . e((string) $value) . '</textarea>';
break;
case 'checkbox':
$checked = $value ? ' checked' : '';
$html .= '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="field-' . e($key) . '" name="' . e($key) . '" value="1"' . $checked . $disabled . '> <span>' . e($labelAr) . '</span></label>';
break;
case 'radio':
foreach ($options as $opt) {
$optVal = $opt['value'] ?? '';
$optLabel = $opt['label_ar'] ?? $optVal;
$checked = ((string) $value === (string) $optVal) ? ' checked' : '';
$html .= '<label style="display:flex;align-items:center;gap:8px;margin-bottom:5px;"><input type="radio" name="' . e($key) . '" value="' . e($optVal) . '"' . $checked . $disabled . '> <span>' . e($optLabel) . '</span></label>';
}
break;
case 'file':
$html .= '<input type="file" id="field-' . e($key) . '" name="' . e($key) . '" class="form-input"' . $disabled . '>';
break;
case 'computed':
$html .= '<input type="text" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input" style="background:#F3F4F6;' . $errorClass . '" readonly>';
break;
case 'static_text':
$html .= '<div id="field-' . e($key) . '" style="padding:8px 12px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:6px;font-size:14px;min-height:38px;">' . e((string) $value) . '</div>';
break;
case 'hidden':
$html .= '<input type="hidden" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '">';
break;
case 'auto_increment':
case 'signature':
default:
$html .= '<input type="text" id="field-' . e($key) . '" name="' . e($key) . '" value="' . e((string) $value) . '" class="form-input" style="background:#F3F4F6;" readonly>';
break;
}
if (!empty($fieldErrors)) {
foreach ((array) $fieldErrors as $err) {
$html .= '<div style="color:#DC2626;font-size:12px;margin-top:4px;">' . e($err) . '</div>';
}
}
if ($helpText) {
$html .= '<small style="color:#9CA3AF;font-size:12px;">' . e($helpText) . '</small>';
}
$html .= '</div>';
return $html;
}
private static function loadDynamicOptions(string $source): array
{
$db = App::getInstance()->db();
$options = [];
switch ($source) {
case 'governorates':
$rows = $db->select("SELECT code as value, name_ar as label FROM governorates WHERE is_active = 1 ORDER BY name_ar");
return array_map(fn($r) => ['value' => $r['value'], 'label' => $r['label']], $rows);
case 'countries':
$rows = $db->select("SELECT nationality_ar as value, nationality_ar as label FROM countries WHERE is_active = 1 ORDER BY name_ar");
return array_map(fn($r) => ['value' => $r['value'], 'label' => $r['label']], $rows);
case 'qualifications':
$rows = $db->select("SELECT id as value, name_ar as label FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
return array_map(fn($r) => ['value' => (string) $r['value'], 'label' => $r['label']], $rows);
case 'branches':
$rows = $db->select("SELECT id as value, name_ar as label FROM branches WHERE is_active = 1 ORDER BY name_ar");
return array_map(fn($r) => ['value' => (string) $r['value'], 'label' => $r['label']], $rows);
default:
return [];
}
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Forms\Services;
use App\Core\Validator;
use App\Modules\Forms\Models\FormSchema;
final class FormValidator
{
public static function validate(FormSchema $schema, array $submittedData): array
{
$sections = $schema->getSections();
$rules = [];
$allErrors = [];
foreach ($sections as $section) {
$visibleWhen = $section['visible_when'] ?? null;
if ($visibleWhen && !self::evaluateCondition($visibleWhen, $submittedData)) {
continue;
}
foreach ($section['fields'] ?? [] as $field) {
$key = $field['key'] ?? '';
$fieldVisible = true;
$fieldVisibleWhen = $field['visible_when'] ?? null;
if ($fieldVisibleWhen && !self::evaluateCondition($fieldVisibleWhen, $submittedData)) {
$fieldVisible = false;
}
if (!$fieldVisible) {
continue;
}
$validation = $field['validation'] ?? '';
$required = $field['required'] ?? false;
$editable = $field['editable'] ?? true;
$type = $field['type'] ?? 'text';
if (!$editable || $type === 'computed' || $type === 'currency' || $type === 'static_text' || $type === 'auto_increment') {
continue;
}
if ($validation !== '') {
$rules[$key] = $validation;
} elseif ($required) {
$rules[$key] = 'required|string';
} else {
$rules[$key] = 'nullable';
}
}
}
if (empty($rules)) {
return ['errors' => [], 'validated' => $submittedData];
}
$validator = new Validator();
$result = $validator->validate($submittedData, $rules);
return [
'errors' => $result->errors(),
'validated' => $result->validated(),
'passes' => $result->passes(),
];
}
private static function evaluateCondition(array $condition, array $data): bool
{
$field = $condition['field'] ?? '';
$operator = $condition['operator'] ?? 'eq';
$value = $condition['value'] ?? '';
$actual = $data[$field] ?? '';
return match ($operator) {
'eq' => (string) $actual === (string) $value,
'neq' => (string) $actual !== (string) $value,
'in' => in_array((string) $actual, (array) $value, true),
'gt' => (float) $actual > (float) $value,
'lt' => (float) $actual < (float) $value,
'gte' => (float) $actual >= (float) $value,
'lte' => (float) $actual <= (float) $value,
'not_empty' => $actual !== '' && $actual !== null,
'empty' => $actual === '' || $actual === null,
default => true,
};
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>منشئ النماذج<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:250px 1fr;gap:20px;">
<div class="card" style="padding:15px;">
<h4 style="margin-bottom:15px;color:#0D7377;">النماذج المتاحة</h4>
<?php foreach ($schemas as $s): ?>
<a href="/forms/builder/<?= (int) $s['id'] ?>" class="btn btn-sm <?= ($schema && (int) $schema->id === (int) $s['id']) ? 'btn-primary' : 'btn-outline' ?>" style="display:block;margin-bottom:8px;text-align:right;">
<?= e($s['name_ar']) ?> <small style="color:#9CA3AF;">(v<?= (int) $s['version'] ?>)</small>
</a>
<?php endforeach; ?>
</div>
<div>
<?php if ($schema): ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 style="margin:0;color:#0D7377;"><?= e($schema->name_ar) ?> — الإصدار <?= (int) $schema->version ?></h3>
<span style="font-size:12px;color:#6B7280;"><?= e($schema->form_code) ?></span>
</div>
<form method="POST" action="/forms/builder/<?= (int) $schema->id ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي</label>
<input type="text" name="name_ar" value="<?= e($schema->name_ar) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">رسوم النموذج (ج.م)</label>
<input type="number" name="form_fee" value="<?= e($schema->form_fee) ?>" class="form-input" step="0.01">
</div>
</div>
<div class="form-group">
<label class="form-label">هيكل النموذج (JSON)</label>
<textarea name="schema_json" class="form-textarea" rows="25" style="font-family:monospace;direction:ltr;text-align:left;font-size:12px;"><?= e($schema->schema_json) ?></textarea>
</div>
<div style="margin-top:15px;display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
<a href="/forms/render/<?= e($schema->form_code) ?>" class="btn btn-outline" target="_blank">معاينة</a>
</div>
</form>
</div>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<p>اختر نموذجاً من القائمة لتعديله</p>
</div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.print'); ?>
<?php $__template->section('title'); ?><?= e($submission['schema_name_ar'] ?? 'نموذج') ?><?= e($submission['form_number'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:20px;text-align:center;">
<h2 style="color:#0D7377;margin:0;"><?= e($submission['schema_name_ar'] ?? '') ?></h2>
<p style="color:#6B7280;margin:5px 0;">رقم النموذج: <?= e($submission['form_number'] ?? '') ?> — التاريخ: <?= e(substr($submission['created_at'] ?? '', 0, 10)) ?></p>
</div>
<?= $formHtml ?>
<div style="margin-top:40px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:30px;text-align:center;">
<div style="border-top:1px solid #000;padding-top:10px;">توقيع مقدم الطلب</div>
<div style="border-top:1px solid #000;padding-top:10px;">الموظف المسئول</div>
<div style="border-top:1px solid #000;padding-top:10px;">يعتمد / المدير المسئول</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= e($schema->name_ar ?? 'نموذج') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/forms/submissions" class="btn btn-outline">← العودة للنماذج</a>
<?php if (!empty($submission)): ?>
<a href="/forms/print/<?= (int) $submission['id'] ?>" class="btn btn-outline" target="_blank">طباعة</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (!empty($submission)): ?>
<div class="card" style="margin-bottom:20px;padding:15px;display:flex;justify-content:space-between;align-items:center;">
<div>
<strong>رقم النموذج:</strong> <?= e($submission['form_number']) ?>
&nbsp;&nbsp;|&nbsp;&nbsp;
<strong>الحالة:</strong> <?= e($submission['status']) ?>
&nbsp;&nbsp;|&nbsp;&nbsp;
<strong>التاريخ:</strong> <?= e(substr($submission['created_at'], 0, 10)) ?>
<?php if ($submission['employee_name']): ?>
&nbsp;&nbsp;|&nbsp;&nbsp;
<strong>بواسطة:</strong> <?= e($submission['employee_name']) ?>
<?php endif; ?>
</div>
<?php if (empty($readOnly)): ?>
<form method="POST" action="/forms/submissions/<?= (int) $submission['id'] ?>/status" style="display:flex;gap:8px;">
<?= csrf_field() ?>
<select name="status" class="form-select" style="width:auto;">
<option value="submitted">مُقدّم</option>
<option value="under_review">تحت المراجعة</option>
<option value="approved">مقبول</option>
<option value="rejected">مرفوض</option>
<option value="expired">منتهي</option>
</select>
<button type="submit" class="btn btn-sm btn-outline">تحديث الحالة</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (empty($readOnly) && empty($submission)): ?>
<form method="POST" action="/forms/submit/<?= e($schema->form_code) ?>">
<?= csrf_field() ?>
<?= $formHtml ?>
<div style="margin-top:20px;display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">تقديم النموذج</button>
<a href="/forms/submissions" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php else: ?>
<?= $formHtml ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script src="<?= url('assets/js/forms-engine.js') ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof FormsEngine !== 'undefined') {
FormsEngine.init();
}
});
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إدارة النماذج<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<div style="display:flex;gap:10px;">
<?php foreach ($schemas as $s): ?>
<a href="/forms/render/<?= e($s['form_code']) ?>" class="btn btn-sm btn-outline"><?= e($s['name_ar']) ?></a>
<?php endforeach; ?>
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/forms/submissions" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['search'] ?? '') ?>" placeholder="رقم النموذج..." class="form-input" style="min-width:150px;">
</div>
<div>
<label class="form-label" style="font-size:12px;">نوع النموذج</label>
<select name="form_code" class="form-select" style="min-width:150px;">
<option value="">الكل</option>
<?php foreach ($schemas as $s): ?>
<option value="<?= e($s['form_code']) ?>" <?= ($filters['form_code'] ?? '') === $s['form_code'] ? 'selected' : '' ?>><?= e($s['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<option value="draft" <?= ($filters['status'] ?? '') === 'draft' ? 'selected' : '' ?>>مسودة</option>
<option value="submitted" <?= ($filters['status'] ?? '') === 'submitted' ? 'selected' : '' ?>>مُقدّم</option>
<option value="under_review" <?= ($filters['status'] ?? '') === 'under_review' ? 'selected' : '' ?>>تحت المراجعة</option>
<option value="approved" <?= ($filters['status'] ?? '') === 'approved' ? 'selected' : '' ?>>مقبول</option>
<option value="rejected" <?= ($filters['status'] ?? '') === 'rejected' ? 'selected' : '' ?>>مرفوض</option>
<option value="expired" <?= ($filters['status'] ?? '') === 'expired' ? 'selected' : '' ?>>منتهي</option>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline">بحث</button>
<a href="/forms/submissions" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم النموذج</th>
<th>نوع النموذج</th>
<th>الحالة</th>
<th>بواسطة</th>
<th>التاريخ</th>
<th>انتهاء الصلاحية</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($r['form_number']) ?></td>
<td><?= e($r['schema_name_ar'] ?? $r['form_code']) ?></td>
<td>
<?php
$statusColors = [
'draft' => '#6B7280', 'submitted' => '#0284C7', 'under_review' => '#D97706',
'approved' => '#059669', 'rejected' => '#DC2626', 'expired' => '#9CA3AF',
];
$statusLabels = [
'draft' => 'مسودة', 'submitted' => 'مُقدّم', 'under_review' => 'تحت المراجعة',
'approved' => 'مقبول', 'rejected' => 'مرفوض', 'expired' => 'منتهي',
];
$color = $statusColors[$r['status']] ?? '#6B7280';
$label = $statusLabels[$r['status']] ?? $r['status'];
?>
<span style="color:<?= $color ?>;font-weight:600;"><?= e($label) ?></span>
</td>
<td style="font-size:13px;"><?= e($r['employee_name'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e(substr($r['created_at'], 0, 10)) ?></td>
<td style="font-size:12px;"><?= $r['expires_at'] ? e(substr($r['expires_at'], 0, 10)) : '—' ?></td>
<td>
<div style="display:flex;gap:5px;">
<a href="/forms/submissions/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
<a href="/forms/print/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline" target="_blank">طباعة</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد نماذج مقدمة</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('forms', [
'label_ar' => 'النماذج',
'label_en' => 'Forms',
'icon' => '📋',
'route' => '/forms',
'permission' => 'forms.view',
'order' => 160,
'children' => [
['label_ar' => 'النماذج المقدمة', 'label_en' => 'Submissions', 'route' => '/forms/submissions', 'permission' => 'forms.view', 'order' => 1],
['label_ar' => 'منشئ النماذج', 'label_en' => 'Form Builder', 'route' => '/forms/builder', 'permission' => 'forms.edit_schema', 'order' => 2],
],
]);
PermissionRegistry::register('forms', [
'forms.view' => ['ar' => 'عرض النماذج', 'en' => 'View Forms'],
'forms.edit_schema' => ['ar' => 'تعديل هيكل النماذج', 'en' => 'Edit Form Schemas'],
'forms.create_schema' => ['ar' => 'إنشاء نموذج', 'en' => 'Create Form Schema'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `form_field_types` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`type_code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`renderer_config_json` JSON NULL,
`validator_rules_json` JSON NULL,
`is_system` TINYINT(1) NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
UNIQUE KEY `uq_form_field_types_code` (`type_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `form_field_types`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `form_schemas` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`form_code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`form_fee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`validity_days` INT UNSIGNED NULL,
`schema_json` JSON NOT NULL,
`version` INT UNSIGNED NOT NULL DEFAULT 1,
`published_at` TIMESTAMP NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_form_schemas_code` (`form_code`),
INDEX `idx_form_schemas_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `form_schemas`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `form_submissions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`form_schema_id` BIGINT UNSIGNED NOT NULL,
`schema_version` INT UNSIGNED NOT NULL DEFAULT 1,
`form_number` VARCHAR(50) NOT NULL,
`submitted_data_json` JSON NOT NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'draft',
`submitted_by_employee_id` BIGINT UNSIGNED NULL,
`member_id` BIGINT UNSIGNED NULL,
`expires_at` TIMESTAMP NULL DEFAULT NULL,
`fee_receipt_number` VARCHAR(50) NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_form_submissions_number` (`form_number`),
INDEX `idx_form_submissions_schema` (`form_schema_id`),
INDEX `idx_form_submissions_status` (`status`),
INDEX `idx_form_submissions_member` (`member_id`),
INDEX `idx_form_submissions_expires` (`expires_at`),
CONSTRAINT `fk_form_submissions_schema` FOREIGN KEY (`form_schema_id`) REFERENCES `form_schemas`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `form_submissions`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$types = [
[
'type_code' => 'text',
'name_ar' => 'نص',
'name_en' => 'Text',
'renderer_config_json' => '{"input_type":"text","maxlength":500}',
'validator_rules_json' => '{"base":"string"}',
'is_system' => 1,
],
[
'type_code' => 'textarea',
'name_ar' => 'نص طويل',
'name_en' => 'Textarea',
'renderer_config_json' => '{"rows":4,"maxlength":2000}',
'validator_rules_json' => '{"base":"string"}',
'is_system' => 1,
],
[
'type_code' => 'number',
'name_ar' => 'رقم',
'name_en' => 'Number',
'renderer_config_json' => '{"input_type":"number","step":"1"}',
'validator_rules_json' => '{"base":"numeric"}',
'is_system' => 1,
],
[
'type_code' => 'decimal',
'name_ar' => 'رقم عشري',
'name_en' => 'Decimal',
'renderer_config_json' => '{"input_type":"number","step":"0.01"}',
'validator_rules_json' => '{"base":"numeric"}',
'is_system' => 1,
],
[
'type_code' => 'date',
'name_ar' => 'تاريخ',
'name_en' => 'Date',
'renderer_config_json' => '{"input_type":"date"}',
'validator_rules_json' => '{"base":"date"}',
'is_system' => 1,
],
[
'type_code' => 'select',
'name_ar' => 'قائمة منسدلة',
'name_en' => 'Dropdown',
'renderer_config_json' => '{"element":"select"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'select_dynamic',
'name_ar' => 'قائمة ديناميكية',
'name_en' => 'Dynamic Dropdown',
'renderer_config_json' => '{"element":"select","source":"api"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'national_id',
'name_ar' => 'الرقم القومي',
'name_en' => 'National ID',
'renderer_config_json' => '{"input_type":"text","maxlength":14,"pattern":"\\\\d{14}","auto_parse":true}',
'validator_rules_json' => '{"base":"digits:14|national_id"}',
'is_system' => 1,
],
[
'type_code' => 'passport',
'name_ar' => 'جواز السفر',
'name_en' => 'Passport',
'renderer_config_json' => '{"input_type":"text","maxlength":20}',
'validator_rules_json' => '{"base":"string|min:5|max:20"}',
'is_system' => 1,
],
[
'type_code' => 'phone',
'name_ar' => 'هاتف',
'name_en' => 'Phone',
'renderer_config_json' => '{"input_type":"tel","maxlength":20}',
'validator_rules_json' => '{"base":"string|max:20"}',
'is_system' => 1,
],
[
'type_code' => 'phone_eg',
'name_ar' => 'هاتف مصري',
'name_en' => 'Egyptian Phone',
'renderer_config_json' => '{"input_type":"tel","maxlength":11,"pattern":"01[0-9]{9}"}',
'validator_rules_json' => '{"base":"phone_eg"}',
'is_system' => 1,
],
[
'type_code' => 'email',
'name_ar' => 'بريد إلكتروني',
'name_en' => 'Email',
'renderer_config_json' => '{"input_type":"email"}',
'validator_rules_json' => '{"base":"email"}',
'is_system' => 1,
],
[
'type_code' => 'checkbox',
'name_ar' => 'اختيار متعدد',
'name_en' => 'Checkbox',
'renderer_config_json' => '{"element":"checkbox"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'radio',
'name_ar' => 'اختيار واحد',
'name_en' => 'Radio',
'renderer_config_json' => '{"element":"radio"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'file',
'name_ar' => 'ملف',
'name_en' => 'File Upload',
'renderer_config_json' => '{"input_type":"file","accept":"image/*,.pdf"}',
'validator_rules_json' => '{"base":"file"}',
'is_system' => 1,
],
[
'type_code' => 'hidden',
'name_ar' => 'مخفي',
'name_en' => 'Hidden',
'renderer_config_json' => '{"input_type":"hidden"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'computed',
'name_ar' => 'محسوب',
'name_en' => 'Computed (Read-only)',
'renderer_config_json' => '{"readonly":true,"css_class":"computed-field"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'currency',
'name_ar' => 'مبلغ مالي',
'name_en' => 'Currency',
'renderer_config_json' => '{"input_type":"number","step":"0.01","suffix":"ج.م","readonly":true}',
'validator_rules_json' => '{"base":"numeric"}',
'is_system' => 1,
],
[
'type_code' => 'auto_increment',
'name_ar' => 'رقم تسلسلي',
'name_en' => 'Auto Increment',
'renderer_config_json' => '{"readonly":true}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'signature',
'name_ar' => 'توقيع',
'name_en' => 'Signature',
'renderer_config_json' => '{"element":"checkbox","label_override":"أقر بصحة البيانات"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
[
'type_code' => 'static_text',
'name_ar' => 'نص ثابت',
'name_en' => 'Static Text',
'renderer_config_json' => '{"element":"div","css_class":"static-text"}',
'validator_rules_json' => '{}',
'is_system' => 1,
],
];
foreach ($types as $type) {
$existing = $db->selectOne("SELECT id FROM form_field_types WHERE type_code = ?", [$type['type_code']]);
if ($existing) {
continue;
}
$db->insert('form_field_types', [
'type_code' => $type['type_code'],
'name_ar' => $type['name_ar'],
'name_en' => $type['name_en'],
'renderer_config_json' => $type['renderer_config_json'],
'validator_rules_json' => $type['validator_rules_json'],
'is_system' => $type['is_system'],
'is_active' => 1,
]);
}
};
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$schemas = [
[
'form_code' => 'NEW_MEMBERSHIP',
'name_ar' => 'استمارة عضوية جديدة',
'name_en' => 'New Membership Application',
'form_fee' => '505.00',
'validity_days' => 15,
'schema_json' => json_encode([
'sections' => [
[
'key' => 'personal_data', 'label_ar' => 'البيانات الشخصية', 'order' => 1, 'visible_when' => null,
'fields' => [
['key' => 'full_name_ar', 'type' => 'text', 'label_ar' => 'الاسم بالكامل (عربي)', 'required' => true, 'editable' => true, 'validation' => 'required|string|min:10|max:200', 'order' => 1, 'width' => 'full'],
['key' => 'full_name_en', 'type' => 'text', 'label_ar' => 'الاسم بالكامل (إنجليزي)', 'required' => false, 'editable' => true, 'validation' => 'nullable|string|max:200', 'order' => 2, 'width' => 'half'],
['key' => 'id_type', 'type' => 'select', 'label_ar' => 'نوع إثبات الشخصية', 'required' => true, 'editable' => true, 'validation' => 'required|in:national_id,passport,military_id', 'order' => 3, 'width' => 'half', 'options' => [['value' => 'national_id', 'label_ar' => 'رقم قومي'], ['value' => 'passport', 'label_ar' => 'جواز سفر'], ['value' => 'military_id', 'label_ar' => 'بطاقة عسكرية']]],
['key' => 'national_id', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => true, 'editable' => true, 'auto_parse' => true, 'populates' => ['date_of_birth', 'age_years', 'age_months', 'gender', 'governorate_code'], 'validation' => 'required|digits:14|national_id', 'order' => 4, 'width' => 'half', 'visible_when' => ['field' => 'id_type', 'operator' => 'eq', 'value' => 'national_id'], 'help_text_ar' => 'أدخل الرقم القومي المكون من 14 رقم'],
['key' => 'passport_number', 'type' => 'passport', 'label_ar' => 'رقم جواز السفر', 'required' => true, 'editable' => true, 'validation' => 'required|string|min:5|max:20', 'order' => 5, 'width' => 'half', 'visible_when' => ['field' => 'id_type', 'operator' => 'eq', 'value' => 'passport']],
['key' => 'date_of_birth', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'required' => true, 'editable' => false, 'validation' => 'required|date', 'order' => 6, 'width' => 'quarter', 'computed_from' => 'national_id'],
['key' => 'age_years', 'type' => 'computed', 'label_ar' => 'السن (سنوات)', 'required' => false, 'editable' => false, 'order' => 7, 'width' => 'quarter'],
['key' => 'age_months', 'type' => 'computed', 'label_ar' => 'السن (أشهر)', 'required' => false, 'editable' => false, 'order' => 8, 'width' => 'quarter'],
['key' => 'gender', 'type' => 'select', 'label_ar' => 'النوع', 'required' => true, 'editable' => false, 'validation' => 'required|in:male,female', 'order' => 9, 'width' => 'quarter', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']], 'computed_from' => 'national_id'],
['key' => 'governorate_code', 'type' => 'select_dynamic', 'label_ar' => 'محافظة الميلاد', 'required' => false, 'editable' => false, 'order' => 10, 'width' => 'half', 'data_source' => 'governorates', 'computed_from' => 'national_id'],
['key' => 'nationality', 'type' => 'select_dynamic', 'label_ar' => 'الجنسية', 'required' => true, 'editable' => true, 'validation' => 'required|string', 'order' => 11, 'width' => 'half', 'data_source' => 'countries', 'default_value' => 'مصري'],
['key' => 'religion', 'type' => 'select', 'label_ar' => 'الديانة', 'required' => true, 'editable' => true, 'validation' => 'required|string', 'order' => 12, 'width' => 'half', 'options' => [['value' => 'muslim', 'label_ar' => 'مسلم'], ['value' => 'christian', 'label_ar' => 'مسيحي'], ['value' => 'other', 'label_ar' => 'أخرى']]],
['key' => 'qualification_id', 'type' => 'select_dynamic', 'label_ar' => 'المؤهل الدراسي', 'required' => true, 'editable' => true, 'validation' => 'required|integer', 'order' => 13, 'width' => 'half', 'data_source' => 'qualifications', 'help_text_ar' => 'المؤهل يؤثر على قيمة العضوية'],
['key' => 'marital_status', 'type' => 'select', 'label_ar' => 'الحالة الاجتماعية', 'required' => true, 'editable' => true, 'validation' => 'required|in:single,married,divorced,widowed', 'order' => 14, 'width' => 'half', 'options' => [['value' => 'single', 'label_ar' => 'أعزب'], ['value' => 'married', 'label_ar' => 'متزوج'], ['value' => 'divorced', 'label_ar' => 'مطلق'], ['value' => 'widowed', 'label_ar' => 'أرمل']]],
['key' => 'id_issue_date', 'type' => 'date', 'label_ar' => 'تاريخ إصدار إثبات الشخصية', 'required' => true, 'editable' => true, 'validation' => 'required|date|date_before_today', 'order' => 15, 'width' => 'half'],
['key' => 'id_expiry_date', 'type' => 'date', 'label_ar' => 'تاريخ انتهاء إثبات الشخصية', 'required' => false, 'editable' => true, 'validation' => 'nullable|date', 'order' => 16, 'width' => 'half'],
],
],
[
'key' => 'contact', 'label_ar' => 'بيانات الاتصال', 'order' => 2, 'visible_when' => null,
'fields' => [
['key' => 'phone_mobile', 'type' => 'phone_eg', 'label_ar' => 'رقم المحمول', 'required' => true, 'editable' => true, 'validation' => 'required|phone_eg', 'order' => 1, 'width' => 'half'],
['key' => 'phone_home', 'type' => 'phone', 'label_ar' => 'تليفون المنزل', 'required' => false, 'editable' => true, 'validation' => 'nullable|string|max:20', 'order' => 2, 'width' => 'half'],
['key' => 'phone_international', 'type' => 'phone', 'label_ar' => 'رقم تليفون خارج مصر', 'required' => false, 'editable' => true, 'validation' => 'nullable|string|max:30', 'order' => 3, 'width' => 'half'],
['key' => 'email', 'type' => 'email', 'label_ar' => 'البريد الإلكتروني', 'required' => false, 'editable' => true, 'validation' => 'nullable|email', 'order' => 4, 'width' => 'half'],
['key' => 'emergency_name', 'type' => 'text', 'label_ar' => 'شخص للطوارئ — الاسم', 'required' => true, 'editable' => true, 'validation' => 'required|string|min:5|max:100', 'order' => 5, 'width' => 'half'],
['key' => 'emergency_phone', 'type' => 'phone_eg', 'label_ar' => 'شخص للطوارئ — المحمول', 'required' => true, 'editable' => true, 'validation' => 'required|phone_eg', 'order' => 6, 'width' => 'half'],
],
],
[
'key' => 'residence', 'label_ar' => 'بيانات السكن', 'order' => 3, 'visible_when' => null,
'fields' => [
['key' => 'residence_type', 'type' => 'select', 'label_ar' => 'نوع السكن', 'required' => true, 'editable' => true, 'validation' => 'required|in:rented,owned,other', 'order' => 1, 'width' => 'half', 'options' => [['value' => 'rented', 'label_ar' => 'إيجار'], ['value' => 'owned', 'label_ar' => 'ملك'], ['value' => 'other', 'label_ar' => 'أخرى']]],
['key' => 'residence_address', 'type' => 'textarea', 'label_ar' => 'عنوان السكن', 'required' => true, 'editable' => true, 'validation' => 'required|string|min:10|max:500', 'order' => 2, 'width' => 'full'],
['key' => 'landmark', 'type' => 'text', 'label_ar' => 'علامة مميزة', 'required' => false, 'editable' => true, 'order' => 3, 'width' => 'half'],
['key' => 'floor', 'type' => 'text', 'label_ar' => 'الدور', 'required' => false, 'editable' => true, 'order' => 4, 'width' => 'quarter'],
['key' => 'apartment', 'type' => 'text', 'label_ar' => 'رقم الشقة', 'required' => false, 'editable' => true, 'order' => 5, 'width' => 'quarter'],
['key' => 'area', 'type' => 'text', 'label_ar' => 'المنطقة', 'required' => true, 'editable' => true, 'validation' => 'required|string', 'order' => 6, 'width' => 'half'],
['key' => 'governorate', 'type' => 'select_dynamic', 'label_ar' => 'المحافظة', 'required' => true, 'editable' => true, 'validation' => 'required', 'order' => 7, 'width' => 'half', 'data_source' => 'governorates'],
['key' => 'correspondence_address', 'type' => 'select', 'label_ar' => 'عنوان المراسلات', 'required' => true, 'editable' => true, 'validation' => 'required|in:work,residence,other', 'order' => 8, 'width' => 'half', 'options' => [['value' => 'work', 'label_ar' => 'العمل'], ['value' => 'residence', 'label_ar' => 'السكن'], ['value' => 'other', 'label_ar' => 'أخرى']]],
],
],
[
'key' => 'work', 'label_ar' => 'بيانات العمل', 'order' => 4, 'visible_when' => null,
'fields' => [
['key' => 'employment_type', 'type' => 'select', 'label_ar' => 'نوع التوظيف', 'required' => true, 'editable' => true, 'validation' => 'required|in:employed,self_employed,professions,other', 'order' => 1, 'width' => 'half', 'options' => [['value' => 'employed', 'label_ar' => 'موظف'], ['value' => 'self_employed', 'label_ar' => 'أعمال حرة'], ['value' => 'professions', 'label_ar' => 'مهن حرة'], ['value' => 'other', 'label_ar' => 'أخرى']]],
['key' => 'occupation', 'type' => 'text', 'label_ar' => 'المهنة', 'required' => true, 'editable' => true, 'validation' => 'required|string', 'order' => 2, 'width' => 'half'],
['key' => 'job_title', 'type' => 'text', 'label_ar' => 'المركز الوظيفي', 'required' => false, 'editable' => true, 'order' => 3, 'width' => 'half'],
['key' => 'employment_date', 'type' => 'date', 'label_ar' => 'تاريخ الالتحاق بالعمل', 'required' => false, 'editable' => true, 'validation' => 'nullable|date', 'order' => 4, 'width' => 'half'],
['key' => 'business_address', 'type' => 'textarea', 'label_ar' => 'عنوان العمل', 'required' => false, 'editable' => true, 'order' => 5, 'width' => 'full'],
['key' => 'office_phone', 'type' => 'phone', 'label_ar' => 'تليفون العمل', 'required' => false, 'editable' => true, 'order' => 6, 'width' => 'half'],
['key' => 'office_fax', 'type' => 'phone', 'label_ar' => 'فاكس العمل', 'required' => false, 'editable' => true, 'order' => 7, 'width' => 'half'],
['key' => 'business_activity', 'type' => 'text', 'label_ar' => 'نشاط العمل', 'required' => false, 'editable' => true, 'order' => 8, 'width' => 'half'],
],
],
[
'key' => 'referral', 'label_ar' => 'كيف عرفت النادي', 'order' => 5, 'visible_when' => null,
'fields' => [
['key' => 'referral_social_media', 'type' => 'checkbox', 'label_ar' => 'مواقع التواصل الاجتماعي', 'required' => false, 'order' => 1, 'width' => 'half'],
['key' => 'referral_tv', 'type' => 'checkbox', 'label_ar' => 'إعلان التليفزيون', 'required' => false, 'order' => 2, 'width' => 'half'],
['key' => 'referral_friend', 'type' => 'checkbox', 'label_ar' => 'من خلال صديق', 'required' => false, 'order' => 3, 'width' => 'half'],
['key' => 'referral_radio', 'type' => 'checkbox', 'label_ar' => 'إعلان الراديو', 'required' => false, 'order' => 4, 'width' => 'half'],
['key' => 'referral_outdoor', 'type' => 'checkbox', 'label_ar' => 'إعلانات الطريق', 'required' => false, 'order' => 5, 'width' => 'half'],
],
],
[
'key' => 'notes_section', 'label_ar' => 'ملاحظات', 'order' => 6, 'visible_when' => null,
'fields' => [
['key' => 'notes', 'type' => 'textarea', 'label_ar' => 'ملاحظات', 'required' => false, 'editable' => true, 'order' => 1, 'width' => 'full'],
],
],
],
], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'TRANSFER_SEPARATION',
'name_ar' => 'استمارة تحويل / فصل',
'name_en' => 'Transfer / Separation Form',
'form_fee' => '570.00',
'validity_days' => 15,
'schema_json' => json_encode([
'sections' => [
[
'key' => 'transfer_info', 'label_ar' => 'بيانات التحويل', 'order' => 1,
'fields' => [
['key' => 'transfer_type', 'type' => 'select', 'label_ar' => 'نوع التحويل', 'required' => true, 'validation' => 'required|in:divorce,death,child_separation,child_mandatory_25,waiver,sports_conversion,cross_branch', 'order' => 1, 'width' => 'half', 'options' => [['value' => 'divorce', 'label_ar' => 'طلاق'], ['value' => 'death', 'label_ar' => 'وفاة'], ['value' => 'child_separation', 'label_ar' => 'فصل أبناء'], ['value' => 'child_mandatory_25', 'label_ar' => 'تحويل وجوبي 25'], ['value' => 'waiver', 'label_ar' => 'تنازل'], ['value' => 'sports_conversion', 'label_ar' => 'تحويل رياضي'], ['value' => 'cross_branch', 'label_ar' => 'تحويل بين فروع']]],
['key' => 'source_membership_number', 'type' => 'text', 'label_ar' => 'رقم العضوية المصدر', 'required' => true, 'editable' => false, 'order' => 2, 'width' => 'half'],
['key' => 'original_acquisition_date', 'type' => 'date', 'label_ar' => 'تاريخ اكتساب العضوية الأصلية', 'required' => true, 'editable' => false, 'order' => 3, 'width' => 'half'],
['key' => 'years_since_acquisition', 'type' => 'computed', 'label_ar' => 'عدد السنوات منذ الاكتساب', 'required' => false, 'editable' => false, 'order' => 4, 'width' => 'quarter'],
['key' => 'new_membership_value', 'type' => 'currency', 'label_ar' => 'قيمة العضوية الجديدة وقت الطلب', 'required' => true, 'editable' => false, 'order' => 5, 'width' => 'half'],
['key' => 'fee_percentage', 'type' => 'computed', 'label_ar' => 'نسبة رسوم الفصل', 'required' => false, 'editable' => false, 'order' => 6, 'width' => 'quarter'],
['key' => 'separation_fee', 'type' => 'currency', 'label_ar' => 'مبلغ رسوم الفصل', 'required' => false, 'editable' => false, 'order' => 7, 'width' => 'half'],
['key' => 'form_fee_display', 'type' => 'currency', 'label_ar' => 'رسوم الاستمارة', 'required' => false, 'editable' => false, 'order' => 8, 'width' => 'quarter', 'default_value' => '570.00'],
['key' => 'annual_subscription_fee', 'type' => 'currency', 'label_ar' => 'الاشتراك السنوي', 'required' => false, 'editable' => false, 'order' => 9, 'width' => 'quarter'],
['key' => 'total_fee', 'type' => 'currency', 'label_ar' => 'الإجمالي المطلوب', 'required' => false, 'editable' => false, 'order' => 10, 'width' => 'half'],
['key' => 'fee_receipt_number', 'type' => 'text', 'label_ar' => 'رقم إيصال الخزينة', 'required' => true, 'editable' => true, 'validation' => 'required|string', 'order' => 11, 'width' => 'half'],
],
],
[
'key' => 'personal_data', 'label_ar' => 'البيانات الشخصية للعضو الجديد', 'order' => 2,
'fields' => [
['key' => 'full_name_ar', 'type' => 'text', 'label_ar' => 'الاسم بالكامل (عربي)', 'required' => true, 'validation' => 'required|string|min:10|max:200', 'order' => 1, 'width' => 'full'],
['key' => 'national_id', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => true, 'auto_parse' => true, 'populates' => ['date_of_birth', 'age_years', 'gender', 'governorate_code'], 'validation' => 'required|digits:14|national_id', 'order' => 2, 'width' => 'half'],
['key' => 'date_of_birth', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'required' => true, 'editable' => false, 'order' => 3, 'width' => 'quarter'],
['key' => 'age_years', 'type' => 'computed', 'label_ar' => 'السن', 'editable' => false, 'order' => 4, 'width' => 'quarter'],
['key' => 'gender', 'type' => 'select', 'label_ar' => 'النوع', 'editable' => false, 'order' => 5, 'width' => 'quarter', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']]],
['key' => 'qualification_id', 'type' => 'select_dynamic', 'label_ar' => 'المؤهل', 'required' => true, 'validation' => 'required|integer', 'order' => 6, 'width' => 'half', 'data_source' => 'qualifications'],
['key' => 'phone_mobile', 'type' => 'phone_eg', 'label_ar' => 'رقم المحمول', 'required' => true, 'validation' => 'required|phone_eg', 'order' => 7, 'width' => 'half'],
],
],
[
'key' => 'notes_section', 'label_ar' => 'ملاحظات', 'order' => 3,
'fields' => [
['key' => 'notes', 'type' => 'textarea', 'label_ar' => 'ملاحظات', 'required' => false, 'order' => 1, 'width' => 'full'],
],
],
],
], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'ADDITION_CHILD',
'name_ar' => 'استمارة ضم أبناء',
'name_en' => 'Children Addition Form',
'form_fee' => '570.00',
'validity_days' => 15,
'schema_json' => json_encode([
'sections' => [
[
'key' => 'owner_info', 'label_ar' => 'بيانات صاحب العضوية', 'order' => 1,
'fields' => [
['key' => 'member_name', 'type' => 'text', 'label_ar' => 'اسم مقدم الطلب', 'required' => true, 'editable' => false, 'order' => 1, 'width' => 'half'],
['key' => 'membership_number', 'type' => 'text', 'label_ar' => 'رقم العضوية', 'required' => true, 'editable' => false, 'order' => 2, 'width' => 'quarter'],
['key' => 'member_phone', 'type' => 'phone_eg', 'label_ar' => 'رقم المحمول', 'required' => true, 'editable' => false, 'order' => 3, 'width' => 'quarter'],
],
],
[
'key' => 'children', 'label_ar' => 'بيانات الأبناء المراد ضمهم', 'order' => 2, 'repeatable' => true, 'max_repeats' => 3,
'fields' => [
['key' => 'child_name', 'type' => 'text', 'label_ar' => 'الاسم', 'required' => true, 'validation' => 'required|string|min:5|max:200', 'order' => 1, 'width' => 'half'],
['key' => 'child_relationship', 'type' => 'select', 'label_ar' => 'الصفة', 'required' => true, 'validation' => 'required|in:son,daughter,temporary', 'order' => 2, 'width' => 'quarter', 'options' => [['value' => 'son', 'label_ar' => 'ابن'], ['value' => 'daughter', 'label_ar' => 'ابنة'], ['value' => 'temporary', 'label_ar' => 'مؤقت']]],
['key' => 'child_gender', 'type' => 'select', 'label_ar' => 'النوع', 'required' => true, 'order' => 3, 'width' => 'quarter', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']]],
['key' => 'child_national_id', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => false, 'auto_parse' => true, 'populates' => ['child_dob', 'child_age', 'child_gender'], 'order' => 4, 'width' => 'half', 'help_text_ar' => 'مطلوب للأبناء فوق 16 سنة'],
['key' => 'child_dob', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'required' => true, 'order' => 5, 'width' => 'quarter'],
['key' => 'child_age', 'type' => 'computed', 'label_ar' => 'السن', 'editable' => false, 'order' => 6, 'width' => 'quarter'],
['key' => 'child_temp_category', 'type' => 'select', 'label_ar' => 'فئة العضو المؤقت', 'required' => false, 'order' => 7, 'width' => 'half', 'visible_when' => ['field' => 'child_relationship', 'operator' => 'eq', 'value' => 'temporary'], 'options' => [['value' => 'parent', 'label_ar' => 'والدين'], ['value' => 'special_needs', 'label_ar' => 'ذوي احتياجات خاصة'], ['value' => 'unmarried_daughter', 'label_ar' => 'بنات غير متزوجات'], ['value' => 'sister', 'label_ar' => 'شقيقة'], ['value' => 'stepchild', 'label_ar' => 'أبناء الزوج/الزوجة'], ['value' => 'orphan', 'label_ar' => 'طفل يتيم'], ['value' => 'disabled_sibling', 'label_ar' => 'شقيق معاق'], ['value' => 'nanny', 'label_ar' => 'مربية']]],
['key' => 'child_has_championship', 'type' => 'checkbox', 'label_ar' => 'حاصل على بطولات جمهورية', 'required' => false, 'order' => 8, 'width' => 'half'],
['key' => 'child_fee', 'type' => 'currency', 'label_ar' => 'رسوم الإضافة', 'editable' => false, 'order' => 9, 'width' => 'half'],
['key' => 'child_notes', 'type' => 'textarea', 'label_ar' => 'ملاحظات', 'required' => false, 'order' => 10, 'width' => 'full'],
],
],
],
], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'ADDITION_SPOUSE',
'name_ar' => 'استمارة ضم زوج / زوجة',
'name_en' => 'Spouse Addition Form',
'form_fee' => '570.00',
'validity_days' => 15,
'schema_json' => json_encode([
'sections' => [
[
'key' => 'spouse_data', 'label_ar' => 'بيانات الزوج/الزوجة', 'order' => 1,
'fields' => [
['key' => 'spouse_name', 'type' => 'text', 'label_ar' => 'الاسم', 'required' => true, 'validation' => 'required|string|min:10|max:200', 'order' => 1, 'width' => 'full'],
['key' => 'spouse_national_id', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => true, 'auto_parse' => true, 'populates' => ['spouse_dob', 'spouse_age', 'spouse_gender'], 'validation' => 'required|digits:14|national_id', 'order' => 2, 'width' => 'half'],
['key' => 'spouse_dob', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'editable' => false, 'order' => 3, 'width' => 'quarter'],
['key' => 'spouse_age', 'type' => 'computed', 'label_ar' => 'السن', 'editable' => false, 'order' => 4, 'width' => 'quarter'],
['key' => 'spouse_gender', 'type' => 'select', 'label_ar' => 'النوع', 'editable' => false, 'order' => 5, 'width' => 'quarter', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']]],
['key' => 'spouse_classification', 'type' => 'computed', 'label_ar' => 'التصنيف', 'editable' => false, 'order' => 6, 'width' => 'quarter'],
['key' => 'spouse_nationality', 'type' => 'select_dynamic', 'label_ar' => 'الجنسية', 'required' => true, 'validation' => 'required|string', 'order' => 7, 'width' => 'half', 'data_source' => 'countries', 'default_value' => 'مصري'],
['key' => 'spouse_qualification', 'type' => 'select_dynamic', 'label_ar' => 'المؤهل الدراسي', 'required' => true, 'order' => 8, 'width' => 'half', 'data_source' => 'qualifications'],
['key' => 'spouse_religion', 'type' => 'select', 'label_ar' => 'الديانة', 'required' => true, 'order' => 9, 'width' => 'half', 'options' => [['value' => 'muslim', 'label_ar' => 'مسلم'], ['value' => 'christian', 'label_ar' => 'مسيحي'], ['value' => 'other', 'label_ar' => 'أخرى']]],
['key' => 'spouse_occupation', 'type' => 'text', 'label_ar' => 'الوظيفة', 'required' => false, 'order' => 10, 'width' => 'half'],
['key' => 'spouse_mobile', 'type' => 'phone_eg', 'label_ar' => 'محمول', 'required' => true, 'validation' => 'required|phone_eg', 'order' => 11, 'width' => 'half'],
['key' => 'spouse_work_phone', 'type' => 'phone', 'label_ar' => 'تليفون العمل', 'required' => false, 'order' => 12, 'width' => 'half'],
['key' => 'spouse_work_address', 'type' => 'text', 'label_ar' => 'عنوان العمل', 'required' => false, 'order' => 13, 'width' => 'full'],
['key' => 'marriage_date', 'type' => 'date', 'label_ar' => 'تاريخ الزواج', 'required' => true, 'validation' => 'required|date|date_before_today', 'order' => 14, 'width' => 'half'],
['key' => 'spouse_order', 'type' => 'computed', 'label_ar' => 'ترتيب الزوجة', 'editable' => false, 'order' => 15, 'width' => 'quarter'],
['key' => 'spouse_fee', 'type' => 'currency', 'label_ar' => 'رسوم الإضافة', 'editable' => false, 'order' => 16, 'width' => 'half'],
['key' => 'spouse_fee_breakdown', 'type' => 'static_text', 'label_ar' => 'تفصيل الرسوم', 'editable' => false, 'order' => 17, 'width' => 'full'],
['key' => 'fee_receipt_number', 'type' => 'text', 'label_ar' => 'رقم إيصال الخزينة', 'required' => true, 'validation' => 'required|string', 'order' => 18, 'width' => 'half'],
],
],
],
], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'ADDITION_TEMPORARY',
'name_ar' => 'استمارة ضم عضو مؤقت',
'name_en' => 'Temporary Member Addition',
'form_fee' => '570.00',
'validity_days' => 15,
'schema_json' => json_encode(['sections' => [['key' => 'temp_data', 'label_ar' => 'بيانات العضو المؤقت', 'order' => 1, 'fields' => [
['key' => 'temp_name', 'type' => 'text', 'label_ar' => 'الاسم', 'required' => true, 'validation' => 'required|string|min:5|max:200', 'order' => 1, 'width' => 'full'],
['key' => 'temp_category', 'type' => 'select', 'label_ar' => 'الفئة', 'required' => true, 'validation' => 'required', 'order' => 2, 'width' => 'half', 'options' => [['value' => 'parent', 'label_ar' => 'والدين'], ['value' => 'special_needs', 'label_ar' => 'ذوي احتياجات خاصة'], ['value' => 'unmarried_daughter', 'label_ar' => 'بنات غير متزوجات'], ['value' => 'sister', 'label_ar' => 'شقيقة'], ['value' => 'stepchild', 'label_ar' => 'أبناء الزوج/الزوجة'], ['value' => 'orphan', 'label_ar' => 'طفل يتيم'], ['value' => 'disabled_sibling', 'label_ar' => 'شقيق معاق'], ['value' => 'nanny', 'label_ar' => 'مربية']]],
['key' => 'temp_national_id', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => false, 'auto_parse' => true, 'order' => 3, 'width' => 'half'],
['key' => 'temp_dob', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'required' => true, 'order' => 4, 'width' => 'quarter'],
['key' => 'temp_gender', 'type' => 'select', 'label_ar' => 'النوع', 'required' => true, 'order' => 5, 'width' => 'quarter', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']]],
['key' => 'temp_has_championship', 'type' => 'checkbox', 'label_ar' => 'حاصل على بطولات جمهورية', 'order' => 6, 'width' => 'half'],
['key' => 'temp_fee', 'type' => 'currency', 'label_ar' => 'رسوم الإضافة', 'editable' => false, 'order' => 7, 'width' => 'half'],
['key' => 'fee_receipt_number', 'type' => 'text', 'label_ar' => 'رقم إيصال الخزينة', 'required' => true, 'order' => 8, 'width' => 'half'],
]]]], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'SEASONAL_MEMBERSHIP',
'name_ar' => 'استمارة عضوية موسمية',
'name_en' => 'Seasonal Membership Form',
'form_fee' => '0.00',
'validity_days' => null,
'schema_json' => json_encode(['sections' => [['key' => 'seasonal_data', 'label_ar' => 'بيانات العضوية الموسمية', 'order' => 1, 'fields' => [
['key' => 'seasonal_name', 'type' => 'text', 'label_ar' => 'الاسم', 'required' => true, 'validation' => 'required|string', 'order' => 1, 'width' => 'full'],
['key' => 'seasonal_nid', 'type' => 'national_id', 'label_ar' => 'الرقم القومي', 'required' => true, 'auto_parse' => true, 'order' => 2, 'width' => 'half'],
['key' => 'seasonal_dob', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'editable' => false, 'order' => 3, 'width' => 'quarter'],
['key' => 'seasonal_age', 'type' => 'computed', 'label_ar' => 'السن', 'editable' => false, 'order' => 4, 'width' => 'quarter'],
['key' => 'seasonal_qualification', 'type' => 'select_dynamic', 'label_ar' => 'المؤهل', 'required' => true, 'order' => 5, 'width' => 'half', 'data_source' => 'qualifications'],
['key' => 'seasonal_phone', 'type' => 'phone_eg', 'label_ar' => 'التليفون', 'required' => true, 'order' => 6, 'width' => 'half'],
['key' => 'seasonal_address', 'type' => 'text', 'label_ar' => 'العنوان', 'required' => true, 'order' => 7, 'width' => 'full'],
['key' => 'seasonal_fee', 'type' => 'currency', 'label_ar' => 'قيمة الاشتراك', 'editable' => false, 'order' => 8, 'width' => 'half'],
['key' => 'seasonal_start', 'type' => 'date', 'label_ar' => 'تاريخ البداية', 'required' => true, 'order' => 9, 'width' => 'quarter'],
['key' => 'seasonal_end', 'type' => 'date', 'label_ar' => 'تاريخ النهاية', 'editable' => false, 'order' => 10, 'width' => 'quarter'],
['key' => 'seasonal_duration', 'type' => 'static_text', 'label_ar' => 'المدة', 'editable' => false, 'order' => 11, 'width' => 'quarter', 'default_value' => '6 أشهر'],
]]]], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'LOST_CARNET',
'name_ar' => 'استمارة بدل فاقد كارنيه',
'name_en' => 'Lost Carnet Replacement',
'form_fee' => '200.00',
'validity_days' => null,
'schema_json' => json_encode(['sections' => [['key' => 'lost_info', 'label_ar' => 'بيانات البدل الفاقد', 'order' => 1, 'fields' => [
['key' => 'member_name', 'type' => 'text', 'label_ar' => 'اسم العضو', 'editable' => false, 'order' => 1, 'width' => 'half'],
['key' => 'membership_number', 'type' => 'text', 'label_ar' => 'رقم العضوية', 'editable' => false, 'order' => 2, 'width' => 'quarter'],
['key' => 'member_phone', 'type' => 'phone_eg', 'label_ar' => 'رقم المحمول', 'editable' => false, 'order' => 3, 'width' => 'quarter'],
['key' => 'loss_declaration', 'type' => 'checkbox', 'label_ar' => 'أقر بفقدان الكارنيه والتعهد بإعادته في حالة العثور عليه', 'required' => true, 'order' => 4, 'width' => 'full'],
['key' => 'fee_receipt_number', 'type' => 'text', 'label_ar' => 'رقم إيصال رسوم البدل', 'required' => true, 'order' => 5, 'width' => 'half'],
]]]], JSON_UNESCAPED_UNICODE),
],
[
'form_code' => 'DATA_UPDATE',
'name_ar' => 'استمارة تعديل بيانات',
'name_en' => 'Data Update Form',
'form_fee' => '0.00',
'validity_days' => null,
'schema_json' => json_encode(['sections' => [['key' => 'update_info', 'label_ar' => 'بيانات التعديل', 'order' => 1, 'fields' => [
['key' => 'member_name', 'type' => 'text', 'label_ar' => 'اسم العضو', 'editable' => false, 'order' => 1, 'width' => 'half'],
['key' => 'membership_number', 'type' => 'text', 'label_ar' => 'رقم العضوية', 'editable' => false, 'order' => 2, 'width' => 'half'],
['key' => 'fields_to_update', 'type' => 'textarea', 'label_ar' => 'الحقول المراد تعديلها', 'required' => true, 'order' => 3, 'width' => 'full'],
['key' => 'update_reason', 'type' => 'textarea', 'label_ar' => 'سبب التعديل', 'required' => true, 'order' => 4, 'width' => 'full'],
['key' => 'supporting_documents', 'type' => 'file', 'label_ar' => 'المستندات الداعمة', 'required' => false, 'order' => 5, 'width' => 'full'],
]]]], JSON_UNESCAPED_UNICODE),
],
];
foreach ($schemas as $s) {
$existing = $db->selectOne("SELECT id FROM form_schemas WHERE form_code = ?", [$s['form_code']]);
if ($existing) {
continue;
}
$db->insert('form_schemas', [
'form_code' => $s['form_code'],
'name_ar' => $s['name_ar'],
'name_en' => $s['name_en'] ?? null,
'form_fee' => $s['form_fee'],
'validity_days' => $s['validity_days'],
'schema_json' => $s['schema_json'],
'version' => 1,
'published_at' => $ts,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
\ No newline at end of file
/**
* Forms Engine — Client-side dynamic form companion
* Handles: conditional visibility, NID auto-parse, dependent fields, fee displays
*/
var FormsEngine = (function() {
'use strict';
function init() {
initConditionalVisibility();
initNidParsers();
initDynamicSources();
}
function initConditionalVisibility() {
var elements = document.querySelectorAll('[data-visible-when]');
elements.forEach(function(el) {
try {
var condition = JSON.parse(el.getAttribute('data-visible-when'));
if (!condition || !condition.field) return;
var watchField = document.getElementById('field-' + condition.field);
if (!watchField) {
watchField = document.querySelector('[name="' + condition.field + '"]');
}
if (!watchField) return;
function evaluate() {
var actual = watchField.value;
var expected = condition.value;
var op = condition.operator || 'eq';
var visible = false;
switch (op) {
case 'eq': visible = actual === expected; break;
case 'neq': visible = actual !== expected; break;
case 'not_empty': visible = actual !== '' && actual !== null; break;
case 'empty': visible = actual === '' || actual === null; break;
default: visible = actual === expected;
}
el.style.display = visible ? '' : 'none';
var inputs = el.querySelectorAll('input, select, textarea');
inputs.forEach(function(inp) {
if (!visible) {
inp.removeAttribute('required');
}
});
}
watchField.addEventListener('change', evaluate);
watchField.addEventListener('input', evaluate);
evaluate();
} catch (e) {
// Silently ignore bad JSON
}
});
}
function initNidParsers() {
var nidInputs = document.querySelectorAll('[data-nid-parser="true"]');
nidInputs.forEach(function(input) {
input.addEventListener('input', function() {
var val = input.value.replace(/\D/g, '');
input.value = val;
if (val.length === 14) {
parseNid(val, input);
}
});
input.addEventListener('blur', function() {
var val = input.value.replace(/\D/g, '');
if (val.length === 14) {
parseNid(val, input);
}
});
});
}
function parseNid(nid, inputEl) {
if (nid.length !== 14 || !/^\d{14}$/.test(nid)) return;
// Client-side parsing
var century = parseInt(nid[0]);
if (century !== 2 && century !== 3) return;
var yearPrefix = century === 2 ? '19' : '20';
var year = yearPrefix + nid.substring(1, 3);
var month = nid.substring(3, 5);
var day = nid.substring(5, 7);
var monthInt = parseInt(month);
var dayInt = parseInt(day);
if (monthInt < 1 || monthInt > 12 || dayInt < 1 || dayInt > 31) return;
var dob = year + '-' + month + '-' + day;
// Calculate age
var birthDate = new Date(parseInt(year), monthInt - 1, dayInt);
var today = new Date();
var ageYears = today.getFullYear() - birthDate.getFullYear();
var ageMonths = today.getMonth() - birthDate.getMonth();
if (ageMonths < 0 || (ageMonths === 0 && today.getDate() < birthDate.getDate())) {
ageYears--;
ageMonths += 12;
}
if (today.getDate() < birthDate.getDate()) {
ageMonths--;
if (ageMonths < 0) ageMonths += 12;
}
// Gender from positions 12-13
var seqNum = parseInt(nid.substring(12, 13));
var gender = (seqNum % 2 !== 0) ? 'male' : 'female';
// Governorate code
var govCode = nid.substring(7, 9);
// Try to populate fields
var populatesAttr = inputEl.getAttribute('data-populates');
var populates = [];
if (populatesAttr) {
try { populates = JSON.parse(populatesAttr); } catch(e) {}
}
// Auto-fill DOB
setFieldValue('date_of_birth', dob);
setFieldValue('child_dob', dob);
setFieldValue('spouse_dob', dob);
setFieldValue('seasonal_dob', dob);
// Auto-fill age
setFieldValue('age_years', ageYears.toString());
setFieldValue('age_months', ageMonths.toString());
setFieldValue('child_age', ageYears.toString());
setFieldValue('spouse_age', ageYears.toString());
setFieldValue('seasonal_age', ageYears.toString());
// Auto-fill gender
setFieldValue('gender', gender);
setFieldValue('child_gender', gender);
setFieldValue('spouse_gender', gender);
// Auto-fill governorate
setFieldValue('governorate_code', govCode);
// Spouse classification
if (ageYears >= 21) {
setFieldValue('spouse_classification', 'عضو عامل');
} else {
setFieldValue('spouse_classification', 'عضو تابع');
}
// Visual feedback
inputEl.style.borderColor = '#059669';
setTimeout(function() {
inputEl.style.borderColor = '';
}, 2000);
}
function setFieldValue(fieldKey, value) {
var el = document.getElementById('field-' + fieldKey);
if (!el) return;
if (el.tagName === 'SELECT') {
for (var i = 0; i < el.options.length; i++) {
if (el.options[i].value === value) {
el.selectedIndex = i;
break;
}
}
} else if (el.tagName === 'DIV') {
el.textContent = value;
} else {
el.value = value;
}
// Fire change event
var evt = new Event('change', { bubbles: true });
el.dispatchEvent(evt);
}
function initDynamicSources() {
// Dynamic sources are loaded server-side by FormRenderer
// This is a placeholder for future AJAX-based dynamic loading
}
return {
init: init,
parseNid: parseNid,
setFieldValue: setFieldValue
};
})();
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment