Files
eCert-MBIP/resources/views/admin/programs/template/show.blade.php
Saufi f39eca4b1c feat: input field saiz font No IC dalam konfigurasi template
- Tambah fields[name][ic_font_size] dalam form — baris: Warna | Saiz Font No IC | Align
- Default: 70% daripada saiz font nama (sebelum ini hardcode 50%)
- loadPreview() hantar ic_font_size terkini ke endpoint pratonton
- writeIcBelow() baca ic_font_size dari config, fallback 70% jika tiada
- Validasi updateConfig: ic_font_size nullable|integer|min:8|max:200

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:18:18 +08:00

419 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.admin')
@section('title', 'Template Sijil — ' . $program->title)
@section('header', 'Template Sijil')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Template Sijil</li>
@endsection
@section('header-actions')
<a href="{{ route('admin.programs.show', $program) }}#tab-template" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Kembali
</a>
@endsection
@section('content')
@php
$config = $template?->config_json ?? [];
$fields = $config['fields'] ?? [];
$imgWidth = $config['width'] ?? 0;
$imgHeight = $config['height'] ?? 0;
$isPortrait = $imgHeight > $imgWidth && $imgWidth > 0;
@endphp
{{-- ── Panduan Template (atas, boleh lipat) ────────────────────────────── --}}
<div class="card border-0 bg-light mb-4">
<div class="card-header bg-transparent border-0 py-2 d-flex justify-content-between align-items-center"
role="button" data-bs-toggle="collapse" data-bs-target="#guidePanel" aria-expanded="false">
<span class="fw-semibold small"><i class="bi bi-info-circle me-2 text-primary"></i>Panduan Template</span>
<i class="bi bi-chevron-down small text-muted" id="guideChevron"></i>
</div>
<div class="collapse" id="guidePanel">
<div class="card-body pt-0 pb-3">
<div class="row g-3">
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-aspect-ratio text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Resolusi disyorkan <strong>1754 × 1240px</strong> (A4 landscape 150dpi)
atau <strong>1240 × 1754px</strong> (portrait).
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-cursor text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Koordinat <strong>(0, 0)</strong> adalah sudut kiri atas imej.
X bertambah ke kanan, Y bertambah ke bawah.
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-eye text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Guna butang <strong>Pratonton</strong> untuk semak kedudukan teks
sebelum jana sijil sebenar.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if($template)
{{-- ── Template Aktif (kiri) + Konfigurasi (kanan) ─────────────────────── --}}
<div class="row g-4">
{{-- Kiri: Template Aktif --}}
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<h6 class="mb-0 fw-semibold"><i class="bi bi-image me-2 text-primary"></i>Template Aktif</h6>
<span id="orientationBadge" class="badge bg-secondary" style="font-size:.7rem;">
{{ $isPortrait ? 'Portrait' : ($imgWidth > 0 ? 'Landscape' : '—') }}
</span>
</div>
<form method="POST" action="{{ route('admin.programs.template.destroy', $program) }}"
onsubmit="return confirm('Padam template sijil ini? Tindakan ini tidak boleh diundur.')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash me-1"></i> Padam
</button>
</form>
</div>
<div class="card-body d-flex flex-column">
{{-- Image viewer tinggi berubah ikut orientasi --}}
<div id="previewWrapper"
class="border rounded overflow-hidden mb-2 d-flex align-items-center justify-content-center bg-light"
style="max-height:{{ $isPortrait ? '520px' : '340px' }}; transition: max-height .3s ease;">
<img src="{{ route('admin.programs.template.preview', $program) }}"
id="templatePreview"
alt="Template Preview"
class="img-fluid"
style="max-width:100%; max-height:{{ $isPortrait ? '520px' : '340px' }}; object-fit:contain;">
</div>
<div class="text-muted small text-center mb-3">
{{ $template->original_filename }}
@if($imgWidth > 0)
&nbsp;·&nbsp;<span id="dimensionLabel">{{ $imgWidth }} × {{ $imgHeight }} px</span>
@endif
</div>
{{-- Jana Pratonton --}}
<div class="border rounded p-3 bg-light mt-auto">
<label class="form-label small fw-medium mb-2">Jana Pratonton</label>
<div class="row g-2">
<div class="col-8">
<input type="text" id="sampleName" class="form-control form-control-sm"
value="NAMA PESERTA CONTOH" placeholder="Nama untuk pratonton">
</div>
<div class="col-4">
<button type="button" id="previewBtn" class="btn btn-sm btn-primary w-100"
onclick="loadPreview()">
<i class="bi bi-eye me-1"></i> Pratonton
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- Kanan: Konfigurasi Kedudukan Teks --}}
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-sliders me-2 text-warning"></i>Konfigurasi Kedudukan Teks</h6>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Koordinat X dan Y dikira dari sudut kiri atas imej (piksel).
@if($imgWidth > 0)
Saiz imej: <strong>{{ $imgWidth }} × {{ $imgHeight }} px</strong>.
@endif
</p>
<form method="POST" action="{{ route('admin.programs.template.config', $program) }}">
@csrf @method('PUT')
{{-- Nama Peserta --}}
<div class="card border mb-3">
<div class="card-header py-2 bg-light">
<span class="fw-medium small">Nama Peserta</span>
</div>
<div class="card-body py-3">
<div class="row g-2">
<div class="col-4">
<label class="form-label small">X (px)</label>
<input type="number" name="fields[name][x]" class="form-control form-control-sm"
value="{{ $fields['name']['x'] ?? 800 }}">
</div>
<div class="col-4">
<label class="form-label small">Y (px)</label>
<input type="number" name="fields[name][y]" class="form-control form-control-sm"
value="{{ $fields['name']['y'] ?? 400 }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font</label>
<input type="number" name="fields[name][font_size]" class="form-control form-control-sm"
value="{{ $fields['name']['font_size'] ?? 52 }}">
</div>
<div class="col-4">
<label class="form-label small">Warna</label>
<input type="color" name="fields[name][font_color]"
class="form-control form-control-color form-control-sm"
value="{{ $fields['name']['font_color'] ?? '#1a3a6b' }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font No IC</label>
<input type="number" name="fields[name][ic_font_size]"
class="form-control form-control-sm" min="8" max="200"
value="{{ $fields['name']['ic_font_size'] ?? (int) round(($fields['name']['font_size'] ?? 52) * 0.7) }}">
</div>
<div class="col-4">
<label class="form-label small">Align</label>
<select name="fields[name][align]" class="form-select form-select-sm">
<option value="left" {{ ($fields['name']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
<option value="center" {{ ($fields['name']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
<option value="right" {{ ($fields['name']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
</select>
</div>
</div>
</div>
</div>
{{-- No. Sijil (pilihan) --}}
<div class="card border mb-4">
<div class="card-header py-2 bg-light d-flex justify-content-between align-items-center">
<span class="fw-medium small">No. Sijil <span class="text-muted">(Pilihan)</span></span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="showCertNo"
onchange="toggleCertNo(this)"
{{ isset($fields['certificate_no']) ? 'checked' : '' }}>
<label class="form-check-label small" for="showCertNo">Papar</label>
</div>
</div>
<div class="card-body py-3" id="certNoFields"
{{ isset($fields['certificate_no']) ? '' : 'style=display:none' }}>
<div class="row g-2">
<div class="col-4">
<label class="form-label small">X (px)</label>
<input type="number" name="fields[certificate_no][x]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['x'] ?? 800 }}">
</div>
<div class="col-4">
<label class="form-label small">Y (px)</label>
<input type="number" name="fields[certificate_no][y]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['y'] ?? 460 }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font</label>
<input type="number" name="fields[certificate_no][font_size]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['font_size'] ?? 28 }}">
</div>
<div class="col-4">
<label class="form-label small">Warna</label>
<input type="color" name="fields[certificate_no][font_color]"
class="form-control form-control-color form-control-sm"
value="{{ $fields['certificate_no']['font_color'] ?? '#555555' }}">
</div>
<div class="col-4">
<label class="form-label small">Align</label>
<select name="fields[certificate_no][align]" class="form-select form-select-sm">
<option value="left" {{ ($fields['certificate_no']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
<option value="center" {{ ($fields['certificate_no']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
<option value="right" {{ ($fields['certificate_no']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
</select>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i> Simpan Konfigurasi
</button>
</form>
</div>
</div>
</div>
</div><!-- /.row -->
@else
{{-- Tiada template form upload --}}
<div class="row g-4">
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-upload me-2 text-primary"></i>Muat Naik Template Sijil</h6>
</div>
<div class="card-body">
<div class="alert alert-info small mb-4">
<i class="bi bi-lightbulb me-1"></i>
Format <strong>JPG atau PNG</strong>, maksimum <strong>10MB</strong>.
Resolusi disyorkan: <strong>1754 × 1240px</strong> (A4 landscape) atau
<strong>1240 × 1754px</strong> (portrait).
</div>
<form method="POST" action="{{ route('admin.programs.template.store', $program) }}"
enctype="multipart/form-data">
@csrf
<div class="mb-4">
<label class="form-label fw-medium">Fail Template <span class="text-danger">*</span></label>
<input type="file" name="template_image" accept="image/jpeg,image/png"
class="form-control @error('template_image') is-invalid @enderror"
onchange="previewImage(this)">
@error('template_image')<div class="invalid-feedback">{{ $message }}</div>@enderror
<div class="form-text">Format: JPG, PNG Maksimum 10MB</div>
</div>
<div id="imagePreviewBox" class="mb-4 d-none">
<label class="form-label small text-muted">Pratonton:</label>
<div id="uploadPreviewWrapper" class="border rounded overflow-hidden">
<img id="imagePreviewEl" src="" alt="preview" class="img-fluid w-100">
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload me-1"></i> Muat Naik
</button>
</form>
</div>
</div>
</div>
</div>
@endif
@endsection
@push('scripts')
<script>
// ── Auto-detect orientasi dan laras tinggi viewer ─────────────────────────────
function applyOrientation(naturalW, naturalH) {
const wrapper = document.getElementById('previewWrapper');
const img = document.getElementById('templatePreview');
const badge = document.getElementById('orientationBadge');
if (!wrapper || !img) return;
const isPortrait = naturalH > naturalW;
const maxH = isPortrait ? '520px' : '340px';
wrapper.style.maxHeight = maxH;
img.style.maxHeight = maxH;
if (badge) {
badge.textContent = isPortrait ? 'Portrait' : 'Landscape';
badge.className = 'badge ' + (isPortrait ? 'bg-info' : 'bg-success');
}
}
// Jalankan sekali apabila imej template dimuatkan
const templateImg = document.getElementById('templatePreview');
if (templateImg) {
if (templateImg.complete && templateImg.naturalWidth) {
applyOrientation(templateImg.naturalWidth, templateImg.naturalHeight);
} else {
templateImg.addEventListener('load', function () {
applyOrientation(this.naturalWidth, this.naturalHeight);
});
}
}
// ── Upload pratonton (form muat naik) ─────────────────────────────────────────
function previewImage(input) {
if (!input.files || !input.files[0]) return;
const reader = new FileReader();
reader.onload = e => {
const el = document.getElementById('imagePreviewEl');
const box = document.getElementById('imagePreviewBox');
el.src = e.target.result;
box.classList.remove('d-none');
// Laras pratonton upload ikut orientasi
el.onload = function () {
const wrap = document.getElementById('uploadPreviewWrapper');
if (wrap) wrap.style.maxHeight = this.naturalHeight > this.naturalWidth ? '520px' : '340px';
};
};
reader.readAsDataURL(input.files[0]);
}
// ── Toggle No. Sijil ──────────────────────────────────────────────────────────
function toggleCertNo(cb) {
document.getElementById('certNoFields').style.display = cb.checked ? '' : 'none';
}
// ── Toggle panduan (ikon chevron) ─────────────────────────────────────────────
document.getElementById('guidePanel')?.addEventListener('show.bs.collapse', () => {
document.getElementById('guideChevron').className = 'bi bi-chevron-up small text-muted';
});
document.getElementById('guidePanel')?.addEventListener('hide.bs.collapse', () => {
document.getElementById('guideChevron').className = 'bi bi-chevron-down small text-muted';
});
// ── Jana Pratonton ────────────────────────────────────────────────────────────
function loadPreview() {
const name = document.getElementById('sampleName').value.trim() || 'NAMA PESERTA CONTOH';
const img = document.getElementById('templatePreview');
const btn = document.getElementById('previewBtn');
const url = "{{ route('admin.programs.template.test', $program) }}";
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span> Memuatkan...';
const fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('sample_name', name);
// Hantar nilai form semasa — preview guna koordinat terkini walaupun belum simpan
const read = (sel) => document.querySelector(sel)?.value ?? '';
fd.append('fields[name][x]', read('[name="fields[name][x]"]'));
fd.append('fields[name][y]', read('[name="fields[name][y]"]'));
fd.append('fields[name][font_size]', read('[name="fields[name][font_size]"]'));
fd.append('fields[name][font_color]', read('[name="fields[name][font_color]"]'));
fd.append('fields[name][ic_font_size]', read('[name="fields[name][ic_font_size]"]'));
fd.append('fields[name][align]', read('[name="fields[name][align]"]'));
// Sertakan No. Sijil hanya jika toggle aktif
if (document.getElementById('showCertNo')?.checked) {
fd.append('fields[certificate_no][x]', read('[name="fields[certificate_no][x]"]'));
fd.append('fields[certificate_no][y]', read('[name="fields[certificate_no][y]"]'));
fd.append('fields[certificate_no][font_size]', read('[name="fields[certificate_no][font_size]"]'));
fd.append('fields[certificate_no][font_color]', read('[name="fields[certificate_no][font_color]"]'));
fd.append('fields[certificate_no][align]', read('[name="fields[certificate_no][align]"]'));
}
fetch(url, { method: 'POST', body: fd })
.then(r => {
if (!r.ok) return r.json().then(j => { throw new Error(j.error || 'Ralat pelayan (' + r.status + ')'); });
return r.blob();
})
.then(blob => {
const prevSrc = img.src;
img.src = URL.createObjectURL(blob);
if (prevSrc.startsWith('blob:')) URL.revokeObjectURL(prevSrc);
})
.catch(err => {
alert('Gagal jana pratonton: ' + err.message);
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-eye me-1"></i> Pratonton';
});
}
</script>
@endpush