feat: certificate template management and generation (Fasa 7)
- CertificateService: Intervention Image v3 text overlay on template - GenerateCertificateJob: queued generation with retry logic - SendCertificateEmailJob: stub (implemented in Fasa 8) - CertificateTemplateController: upload, config editor, preview, test generate - Admin/CertificateController: list, generate-all, email-all - Public/CertificateController: show with questionnaire gate, download - DejaVuSans fonts bundled under resources/fonts - Views: admin template/certificate management, public certificate download Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,86 @@
|
||||
namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Certificate;
|
||||
use App\Models\QuestionnaireResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CertificateController extends Controller
|
||||
{
|
||||
//
|
||||
public function show(string $cert_token): View|RedirectResponse
|
||||
{
|
||||
$certificate = Certificate::where('token', $cert_token)
|
||||
->with(['participant', 'program', 'template'])
|
||||
->firstOrFail();
|
||||
|
||||
$program = $certificate->program;
|
||||
$participant = $certificate->participant;
|
||||
|
||||
// Check questionnaire gate
|
||||
$pq = $program->questionnaire()->first();
|
||||
$needsQuestionnaire = $pq && $pq->is_confirmed;
|
||||
$hasAnswered = false;
|
||||
|
||||
if ($needsQuestionnaire) {
|
||||
$hasAnswered = QuestionnaireResponse::where('program_id', $program->id)
|
||||
->where('participant_id', $participant->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
// Find active QR code for questionnaire redirect
|
||||
$qrCode = $program->qrCode;
|
||||
|
||||
return view('public.certificate.show', compact(
|
||||
'certificate', 'program', 'participant', 'pq',
|
||||
'needsQuestionnaire', 'hasAnswered', 'qrCode'
|
||||
));
|
||||
}
|
||||
|
||||
public function download(string $cert_token): Response|RedirectResponse
|
||||
{
|
||||
$certificate = Certificate::where('token', $cert_token)
|
||||
->with(['participant', 'program'])
|
||||
->firstOrFail();
|
||||
|
||||
if (! $certificate->isGenerated()) {
|
||||
return back()->with('error', 'Sijil belum sedia untuk dimuat turun.');
|
||||
}
|
||||
|
||||
if (! $certificate->file_path || ! Storage::disk('local')->exists($certificate->file_path)) {
|
||||
return back()->with('error', 'Fail sijil tidak dijumpai. Sila hubungi penganjur.');
|
||||
}
|
||||
|
||||
// Check questionnaire gate
|
||||
$program = $certificate->program;
|
||||
$pq = $program->questionnaire()->first();
|
||||
|
||||
if ($pq && $pq->is_confirmed) {
|
||||
$hasAnswered = QuestionnaireResponse::where('program_id', $program->id)
|
||||
->where('participant_id', $certificate->participant_id)
|
||||
->exists();
|
||||
|
||||
if (! $hasAnswered) {
|
||||
$qrCode = $program->qrCode;
|
||||
$redirectTo = $qrCode
|
||||
? route('public.questionnaire.show', [$qrCode->token, $certificate->participant->uuid])
|
||||
: back();
|
||||
return redirect($redirectTo)->with('info', 'Sila jawab borang penilaian terlebih dahulu.');
|
||||
}
|
||||
}
|
||||
|
||||
$certificate->recordDownload();
|
||||
|
||||
$content = Storage::disk('local')->get($certificate->file_path);
|
||||
$filename = 'Sijil-' . str($certificate->participant->name)->slug() . '.jpg';
|
||||
|
||||
return response($content, 200, [
|
||||
'Content-Type' => 'image/jpeg',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
'Content-Length' => strlen($content),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user