certificateTemplate; return view('admin.programs.template.show', compact('program', 'template')); } public function store(Request $request, Program $program): RedirectResponse { $request->validate([ 'template_image' => 'required|image|mimes:jpg,jpeg,png|max:10240', ]); $file = $request->file('template_image'); $filename = $file->getClientOriginalName(); $path = $file->store('templates/' . $program->id, 'local'); // Deactivate previous templates $program->certificateTemplates()->update(['is_active' => false]); $program->certificateTemplates()->create([ 'original_filename' => $filename, 'image_path' => $path, 'is_active' => true, 'uploaded_by' => auth()->id(), 'config_json' => $this->defaultConfig($file->getPath() . '/' . $file->getFilename()), ]); return redirect()->route('admin.programs.template.show', $program) ->with('success', 'Template sijil berjaya dimuat naik. Konfigurasi kedudukan teks di bawah.'); } public function updateConfig(Request $request, Program $program): RedirectResponse { $template = $program->certificateTemplate; abort_if(! $template, 404); $request->validate([ 'fields' => 'required|array', 'fields.*.x' => 'required|integer|min:0', 'fields.*.y' => 'required|integer|min:0', 'fields.*.font_size' => 'required|integer|min:8|max:200', 'fields.*.font_color' => 'required|string|max:20', 'fields.*.align' => 'required|in:left,center,right', ]); $config = $template->config_json ?? []; $config['fields'] = array_merge($config['fields'] ?? [], $request->fields); $template->update(['config_json' => $config]); return redirect()->route('admin.programs.template.show', $program) ->with('success', 'Konfigurasi template berjaya dikemaskini.'); } public function destroy(Program $program): RedirectResponse { $template = $program->certificateTemplate; abort_if(! $template, 404); Storage::disk('local')->delete($template->image_path); $template->delete(); return redirect()->route('admin.programs.template.show', $program) ->with('success', 'Template sijil dipadam.'); } public function preview(Program $program): Response { $template = $program->certificateTemplate; abort_if(! $template, 404); $content = Storage::disk('local')->get($template->image_path); return response($content, 200, [ 'Content-Type' => 'image/jpeg', 'Content-Disposition' => 'inline', 'Cache-Control' => 'private, max-age=3600', ]); } public function testGenerate(Request $request, Program $program, CertificateService $service): \Illuminate\Http\Response|\Illuminate\Http\JsonResponse { $template = $program->certificateTemplate; abort_if(! $template, 404); $sampleName = $request->input('sample_name', 'NAMA PESERTA CONTOH'); $sampleNo = $request->input('sample_no', 'ECT/2025/0001'); // Bina override dari nilai form semasa (belum disimpan) // Gabung dengan config tersimpan supaya font_file & valign kekal $liveFields = null; if ($request->has('fields') && is_array($request->input('fields'))) { $saved = $template->config_json['fields'] ?? []; $liveFields = []; foreach ($request->input('fields') as $key => $cfg) { $liveFields[$key] = array_merge($saved[$key] ?? [], array_filter($cfg, fn($v) => $v !== null && $v !== '')); } } try { $imageData = $service->generatePreview($template, $sampleName, $sampleNo, $liveFields); } catch (\Throwable $e) { return response()->json(['error' => $e->getMessage()], 500); } return response($imageData, 200, [ 'Content-Type' => 'image/jpeg', 'Content-Disposition' => 'inline; filename="preview.jpg"', ]); } private function defaultConfig(string $imagePath): array { [$width, $height] = getimagesize($imagePath) + [0, 0, 0, 0]; $cx = (int) round(($width ?: 1600) / 2); $cy = (int) round(($height ?: 1100) * 0.52); return [ 'width' => $width ?: 1600, 'height' => $height ?: 1100, 'fields' => [ 'name' => [ 'x' => $cx, 'y' => $cy, 'font_size' => 52, 'font_color' => '#1a3a6b', 'font_file' => 'DejaVuSans-Bold.ttf', 'align' => 'center', 'valign' => 'top', ], ], ]; } }