google maps
This commit is contained in:
parent
0216c572cf
commit
4bdfba7ea3
|
|
@ -16,7 +16,7 @@ class PoinController extends Controller
|
|||
public function getExpiringPoints()
|
||||
{
|
||||
try {
|
||||
$userId = auth()->user()->id;
|
||||
$userId = auth()->id();
|
||||
|
||||
$poinService = new PoinService();
|
||||
$poinExpiring = $poinService->getExpiringPoints($userId);
|
||||
|
|
@ -45,7 +45,7 @@ public function getExpiringPoints()
|
|||
public function getPointHistory()
|
||||
{
|
||||
try {
|
||||
$userId = auth()->user()->id;
|
||||
$userId = auth()->id();
|
||||
|
||||
$poinService = new PoinService();
|
||||
$history = $poinService->getPointHistory($userId);
|
||||
|
|
@ -72,7 +72,7 @@ public function redeem(Request $request)
|
|||
}
|
||||
|
||||
try {
|
||||
$userId = auth()->user()->id;
|
||||
$userId = auth()->id();
|
||||
$jumlah = $request->jumlah;
|
||||
$keterangan = $request->keterangan;
|
||||
|
||||
|
|
@ -86,6 +86,21 @@ public function redeem(Request $request)
|
|||
preg_match('/\[(\d{4}-\d{2}-\d{2})\]/', $keterangan, $matches);
|
||||
$tanggalPenggunaan = isset($matches[1]) ? $matches[1] : now()->format('Y-m-d');
|
||||
|
||||
$jadwal = \App\Models\JadwalKerja::with('shift')
|
||||
->where('id_user', $userId)
|
||||
->where('tanggal', $tanggalPenggunaan)
|
||||
->first();
|
||||
|
||||
if (!$jadwal || !$jadwal->shift) {
|
||||
return ApiResponse::error('Jadwal kerja tidak ditemukan untuk tanggal ' . Carbon::parse($tanggalPenggunaan)->format('d/m/Y') . '.', 400);
|
||||
}
|
||||
|
||||
$waktuBatasPengajuan = Carbon::parse($tanggalPenggunaan . ' ' . $jadwal->shift->jam_mulai)->subHour();
|
||||
|
||||
if (now()->greaterThan($waktuBatasPengajuan)) {
|
||||
return ApiResponse::error('Pengajuan poin harus dilakukan maksimal 1 jam sebelum jam kerja dimulai (Batas: ' . $waktuBatasPengajuan->format('d/m/Y H:i') . ').', 400);
|
||||
}
|
||||
|
||||
PenggunaanPoin::create([
|
||||
'id_user' => $userId,
|
||||
'tanggal_penggunaan' => $tanggalPenggunaan,
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ public function store(Request $request)
|
|||
'foto' => 'required|image|max:2048',
|
||||
'status' => 'required|in:masuk,pulang',
|
||||
'keterangan_luar_radius' => 'nullable|string|max:500',
|
||||
'alasan_telat' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
|
@ -277,6 +278,12 @@ public function history(Request $request)
|
|||
'verifikasi_wajah' => (bool) $item->verifikasi_wajah,
|
||||
'status_validasi' => $item->id_validasi,
|
||||
'alasan_penolakan' => $item->alasan_penolakan,
|
||||
'foto_masuk_url' => $item->foto_wajah_masuk
|
||||
? asset('storage/' . $item->foto_wajah_masuk)
|
||||
: null,
|
||||
'foto_pulang_url' => $item->foto_wajah_pulang
|
||||
? asset('storage/' . $item->foto_wajah_pulang)
|
||||
: null,
|
||||
];
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public function rules(): array
|
|||
{
|
||||
return [
|
||||
'nama_kantor' => ['required', 'string', 'max:255'],
|
||||
'tipe' => ['required', 'string', 'in:Pusat,Cabang'],
|
||||
'alamat' => ['nullable', 'string'],
|
||||
|
||||
'latitude' => ['required', 'numeric', 'between:-90,90'],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public function rules(): array
|
|||
{
|
||||
return [
|
||||
'nama_kantor' => ['required', 'string', 'max:255'],
|
||||
'tipe' => ['required', 'string', 'in:Pusat,Cabang'],
|
||||
'alamat' => ['nullable', 'string'],
|
||||
|
||||
'latitude' => ['required', 'numeric', 'between:-90,90'],
|
||||
|
|
|
|||
|
|
@ -23,4 +23,8 @@
|
|||
],
|
||||
],
|
||||
|
||||
'google_maps' => [
|
||||
'key' => env('GOOGLE_MAPS_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('kantor', function (Blueprint $table) {
|
||||
$table->string('tipe', 20)->default('Cabang')->after('nama_kantor');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('kantor', function (Blueprint $table) {
|
||||
$table->dropColumn('tipe');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,45 +1,47 @@
|
|||
let isFormModified = false;
|
||||
(function() {
|
||||
let isFormModified = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const form = document.querySelector('form');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const submitText = document.getElementById('submitText');
|
||||
const submitSpinner = document.getElementById('submitSpinner');
|
||||
const form = document.querySelector('form');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const submitText = document.getElementById('submitText');
|
||||
const submitSpinner = document.getElementById('submitSpinner');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('change', () => {
|
||||
isFormModified = true;
|
||||
});
|
||||
if (form) {
|
||||
form.addEventListener('change', () => {
|
||||
isFormModified = true;
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function() {
|
||||
isFormModified = false;
|
||||
form.addEventListener('submit', function() {
|
||||
isFormModified = false;
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-75', 'cursor-not-allowed');
|
||||
}
|
||||
if (submitSpinner) submitSpinner.classList.remove('hidden');
|
||||
if (submitText) submitText.textContent = 'Menyimpan...';
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (isFormModified) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
const backLinks = document.querySelectorAll('.btn-back');
|
||||
backLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (isFormModified) {
|
||||
const confirmLeave = confirm('Anda memiliki perubahan yang belum disimpan. Yakin ingin keluar?');
|
||||
if (!confirmLeave) {
|
||||
e.preventDefault();
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-75', 'cursor-not-allowed');
|
||||
}
|
||||
if (submitSpinner) submitSpinner.classList.remove('hidden');
|
||||
if (submitText) submitText.textContent = 'Menyimpan...';
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (isFormModified) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
const backLinks = document.querySelectorAll('.btn-back');
|
||||
backLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (isFormModified) {
|
||||
const confirmLeave = confirm('Anda memiliki perubahan yang belum disimpan. Yakin ingin keluar?');
|
||||
if (!confirmLeave) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* Image Cropper - Profile Photo
|
||||
* Menggunakan Cropper.js untuk crop foto profil berbentuk lingkaran
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fotoInput = document.getElementById('foto-input');
|
||||
const cropModal = document.getElementById('crop-modal');
|
||||
const cropImage = document.getElementById('crop-image');
|
||||
const cropSaveBtn = document.getElementById('crop-save-btn');
|
||||
const cropCancelBtn = document.getElementById('crop-cancel-btn');
|
||||
const cropRotateLeftBtn = document.getElementById('crop-rotate-left');
|
||||
const cropRotateRightBtn = document.getElementById('crop-rotate-right');
|
||||
const cropResetBtn = document.getElementById('crop-reset');
|
||||
const cropZoomInBtn = document.getElementById('crop-zoom-in');
|
||||
const cropZoomOutBtn = document.getElementById('crop-zoom-out');
|
||||
|
||||
let cropper = null;
|
||||
|
||||
if (!fotoInput || !cropModal) return;
|
||||
|
||||
fotoInput.addEventListener('change', function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('File harus berupa gambar.');
|
||||
fotoInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
cropImage.src = event.target.result;
|
||||
cropModal.classList.remove('hidden');
|
||||
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
cropper = new Cropper(cropImage, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
dragMode: 'move',
|
||||
autoCropArea: 0.85,
|
||||
cropBoxResizable: false,
|
||||
cropBoxMovable: false,
|
||||
guides: false,
|
||||
center: true,
|
||||
highlight: false,
|
||||
background: false,
|
||||
responsive: true,
|
||||
restore: false,
|
||||
checkCrossOrigin: false,
|
||||
ready: function () {
|
||||
const containerData = cropper.getContainerData();
|
||||
const size = Math.min(containerData.width, containerData.height) * 0.8;
|
||||
cropper.setCropBoxData({
|
||||
left: (containerData.width - size) / 2,
|
||||
top: (containerData.height - size) / 2,
|
||||
width: size,
|
||||
height: size,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
if (cropRotateLeftBtn) {
|
||||
cropRotateLeftBtn.addEventListener('click', function () {
|
||||
if (cropper) cropper.rotate(-90);
|
||||
});
|
||||
}
|
||||
|
||||
if (cropRotateRightBtn) {
|
||||
cropRotateRightBtn.addEventListener('click', function () {
|
||||
if (cropper) cropper.rotate(90);
|
||||
});
|
||||
}
|
||||
|
||||
if (cropResetBtn) {
|
||||
cropResetBtn.addEventListener('click', function () {
|
||||
if (cropper) cropper.reset();
|
||||
});
|
||||
}
|
||||
|
||||
if (cropZoomInBtn) {
|
||||
cropZoomInBtn.addEventListener('click', function () {
|
||||
if (cropper) cropper.zoom(0.1);
|
||||
});
|
||||
}
|
||||
|
||||
if (cropZoomOutBtn) {
|
||||
cropZoomOutBtn.addEventListener('click', function () {
|
||||
if (cropper) cropper.zoom(-0.1);
|
||||
});
|
||||
}
|
||||
|
||||
cropSaveBtn.addEventListener('click', function () {
|
||||
if (!cropper) return;
|
||||
|
||||
cropSaveBtn.disabled = true;
|
||||
cropSaveBtn.innerHTML = `
|
||||
<svg class="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Memproses...
|
||||
`;
|
||||
|
||||
const canvas = cropper.getCroppedCanvas({
|
||||
width: 512,
|
||||
height: 512,
|
||||
fillColor: '#fff',
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: 'high',
|
||||
});
|
||||
|
||||
canvas.toBlob(function (blob) {
|
||||
const croppedFile = new File([blob], 'profile-photo.jpg', {
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(croppedFile);
|
||||
fotoInput.files = dataTransfer.files;
|
||||
|
||||
const preview = document.getElementById('preview-foto');
|
||||
const previewNew = document.getElementById('preview-foto-new');
|
||||
const placeholder = document.getElementById('placeholder-foto');
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (preview) {
|
||||
preview.src = url;
|
||||
} else {
|
||||
if (placeholder) placeholder.classList.add('hidden');
|
||||
if (previewNew) {
|
||||
previewNew.src = url;
|
||||
previewNew.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
closeCropModal();
|
||||
}, 'image/jpeg', 0.9);
|
||||
});
|
||||
|
||||
cropCancelBtn.addEventListener('click', function () {
|
||||
fotoInput.value = '';
|
||||
closeCropModal();
|
||||
});
|
||||
|
||||
cropModal.addEventListener('click', function (e) {
|
||||
if (e.target === cropModal) {
|
||||
fotoInput.value = '';
|
||||
closeCropModal();
|
||||
}
|
||||
});
|
||||
|
||||
function closeCropModal() {
|
||||
cropModal.classList.add('hidden');
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
cropSaveBtn.disabled = false;
|
||||
cropSaveBtn.innerHTML = `
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Simpan
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,9 +1,23 @@
|
|||
(function () {
|
||||
const defaultLoc = [-7.9797, 112.6304];
|
||||
let maps = { create: null, edit: null };
|
||||
let markers = { create: null, edit: null };
|
||||
let circles = { create: null, edit: null };
|
||||
let geocoders = { create: null, edit: null };
|
||||
const defaultLoc = { lat: -7.9797, lng: 112.6304 };
|
||||
let maps = { create: null, edit: null, detail: null };
|
||||
let markers = { create: null, edit: null, detail: null };
|
||||
let circles = { create: null, edit: null, detail: null };
|
||||
let autocompletes = { create: null, edit: null };
|
||||
let geocoder = null;
|
||||
|
||||
function getGeocoder() {
|
||||
if (!geocoder) geocoder = new google.maps.Geocoder();
|
||||
return geocoder;
|
||||
}
|
||||
|
||||
function reverseGeocode(lat, lng, type) {
|
||||
getGeocoder().geocode({ location: { lat, lng } }, function (results, status) {
|
||||
if (status === 'OK' && results[0]) {
|
||||
setVal(`${type === 'create' ? '' : 'edit-'}alamat`, results[0].formatted_address);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setVal(id, v) {
|
||||
const el = document.getElementById(id);
|
||||
|
|
@ -12,176 +26,181 @@
|
|||
|
||||
function locateUser(type) {
|
||||
if (!navigator.geolocation) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Tidak Didukung',
|
||||
text: 'Browser Anda tidak mendukung fitur lokasi.',
|
||||
confirmButtonColor: '#1e293b',
|
||||
});
|
||||
Swal.fire({ icon: 'error', title: 'Tidak Didukung', text: 'Browser Anda tidak mendukung fitur lokasi.', confirmButtonColor: '#1e293b' });
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Mencari Lokasi...',
|
||||
text: 'Mengambil posisi GPS perangkat Anda',
|
||||
allowOutsideClick: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => Swal.showLoading()
|
||||
});
|
||||
Swal.fire({ title: 'Mencari Lokasi...', text: 'Mengambil posisi GPS perangkat Anda', allowOutsideClick: false, showConfirmButton: false, didOpen: () => Swal.showLoading() });
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
Swal.close();
|
||||
const lat = pos.coords.latitude;
|
||||
const lng = pos.coords.longitude;
|
||||
const position = { lat, lng };
|
||||
|
||||
if (maps[type]) {
|
||||
maps[type].setView([lat, lng], 16);
|
||||
if (markers[type]) markers[type].setLatLng([lat, lng]);
|
||||
if (circles[type]) circles[type].setLatLng([lat, lng]);
|
||||
maps[type].panTo(position);
|
||||
maps[type].setZoom(16);
|
||||
if (markers[type]) markers[type].setPosition(position);
|
||||
if (circles[type]) circles[type].setCenter(position);
|
||||
setVal(`${type}-lat`, lat.toFixed(6));
|
||||
setVal(`${type}-long`, lng.toFixed(6));
|
||||
reverseGeocode(lat, lng, type);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
Swal.close();
|
||||
let msg = 'Gagal mendapatkan lokasi.';
|
||||
if (error.code === 1) msg = 'Izin lokasi ditolak oleh pengguna. Aktifkan izin lokasi di pengaturan browser.';
|
||||
if (error.code === 1) msg = 'Izin lokasi ditolak. Aktifkan di pengaturan browser.';
|
||||
else if (error.code === 2) msg = 'Posisi tidak dapat ditentukan. Pastikan GPS aktif.';
|
||||
else if (error.code === 3) msg = 'Waktu permintaan lokasi habis. Coba lagi.';
|
||||
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Lokasi Tidak Ditemukan',
|
||||
text: msg,
|
||||
confirmButtonColor: '#1e293b',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
Swal.fire({ icon: 'warning', title: 'Lokasi Tidak Ditemukan', text: msg, confirmButtonColor: '#1e293b' });
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
function removeExtraMarkers(type) {
|
||||
if (!maps[type]) return;
|
||||
maps[type].eachLayer(function(layer) {
|
||||
if (layer instanceof L.Marker && layer !== markers[type]) {
|
||||
maps[type].removeLayer(layer);
|
||||
}
|
||||
});
|
||||
function getCircleColor(type) {
|
||||
if (type === 'create') return '#3b82f6';
|
||||
if (type === 'detail') return '#1e293b';
|
||||
return '#f59e0b';
|
||||
}
|
||||
|
||||
function initMap(type, lat, lng, radius) {
|
||||
const container = document.getElementById(`map-${type}`);
|
||||
if (!container) return;
|
||||
|
||||
const position = { lat: parseFloat(lat), lng: parseFloat(lng) };
|
||||
const isDraggable = type !== 'detail';
|
||||
|
||||
if (maps[type]) {
|
||||
const pos = [lat, lng];
|
||||
maps[type].setView(pos, 15);
|
||||
if (markers[type]) markers[type].setLatLng(pos);
|
||||
if (circles[type]) circles[type].setLatLng(pos);
|
||||
if (circles[type]) circles[type].setRadius(radius);
|
||||
removeExtraMarkers(type);
|
||||
setTimeout(() => maps[type].invalidateSize(), 300);
|
||||
maps[type].panTo(position);
|
||||
maps[type].setZoom(15);
|
||||
if (markers[type]) markers[type].setPosition(position);
|
||||
if (circles[type]) {
|
||||
circles[type].setCenter(position);
|
||||
circles[type].setRadius(parseFloat(radius));
|
||||
}
|
||||
google.maps.event.trigger(maps[type], 'resize');
|
||||
return;
|
||||
}
|
||||
|
||||
maps[type] = L.map(`map-${type}`).setView([lat, lng], 15);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(maps[type]);
|
||||
maps[type] = new google.maps.Map(container, {
|
||||
center: position,
|
||||
zoom: 15,
|
||||
mapTypeControl: false,
|
||||
streetViewControl: false,
|
||||
fullscreenControl: false,
|
||||
gestureHandling: 'greedy',
|
||||
styles: [{ featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }] }]
|
||||
});
|
||||
|
||||
markers[type] = L.marker([lat, lng], { draggable: true }).addTo(maps[type]);
|
||||
circles[type] = L.circle([lat, lng], {
|
||||
color: type === 'create' ? '#3b82f6' : (type === 'detail' ? '#1e293b' : '#f59e0b'),
|
||||
markers[type] = new google.maps.Marker({
|
||||
position: position,
|
||||
map: maps[type],
|
||||
draggable: isDraggable,
|
||||
animation: google.maps.Animation.DROP,
|
||||
});
|
||||
|
||||
circles[type] = new google.maps.Circle({
|
||||
map: maps[type],
|
||||
center: position,
|
||||
radius: parseFloat(radius),
|
||||
fillColor: getCircleColor(type),
|
||||
fillOpacity: 0.1,
|
||||
radius: radius
|
||||
}).addTo(maps[type]);
|
||||
strokeColor: getCircleColor(type),
|
||||
strokeWeight: 2,
|
||||
clickable: false,
|
||||
});
|
||||
|
||||
const update = (e) => {
|
||||
const latlng = e.target ? e.target.getLatLng() : e.latlng;
|
||||
if (markers[type]) markers[type].setLatLng([latlng.lat, latlng.lng]);
|
||||
if (circles[type]) circles[type].setLatLng([latlng.lat, latlng.lng]);
|
||||
setVal(`${type}-lat`, latlng.lat.toFixed(6));
|
||||
setVal(`${type}-long`, latlng.lng.toFixed(6));
|
||||
removeExtraMarkers(type);
|
||||
};
|
||||
if (isDraggable) {
|
||||
markers[type].addListener('dragend', function () {
|
||||
const pos = markers[type].getPosition();
|
||||
circles[type].setCenter(pos);
|
||||
setVal(`${type}-lat`, pos.lat().toFixed(6));
|
||||
setVal(`${type}-long`, pos.lng().toFixed(6));
|
||||
reverseGeocode(pos.lat(), pos.lng(), type);
|
||||
});
|
||||
|
||||
markers[type].on('dragend', update);
|
||||
maps[type].on('click', update);
|
||||
maps[type].addListener('click', function (e) {
|
||||
const pos = e.latLng;
|
||||
markers[type].setPosition(pos);
|
||||
circles[type].setCenter(pos);
|
||||
setVal(`${type}-lat`, pos.lat().toFixed(6));
|
||||
setVal(`${type}-long`, pos.lng().toFixed(6));
|
||||
reverseGeocode(pos.lat(), pos.lng(), type);
|
||||
});
|
||||
|
||||
if (typeof L.Control.Geocoder !== 'undefined') {
|
||||
geocoders[type] = L.Control.geocoder({
|
||||
placeholder: 'Cari lokasi...',
|
||||
errorMessage: 'Lokasi tidak ditemukan',
|
||||
collapsed: false,
|
||||
defaultMarkGeocode: false,
|
||||
geocoder: L.Control.Geocoder.nominatim({
|
||||
geocodingQueryParams: {
|
||||
countrycodes: 'id',
|
||||
'accept-language': 'id'
|
||||
// Google Places Autocomplete (search lokasi)
|
||||
const searchInput = document.getElementById(`search-${type}`);
|
||||
if (searchInput && !autocompletes[type]) {
|
||||
const autocomplete = new google.maps.places.Autocomplete(searchInput, {
|
||||
fields: ['geometry', 'name', 'formatted_address'],
|
||||
componentRestrictions: { country: 'id' },
|
||||
});
|
||||
|
||||
autocomplete.bindTo('bounds', maps[type]);
|
||||
|
||||
autocomplete.addListener('place_changed', function () {
|
||||
const place = autocomplete.getPlace();
|
||||
if (!place.geometry || !place.geometry.location) {
|
||||
console.warn("Tempat tidak ditemukan:", place);
|
||||
return;
|
||||
}
|
||||
})
|
||||
}).on('markgeocode', function (e) {
|
||||
const { center } = e.geocode;
|
||||
maps[type].setView(center, 16);
|
||||
if (markers[type]) markers[type].setLatLng(center);
|
||||
if (circles[type]) circles[type].setLatLng(center);
|
||||
setVal(`${type}-lat`, center.lat.toFixed(6));
|
||||
setVal(`${type}-long`, center.lng.toFixed(6));
|
||||
|
||||
removeExtraMarkers(type);
|
||||
const loc = place.geometry.location;
|
||||
maps[type].panTo(loc);
|
||||
maps[type].setZoom(16);
|
||||
markers[type].setPosition(loc);
|
||||
circles[type].setCenter(loc);
|
||||
setVal(`${type}-lat`, loc.lat().toFixed(6));
|
||||
setVal(`${type}-long`, loc.lng().toFixed(6));
|
||||
|
||||
setTimeout(() => {
|
||||
removeExtraMarkers(type);
|
||||
}, 200);
|
||||
}).addTo(maps[type]);
|
||||
}
|
||||
if (place.formatted_address) {
|
||||
setVal(`${type === 'create' ? '' : 'edit-'}alamat`, place.formatted_address);
|
||||
}
|
||||
});
|
||||
|
||||
const locateBtn = L.control({ position: 'topleft' });
|
||||
locateBtn.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.width = '30px';
|
||||
div.style.height = '30px';
|
||||
div.style.display = 'flex';
|
||||
div.style.alignItems = 'center';
|
||||
div.style.justifyContent = 'center';
|
||||
div.style.cursor = 'pointer';
|
||||
div.innerHTML = '<svg class="w-4 h-4 text-slate-700" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path></svg>';
|
||||
div.onclick = function(e) {
|
||||
// Cegah Enter submit form
|
||||
searchInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') e.preventDefault();
|
||||
});
|
||||
|
||||
autocompletes[type] = autocomplete;
|
||||
}
|
||||
|
||||
// Tombol "Lokasi Saya"
|
||||
const locateBtn = document.createElement('button');
|
||||
locateBtn.type = 'button';
|
||||
locateBtn.innerHTML = '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path></svg>';
|
||||
locateBtn.style.cssText = 'background:white;border:none;width:40px;height:40px;display:flex;align-items:center;justify-content:center;cursor:pointer;margin:10px;border-radius:8px;box-shadow:0 2px 6px rgba(0,0,0,0.15);color:#475569;transition:all 0.2s;';
|
||||
locateBtn.addEventListener('mouseenter', function () { this.style.background = '#f1f5f9'; });
|
||||
locateBtn.addEventListener('mouseleave', function () { this.style.background = 'white'; });
|
||||
locateBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
locateUser(type);
|
||||
};
|
||||
return div;
|
||||
};
|
||||
locateBtn.addTo(maps[type]);
|
||||
|
||||
setTimeout(() => maps[type].invalidateSize(), 300);
|
||||
});
|
||||
maps[type].controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(locateBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function setupRadiusListeners() {
|
||||
['create', 'edit'].forEach(t => {
|
||||
const el = document.getElementById(`${t}-radius`);
|
||||
if (el) el.addEventListener('input', (e) => circles[t] ? circles[t].setRadius(e.target.value) : null);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupMaps() {
|
||||
['create', 'edit', 'detail'].forEach(t => {
|
||||
if (maps[t]) {
|
||||
maps[t].remove();
|
||||
maps[t] = null;
|
||||
markers[t] = null;
|
||||
circles[t] = null;
|
||||
geocoders[t] = null;
|
||||
}
|
||||
if (el) el.addEventListener('input', (e) => {
|
||||
if (circles[t]) circles[t].setRadius(parseFloat(e.target.value) || 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('open-modal', function (e) {
|
||||
if (e.detail == 'create-kantor') {
|
||||
setVal('create-lat', defaultLoc[0]);
|
||||
setVal('create-long', defaultLoc[1]);
|
||||
setVal('create-lat', defaultLoc.lat);
|
||||
setVal('create-long', defaultLoc.lng);
|
||||
setTimeout(() => {
|
||||
initMap('create', defaultLoc[0], defaultLoc[1], 50);
|
||||
initMap('create', defaultLoc.lat, defaultLoc.lng, 50);
|
||||
locateUser('create');
|
||||
}, 300);
|
||||
}
|
||||
|
|
@ -205,7 +224,7 @@
|
|||
setTimeout(() => initMap('edit', parseFloat(d.lat), parseFloat(d.long), parseInt(d.radius)), 300);
|
||||
};
|
||||
|
||||
window.openDetailModal = function(btn) {
|
||||
window.openDetailModal = function (btn) {
|
||||
const data = btn.dataset;
|
||||
document.getElementById('detail-nama').innerText = data.nama;
|
||||
document.getElementById('detail-tipe').innerText = data.tipe;
|
||||
|
|
@ -214,14 +233,11 @@
|
|||
document.getElementById('detail-radius').innerText = data.radius + ' m';
|
||||
document.getElementById('detail-alamat').innerText = data.alamat || '-';
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', {
|
||||
detail: 'detail-kantor'
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'detail-kantor' }));
|
||||
setTimeout(() => {
|
||||
initMap('detail', parseFloat(data.lat), parseFloat(data.long), parseInt(data.radius));
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
setupRadiusListeners();
|
||||
|
||||
|
|
@ -229,7 +245,12 @@
|
|||
window.__kantorTurboAttached = true;
|
||||
|
||||
document.addEventListener('turbo:before-cache', function () {
|
||||
cleanupMaps();
|
||||
['create', 'edit', 'detail'].forEach(t => {
|
||||
maps[t] = null;
|
||||
markers[t] = null;
|
||||
circles[t] = null;
|
||||
autocompletes[t] = null;
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('turbo:load', function () {
|
||||
|
|
|
|||
|
|
@ -3,29 +3,143 @@
|
|||
@section('title', 'Data Kantor')
|
||||
|
||||
@section('style')
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.css" />
|
||||
<style>
|
||||
#map-create,
|
||||
#map-edit {
|
||||
#map-edit,
|
||||
#map-detail {
|
||||
height: 300px !important;
|
||||
width: 100% !important;
|
||||
border-radius: 0.75rem;
|
||||
z-index: 1;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
.map-search-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.map-search-wrapper svg {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #94a3b8;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.map-search-input {
|
||||
width: 100%;
|
||||
padding: 11px 14px 11px 40px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: all 0.25s ease;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
}
|
||||
.map-search-input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.map-search-input:focus {
|
||||
border-color: #3b82f6;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(59,130,246,0.1);
|
||||
}
|
||||
.pac-container {
|
||||
z-index: 99999 !important;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
}
|
||||
.pac-item {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pac-item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.pac-item:hover {
|
||||
background: #f0f9ff;
|
||||
}
|
||||
.pac-item-selected {
|
||||
background: #eff6ff;
|
||||
}
|
||||
.pac-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.pac-item-query {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
}
|
||||
.map-section-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.map-section-label svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
.coord-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
.coord-chip svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
.detail-info-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
.detail-info-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.detail-info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('script')
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet-control-geocoder@2.4.0/dist/Control.Geocoder.js"></script>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key={{ config('services.google_maps.key') }}&libraries=places"></script>
|
||||
<script>
|
||||
window.kantorUpdateUrl = "{{ route('kantor.update', ':id') }}";
|
||||
</script>
|
||||
<script src="{{ asset('js/kantor-map.js') }}"></script>
|
||||
<script src="{{ asset('js/kantor-map.js') }}?v={{ time() }}"></script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
|
@ -60,10 +174,13 @@ class="px-5 py-2.5 bg-primary text-white text-sm font-semibold rounded-xl hover:
|
|||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-left">
|
||||
<code
|
||||
class="text-[11px] font-mono bg-slate-100 px-2 py-1 rounded border border-slate-200 text-slate-600">
|
||||
{{ number_format($k->latitude, 5) }}, {{ number_format($k->longitude, 5) }}
|
||||
</code>
|
||||
<span class="coord-chip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
{{ number_format($k->latitude, 5) }}, {{ number_format($k->longitude, 5) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<x-badge color="blue">
|
||||
|
|
@ -108,11 +225,24 @@ class="p-2 bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200 transition"
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modal Tambah Kantor --}}
|
||||
<x-modal name="create-kantor" title="Tambah Lokasi Kantor">
|
||||
<form action="{{ route('kantor.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-2">Tentukan Lokasi</label>
|
||||
<div class="map-section-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
Tentukan Lokasi
|
||||
</div>
|
||||
<div class="map-search-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input type="text" id="search-create" class="map-search-input" placeholder="Cari lokasi... (contoh: Jl. Merdeka, Malang)" autocomplete="off">
|
||||
</div>
|
||||
<div id="map-create"></div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
|
|
@ -123,8 +253,8 @@ class="p-2 bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200 transition"
|
|||
</x-select></div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1"><x-input label="Lat" name="latitude" id="create-lat" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Long" name="longitude" id="create-long" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Latitude" name="latitude" id="create-lat" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Longitude" name="longitude" id="create-long" readonly required /></div>
|
||||
<div class="w-full md:w-32"><x-input type="number" label="Radius (m)" name="radius" id="create-radius" value="50" required /></div>
|
||||
</div>
|
||||
<x-textarea label="Alamat" name="alamat" rows="2" />
|
||||
|
|
@ -137,10 +267,17 @@ class="px-4 py-2 text-sm bg-primary text-white rounded-xl hover:bg-primary/90">S
|
|||
</form>
|
||||
</x-modal>
|
||||
|
||||
{{-- Modal Edit Kantor --}}
|
||||
<x-modal name="edit-kantor" title="Edit Data Kantor">
|
||||
<form id="form-edit" action="#" method="POST">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-4">
|
||||
<div class="map-search-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input type="text" id="search-edit" class="map-search-input" placeholder="Cari lokasi..." autocomplete="off">
|
||||
</div>
|
||||
<div id="map-edit"></div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
|
|
@ -151,8 +288,8 @@ class="px-4 py-2 text-sm bg-primary text-white rounded-xl hover:bg-primary/90">S
|
|||
</x-select></div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1"><x-input label="Lat" name="latitude" id="edit-lat" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Long" name="longitude" id="edit-long" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Latitude" name="latitude" id="edit-lat" readonly required /></div>
|
||||
<div class="flex-1"><x-input label="Longitude" name="longitude" id="edit-long" readonly required /></div>
|
||||
<div class="w-full md:w-32"><x-input type="number" label="Radius (m)" name="radius" id="edit-radius" required /></div>
|
||||
</div>
|
||||
<x-textarea label="Alamat" name="alamat" id="edit-alamat" rows="2" />
|
||||
|
|
@ -164,38 +301,44 @@ class="px-4 py-2 text-sm bg-amber-500 text-white rounded-xl hover:bg-amber-600">
|
|||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
|
||||
{{-- Modal Detail Kantor --}}
|
||||
<x-modal name="detail-kantor" title="Detail Lokasi Kantor">
|
||||
<div class="p-1 space-y-4">
|
||||
<div id="map-detail" class="h-64 rounded-xl border border-slate-200 mb-4"></div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Nama Kantor</label>
|
||||
<p id="detail-nama" class="text-slate-800 font-bold"></p>
|
||||
<div class="space-y-4">
|
||||
<div id="map-detail" class="rounded-xl border border-slate-200"></div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Nama Kantor</div>
|
||||
<p id="detail-nama" class="detail-info-value"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Tipe</label>
|
||||
<p id="detail-tipe" class="text-slate-800"></p>
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Tipe Kantor</div>
|
||||
<p id="detail-tipe" class="detail-info-value"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Latitude</label>
|
||||
<p id="detail-lat" class="text-slate-800 font-mono text-xs"></p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Latitude</div>
|
||||
<p id="detail-lat" class="detail-info-value font-mono text-xs"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Longitude</label>
|
||||
<p id="detail-long" class="text-slate-800 font-mono text-xs"></p>
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Longitude</div>
|
||||
<p id="detail-long" class="detail-info-value font-mono text-xs"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Radius</label>
|
||||
<p id="detail-radius" class="text-slate-800"></p>
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Radius</div>
|
||||
<p id="detail-radius" class="detail-info-value text-blue-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Alamat</label>
|
||||
<p id="detail-alamat" class="text-slate-700 text-sm leading-relaxed"></p>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<div class="detail-info-label">Alamat Lengkap</div>
|
||||
<p id="detail-alamat" class="text-slate-700 text-sm leading-relaxed mt-1"></p>
|
||||
</div>
|
||||
<div class="flex justify-end pt-4 border-t border-slate-100">
|
||||
|
||||
<div class="flex justify-end pt-3 border-t border-slate-100">
|
||||
<button type="button" x-data @click="$dispatch('close-modal', 'detail-kantor')"
|
||||
class="px-5 py-2 text-sm bg-slate-800 text-white rounded-xl hover:bg-slate-700 transition shadow-sm">Tutup</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Pengaturan Akun')
|
||||
|
||||
|
|
@ -45,8 +45,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
|
|||
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="file" id="foto-input" name="foto" class="hidden" accept="image/*"
|
||||
onchange="previewImage(this)">
|
||||
<input type="file" id="foto-input" name="foto" class="hidden" accept="image/*">
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3 class="font-bold text-slate-800">{{ auth()->user()->nama_lengkap }}</h3>
|
||||
|
|
@ -86,27 +85,121 @@ class="px-6 py-2.5 bg-primary text-white font-semibold rounded-xl hover:bg-prima
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewImage(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const preview = document.getElementById('preview-foto');
|
||||
const previewNew = document.getElementById('preview-foto-new');
|
||||
const placeholder = document.getElementById('placeholder-foto');
|
||||
{{-- Modal Crop Foto --}}
|
||||
<div id="crop-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-500/50 p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden" onclick="event.stopPropagation()">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-800">Atur Foto Profil</h3>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Geser dan zoom untuk mengatur posisi foto</p>
|
||||
</div>
|
||||
<button id="crop-cancel-btn" type="button"
|
||||
class="p-2 rounded-lg hover:bg-slate-100 transition text-slate-400 hover:text-slate-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
if (preview) {
|
||||
preview.src = e.target.result;
|
||||
} else {
|
||||
if (placeholder) placeholder.classList.add('hidden');
|
||||
if (previewNew) {
|
||||
previewNew.src = e.target.result;
|
||||
previewNew.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
{{-- Crop Area --}}
|
||||
<div class="relative bg-slate-900 flex items-center justify-center" style="height: 380px;">
|
||||
<img id="crop-image" src="" alt="Crop Preview" class="max-w-full max-h-full">
|
||||
</div>
|
||||
|
||||
{{-- Toolbar --}}
|
||||
<div class="flex items-center justify-center gap-2 px-6 py-3 bg-slate-50 border-t border-b border-slate-100">
|
||||
<button id="crop-rotate-left" type="button"
|
||||
class="p-2.5 rounded-xl hover:bg-white hover:shadow-sm transition text-slate-500 hover:text-slate-700"
|
||||
title="Putar Kiri">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h1l1-2m0 0l2-2m-2 2l2 2m6-8a9 9 0 11-4.2 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="crop-rotate-right" type="button"
|
||||
class="p-2.5 rounded-xl hover:bg-white hover:shadow-sm transition text-slate-500 hover:text-slate-700"
|
||||
title="Putar Kanan">
|
||||
<svg class="w-5 h-5 transform scale-x-[-1]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h1l1-2m0 0l2-2m-2 2l2 2m6-8a9 9 0 11-4.2 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 bg-slate-200 mx-1"></div>
|
||||
<button id="crop-zoom-in" type="button"
|
||||
class="p-2.5 rounded-xl hover:bg-white hover:shadow-sm transition text-slate-500 hover:text-slate-700"
|
||||
title="Zoom In">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="crop-zoom-out" type="button"
|
||||
class="p-2.5 rounded-xl hover:bg-white hover:shadow-sm transition text-slate-500 hover:text-slate-700"
|
||||
title="Zoom Out">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 bg-slate-200 mx-1"></div>
|
||||
<button id="crop-reset" type="button"
|
||||
class="p-2.5 rounded-xl hover:bg-white hover:shadow-sm transition text-slate-500 hover:text-slate-700"
|
||||
title="Reset">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4">
|
||||
<button id="crop-cancel-btn-footer" type="button"
|
||||
class="px-5 py-2.5 text-sm font-semibold text-slate-600 bg-slate-100 rounded-xl hover:bg-slate-200 transition"
|
||||
onclick="document.getElementById('crop-cancel-btn').click()">
|
||||
Batal
|
||||
</button>
|
||||
<button id="crop-save-btn" type="button"
|
||||
class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-primary rounded-xl hover:bg-primary/90 transition shadow-lg shadow-indigo-500/20">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Cropper.js CDN --}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
|
||||
<script src="{{ asset('js/image-cropper.js') }}"></script>
|
||||
|
||||
{{-- Cropper circular overlay --}}
|
||||
<style>
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
</script>
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 3px solid rgba(255, 255, 255, 0.8);
|
||||
outline-offset: 0;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.cropper-dashed,
|
||||
.cropper-point,
|
||||
.cropper-line {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-crop-box {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
background-color: rgba(15, 23, 42, 0.85);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Reference in New Issue