MIF_E31230069/spk_kontrakan/resources/views/Kontrakan/create.blade.php

1350 lines
60 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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', 'Tambah Kontrakan')
@section('content')
<div class="container-fluid px-4">
<style>
.breadcrumb {
background: transparent;
border-radius: 8px;
padding: 0;
margin-bottom: 2rem;
}
.breadcrumb-item a {
color: #667eea;
font-weight: 500;
}
.breadcrumb-item.active {
color: #764ba2;
font-weight: 600;
}
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
}
.page-header h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.form-card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
overflow: hidden;
}
.section-header {
color: #667eea;
font-size: 1.1rem;
font-weight: 700;
padding-bottom: 1rem;
border-bottom: 2px solid #667eea;
margin-bottom: 1.5rem;
}
.form-label {
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
}
.input-group-text {
border: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
font-weight: 600;
padding: 0.75rem 1.75rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
color: white;
}
.info-card {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border: 1px solid #667eea;
border-radius: 10px;
padding: 1.5rem;
margin-top: 2rem;
}
</style>
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('kontrakan.index') }}">Kontrakan</a></li>
<li class="breadcrumb-item active">Tambah Data</li>
</ol>
</nav>
<!-- Page Header -->
<div class="page-header mb-4">
<h2 class="mb-2">
<i class="bi bi-plus-circle me-2"></i>Tambah Kontrakan Baru
</h2>
<p class="mb-0 fs-6">Lengkapi formulir di bawah untuk menambahkan data kontrakan ke sistem</p>
</div>
<!-- Form Card -->
<div class="row">
<div class="col-12">
<div class="card form-card">
<div class="card-body p-4">
<form action="{{ route('kontrakan.store') }}" method="POST" enctype="multipart/form-data" id="kontrakanForm">
@csrf
<!-- Informasi Dasar Section -->
<div class="mb-4">
<h5 class="section-header">
<i class="bi bi-info-circle me-2"></i>Informasi Dasar
</h5>
<div class="row g-3">
<!-- Nama Kontrakan -->
<div class="col-md-12">
<label for="nama" class="form-label fw-semibold">
Nama Kontrakan <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-building" style="color: #667eea;"></i>
</span>
<input
type="text"
name="nama"
class="form-control border-start-0 @error('nama') is-invalid @enderror"
id="nama"
placeholder="Contoh: Kontrakan Melati Residence"
value="{{ old('nama') }}"
required
>
@error('nama')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Masukkan nama kontrakan yang mudah dikenali</small>
</div>
<!-- Alamat -->
<div class="col-md-12">
<label for="alamat" class="form-label fw-semibold">
Alamat Lengkap <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0 align-items-start pt-2">
<i class="bi bi-geo-alt text-danger"></i>
</span>
<textarea
name="alamat"
class="form-control border-start-0 @error('alamat') is-invalid @enderror"
id="alamat"
rows="3"
placeholder="Contoh: Jl. Kaliurang KM 5, Sleman, Yogyakarta"
required
>{{ old('alamat') }}</textarea>
@error('alamat')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Alamat lengkap beserta RT/RW jika ada. Alamat akan auto-update saat klik peta.</small>
</div>
<!-- WhatsApp -->
<div class="col-md-12">
<label for="no_whatsapp" class="form-label fw-semibold">
<i class="bi bi-whatsapp text-success"></i> Nomor WhatsApp Pemilik <span class="text-muted">(Opsional)</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-telephone text-success"></i>
</span>
<input
type="text"
name="no_whatsapp"
class="form-control border-start-0 @error('no_whatsapp') is-invalid @enderror"
id="no_whatsapp"
placeholder="Contoh: 081234567890"
value="{{ old('no_whatsapp') }}"
maxlength="20"
>
@error('no_whatsapp')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Format: 08xxx atau 628xxx (untuk kemudahan calon penyewa menghubungi)
</small>
</div>
</div>
</div>
<!-- ========== LOKASI & KOORDINAT SECTION - UPDATED! ========== -->
<div class="mb-4">
<h5 class="section-header">
<i class="bi bi-pin-map-fill me-2"></i>Lokasi & Koordinat
</h5>
<!-- Alert Info - UPDATED -->
<div class="alert alert-info border-info">
<div class="d-flex">
<i class="bi bi-info-circle me-2 mt-1"></i>
<div>
<strong> Cara Menentukan Lokasi (PILIH SALAH SATU):</strong>
<ul class="mb-0 mt-2 small">
<li class="mb-2">
<strong class="text-success">🎯 REKOMENDASI #1: Klik di Peta</strong>
<div class="text-muted" style="font-size: 0.85rem;">
Klik pada peta di lokasi kontrakan Alamat & koordinat <strong>otomatis terisi</strong><br>
<span class="badge bg-success bg-opacity-10 text-success mt-1">Paling Akurat & Mudah!</span>
</div>
</li>
<li class="mb-2">
<strong style="color: #667eea;">📍 Deteksi GPS</strong>
<div class="text-muted" style="font-size: 0.85rem;">
Klik "Deteksi Lokasi Saya" <strong>HANYA jika Anda sedang berada di lokasi kontrakan</strong>
</div>
</li>
<li class="mb-2">
<strong class="text-warning">⌨️ Input Manual Koordinat</strong>
<div class="text-muted" style="font-size: 0.85rem;">
Ketik latitude & longitude dari Google Maps Marker otomatis pindah
</div>
</li>
<li>
<strong class="text-secondary">🔍 Cari dari Alamat</strong>
<div class="text-muted" style="font-size: 0.85rem;">
<span class="text-danger">⚠️ Hanya untuk alamat umum</span> (contoh: "Sumbersari, Jember")<br>
<span class="text-danger"> TIDAK untuk nama perumahan/kontrakan spesifik</span>
</div>
</li>
</ul>
<div class="alert alert-warning bg-opacity-10 border-warning mt-2 mb-0 py-2 px-3">
<small class="mb-0">
<strong>💡 Tips:</strong> Jika alamat berisi nama perumahan/gang (contoh: "Perumahan Melati, Gang Mawar"),
<strong class="text-danger">gunakan cara KLIK PETA atau INPUT KOORDINAT manual</strong>,
jangan gunakan tombol "Cari dari Alamat"
</small>
</div>
</div>
</div>
</div>
<!-- Tombol Geocoding & Deteksi Lokasi -->
<div class="row g-2 mb-3">
<div class="col-12 col-md-6">
<button type="button" id="geocodeBtn" class="btn btn-secondary w-100">
<i class="bi bi-search me-2"></i>Cari dari Alamat Umum
</button>
<small class="text-danger d-block mt-1">
<i class="bi bi-exclamation-triangle me-1"></i>
Hanya untuk alamat umum (Kota/Kecamatan), bukan nama perumahan!
</small>
</div>
<div class="col-12 col-md-6">
<button type="button" id="detectLocation" class="btn w-100" style="border: 2px solid #667eea; color: #667eea; background: transparent;">
<i class="bi bi-crosshair me-2"></i>Deteksi Lokasi Saya
</button>
<small class="text-muted d-block mt-1">
<i class="bi bi-geo-alt me-1"></i>Gunakan GPS Anda (harus di lokasi)
</small>
</div>
</div>
<div class="row g-3">
<!-- Latitude - UPDATED: Bisa Input Manual -->
<div class="col-md-6">
<label for="latitude" class="form-label fw-semibold">
Latitude <span class="text-danger">*</span>
<span class="badge bg-warning bg-opacity-10 text-warning ms-2 small">
<i class="bi bi-pencil me-1"></i>Bisa input manual
</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="bi bi-geo text-danger"></i>
</span>
<input
type="number"
name="latitude"
class="form-control @error('latitude') is-invalid @enderror"
id="latitude"
placeholder="-6.966667"
value="{{ old('latitude') }}"
step="0.00000001"
min="-90"
max="90"
required
>
@error('latitude')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Range: -90 sampai 90</small>
</div>
<!-- Longitude - UPDATED: Bisa Input Manual -->
<div class="col-md-6">
<label for="longitude" class="form-label fw-semibold">
Longitude <span class="text-danger">*</span>
<span class="badge bg-warning bg-opacity-10 text-warning ms-2 small">
<i class="bi bi-pencil me-1"></i>Bisa input manual
</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="bi bi-geo text-danger"></i>
</span>
<input
type="number"
name="longitude"
class="form-control @error('longitude') is-invalid @enderror"
id="longitude"
placeholder="110.416664"
value="{{ old('longitude') }}"
step="0.00000001"
min="-180"
max="180"
required
>
@error('longitude')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Range: -180 sampai 180</small>
</div>
<!-- Map Container -->
<div class="col-12">
<div id="map" style="height: 400px; border-radius: 8px; border: 2px solid #dee2e6;"></div>
<small class="text-muted d-block mt-2">
<i class="bi bi-hand-index me-1"></i>Klik pada peta alamat & koordinat otomatis terisi | Drag marker untuk ubah posisi
</small>
</div>
</div>
</div>
<!-- Detail Properti Section -->
<div class="mb-4">
<h5 class="section-header">
<i class="bi bi-house me-2"></i>Detail Properti
</h5>
<div class="row g-3">
<!-- Harga -->
<div class="col-md-6">
<label for="harga" class="form-label fw-semibold">
Harga Sewa/Tahun <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-currency-dollar text-success"></i>
</span>
<input
type="number"
name="harga"
class="form-control border-start-0 @error('harga') is-invalid @enderror"
id="harga"
placeholder="500000"
value="{{ old('harga') }}"
min="0"
required
>
@error('harga')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Harga dalam Rupiah per tahun</small>
<div id="harga_preview" class="mt-1" style="display:none;">
<span class="badge bg-success-subtle text-success border border-success-subtle" style="font-size:0.82rem; padding: 4px 10px;">
<i class="bi bi-info-circle me-1"></i>
<span id="harga_preview_text"></span>
</span>
</div>
</div>
<!-- jumlah_kamar -->
<div class="col-md-6">
<label for="jumlah_kamar" class="form-label fw-semibold">
Jumlah Kamar Tidur <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-door-closed" style="color: #667eea;"></i>
</span>
<input
type="number"
name="jumlah_kamar"
class="form-control border-start-0 @error('jumlah_kamar') is-invalid @enderror"
id="jumlah_kamar"
placeholder="Contoh: 3"
value="{{ old('jumlah_kamar') }}"
min="1"
step="1"
required
>
<span class="input-group-text bg-light">kamar</span>
@error('jumlah_kamar')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Jumlah kamar tidur yang tersedia</small>
</div>
<!-- Jarak - AUTO CALCULATED! -->
<div class="col-md-6">
<label for="jarak" class="form-label fw-semibold">
Jarak ke Kampus <span class="text-danger">*</span>
<span class="badge bg-info bg-opacity-10 text-info ms-2">
<i class="bi bi-calculator me-1"></i>Auto-calculated
</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-pin-map text-info"></i>
</span>
<input
type="number"
name="jarak"
class="form-control border-start-0 @error('jarak') is-invalid @enderror"
id="jarak"
placeholder="Otomatis dihitung"
value="{{ old('jarak') }}"
min="0"
readonly
required
>
<span class="input-group-text bg-light">meter</span>
@error('jarak')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Jarak otomatis dihitung dari koordinat kontrakan ke kampus
</small>
</div>
<!-- Fasilitas -->
<div class="col-md-6">
<label for="fasilitas" class="form-label fw-semibold">
Fasilitas
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-star text-warning"></i>
</span>
<input
type="text"
name="fasilitas"
class="form-control border-start-0 @error('fasilitas') is-invalid @enderror"
id="fasilitas"
placeholder="Contoh: WiFi, AC, Kasur, Lemari"
value="{{ old('fasilitas') }}"
>
@error('fasilitas')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Pisahkan dengan koma (,) untuk fasilitas lebih dari satu</small>
</div>
</div>
</div>
<!-- Upload Foto Section -->
<div class="mb-4">
<h5 class="section-header">
<i class="bi bi-camera me-2"></i>Upload Foto
</h5>
<div class="row g-3">
<!-- Input Upload Foto -->
<div class="col-md-12">
<label for="foto" class="form-label fw-semibold">
Foto Kontrakan <span class="text-muted">(Opsional)</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-image" style="color: #667eea;"></i>
</span>
<input
type="file"
name="foto"
class="form-control border-start-0 @error('foto') is-invalid @enderror"
id="foto"
accept="image/jpeg,image/png,image/jpg"
>
@error('foto')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<small class="text-muted">Format: JPG, PNG, JPEG. Maksimal 2MB</small>
</div>
<!-- Preview Foto -->
<div class="col-md-12">
<div id="preview-foto-container" style="display: none;">
<label class="form-label fw-semibold">Preview Foto:</label>
<div class="border rounded p-2 bg-light position-relative">
<img id="preview-foto" src="" alt="Preview" style="max-width: 100%; max-height: 300px; border-radius: 8px; display: block; margin: 0 auto;">
<button type="button" class="btn btn-sm btn-danger mt-2" onclick="removeImage()">
<i class="bi bi-x-circle me-1"></i>Hapus Foto
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Section -->
<div class="alert alert-light border mb-4" id="previewSection" style="display: none;">
<h6 class="fw-bold mb-3">
<i class="bi bi-eye me-2" style="color: #667eea;"></i>Preview Data
</h6>
<div class="row">
<div class="col-md-6">
<p class="mb-1"><small class="text-muted">Nama:</small> <span id="previewNama" class="fw-semibold">-</span></p>
<p class="mb-1"><small class="text-muted">Harga:</small> <span id="previewHarga" class="fw-semibold">-</span></p>
</div>
<div class="col-md-6">
<p class="mb-1"><small class="text-muted">Kamar Tidur:</small> <span id="previewjumlah_kamar" class="fw-semibold">-</span></p>
<p class="mb-1"><small class="text-muted">Kamar Mandi:</small> <span id="previewBathroomCount" class="fw-semibold">-</span></p>
<p class="mb-1"><small class="text-muted">Jarak:</small> <span id="previewJarak" class="fw-semibold">-</span></p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a href="{{ route('kontrakan.index') }}" class="btn btn-outline-secondary px-4">
<i class="bi bi-arrow-left me-2"></i>Batal
</a>
<button type="submit" class="btn btn-submit px-4">
<i class="bi bi-check-circle me-2"></i>Simpan Data
</button>
</div>
</form>
</div>
</div>
<!-- Info Card -->
<div class="info-card">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-lightbulb text-warning" style="font-size: 1.5rem;"></i>
</div>
<div>
<h6 class="fw-bold mb-2">Tips Pengisian Data:</h6>
<ul class="mb-0 small">
<li class="mb-2">Pastikan semua data yang ditandai <span class="text-danger">(*)</span> wajib diisi</li>
<li class="mb-2"><strong class="text-success">CARA TERBAIK:</strong> Klik langsung pada peta untuk menentukan lokasi! Alamat akan otomatis terisi.</li>
<li class="mb-2"><strong class="text-warning">PENTING:</strong> Tombol "Cari dari Alamat" HANYA untuk alamat umum seperti "Jember" atau "Sumbersari", BUKAN untuk nama perumahan spesifik.</li>
<li class="mb-2"><strong>Alternatif:</strong> Copy koordinat dari Google Maps, lalu ketik manual di form (marker otomatis pindah)</li>
<li class="mb-2">Koordinat bisa input manual, marker otomatis pindah!</li>
<li class="mb-2">Jarak ke kampus akan otomatis dihitung setelah koordinat terisi</li>
<li>Nomor WhatsApp memudahkan calon penyewa untuk menghubungi</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// ========== KOORDINAT KAMPUS POLIJE ==========
const KAMPUS_LAT = -8.15981;
const KAMPUS_LNG = 113.72312;
// ========== MAP INITIALIZATION ==========
const map = L.map('map').setView([KAMPUS_LAT, KAMPUS_LNG], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
// Marker kampus (optional - untuk referensi)
const kampusIcon = L.divIcon({
className: 'custom-marker',
html: '<div style="background: #dc3545; color: white; padding: 5px 10px; border-radius: 5px; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3);">🏫 KAMPUS</div>',
iconSize: [80, 30],
iconAnchor: [40, 15]
});
L.marker([KAMPUS_LAT, KAMPUS_LNG], { icon: kampusIcon }).addTo(map);
let marker;
let isUpdatingFromInput = false; // Flag untuk prevent loop
// ========== FUNCTION: REVERSE GEOCODING (KOORDINAT → ALAMAT) - BARU! ==========
function reverseGeocode(lat, lng) {
const reverseGeocodeURL = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
fetch(reverseGeocodeURL)
.then(response => response.json())
.then(data => {
if (data && data.display_name) {
document.getElementById('alamat').value = data.display_name;
console.log('✅ Alamat berhasil di-update:', data.display_name);
}
})
.catch(error => {
console.error('⚠️ Reverse geocoding error:', error);
});
}
// ========== FUNCTION: ADD/UPDATE MARKER ==========
function addMarker(lat, lng, shouldReverseGeocode = true) {
if (marker) {
marker.setLatLng([lat, lng]);
} else {
marker = L.marker([lat, lng], {
draggable: true
}).addTo(map);
// Event: Drag marker
marker.on('dragend', function(e) {
const position = marker.getLatLng();
updateCoordinates(position.lat, position.lng);
calculateDistance(position.lat, position.lng);
reverseGeocode(position.lat, position.lng); // Auto update alamat
});
}
map.setView([lat, lng], 15);
updateCoordinates(lat, lng);
calculateDistance(lat, lng);
// Reverse geocoding (koordinat → alamat)
if (shouldReverseGeocode) {
reverseGeocode(lat, lng);
}
}
// ========== FUNCTION: UPDATE COORDINATES INPUT ==========
function updateCoordinates(lat, lng) {
if (!isUpdatingFromInput) {
document.getElementById('latitude').value = lat.toFixed(8);
document.getElementById('longitude').value = lng.toFixed(8);
}
}
// ========== FUNCTION: VALIDATE COORDINATES - BARU! ==========
function validateCoordinates(lat, lng) {
lat = parseFloat(lat);
lng = parseFloat(lng);
if (isNaN(lat) || isNaN(lng)) {
return { valid: false, message: '❌ Koordinat harus berupa angka!' };
}
if (lat < -90 || lat > 90) {
return { valid: false, message: '❌ Latitude harus antara -90 sampai 90!' };
}
if (lng < -180 || lng > 180) {
return { valid: false, message: '❌ Longitude harus antara -180 sampai 180!' };
}
return { valid: true, lat: lat, lng: lng };
}
// ========== FUNCTION: CALCULATE DISTANCE (HAVERSINE FORMULA) ==========
function calculateDistance(lat, lng) {
const R = 6371e3; // Earth radius in meters
const φ1 = KAMPUS_LAT * Math.PI / 180;
const φ2 = lat * Math.PI / 180;
const Δφ = (lat - KAMPUS_LAT) * Math.PI / 180;
const Δλ = (lng - KAMPUS_LNG) * Math.PI / 180;
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const distance = Math.round(R * c); // Distance in meters
document.getElementById('jarak').value = distance;
// Update preview
updatePreview();
}
// ========== INPUT MANUAL KOORDINAT → UPDATE PETA (FITUR BARU!) ==========
const latInput = document.getElementById('latitude');
const lngInput = document.getElementById('longitude');
let inputTimeout;
function handleCoordinateInput() {
clearTimeout(inputTimeout);
inputTimeout = setTimeout(() => {
const lat = latInput.value;
const lng = lngInput.value;
if (lat && lng) {
const validation = validateCoordinates(lat, lng);
if (validation.valid) {
isUpdatingFromInput = true;
addMarker(validation.lat, validation.lng, true); // true = reverse geocode
isUpdatingFromInput = false;
console.log('✅ Marker dipindahkan ke:', validation.lat, validation.lng);
} else {
alert(validation.message);
}
}
}, 800); // Delay 800ms untuk avoid spam
}
latInput.addEventListener('input', handleCoordinateInput);
lngInput.addEventListener('input', handleCoordinateInput);
// ========== GEOCODING: ALAMAT → KOORDINAT (IMPROVED!) ==========
document.getElementById('geocodeBtn').addEventListener('click', function() {
const alamat = document.getElementById('alamat').value.trim();
if (!alamat) {
alert('⚠️ Mohon isi alamat lengkap terlebih dahulu!');
document.getElementById('alamat').focus();
return;
}
const button = this;
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Mencari koordinat...';
// Fungsi untuk mencari koordinat
function tryGeocode(searchQuery) {
const geocodeURL = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&countrycodes=id`;
return fetch(geocodeURL)
.then(response => response.json())
.then(data => {
if (data && data.length > 0) {
// Filter hasil yang paling relevan (prioritaskan yang ada "jember")
let bestMatch = data[0];
const jemberMatch = data.find(item =>
item.display_name.toLowerCase().includes('jember')
);
if (jemberMatch) bestMatch = jemberMatch;
return bestMatch;
}
return null;
});
}
// STRATEGI 1: Cari dengan alamat lengkap
tryGeocode(alamat)
.then(result => {
if (result) {
return result;
}
// STRATEGI 2: Ambil kata kunci penting (kecamatan + kota)
const parts = alamat.split(',').map(s => s.trim());
if (parts.length >= 2) {
// Coba: Kecamatan, Kota
const simplifiedQuery = `${parts[parts.length - 2]}, ${parts[parts.length - 1]}`;
return tryGeocode(simplifiedQuery);
}
return null;
})
.then(result => {
if (result) {
return result;
}
// STRATEGI 3: Cari hanya nama kota/kabupaten
const cityMatch = alamat.match(/jember|sumbersari/i);
if (cityMatch) {
return tryGeocode('Jember, Jawa Timur, Indonesia');
}
return null;
})
.then(result => {
button.disabled = false;
button.innerHTML = originalHTML;
if (result) {
const lat = parseFloat(result.lat);
const lng = parseFloat(result.lon);
addMarker(lat, lng, false);
// Success notification dengan info lengkap
alert('✅ Koordinat berhasil ditemukan!\n\n' +
'📍 Lokasi ditemukan: ' + result.display_name + '\n' +
'🌍 Koordinat: ' + lat.toFixed(6) + ', ' + lng.toFixed(6) + '\n' +
'📏 Jarak ke kampus: ' + document.getElementById('jarak').value + ' meter\n\n' +
'💡 Anda bisa drag marker untuk penyesuaian lokasi yang lebih tepat!');
} else {
// Gagal semua strategi
alert('❌ Alamat tidak ditemukan di peta!\n\n' +
'💡 Solusi Alternatif:\n\n' +
'1⃣ CARA TERMUDAH: Klik langsung pada PETA di lokasi yang Anda inginkan\n' +
' → Alamat akan otomatis terisi!\n\n' +
'2⃣ Sederhanakan alamat:\n' +
' • Dari: "' + alamat + '"\n' +
' • Jadi: "Sumbersari, Jember, Jawa Timur"\n' +
' • Atau: "Jember, Jawa Timur"\n\n' +
'3⃣ Gunakan GPS:\n' +
' • Klik "Deteksi Lokasi Saya" jika Anda di lokasi\n\n' +
'4⃣ Input Manual:\n' +
' • Ketik koordinat latitude & longitude langsung\n' +
' • Contoh: Latitude: -8.159917, Longitude: 113.722750');
}
})
.catch(error => {
button.disabled = false;
button.innerHTML = originalHTML;
console.error('Geocoding error:', error);
alert('⚠️ Terjadi kesalahan saat mencari koordinat.\n\n' +
'🎯 SOLUSI CEPAT:\n' +
'• Klik langsung pada PETA (paling mudah!)\n' +
'• Atau gunakan "Deteksi Lokasi Saya"\n' +
'• Atau ketik koordinat manual');
});
});
// ========== DETECT USER LOCATION (GPS) ==========
document.getElementById('detectLocation').addEventListener('click', function() {
const button = this;
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Mendeteksi lokasi...';
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
addMarker(lat, lng, true); // true = reverse geocode
button.disabled = false;
button.innerHTML = originalHTML;
alert('✅ Lokasi GPS berhasil terdeteksi!\n\n' +
'🌍 Koordinat: ' + lat.toFixed(6) + ', ' + lng.toFixed(6) + '\n' +
'📏 Jarak ke kampus: ' + document.getElementById('jarak').value + ' meter\n' +
'📍 Alamat otomatis di-update!');
},
function(error) {
button.disabled = false;
button.innerHTML = originalHTML;
let errorMsg = '❌ Gagal mendeteksi lokasi GPS.\n\n';
switch(error.code) {
case error.PERMISSION_DENIED:
errorMsg += '🚫 Izin lokasi ditolak.\n\n' +
'💡 Solusi:\n' +
'1. Klik ikon gembok/info di address bar\n' +
'2. Izinkan akses lokasi\n' +
'3. Refresh halaman dan coba lagi';
break;
case error.POSITION_UNAVAILABLE:
errorMsg += '📡 Informasi lokasi tidak tersedia.\n\n' +
'💡 Coba gunakan "Cari dari Alamat" sebagai gantinya.';
break;
case error.TIMEOUT:
errorMsg += '⏱️ Waktu permintaan habis.\n\n' +
'💡 Coba lagi atau gunakan cara lain.';
break;
default:
errorMsg += '⚠️ Terjadi kesalahan yang tidak diketahui.';
}
alert(errorMsg);
}
);
} else {
button.disabled = false;
button.innerHTML = originalHTML;
alert('❌ Browser Anda tidak mendukung Geolocation.\n\n' +
'💡 Gunakan "Cari dari Alamat" atau klik pada peta.');
}
});
// ========== CLICK ON MAP → AUTO UPDATE ALAMAT (FITUR BARU!) ==========
map.on('click', function(e) {
addMarker(e.latlng.lat, e.latlng.lng, true); // true = reverse geocode
});
// ========== WHATSAPP VALIDATION ==========
const whatsappInput = document.getElementById('no_whatsapp');
whatsappInput.addEventListener('input', function(e) {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value.length > 20) {
this.value = this.value.substring(0, 20);
}
});
whatsappInput.addEventListener('blur', function() {
const value = this.value.trim();
if (value) {
if (!value.startsWith('08') && !value.startsWith('628') && !value.startsWith('62')) {
alert('⚠️ Format nomor WhatsApp tidak valid.\n\nGunakan format: 08xxx atau 628xxx');
this.focus();
}
if (value.length < 10) {
alert('⚠️ Nomor WhatsApp terlalu pendek.\n\nMinimal 10 digit.');
this.focus();
}
}
});
// ========== LIVE PREVIEW ==========
const form = document.getElementById('kontrakanForm');
const previewSection = document.getElementById('previewSection');
const namaInput = document.getElementById('nama');
const hargaInput = document.getElementById('harga');
const jumlah_kamarInput = document.getElementById('jumlah_kamar');
const bathroomCountInput = document.getElementById('bathroom_count');
const jarakInput = document.getElementById('jarak');
function updatePreview() {
const nama = namaInput ? namaInput.value : '';
const harga = hargaInput ? hargaInput.value : '';
const jumlah_kamar = jumlah_kamarInput ? jumlah_kamarInput.value : '';
const bathroomCount = bathroomCountInput ? bathroomCountInput.value : '';
const jarak = jarakInput ? jarakInput.value : '';
if (nama || harga || jumlah_kamar || bathroomCount || jarak) {
previewSection.style.display = 'block';
document.getElementById('previewNama').textContent = nama || '-';
document.getElementById('previewHarga').textContent = harga ? 'Rp ' + parseInt(harga).toLocaleString('id-ID') + '/tahun' : '-';
document.getElementById('previewjumlah_kamar').textContent = jumlah_kamar ? jumlah_kamar + ' kamar' : '-';
document.getElementById('previewBathroomCount').textContent = bathroomCount ? bathroomCount + ' kamar mandi' : '-';
document.getElementById('previewJarak').textContent = jarak ? jarak + ' meter (' + (jarak/1000).toFixed(2) + ' km)' : '-';
} else {
previewSection.style.display = 'none';
}
}
if (namaInput) namaInput.addEventListener('input', updatePreview);
if (hargaInput) hargaInput.addEventListener('input', updatePreview);
if (jumlah_kamarInput) jumlah_kamarInput.addEventListener('input', updatePreview);
if (bathroomCountInput) bathroomCountInput.addEventListener('input', updatePreview);
if (jarakInput) jarakInput.addEventListener('input', updatePreview);
// ========== FOTO PREVIEW ==========
const fotoInput = document.getElementById('foto');
const previewFoto = document.getElementById('preview-foto');
const previewContainer = document.getElementById('preview-foto-container');
fotoInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
if (file.size > 2097152) {
alert('❌ Ukuran file maksimal 2MB!');
this.value = '';
previewContainer.style.display = 'none';
return;
}
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
alert('❌ Hanya file JPG, PNG, dan JPEG yang diperbolehkan!');
this.value = '';
previewContainer.style.display = 'none';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
previewFoto.src = e.target.result;
previewContainer.style.display = 'block';
}
reader.readAsDataURL(file);
} else {
previewContainer.style.display = 'none';
}
});
window.removeImage = function() {
fotoInput.value = '';
previewContainer.style.display = 'none';
previewFoto.src = '';
}
// ========== FORM VALIDATION ==========
form.addEventListener('submit', function(e) {
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
// Validasi koordinat
const validation = validateCoordinates(lat, lng);
if (!validation.valid) {
e.preventDefault();
alert(validation.message);
return;
}
// Validasi WhatsApp
const waValue = whatsappInput.value.trim();
if (waValue) {
if (waValue.length < 10) {
e.preventDefault();
alert('❌ Nomor WhatsApp minimal 10 digit!');
whatsappInput.focus();
return;
}
if (!waValue.startsWith('08') && !waValue.startsWith('628') && !waValue.startsWith('62')) {
e.preventDefault();
alert('❌ Format nomor WhatsApp tidak valid.\n\nGunakan: 08xxx atau 628xxx');
whatsappInput.focus();
return;
}
}
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
form.classList.add('was-validated');
});
// ========== LIVE PREVIEW HARGA ==========
const hargaPreview = document.getElementById('harga_preview');
const hargaPreviewText = document.getElementById('harga_preview_text');
function terbilangSingkat(angka) {
if (!angka || angka <= 0) return '';
const m = Math.floor(angka / 1000000);
const rb = Math.floor((angka % 1000000) / 1000);
const s = angka % 1000;
let parts = [];
if (m > 0) parts.push(m + ' Juta');
if (rb > 0) parts.push(rb + ' Ribu');
if (s > 0) parts.push(s + ' Rupiah');
return parts.join(' ') || '0';
}
function formatRibu(angka) {
return parseInt(angka).toLocaleString('id-ID');
}
function updateHargaPreview() {
const val = parseInt(hargaInput.value);
if (val > 0) {
hargaPreviewText.textContent = 'Rp ' + formatRibu(val) + ' — ' + terbilangSingkat(val);
hargaPreview.style.display = 'block';
} else {
hargaPreview.style.display = 'none';
}
}
if (hargaInput) hargaInput.addEventListener('input', updateHargaPreview);
// Tampilkan saat load jika sudah ada nilai
if (hargaInput && hargaInput.value) updateHargaPreview();
});
</script>
<style>
.form-control:focus,
.form-select:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
}
.input-group-text {
transition: all 0.3s ease;
}
.form-control:focus + .input-group-text,
.input-group:focus-within .input-group-text {
border-color: #86b7fe;
background-color: #e7f1ff;
}
.card {
transition: box-shadow 0.3s ease;
}
.was-validated .form-control:invalid {
border-color: #dc3545;
}
.was-validated .form-control:valid {
border-color: #198754;
}
#preview-foto-container {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.leaflet-container {
font-family: inherit;
}
.leaflet-popup-content-wrapper {
border-radius: 8px;
}
#no_whatsapp:focus {
border-color: #25D366 !important;
box-shadow: 0 0 0 0.25rem rgba(37, 211, 102, 0.15) !important;
}
#no_whatsapp.is-valid {
border-color: #25D366;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2325D366' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
}
/* Loading animation */
.spinner-border {
width: 1rem;
height: 1rem;
border-width: 0.15em;
}
/* Custom marker kampus */
.custom-marker {
background: none;
border: none;
}
/* Input koordinat style - BARU! */
#latitude:focus,
#longitude:focus {
border-color: #ffc107 !important;
box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.15) !important;
}
/* Highlight saat input manual aktif - BARU! */
#latitude:not([readonly]):hover,
#longitude:not([readonly]):hover {
background-color: #fffbf0;
cursor: text;
}
/* Badge animation */
.badge {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Smooth transitions */
.form-control,
.btn {
transition: all 0.3s ease;
}
/* Alert styling */
.alert-info {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-left: 4px solid #2196f3;
}
/* Map container styling */
#map {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
#map:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
/* Button hover effects */
#geocodeBtn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(25, 135, 84, 0.3);
}
#detectLocation:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(13, 110, 253, 0.3);
}
/* Responsive adjustments */
@media (max-width: 768px) {
#map {
height: 300px !important;
}
.alert ul {
padding-left: 1.2rem;
}
.badge {
font-size: 0.7rem;
}
}
/* Input number remove spinner arrows (optional) */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
opacity: 0.5;
}
input[type="number"]:hover::-webkit-inner-spin-button,
input[type="number"]:hover::-webkit-outer-spin-button {
opacity: 1;
}
/* Success state for coordinates */
#latitude.is-valid,
#longitude.is-valid {
border-color: #198754;
background-color: #f0fff4;
}
/* Warning state for manual input */
#latitude:not([readonly]),
#longitude:not([readonly]) {
border-left: 3px solid #ffc107;
}
/* Tooltip style */
small.text-muted {
font-size: 0.8rem;
display: block;
margin-top: 0.25rem;
}
/* Preview section animation */
#previewSection {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 200px;
transform: translateY(0);
}
}
/* Button loading state */
.btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Card info styling */
.card.bg-light {
border-left: 4px solid #0d6efd;
}
/* Input group icon colors */
.input-group-text i.bi-building {
color: #0d6efd;
}
.input-group-text i.bi-geo-alt {
color: #dc3545;
}
.input-group-text i.bi-geo {
color: #dc3545;
}
.input-group-text i.bi-telephone {
color: #25D366;
}
.input-group-text i.bi-currency-dollar {
color: #198754;
}
.input-group-text i.bi-rulers {
color: #ffc107;
}
.input-group-text i.bi-pin-map {
color: #0dcaf0;
}
.input-group-text i.bi-star {
color: #ffc107;
}
.input-group-text i.bi-image {
color: #0d6efd;
}
/* Smooth scroll behavior */
html {
scroll-behavior: smooth;
}
/* Focus visible for accessibility */
*:focus-visible {
outline: 2px solid #0d6efd;
outline-offset: 2px;
}
/* Print styles */
@media print {
.btn,
.breadcrumb,
nav,
#map {
display: none !important;
}
.card {
box-shadow: none !important;
border: 1px solid #dee2e6 !important;
}
}
</style>
@endsection