diff --git a/app.py b/app.py index acb196e..f1b4f44 100644 --- a/app.py +++ b/app.py @@ -1,65 +1,45 @@ from flask import Flask, request, jsonify -from flask_cors import CORS -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.naive_bayes import MultinomialNB -import numpy as np +from groq import Groq app = Flask(__name__) -CORS(app) # Agar bisa diakses dari HTML berbeda domain/port -# 1. DATA LATIH (Training Data) -# Kita ajari bot dengan contoh kalimat dan labelnya -data_latih = [ - ("halo", "salam"), - ("selamat pagi", "salam"), - ("berapa harga rumah", "harga"), - ("biaya cicilan mahal gak", "harga"), - ("saya mau kredit rumah", "kpr"), - ("proses KPR gimana", "kpr"), - ("lokasi kantor dimana", "lokasi"), - ("alamat properti di mana", "lokasi") -] +client = Groq( + api_key="API_KEY_GROQ_KAMU" +) -texts, labels = zip(*data_latih) - -# 2. PROSES TRAINING (Mesin Belajar) -# Mengubah teks jadi angka (Vector) -vectorizer = TfidfVectorizer() -X = vectorizer.fit_transform(texts) -y = np.array(labels) - -# Melatih model Naive Bayes -model = MultinomialNB() -model.fit(X, y) - -# 3. API ENDPOINT (Tempat Browser Menghubungi) @app.route('/chat', methods=['POST']) def chat(): - user_input = request.json.get('message') - - # Ubah input user jadi vektor sesuai pola training - user_vector = vectorizer.transform([user_input]) - - # Prediksi intent - prediksi = model.predict(user_vector)[0] - confidence = max(model.predict_proba(user_vector)[0]) - - # Logika Respon Berdasarkan Hasil Prediksi ML - respon = "" - if confidence < 0.5: # Jika model kurang yakin - respon = "Maaf, saya kurang mengerti. Bisa diulang dengan bahasa lain?" - elif prediksi == 'salam': - respon = "Halo! Selamat datang di PropertiHarmoni. Ada yang bisa dibantu? 😊" - elif prediksi == 'harga': - respon = "Harga kami bervariasi mulai 500jt hingga 3M. Mau cari di range berapa?" - elif prediksi == 'kpr': - respon = "Untuk KPR, kami kerjasama dengan BCA, Mandiri, dan BNI. Mau simulasi cicilan?" - elif prediksi == 'lokasi': - respon = "Kantor kami ada di Jl. Raya Malang No. 10. Mau kirim Google Maps-nya?" - else: - respon = "Mohon maaf, tim kami akan segera membalas pertanyaan spesifik Anda." - return jsonify({'response': respon, 'intent': prediksi}) + data = request.json + user_message = data.get("message") -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + completion = client.chat.completions.create( + model="llama3-8b-8192", + messages=[ + { + "role": "system", + "content": """ + Kamu adalah CaraniBot, asisten properti yang ramah, santai, dan profesional. + Jawab dengan bahasa Indonesia natural. + """ + }, + { + "role": "user", + "content": user_message + } + ] + ) + + reply = completion.choices[0].message.content + + return jsonify({ + "reply": reply + }) + + +if __name__ == "__main__": + app.run( + host="127.0.0.1", + port=5000, + debug=True + ) \ No newline at end of file diff --git a/app/Helpers/StatusHelper.php b/app/Helpers/StatusHelper.php new file mode 100644 index 0000000..c1f3133 --- /dev/null +++ b/app/Helpers/StatusHelper.php @@ -0,0 +1,36 @@ +update([ + 'status_transaksi' => $statusTransaksi, + ]); + + // UPDATE PEMESANAN + if ($transaksi->pemesanan) { + + $statusPemesanan = 'Proses'; + + if ( + $statusTransaksi == 'berhasil' || + $statusTransaksi == 'selesai' + ) { + $statusPemesanan = 'Selesai'; + } + + elseif ($statusTransaksi == 'ditolak') { + $statusPemesanan = 'Ditolak'; + } + + $transaksi->pemesanan->update([ + 'status' => $statusPemesanan, + 'tahap_saat_ini' => $tahap, + ]); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/GoogleController.php b/app/Http/Controllers/Auth/GoogleController.php new file mode 100644 index 0000000..5de9c17 --- /dev/null +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -0,0 +1,52 @@ +redirect(); + } + + // 2. Callback dari Google + public function callback() + { + $googleUser = Socialite::driver('google')->user(); + + // cari user + $user = User::where('email', $googleUser->email)->first(); + + // kalau belum ada, buat baru + if (!$user) { + $user = User::create([ + 'name' => $googleUser->name, + 'email' => $googleUser->email, + 'password' => bcrypt(Str::random(16)), + 'google_avatar' => $googleUser->avatar, + 'profile_photo' => null, + ]); + } else { + // update avatar setiap login Google + $user->update([ + 'name' => $googleUser->name, + 'google_avatar' => $googleUser->avatar, + ]); + } + + // 🔥 INI PENTING: login ke session Laravel + Auth::login($user); + + // regenerate session biar navbar update + session()->regenerate(); + + return redirect('/'); // atau route home kamu + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 5799e2b..575ab63 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -5,47 +5,45 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Laravel\Socialite\Facades\Socialite; use App\Models\User; +use App\Models\LogAktivitas; class AuthController extends Controller { - // Tampilkan form login + // ========================= + // HALAMAN LOGIN + // ========================= public function index() { return view('login'); } - // Proses login - // AuthController.php - + // ========================= + // LOGIN MANUAL + // ========================= public function login(Request $request) { $request->validate([ - 'email' => 'required|email', + 'email' => 'required|email', 'password' => 'required', - ], [ - 'email.required' => 'Email wajib diisi', - 'email.email' => 'Format email tidak valid', - 'password.required' => 'Password wajib diisi', ]); $user = User::where('email_user', $request->email)->first(); - // AuthController.php - method login() - if ($user && Hash::check($request->password, $user->password_user)) { - + Auth::login($user); $request->session()->regenerate(); - // 🔥 AMBIL PARAMETER 'redirect' (pakai input() lebih reliable) - $redirectUrl = $request->input('redirect'); + // LOG AKTIVITAS LOGIN + LogAktivitas::create([ + 'id_user' => $user->id_user, + 'aktivitas' => 'Login ke sistem', + 'icon' => 'fa-right-to-bracket', + 'tipe' => 'login' + ]); - if ($redirectUrl && str_starts_with($redirectUrl, '/')) { - return redirect($redirectUrl); - } - - // Fallback jika redirect tidak valid / tidak ada if ($user->role_user === 'admin') { return redirect()->route('admin.welcome'); } @@ -54,92 +52,156 @@ public function login(Request $request) } return back()->withErrors([ - 'email' => 'Email atau password yang Anda masukkan salah.', + 'email' => 'Email atau password salah.' ])->withInput($request->except('password')); } - // Tampilkan form ganti password + // ========================= + // GOOGLE LOGIN + // ========================= + public function redirectToGoogle() + { + return Socialite::driver('google')->redirect(); + } + + public function handleGoogleCallback() + { + try { + + $googleUser = Socialite::driver('google')->user(); + + $user = User::where('email_user', $googleUser->email)->first(); + + if (!$user) { + + $user = User::create([ + 'nama_user' => $googleUser->name, + 'email_user' => $googleUser->email, + 'password_user' => bcrypt('google-login'), + 'role_user' => 'customer', + 'google_id' => $googleUser->id, + 'google_avatar' => $googleUser->avatar, + ]); + + } else { + + $user->update([ + 'google_id' => $googleUser->id, + 'google_avatar' => $googleUser->avatar, + ]); + } + + Auth::login($user); + request()->session()->regenerate(); + + // LOG AKTIVITAS GOOGLE LOGIN + LogAktivitas::create([ + 'id_user' => $user->id_user, + 'aktivitas' => 'Login menggunakan Google', + 'icon' => 'fa-google', + 'tipe' => 'login' + ]); + + return redirect()->route('halaman-katalog'); + + } catch (\Exception $e) { + + return redirect()->route('login')->with('error', 'Login Google gagal.'); + } + } + + // ========================= + // FORM GANTI PASSWORD + // ========================= public function formGantiPassword(Request $request) { $email = $request->query('email'); + return view('gantipass', compact('email')); } - // Proses update password + // ========================= + // UPDATE PASSWORD + // ========================= public function updatePassword(Request $request) { $request->validate([ - 'email' => 'required|email|exists:users,email_user', - 'password_baru' => 'required|min:8|confirmed', - ], [ - 'email.exists' => 'Email tidak terdaftar dalam sistem.', - 'password_baru.min' => 'Password minimal 8 karakter.', - 'password_baru.confirmed' => 'Konfirmasi password tidak cocok.', + 'email' => 'required|email|exists:user,email_user', + 'password_baru' => 'required|min:8|confirmed', ]); $user = User::where('email_user', $request->email)->first(); if ($user) { + $user->update([ 'password_user' => Hash::make($request->password_baru) ]); } - return redirect()->route('login') - ->with('success', 'Password berhasil diubah! Silakan login dengan password baru.'); + return redirect()->route('login')->with('success', 'Password berhasil diubah.'); } - // Logout + // ========================= + // LOGOUT + // ========================= public function logout(Request $request) { - $role = Auth::user()->role_user; // ← cek role sebelum logout - + $user = Auth::user(); + + // LOG SEBELUM LOGOUT + if ($user) { + LogAktivitas::create([ + 'id_user' => $user->id_user, + 'aktivitas' => 'Logout dari sistem', + 'icon' => 'fa-right-from-bracket', + 'tipe' => 'logout' + ]); + } + + $role = $user->role_user ?? null; + Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); - - // Redirect berdasarkan role + if ($role === 'admin') { - return redirect()->route('login'); // ← admin ke login + return redirect()->route('login'); } - - return redirect()->route('welcome'); // ← pengguna ke welcome + + return redirect()->route('welcome'); } + // ========================= // REGISTER + // ========================= public function register(Request $request) { - // 🔥 1. VALIDASI INPUT - $validated = $request->validate([ - 'nama_lengkap' => 'required|string|max:255', - 'email_user' => 'required|email|unique:users,email_user', - 'no_hp' => 'required|string|max:20', - 'pekerjaan' => 'nullable|string|max:100', + $request->validate([ + 'nama_user' => 'required', + 'email_user' => 'required|email|unique:user,email_user', + 'no_hp' => 'required', 'password_user' => 'required|min:8|confirmed', - ], [ - // Custom error messages (opsional tapi disarankan) - 'nama_lengkap.required' => 'Nama lengkap wajib diisi', - 'email_user.required' => 'Email wajib diisi', - 'email_user.email' => 'Format email tidak valid', - 'email_user.unique' => 'Email sudah terdaftar', - 'no_hp.required' => 'Nomor HP wajib diisi', - 'password_user.required' => 'Password wajib diisi', - 'password_user.min' => 'Password minimal 8 karakter', - 'password_user.confirmed' => 'Konfirmasi password tidak cocok', ]); - // 🔥 2. SIMPAN USER KE DATABASE + $role = ($request->email_user === 'admin@gmail.com') ? 'admin' : 'customer'; + $user = User::create([ - 'nama_user' => $validated['nama_lengkap'], - 'email_user' => $validated['email_user'], - 'no_hp' => $validated['no_hp'], - 'pekerjaan' => $validated['pekerjaan'] ?? null, - 'password_user' => Hash::make($validated['password_user']), - 'role_user' => 'user', // default role + 'nama_user' => $request->nama_user, + 'email_user' => $request->email_user, + 'no_hp' => $request->no_hp, + 'password_user' => bcrypt($request->password_user), + 'role_user' => $role, ]); - // 🔥 3. REDIRECT KE WELCOME DENGAN PESAN SUKSES - return redirect()->route('welcome') - ->with('success', 'Akun berhasil dibuat! Silakan login.'); + // LOG REGISTER + LogAktivitas::create([ + 'id_user' => $user->id_user, + 'aktivitas' => 'Mendaftar akun baru', + 'icon' => 'fa-user-plus', + 'tipe' => 'register' + ]); + + return redirect()->route('login')->with('success', 'Register berhasil.'); } } \ No newline at end of file diff --git a/app/Http/Controllers/BlokController.php b/app/Http/Controllers/BlokController.php new file mode 100644 index 0000000..b68fcf5 --- /dev/null +++ b/app/Http/Controllers/BlokController.php @@ -0,0 +1,51 @@ +get(); + + return view('admin.kelola-blok', compact('perumahan', 'blok')); + } + + public function store(Request $request, $id) + { + Blok::create([ + 'id_perumahan' => $id, + 'nama_blok' => $request->nama_blok, + 'jumlah_unit' => $request->jumlah_unit, + ]); + + return back()->with('success', 'Blok berhasil ditambahkan'); + } + + public function update(Request $request, $id) + { + $blok = Blok::findOrFail($id); + $blok->update([ + 'nama_blok' => $request->nama_blok, + ]); + + return back()->with('success', 'Blok berhasil diupdate'); + } + + public function destroy($id) + { + Blok::findOrFail($id)->delete(); + return back()->with('success', 'Blok berhasil dihapus'); + } + + // NGAMBIL DATA BLOK SESUAI DENGAN PERUMAHAN (DI HALAMAN TAMBAH RUMAH) + public function getByPerumahan($id) +{ + return Blok::where('id_perumahan', $id)->get(); +} +} \ No newline at end of file diff --git a/app/Http/Controllers/ChatbotController.php b/app/Http/Controllers/ChatbotController.php index 2765800..9398c22 100644 --- a/app/Http/Controllers/ChatbotController.php +++ b/app/Http/Controllers/ChatbotController.php @@ -3,69 +3,397 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Auth; +use App\Models\ChatSession; +use App\Models\ChatbotMessage; -class ChatBotController extends Controller +class ChatbotController extends Controller { - public function sendMessage(Request $request) + // ========================= + // HALAMAN CHATBOT + // ========================= + public function index() { - $message = strtolower(trim($request->input('message'))); - - // Database pengetahuan (bisa nanti dipindah ke database MySQL jika mau) - $responses = [ - [ - 'keywords' => ['halo', 'hai', 'hello', 'pagi', 'siang', 'malam'], - 'reply' => "Halo kak! 👋 Selamat datang di Carani Estate. Ada yang bisa dibantu hari ini?", - 'options' => ["Cari Rumah", "Info KPR", "Lokasi Kantor"] - ], - [ - 'keywords' => ['harga', 'biaya', 'mahal', 'murah', 'budget', 'dp'], - 'reply' => "Untuk harga, kami punya banyak pilihan mulai dari Rp 500 jutaan sampai miliaran, Kak. 😊 Kamu lagi cari properti di daerah mana nih?", - 'options' => ["Daerah Malang", "Daerah Batu", "Budget < 1 Milyar"] - ], - [ - 'keywords' => ['kpr', 'kredit', 'bank', 'cicil', 'bunga'], - 'reply' => "Siap! Untuk KPR, bunganya mulai dari 4% fixed tahun pertama lho. 🏦 Mau aku hitungin simulasi cicilannya sekalian?", - 'options' => ["Hitung Simulasi", "Syarat Dokumen"] - ], - [ - 'keywords' => ['lokasi', 'alamat', 'dimana', 'tempat', 'kantornya'], - 'reply' => "Kantor pemasaran kami ada di pusat kota, strategis banget! 📍 Mau dikirim lokasi Google Maps-nya?", - 'options' => ["Kirim Peta", "Jadwalkan Kunjungan"] - ], - [ - 'keywords' => ['tipe', 'jenis', 'rumah', 'apartemen', 'spesifikasi', 'luas'], - 'reply' => "Kami punya tipe Minimalis Modern dan Industrial Kekinian. Keduanya udah include kanopi & pagar loh! 🏠 Kamu lebih suka yang mana?", - 'options' => ["Lihat Foto Tipe A", "Lihat Foto Tipe B"] - ] - ]; - - $bestMatch = null; - $highestScore = 0; - - // Algoritma pencocokan sederhana (Naive Logic) - foreach ($responses as $data) { - $score = 0; - foreach ($data['keywords'] as $keyword) { - if (str_contains($message, $keyword)) { - $score++; - } + $chatSession = ChatSession::with([ + 'messages' => function ($q) { + $q->orderBy('created_at', 'asc'); } - if ($score > $highestScore) { - $highestScore = $score; - $bestMatch = $data; - } - } + ]) + ->where('id_user', auth()->id()) + ->where('status_chat', 'aktif') + ->first(); - if ($bestMatch && $highestScore > 0) { - return response()->json([ - 'reply' => $bestMatch['reply'], - 'options' => $bestMatch['options'] - ]); - } else { - return response()->json([ - 'reply' => "Waduh, maaf Kak, saya belum paham maksudnya 😅. Bisa coba tanya tentang 'Harga', 'KPR', atau 'Lokasi'?", - 'options' => ["Tanya Harga", "Tanya KPR"] - ]); - } + $messages = $chatSession + ? $chatSession->messages + : collect([]); + + return view( + 'halaman-chatbot', + compact( + 'chatSession', + 'messages' + ) + ); } + + + // ========================= + // KIRIM CHAT + // ========================= + public function kirim(Request $request) + { + $pesanUser = $request->input('message'); + + $userId = Auth::id(); + + // ========================= + // CEK SESSION CHAT + // ========================= + $chatSession = DB::table('chat_sessions') + ->where('id_user', $userId) + ->where('status_chat', 'aktif') + ->first(); + + // kalau belum ada session + if (!$chatSession) { + + $sessionId = + DB::table('chat_sessions') + ->insertGetId([ + 'id_user' => $userId, + 'status_chat' => 'aktif', + 'started_at' => now(), + 'ended_at' => now() + ]); + + } else { + + $sessionId = + $chatSession->id_sessions; + session([ + 'chat_session_id' => $sessionId + ]); + } + + + // ========================= + // SIMPAN CHAT USER + // ========================= + DB::table('chat_messages')->insert([ + 'id_sessions' => $sessionId, + 'sender' => 'user', + 'message' => $pesanUser, + 'created_at' => now() + ]); + + + // ========================= + // DEFAULT + // ========================= + $rekomendasiProperti = []; + + $balasan = 'Maaf kak, terjadi kesalahan 😥'; + + + // ========================= + // API KEY + // ========================= + $apiKey = env('GROQ_API_KEY'); + + if (!$apiKey) { + + return response()->json([ + + 'reply' => 'API key tidak ditemukan!', + + 'options' => [], + + 'properti' => [] + ]); + } + + + // ========================= + // DATA PROPERTI + // ========================= + $properti = DB::table('properti') + ->where('status_unit', 'tersedia') + ->select( + 'id_properti', + 'nama_properti', + 'jenis_properti', + 'kategori_properti', + 'tipe_properti', + 'harga_properti', + 'luas_bangunan', + 'luas_tanah', + 'stok_unit' + ) + ->get(); + + $propertiJson = + json_encode($properti); + + + // ========================= + // SYSTEM PROMPT + // ========================= + $systemPrompt = " +Kamu adalah CaraniBot, customer service virtual PT. Carani Bhanu Balakosa. + +Gaya bicara: +- Santai +- Natural +- Ramah +- Seperti admin chat WhatsApp +- Jangan terlalu formal +- Jangan terlalu kaku +- Gunakan emoji seperlunya 😊 +- Panggil user dengan 'kak' jika cocok + +ATURAN PENTING: +- DILARANG menggunakan markdown. +- DILARANG menggunakan tanda bintang seperti * atau **. +- DILARANG membuat bullet list. +- DILARANG membuat format poin-poin. +- Balasan harus seperti chat biasa. +- Gunakan kalimat natural dan mengalir. +- Jangan terlalu panjang. + +Jika user baru menyebut ingin cari rumah/properti: +- Jangan langsung memberi rekomendasi properti. +- Tanya kebutuhan user dulu secara natural. + +Contoh gaya yang BENAR: +'Siap kak 😊 +Boleh tahu budget dan lokasi yang kakak inginkan?' + +Contoh gaya yang SALAH: +'* Budget? * +* Lokasi? *' + +Kamu bisa membantu: +- konsultasi properti +- rekomendasi rumah +- KPR +- subsidi +- lokasi properti +- harga rumah +- proses pembelian +- pertanyaan umum pelanggan + +Data properti: +$propertiJson + +Jika ingin merekomendasikan properti, gunakan format: +[REKOMENDASI:id] + +Contoh: +[REKOMENDASI:1,2] +"; + + + try { + + $client = new \GuzzleHttp\Client(); + + $response = $client->post( + 'https://api.groq.com/openai/v1/chat/completions', + [ + + 'headers' => [ + + 'Content-Type' => 'application/json', + + 'Authorization' => 'Bearer ' . $apiKey + ], + + 'json' => [ + + 'model' => 'llama-3.1-8b-instant', + + 'max_tokens' => 500, + + 'messages' => [ + + [ + 'role' => 'system', + + 'content' => $systemPrompt + ], + + [ + 'role' => 'user', + + 'content' => $pesanUser + ] + ] + ] + ] + ); + + $result = + json_decode( + $response->getBody(), + true + ); + + $balasan = + $result['choices'][0]['message']['content'] + ?? 'Tidak ada balasan'; + + + // ========================= + // CEK TAG REKOMENDASI + // ========================= + if ( + preg_match( + '/\[REKOMENDASI:([\d,]+)\]/', + $balasan, + $matches + ) + ) { + + $ids = + explode(',', $matches[1]); + + $rekomendasiProperti = + DB::table('properti') + ->whereIn( + 'id_properti', + $ids + ) + ->where( + 'status_unit', + 'tersedia' + ) + ->select( + 'id_properti', + 'nama_properti', + 'jenis_properti', + 'kategori_properti', + 'tipe_properti', + 'harga_properti', + 'luas_bangunan', + 'luas_tanah', + 'stok_unit' + ) + ->get() + ->toArray(); + + // hapus tag dari chat + $balasan = trim( + preg_replace( + '/\[REKOMENDASI:[\d,]+\]/', + '', + $balasan + ) + ); + } + + } catch (\Exception $e) { + + $balasan = + 'Maaf kak, server lagi gangguan 😥'; + + $rekomendasiProperti = []; + } + + // ========================= + // SIMPAN BALASAN BOT + // ========================= + DB::table('chat_messages')->insert([ + + 'id_sessions' => $sessionId, + + 'sender' => 'bot', + + 'message' => $balasan, + + 'properti_data' => json_encode($rekomendasiProperti), + + 'created_at' => now() + ]); + + + // ========================= + // RESPONSE JSON + // ========================= + return response()->json([ + 'reply' => $balasan, + 'options' => [], + 'properti' => $rekomendasiProperti + ]); + } + + + // ========================= + // HAPUS RIWAYAT CHAT + // ========================= + public function hapusRiwayat() + { + $userId = auth()->id(); + + $session = DB::table('chat_sessions') + ->where('id_user', $userId) + ->where('status_chat', 'aktif') + ->first(); + + if ($session) { + + DB::table('chat_messages') + ->where('id_sessions', $session->id_sessions) + ->delete(); + + DB::table('chat_sessions') + ->where('id_sessions', $session->id_sessions) + ->delete(); + } + + return response()->json([ + 'success' => true + ]); + } + + // PESAN TERBARU + public function pesanTerbaru(Request $request) +{ + $sessionId = session('chat_session_id'); + + if (!$sessionId) { + + return response()->json([ + 'messages' => [] + ]); + } + + $lastId = $request->last_id ?? 0; + + $messages = \App\Models\ChatbotMessage::where( + 'id_sessions', + $sessionId + ) + ->where( + 'id_messages', + '>', + $lastId + ) + ->orderBy( + 'created_at', + 'asc' + ) + ->get(); + + \Log::info([ + 'session_chatbot' => $sessionId, + 'last_id' => $lastId, + 'jumlah_pesan' => $messages->count(), + 'messages' => $messages->pluck('sender') +]); + + return response()->json([ + 'messages' => $messages + ]); +} } \ No newline at end of file diff --git a/app/Http/Controllers/DokumenController.php b/app/Http/Controllers/DokumenController.php new file mode 100644 index 0000000..91493f4 --- /dev/null +++ b/app/Http/Controllers/DokumenController.php @@ -0,0 +1,196 @@ +validate([ + 'id_transaksi' => 'required|exists:transaksi,id_transaksi', + 'bukti_pembayaran' => 'required|image|mimes:jpg,jpeg,png,pdf|max:5120', + ]); + + // ambil transaksi + $transaksi = Transaksi::findOrFail($request->id_transaksi); + + // simpan file + $file = $request->file('bukti_pembayaran'); + + $namaFile = time() . '_' . $file->getClientOriginalName(); + + $path = $file->storeAs( + 'dokumen/bukti_pembayaran', + $namaFile, + 'public' + ); + + // ========================= + // 1. UPDATE TRANSAKSI (LEGACY SYSTEM tetap jalan) + // ========================= + $transaksi->bukti_transaksi = $namaFile; + $transaksi->status_transaksi = 'menunggu_verifikasi'; + $transaksi->save(); + + // ========================= + // 2. SIMPAN KE DOKUMEN (NEW SYSTEM untuk monitoring) + // ========================= + + Dokumen::create([ + 'id_user' => auth()->id(), + 'id_transaksi' => $transaksi->id_transaksi, + 'id_pemesanan' => $transaksi->pemesanan->id_pemesanan ?? null, + + 'jenis_dokumen' => 'bukti_pembayaran', + 'nama_file' => $namaFile, + 'path_file' => 'dokumen/bukti_pembayaran/'.$namaFile, + 'tipe_file' => $file->getClientMimeType(), + 'ukuran_file' => $file->getSize(), + 'status_verifikasi' => 'pending', + 'sumber_dokumen' => 'pelanggan', + ]); + + // ========================= + // LOG AKTIVITAS + // ========================= + LogAktivitas::create([ + 'id_user' => auth()->id(), + 'aktivitas' => 'Upload bukti pembayaran transaksi #' . $transaksi->id_transaksi, + 'icon' => 'fa-receipt', + 'tipe' => 'transaksi' + ]); + + return back()->with('success', 'Bukti pembayaran berhasil dikirim dan menunggu verifikasi admin.'); + } + + + /** + * ====================================== + * ADMIN - TERIMA BUKTI + * ====================================== + */ + public function approveBukti($id) +{ + $dokumen = Dokumen::findOrFail($id); + + // 1. update dokumen + $dokumen->update([ + 'status_verifikasi' => 'diterima', + 'verified_at' => now() + ]); + + // 2. ambil transaksi + pemesanan + $transaksi = Transaksi::with('pemesanan') + ->find($dokumen->id_transaksi); + + if ($transaksi) { + + // 3. UPDATE STATUS PAKAI SYNC (INI INTI NYA) + StatusHelper::sync( + $transaksi, + 'berhasil', + 'Pelunasan Pembayaran' + ); + + // 4. NOTIFIKASI (JANGAN DIHAPUS) + if ($transaksi->pemesanan) { + + $pemesanan = $transaksi->pemesanan; + + \App\Models\Notifikasi::create([ + 'id_user' => $pemesanan->id_user, + 'judul' => 'Transaksi Berhasil Diselesaikan', + 'pesan' => 'Pembayaran telah diverifikasi. Invoice sekarang tersedia.', + 'tipe' => 'pelunasan', + 'status_baca' => 0, + 'status_kirim' => 'terkirim', + 'channel' => 'in_app', + 'referensi_id' => $transaksi->id_transaksi, + 'referensi_tipe' => 'invoice', + ]); + } + } + + // 5. LOG AKTIVITAS (JUGA TETAP) + LogAktivitas::create([ + 'id_user' => auth()->id(), + 'aktivitas' => 'Menerima bukti pembayaran', + 'icon' => 'fa-check-circle', + 'tipe' => 'transaksi' + ]); + + return back()->with( + 'success', + 'Bukti pembayaran diterima dan transaksi selesai.' + ); +} + + + /** + * ====================================== + * ADMIN - TOLAK BUKTI + * ====================================== + */ + public function rejectBukti($id) +{ + $dokumen = Dokumen::findOrFail($id); + + // 1. update dokumen + $dokumen->update([ + 'status_verifikasi' => 'ditolak', + 'verified_at' => now() + ]); + + $transaksi = Transaksi::with('pemesanan') + ->find($dokumen->id_transaksi); + + if ($transaksi) { + + // 2. sync status (JANGAN MANUAL) + StatusHelper::sync( + $transaksi, + 'perlu_upload_ulang', // atau 'menunggu_verifikasi' + 'Upload Ulang Dokumen' + ); + + // 3. NOTIFIKASI (TETAP ADA) + if ($transaksi->pemesanan) { + + \App\Models\Notifikasi::create([ + 'id_user' => $transaksi->pemesanan->id_user, + 'judul' => 'Bukti Pembayaran Ditolak', + 'pesan' => 'Bukti pembayaran Anda ditolak. Silakan upload ulang dengan data yang benar.', + 'tipe' => 'verifikasi', + 'status_baca' => 0, + 'status_kirim' => 'terkirim', + 'channel' => 'in_app', + 'referensi_id' => $transaksi->id_transaksi, + 'referensi_tipe' => 'transaksi', + ]); + } + } + + // 4. LOG AKTIVITAS (TETAP) + LogAktivitas::create([ + 'id_user' => auth()->id(), + 'aktivitas' => 'Menolak bukti pembayaran', + 'icon' => 'fa-times-circle', + 'tipe' => 'transaksi' + ]); + + return back()->with('success', 'Bukti pembayaran ditolak.'); +} +} \ No newline at end of file diff --git a/app/Http/Controllers/LaporanPenjualanController.php b/app/Http/Controllers/LaporanPenjualanController.php new file mode 100644 index 0000000..394eb02 --- /dev/null +++ b/app/Http/Controllers/LaporanPenjualanController.php @@ -0,0 +1,564 @@ +date_start; + $dateEnd = $request->date_end; + + if ($dateStart && $dateEnd) { + + $query->whereBetween('tanggal_transaksi', [ + $dateStart . ' 00:00:00', + $dateEnd . ' 23:59:59' + ]); + + } + + + + /* + |-------------------------------------------------------------------------- + | FILTER STATUS + |-------------------------------------------------------------------------- + */ + + if ( + $request->filled('status_transaksi') && + $request->status_transaksi != 'all' + ) { + + $query->where( + 'status_transaksi', + $request->status_transaksi + ); + } + + + /* + |-------------------------------------------------------------------------- + | FILTER TIPE PROPERTI + |-------------------------------------------------------------------------- + */ + + if ( + $request->filled('type') && + $request->type != 'all' + ) { + + $query->whereHas('properti', function ($q) use ($request) { + + $q->where( + 'tipe_properti', + $request->type + ); + + }); + } + + + /* + |-------------------------------------------------------------------------- + | SEARCH + |-------------------------------------------------------------------------- + */ + + if ($request->filled('search')) { + + $search = $request->search; + + $query->where(function ($q) use ($search) { + + $q->where( + 'id_transaksi', + 'like', + "%{$search}%" + ) + + ->orWhereHas('user', function ($user) use ($search) { + + $user->where( + 'nama_user', + 'like', + "%{$search}%" + ); + + }) + + ->orWhereHas('properti', function ($properti) use ($search) { + + $properti->where( + 'nama_properti', + 'like', + "%{$search}%" + ); + + }); + + }); + } + + + /* + |-------------------------------------------------------------------------- + | DATA TRANSAKSI + |-------------------------------------------------------------------------- + */ + + $transaksi = $query + ->orderBy('tanggal_transaksi', 'desc') + ->paginate(10); + + + /* + |-------------------------------------------------------------------------- + | CLONE QUERY + |-------------------------------------------------------------------------- + */ + + $summaryQuery = clone $query; + + + /* + |-------------------------------------------------------------------------- + | TOTAL PENDAPATAN + |-------------------------------------------------------------------------- + */ + + $pendapatanBulanIni = (clone $summaryQuery) + ->where('status_transaksi', 'berhasil') + ->sum('total_harga'); + + + /* + |-------------------------------------------------------------------------- + | TOTAL TRANSAKSI + |-------------------------------------------------------------------------- + */ + + $totalTransaksi = (clone $summaryQuery) + ->count(); + + + /* + |-------------------------------------------------------------------------- + | TOTAL PENDING + |-------------------------------------------------------------------------- + */ + + $totalPending = (clone $summaryQuery) + + ->whereIn('status_transaksi', [ + + 'menunggu_verifikasi', + 'menunggu_pembayaran', + 'perlu_upload_ulang' + + ]) + + ->count(); + + + /* + |-------------------------------------------------------------------------- + | TOTAL DITOLAK + |-------------------------------------------------------------------------- + */ + + $totalBatal = (clone $summaryQuery) + + ->where('status_transaksi', 'ditolak') + + ->count(); + + + /* + |-------------------------------------------------------------------------- + | PERSENTASE DUMMY + |-------------------------------------------------------------------------- + */ + + $persenPendapatan = 12.5; + $persenTransaksi = 8.3; + $persenPending = -3.1; + $persenBatal = 1.2; + + + /* + |-------------------------------------------------------------------------- + | CHART BULANAN + |-------------------------------------------------------------------------- + */ + + $labelBulanan = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des' + ]; + + $dataTransaksi = []; + $dataPendapatan = []; + + for ($bulan = 1; $bulan <= 12; $bulan++) { + + // Total transaksi + $totalTransaksiBulanan = Transaksi::whereMonth('tanggal_transaksi', $bulan) + ->whereYear('tanggal_transaksi', date('Y')) + ->count(); + + // Total pendapatan + $totalPendapatanBulanan = Transaksi::whereMonth('tanggal_transaksi', $bulan) + ->whereYear('tanggal_transaksi', date('Y')) + ->where('status_transaksi', 'berhasil') + ->sum('total_harga'); + + $dataTransaksi[] = $totalTransaksiBulanan; + $dataPendapatan[] = $totalPendapatanBulanan; + } + + + /* + |-------------------------------------------------------------------------- + | CHART TAHUNAN + |-------------------------------------------------------------------------- + */ + + $labelTahunan = []; + $dataTahunanTransaksi = []; + $dataTahunanPendapatan = []; + + $tahunSekarang = date('Y'); + for ($tahun = $tahunSekarang - 4; $tahun <= $tahunSekarang; $tahun++) { + $labelTahunan[] = $tahun; + + // Total transaksi tahunan + $totalTransaksiTahunan = Transaksi::whereYear('tanggal_transaksi', $tahun) + ->count(); + + // Total pendapatan tahunan + $totalPendapatanTahunan = Transaksi::whereYear('tanggal_transaksi', $tahun) + ->where('status_transaksi', 'berhasil') + ->sum('total_harga'); + + $dataTahunanTransaksi[] = $totalTransaksiTahunan; + + $dataTahunanPendapatan[] = $totalPendapatanTahunan; + } + + + /* + |-------------------------------------------------------------------------- + | CHART MINGGUAN + |-------------------------------------------------------------------------- + */ + + $chartMingguan = Transaksi::selectRaw(' + WEEK(tanggal_transaksi) as minggu, + COUNT(*) as total + ') + ->whereMonth( + 'tanggal_transaksi', + now()->month + ) + ->whereYear( + 'tanggal_transaksi', + now()->year + ) + ->groupBy('minggu') + ->orderBy('minggu') + ->get(); + + + $labelMingguan = []; + $dataMingguan = []; + + + foreach ($chartMingguan as $item) { + + $labelMingguan[] = + 'Minggu ' . $item->minggu; + + $dataMingguan[] = + $item->total; + } + + + /* + |-------------------------------------------------------------------------- + | CHART STATUS + |-------------------------------------------------------------------------- + */ + + $chartStatus = [ + + 'berhasil' => Transaksi::where( + 'status_transaksi', + 'berhasil' + )->count(), + + 'menunggu_pembayaran' => Transaksi::where( + 'status_transaksi', + 'menunggu_pembayaran' + )->count(), + + 'menunggu_verifikasi' => Transaksi::where( + 'status_transaksi', + 'menunggu_verifikasi' + )->count(), + + 'perlu_upload_ulang' => Transaksi::where( + 'status_transaksi', + 'perlu_upload_ulang' + )->count(), + + 'ditolak' => Transaksi::where( + 'status_transaksi', + 'ditolak' + )->count(), + ]; + + + /* + |-------------------------------------------------------------------------- + | RETURN VIEW + |-------------------------------------------------------------------------- + */ + + return view( + 'admin.laporan_penjualan', + compact( + + 'transaksi', + + 'pendapatanBulanIni', + 'persenPendapatan', + + 'totalTransaksi', + 'persenTransaksi', + + 'totalPending', + 'persenPending', + + 'totalBatal', + 'persenBatal', + + // 'chartPenjualan', + 'chartStatus', + + 'labelBulanan', + 'dataTransaksi', + 'dataPendapatan', + + 'labelTahunan', + 'dataTahunanTransaksi', + 'dataTahunanPendapatan', + + 'labelMingguan', + 'dataMingguan' + ) + ); + } + + + /* + |-------------------------------------------------------------------------- + | EXPORT CSV + |-------------------------------------------------------------------------- + */ + + public function export(Request $request) + { + $query = Transaksi::with([ + 'user', + 'properti' + ]); + + + /* + |-------------------------------------------------------------------------- + | FILTER TANGGAL + |-------------------------------------------------------------------------- + */ + + if ( + $request->filled('date_start') && + $request->filled('date_end') + ) { + + $query->whereBetween( + 'tanggal_transaksi', + [ + $request->date_start . ' 00:00:00', + $request->date_end . ' 23:59:59' + ] + ); + } + + + /* + |-------------------------------------------------------------------------- + | FILTER STATUS + |-------------------------------------------------------------------------- + */ + + if ( + $request->filled('status_transaksi') && + $request->status_transaksi != 'all' + ) { + + $query->where( + 'status_transaksi', + $request->status_transaksi + ); + } + + + /* + |-------------------------------------------------------------------------- + | FILTER TIPE PROPERTI + |-------------------------------------------------------------------------- + */ + + if ( + $request->filled('type') && + $request->type != 'all' + ) { + + $query->whereHas( + 'properti', + function ($q) use ($request) { + + $q->where( + 'tipe_properti', + $request->type + ); + + } + ); + } + + + /* + |-------------------------------------------------------------------------- + | DATA EXPORT + |-------------------------------------------------------------------------- + */ + + $transaksi = $query + ->orderBy( + 'tanggal_transaksi', + 'desc' + ) + ->get(); + + + $filename = + 'laporan_penjualan_' . + now()->format('Ymd_His') . + '.csv'; + + + $headers = [ + + 'Content-Type' => + 'text/csv', + + 'Content-Disposition' => + "attachment; filename=$filename", + + ]; + + + $callback = function () use ($transaksi) { + + $file = fopen( + 'php://output', + 'w' + ); + + + /* + |-------------------------------------------------------------------------- + | HEADER CSV + |-------------------------------------------------------------------------- + */ + + fputcsv($file, [ + + 'ID Transaksi', + 'Tanggal', + 'Pembeli', + 'Properti', + 'Metode Pembayaran', + 'Total Harga', + 'Status' + + ]); + + + /* + |-------------------------------------------------------------------------- + | DATA CSV + |-------------------------------------------------------------------------- + */ + + foreach ($transaksi as $item) { + + fputcsv($file, [ + $item->id_transaksi, + $item->tanggal_transaksi, + $item->user->nama_user ?? '-', + $item->properti->nama_properti ?? '-', + $item->metode_pembayaran ?? '-', + $item->total_harga, + $item->status_transaksi + ]); + } + + fclose($file); + }; + + + return response()->stream( + $callback, + 200, + $headers + ); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MonitoringController.php b/app/Http/Controllers/MonitoringController.php new file mode 100644 index 0000000..1910ceb --- /dev/null +++ b/app/Http/Controllers/MonitoringController.php @@ -0,0 +1,362 @@ +search) { + + $query->whereHas('user', function ($q) use ($request) { + + $q->where( + 'nama_user', + 'like', + '%' . $request->search . '%' + ); + + })->orWhereHas('properti', function ($q) use ($request) { + + $q->where( + 'nama_properti', + 'like', + '%' . $request->search . '%' + ); + + }); + } + + // FILTER STATUS + if ($request->status) { + + $query->where( + 'status', + $request->status + ); + } + + // FILTER METODE + if ($request->metode) { + + $query->where( + 'metode_pembayaran', + $request->metode + ); + } + + // SORTING + if ($request->sort == 'oldest') { + + $query->oldest(); + + } elseif ($request->sort == 'name_asc') { + + $query->join( + 'users', + 'pemesanan.id_user', + '=', + 'users.id_user' + )->orderBy( + 'users.nama_user', + 'asc' + ); + + } elseif ($request->sort == 'name_desc') { + + $query->join( + 'users', + 'pemesanan.id_user', + '=', + 'users.id_user' + )->orderBy( + 'users.nama_user', + 'desc' + ); + + } else { + + $query->latest(); + } + + $pemesanan = $query + ->latest() + ->paginate(6) + ->withQueryString(); + + return view( + 'admin.monitoring-pemesanan', + compact('pemesanan') + ); + } + + // ========================================================= + // DETAIL MONITORING ADMIN + // ========================================================= + public function show($id) + { + $pemesanan = Pemesanan::with([ + 'user', + 'properti', + 'dokumen' + ])->findOrFail($id); + + + // AMBIL RIWAYAT AKTIVITAS + $aktivitas = LogAktivitas::where( + 'ref_id', + $pemesanan->id_pemesanan + ) + ->latest() + ->get(); + + return view( + 'admin.detail-monitoring', + compact( + 'pemesanan', + 'aktivitas' + ) + ); + } + + // ========================================================= + // UPDATE MONITORING + // ========================================================= + public function update(Request $request, $id) + { + $pemesanan = Pemesanan::findOrFail($id); + + /* + |------------------------------------------------------------------ + | TENTUKAN STATUS TRANSAKSI + |------------------------------------------------------------------ + */ + + $statusTransaksi = 'menunggu_pembayaran'; + if (strtolower($request->status) == 'selesai') { + + $statusTransaksi = 'berhasil'; + + } + elseif (strtolower($request->status) == 'ditolak') { + + $statusTransaksi = 'ditolak'; + + } + elseif ( + strtolower($request->tahap_saat_ini) == 'pelunasan pembayaran' + ) { + + $statusTransaksi = 'menunggu_pembayaran'; + } + elseif ( + strtolower($request->tahap_saat_ini) == 'serah terima rumah' + ) { + + $statusTransaksi = 'berhasil'; + } + /* + |------------------------------------------------------------------ + | UPDATE DATA PEMESANAN + |------------------------------------------------------------------ + */ + + $pemesanan->update([ + + 'tahap_saat_ini' => + $request->tahap_saat_ini, + + 'status' => + $request->status, + + 'estimasi_proses' => + $request->estimasi_proses, + + 'catatan_admin' => + $request->catatan_admin, + ]); + + + /* + |------------------------------------------------------------------ + | UPDATE STATUS TRANSAKSI + |------------------------------------------------------------------ + */ + + $transaksi = Transaksi::where( + 'id_user', + $pemesanan->id_user + ) + ->where( + 'id_properti', + $pemesanan->id_properti + ) + ->orderBy('id_transaksi', 'desc') + ->first(); + + if ($transaksi) { + + $transaksi->update([ + + 'status_transaksi' => $statusTransaksi + ]); + } + + /* + |------------------------------------------------------------------ + | LOG AKTIVITAS + |------------------------------------------------------------------ + */ + + LogAktivitas::create([ + + 'id_user' => + auth()->user()->id_user, + + 'aktivitas' => + 'Admin mengubah progress menjadi "' . + $request->tahap_saat_ini . + '" dengan status "' . + $request->status . + '"', + + 'icon' => + 'fa-sync-alt', + + 'tipe' => + 'monitoring', + + 'ref_id' => + $pemesanan->id_pemesanan + ]); + + + /* + |------------------------------------------------------------------ + | KIRIM NOTIFIKASI + |------------------------------------------------------------------ + */ + + if ($request->kirim_notifikasi) { + $judul = 'Update Progress Pemesanan'; + $pesan = + 'Pemesanan properti kamu saat ini berada pada tahap "' . + $request->tahap_saat_ini . + '" dengan status "' . + $request->status . + '".'; + + // KHUSUS PELUNASAN + if ( + strtolower($request->progres) == 'pelunasan' || + strtolower($request->tahap_saat_ini) == 'pelunasan pembayaran' + ) { + $judul = 'Tagihan Pelunasan'; + $pesan = 'Silakan melakukan pelunasan pembayaran rumah Anda. Klik notifikasi ini untuk melihat invoice dan detail tagihan.'; + Notifikasi::create([ + 'id_user' => $pemesanan->id_user, + 'judul' => $judul, + 'pesan' => $pesan, + 'tipe' => 'pelunasan', + 'status_baca' => 0, + 'status_kirim' => 'terkirim', + 'channel' => 'in_app', + 'referensi_id' => $transaksi->id_transaksi, + 'referensi_tipe' => 'invoice', + ]); + + } else { + + Notifikasi::create([ + 'id_user' => $pemesanan->id_user, + 'judul' => $judul, + 'pesan' => $pesan, + 'tipe' => 'monitoring', + 'status_baca' => 0, + 'status_kirim' => 'terkirim', + 'channel' => 'in_app', + 'referensi_id' => $pemesanan->id_pemesanan, + 'referensi_tipe' => 'pemesanan', + ]); + } + + /* + |-------------------------------------------------------------- + | LOG NOTIFIKASI + |-------------------------------------------------------------- + */ + + LogAktivitas::create([ + + 'id_user' => + auth()->user()->id_user, + + 'aktivitas' => + 'Admin mengirim notifikasi monitoring pemesanan', + + 'icon' => + 'fa-paper-plane', + + 'tipe' => + 'monitoring', + + 'ref_id' => + $pemesanan->id_pemesanan + ]); + } + + + return redirect() + ->back() + ->with( + 'success', + 'Monitoring berhasil diperbarui' + ); + } + + + + // ========================================================= + // HALAMAN MONITORING PELANGGAN + // ========================================================= + public function pelanggan($id) +{ + // 1. Ambil transaksi dulu (dari klik user) + // $transaksi = Transaksi::with(['pemesanan','properti']) + // ->where('id_transaksi', $id) + // ->where('id_user', auth()->id()) + // ->firstOrFail(); + $pemesanan = Pemesanan::where('id_pemesanan', $id) + ->where('id_user', auth()->id()) + ->firstOrFail(); + + // 2. Ambil pemesanan dari transaksi + $transaksi = $pemesanan->transaksi; + + // 3. Ambil aktivitas dari PEMESANAN (bukan transaksi) + $aktivitas = LogAktivitas::where( + 'ref_id', + $pemesanan->id_pemesanan + ) + ->latest() + ->get(); + + return view('detail-pemesanan', compact('pemesanan', 'aktivitas', 'transaksi')); +} +} \ No newline at end of file diff --git a/app/Http/Controllers/PemesananController.php b/app/Http/Controllers/PemesananController.php index 7a5367b..f59ced6 100644 --- a/app/Http/Controllers/PemesananController.php +++ b/app/Http/Controllers/PemesananController.php @@ -3,121 +3,334 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Mail; + use App\Models\Transaksi; use App\Models\Properti; use App\Models\Notifikasi; use App\Models\Dokumen; use App\Models\User; -use Illuminate\Support\Facades\Auth; +use App\Models\Pemesanan; +use App\Models\LogAktivitas; + +use App\Mail\NotifikasiEmail; class PemesananController extends Controller { public function proses(Request $request) { + // ====================================== + // VALIDASI + // ====================================== + $request->validate([ - 'id_properti' => 'required|exists:properti,id_properti', - 'jenis_transaksi'=> 'required|in:lunas,kredit,kpr,cash,installment,other', + 'id_properti' => 'required|exists:properti,id_properti', + + 'jenis_transaksi' => 'required|in:lunas,kredit', + + 'bukti_booking' => $request->jenis_transaksi == 'lunas' + ? 'required|file|mimes:jpg,jpeg,png,pdf|max:5120' + : 'nullable' ]); - $properti = Properti::find($request->id_properti); + // ====================================== + // AMBIL DATA + // ====================================== + + $user = Auth::user(); + + $properti = Properti::findOrFail($request->id_properti); + + // ====================================== + // SIMPAN TRANSAKSI + // ====================================== - // Simpan Transaksi $transaksi = Transaksi::create([ - 'id_user' => Auth::user()->id_user, - 'id_properti' => $request->id_properti, - 'tanggal_transaksi'=> now()->toDateString(), - 'total_harga' => $properti->harga_properti, - 'jenis_transaksi' => $request->jenis_transaksi, + 'id_user' => $user->id_user, + + 'id_properti' => $request->id_properti, + + 'tanggal_transaksi' => now()->toDateString(), + + 'total_harga' => $properti->harga_properti, + + 'jenis_transaksi' => $request->jenis_transaksi, + 'status_transaksi' => 'menunggu_verifikasi', ]); - // Daftar dokumen yang perlu disimpan - // key = nama input di form, value = enum jenis_dokumen di DB + // ====================================== + // SIMPAN PEMESANAN + // ====================================== + + $pemesanan = Pemesanan::create([ + + 'kode_pemesanan' => 'PSN-' . time(), + 'id_transaksi' => $transaksi->id_transaksi, + 'id_user' => $user->id_user, + 'id_properti' => $request->id_properti, + 'total_harga' => $properti->harga_properti, + 'metode_pembayaran' => $request->jenis_transaksi, + 'tahap_saat_ini' => 'Pemesanan Dibuat', + 'status' => 'Proses', + 'tanggal_pemesanan' => now(), + ]); + + // ====================================== + // LOG AKTIVITAS + // ====================================== + + LogAktivitas::create([ + 'id_user' => $user->id_user, + + 'aktivitas' => 'Melakukan pemesanan properti ' . $properti->nama_properti, + + 'icon' => 'fa-cart-shopping', + + 'tipe' => 'transaksi' + ]); + + // ====================================== + // LIST DOKUMEN + // ====================================== + $dokumenList = [ - 'ktp' => 'ktp', - 'kk' => 'kk', - 'surat_nikah' => 'surat_nikah', - 'npwp' => 'npwp', - 'slip_gaji' => 'slip_gaji', - 'rekening_koran' => 'rekening_koran', - 'foto_3x4' => 'foto_3x4', - 'surat_kerja' => 'surat_kerja', - 'selfie' => 'selfie', - 'foto_tempat_kerja' => 'foto_tempat_kerja', + + 'ktp' => 'ktp', + 'kk' => 'kk', + 'surat_nikah' => 'surat_nikah', + 'npwp' => 'npwp', + 'slip_gaji' => 'slip_gaji', + 'rekening_koran' => 'rekening_koran', + 'foto_3x4' => 'foto_3x4', + 'surat_kerja' => 'surat_kerja', + 'selfie' => 'selfie', + 'foto_tempat_kerja' => 'foto_tempat_kerja', + // Wiraswasta - 'ktpw' => 'ktp', - 'kkW' => 'kk', - 'surat_nikahW' => 'surat_nikah', - 'npwp_W' => 'npwp', - 'spt' => 'spt', - 'surat_ket_usaha' => 'surat_ket_usaha', - 'surat_penghasilan_usaha'=> 'surat_penghasilan_usaha', - 'pembukuan_usaha' => 'pembukuan_usaha', - 'rekening_koranW' => 'rekening_koran', - 'selfieW' => 'selfie', - 'foto_tempat_usaha' => 'foto_tempat_usaha', + 'spt_pajak' => 'spt_pajak', + 'surat_keterangan_usaha' => 'surat_keterangan_usaha', + 'surat_penghasilan_usaha' => 'surat_penghasilan_usaha', + 'pembukuan_usaha' => 'pembukuan_usaha', + 'foto_lokasi_usaha' => 'foto_lokasi_usaha', ]; + // ====================================== + // SIMPAN DOKUMEN + // ====================================== + foreach ($dokumenList as $inputName => $jenisDokumen) { + if ($request->hasFile($inputName)) { - $files = is_array($request->file($inputName)) - ? $request->file($inputName) + + $files = is_array($request->file($inputName)) + ? $request->file($inputName) : [$request->file($inputName)]; foreach ($files as $file) { - $namaFile = time() . '_' . $file->getClientOriginalName(); - $path = $file->storeAs('dokumen/' . Auth::user()->id_user, $namaFile, 'public'); + + $namaFile = + time() . '_' . + $inputName . '_' . + $file->getClientOriginalName(); + + $path = $file->storeAs( + 'dokumen/' . $user->id_user, + $namaFile, + 'public' + ); Dokumen::create([ - 'id_user' => Auth::user()->id_user, - 'jenis_dokumen' => $jenisDokumen, - 'nama_file' => $namaFile, - 'path_file' => $path, - 'tipe_file' => $file->getClientMimeType(), - 'ukuran_file' => $file->getSize(), - 'status_verifikasi'=> 'pending', - 'catatan_verifikasi'=> '', + + 'id_user' => $user->id_user, + + 'id_pemesanan' => $pemesanan->id_pemesanan, + + 'id_transaksi' => $transaksi->id_transaksi, + + 'jenis_dokumen' => $jenisDokumen, + + 'nama_file' => $namaFile, + + 'path_file' => $path, + + 'tipe_file' => $file->getClientMimeType(), + + 'ukuran_file' => $file->getSize(), + + 'status_verifikasi' => 'pending', + + 'catatan_verifikasi' => '', ]); } } } - // Notifikasi ke User - Notifikasi::create([ - 'id_user' => Auth::user()->id_user, - 'judul' => 'Pemesanan Berhasil Dikirim', - 'pesan' => 'Pemesanan kamu untuk ' . $properti->nama_properti . ' sedang menunggu verifikasi admin.', - 'tipe' => 'transaksi', - 'status_baca' => 0, - 'status_kirim' => 'terkirim', - 'channel' => 'in_app', - 'referensi_id' => $transaksi->id_transaksi, - 'referensi_tipe' => 'transaksi', - ]); + // ====================================== + // SIMPAN BUKTI BOOKING + // ====================================== - // Notifikasi ke semua Admin - $admins = User::where('role_user', 'admin')->get(); - foreach ($admins as $admin) { - Notifikasi::create([ - 'id_user' => $admin->id_user, - 'judul' => 'Pemesanan Baru Masuk', - 'pesan' => Auth::user()->nama_user . ' memesan ' . $properti->nama_properti . '.', - 'tipe' => 'transaksi', - 'status_baca' => 0, - 'status_kirim' => 'terkirim', - 'channel' => 'in_app', - 'referensi_id' => $transaksi->id_transaksi, - 'referensi_tipe' => 'transaksi', + if ($request->hasFile('bukti_booking')) { + + $file = $request->file('bukti_booking'); + + $namaFile = + time() . '_booking_' . + $file->getClientOriginalName(); + + $path = $file->storeAs( + 'dokumen/' . $user->id_user, + $namaFile, + 'public' + ); + + Dokumen::create([ + + 'id_user' => $user->id_user, + + 'id_pemesanan' => $pemesanan->id_pemesanan, + + 'id_transaksi' => $transaksi->id_transaksi, + + 'jenis_dokumen' => 'bukti_booking', + + 'nama_file' => $namaFile, + + 'path_file' => $path, + + 'tipe_file' => $file->getClientMimeType(), + + 'ukuran_file' => $file->getSize(), + + 'status_verifikasi' => 'pending', + + 'catatan_verifikasi' => '', + ]); + + // ====================================== + // LOG AKTIVITAS DOKUMEN + // ====================================== + + LogAktivitas::create([ + 'id_user' => $user->id_user, + + 'aktivitas' => 'Mengupload bukti booking', + + 'icon' => 'fa-file-upload', + + 'tipe' => 'dokumen' ]); } - return redirect()->route('pemesanan.terima-kasih', ['id' => $transaksi->id_transaksi]); - } + // ====================================== + // NOTIFIKASI CUSTOMER + // ====================================== - $request->validate([ - 'jenis_transaksi' => 'required', - 'bukti_booking' => $request->jenis_transaksi == 'lunas' - ? 'required|file|mimes:jpg,jpeg,png,pdf|max:5120' - : 'nullable' -]); + Notifikasi::create([ + + 'id_user' => $user->id_user, + + 'judul' => 'Pemesanan Berhasil Dikirim', + + 'pesan' => + 'Pemesanan kamu untuk ' . + $properti->nama_properti . + ' sedang menunggu verifikasi admin.', + + 'tipe' => 'transaksi', + + 'status_baca' => 0, + + 'status_kirim' => 'terkirim', + + 'channel' => 'in_app', + + 'referensi_id' => $transaksi->id_transaksi, + + 'referensi_tipe' => 'transaksi', + ]); + + // ====================================== + // EMAIL CUSTOMER + // ====================================== + + if ($user->email_user) { + + Mail::to($user->email_user)->send( + + new NotifikasiEmail([ + + 'judul' => 'Pemesanan Berhasil', + + 'pesan' => + 'Pemesanan properti ' . + $properti->nama_properti . + ' berhasil dikirim dan menunggu verifikasi admin.' + ]) + ); + } + + // ====================================== + // NOTIFIKASI ADMIN + // ====================================== + + $admins = User::where('role_user', 'admin')->get(); + + foreach ($admins as $admin) { + + Notifikasi::create([ + + 'id_user' => $admin->id_user, + + 'judul' => 'Pemesanan Baru Masuk', + + 'pesan' => + $user->nama_user . + ' memesan ' . + $properti->nama_properti . '.', + + 'tipe' => 'transaksi', + + 'status_baca' => 0, + + 'status_kirim' => 'terkirim', + + 'channel' => 'in_app', + + 'referensi_id' => $transaksi->id_transaksi, + + 'referensi_tipe' => 'transaksi', + ]); + + // EMAIL ADMIN + + if ($admin->email_user) { + + Mail::to($admin->email_user)->send( + + new NotifikasiEmail([ + + 'judul' => 'Pemesanan Baru', + + 'pesan' => + $user->nama_user . + ' melakukan pemesanan properti ' . + $properti->nama_properti + ]) + ); + } + } + + // ====================================== + // REDIRECT + // ====================================== + + return redirect()->route( + 'pemesanan.terima-kasih', + [ + 'id' => $transaksi->id_transaksi + ] + ); + } } \ No newline at end of file diff --git a/app/Http/Controllers/PerumahanController.php b/app/Http/Controllers/PerumahanController.php new file mode 100644 index 0000000..b0838dc --- /dev/null +++ b/app/Http/Controllers/PerumahanController.php @@ -0,0 +1,66 @@ +update([ + 'nama_perumahan' => $request->nama_perumahan, + 'lokasi_perumahan' => $request->lokasi_perumahan, + ]); + + return redirect()->route('admin.perumahan') + ->with('success', 'Data berhasil diupdate'); +} + +public function destroy($id) +{ + Perumahan::where('id_perumahan', $id)->delete(); + + return redirect()->back()->with('success', 'Data perumahan berhasil dihapus'); +} + +public function store(Request $request) +{ + $perumahan = new Perumahan(); + $perumahan->nama_perumahan = $request->nama_perumahan; + $perumahan->lokasi_perumahan = $request->lokasi_perumahan; + $perumahan->save(); + + return redirect()->route('admin.perumahan') + ->with('success', 'Data berhasil ditambahkan'); +} + +public function kelolaBlok($id) +{ + $perumahan = Perumahan::with('blok') + ->findOrFail($id); + + return view( + 'admin.kelola_blok', + compact('perumahan') + ); +} +} diff --git a/app/Http/Controllers/TransaksiController.php b/app/Http/Controllers/TransaksiController.php new file mode 100644 index 0000000..c82f1f6 --- /dev/null +++ b/app/Http/Controllers/TransaksiController.php @@ -0,0 +1,123 @@ +findOrFail($id); + + LogAktivitas::create([ + 'id_user' => auth()->id(), + 'aktivitas' => + 'Membuka invoice transaksi #' . + $transaksi->id_transaksi, + 'icon' => 'fa-file-invoice', + 'tipe' => 'transaksi' + ]); + + return view( + 'invoice', + compact('transaksi') + ); + } + + // ========================= +// DOWNLOAD PDF INVOICE +// ========================= +public function downloadPdf($id) +{ + $transaksi = Transaksi::with([ + 'properti.gambar', + 'properti.blok', + 'properti.perumahan', + 'user' + ])->findOrFail($id); + + $pdf = Pdf::loadView( + 'pdf.invoice', + compact('transaksi') + ); + + return $pdf->download( + 'Invoice-'.$transaksi->id_transaksi.'.pdf' + ); +} + + + // ========================= + // SIMPAN PEMBAYARAN LUNAS + // ========================= + public function storePembayaranLunas(Request $request) +{ + $request->validate([ + 'id_transaksi' => 'required', + 'tanggal_transfer' => 'required', + // 'bank_pengirim' => 'required', + 'bukti_pembayaran' => + 'required|mimes:jpg,jpeg,png,pdf|max:5120' + ]); + + $file = $request->file('bukti_pembayaran'); + + $namaFile = + time() . '_' . + $file->getClientOriginalName(); + + $file->move( + public_path('uploads/pembayaran-lunas'), + $namaFile + ); + + $transaksi = + Transaksi::findOrFail( + $request->id_transaksi + ); + + $transaksi->bukti_transaksi = + $namaFile; + + $transaksi->metode_pembayaran = + 'transfer_bank'; + + $transaksi->status_transaksi = + 'menunggu_verifikasi'; + + $transaksi->save(); + dd($transaksi->fresh()->bukti_transaksi); + + LogAktivitas::create([ + 'id_user' => auth()->id(), + 'aktivitas' => + 'Mengupload bukti pembayaran lunas untuk transaksi #' + . $transaksi->id_transaksi, + 'icon' => 'fa-file-invoice-dollar', + 'tipe' => 'transaksi' + ]); + + return redirect() + ->route( + 'invoice', + $transaksi->id_transaksi + ) + ->with( + 'success', + 'Bukti pembayaran berhasil dikirim dan sedang menunggu verifikasi admin.' + ); +} + + +} \ No newline at end of file diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d7f2236..fb19183 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -1,13 +1,17 @@ delete(); + return redirect()->back()->with('success', 'User berhasil dihapus.'); } - // 🔥🔥🔥 TAMBAHAN UNTUK UPLOAD PP - public function uploadPP(Request $request) + + // ========================= + // UPLOAD PROFIL + // ========================= + public function updateProfile(Request $request) { $request->validate([ - 'foto' => 'required|image|mimes:jpg,jpeg,png|max:2048' + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:255' ]); - // 🔥 WAJIB pakai ini $user = Auth::user(); - if ($request->hasFile('foto')) { - $file = $request->file('foto'); - $namaFile = time().'_'.$file->getClientOriginalName(); - $file->move(public_path('uploads/profile'), $namaFile); - - $user->profile_photo = $namaFile; - } + $user->name = $request->name; + $user->email = $request->email; $user->save(); - return back()->with('success', 'Berhasil upload PP'); + return back()->with('success', 'Profil berhasil diperbarui.'); } + + // ========================= + // UPLOAD FOTO PROFIL + // ========================= + public function uploadPP(Request $request) +{ + $request->validate([ + 'photo' => 'required|image|mimes:jpg,jpeg,png|max:2048' + ]); + + $user = Auth::user(); + + if ($request->hasFile('photo')) { + + // hapus foto lama kalau ada + if ($user->profile_photo) { + Storage::disk('public')->delete( + 'profile_photos/' . $user->profile_photo + ); + } + + // upload foto baru + $file = $request->file('photo'); + + // simpan ke storage/app/public/profile_photos + $path = $file->store('profile_photos', 'public'); + + // simpan nama file ke database + $user->profile_photo = basename($path); + $user->save(); + } + + return back()->with('success', 'Foto profil berhasil diperbarui.'); +} } \ No newline at end of file diff --git a/app/Http/Controllers/VisitorController.php b/app/Http/Controllers/VisitorController.php index 3df5f3b..84440d3 100644 --- a/app/Http/Controllers/VisitorController.php +++ b/app/Http/Controllers/VisitorController.php @@ -2,9 +2,15 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; +use App\Models\VisitorLog; class VisitorController extends Controller { - // -} + public static function track($page) + { + VisitorLog::create([ + 'ip_address' => request()->ip(), + 'page' => $page + ]); + } +} \ No newline at end of file diff --git a/app/Mail/NotifikasiEmail.php b/app/Mail/NotifikasiEmail.php new file mode 100644 index 0000000..4f9514d --- /dev/null +++ b/app/Mail/NotifikasiEmail.php @@ -0,0 +1,23 @@ +data = $data; + } + + public function build() + { + return $this->subject($this->data['judul']) + ->view('emails.notifikasi') + ->with($this->data); + } +} \ No newline at end of file diff --git a/app/Models/Blok.php b/app/Models/Blok.php index 1fe99a3..91c2195 100644 --- a/app/Models/Blok.php +++ b/app/Models/Blok.php @@ -8,4 +8,21 @@ class Blok extends Model { protected $table = 'blok'; protected $primaryKey = 'id_blok'; + public $timestamps = false; + + protected $fillable = [ + 'id_perumahan', + 'nama_blok' + ]; + + + public function perumahan() +{ + return $this->belongsTo( + \App\Models\Perumahan::class, + 'id_perumahan', + 'id_perumahan' + ); } +} + diff --git a/app/Models/ChatSession.php b/app/Models/ChatSession.php new file mode 100644 index 0000000..3a54c13 --- /dev/null +++ b/app/Models/ChatSession.php @@ -0,0 +1,31 @@ +hasMany(ChatbotMessage::class, 'id_sessions', 'id_sessions'); + } + + public function user() + { + return $this->belongsTo(User::class, 'id_user'); + } +} \ No newline at end of file diff --git a/app/Models/ChatbotMessage.php b/app/Models/ChatbotMessage.php new file mode 100644 index 0000000..afd29a7 --- /dev/null +++ b/app/Models/ChatbotMessage.php @@ -0,0 +1,31 @@ +belongsTo( + ChatSession::class, + 'id_sessions', + 'id_sessions' + ); + } +} \ No newline at end of file diff --git a/app/Models/Dokumen.php b/app/Models/Dokumen.php index 5860e2b..574bd49 100644 --- a/app/Models/Dokumen.php +++ b/app/Models/Dokumen.php @@ -7,17 +7,39 @@ class Dokumen extends Model { protected $table = 'dokumen'; + protected $primaryKey = 'id_dokumen'; + public $timestamps = false; protected $fillable = [ - 'id_user', 'jenis_dokumen', 'nama_file', - 'path_file', 'tipe_file', 'ukuran_file', - 'status_verifikasi', 'catatan_verifikasi', + 'id_user', + 'id_pemesanan', + 'id_transaksi', + 'jenis_dokumen', + 'nama_file', + 'path_file', + 'tipe_file', + 'ukuran_file', + 'status_verifikasi', + 'catatan_verifikasi', + 'sumber_dokumen', ]; + // ✅ Relasi ke Transaksi + public function transaksi() + { + return $this->belongsTo(\App\Models\Transaksi::class, 'id_transaksi', 'id_transaksi'); + } + + // ✅ Relasi ke User public function user() { return $this->belongsTo(User::class, 'id_user', 'id_user'); } -} + + public function pemesanan() + { + return $this->belongsTo(Pemesanan::class, 'id_pemesanan'); + } +} \ No newline at end of file diff --git a/app/Models/GambarProperti.php b/app/Models/GambarProperti.php new file mode 100644 index 0000000..92fd4ce --- /dev/null +++ b/app/Models/GambarProperti.php @@ -0,0 +1,22 @@ +belongsTo(Properti::class, 'id_properti', 'id_properti'); + } +} \ No newline at end of file diff --git a/app/Models/LeadEmail.php b/app/Models/LeadEmail.php new file mode 100644 index 0000000..fa3a6c8 --- /dev/null +++ b/app/Models/LeadEmail.php @@ -0,0 +1,13 @@ +belongsTo(User::class, 'id_user'); + } +} diff --git a/app/Models/Pemesanan.php b/app/Models/Pemesanan.php new file mode 100644 index 0000000..5c50dfc --- /dev/null +++ b/app/Models/Pemesanan.php @@ -0,0 +1,52 @@ +belongsTo(User::class, 'id_user'); + } + + public function properti() + { + return $this->belongsTo(Properti::class, 'id_properti'); + } + + public function dokumen() + { + return $this->hasMany( + Dokumen::class, + 'id_pemesanan', + 'id_pemesanan' + ); + } + + public function transaksi() + { + return $this->belongsTo( + Transaksi::class, + 'id_transaksi', + 'id_transaksi' + ); + } +} \ No newline at end of file diff --git a/app/Models/Perumahan.php b/app/Models/Perumahan.php index 89e0ee5..be162d3 100644 --- a/app/Models/Perumahan.php +++ b/app/Models/Perumahan.php @@ -1,12 +1,22 @@ hasMany(Blok::class, 'id_perumahan', 'id_perumahan'); + } } \ No newline at end of file diff --git a/app/Models/Properti.php b/app/Models/Properti.php index 06b2304..f612ca7 100644 --- a/app/Models/Properti.php +++ b/app/Models/Properti.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model; use App\Models\Perumahan; use App\Models\Blok; +use App\Models\GambarProperti; class Properti extends Model { @@ -28,8 +29,13 @@ class Properti extends Model 'status_unit', 'id_perumahan', 'id_blok', - 'deskripsi', // jika ada - // ... tambah kolom lainnya sesuai tabel + + 'bookingFee', + 'luas_bangunan', + 'luas_tanah', + 'stok_unit', + + 'deskripsi', ]; // ✅ Relationship ke Perumahan @@ -74,4 +80,10 @@ public function getEstimasiCicilanAttribute() return 'Rp ' . number_format($cicilan, 0, ',', '.'); } + + // RELASI KE GAMBAR PROPERTI + public function gambar() +{ + return $this->hasMany(GambarProperti::class, 'id_properti', 'id_properti'); +} } \ No newline at end of file diff --git a/app/Models/Transaksi.php b/app/Models/Transaksi.php index cdc2757..566c663 100644 --- a/app/Models/Transaksi.php +++ b/app/Models/Transaksi.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use App\Models\Kpr; +use App\Models\Dokumen; // ✅ Tambah import ini class Transaksi extends Model { @@ -17,7 +18,9 @@ class Transaksi extends Model 'tanggal_transaksi', 'total_harga', 'jenis_transaksi', + 'metode_pembayaran', 'status_transaksi', + 'bukti_transaksi' ]; // Relasi ke properti @@ -26,18 +29,43 @@ public function properti() return $this->belongsTo(Properti::class, 'id_properti', 'id_properti'); } + // Relasi ke user public function user() { return $this->belongsTo(User::class, 'id_user', 'id_user'); } + // Relasi ke pembayaran public function pembayaran() { return $this->hasMany(Pembayaran::class, 'id_transaksi', 'id_transaksi'); } + // Relasi ke KPR public function kpr() { return $this->hasOne(Kpr::class, 'id_transaksi', 'id_transaksi'); } + + // ✅ TAMBAHKAN INI: Relasi ke Dokumen + public function dokumen() + { + return $this->hasMany( + Dokumen::class, + 'id_transaksi', + 'id_transaksi' + ); + } + + // RELASI KE PEMESANAN + public function pemesanan() + { + return $this->hasOne( + Pemesanan::class, + 'id_transaksi', + 'id_transaksi' + ); + } + + } \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 9dca9dd..b733f2f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,48 +10,43 @@ class User extends Authenticatable { use HasFactory, Notifiable; - // ✅ Tentukan primary key kustom + // Nama tabel & primary key custom protected $table = 'user'; protected $primaryKey = 'id_user'; - public $incrementing = true; // atau false jika id_user bukan auto-increment - protected $keyType = 'int'; // atau 'string' jika UUID - // ✅ Tentukan nama kolom password kustom - protected $password = 'password_user'; + public $incrementing = true; + protected $keyType = 'int'; - // ✅ Kolom yang bisa diisi mass-assignment + // Kolom yang boleh diisi protected $fillable = [ 'nama_user', 'email_user', 'no_hp', 'password_user', 'role_user', + 'google_id', + 'google_avatar', ]; - // ✅ Hidden attributes (tidak dikirim saat toJSON/toArray) + // Hidden fields protected $hidden = [ 'password_user', 'remember_token', ]; - // ✅ Beritahu Laravel field mana yang digunakan untuk auth - public function getAuthIdentifierName() - { - return 'id_user'; - } - + // Laravel auth pakai password_user public function getAuthPassword() { return $this->password_user; } - public function getAuthPasswordName() - { - return 'password_user'; - } - + // Relasi public function dokumen() { - return $this->hasMany(\App\Models\Dokumen::class, 'id_user', 'id_user'); + return $this->hasMany( + \App\Models\Dokumen::class, + 'id_user', + 'id_user' + ); } } \ No newline at end of file diff --git a/app/Models/VisitorLog.php b/app/Models/VisitorLog.php new file mode 100644 index 0000000..d8d824d --- /dev/null +++ b/app/Models/VisitorLog.php @@ -0,0 +1,13 @@ +=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, { "name": "league/uri", "version": "7.8.1", @@ -2083,6 +2527,73 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -2685,6 +3196,125 @@ ], "time": "2026-03-17T20:53:51+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "php-http/discovery", "version": "1.20.0", @@ -2895,6 +3525,116 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -3584,6 +4324,86 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.3.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.2.8", + "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.4.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" + }, + "time": "2026-03-03T17:31:43+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", @@ -6085,6 +6905,149 @@ ], "time": "2026-02-15T10:53:20+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", diff --git a/config/app.php b/config/app.php index 423eed5..c74a68d 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ | */ - 'timezone' => 'UTC', + 'timezone' => 'Asia/Jakarta', /* |-------------------------------------------------------------------------- diff --git a/config/services.php b/config/services.php index 6a90eb8..45dc538 100644 --- a/config/services.php +++ b/config/services.php @@ -14,6 +14,12 @@ | */ + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URI'), + ], + 'postmark' => [ 'key' => env('POSTMARK_API_KEY'), ], diff --git a/database/migrations/2026_05_03_110331_create_lead_emails_table.php b/database/migrations/2026_05_03_110331_create_lead_emails_table.php new file mode 100644 index 0000000..d7f0ef4 --- /dev/null +++ b/database/migrations/2026_05_03_110331_create_lead_emails_table.php @@ -0,0 +1,36 @@ +id(); + + $table->string('email')->unique(); + + $table->string('sumber')->nullable(); + + $table->string('ip_address')->nullable(); + + $table->timestamps(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('lead_emails'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_03_144621_create_visitor_logs__table.php b/database/migrations/2026_05_03_144621_create_visitor_logs__table.php new file mode 100644 index 0000000..fb62b20 --- /dev/null +++ b/database/migrations/2026_05_03_144621_create_visitor_logs__table.php @@ -0,0 +1,34 @@ +id(); + + $table->string('ip_address')->nullable(); + + $table->string('page')->nullable(); + + $table->timestamps(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('visitor_logs_'); + } +}; diff --git a/database/migrations/2026_05_05_172115_add_google_avatar_to_user_table.php b/database/migrations/2026_05_05_172115_add_google_avatar_to_user_table.php new file mode 100644 index 0000000..53405cf --- /dev/null +++ b/database/migrations/2026_05_05_172115_add_google_avatar_to_user_table.php @@ -0,0 +1,29 @@ +string('google_id')->nullable(); + $table->text('google_avatar')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_05_07_170900_add_id_transaksi_to_dokumen_table.php b/database/migrations/2026_05_07_170900_add_id_transaksi_to_dokumen_table.php new file mode 100644 index 0000000..a506ef1 --- /dev/null +++ b/database/migrations/2026_05_07_170900_add_id_transaksi_to_dokumen_table.php @@ -0,0 +1,23 @@ +unsignedBigInteger('id_transaksi') + ->after('id_user'); + }); + } + + public function down(): void + { + Schema::table('dokumen', function (Blueprint $table) { + $table->dropColumn('id_transaksi'); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_14_201642_create_log_aktivitas_table.php b/database/migrations/2026_05_14_201642_create_log_aktivitas_table.php new file mode 100644 index 0000000..10875af --- /dev/null +++ b/database/migrations/2026_05_14_201642_create_log_aktivitas_table.php @@ -0,0 +1,43 @@ +id(); + + // HARUS SAMA: INT (bukan bigInteger) + $table->integer('id_user')->nullable(); + + $table->foreign('id_user') + ->references('id_user') + ->on('user') + ->onDelete('set null'); + + $table->string('aktivitas'); + $table->string('icon')->nullable(); + $table->string('tipe')->nullable(); + $table->unsignedBigInteger('ref_id')->nullable(); + + $table->timestamps(); + + $table->index('id_user'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('log_aktivitas'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_16_183814_add_properti_data_to_chat_messages.php b/database/migrations/2026_05_16_183814_add_properti_data_to_chat_messages.php new file mode 100644 index 0000000..ef97a04 --- /dev/null +++ b/database/migrations/2026_05_16_183814_add_properti_data_to_chat_messages.php @@ -0,0 +1,32 @@ +json('properti_data') + ->nullable() + ->after('message'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('chat_messages', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_05_25_161821_create_gambar_properti_table.php b/database/migrations/2026_05_25_161821_create_gambar_properti_table.php new file mode 100644 index 0000000..0cbea8e --- /dev/null +++ b/database/migrations/2026_05_25_161821_create_gambar_properti_table.php @@ -0,0 +1,33 @@ +id('id_gambar'); + $table->unsignedBigInteger('id_properti'); + $table->string('path_gambar'); + $table->timestamps(); + $table->foreign('id_properti') + ->references('id_properti') + ->on('properti') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('gambar_properti'); + } +}; diff --git a/public/images/cta.webp b/public/images/cta.webp new file mode 100644 index 0000000..621f7f5 Binary files /dev/null and b/public/images/cta.webp differ diff --git a/public/images/dapur 30.png b/public/images/dapur 30.png new file mode 100644 index 0000000..4afb563 Binary files /dev/null and b/public/images/dapur 30.png differ diff --git a/public/images/gambar-perusahaan.webp b/public/images/gambar-perusahaan.webp new file mode 100644 index 0000000..6b54afb Binary files /dev/null and b/public/images/gambar-perusahaan.webp differ diff --git a/public/images/header-sistem.png b/public/images/header-sistem.png new file mode 100644 index 0000000..1abf467 Binary files /dev/null and b/public/images/header-sistem.png differ diff --git a/public/images/header1.png b/public/images/header1.png new file mode 100644 index 0000000..2315a46 Binary files /dev/null and b/public/images/header1.png differ diff --git a/public/images/header1.webp b/public/images/header1.webp new file mode 100644 index 0000000..5adafa6 Binary files /dev/null and b/public/images/header1.webp differ diff --git a/public/images/kamar tidur 30.png b/public/images/kamar tidur 30.png new file mode 100644 index 0000000..0907e5c Binary files /dev/null and b/public/images/kamar tidur 30.png differ diff --git a/public/images/logoPT Pengguna.jpg b/public/images/logoPT Pengguna.jpg new file mode 100644 index 0000000..e29ff9a Binary files /dev/null and b/public/images/logoPT Pengguna.jpg differ diff --git a/public/images/placeholder-properti.png b/public/images/placeholder-properti.png new file mode 100644 index 0000000..f4d4b82 Binary files /dev/null and b/public/images/placeholder-properti.png differ diff --git a/public/images/ruang tamu 30.png b/public/images/ruang tamu 30.png new file mode 100644 index 0000000..a998cf6 Binary files /dev/null and b/public/images/ruang tamu 30.png differ diff --git a/public/images/rumah30.png b/public/images/rumah30.png new file mode 100644 index 0000000..a6c542a Binary files /dev/null and b/public/images/rumah30.png differ diff --git a/public/images/rumah60.png b/public/images/rumah60.png new file mode 100644 index 0000000..308de9b Binary files /dev/null and b/public/images/rumah60.png differ diff --git a/public/uploads/pembayaran-lunas/1779041095_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779041095_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779041095_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779041474_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779041474_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779041474_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779042174_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779042174_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779042174_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779042316_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779042316_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779042316_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779042782_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779042782_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779042782_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779042821_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779042821_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779042821_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779074366_Activity diagram form pemesanan.drawio.png b/public/uploads/pembayaran-lunas/1779074366_Activity diagram form pemesanan.drawio.png new file mode 100644 index 0000000..c699cda Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779074366_Activity diagram form pemesanan.drawio.png differ diff --git a/public/uploads/pembayaran-lunas/1779078559_Activity diagram login pelanggan.drawio.png b/public/uploads/pembayaran-lunas/1779078559_Activity diagram login pelanggan.drawio.png new file mode 100644 index 0000000..f17f016 Binary files /dev/null and b/public/uploads/pembayaran-lunas/1779078559_Activity diagram login pelanggan.drawio.png differ diff --git a/resources/views/admin/aktivitas.blade.php b/resources/views/admin/aktivitas.blade.php new file mode 100644 index 0000000..3c11b90 --- /dev/null +++ b/resources/views/admin/aktivitas.blade.php @@ -0,0 +1,1390 @@ + + + + + + Log Aktivitas - Admin + + + + + + + + + + +
+ + + + + + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ Timeline Aktivitas +
+ 156 aktivitas ditemukan +
+
+ +
+
+
+ + + + +
+ +
+
Pengguna Aktif
+
+
+
+
+
+ + + + + +
+ + + + + + + diff --git a/resources/views/admin/data_rumah.blade.php b/resources/views/admin/data_rumah.blade.php index 6ba7b85..30ad320 100644 --- a/resources/views/admin/data_rumah.blade.php +++ b/resources/views/admin/data_rumah.blade.php @@ -115,6 +115,48 @@ overflow: hidden; } + .nav-group { + display: flex; + flex-direction: column; + } + + .nav-parent { + justify-content: space-between; + } + + .arrow { + margin-left: auto; + transition: transform 0.3s ease; + } + + .nav-submenu { + display: none; + flex-direction: column; + padding-left: 40px; + transition: all 0.2s ease; + } + + + /* optional arrow */ + .nav-group.open .arrow { + transform: rotate(180deg); + } + + .nav-submenu a { + padding: 10px 20px; + font-size: 0.9rem; + opacity: 0.85; + } + + .nav-submenu a:hover { + opacity: 1; + } + + /* open state */ + .nav-group.open .nav-submenu { + display: flex; + } + .nav-item:hover { background: rgba(255,255,255,0.08); border-left-color: var(--primary-blue); @@ -156,6 +198,7 @@ margin-top: auto; white-space: nowrap; overflow: hidden; + color: #ffff; } .logout-btn:hover { @@ -686,7 +729,7 @@ - @@ -960,7 +1103,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG/PDF | Max 5MB

- +
@@ -971,7 +1114,8 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG | Max 2MB

- + +
@@ -981,7 +1125,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG/PDF | Max 5MB

- +
@@ -992,7 +1136,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG/PDF | Max 5MB

- +
@@ -1003,7 +1147,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG/PDF | Max 5MB

- +
@@ -1025,7 +1169,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG | Max 3MB

- +
@@ -1036,7 +1180,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Klik atau drag file

JPG/PNG | Max 3MB

- +
@@ -1047,31 +1191,127 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">

Informasi Pembayaran

+ + @php + $harga = $properti->harga_properti ?? 0; + + // ambil booking fee dari database + $bookingFee = (int) ($properti->bookingFee ?? 0); + + // total pembayaran = booking fee saja + $totalPembayaran = $harga + $bookingFee; + @endphp +
Rincian Biaya
+
+
Harga Properti - Rp 285.000.000 + + Rp {{ number_format($harga, 0, ',', '.') }} +
+
- Uang Muka - Rp 45.000.000 -
-
- Biaya Booking - Rp 2.000.000 + Booking Fee + + Rp {{ number_format($bookingFee, 0, ',', '.') }} +
+
Total Pembayaran - Rp 47.000.000 + + Rp {{ number_format($totalPembayaran, 0, ',', '.') }} +
+
+

- Catatan: Biaya booking akan dikembalikan jika KPR disetujui bank. Pembayaran dapat dilakukan melalui transfer bank atau datang langsung ke kantor pemasaran. + Catatan: Booking fee mengikuti unit properti yang dipilih.

+ + +
+
+ + Informasi Transfer Booking Fee +
+ +
+ Bank + + Bank BCA + +
+ +
+ Nomor Rekening + + + 1234567890 + + + +
+ +
+ Atas Nama + + + PT Carani Bhanu Balakosa + +
+ +
+ + Total Transfer + + + + Rp {{ number_format($bookingFee,0,',','.') }} + +
+ +
+ + +
@@ -1083,31 +1323,6 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
- - - {{-- tutup form --}} @@ -1117,53 +1332,11 @@ class="file-input" document.addEventListener('DOMContentLoaded', function() { // Payment method dropdown change handler // ========== MODAL PREVIEW ========== - const modal = document.createElement('div'); - modal.id = 'previewModal'; - modal.style.cssText = ` - display:none; position:fixed; inset:0; background:rgba(0,0,0,0.75); - z-index:9999; align-items:center; justify-content:center; padding:20px; - `; - modal.innerHTML = ` -
- -

-
-
- `; - document.body.appendChild(modal); - - modal.addEventListener('click', e => { if (e.target === modal) closePreview(); }); - document.getElementById('previewClose').addEventListener('click', closePreview); - document.addEventListener('keydown', e => { if (e.key === 'Escape') closePreview(); }); - - function closePreview() { - modal.style.display = 'none'; - document.getElementById('previewContent').innerHTML = ''; - } - function openPreview(file) { - document.getElementById('previewTitle').textContent = file.name; - const content = document.getElementById('previewContent'); - content.innerHTML = ''; - const url = URL.createObjectURL(file); + // buka di tab baru + window.open(url, '_blank'); - if (file.type.startsWith('image/')) { - const img = document.createElement('img'); - img.style.cssText = 'max-width:100%; max-height:75vh; display:block; border-radius:6px;'; - img.src = url; - img.onload = () => URL.revokeObjectURL(url); - content.appendChild(img); - } else if (file.type === 'application/pdf') { - const iframe = document.createElement('iframe'); - iframe.src = url; - iframe.style.cssText = 'width:75vw; height:75vh; border:none; border-radius:6px;'; - content.appendChild(iframe); - } - - modal.style.display = 'flex'; } // ========== FILE UPLOAD HANDLER (GANTI YANG LAMA) ========== @@ -1197,12 +1370,30 @@ function setupFileUpload(uploadAreaId, fileInputId, fileNameId) { const btnLihat = document.createElement('button'); btnLihat.type = 'button'; btnLihat.setAttribute('data-preview', '1'); - btnLihat.textContent = '👁 Lihat'; + + btnLihat.innerHTML = ''; + btnLihat.style.cssText = ` - font-size:11px; padding:2px 8px; border-radius:4px; cursor:pointer; - background:#e8f0fe; border:1px solid #7AB2D3; color:#1E3A5F; - white-space:nowrap; flex-shrink:0; + width:30px; + height:30px; + border:none; + border-radius:8px; + cursor:pointer; + background:#e8f0fe; + color:#1E3A5F; + transition:0.2s; + flex-shrink:0; `; + + btnLihat.onmouseenter = () => { + btnLihat.style.background = '#7AB2D3'; + btnLihat.style.color = '#fff'; + }; + + btnLihat.onmouseleave = () => { + btnLihat.style.background = '#e8f0fe'; + btnLihat.style.color = '#1E3A5F'; + }; btnLihat.addEventListener('click', (e) => { e.stopPropagation(); // supaya tidak trigger uploadArea click openPreview(file); @@ -1241,6 +1432,8 @@ function setupFileUpload(uploadAreaId, fileInputId, fileNameId) { setupFileUpload('selfieUploadW', 'selfieFileW', 'selfieFileNameW'); setupFileUpload('businessPlaceUpload', 'businessPlaceFile', 'businessPlaceFileName'); + setupFileUpload('buktiBookingUpload', 'buktiBooking', 'buktiBookingName'); + // ========== SISA SCRIPT LAMA (payment, employment toggle, submit, cancel) ========== const paymentMethodSelect = document.getElementById('paymentMethod'); const downPaymentInput = document.getElementById('downPayment'); @@ -1265,43 +1458,85 @@ function setupFileUpload(uploadAreaId, fileInputId, fileNameId) { // Cancel button document.getElementById('cancelBtn').addEventListener('click', function() { if (confirm('Apakah Anda yakin ingin membatalkan pemesanan? Semua data yang telah diisi akan hilang.')) { - window.location.href = 'katalog.html'; + window.location.href = '{{ route("halaman-katalog") }}'; } }); }); + + diff --git a/resources/views/gantipass.blade.php b/resources/views/gantipass.blade.php index cce2b5d..f04e88e 100644 --- a/resources/views/gantipass.blade.php +++ b/resources/views/gantipass.blade.php @@ -403,20 +403,24 @@

Carani Estate

-

Platform terpercaya untuk membeli, menjual, dan menyewa properti berkualitas sejak 2015

- +

+ Platform properti terpercaya untuk membantu Anda menemukan dan membeli properti dengan proses yang mudah dan aman. +

+ @@ -493,13 +497,13 @@ class="form-control password-input" - @error('konfirmasi_password') + @error('password_baru_confirmation')
{{ $message }}
@enderror diff --git a/resources/views/halaman-chatbot.blade.php b/resources/views/halaman-chatbot.blade.php index f30b400..bf0ebc1 100644 --- a/resources/views/halaman-chatbot.blade.php +++ b/resources/views/halaman-chatbot.blade.php @@ -151,6 +151,7 @@ justify-content: center; cursor: pointer; transition: all 0.3s ease; + text-decoration: none; } .profile-icon:hover { @@ -279,6 +280,23 @@ display: inline-block; margin-right: 5px; } + + /* HAPUS CHAT */ + .hapus-chat-btn{ + margin-left:auto; + width:40px; + height:40px; + border:none; + border-radius:10px; + background:#fee2e2; + color:#dc2626; + cursor:pointer; + transition:0.2s; + } + + .hapus-chat-btn:hover{ + background:#fecaca; + } /* Messages Area */ .messages-area { @@ -480,6 +498,153 @@ 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } + + .chat-katalog-wrapper { + max-width: 100%; /* samakan dengan bubble chat */ + width: fit-content; + margin-top: 12px; + margin-left: 0; + padding-left: 20px; /* sejajar avatar bot */ + } + + /* Property Cards */ + .properti-cards { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 12px; + } + + .properti-card{ + background:#f8fafc; + border:1px solid #e2e8f0; + border-radius:14px; + overflow:hidden; + transition:0.3s; + } + + .properti-card:hover { + box-shadow: 0 4px 15px rgba(122, 178, 211, 0.3); + border-color: var(--primary-blue); + } + + .properti-card-content{ + padding:16px; + } + + .properti-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + .properti-card-nama { + font-weight: 700; + font-size: 0.95rem; + margin-bottom:8px; + color: var(--dark-blue); + } + + .properti-card-badge { + font-size: 0.7rem; + padding: 3px 8px; + border-radius: 20px; + font-weight: 600; + } + + .badge-subsidi { + background: #dcfce7; + color: #16a34a; + } + + .badge-komersial { + background: #fef3c7; + color: #d97706; + } + + .properti-card-info{ + display:flex; + flex-wrap:wrap; + gap:8px 12px; + margin-bottom:14px; + color:#64748b; + } + + .properti-card-info span { + display: flex; + align-items: center; + gap: 4px; + } + + .properti-card-harga { + font-weight: 700; + color: var(--primary-blue); + font-size: 1rem; + margin-bottom: 10px; + } + + .properti-card-image{ + width:100%; + height:170px; + object-fit:cover; + display:block; + margin:0; + border-radius:0; + } + + .properti-card-lokasi{ + font-size:13px; + color:#64748b; + margin-top:8px; + margin-bottom:12px; + line-height:1.5; + } + + .properti-card-lokasi div{ + margin-bottom:4px; + } + + .properti-card-btn { + width: 100%; + padding: 8px; + background: var(--primary-blue); + color: white; + border: none; + border-radius: 8px; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: block; + text-align: center; + } + + .properti-card-btn:hover { + background: var(--dark-blue); + color: white; + } + + /* KATALOG */ + + + /* Profilll */ + .profile-avatar, + .profile-avatar-default { + width: 35px; + height: 35px; + border-radius: 50%; + object-fit: cover; + } + + .profile-avatar-default { + background: #7AB2D3; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } /* Responsive Design */ @media (max-width: 768px) { @@ -654,6 +819,7 @@ + {{ Auth::check() ? 'LOGIN BERHASIL' : 'BELUM LOGIN' }}
@@ -721,16 +887,41 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> {{-- Guest --}} @guest - @else {{-- HANYA ICON PROFILE --}} - Profile + @php + $user = Auth::user(); + @endphp + + {{-- Prioritas 1: Foto upload user --}} + @if($user->profile_photo) + + Profile Photo + + {{-- Prioritas 2: Foto Google --}} + @elseif($user->google_avatar) + + Google Photo + + {{-- Prioritas 3: Inisial --}} + @else + +
+ {{ strtoupper(substr($user->nama_user, 0, 1)) }} +
+ + @endif +
@endguest
@@ -745,21 +936,26 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
-

PropertiBot Assistant

+

CaraniBot Assistant

Online - Siap membantu Anda

+ + +
PH
-
PropertiBot
+
CaraniBot
- Halo! Saya PropertiBot. Ada yang bisa dibantu hari ini? 😊
+ Halo! Saya CaraniBot. Ada yang bisa dibantu hari ini? 😊
Silakan tanya tentang harga, lokasi, atau KPR.
{{ date('H:i') }}
@@ -768,17 +964,16 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
- +
- - -
+ +
@@ -788,289 +983,974 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
+ - // TOGGLE NAV - function toggleMenu(){ - document.getElementById('navMenu').classList.toggle('show'); - } + + + + diff --git a/resources/views/halaman-katalog.blade.php b/resources/views/halaman-katalog.blade.php index ed4eb73..bc451e1 100644 --- a/resources/views/halaman-katalog.blade.php +++ b/resources/views/halaman-katalog.blade.php @@ -148,6 +148,7 @@ justify-content: center; cursor: pointer; transition: all 0.3s ease; + text-decoration: none; } .profile-icon:hover { @@ -692,6 +693,24 @@ font-size: 0.9rem; color: #cbd5e0; } + + /* Profilll */ + .profile-avatar, + .profile-avatar-default { + width: 35px; + height: 35px; + border-radius: 50%; + object-fit: cover; + } + + .profile-avatar-default { + background: #7AB2D3; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } /* Responsive Design */ @media (max-width: 992px) { @@ -846,6 +865,7 @@ + {{ Auth::check() ? 'LOGIN BERHASIL' : 'BELUM LOGIN' }}
@@ -1005,7 +1050,13 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
- {{ $p->nama_properti }} + {{ $p->nama_properti }} + {{-- Badge tersedia pindah ke sini --}}
{{ $labelStatus }} @@ -1066,7 +1117,6 @@ class="action-btn btn-view">
- diff --git a/resources/views/halaman-kontak.blade.php b/resources/views/halaman-kontak.blade.php index 9e6afe9..caf6a32 100644 --- a/resources/views/halaman-kontak.blade.php +++ b/resources/views/halaman-kontak.blade.php @@ -147,6 +147,7 @@ justify-content: center; cursor: pointer; transition: all 0.3s ease; + text-decoration: none; } .profile-icon:hover { @@ -386,6 +387,118 @@ color: white; transform: translateY(-3px); } + + /* Footer */ + .footer { + background: var(--dark-blue); + color: white; + padding: 50px 30px 20px; + margin-top: 80px; + } + + .footer-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 40px; + } + + .footer-column h3 { + font-size: 1.2rem; + margin-bottom: 20px; + position: relative; + } + + .footer-column h3::after { + content: ''; + position: absolute; + bottom: -8px; + left: 0; + width: 40px; + height: 2px; + background: var(--primary-blue); + } + + .footer-links { + list-style: none; + padding: 0; + margin: 0; + } + + .footer-links li { + margin-bottom: 10px; + } + + .footer-links a { + color: #cbd5e0; + text-decoration: none; + transition: color 0.3s ease; + } + + .footer-links a:hover { + color: var(--primary-blue); + } + + .footer-contact p { + margin: 10px 0; + display: flex; + align-items: center; + gap: 10px; + } + + .footer-contact i { + color: var(--primary-blue); + } + + .footer-social { + display: flex; + gap: 15px; + margin-top: 20px; + } + + .social-icon { + width: 40px; + height: 40px; + background: rgba(255,255,255,0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + transition: all 0.3s ease; + text-decoration:none; + } + + .social-icon:hover { + background: var(--primary-blue); + transform: translateY(-2px); + } + + .copyright { + text-align: center; + padding-top: 30px; + border-top: 1px solid rgba(255,255,255,0.1); + margin-top: 40px; + font-size: 0.9rem; + color: #cbd5e0; + } + + /* Profilll */ + .profile-avatar, + .profile-avatar-default { + width: 35px; + height: 35px; + border-radius: 50%; + object-fit: cover; + } + + .profile-avatar-default { + background: #7AB2D3; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } /* Responsive Design */ @media (max-width: 992px) { @@ -520,6 +633,7 @@ + {{ Auth::check() ? 'LOGIN BERHASIL' : 'BELUM LOGIN' }}
@@ -710,28 +849,55 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> -
+
+
+ + + +
+
+ + + + + + +@if(session('success')) + +@endif + + +
+ + + × + + + +
+ + +``` diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index 95977a6..ea88887 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -157,14 +157,14 @@ } .form-group { - margin-bottom: 22px; + margin-bottom: 15px; position: relative; } .form-label { font-weight: 600; color: #2d3748; - margin-bottom: 10px; + margin-bottom: 5px; font-size: 0.95rem; } @@ -450,20 +450,24 @@

Carani Estate

-

Platform terpercaya untuk membeli, menjual, dan menyewa properti berkualitas sejak 2015

- +

+ Platform properti terpercaya untuk membantu Anda menemukan dan membeli properti dengan proses yang mudah dan aman. +

+ @@ -534,6 +538,21 @@ class="form-control password-input @error('password') is-invalid @enderror" Masuk ke Akun +
+ atau +
+ + + + Login dengan Google + + + + + + diff --git a/resources/views/pembayaran_lunas.blade.php b/resources/views/pembayaran_lunas.blade.php new file mode 100644 index 0000000..178197a --- /dev/null +++ b/resources/views/pembayaran_lunas.blade.php @@ -0,0 +1,2906 @@ + + + + + + Pembayaran Lunas - Kavling + + + + + + + +
+ +
+ + +
+ + +
+ + + + +
+

Pembayaran Lunas

+

Dokumen Anda telah disetujui. Silakan selesaikan pembayaran lunas untuk kavling Anda.

+
+ + +
+ + + + Dokumen telah diverifikasi. + Anda dapat melanjutkan pembayaran lunas untuk + {{ $transaksi->properti->nama_properti ?? 'Properti yang dipilih' }}. + +
+ +
+ + @csrf + + + + + +
+ +
+ +
+
+
+ + Ringkasan Pesanan +
+ Disetujui +
+ +
+
+ +
+ +
+ +
+ {{ $transaksi->properti->nama_properti ?? '-' }} +
+ +
+ + + Luas: {{ $transaksi->properti->luas_tanah ?? '-' }} m² + + + + + Kategori: {{ $transaksi->properti->kategori_properti ?? '-' }} + + + + + {{ $transaksi->properti->blok->nama_blok ?? '-' }} + + + + + No. Booking: {{ $transaksi->kode_booking ?? $transaksi->id_transaksi }} + +
+
+
+ + @php + $properti = $transaksi->properti; + + $hargaProperti = $properti->harga_properti ?? 0; + + $biayaAdmin = 500000; + $biayaNotaris = 2500000; + + // ambil langsung dari database + $bookingFee = $properti->bookingFee ?? 0; + + $uangMuka = 0; + + if (strtolower($properti->kategori_properti ?? '') == 'komersil') { + $uangMuka = $hargaProperti * 0.15; + } + + $totalHarga = $hargaProperti + $biayaAdmin + $biayaNotaris + $uangMuka; + + $sisaBayar = $totalHarga - $bookingFee; + @endphp + +
+
+ Harga Properti + + Rp {{ number_format($properti->harga_properti ?? 0, 0, ',', '.') }} + +
+ +
+ Biaya Administrasi + Rp {{ number_format($biayaAdmin,0,',','.') }} +
+ +
+ Biaya Notaris & Akta + Rp {{ number_format($biayaNotaris,0,',','.') }} +
+ + @if($uangMuka > 0) +
+ Uang Muka 15% + Rp {{ number_format($uangMuka,0,',','.') }} +
+ @endif + +
+ +
+ Total Harga + Rp {{ number_format($totalHarga,0,',','.') }} +
+ + + +
+ +
+ Sisa Pembayaran Lunas + Rp {{ number_format($sisaBayar,0,',','.') }} +
+
+
+ + + + + +
+ +
+
+ + Metode Pembayaran +
+
+ +
+ +
+
+ Transfer Bank +
+ + +
+
+
+
+
+ +
+
+
+ Bank BCA +
+ +
+ Transfer via BCA +
+
+
+
+ +
+
+ Bank + BCA +
+
+ No. Rekening +
+ + 8720193456 + + +
+
+
+ A/N + PT Kavling Bersama Indonesia +
+
+ Total Bayar + + Rp {{ number_format($sisaBayar,0,',','.') }} + +
+
+
+ + + +
+
+
+
+
+ +
+
+
+ Bank Mandiri +
+
+ Transfer via Mandiri +
+
+
+
+ +
+
+ Bank + Mandiri +
+
+ No. Rekening +
+ + 1400012345678 + + +
+
+ +
+ A/N + PT Kavling Bersama Indonesia +
+ +
+ Total Bayar + + + Rp {{ number_format($sisaBayar,0,',','.') }} + +
+
+
+
+ + +
+
QRIS
+
+ +
+
+
+
+ +
+ +
+
+ QRIS +
+ +
+ Scan QR dari aplikasi apapun +
+
+
+
+ + +
+ +
+ + QRIS + + + +

+ Scan QR menggunakan: +

+ +
+ DANA + OVO + GoPay + ShopeePay + Mobile Banking +
+ +
+ Total Bayar + + + Rp {{ number_format($sisaBayar,0,',','.') }} + +
+ +
+ Berlaku hingga: + + 23:59:59 + +
+
+
+
+
+
+
+ + +
+
+
+ + Upload Bukti Pembayaran +
+
+ +
+ + Setelah melakukan transfer, upload bukti pembayaran untuk konfirmasi. Admin akan memverifikasi dalam 1×24 jam. +
+ +
+ +
+
+
+
bukti_transfer.jpg
+
2.4 MB
+
+ +
+
+ +
+ + +
+ +
+ + + +
+ + + +
+ +
+ +
+ +
+ Klik untuk upload bukti pembayaran +
+ +
+ JPG, PNG, PDF • Maksimal 5MB +
+ + +
+ + + + +
+ + +
+
+ + +
+ + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + diff --git a/resources/views/pembayran_lunas.blade.php b/resources/views/pembayran_lunas.blade.php deleted file mode 100644 index 217ba18..0000000 --- a/resources/views/pembayran_lunas.blade.php +++ /dev/null @@ -1,1083 +0,0 @@ - - - - - - Transaksi Lunas - Carani Estate - - - - - -
-
- - - - - - - -
-
- -
-
- -
-
-
- Pemesanan -
-
-
-
- Verifikasi -
-
-
-
3
- Pembayaran -
-
-
-
4
- Serah Terima -
-
- - -
-
-
- -
-
-

Dokumen Anda Terverifikasi!

-

Selamat, dokumen pemesanan Anda telah diverifikasi oleh admin. Silakan lakukan pembayaran untuk melanjutkan proses transaksi dan mendapatkan notifikasi serah terima.

-
-
-
- -
- -
-
- -

Detail Pemesanan

-
-
-
- - Terverifikasi -
-
- Tipe Rumah - Tipe 36 - Griya Harmoni -
-
- Luas Tanah / Bangunan - 72m² / 36m² -
-
- Lokasi - Bondowoso, Jawa Timur -
-
- Tanggal Pesan - 15 Januari 2025 -
-
- No. Pemesanan - #PMS-2025-00142 -
-
- Status Dokumen - Terverifikasi -
-
-
- - -
-
- -

Rincian Pembayaran

-
-
-
- Harga Rumah - Rp 150.000.000 -
-
- Metode Bayar - Kredit / Cicilan -
-
- Uang Muka (DP 20%) - Rp 30.000.000 -
-
- Sisa / Total Pinjaman - Rp 120.000.000 -
-
- Tenor - 10 Tahun (120 Bulan) -
-
- Bunga per Tahun - 8.5% Fixed -
-
- - Cicilan/Bulan - - - Rp 1.493.000 - -
-
- - Silakan transfer DP ke rekening BCA 123-456-7890 a.n PT. Carani Bhanu Balakosa -
-
-
-
- - -
-
- -

Upload Bukti Pembayaran

-
-
-
- -
- -
-
-

Seret & Lepas File di Sini

-

atau Pilih File

- Format: JPG, PNG, PDF (Maks. 5MB) -
- Preview - -
- - - -
- - -
-
-
-
-
- - - - -
-
-
- - - - - diff --git a/resources/views/register.blade.php b/resources/views/register.blade.php index 6e6be51..433fabb 100644 --- a/resources/views/register.blade.php +++ b/resources/views/register.blade.php @@ -372,20 +372,24 @@

Carani Estate

-

Platform terpercaya untuk membeli, menjual, dan menyewa properti berkualitas sejak 2015

- +

+ Platform properti terpercaya untuk membantu Anda menemukan dan membeli properti dengan proses yang mudah dan aman. +

+ @@ -401,44 +405,63 @@

Daftar sekarang untuk mengakses semua fitur layanan kami

-
+ + @csrf +
- +
- +
- +
- +
- +
- +
- +
- + @@ -446,12 +469,17 @@
- +
- + @@ -461,9 +489,11 @@ - +
diff --git a/resources/views/riwayat-pemesanan.blade.php b/resources/views/riwayat-pemesanan.blade.php index 02afa0f..afb0100 100644 --- a/resources/views/riwayat-pemesanan.blade.php +++ b/resources/views/riwayat-pemesanan.blade.php @@ -147,6 +147,7 @@ justify-content: center; cursor: pointer; transition: all 0.3s ease; + text-decoration: none; } .profile-icon:hover { @@ -435,6 +436,26 @@ background: #dbeafe; color: #2563eb; } + + .payment-cash{ + background: #dcfce7; + color: #166534; + } + + .payment-kpr{ + background: #dbeafe; + color: #1d4ed8; + } + + .payment-bertahap{ + background: #e0f2fe; + color: #0369a1; + } + + .payment-default{ + background: #e2e8f0; + color: #475569; + } .price { font-weight: 700; @@ -451,6 +472,7 @@ cursor: pointer; transition: all 0.3s ease; margin-right: 5px; + text-decoration: none; } .btn-view { @@ -634,6 +656,24 @@ font-size: 0.9rem; color: #cbd5e0; } + + /* Profilll */ + .profile-avatar, + .profile-avatar-default { + width: 35px; + height: 35px; + border-radius: 50%; + object-fit: cover; + } + + .profile-avatar-default { + background: #7AB2D3; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } /* Responsive Design */ @media (max-width: 992px) { @@ -786,6 +826,7 @@ + {{ Auth::check() ? 'LOGIN BERHASIL' : 'BELUM LOGIN' }}
@@ -931,33 +997,49 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> Properti Tanggal Pemesanan Harga - Status + Metode Pembayaran + Tahap Aksi @forelse($transaksi as $t) @php - $statusClass = match($t->status_transaksi) { - 'menunggu_pembayaran' => 'status-pending', - 'menunggu_verifikasi' => 'status-pending', - 'berhasil' => 'status-approved', - 'ditolak' => 'status-rejected', - default => 'status-pending' - }; - $statusLabel = match($t->status_transaksi) { - 'menunggu_pembayaran' => 'Menunggu Pembayaran', - 'menunggu_verifikasi' => 'Menunggu Verifikasi', - 'berhasil' => 'Berhasil', - 'ditolak' => 'Ditolak', - default => $t->status_transaksi - }; - @endphp + $tahap = $t->pemesanan->tahap_saat_ini ?? ''; + $status = $t->pemesanan->status ?? ''; + + // normalisasi biar aman dari huruf besar/kecil & spasi + $tahapClean = strtolower(trim($tahap)); + + // STATUS SELESAI + if (in_array($tahapClean, ['serah terima rumah', 'selesai'])) { + + $statusClass = 'status-approved'; + $statusLabel = 'Selesai'; + + } + + // STATUS DITOLAK + elseif ($status == 'Ditolak') { + + $statusClass = 'status-rejected'; + $statusLabel = 'Ditolak'; + + } + + // STATUS PROSES + else { + + $statusClass = 'status-pending'; + $statusLabel = $tahap ?: 'Proses'; + + } + @endphp {{ $loop->iteration }}
- +

{{ $t->properti->nama_properti ?? '-' }}

Tipe {{ $t->properti->tipe_properti ?? '-' }}

@@ -966,26 +1048,53 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> {{ \Carbon\Carbon::parse($t->tanggal_transaksi)->format('d M Y') }} Rp {{ number_format($t->total_harga, 0, ',', '.') }} + + @php + $metode = strtolower($t->pemesanan->metode_pembayaran ?? ''); + + $metodeClass = match($metode) { + 'lunas' => 'payment-cash', + 'kredit' => 'payment-kpr', + default => 'payment-default' + }; + + $metodeLabel = match($metode) { + 'lunas' => 'Lunas', + 'kredit' => 'KPR', + default => '-' + }; + @endphp + + + + {{ $metodeLabel }} + + @if($t->bank_kpr) + - {{ $t->bank_kpr }} + @endif + + + {{ $statusLabel }} - Lihat - + @empty - + Belum ada riwayat pemesanan @@ -1000,7 +1109,6 @@ class="action-btn btn-view">
-
- - -
-
-
-
-
10.000+
-
Properti Terbangun
-
-
-
8.500+
-
Keluarga Bahagia
-
-
-
25+
-
Lokasi Strategis
-
-
-
98%
-
Kepuasan Pelanggan
-
-
-
-
- -
-
-

Tim Manajemen

-

Profesional berpengalaman yang siap melayani Anda dengan sepenuh hati.

- -
-
-
B
-

Bambang Sutrisno

-

Direktur Utama

-
- - -
-
- -
-
S
-

Sari Dewi

-

Manajer Pemasaran

-
- - -
-
- -
-
R
-

Rudi Hartono

-

Manajer Proyek

-
- - -
-
- -
-
N
-

Nayla Putri

-

Customer Service

-
- - -
-
-
-
-
@@ -1679,7 +1677,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
- + Jelajahi Properti @@ -1711,24 +1709,15 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> - + @@ -2193,22 +2240,32 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
- +
-

Pendampingan KPR

+ +

Monitoring Pemesanan

+

- Kami bantu proses pengajuan KPR Anda dari awal hingga cair, dengan bank partner terpercaya. + Pantau seluruh proses pemesanan properti mulai dari verifikasi data hingga serah terima rumah secara transparan.

+
    -
  • Simulasi cicilan gratis
  • -
  • Bantuan dokumen lengkap
  • -
  • Proses cepat 7-14 hari
  • +
  • Tracking progres transaksi
  • +
  • Update status real-time
  • +
  • Dokumen digital lengkap
- - Ajukan KPR - + + @auth + + Lihat Monitoring + + @else + + Login untuk Monitoring + + @endauth
@@ -2240,7 +2297,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
-
+
@@ -2252,7 +2309,7 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}">
@@ -2290,7 +2347,12 @@ class="nav-item {{ request()->routeIs('kontak') ? 'active' : '' }}"> @foreach($unggulan as $p)
- {{ $p->nama_properti }} + {{ $p->nama_properti }}
{{ ucfirst($p->jenis_properti) }} @@ -2321,7 +2383,7 @@ class="btn-primary properti-btn"> -
+
@@ -2633,7 +2695,6 @@ class="btn-primary properti-btn"> -