From a7c178d5411d079f8b6ba73bdd85264bef7bc609 Mon Sep 17 00:00:00 2001 From: bayughazali Date: Mon, 29 Jun 2026 08:26:27 +0700 Subject: [PATCH] sistem fix acc dosen --- .../AdminForgotPasswordController.php | 55 +++-- .../Auth/ForgotPasswordController.php | 28 +-- .../Controllers/Auth/RegisterController.php | 197 +++++++++--------- app/Http/Controllers/CheckoutController.php | 144 ++++++++++--- app/Http/Controllers/ProductController.php | 15 +- .../ProductRecommendationController.php | 1 + app/Http/Controllers/TransaksiController.php | 86 ++++++++ app/Http/Controllers/UserController.php | 43 ++-- app/Models/Transaksi.php | 3 + app/Services/FonnteService.php | 56 +++-- config/app.php | 2 + ...5402_add_address_fields_to_users_table.php | 31 +++ ..._add_payment_proof_to_transaksis_table.php | 29 +++ resources/views/admin/cancellations.blade.php | 10 +- .../views/admin/forgot-password.blade.php | 51 +++-- resources/views/admin/orders.blade.php | 97 +++++---- resources/views/admin/products/edit.blade.php | 2 +- .../views/admin/transaksis/show.blade.php | 64 +++++- resources/views/admin/users.blade.php | 8 +- resources/views/admin/users/Create.blade.php | 6 +- resources/views/admin/users/Edit.blade.php | 6 +- resources/views/admin/users/Show.blade.php | 8 +- .../views/auth/forgot-password.blade.php | 34 +-- resources/views/auth/register.blade.php | 61 +++--- resources/views/checkout/index.blade.php | 68 +++--- resources/views/checkout/success.blade.php | 110 +++++++--- resources/views/home.blade.php | 125 +++++++++-- .../views/products/best-selling.blade.php | 4 +- routes/web.php | 20 +- 29 files changed, 920 insertions(+), 444 deletions(-) create mode 100644 database/migrations/2026_06_04_005402_add_address_fields_to_users_table.php create mode 100644 database/migrations/2026_06_04_034824_add_payment_proof_to_transaksis_table.php diff --git a/app/Http/Controllers/AdminForgotPasswordController.php b/app/Http/Controllers/AdminForgotPasswordController.php index af41dd5..233953b 100644 --- a/app/Http/Controllers/AdminForgotPasswordController.php +++ b/app/Http/Controllers/AdminForgotPasswordController.php @@ -18,7 +18,7 @@ class AdminForgotPasswordController extends Controller // STEP 1 — Tampilkan form input email // ───────────────────────────────────────────────────────── -protected FonnteService $fonnte; +protected $fonnte; // ✅ public function __construct(FonnteService $fonnte) { @@ -83,24 +83,27 @@ private function sendOtpViaEmail(Request $request) private function sendOtpViaWhatsapp(Request $request) { - $request->validate([ - 'phone' => ['required', 'digits_between:10,15'], - ], [ - 'phone.required' => 'Nomor WhatsApp wajib diisi.', - 'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).', - ]); + $request->validate([ + 'phone' => ['required', 'min:10', 'max:15'], +], [ + 'phone.required' => 'Nomor WhatsApp wajib diisi.', + 'phone.min' => 'Nomor WhatsApp minimal 10 digit.', + 'phone.max' => 'Nomor WhatsApp maksimal 15 digit.', +]); - $phoneFormatted = FonnteService::formatPhone($request->phone); +$phone = preg_replace('/\D/', '', $request->phone); - // Cari user dengan nomor WA ini — WAJIB is_admin = true - $admin = User::where('is_admin', true) - ->where(function ($q) use ($phoneFormatted, $request) { - $q->where('phone', $phoneFormatted) - ->orWhere('phone', '+' . $phoneFormatted) - ->orWhere('phone', $request->phone) - ->orWhere('phone', '0' . ltrim($request->phone, '0')); - }) - ->first(); +$admin = User::where('is_admin', true) + ->where(function ($q) use ($phone, $request) { + $q->where('phone', $phone) + ->orWhere('phone', '+' . $phone) + ->orWhere('phone', $request->phone) + ->orWhere('phone', '0' . ltrim($phone, '0')) + ->orWhere('phone', '+62' . ltrim($phone, '0')); + }) + ->first(); + +$phoneFormatted = FonnteService::formatPhone($phone); if (!$admin) { return back()->withErrors([ @@ -117,11 +120,21 @@ private function sendOtpViaWhatsapp(Request $request) . "Berlaku selama *3 menit*.\n" . "Jangan berikan kode ini kepada siapapun."; - $sent = $this->fonnte->sendMessage($phoneFormatted, $pesan); + \Illuminate\Support\Facades\Log::info('Fonnte Admin Debug', [ + 'phone_input' => $request->phone, + 'phone_formatted' => $phoneFormatted, + 'admin_found' => $admin->name, + 'admin_phone_db' => $admin->phone, +]); - if (!$sent) { - return back()->with('error', 'Gagal mengirim pesan WhatsApp. Coba lagi.'); - } +$sent = $this->fonnte->sendMessage($phoneFormatted, $pesan); + +if (!$sent) { + \Illuminate\Support\Facades\Log::error('Fonnte Admin GAGAL', [ + 'phone_formatted' => $phoneFormatted, + ]); + return back()->with('error', 'Gagal mengirim pesan WhatsApp. Coba lagi.'); +} $request->session()->put('admin_reset_email', $admin->email); $request->session()->put('admin_reset_method', 'whatsapp'); diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 7c37578..903100c 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -76,20 +76,24 @@ private function sendOtpViaEmail(Request $request) // ── Kirim OTP via WhatsApp ── private function sendOtpViaWhatsapp(Request $request) { - $request->validate([ - 'phone' => ['required', 'digits_between:10,15'], - ], [ - 'phone.required' => 'Nomor WhatsApp wajib diisi.', - 'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).', - ]); + $request->validate([ + 'phone' => ['required', 'min:10', 'max:15'], +], [ + 'phone.required' => 'Nomor WhatsApp wajib diisi.', + 'phone.min' => 'Nomor WhatsApp minimal 10 digit.', + 'phone.max' => 'Nomor WhatsApp maksimal 15 digit.', +]); - $phoneFormatted = FonnteService::formatPhone($request->phone); +$phone = preg_replace('/\D/', '', $request->phone); - $user = User::where('phone', $phoneFormatted) - ->orWhere('phone', '+' . $phoneFormatted) - ->orWhere('phone', $request->phone) - ->orWhere('phone', '0' . ltrim($request->phone, '0')) - ->first(); +$user = User::where('phone', $phone) + ->orWhere('phone', '+' . $phone) + ->orWhere('phone', $request->phone) + ->orWhere('phone', '0' . ltrim($phone, '0')) + ->orWhere('phone', '+62' . ltrim($phone, '0')) + ->first(); + +$phoneFormatted = FonnteService::formatPhone($phone); if (!$user) { return back()->withErrors([ diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 7f84bca..37d43d2 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -21,53 +21,50 @@ public function showRegistrationForm(): View public function register(Request $request): RedirectResponse { // Validasi dasar terlebih dahulu - $validator = Validator::make($request->all(), [ - 'name' => 'required|string|min:8|max:255', - 'email' => 'required|string|email|max:255|unique:users|regex:/@gmail\.com$/|regex:/^\S+$/', - 'phone' => 'required|string|regex:/^\+62[0-9]{9,15}$/|max:20', - 'address' => 'required|string|min:12|max:500', - 'password' => 'required|string|min:8|confirmed|regex:/^\S+$/', - 'agree' => 'required|accepted', - ], [ - 'name.required' => 'Nama wajib diisi.', - 'name.min' => 'Nama minimal harus 8 karakter.', - 'email.required' => 'Email wajib diisi.', - 'email.email' => 'Format email tidak valid.', - 'email.unique' => 'Email sudah digunakan.', - 'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.', - 'phone.required' => 'Nomor telepon wajib diisi.', - 'phone.regex' => 'Nomor telepon harus diawali dengan +62 dan hanya boleh berisi angka.', - 'phone.unique' => 'Nomor telepon sudah terdaftar di sistem kami.', - // 'phone.min' => 'Nomor telepon minimal harus 12 karakter.', - 'address.required' => 'Alamat wajib diisi.', - 'address.min' => 'Alamat minimal harus 12 karakter.', - 'password.required' => 'Password wajib diisi.', - 'password.min' => 'Password minimal harus 8 karakter.', - 'password.confirmed' => 'Konfirmasi password tidak cocok.', - 'password.regex' => 'Password tidak boleh mengandung spasi.', - 'agree.required' => 'Anda harus menyetujui syarat dan ketentuan.', - 'agree.accepted' => 'Anda harus menyetujui syarat dan ketentuan.', - ]); + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|min:8|max:255', + 'email' => 'required|string|email|max:255|unique:users|regex:/@gmail\.com$/|regex:/^\S+$/', + 'phone' => 'required|string|min:10|max:20', + 'address' => 'required|string|min:20|max:500', + 'password' => 'required|string|min:8|confirmed', + 'agree' => 'required|accepted', +], [ + 'name.required' => 'Nama wajib diisi.', + 'name.min' => 'Nama minimal harus 8 karakter.', + 'email.required' => 'Email wajib diisi.', + 'email.email' => 'Format email tidak valid.', + 'email.unique' => 'Email sudah digunakan.', + 'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.', + 'phone.required' => 'Nomor telepon wajib diisi.', + 'phone.min' => 'Nomor telepon minimal 10 karakter.', + 'address.required' => 'Alamat wajib diisi.', + 'address.min' => 'Alamat terlalu pendek, minimal 20 karakter. Harap isi lengkap.', + 'password.required' => 'Password wajib diisi.', + 'password.min' => 'Password minimal harus 8 karakter.', + 'password.confirmed'=> 'Konfirmasi password tidak cocok.', + 'agree.required' => 'Anda harus menyetujui syarat dan ketentuan.', + 'agree.accepted' => 'Anda harus menyetujui syarat dan ketentuan.', +]); // Validasi password familiar // Validasi password familiar & pernah dipakai user lain -$validator->after(function ($validator) use ($request) { - if ($request->has('password')) { - if ($this->isCommonPassword($request->password)) { - $validator->errors()->add('password', 'Password yang Anda gunakan terlalu umum. Silakan pilih password yang lebih unik.'); - } +// $validator->after(function ($validator) use ($request) { +// if ($request->has('password')) { +// if ($this->isCommonPassword($request->password)) { +// $validator->errors()->add('password', 'Password yang Anda gunakan terlalu umum. Silakan pilih password yang lebih unik.'); +// } - if ($this->isPasswordAlreadyUsed($request->password)) { - $validator->errors()->add('password', 'Password ini sudah pernah digunakan oleh akun lain. Silakan gunakan password yang berbeda.'); - } - } +// if ($this->isPasswordAlreadyUsed($request->password)) { +// $validator->errors()->add('password', 'Password ini sudah pernah digunakan oleh akun lain. Silakan gunakan password yang berbeda.'); +// } +// } - if ($request->has('phone') && $request->phone) { - if ($this->isPhoneAlreadyUsed($request->phone)) { - $validator->errors()->add('phone', 'Nomor telepon sudah terdaftar di sistem kami.'); - } - } -}); +// if ($request->has('phone') && $request->phone) { +// if ($this->isPhoneAlreadyUsed($request->phone)) { +// $validator->errors()->add('phone', 'Nomor telepon sudah terdaftar di sistem kami.'); +// } +// } +// }); if ($validator->fails()) { return redirect()->back()->withErrors($validator)->withInput(); @@ -91,84 +88,84 @@ public function register(Request $request): RedirectResponse /** * Cek apakah password termasuk password yang umum/familiar */ - private function isCommonPassword(string $password): bool - { - $commonPasswords = [ - // Password numerik umum - '12345678', '123456789', '1234567890', - '87654321', '11111111', '22222222', '33333333', '44444444', '55555555', - '66666666', '77777777', '88888888', '99999999', '00000000', + // private function isCommonPassword(string $password): bool + // { + // $commonPasswords = [ + // // Password numerik umum + // '12345678', '123456789', '1234567890', + // '87654321', '11111111', '22222222', '33333333', '44444444', '55555555', + // '66666666', '77777777', '88888888', '99999999', '00000000', - // Password alfabetik umum - 'password', 'Password', 'PASSWORD', 'password123', 'Password123', - 'qwerty123', 'qwertyui', 'asdfghjk', 'zxcvbnm123', - 'abcd1234', 'admin123', 'user1234', 'welcome123', + // // Password alfabetik umum + // 'password', 'Password', 'PASSWORD', 'password123', 'Password123', + // 'qwerty123', 'qwertyui', 'asdfghjk', 'zxcvbnm123', + // 'abcd1234', 'admin123', 'user1234', 'welcome123', - // Password Indonesia umum - 'indonesia', 'Indonesia', 'garuda123', 'nusantara', - 'jakarta123', 'bali2023', 'surabaya', 'bandung123', - 'password1', 'rahasia123', 'bismillah', + // // Password Indonesia umum + // 'indonesia', 'Indonesia', 'garuda123', 'nusantara', + // 'jakarta123', 'bali2023', 'surabaya', 'bandung123', + // 'password1', 'rahasia123', 'bismillah', - // Kombinasi tanggal umum - '01011990', '01011995', '01012000', '17081945', - '12345abc', 'abc12345', '123qwe123', 'qwe12345', + // // Kombinasi tanggal umum + // '01011990', '01011995', '01012000', '17081945', + // '12345abc', 'abc12345', '123qwe123', 'qwe12345', - // Pattern keyboard umum - 'qwerty12', 'asdf1234', 'zxcv1234', '1qaz2wsx', - '1q2w3e4r', 'qazwsxed', 'plokijuh', + // // Pattern keyboard umum + // 'qwerty12', 'asdf1234', 'zxcv1234', '1qaz2wsx', + // '1q2w3e4r', 'qazwsxed', 'plokijuh', - // Password dengan nama umum - 'admin1234', 'guest1234', 'test1234', 'demo1234', - 'sample123', 'example123', 'default123', + // // Password dengan nama umum + // 'admin1234', 'guest1234', 'test1234', 'demo1234', + // 'sample123', 'example123', 'default123', - // Password sosial media umum - 'facebook', 'instagram', 'whatsapp', 'twitter123', - 'gmail123', 'yahoo123', 'google123', + // // Password sosial media umum + // 'facebook', 'instagram', 'whatsapp', 'twitter123', + // 'gmail123', 'yahoo123', 'google123', - // Password gaming umum - 'minecraft', 'roblox123', 'fortnite', 'pubg1234', - 'mobilelegend', 'freefire', 'valorant', + // // Password gaming umum + // 'minecraft', 'roblox123', 'fortnite', 'pubg1234', + // 'mobilelegend', 'freefire', 'valorant', - // Password device umum - 'android123', 'iphone123', 'samsung123', 'nokia1234', + // // Password device umum + // 'android123', 'iphone123', 'samsung123', 'nokia1234', - // Tahun umum - '20232023', '20242024', '19901990', '20002000', - ]; + // // Tahun umum + // '20232023', '20242024', '19901990', '20002000', + // ]; - return in_array(strtolower($password), array_map('strtolower', $commonPasswords)); - } + // return in_array(strtolower($password), array_map('strtolower', $commonPasswords)); + // } /** * Cek apakah password sudah pernah dipakai oleh user lain di database */ -private function isPasswordAlreadyUsed(string $password): bool -{ - $users = User::whereNotNull('password')->get(['password']); +// private function isPasswordAlreadyUsed(string $password): bool +// { +// $users = User::whereNotNull('password')->get(['password']); - foreach ($users as $user) { - if (Hash::check($password, $user->password)) { - return true; - } - } +// foreach ($users as $user) { +// if (Hash::check($password, $user->password)) { +// return true; +// } +// } - return false; +// return false; } /** * Cek nomor telepon sudah terdaftar dalam berbagai format */ - private function isPhoneAlreadyUsed(string $phone): bool - { - $digitsOnly = preg_replace('/\D/', '', $phone); + // private function isPhoneAlreadyUsed(string $phone): bool + // { + // $digitsOnly = preg_replace('/\D/', '', $phone); - $formats = [ - $phone, // +6281234567890 - $digitsOnly, // 6281234567890 - '+' . $digitsOnly, // +6281234567890 - '0' . substr($digitsOnly, 2), // 081234567890 - ]; + // $formats = [ + // $phone, // +6281234567890 + // $digitsOnly, // 6281234567890 + // '+' . $digitsOnly, // +6281234567890 + // '0' . substr($digitsOnly, 2), // 081234567890 + // ]; + + // return User::whereIn('phone', $formats)->exists(); + // } - return User::whereIn('phone', $formats)->exists(); - } -} diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index 42a69d1..1686cc4 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -11,41 +11,55 @@ use Illuminate\Support\Facades\Log; use App\Mail\AdminOrderNotification; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Storage; class CheckoutController extends Controller { public function index(Request $request) - { - try { - $productId = $request->get('product_id'); - $quantity = $request->get('quantity', 1); - - if (!$productId) { - return redirect()->route('home')->with('error', 'Produk tidak ditemukan'); - } - - $product = Product::find($productId); - - if (!$product) { - return redirect()->route('home')->with('error', 'Produk tidak ditemukan'); - } - - if ($product->stock < $quantity) { - return redirect()->back()->with('error', 'Stok tidak mencukupi. Stok tersedia: ' . $product->stock); - } - - $subtotal = $product->price * $quantity; - $ongkir = 15000; - $total = $subtotal + $ongkir; - - // Panggil view checkout.index (form checkout) - return view('checkout.index', compact('product', 'quantity', 'subtotal', 'ongkir', 'total')); - - } catch (\Exception $e) { - Log::error('Checkout Index Error: ' . $e->getMessage()); - return redirect()->route('home')->with('error', 'Terjadi kesalahan: ' . $e->getMessage()); +{ + try { + $productId = $request->get('product_id'); + $quantity = $request->get('quantity', 1); + + if (!$productId) { + return redirect()->route('home')->with('error', 'Produk tidak ditemukan'); } + + $product = Product::find($productId); + + if (!$product) { + return redirect()->route('home')->with('error', 'Produk tidak ditemukan'); + } + + if ($product->stock < $quantity) { + return redirect()->back()->with('error', 'Stok tidak mencukupi. Stok tersedia: ' . $product->stock); + } + + $subtotal = $product->price * $quantity; + $ongkir = 15000; + $total = $subtotal + $ongkir; + + $prefill = null; + if ($request->get('use_saved') && Auth::check()) { + $user = Auth::user(); + $prefill = [ + 'name' => $user->name, + 'phone' => $user->phone ?? '', + 'email' => $user->email, + 'address' => $user->address ?? '', + 'city' => $user->city ?? '', + 'postal_code' => $user->postal_code ?? '68200', + ]; + } + + return view('checkout.index', compact('product', 'quantity', 'subtotal', 'ongkir', 'total', 'prefill')); + + } catch (\Exception $e) { + Log::error('Checkout Index Error: ' . $e->getMessage()); + return redirect()->route('home')->with('error', 'Terjadi kesalahan: ' . $e->getMessage()); } +} public function process(Request $request) { @@ -53,7 +67,7 @@ public function process(Request $request) 'product_id' => 'required|exists:products,id', 'quantity' => 'required|integer|min:1', 'name' => 'required|string|min:8|max:255', - 'phone' => ['required', 'string', 'min:11', 'regex:/^\+62[0-9]{8,}$/'], + 'phone' => ['required', 'string', 'min:10', 'max:15'], 'email' => 'required|email|min:8|max:255', 'address' => ['required', 'string', 'min:20', 'regex:/(?=.*[Jj]l\.?|.*[Jj]alan)(?=.*[Nn]o\.?)(?=.*[Rr][Tt])(?=.*[Rr][Ww])/'], @@ -65,9 +79,9 @@ public function process(Request $request) ], [ 'name.required' => 'Nama lengkap wajib diisi.', 'name.min' => 'Nama lengkap minimal 8 karakter.', - 'phone.required' => 'Nomor telepon wajib diisi.', - 'phone.min' => 'Nomor telepon minimal 11 karakter.', - 'phone.regex' => 'Nomor telepon harus diawali +62 dan hanya berisi angka. Contoh: +6281234567890', + 'phone.required' => 'Nomor telepon wajib diisi.', + 'phone.min' => 'Nomor telepon minimal 10 karakter.', + 'phone.max' => 'Nomor telepon maksimal 15 karakter.', 'email.required' => 'Email wajib diisi.', 'email.email' => 'Format email tidak valid.', 'email.min' => 'Email minimal 8 karakter.', @@ -230,4 +244,68 @@ public function getStatus($id) 'order_status' => $transaksi->order_status, ]); } +public function getLastAddress() +{ + $user = Auth::user(); + return response()->json([ + 'name' => $user->name, + 'phone' => $user->phone ?? '', + 'email' => $user->email, + 'address' => $user->address ?? '', + 'city' => $user->city ?? '', + 'postal_code' => $user->postal_code ?? '', + ]); +} + +// Tambahkan method ini di CheckoutController + +public function konfirmasiPembayaran(Request $request, $id) +{ + $request->validate([ + 'payment_proof' => 'required|image|mimes:jpg,jpeg,png|max:2048', + ], [ + 'payment_proof.required' => 'Bukti pembayaran wajib diupload.', + 'payment_proof.image' => 'File harus berupa gambar.', + 'payment_proof.mimes' => 'Format gambar harus JPG atau PNG.', + 'payment_proof.max' => 'Ukuran gambar maksimal 2MB.', + ]); + + $transaksi = Transaksi::findOrFail($id); + + // Simpan bukti bayar + $path = $request->file('payment_proof')->store('payment_proofs', 'public'); + $transaksi->update([ + 'payment_proof' => $path, + 'payment_status' => 'pending',// menunggu konfirmasi admin + ]); + + // Kirim notifikasi WA ke admin via Fonnte + $this->kirimNotifWaAdmin($transaksi); + + return redirect()->route('home') + ->with('success', 'Bukti pembayaran berhasil dikirim! Admin akan segera mengkonfirmasi.'); +} + +private function kirimNotifWaAdmin(Transaksi $transaksi) +{ + $adminPhone = config('app.admin_phone'); // simpan di .env: ADMIN_PHONE=628131830561 + $fonnteToken = config('app.fonnte_token'); // simpan di .env: FONNTE_TOKEN=xxxxx + + $pesan = "🔔 *Konfirmasi Pembayaran Masuk!*\n\n" + . "📋 Invoice: *{$transaksi->invoice_number}*\n" + . "👤 Pembeli: {$transaksi->customer_name}\n" + . "📞 Telepon: {$transaksi->customer_phone}\n" + . "💳 Metode: " . strtoupper($transaksi->payment_method) . "\n" + . "💰 Total: Rp " . number_format($transaksi->total_amount, 0, ',', '.') . "\n\n" + . "Silakan cek dan approve di panel admin:\n" + . url("/admin/transaksi/{$transaksi->id}"); + + Http::withHeaders([ + 'Authorization' => $fonnteToken, + ])->post('https://api.fonnte.com/send', [ + 'target' => $adminPhone, + 'message' => $pesan, + ]); +} + } \ No newline at end of file diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index b99e934..7c87075 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -48,7 +48,7 @@ public function store(Request $request) $request->validate([ 'name' => 'required|string|max:255', 'category' => 'required|string|max:100', - 'price' => 'required|numeric|min:0', + 'price' => 'required|numeric|min:1', 'stock' => 'required|integer|min:0', 'status' => 'required|in:aktif,nonaktif', 'description' => 'nullable|string', @@ -225,21 +225,24 @@ public function debugImage($id) private function syncLabelAfterSave(Product $product) { if ($product->stock <= 0) { - return; // Sudah di-handle updateLabelBasedOnStock + return; } $bestSellingIds = \Illuminate\Support\Facades\DB::table('transaksi_details') - ->select('product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity) as total_sold')) + ->select('product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity) as total_sold'))// ← ini rumus (1) ->groupBy('product_id') - ->orderByDesc('total_sold') - ->take(10) + ->having('total_sold', '>=', 100) + ->orderByDesc('total_sold') // ← ini rumus (2): si > sj + ->take(4) ->pluck('product_id') ->toArray(); if (in_array($product->id, $bestSellingIds)) { $product->label = 'terlaris'; $product->save(); + } else { + $product->label = 'tersedia'; + $product->save(); } } - } \ No newline at end of file diff --git a/app/Http/Controllers/ProductRecommendationController.php b/app/Http/Controllers/ProductRecommendationController.php index 4462735..6b6bb73 100644 --- a/app/Http/Controllers/ProductRecommendationController.php +++ b/app/Http/Controllers/ProductRecommendationController.php @@ -33,6 +33,7 @@ public function index() 'products.price', 'products.stock' ) + ->having('total_sold', '>=', 100) // ← tambah baris ini ->orderByDesc('total_sold') ->take(10) ->get(); diff --git a/app/Http/Controllers/TransaksiController.php b/app/Http/Controllers/TransaksiController.php index 84611af..54d85dd 100644 --- a/app/Http/Controllers/TransaksiController.php +++ b/app/Http/Controllers/TransaksiController.php @@ -12,6 +12,8 @@ use App\Notifications\OrderStatusChanged; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Storage; class TransaksiController extends Controller { @@ -501,5 +503,89 @@ public function rejectCancellation(Request $request, $id) return back()->with('success', 'Pengajuan pembatalan berhasil ditolak.'); } +/** + * Pembeli upload bukti pembayaran + */ +public function konfirmasiPembayaran(Request $request, $id) +{ + $request->validate([ + 'payment_proof' => 'required|image|mimes:jpg,jpeg,png|max:2048', + ], [ + 'payment_proof.required' => 'Bukti pembayaran wajib diupload.', + 'payment_proof.image' => 'File harus berupa gambar.', + 'payment_proof.mimes' => 'Format gambar harus JPG atau PNG.', + 'payment_proof.max' => 'Ukuran gambar maksimal 2MB.', + ]); + + $transaksi = Transaksi::findOrFail($id); + + $path = $request->file('payment_proof')->store('payment_proofs', 'public'); + + $transaksi->update([ + 'payment_proof' => $path, + 'payment_status' => 'waiting_confirmation', + ]); + + // Kirim notif WA ke admin via Fonnte + try { + $adminPhone = env('ADMIN_PHONE', '628131830561'); + $fonnteToken = env('FONNTE_TOKEN'); + + $pesan = "🔔 *Konfirmasi Pembayaran Masuk!*\n\n" + . "📋 Invoice: *{$transaksi->invoice_number}*\n" + . "👤 Pembeli: {$transaksi->customer_name}\n" + . "📞 Telepon: {$transaksi->customer_phone}\n" + . "💳 Metode: " . strtoupper($transaksi->payment_method) . "\n" + . "💰 Total: Rp " . number_format($transaksi->total_amount, 0, ',', '.') . "\n\n" + . "Silakan approve di panel admin:\n" + . url("/admin/transaksis/{$transaksi->id}"); + + Http::withHeaders(['Authorization' => $fonnteToken]) + ->post('https://api.fonnte.com/send', [ + 'target' => $adminPhone, + 'message' => $pesan, + ]); + } catch (\Exception $e) { + Log::warning('Gagal kirim notif WA admin: ' . $e->getMessage()); + } + + return back()->with('success', 'Bukti pembayaran berhasil dikirim! Menunggu konfirmasi admin.'); +} + +/** + * Admin approve pembayaran → tandai lunas + notif WA ke pembeli + */ +public function approvePembayaran($id) +{ + $transaksi = Transaksi::findOrFail($id); + + $transaksi->update([ + 'payment_status' => 'paid', + 'paid_at' => now(), + 'confirmed_at' => now(), + ]); + + // Kirim notif WA ke pembeli via Fonnte + try { + $fonnteToken = env('FONNTE_TOKEN'); + + $pesan = "✅ *Pembayaran Dikonfirmasi!*\n\n" + . "Halo *{$transaksi->customer_name}*,\n" + . "Pembayaran Anda untuk invoice *{$transaksi->invoice_number}* telah dikonfirmasi.\n\n" + . "📦 Pesanan Anda sedang kami proses dan akan segera dikirim.\n" + . "Terima kasih telah berbelanja di *Shop Bibit Cabai Bondowoso*! 🌶️"; + + Http::withHeaders(['Authorization' => $fonnteToken]) + ->post('https://api.fonnte.com/send', [ + 'target' => $transaksi->customer_phone, + 'message' => $pesan, + ]); + } catch (\Exception $e) { + Log::warning('Gagal kirim notif WA pembeli: ' . $e->getMessage()); + } + + return back()->with('success', 'Pembayaran berhasil dikonfirmasi dan pembeli telah dinotifikasi!'); +} + } \ No newline at end of file diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a28c090..c7b9206 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -56,10 +56,9 @@ public function store(Request $request) 'required', 'confirmed', 'min:8', 'regex:/^(?=.*[a-zA-Z])(?=.*[0-9])\S+$/' // huruf+angka, no spasi ], - 'phone' => [ - 'required', 'string', 'min:12', - 'regex:/^(\+62)[0-9]+$/' // harus diawali +62, hanya angka - ], + 'phone' => [ + 'required', 'string', 'min:10', +], 'address' => ['required', 'string', 'min:10'], ], [ 'name.required' => 'Nama lengkap wajib diisi.', @@ -77,8 +76,7 @@ public function store(Request $request) 'password.regex' => 'Password tidak boleh mengandung spasi dan harus terdiri dari huruf dan angka.', 'phone.required' => 'Nomor telepon wajib diisi.', - 'phone.min' => 'Nomor telepon minimal 12 karakter.', - 'phone.regex' => 'Nomor telepon harus diawali +62 dan hanya boleh berisi angka.', + 'phone.min' => 'Nomor telepon minimal 10 karakter.', 'address.required' => 'Alamat wajib diisi.', 'address.min' => 'Alamat minimal 10 karakter.', @@ -102,7 +100,7 @@ public function store(Request $request) */ public function show(User $user) { - return view('admin.users.show', [ + return view('admin.users.Show', [ 'user' => $user ]); } @@ -118,7 +116,7 @@ public function edit(User $user) ->with('error', 'Anda tidak dapat mengedit akun pengguna lain.'); } - return view('admin.users.edit', compact('user')); + return view('admin.users.Edit', compact('user')); } /** @@ -143,9 +141,8 @@ public function update(Request $request, User $user) 'unique:users,email,' . $user->id, 'regex:/^[^\s]+@gmail\.com$/' ], - 'phone' => [ - 'required', 'string', 'min:12', - 'regex:/^(\+62)[0-9]+$/' + 'phone' => [ + 'required', 'string', 'min:10', ], 'address' => ['required', 'string', 'min:10'], ], [ @@ -159,20 +156,21 @@ public function update(Request $request, User $user) 'email.regex' => 'Email tidak boleh mengandung spasi dan harus menggunakan @gmail.com.', 'phone.required' => 'Nomor telepon wajib diisi.', - 'phone.min' => 'Nomor telepon minimal 12 karakter.', - 'phone.regex' => 'Nomor telepon harus diawali +62 dan hanya boleh berisi angka.', +'phone.min' => 'Nomor telepon minimal 10 karakter.', 'address.required' => 'Alamat wajib diisi.', 'address.min' => 'Alamat minimal 10 karakter.', ]); - $data = [ - 'name' => $request->name, - 'email' => $request->email, - 'phone' => $request->phone, - 'address' => $request->address, - 'is_admin' => $request->has('is_admin') ? 1 : 0, - ]; + $data = [ + 'name' => $request->name, + 'email' => $request->email, + 'phone' => $request->phone, + 'address' => $request->address, + 'is_admin' => ($user->id === auth()->id()) + ? $user->is_admin + : ($request->has('is_admin') ? 1 : 0), +]; if ($request->filled('password')) { $request->validate([ @@ -199,11 +197,6 @@ public function update(Request $request, User $user) */ public function destroy(User $user) { - // Tidak bisa hapus akun siapapun selain diri sendiri - if (auth()->id() !== $user->id) { - return back()->with('error', 'Anda tidak dapat menghapus akun pengguna lain.'); - } - // Tidak bisa hapus akun sendiri if (auth()->id() === $user->id) { return back()->with('error', 'Anda tidak dapat menghapus akun sendiri.'); diff --git a/app/Models/Transaksi.php b/app/Models/Transaksi.php index 0956106..3326b17 100644 --- a/app/Models/Transaksi.php +++ b/app/Models/Transaksi.php @@ -26,6 +26,8 @@ class Transaksi extends Model 'total_amount', 'payment_method', 'payment_status', + 'payment_proof', // ← pastikan ada ini + 'confirmed_at', // ← pastikan ada ini 'order_status', 'notes', 'paid_at' @@ -33,6 +35,7 @@ class Transaksi extends Model protected $casts = [ 'paid_at' => 'datetime', + 'confirmed_at' => 'datetime', // ← tambah ini 'subtotal' => 'decimal:2', 'shipping_cost'=> 'decimal:2', 'total_amount' => 'decimal:2', diff --git a/app/Services/FonnteService.php b/app/Services/FonnteService.php index f767177..1b9b052 100644 --- a/app/Services/FonnteService.php +++ b/app/Services/FonnteService.php @@ -22,31 +22,43 @@ public function __construct() * @param string $message Isi pesan * @return bool */ - public function sendMessage(string $phone, string $message): bool - { - try { - $response = Http::withHeaders([ - 'Authorization' => $this->token, - ])->post($this->apiUrl, [ - 'target' => $phone, - 'message' => $message, - 'countryCode' => '62', // Indonesia - ]); + public function sendMessage(string $phone, string $message): bool +{ + try { + // Bersihkan format nomor — hapus semua non-digit + $phoneCleaned = preg_replace('/\D/', '', $phone); - $body = $response->json(); - - if ($response->successful() && isset($body['status']) && $body['status'] === true) { - return true; - } - - Log::error('Fonnte send failed', ['response' => $body]); - return false; - - } catch (\Exception $e) { - Log::error('Fonnte exception: ' . $e->getMessage()); - return false; + // Pastikan diawali 62 + if (str_starts_with($phoneCleaned, '0')) { + $phoneCleaned = '62' . substr($phoneCleaned, 1); + } elseif (!str_starts_with($phoneCleaned, '62')) { + $phoneCleaned = '62' . $phoneCleaned; } + + Log::info('Fonnte kirim ke: ' . $phoneCleaned); + + $response = Http::withHeaders([ + 'Authorization' => $this->token, + ])->post($this->apiUrl, [ + 'target' => $phoneCleaned, + 'message' => $message, + 'countryCode' => '62', + ]); + + $body = $response->json(); + + if ($response->successful() && isset($body['status']) && $body['status'] === true) { + return true; + } + + Log::error('Fonnte send failed', ['response' => $body]); + return false; + + } catch (\Exception $e) { + Log::error('Fonnte exception: ' . $e->getMessage()); + return false; } +} /** * Format nomor WA ke format internasional diff --git a/config/app.php b/config/app.php index 423eed5..b2e7fb9 100644 --- a/config/app.php +++ b/config/app.php @@ -14,6 +14,8 @@ */ 'name' => env('APP_NAME', 'Laravel'), + 'admin_phone' => env('ADMIN_PHONE'), +'fonnte_token' => env('FONNTE_TOKEN'), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_06_04_005402_add_address_fields_to_users_table.php b/database/migrations/2026_06_04_005402_add_address_fields_to_users_table.php new file mode 100644 index 0000000..616e50c --- /dev/null +++ b/database/migrations/2026_06_04_005402_add_address_fields_to_users_table.php @@ -0,0 +1,31 @@ +string('phone')->nullable()->after('email'); + $table->text('address')->nullable()->after('phone'); + $table->string('city')->nullable()->after('address'); + $table->string('postal_code')->nullable()->after('city'); + }); +} + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_06_04_034824_add_payment_proof_to_transaksis_table.php b/database/migrations/2026_06_04_034824_add_payment_proof_to_transaksis_table.php new file mode 100644 index 0000000..91d8ef1 --- /dev/null +++ b/database/migrations/2026_06_04_034824_add_payment_proof_to_transaksis_table.php @@ -0,0 +1,29 @@ +string('payment_proof')->nullable()->after('payment_method'); + } + if (!Schema::hasColumn('transaksis', 'confirmed_at')) { + $table->timestamp('confirmed_at')->nullable()->after('paid_at'); + } + // Tambah status waiting_confirmation + // (tidak perlu ubah kolom, cukup allow value baru di payment_status) + }); +} + +public function down(): void +{ + Schema::table('transaksis', function (Blueprint $table) { + $table->dropColumn(['payment_proof', 'confirmed_at']); + }); +} +}; \ No newline at end of file diff --git a/resources/views/admin/cancellations.blade.php b/resources/views/admin/cancellations.blade.php index 5074c43..5b2aa6e 100644 --- a/resources/views/admin/cancellations.blade.php +++ b/resources/views/admin/cancellations.blade.php @@ -548,7 +548,7 @@ Alasan Keterangan Tgl Pengajuan - Status + Tgl Review Aksi @@ -576,11 +576,11 @@ {{ $item->created_at->format('H:i') }} - - {{ $item->status === 'pending' ? 'Menunggu' : ($item->status === 'approved' ? 'Disetujui' : 'Ditolak') }} - @if($item->reviewed_at) -
{{ $item->reviewed_at->format('d M Y') }} + {{ $item->reviewed_at->format('d M Y') }}
+ {{ $item->reviewed_at->format('H:i') }} + @else + - @endif diff --git a/resources/views/admin/forgot-password.blade.php b/resources/views/admin/forgot-password.blade.php index 5a9e6d7..32457b2 100644 --- a/resources/views/admin/forgot-password.blade.php +++ b/resources/views/admin/forgot-password.blade.php @@ -181,29 +181,28 @@ class="form-control @error('email') is-invalid @enderror" - {{-- WHATSAPP SECTION --}} -