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
This diff is collapsed.
<?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
This diff is collapsed.
/**
* 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