Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
Clubphp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Clubphp
Commits
f90db6f4
Commit
f90db6f4
authored
Apr 07, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 18 files via Son of Anton
parent
7537971e
Changes
18
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
1750 additions
and
0 deletions
+1750
-0
FormBuilderController.php
app/Modules/Forms/Controllers/FormBuilderController.php
+54
-0
FormController.php
app/Modules/Forms/Controllers/FormController.php
+200
-0
FormSchema.php
app/Modules/Forms/Models/FormSchema.php
+49
-0
FormSubmission.php
app/Modules/Forms/Models/FormSubmission.php
+95
-0
Routes.php
app/Modules/Forms/Routes.php
+15
-0
FormRenderer.php
app/Modules/Forms/Services/FormRenderer.php
+233
-0
FormValidator.php
app/Modules/Forms/Services/FormValidator.php
+89
-0
builder.php
app/Modules/Forms/Views/builder.php
+51
-0
print.php
app/Modules/Forms/Views/print.php
+14
-0
render.php
app/Modules/Forms/Views/render.php
+66
-0
submissions.php
app/Modules/Forms/Views/submissions.php
+104
-0
bootstrap.php
app/Modules/Forms/bootstrap.php
+24
-0
Phase_06_001_create_form_field_types_table.php
...migrations/Phase_06_001_create_form_field_types_table.php
+19
-0
Phase_06_002_create_form_schemas_table.php
...ase/migrations/Phase_06_002_create_form_schemas_table.php
+26
-0
Phase_06_003_create_form_submissions_table.php
...migrations/Phase_06_003_create_form_submissions_table.php
+31
-0
Phase_06_001_seed_field_types.php
database/seeds/Phase_06_001_seed_field_types.php
+193
-0
Phase_06_002_seed_form_schemas.php
database/seeds/Phase_06_002_seed_form_schemas.php
+294
-0
forms-engine.js
public/assets/js/forms-engine.js
+193
-0
No files found.
app/Modules/Forms/Controllers/FormBuilderController.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Controllers/FormController.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Models/FormSchema.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Models/FormSubmission.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Routes.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Services/FormRenderer.php
0 → 100644
View file @
f90db6f4
This diff is collapsed.
Click to expand it.
app/Modules/Forms/Services/FormValidator.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Views/builder.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Views/print.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/Views/render.php
0 → 100644
View file @
f90db6f4
<?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'
])
?>
|
<strong>
الحالة:
</strong>
<?=
e
(
$submission
[
'status'
])
?>
|
<strong>
التاريخ:
</strong>
<?=
e
(
substr
(
$submission
[
'created_at'
],
0
,
10
))
?>
<?php
if
(
$submission
[
'employee_name'
])
:
?>
|
<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
app/Modules/Forms/Views/submissions.php
0 → 100644
View file @
f90db6f4
<?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
app/Modules/Forms/bootstrap.php
0 → 100644
View file @
f90db6f4
<?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
database/migrations/Phase_06_001_create_form_field_types_table.php
0 → 100644
View file @
f90db6f4
<?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
database/migrations/Phase_06_002_create_form_schemas_table.php
0 → 100644
View file @
f90db6f4
<?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
database/migrations/Phase_06_003_create_form_submissions_table.php
0 → 100644
View file @
f90db6f4
<?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
database/seeds/Phase_06_001_seed_field_types.php
0 → 100644
View file @
f90db6f4
<?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
database/seeds/Phase_06_002_seed_form_schemas.php
0 → 100644
View file @
f90db6f4
This diff is collapsed.
Click to expand it.
public/assets/js/forms-engine.js
0 → 100644
View file @
f90db6f4
/**
* 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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment