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
d51f23be
Commit
d51f23be
authored
May 23, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
clot
parent
4a2f8851
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
215 additions
and
40 deletions
+215
-40
TutorialController.php
app/Modules/Tutorials/Controllers/TutorialController.php
+63
-11
Routes.php
app/Modules/Tutorials/Routes.php
+3
-1
book.php
app/Modules/Tutorials/Views/book.php
+63
-25
cli.php
cli.php
+79
-0
000-default.conf
docker/000-default.conf
+4
-0
php.ini
docker/php.ini
+3
-3
No files found.
app/Modules/Tutorials/Controllers/TutorialController.php
View file @
d51f23be
...
@@ -7,7 +7,6 @@ use App\Core\Controller;
...
@@ -7,7 +7,6 @@ use App\Core\Controller;
use
App\Core\Request
;
use
App\Core\Request
;
use
App\Core\Response
;
use
App\Core\Response
;
use
App\Modules\Tutorials\TutorialRegistry
;
use
App\Modules\Tutorials\TutorialRegistry
;
use
App\Shared\Services\PdfExportService
;
class
TutorialController
extends
Controller
class
TutorialController
extends
Controller
{
{
...
@@ -557,22 +556,75 @@ class TutorialController extends Controller
...
@@ -557,22 +556,75 @@ class TutorialController extends Controller
]);
]);
}
}
public
function
export
Pdf
(
Request
$request
)
:
Response
public
function
export
Start
(
Request
$request
)
:
Response
{
{
$screenshotsPath
=
realpath
(
__DIR__
.
'/../../../../public/assets/tutorials/screenshots'
);
$storageDir
=
__DIR__
.
'/../../../../storage/cache'
;
$viewsPath
=
realpath
(
__DIR__
.
'/../Views'
);
$lockFile
=
$storageDir
.
'/book-export.lock'
;
$pdfFile
=
$storageDir
.
'/book-export.pdf'
;
$bookData
=
$this
->
collectBookData
(
$screenshotsPath
,
$viewsPath
);
if
(
file_exists
(
$lockFile
)
&&
(
time
()
-
filemtime
(
$lockFile
))
<
300
)
{
return
$this
->
json
([
'status'
=>
'processing'
]);
}
file_put_contents
(
$lockFile
,
'started'
);
@
unlink
(
$pdfFile
);
$phpBinary
=
PHP_BINARY
;
$scriptPath
=
realpath
(
__DIR__
.
'/../../../../cli.php'
);
$cmd
=
sprintf
(
'%s %s export:book > /dev/null 2>&1 &'
,
escapeshellarg
(
$phpBinary
),
escapeshellarg
(
$scriptPath
)
);
exec
(
$cmd
);
return
$this
->
json
([
'status'
=>
'started'
]);
}
ob_start
();
public
function
exportStatus
(
Request
$request
)
:
Response
$data
=
$bookData
;
{
include
__DIR__
.
'/../Views/export_pdf.php'
;
$storageDir
=
__DIR__
.
'/../../../../storage/cache'
;
$html
=
ob_get_clean
();
$lockFile
=
$storageDir
.
'/book-export.lock'
;
$pdfFile
=
$storageDir
.
'/book-export.pdf'
;
$errorFile
=
$storageDir
.
'/book-export.error'
;
if
(
file_exists
(
$errorFile
))
{
$error
=
file_get_contents
(
$errorFile
);
@
unlink
(
$errorFile
);
@
unlink
(
$lockFile
);
return
$this
->
json
([
'status'
=>
'error'
,
'message'
=>
$error
]);
}
if
(
file_exists
(
$pdfFile
))
{
@
unlink
(
$lockFile
);
return
$this
->
json
([
'status'
=>
'done'
,
'size'
=>
filesize
(
$pdfFile
)]);
}
return
PdfExportService
::
renderToPdf
(
$html
,
'Book-of-the-ERP.pdf'
);
if
(
file_exists
(
$lockFile
))
{
return
$this
->
json
([
'status'
=>
'processing'
]);
}
return
$this
->
json
([
'status'
=>
'idle'
]);
}
public
function
exportDownload
(
Request
$request
)
:
Response
{
$pdfFile
=
realpath
(
__DIR__
.
'/../../../../storage/cache'
)
.
'/book-export.pdf'
;
if
(
!
file_exists
(
$pdfFile
))
{
return
$this
->
json
([
'error'
=>
'File not found'
],
404
);
}
$content
=
file_get_contents
(
$pdfFile
);
$response
=
new
Response
();
return
$response
->
html
(
$content
,
200
,
[
'Content-Type'
=>
'application/pdf'
,
'Content-Disposition'
=>
'attachment; filename="Book-of-the-ERP.pdf"'
,
'Content-Length'
=>
(
string
)
strlen
(
$content
),
]);
}
}
p
rivate
function
collectBookData
(
string
$screenshotsPath
,
string
$viewsPath
)
:
array
p
ublic
function
collectBookData
(
string
$screenshotsPath
,
string
$viewsPath
)
:
array
{
{
$book
=
[
$book
=
[
'generatedAt'
=>
date
(
'Y-m-d H:i'
),
'generatedAt'
=>
date
(
'Y-m-d H:i'
),
...
...
app/Modules/Tutorials/Routes.php
View file @
d51f23be
...
@@ -4,7 +4,9 @@ declare(strict_types=1);
...
@@ -4,7 +4,9 @@ declare(strict_types=1);
return
[
return
[
[
'GET'
,
'/tutorials'
,
'Tutorials\Controllers\TutorialController@index'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials'
,
'Tutorials\Controllers\TutorialController@index'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/book'
,
'Tutorials\Controllers\TutorialController@bookPage'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/book'
,
'Tutorials\Controllers\TutorialController@bookPage'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/export-pdf'
,
'Tutorials\Controllers\TutorialController@exportPdf'
,
[
'auth'
],
'tutorials.view'
],
[
'POST'
,
'/tutorials/export-start'
,
'Tutorials\Controllers\TutorialController@exportStart'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/export-status'
,
'Tutorials\Controllers\TutorialController@exportStatus'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/export-download'
,
'Tutorials\Controllers\TutorialController@exportDownload'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/sports-activity'
,
'Tutorials\Controllers\TutorialController@sportsActivity'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/sports-activity'
,
'Tutorials\Controllers\TutorialController@sportsActivity'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/sports-activity/{slug}'
,
'Tutorials\Controllers\TutorialController@show'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/sports-activity/{slug}'
,
'Tutorials\Controllers\TutorialController@show'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/membership'
,
'Tutorials\Controllers\TutorialController@membership'
,
[
'auth'
],
'tutorials.view'
],
[
'GET'
,
'/tutorials/membership'
,
'Tutorials\Controllers\TutorialController@membership'
,
[
'auth'
],
'tutorials.view'
],
...
...
app/Modules/Tutorials/Views/book.php
View file @
d51f23be
...
@@ -126,15 +126,17 @@
...
@@ -126,15 +126,17 @@
<script>
<script>
var
exportSteps
=
[
var
exportSteps
=
[
{
pct
:
5
,
label
:
'تحضير البيانات...'
},
{
pct
:
10
,
label
:
'تحضير البيانات...'
},
{
pct
:
1
5
,
label
:
'تجميع شروحات العضوية...'
},
{
pct
:
2
5
,
label
:
'تجميع شروحات العضوية...'
},
{
pct
:
3
0
,
label
:
'تجميع شروحات الأنشطة الرياضية...'
},
{
pct
:
4
0
,
label
:
'تجميع شروحات الأنشطة الرياضية...'
},
{
pct
:
4
5
,
label
:
'تجميع شروحات الخزنة...'
},
{
pct
:
5
5
,
label
:
'تجميع شروحات الخزنة...'
},
{
pct
:
5
5
,
label
:
'تجميع باقي الأقسام...'
},
{
pct
:
6
5
,
label
:
'تجميع باقي الأقسام...'
},
{
pct
:
7
0
,
label
:
'تضمين لقطات الشاشة...'
},
{
pct
:
7
5
,
label
:
'تضمين لقطات الشاشة...'
},
{
pct
:
85
,
label
:
'توليد ملف PDF...'
},
{
pct
:
85
,
label
:
'توليد ملف PDF...'
},
{
pct
:
9
5
,
label
:
'تجهيز التحميل...'
},
{
pct
:
9
2
,
label
:
'تجهيز التحميل...'
},
];
];
var
pollTimer
=
null
;
var
stepTimer
=
null
;
function
startExport
()
{
function
startExport
()
{
document
.
getElementById
(
'beforeExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'beforeExport'
).
style
.
display
=
'none'
;
...
@@ -155,45 +157,81 @@ function startExport() {
...
@@ -155,45 +157,81 @@ function startExport() {
progressStep
.
textContent
=
step
.
label
;
progressStep
.
textContent
=
step
.
label
;
stepIndex
++
;
stepIndex
++
;
if
(
stepIndex
<
exportSteps
.
length
)
{
if
(
stepIndex
<
exportSteps
.
length
)
{
s
etTimeout
(
animateStep
,
6
00
);
s
tepTimer
=
setTimeout
(
animateStep
,
40
00
);
}
}
}
}
animateStep
();
animateStep
();
fetch
(
'/tutorials/export-pdf'
)
fetch
(
'/tutorials/export-start'
,
{
method
:
'POST'
,
headers
:
{
'X-Requested-With'
:
'XMLHttpRequest'
}
})
.
then
(
function
(
r
)
{
return
r
.
json
();
})
.
then
(
function
(
data
)
{
if
(
data
.
status
===
'started'
||
data
.
status
===
'processing'
)
{
pollStatus
();
}
else
{
showError
(
'فشل بدء التصدير'
);
}
})
.
catch
(
function
(
err
)
{
showError
(
'فشل الاتصال: '
+
err
.
message
);
});
}
function
pollStatus
()
{
pollTimer
=
setInterval
(
function
()
{
fetch
(
'/tutorials/export-status'
)
.
then
(
function
(
r
)
{
return
r
.
json
();
})
.
then
(
function
(
data
)
{
if
(
data
.
status
===
'done'
)
{
clearInterval
(
pollTimer
);
clearTimeout
(
stepTimer
);
document
.
getElementById
(
'progressBar'
).
style
.
width
=
'100%'
;
document
.
getElementById
(
'progressLabel'
).
textContent
=
'100%'
;
document
.
getElementById
(
'progressStep'
).
textContent
=
'اكتمل! جاري التحميل...'
;
downloadFile
();
}
else
if
(
data
.
status
===
'error'
)
{
clearInterval
(
pollTimer
);
clearTimeout
(
stepTimer
);
showError
(
data
.
message
||
'فشل التصدير'
);
}
})
.
catch
(
function
()
{});
},
3000
);
}
function
downloadFile
()
{
fetch
(
'/tutorials/export-download'
)
.
then
(
function
(
response
)
{
.
then
(
function
(
response
)
{
if
(
!
response
.
ok
)
throw
new
Error
(
'HTTP '
+
response
.
status
);
if
(
!
response
.
ok
)
throw
new
Error
(
'HTTP '
+
response
.
status
);
var
contentType
=
response
.
headers
.
get
(
'Content-Type'
)
||
''
;
return
response
.
blob
();
return
response
.
blob
().
then
(
function
(
blob
)
{
return
{
blob
:
blob
,
contentType
:
contentType
};
});
})
})
.
then
(
function
(
result
)
{
.
then
(
function
(
blob
)
{
progressBar
.
style
.
width
=
'100%'
;
var
url
=
window
.
URL
.
createObjectURL
(
blob
);
progressLabel
.
textContent
=
'100%'
;
progressStep
.
textContent
=
'اكتمل!'
;
var
ext
=
result
.
contentType
.
indexOf
(
'pdf'
)
!==
-
1
?
'.pdf'
:
'.html'
;
var
filename
=
'Book-of-the-ERP'
+
ext
;
var
url
=
window
.
URL
.
createObjectURL
(
result
.
blob
);
var
a
=
document
.
createElement
(
'a'
);
var
a
=
document
.
createElement
(
'a'
);
a
.
href
=
url
;
a
.
href
=
url
;
a
.
download
=
filename
;
a
.
download
=
'Book-of-the-ERP.pdf'
;
document
.
body
.
appendChild
(
a
);
document
.
body
.
appendChild
(
a
);
a
.
click
();
a
.
click
();
setTimeout
(
function
()
{
setTimeout
(
function
()
{
window
.
URL
.
revokeObjectURL
(
url
);
window
.
URL
.
revokeObjectURL
(
url
);
document
.
body
.
removeChild
(
a
);
document
.
body
.
removeChild
(
a
);
},
100
);
},
100
);
setTimeout
(
function
()
{
finishExport
(
true
);
},
600
);
setTimeout
(
function
()
{
finishExport
(
true
);
},
600
);
})
})
.
catch
(
function
(
err
)
{
.
catch
(
function
(
err
)
{
document
.
getElementById
(
'errorMessage'
).
textContent
=
'فشل التصدير: '
+
err
.
message
;
showError
(
'فشل التحميل: '
+
err
.
message
);
finishExport
(
false
);
});
});
}
}
function
showError
(
msg
)
{
clearInterval
(
pollTimer
);
clearTimeout
(
stepTimer
);
document
.
getElementById
(
'errorMessage'
).
textContent
=
msg
;
finishExport
(
false
);
}
function
finishExport
(
success
)
{
function
finishExport
(
success
)
{
document
.
getElementById
(
'duringExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'duringExport'
).
style
.
display
=
'none'
;
if
(
success
)
{
if
(
success
)
{
...
...
cli.php
View file @
d51f23be
...
@@ -223,6 +223,84 @@ switch ($command) {
...
@@ -223,6 +223,84 @@ switch ($command) {
}
}
break
;
break
;
case
'export:book'
:
echo
"📖 Generating Book of the ERP PDF...
\n
"
;
$storageDir
=
__DIR__
.
'/storage/cache'
;
$lockFile
=
$storageDir
.
'/book-export.lock'
;
$pdfFile
=
$storageDir
.
'/book-export.pdf'
;
$errorFile
=
$storageDir
.
'/book-export.error'
;
@
unlink
(
$errorFile
);
file_put_contents
(
$lockFile
,
'processing'
);
try
{
$screenshotsPath
=
realpath
(
__DIR__
.
'/public/assets/tutorials/screenshots'
);
$viewsPath
=
realpath
(
__DIR__
.
'/app/Modules/Tutorials/Views'
);
$app
=
\App\Core\App
::
getInstance
();
$app
->
setDb
(
$db
);
$controller
=
new
\App\Modules\Tutorials\Controllers\TutorialController
();
$bookData
=
$controller
->
collectBookData
(
$screenshotsPath
,
$viewsPath
);
ob_start
();
$data
=
$bookData
;
include
__DIR__
.
'/app/Modules/Tutorials/Views/export_pdf.php'
;
$html
=
ob_get_clean
();
echo
" HTML generated ("
.
strlen
(
$html
)
.
" bytes)
\n
"
;
echo
" Running wkhtmltopdf...
\n
"
;
$wkhtmltopdf
=
null
;
foreach
([
'/usr/local/bin/wkhtmltopdf'
,
'/usr/bin/wkhtmltopdf'
]
as
$path
)
{
if
(
file_exists
(
$path
)
&&
is_executable
(
$path
))
{
$wkhtmltopdf
=
$path
;
break
;
}
}
if
(
!
$wkhtmltopdf
)
{
$wkhtmltopdf
=
trim
((
string
)
shell_exec
(
'which wkhtmltopdf 2>/dev/null'
));
}
if
(
!
$wkhtmltopdf
)
{
file_put_contents
(
$pdfFile
,
$html
);
echo
" ⚠️ wkhtmltopdf not found, saved as HTML
\n
"
;
}
else
{
$tmpInput
=
tempnam
(
sys_get_temp_dir
(),
'pdf_in_'
)
.
'.html'
;
file_put_contents
(
$tmpInput
,
$html
);
$cmd
=
escapeshellarg
(
$wkhtmltopdf
)
.
' --encoding utf-8 --page-size A4'
.
' --margin-top 15 --margin-bottom 15 --margin-left 12 --margin-right 12'
.
' --enable-local-file-access'
.
' --no-stop-slow-scripts'
.
' --javascript-delay 200'
.
' --image-quality 85'
.
' --print-media-type'
.
' '
.
escapeshellarg
(
$tmpInput
)
.
' '
.
escapeshellarg
(
$pdfFile
)
.
' 2>&1'
;
$output
=
[];
exec
(
$cmd
,
$output
,
$returnCode
);
@
unlink
(
$tmpInput
);
if
((
$returnCode
===
0
||
$returnCode
===
1
)
&&
file_exists
(
$pdfFile
)
&&
filesize
(
$pdfFile
)
>
0
)
{
echo
" ✅ PDF generated: "
.
round
(
filesize
(
$pdfFile
)
/
1024
/
1024
,
2
)
.
" MB
\n
"
;
}
else
{
$errMsg
=
implode
(
"
\n
"
,
$output
);
file_put_contents
(
$errorFile
,
$errMsg
?:
"wkhtmltopdf failed (code:
{
$returnCode
}
)"
);
@
unlink
(
$pdfFile
);
echo
" ❌ wkhtmltopdf failed:
{
$errMsg
}
\n
"
;
}
}
}
catch
(
\Throwable
$e
)
{
file_put_contents
(
$errorFile
,
$e
->
getMessage
());
echo
" ❌ Error: "
.
$e
->
getMessage
()
.
"
\n
"
;
}
@
unlink
(
$lockFile
);
break
;
case
'help'
:
case
'help'
:
default
:
default
:
echo
"THE CLUB ERP — CLI Commands
\n
"
;
echo
"THE CLUB ERP — CLI Commands
\n
"
;
...
@@ -234,6 +312,7 @@ switch ($command) {
...
@@ -234,6 +312,7 @@ switch ($command) {
echo
" php cli.php seed:run <Name> Run specific seed
\n
"
;
echo
" php cli.php seed:run <Name> Run specific seed
\n
"
;
echo
" php cli.php cron Run background jobs
\n
"
;
echo
" php cli.php cron Run background jobs
\n
"
;
echo
" php cli.php routes List all routes
\n
"
;
echo
" php cli.php routes List all routes
\n
"
;
echo
" php cli.php export:book Generate Book of the ERP PDF
\n
"
;
echo
" php cli.php permissions:orphans Registered but unused permissions
\n
"
;
echo
" php cli.php permissions:orphans Registered but unused permissions
\n
"
;
echo
" php cli.php permissions:unprotected Routes without permissions
\n
"
;
echo
" php cli.php permissions:unprotected Routes without permissions
\n
"
;
echo
" php cli.php permissions:stats Permission count per module
\n
"
;
echo
" php cli.php permissions:stats Permission count per module
\n
"
;
...
...
docker/000-default.conf
View file @
d51f23be
...
@@ -2,6 +2,10 @@
...
@@ -2,6 +2,10 @@
ServerAdmin
webmaster
@
localhost
ServerAdmin
webmaster
@
localhost
DocumentRoot
/
var
/
www
/
html
/
public
DocumentRoot
/
var
/
www
/
html
/
public
# Allow long-running exports (PDF generation)
TimeOut
480
ProxyTimeout
480
<
Directory
/
var
/
www
/
html
/
public
>
<
Directory
/
var
/
www
/
html
/
public
>
Options
-
Indexes
+
FollowSymLinks
Options
-
Indexes
+
FollowSymLinks
AllowOverride
All
AllowOverride
All
...
...
docker/php.ini
View file @
d51f23be
...
@@ -2,9 +2,9 @@
...
@@ -2,9 +2,9 @@
upload_max_filesize
=
20M
upload_max_filesize
=
20M
post_max_size
=
25M
post_max_size
=
25M
memory_limit
=
256
M
memory_limit
=
1024
M
max_execution_time
=
12
0
max_execution_time
=
48
0
max_input_time
=
6
0
max_input_time
=
24
0
max_input_vars
=
5000
max_input_vars
=
5000
date.timezone
=
Africa/Cairo
date.timezone
=
Africa/Cairo
...
...
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