add certificates
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
APP_NAME=Alrahma_API
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:RfIZKqgXC9seghl2Jqs2MgLh1X1Z7APRsnhUK8CgWx8=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=America/New_York
|
||||
APP_URL=http://localhost:8080
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
|
||||
# ----------------------------
|
||||
# DATABASE
|
||||
# ----------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql_db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=school_api
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
# ----------------------------
|
||||
# SESSION
|
||||
# ----------------------------
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# ----------------------------
|
||||
# CACHE & QUEUE
|
||||
# ----------------------------
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# ----------------------------
|
||||
# REDIS
|
||||
# ----------------------------
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ----------------------------
|
||||
# MAIL
|
||||
# ----------------------------
|
||||
MAIL_MAILER=log
|
||||
MAIL_PROFILE_DEFAULT=default
|
||||
MAIL_DEFAULT_HOST=smtp.gmail.com
|
||||
MAIL_DEFAULT_PORT=587
|
||||
MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com
|
||||
MAIL_DEFAULT_PASS="psnp emdq dykw ypul"
|
||||
MAIL_DEFAULT_ENCRYPTION=tls
|
||||
MAIL_DEFAULT_FROM_EMAIL="alrahma.sunday.school@gmail.com"
|
||||
MAIL_DEFAULT_FROM_NAME="Alrahma API"
|
||||
MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com"
|
||||
MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
|
||||
MAIL_COMMUNICATION_REPLY_TO="alrahma.isgl@gmail.com"
|
||||
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
|
||||
MAIL_PAYMENT_REPLY_TO="alrahma.isgl@gmail.com"
|
||||
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
|
||||
MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"alrahma.sunday.school@gmail.com"},"communication":{"name":"School Communications","email":"alrahma.sunday.school@gmail.com"},"payment":{"name":"School Payments","email":"alrahma.sunday.school@gmail.com"}}'
|
||||
|
||||
# Laravel mailer compatibility
|
||||
MAIL_HOST="${MAIL_DEFAULT_HOST}"
|
||||
MAIL_PORT="${MAIL_DEFAULT_PORT}"
|
||||
MAIL_USERNAME="${MAIL_DEFAULT_USER}"
|
||||
MAIL_PASSWORD="${MAIL_DEFAULT_PASS}"
|
||||
MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}"
|
||||
MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}"
|
||||
|
||||
# Legacy fallback keys still read by some services
|
||||
SMTP_HOST="${MAIL_DEFAULT_HOST}"
|
||||
SMTP_USER="${MAIL_DEFAULT_USER}"
|
||||
SMTP_PASS="${MAIL_DEFAULT_PASS}"
|
||||
SMTP_PORT="${MAIL_DEFAULT_PORT}"
|
||||
SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
|
||||
# ----------------------------
|
||||
# FILESYSTEM
|
||||
# ----------------------------
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# ----------------------------
|
||||
# AWS STORAGE
|
||||
# ----------------------------
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
# ----------------------------
|
||||
# JWT
|
||||
# ----------------------------
|
||||
JWT_SECRET=Uj8rGnYcXMgeRc5qCIn9Wn03tYo1pCsBz1Biou8T9zWtaNxBYi3P4NP5vuFiXHmd
|
||||
JWT_ALGO=HS256
|
||||
JWT_TTL=60
|
||||
JWT_REFRESH_TTL=20160
|
||||
JWT_BLACKLIST_ENABLED=true
|
||||
JWT_BLACKLIST_GRACE_PERIOD=0
|
||||
|
||||
# ----------------------------
|
||||
# DOCS
|
||||
# ----------------------------
|
||||
DOCS_CLIENT_URL=http://localhost:3000
|
||||
DOCS_INDEX_TITLE=Alrahma API Documentation
|
||||
DOCS_INDEX_DESCRIPTION=
|
||||
|
||||
# ----------------------------
|
||||
# BADGE
|
||||
# ----------------------------
|
||||
BADGE_SCHOOL_NAME=Al Rahma Sunday School
|
||||
BADGE_HEADER_SUBTITLE=
|
||||
BADGE_MOTTO=
|
||||
BADGE_MOTTO_FALLBACK=
|
||||
BADGE_ROLE_LABEL=Student
|
||||
BADGE_FOOTER_ADDRESS=5 Courthouse Lane, Chelmsford, MA 01824, USA
|
||||
|
||||
# ----------------------------
|
||||
# FRONT-END
|
||||
# ----------------------------
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -0,0 +1,133 @@
|
||||
APP_NAME=Alrahma_API
|
||||
APP_ENV=production
|
||||
APP_KEY=base64:CHANGE_ME_RUN_php_artisan_key_generate
|
||||
APP_DEBUG=false
|
||||
APP_TIMEZONE=America/New_York
|
||||
APP_URL=https://api.alrahmaisgl.org
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
LOG_CHANNEL=stderr
|
||||
LOG_LEVEL=info
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
|
||||
# ----------------------------
|
||||
# DATABASE
|
||||
# ----------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=CHANGE_ME
|
||||
DB_USERNAME=CHANGE_ME
|
||||
DB_PASSWORD=CHANGE_ME
|
||||
|
||||
# ----------------------------
|
||||
# SESSION
|
||||
# ----------------------------
|
||||
SESSION_DRIVER=cookie
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
SESSION_SECURE_COOKIE=true
|
||||
SESSION_SAME_SITE=strict
|
||||
|
||||
# ----------------------------
|
||||
# CACHE & QUEUE
|
||||
# ----------------------------
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# ----------------------------
|
||||
# REDIS
|
||||
# ----------------------------
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ----------------------------
|
||||
# MAIL
|
||||
# ----------------------------
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_PROFILE_DEFAULT=default
|
||||
MAIL_DEFAULT_HOST=CHANGE_ME
|
||||
MAIL_DEFAULT_PORT=587
|
||||
MAIL_DEFAULT_USER=CHANGE_ME
|
||||
MAIL_DEFAULT_PASS=CHANGE_ME
|
||||
MAIL_DEFAULT_ENCRYPTION=tls
|
||||
MAIL_DEFAULT_FROM_EMAIL="no-reply@alrahmaisgl.org"
|
||||
MAIL_DEFAULT_FROM_NAME="Alrahma API"
|
||||
MAIL_DEFAULT_REPLY_TO="no-reply@alrahmaisgl.org"
|
||||
MAIL_DEFAULT_REPLY_TO_NAME="Alrahma API"
|
||||
MAIL_COMMUNICATION_REPLY_TO="no-reply@alrahmaisgl.org"
|
||||
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
|
||||
MAIL_PAYMENT_REPLY_TO="no-reply@alrahmaisgl.org"
|
||||
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
|
||||
MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"no-reply@alrahmaisgl.org"},"communication":{"name":"School Communications","email":"no-reply@alrahmaisgl.org"},"payment":{"name":"School Payments","email":"no-reply@alrahmaisgl.org"}}'
|
||||
|
||||
# Laravel mailer compatibility
|
||||
MAIL_HOST="${MAIL_DEFAULT_HOST}"
|
||||
MAIL_PORT="${MAIL_DEFAULT_PORT}"
|
||||
MAIL_USERNAME="${MAIL_DEFAULT_USER}"
|
||||
MAIL_PASSWORD="${MAIL_DEFAULT_PASS}"
|
||||
MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}"
|
||||
MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}"
|
||||
|
||||
# Legacy fallback keys still read by some services
|
||||
SMTP_HOST="${MAIL_DEFAULT_HOST}"
|
||||
SMTP_USER="${MAIL_DEFAULT_USER}"
|
||||
SMTP_PASS="${MAIL_DEFAULT_PASS}"
|
||||
SMTP_PORT="${MAIL_DEFAULT_PORT}"
|
||||
SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
|
||||
# ----------------------------
|
||||
# FILESYSTEM
|
||||
# ----------------------------
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# ----------------------------
|
||||
# AWS STORAGE (optional)
|
||||
# ----------------------------
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
# ----------------------------
|
||||
# JWT
|
||||
# ----------------------------
|
||||
JWT_SECRET=CHANGE_ME_USE_STRONG_RANDOM_SECRET
|
||||
JWT_ALGO=HS256
|
||||
JWT_TTL=60
|
||||
JWT_REFRESH_TTL=20160
|
||||
JWT_BLACKLIST_ENABLED=true
|
||||
JWT_BLACKLIST_GRACE_PERIOD=0
|
||||
|
||||
# ----------------------------
|
||||
# DOCS
|
||||
# ----------------------------
|
||||
DOCS_CLIENT_URL=https://CHANGE_ME
|
||||
DOCS_INDEX_TITLE=Alrahma API Documentation
|
||||
DOCS_INDEX_DESCRIPTION=
|
||||
|
||||
# ----------------------------
|
||||
# BADGE
|
||||
# ----------------------------
|
||||
BADGE_SCHOOL_NAME=Al Rahma Sunday School
|
||||
BADGE_HEADER_SUBTITLE=
|
||||
BADGE_MOTTO=
|
||||
BADGE_MOTTO_FALLBACK=
|
||||
BADGE_ROLE_LABEL=Student
|
||||
BADGE_FOOTER_ADDRESS=5 Courthouse Lane, Chelmsford, MA 01824, USA
|
||||
|
||||
# ----------------------------
|
||||
# FRONT-END
|
||||
# ----------------------------
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
+5
-2
@@ -3,7 +3,7 @@ FROM composer:2 AS vendor
|
||||
WORKDIR /app
|
||||
COPY composer.json composer.lock ./
|
||||
COPY app/Helpers/ci_helpers.php app/Helpers/
|
||||
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts
|
||||
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --ignore-platform-req=ext-gd
|
||||
COPY . .
|
||||
RUN composer dump-autoload --optimize \
|
||||
&& php artisan package:discover --ansi
|
||||
@@ -30,7 +30,10 @@ RUN apt-get update \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
&& docker-php-ext-install pdo_mysql mbstring xml zip \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install pdo_mysql mbstring xml zip gd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
@@ -111,7 +111,13 @@ if (!function_exists('ci_password_verify')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($storedHash, ':') !== false || strpos($storedHash, '$') !== false) {
|
||||
$hashInfo = password_get_info($storedHash);
|
||||
if (($hashInfo['algo'] ?? null) !== null) {
|
||||
return Hash::check($password, $storedHash);
|
||||
}
|
||||
|
||||
$parts = preg_split('/[:$]/', $storedHash);
|
||||
if (is_array($parts) && count($parts) === 4) {
|
||||
return pbkdf2_verify($password, $storedHash);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Certificates;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Models\Configuration;
|
||||
use App\Services\Certificates\CertificatePdfService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CertificateController extends BaseApiController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/certificates/form-options
|
||||
*/
|
||||
public function formOptions(Request $request): JsonResponse
|
||||
{
|
||||
$schoolYearParam = $request->query('school_year');
|
||||
if ($schoolYearParam !== null) {
|
||||
$schoolYear = trim((string) $schoolYearParam);
|
||||
} else {
|
||||
try {
|
||||
$schoolYear = trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
} catch (\Throwable) {
|
||||
$schoolYear = '';
|
||||
}
|
||||
}
|
||||
$classSectionId = $request->query('class_section_id');
|
||||
|
||||
try {
|
||||
$classSections = DB::table('classSection')
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate formOptions class query failed: ' . $e->getMessage());
|
||||
return $this->error('Failed to load classes: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$students = [];
|
||||
if ($classSectionId) {
|
||||
try {
|
||||
$studentQuery = DB::table('student_class as sc')
|
||||
->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.class_section_id', (int) $classSectionId)
|
||||
->where('s.is_active', 1)
|
||||
->orderBy('s.lastname')
|
||||
->orderBy('s.firstname');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$studentQuery->where('sc.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$students = $studentQuery->get()->map(fn ($r) => (array) $r)->all();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate formOptions student query failed: ' . $e->getMessage());
|
||||
return $this->error('Failed to load students: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'class_sections' => $classSections,
|
||||
'students' => $students,
|
||||
'school_year' => $schoolYear,
|
||||
'selected_class_id' => $classSectionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/administrator/certificates/generate
|
||||
*/
|
||||
public function generate(Request $request): Response|JsonResponse
|
||||
{
|
||||
$studentIds = $request->input('student_ids', []);
|
||||
$certDate = trim((string) ($request->input('cert_date') ?? date('m/d/Y')));
|
||||
$classSectionId = $request->input('class_section_id');
|
||||
|
||||
if (empty($studentIds) || !is_array($studentIds)) {
|
||||
return $this->error('Please select at least one student.', 422);
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_filter(array_map('intval', $studentIds)));
|
||||
$certDate = preg_replace('/[^0-9\/\-]/', '', $certDate) ?? date('m/d/Y');
|
||||
|
||||
if (empty($studentIds)) {
|
||||
return $this->error('Invalid student selection.', 422);
|
||||
}
|
||||
|
||||
$students = [];
|
||||
foreach ($studentIds as $id) {
|
||||
$row = null;
|
||||
|
||||
if ($classSectionId) {
|
||||
$row = DB::table('student_class as sc')
|
||||
->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.class_section_id', (int) $classSectionId)
|
||||
->where('s.id', $id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
$row = DB::table('students')
|
||||
->select('id', 'firstname', 'lastname', 'registration_grade as grade')
|
||||
->where('id', $id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row) {
|
||||
$students[] = (array) $row;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($students)) {
|
||||
return $this->error('No valid students found.', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdfService = app(CertificatePdfService::class);
|
||||
$pdfContent = $pdfService->generate($students, $certDate);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate PDF generation failed: ' . $e->getMessage(), ['exception' => $e]);
|
||||
return $this->error('Failed to generate certificates.', 500);
|
||||
}
|
||||
|
||||
$filename = 'Certificates_' . date('Ymd_His') . '.pdf';
|
||||
|
||||
return response($pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
private function currentSchoolYear(): string
|
||||
{
|
||||
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Certificates;
|
||||
|
||||
class CertificatePdfService
|
||||
{
|
||||
private string $fontDir;
|
||||
private string $imgDir;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->fontDir = public_path('assets/certificates/fonts') . DIRECTORY_SEPARATOR;
|
||||
$this->imgDir = public_path('assets/certificates/images') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{firstname: string, lastname: string, grade: string}> $students
|
||||
*/
|
||||
public function generate(array $students, string $certDate): string
|
||||
{
|
||||
$edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
|
||||
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
|
||||
$pdf->SetCreator('Al Rahma Sunday School');
|
||||
$pdf->SetTitle('Student Certificates');
|
||||
$pdf->SetMargins(0, 0, 0, true);
|
||||
$pdf->SetAutoPageBreak(false, 0);
|
||||
$pdf->setPrintHeader(false);
|
||||
$pdf->setPrintFooter(false);
|
||||
$pdf->SetHeaderMargin(0);
|
||||
$pdf->SetFooterMargin(0);
|
||||
|
||||
$W = $pdf->getPageWidth();
|
||||
$H = $pdf->getPageHeight();
|
||||
|
||||
foreach ($students as $student) {
|
||||
$pdf->AddPage();
|
||||
$name = $student['firstname'] . ' ' . $student['lastname'];
|
||||
$grade = $this->formatGrade((string) ($student['grade'] ?? ''));
|
||||
$this->drawCertificate($pdf, $W, $H, $name, $grade, $certDate, $edwardianFont, $garamondBold, $ebGaramond);
|
||||
}
|
||||
|
||||
return (string) $pdf->Output('', 'S');
|
||||
}
|
||||
|
||||
private function drawCertificate(
|
||||
\TCPDF $pdf,
|
||||
float $W,
|
||||
float $H,
|
||||
string $name,
|
||||
string $grade,
|
||||
string $certDate,
|
||||
string $edwardianFont,
|
||||
string $garamondBold,
|
||||
string $ebGaramond
|
||||
): void {
|
||||
$pdf->Image($this->imgDir . 'title.png', 126, 0, 600);
|
||||
$pdf->Image($this->imgDir . 'background.png', 280, 176, 291, 340);
|
||||
$pdf->Image($this->imgDir . 'signature.png', 140, 399, 90, 90);
|
||||
|
||||
$pdf->SetFont('times', 'B', 24);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
$pdf->SetXY(0, 151);
|
||||
$pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C');
|
||||
|
||||
$pdf->SetFont($edwardianFont, '', 38);
|
||||
$pdf->SetDrawColor(0, 0, 0);
|
||||
$pdf->setTextRenderingMode(0.4, true, false);
|
||||
$pdf->SetXY(0, 219);
|
||||
$pdf->Cell($W, 38, $name, 0, 0, 'C');
|
||||
$pdf->setTextRenderingMode(0, true, false);
|
||||
|
||||
$pdf->SetFont('times', '', 20);
|
||||
$pdf->SetXY(0, 243);
|
||||
$pdf->Cell(600, 20, '___________________________________', 0, 0, 'R');
|
||||
|
||||
$pdf->SetFont($ebGaramond, '', 20);
|
||||
$pdf->SetXY(0, 273);
|
||||
$pdf->Cell($W, 20, 'for successfully completing the requirements of', 0, 0, 'C');
|
||||
|
||||
$pdf->SetFont($garamondBold, '', 20);
|
||||
$pdf->SetXY(0, 310);
|
||||
$pdf->Cell($W, 20, $grade, 0, 0, 'C');
|
||||
|
||||
$pdf->SetFont('times', '', 20);
|
||||
$pdf->SetXY(0, 343);
|
||||
$pdf->Cell($W, 20, 'at', 0, 0, 'C');
|
||||
|
||||
$pdf->SetFont($garamondBold, '', 20);
|
||||
$pdf->SetXY(0, 375);
|
||||
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
|
||||
|
||||
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 598, 453);
|
||||
|
||||
$pdf->SetFont('times', '', 20);
|
||||
$pdf->SetXY(0, 463);
|
||||
$pdf->Cell(742, 20, '_________________', 0, 0, 'R');
|
||||
$pdf->SetXY(650, 492);
|
||||
$pdf->Cell(80, 20, 'Date', 0, 0, 'C');
|
||||
|
||||
$pdf->SetXY(106, 463);
|
||||
$pdf->Cell(200, 20, '_________________', 0, 0, 'L');
|
||||
$pdf->SetXY(106, 492);
|
||||
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
|
||||
}
|
||||
|
||||
private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void
|
||||
{
|
||||
$pdf->SetFont($fontName, '', $fontSize);
|
||||
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$pdf->setAlpha(($i + 1) * 25 / 255);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
$pdf->Text($x + $i / 5, $y + $i / 5, $text);
|
||||
}
|
||||
|
||||
$pdf->setAlpha(1);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
$pdf->Text($x, $y, $text);
|
||||
}
|
||||
|
||||
private function formatGrade(string $raw): string
|
||||
{
|
||||
$clean = trim($raw);
|
||||
$lower = strtolower($clean);
|
||||
|
||||
if (preg_match('/^\d+$/', $clean) && (int) $clean >= 1 && (int) $clean <= 9) {
|
||||
return 'Grade ' . $clean;
|
||||
}
|
||||
if ($lower === 'youth') {
|
||||
return 'Youth';
|
||||
}
|
||||
if ($lower === 'kg') {
|
||||
return 'Kindergarten';
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -13,7 +13,8 @@
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"php-open-source-saver/jwt-auth": "^2.8",
|
||||
"setasign/fpdf": "*"
|
||||
"setasign/fpdf": "*",
|
||||
"tecnickcom/tcpdf": "^6.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
Generated
+79
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "661306f5e5cfc18e168ca8a287b7097a",
|
||||
"content-hash": "5b18bd0e376f1f5e5670692ccc52165c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -6673,6 +6673,83 @@
|
||||
],
|
||||
"time": "2026-02-15T10:53:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tecnickcom/tcpdf",
|
||||
"version": "6.11.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tecnickcom/TCPDF.git",
|
||||
"reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/b18f6119161019916c5bb07cb8da5205ae5c1b63",
|
||||
"reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Enables additional image handling in some workflows.",
|
||||
"ext-imagick": "Enables additional image format support when available.",
|
||||
"ext-zlib": "Recommended for compressed streams and related features.",
|
||||
"tecnickcom/tc-lib-pdf": "Modern replacement for TCPDF for new projects."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"config",
|
||||
"include",
|
||||
"tcpdf.php",
|
||||
"tcpdf_barcodes_1d.php",
|
||||
"tcpdf_barcodes_2d.php",
|
||||
"include/tcpdf_colors.php",
|
||||
"include/tcpdf_filters.php",
|
||||
"include/tcpdf_font_data.php",
|
||||
"include/tcpdf_fonts.php",
|
||||
"include/tcpdf_images.php",
|
||||
"include/tcpdf_static.php",
|
||||
"include/barcodes/datamatrix.php",
|
||||
"include/barcodes/pdf417.php",
|
||||
"include/barcodes/qrcode.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicola Asuni",
|
||||
"email": "info@tecnick.com",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Deprecated legacy PDF engine for PHP. For new projects use tecnickcom/tc-lib-pdf.",
|
||||
"homepage": "https://tcpdf.org",
|
||||
"keywords": [
|
||||
"PDFD32000-2008",
|
||||
"TCPDF",
|
||||
"barcodes",
|
||||
"datamatrix",
|
||||
"pdf",
|
||||
"pdf417",
|
||||
"qrcode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tecnickcom/TCPDF/issues",
|
||||
"source": "https://github.com/tecnickcom/TCPDF"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-21T17:00:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
"version": "v3.4.0",
|
||||
@@ -9415,5 +9492,5 @@
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: alrahma_dev_app
|
||||
env_file:
|
||||
- .env.dev
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
- /var/www/html/vendor
|
||||
networks:
|
||||
- alrahma_sunday_school_default
|
||||
|
||||
web:
|
||||
image: nginx:1.25-alpine
|
||||
container_name: alrahma_dev_web
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./:/var/www/html:ro
|
||||
- ./docker/nginx/app.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- alrahma_sunday_school_default
|
||||
|
||||
networks:
|
||||
alrahma_sunday_school_default:
|
||||
external: true
|
||||
+28
-8
@@ -49,16 +49,36 @@ REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ----------------------------
|
||||
# MAIL (logs only)
|
||||
# MAIL
|
||||
# ----------------------------
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="no-reply@alrahmaisgl.org"
|
||||
MAIL_FROM_NAME="Alrahma API"
|
||||
MAIL_PROFILE_DEFAULT=default
|
||||
MAIL_DEFAULT_HOST=127.0.0.1
|
||||
MAIL_DEFAULT_PORT=2525
|
||||
MAIL_DEFAULT_USER=
|
||||
MAIL_DEFAULT_PASS=
|
||||
MAIL_DEFAULT_ENCRYPTION=tls
|
||||
MAIL_DEFAULT_FROM_EMAIL="no-reply@alrahmaisgl.org"
|
||||
MAIL_DEFAULT_FROM_NAME="Alrahma API"
|
||||
MAIL_DEFAULT_REPLY_TO="no-reply@alrahmaisgl.org"
|
||||
MAIL_DEFAULT_REPLY_TO_NAME="Alrahma API"
|
||||
MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"no-reply@alrahmaisgl.org"},"communication":{"name":"School Communications","email":"no-reply@alrahmaisgl.org"},"payment":{"name":"School Payments","email":"no-reply@alrahmaisgl.org"}}'
|
||||
|
||||
# Laravel mailer compatibility
|
||||
MAIL_HOST="${MAIL_DEFAULT_HOST}"
|
||||
MAIL_PORT="${MAIL_DEFAULT_PORT}"
|
||||
MAIL_USERNAME="${MAIL_DEFAULT_USER}"
|
||||
MAIL_PASSWORD="${MAIL_DEFAULT_PASS}"
|
||||
MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}"
|
||||
MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}"
|
||||
|
||||
# Legacy fallback keys still read by some services
|
||||
SMTP_HOST="${MAIL_DEFAULT_HOST}"
|
||||
SMTP_USER="${MAIL_DEFAULT_USER}"
|
||||
SMTP_PASS="${MAIL_DEFAULT_PASS}"
|
||||
SMTP_PORT="${MAIL_DEFAULT_PORT}"
|
||||
SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}"
|
||||
|
||||
# ----------------------------
|
||||
# AWS STORAGE (optional)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -122,6 +122,7 @@ use App\Http\Controllers\Api\Public\PublicWinnersController;
|
||||
use App\Http\Controllers\Api\ScannerController;
|
||||
use App\Http\Controllers\Api\Staff\TimeOffNotificationController;
|
||||
use App\Http\Controllers\Api\System\AccessDeniedController;
|
||||
use App\Http\Controllers\Api\Certificates\CertificateController;
|
||||
// CodeIgniter also registers POST /api/login and POST /api/register (public, no /v1).
|
||||
Route::post('login', [AuthController::class, 'login']);
|
||||
Route::post('register', [RegisterController::class, 'store']);
|
||||
@@ -390,6 +391,11 @@ Route::prefix('v1')->group(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('certificates')->group(function () {
|
||||
Route::get('form-options', [CertificateController::class, 'formOptions']);
|
||||
Route::post('generate', [CertificateController::class, 'generate']);
|
||||
});
|
||||
|
||||
Route::prefix('role-permission')->group(function () {
|
||||
Route::get('roles', [RolePermissionController::class, 'roles']);
|
||||
Route::post('roles', [RolePermissionController::class, 'storeRole']);
|
||||
|
||||
Reference in New Issue
Block a user