525 lines
30 KiB
PHP
525 lines
30 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Manajemen Template')
|
|
|
|
@section('content')
|
|
<div class="container mx-auto py-4">
|
|
<!-- Header -->
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-xl font-bold">
|
|
@isset($kategori)
|
|
Template Kategori: {{ $kategori->nama }}
|
|
@else
|
|
Semua Template
|
|
@endisset
|
|
</h3>
|
|
|
|
@if (!isset($kategori))
|
|
<button id="openTambahModal" class="bg-blue-600 text-white px-3 py-2.5 rounded flex items-center">
|
|
<i class="bi bi-plus-lg mr-1"></i> Tambah Template
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Flash & Errors -->
|
|
@if (session('success'))
|
|
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
|
|
{{ session('success') }}
|
|
</div>
|
|
<script>
|
|
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
|
|
</script>
|
|
@endif
|
|
|
|
@if ($errors->any())
|
|
<div id="toast-error" class="mb-4 p-3 rounded bg-red-100 text-red-800 border border-red-300 shadow">
|
|
<ul class="list-disc ml-5">
|
|
@foreach ($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
<script>
|
|
setTimeout(() => document.getElementById('toast-error')?.remove(), 5000);
|
|
</script>
|
|
@endif
|
|
|
|
<!-- Tabel Template -->
|
|
<div class="bg-white rounded-lg shadow-sm">
|
|
<div class="p-4 overflow-x-auto">
|
|
<table class="w-full table-fixed text-left border border-gray-300 border-collapse">
|
|
<thead class="bg-gray-100">
|
|
<tr>
|
|
<th class="p-2 border border-gray-300 w-[50px] text-center">Nomor</th>
|
|
<th class="p-2 border border-gray-300 w-[200px] text-center">Nama Template</th>
|
|
<th class="p-2 border border-gray-300 w-[150px] text-center">Kategori</th>
|
|
<th class="p-2 border border-gray-300 w-[200px] text-center">Fitur</th>
|
|
<th class="p-2 border border-gray-300 w-[90px] text-center">Foto</th>
|
|
<th class="p-2 border border-gray-300 w-[100px] text-center">Harga</th>
|
|
<th class="p-2 border border-gray-300 w-[130px] text-center">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($templates as $key => $template)
|
|
<tr>
|
|
<td class="p-2 border border-gray-300 text-center">
|
|
{{ ($templates->currentPage() - 1) * $templates->perPage() + $key + 1 }}
|
|
</td>
|
|
<td class="p-2 border border-gray-300 truncate">{{ $template->nama_template }}</td>
|
|
<td class="p-2 border border-gray-300 truncate">{{ $template->kategori->nama ?? '-' }}</td>
|
|
<td class="p-2 border border-gray-300 align-top">
|
|
<div class="flex flex-wrap gap-1">
|
|
@forelse($template->fiturs as $f)
|
|
<span class="inline-block bg-gray-200 px-2 py-0.5 rounded text-xs">
|
|
{{ $f->deskripsi }}
|
|
</span>
|
|
@empty
|
|
<span class="text-gray-500">-</span>
|
|
@endforelse
|
|
</div>
|
|
</td>
|
|
<td class="p-2 border border-gray-300 text-center">
|
|
<div
|
|
class="w-12 h-12 overflow-hidden rounded bg-gray-100 flex items-center justify-center mx-auto">
|
|
<img src="{{ $template->foto ? asset('storage/' . $template->foto) : asset('default-image.png') }}"
|
|
alt="foto" class="max-w-full max-h-full object-contain">
|
|
</div>
|
|
</td>
|
|
<td class="p-2 border border-gray-300 text-left font-semibold">
|
|
Rp {{ number_format($template->harga, 0, ',', '.') }}
|
|
</td>
|
|
<td class="p-2 border border-gray-300 text-center">
|
|
<div class="flex justify-center space-x-2">
|
|
<button class="text-blue-600 hover:underline flex items-center openEditModalBtn"
|
|
data-id="{{ $template->id }}">
|
|
<i class="bi bi-pencil mr-1"></i> Edit
|
|
</button>
|
|
<form action="{{ route('templates.destroy', $template->id) }}" method="POST"
|
|
class="inline" onsubmit="return confirm('Hapus template ini?')">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button
|
|
class="text-red-600 hover:underline flex items-center btn-delete-template"
|
|
data-action="{{ route('templates.destroy', $template->id) }}"
|
|
data-name="{{ $template->nama_template }}">
|
|
<i class="bi bi-trash mr-1"></i> Hapus
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="p-2 text-center text-gray-500 border border-gray-300">Belum ada template</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Tambah Template -->
|
|
@if (!isset($kategori))
|
|
<div id="modalTambah" class="fixed inset-0 hidden items-center justify-center z-50">
|
|
<div class="absolute inset-0 bg-black opacity-50" id="closeTambahModal"></div>
|
|
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 flex flex-col max-h-[90vh]">
|
|
|
|
<!-- Header -->
|
|
<div class="p-4 border-b sticky top-0 bg-white z-10">
|
|
<h5 class="text-lg font-medium">Tambah Template</h5>
|
|
</div>
|
|
|
|
<!-- Scrollable content -->
|
|
<div class="p-4 space-y-4 overflow-y-auto flex-1">
|
|
<form action="{{ route('templates.store') }}" method="POST" enctype="multipart/form-data" class="h-full flex flex-col">
|
|
@csrf
|
|
<div class="flex-1 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium">Nama Template</label>
|
|
<input type="text" name="nama_template" class="w-full p-2 border rounded" required
|
|
value="{{ old('nama_template') }}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium">Kategori</label>
|
|
<select name="kategori_id" id="kategoriSelectTambah" class="w-full p-2 border rounded">
|
|
<option value="">-- Pilih Kategori --</option>
|
|
@foreach ($kategoris as $kategoriItem)
|
|
<option value="{{ $kategoriItem->id }}" @selected(old('kategori_id') == $kategoriItem->id)>
|
|
{{ $kategoriItem->nama }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- FITUR -->
|
|
<div>
|
|
<label class="block text-sm font-medium">Fitur</label>
|
|
<div id="fiturContainerTambah"
|
|
class="space-y-3 max-h-64 overflow-auto p-2 border rounded">
|
|
@foreach ($kategoriFiturs as $kategoriFitur)
|
|
<div>
|
|
<p class="font-semibold text-gray-700 mb-1">
|
|
{{ $kategoriFitur->deskripsi }}
|
|
</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@foreach ($fiturs->where('kategori_fitur_id', $kategoriFitur->id) as $fitur)
|
|
<label class="flex items-center space-x-2">
|
|
<input type="radio" name="fitur_id[{{ $kategoriFitur->id }}]"
|
|
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
|
|
@checked(old("fitur_id.$kategoriFitur->id") == $fitur->id)>
|
|
<span class="text-sm">
|
|
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
|
|
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
|
|
</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
|
|
@if ($fitursTanpaKategori->count())
|
|
<div>
|
|
<p class="font-semibold text-gray-700 mb-1">Fitur Lainnya</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@foreach ($fitursTanpaKategori as $fitur)
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox" name="fitur_id[]"
|
|
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
|
|
{{ is_array(old('fitur_id')) && in_array($fitur->id, old('fitur_id')) ? 'checked' : '' }}>
|
|
<span class="text-sm">
|
|
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
|
|
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
|
|
</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium">Harga</label>
|
|
<input type="text" name="harga" class="w-full p-2 border rounded" required
|
|
min="0" value="{{ old('harga') }}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium">Foto (opsional)</label>
|
|
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
|
|
<small class="text-gray-500">Format: JPG, PNG, GIF. Maks 5MB.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="p-4 border-t flex justify-end space-x-2 sticky bottom-0 bg-white z-10">
|
|
<button type="button" id="closeTambahBtn"
|
|
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
|
|
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Modal Edit Template -->
|
|
@foreach ($templates as $template)
|
|
<div id="modalEdit{{ $template->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
|
|
<div class="absolute inset-0 bg-black opacity-50 closeEditOverlay" data-id="{{ $template->id }}"></div>
|
|
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 flex flex-col max-h-[90vh]">
|
|
|
|
<!-- Header -->
|
|
<div class="p-4 border-b sticky top-0 bg-white z-10">
|
|
<h5 class="text-lg font-medium">Edit Template</h5>
|
|
</div>
|
|
|
|
<!-- Scrollable content -->
|
|
<div class="p-4 space-y-4 overflow-y-auto flex-1">
|
|
<form action="{{ route('templates.update', $template->id) }}" method="POST"
|
|
enctype="multipart/form-data" class="h-full flex flex-col">
|
|
@csrf @method('PUT')
|
|
<div class="flex-1 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium">Nama Template</label>
|
|
<input type="text" name="nama_template"
|
|
value="{{ old('nama_template', $template->nama_template) }}"
|
|
class="w-full p-2 border rounded" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium">Kategori</label>
|
|
<select name="kategori_id" id="kategoriSelectEdit{{ $template->id }}"
|
|
class="w-full p-2 border rounded">
|
|
<option value="">-- Pilih Kategori --</option>
|
|
@foreach ($kategoris as $kategoriItem)
|
|
<option value="{{ $kategoriItem->id }}" @selected($kategoriItem->id == old('kategori_id', $template->kategori_id))>
|
|
{{ $kategoriItem->nama }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- FITUR -->
|
|
<div>
|
|
<label class="block text-sm font-medium">Fitur</label>
|
|
<div id="fiturContainerEdit{{ $template->id }}"
|
|
class="space-y-3 max-h-64 overflow-auto p-2 border rounded">
|
|
@foreach ($kategoriFiturs as $kategoriFitur)
|
|
<div>
|
|
<p class="font-semibold text-gray-700 mb-1">
|
|
{{ $kategoriFitur->deskripsi }}
|
|
</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@foreach ($fiturs->where('kategori_fitur_id', $kategoriFitur->id) as $fiturItem)
|
|
<label class="flex items-center space-x-2">
|
|
<input type="radio" name="fitur_id[{{ $kategoriFitur->id }}]"
|
|
value="{{ $fiturItem->id }}"
|
|
data-harga="{{ $fiturItem->harga }}"
|
|
{{ old("fitur_id.$kategoriFitur->id") == $fiturItem->id || $template->fiturs->contains($fiturItem->id) ? 'checked' : '' }}>
|
|
<span class="text-sm">
|
|
{{ \Illuminate\Support\Str::limit($fiturItem->deskripsi, 80) }}
|
|
(Rp {{ number_format($fiturItem->harga, 0, ',', '.') }})
|
|
</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
|
|
@if ($fitursTanpaKategori->count())
|
|
<div>
|
|
<p class="font-semibold text-gray-700 mb-1">Fitur Lainnya</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@foreach ($fitursTanpaKategori as $fitur)
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox" name="fitur_id[]"
|
|
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
|
|
{{ (is_array(old('fitur_id')) && in_array($fitur->id, old('fitur_id'))) || $template->fiturs->contains($fitur->id) ? 'checked' : '' }}>
|
|
<span class="text-sm">
|
|
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
|
|
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
|
|
</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium">Harga</label>
|
|
<input type="text" name="harga" value="{{ old('harga', $template->harga) }}"
|
|
class="w-full p-2 border rounded" required min="0">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium">Foto (opsional)</label>
|
|
@if ($template->foto)
|
|
<div class="mb-2">
|
|
<small class="text-gray-500">Foto saat ini:</small>
|
|
<div
|
|
class="w-20 h-20 mt-1 overflow-hidden rounded bg-gray-100 flex items-center justify-center border">
|
|
<img src="{{ asset('storage/' . $template->foto) }}" alt="foto"
|
|
class="max-w-full max-h-full object-contain">
|
|
</div>
|
|
</div>
|
|
@endif
|
|
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="p-4 border-t flex justify-end space-x-2 sticky bottom-0 bg-white z-10">
|
|
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeEditBtn"
|
|
data-id="{{ $template->id }}">Batal</button>
|
|
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// =========================
|
|
// Modal Tambah
|
|
// =========================
|
|
const openTambahModal = document.getElementById('openTambahModal');
|
|
const modalTambah = document.getElementById('modalTambah');
|
|
const closeTambahBtn = document.getElementById('closeTambahBtn');
|
|
const closeTambahOverlay = document.getElementById('closeTambahModal');
|
|
|
|
if (openTambahModal) {
|
|
openTambahModal.addEventListener('click', () => {
|
|
modalTambah.classList.remove('hidden');
|
|
modalTambah.classList.add('flex');
|
|
});
|
|
}
|
|
if (closeTambahBtn) {
|
|
closeTambahBtn.addEventListener('click', () => {
|
|
modalTambah.classList.add('hidden');
|
|
modalTambah.classList.remove('flex');
|
|
});
|
|
}
|
|
if (closeTambahOverlay) {
|
|
closeTambahOverlay.addEventListener('click', () => {
|
|
modalTambah.classList.add('hidden');
|
|
modalTambah.classList.remove('flex');
|
|
});
|
|
}
|
|
|
|
// =========================
|
|
// Modal Edit
|
|
// =========================
|
|
const openEditBtns = document.querySelectorAll('.openEditModalBtn');
|
|
const closeEditBtns = document.querySelectorAll('.closeEditBtn');
|
|
const closeEditOverlays = document.querySelectorAll('.closeEditOverlay');
|
|
|
|
openEditBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.dataset.id;
|
|
const modal = document.getElementById('modalEdit' + id);
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
});
|
|
});
|
|
|
|
closeEditBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.dataset.id;
|
|
const modal = document.getElementById('modalEdit' + id);
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
});
|
|
});
|
|
|
|
closeEditOverlays.forEach(overlay => {
|
|
overlay.addEventListener('click', () => {
|
|
const id = overlay.dataset.id;
|
|
const modal = document.getElementById('modalEdit' + id);
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
});
|
|
});
|
|
|
|
// =========================
|
|
// Modal Delete Template
|
|
// =========================
|
|
const deleteTemplateModal = document.getElementById('modalDeleteTemplate');
|
|
const deleteTemplateOverlay = document.getElementById('modalDeleteTemplateOverlay');
|
|
const closeDeleteTemplateModal = document.getElementById('closeDeleteTemplateModal');
|
|
const deleteTemplateForm = document.getElementById('deleteTemplateForm');
|
|
const deleteTemplateName = document.getElementById('deleteTemplateName');
|
|
|
|
document.querySelectorAll('.btn-delete-template').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault(); // cegah form langsung submit
|
|
deleteTemplateForm.action = btn.dataset.action;
|
|
deleteTemplateName.textContent = btn.dataset.name || 'template ini';
|
|
deleteTemplateModal.classList.remove('hidden');
|
|
deleteTemplateModal.classList.add('flex');
|
|
});
|
|
});
|
|
|
|
const closeDeleteModalTemplate = () => {
|
|
deleteTemplateModal.classList.add('hidden');
|
|
deleteTemplateModal.classList.remove('flex');
|
|
};
|
|
|
|
if (closeDeleteTemplateModal) {
|
|
closeDeleteTemplateModal.addEventListener('click', closeDeleteModalTemplate);
|
|
}
|
|
if (deleteTemplateOverlay) {
|
|
deleteTemplateOverlay.addEventListener('click', closeDeleteModalTemplate);
|
|
}
|
|
|
|
// =========================
|
|
// Fitur: Hitung Harga Otomatis + Format ribuan
|
|
// =========================
|
|
function formatRibuan(x) {
|
|
if (!x) return '';
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
}
|
|
|
|
function hitungHarga(container) {
|
|
let total = 0;
|
|
|
|
// Hitung checkbox (fitur tanpa kategori)
|
|
container.querySelectorAll('input[type="checkbox"][name="fitur_id[]"]:checked')
|
|
.forEach(cb => {
|
|
total += parseInt(cb.dataset.harga) || 0;
|
|
});
|
|
|
|
// Hitung radio (fitur per kategori)
|
|
container.querySelectorAll('input[type="radio"]:checked')
|
|
.forEach(radio => {
|
|
total += parseInt(radio.dataset.harga) || 0;
|
|
});
|
|
|
|
const inputHarga = container.querySelector('input[name="harga"]');
|
|
if (inputHarga && !inputHarga.dataset.manual) {
|
|
inputHarga.value = formatRibuan(total);
|
|
}
|
|
}
|
|
|
|
// Format harga manual + tetap auto ribuan
|
|
document.querySelectorAll('input[name="harga"]').forEach(input => {
|
|
input.classList.add("harga-input"); // pastikan ada class
|
|
input.addEventListener('input', (e) => {
|
|
let value = e.target.value.replace(/\D/g, ''); // hanya angka
|
|
e.target.value = formatRibuan(value);
|
|
e.target.dataset.manual = "true"; // flag manual
|
|
});
|
|
});
|
|
|
|
// Modal Tambah
|
|
if (modalTambah) {
|
|
const inputs = modalTambah.querySelectorAll('input[name="fitur_id[]"], input[type="radio"]');
|
|
const inputHarga = modalTambah.querySelector('input[name="harga"]');
|
|
|
|
inputs.forEach(input => {
|
|
input.addEventListener('change', () => hitungHarga(modalTambah));
|
|
});
|
|
|
|
if (inputHarga) {
|
|
inputHarga.addEventListener('input', () => {
|
|
inputHarga.dataset.manual = "true";
|
|
});
|
|
}
|
|
|
|
// Hitung awal kalau ada yang sudah tercentang
|
|
hitungHarga(modalTambah);
|
|
}
|
|
|
|
// Modal Edit (banyak)
|
|
document.querySelectorAll('[id^="modalEdit"]').forEach(modal => {
|
|
const inputs = modal.querySelectorAll('input[name="fitur_id[]"], input[type="radio"]');
|
|
const inputHarga = modal.querySelector('input[name="harga"]');
|
|
|
|
inputs.forEach(input => {
|
|
input.addEventListener('change', () => hitungHarga(modal));
|
|
});
|
|
|
|
if (inputHarga) {
|
|
inputHarga.addEventListener('input', () => {
|
|
inputHarga.dataset.manual = "true";
|
|
});
|
|
}
|
|
|
|
// Hitung awal kalau ada yang sudah tercentang
|
|
hitungHarga(modal);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
|
|
</div>
|
|
@endsection
|