sistem fix

acc dosen
This commit is contained in:
bayughazali 2026-06-29 08:26:27 +07:00
parent aa29baab24
commit a7c178d541
29 changed files with 920 additions and 444 deletions

View File

@ -18,7 +18,7 @@ class AdminForgotPasswordController extends Controller
// STEP 1 — Tampilkan form input email
// ─────────────────────────────────────────────────────────
protected FonnteService $fonnte;
protected $fonnte; // ✅
public function __construct(FonnteService $fonnte)
{
@ -84,24 +84,27 @@ private function sendOtpViaEmail(Request $request)
private function sendOtpViaWhatsapp(Request $request)
{
$request->validate([
'phone' => ['required', 'digits_between:10,15'],
], [
'phone' => ['required', 'min:10', 'max:15'],
], [
'phone.required' => 'Nomor WhatsApp wajib diisi.',
'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).',
]);
'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)
$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($request->phone, '0'));
->orWhere('phone', '0' . ltrim($phone, '0'))
->orWhere('phone', '+62' . ltrim($phone, '0'));
})
->first();
$phoneFormatted = FonnteService::formatPhone($phone);
if (!$admin) {
return back()->withErrors([
'phone' => 'Nomor ini tidak terdaftar sebagai admin.',
@ -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) {
$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');

View File

@ -77,20 +77,24 @@ private function sendOtpViaEmail(Request $request)
private function sendOtpViaWhatsapp(Request $request)
{
$request->validate([
'phone' => ['required', 'digits_between:10,15'],
], [
'phone' => ['required', 'min:10', 'max:15'],
], [
'phone.required' => 'Nomor WhatsApp wajib diisi.',
'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).',
]);
'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)
$user = User::where('phone', $phone)
->orWhere('phone', '+' . $phone)
->orWhere('phone', $request->phone)
->orWhere('phone', '0' . ltrim($request->phone, '0'))
->orWhere('phone', '0' . ltrim($phone, '0'))
->orWhere('phone', '+62' . ltrim($phone, '0'))
->first();
$phoneFormatted = FonnteService::formatPhone($phone);
if (!$user) {
return back()->withErrors([
'phone' => 'Nomor WhatsApp tidak ditemukan di sistem kami.',

View File

@ -24,11 +24,11 @@ public function register(Request $request): RedirectResponse
$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+$/',
'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.',
@ -36,38 +36,35 @@ public function register(Request $request): RedirectResponse
'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.',
'phone.min' => 'Nomor telepon minimal 10 karakter.',
'address.required' => 'Alamat wajib diisi.',
'address.min' => 'Alamat minimal harus 12 karakter.',
'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.',
'password.regex' => 'Password tidak boleh mengandung spasi.',
'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();
}
}

View File

@ -11,11 +11,13 @@
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);
@ -38,14 +40,26 @@ public function index(Request $request)
$ongkir = 15000;
$total = $subtotal + $ongkir;
// Panggil view checkout.index (form checkout)
return view('checkout.index', compact('product', 'quantity', 'subtotal', 'ongkir', 'total'));
$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])/'],
@ -66,8 +80,8 @@ 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.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,
]);
}
}

View File

@ -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();
}
}
}

View File

@ -33,6 +33,7 @@ public function index()
'products.price',
'products.stock'
)
->having('total_sold', '>=', 100) // ← tambah baris ini
->orderByDesc('total_sold')
->take(10)
->get();

View File

@ -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!');
}
}

View File

@ -57,9 +57,8 @@ public function store(Request $request)
'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
],
'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'));
}
/**
@ -144,8 +142,7 @@ public function update(Request $request, User $user)
'regex:/^[^\s]+@gmail\.com$/'
],
'phone' => [
'required', 'string', 'min:12',
'regex:/^(\+62)[0-9]+$/'
'required', 'string', 'min:10',
],
'address' => ['required', 'string', 'min:10'],
], [
@ -159,8 +156,7 @@ 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.',
@ -171,8 +167,10 @@ public function update(Request $request, User $user)
'email' => $request->email,
'phone' => $request->phone,
'address' => $request->address,
'is_admin' => $request->has('is_admin') ? 1 : 0,
];
'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.');

View File

@ -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',

View File

@ -23,14 +23,26 @@ public function __construct()
* @return bool
*/
public function sendMessage(string $phone, string $message): bool
{
{
try {
// Bersihkan format nomor — hapus semua non-digit
$phoneCleaned = preg_replace('/\D/', '', $phone);
// 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' => $phone,
'target' => $phoneCleaned,
'message' => $message,
'countryCode' => '62', // Indonesia
'countryCode' => '62',
]);
$body = $response->json();
@ -46,7 +58,7 @@ public function sendMessage(string $phone, string $message): bool
Log::error('Fonnte exception: ' . $e->getMessage());
return false;
}
}
}
/**
* Format nomor WA ke format internasional

View File

@ -14,6 +14,8 @@
*/
'name' => env('APP_NAME', 'Laravel'),
'admin_phone' => env('ADMIN_PHONE'),
'fonnte_token' => env('FONNTE_TOKEN'),
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->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) {
//
});
}
};

View File

@ -0,0 +1,29 @@
<?php
// database/migrations/xxxx_add_payment_proof_to_transaksis_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('transaksis', function (Blueprint $table) {
if (!Schema::hasColumn('transaksis', 'payment_proof')) {
$table->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']);
});
}
};

View File

@ -548,7 +548,7 @@
<th>Alasan</th>
<th>Keterangan</th>
<th>Tgl Pengajuan</th>
<th>Status</th>
<th>Tgl Review</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
@ -576,11 +576,11 @@
<small style="color:#9ca3af;">{{ $item->created_at->format('H:i') }}</small>
</td>
<td>
<span class="badge-status badge-{{ $item->status }}">
{{ $item->status === 'pending' ? 'Menunggu' : ($item->status === 'approved' ? 'Disetujui' : 'Ditolak') }}
</span>
@if($item->reviewed_at)
<br><small style="color:#9ca3af;">{{ $item->reviewed_at->format('d M Y') }}</small>
<small style="color:#374151;">{{ $item->reviewed_at->format('d M Y') }}</small><br>
<small style="color:#9ca3af;">{{ $item->reviewed_at->format('H:i') }}</small>
@else
<small style="color:#9ca3af;">-</small>
@endif
</td>
<td class="text-center">

View File

@ -186,23 +186,22 @@ class="form-control @error('email') is-invalid @enderror"
<div class="mb-4">
<label for="phone" class="form-label fw-semibold">Nomor WhatsApp Admin</label>
<div class="input-group">
<span class="input-group-text fw-semibold" style="color:#11998e;">+62</span>
<input type="tel"
class="form-control @error('phone') is-invalid @enderror"
id="phone"
name="phone"
value="{{ old('phone') }}"
placeholder="8123456789"
placeholder="081234567890"
inputmode="numeric">
</div>
<div class="form-text text-muted small">
Masukkan nomor tanpa angka 0 di depan
Masukkan nomor WhatsApp minimal 10 digit
</div>
@error('phone')
<div class="text-danger mt-1 small">{{ $message }}</div>
@enderror
<div id="phoneError" class="text-danger mt-1" style="display:none;">
<small>⚠️ Nomor WhatsApp tidak valid (min 9 digit)</small>
<small>⚠️ Nomor WhatsApp minimal 10 digit</small>
</div>
</div>
</div>
@ -274,11 +273,11 @@ function validateInput() {
submitBtn.disabled = !valid;
} else {
const val = document.getElementById('phone').value.replace(/\D/g, '');
const valid = val.length >= 9 && val.length <= 13;
const valid = val.length >= 10 && val.length <= 15;
document.getElementById('phoneError').style.display =
(val.length > 0 && !valid) ? 'block' : 'none';
submitBtn.disabled = !valid;
}
}
}
document.addEventListener('DOMContentLoaded', function () {

View File

@ -262,8 +262,8 @@
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
flex-wrap: nowrap;
}
.section-title {
font-size: 1rem;
@ -276,10 +276,11 @@
.table-controls {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
align-items: flex-end;
flex-shrink: 0;
}
/* Table */
.data-table {
@ -569,14 +570,13 @@
</div>
<!-- Table Card -->
<div class="table-card">
<div class="table-card-header">
<div class="section-title">
<i class="fas fa-shopping-cart text-success"></i>
Daftar Pesanan ({{ $orders->total() }})
</div>
<div class="table-controls">
<select class="form-select form-select-sm" id="filterStatus" onchange="filterOrders()" style="min-width:140px;">
<select class="form-select form-select-sm" id="filterStatus" onchange="filterOrders()" style="min-width:160px;">
<option value="">Semua Status</option>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
@ -584,11 +584,17 @@
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
<select class="form-select form-select-sm" id="filterPayment" onchange="filterOrders()" style="min-width:160px;">
<option value="">Semua Payment Status</option>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
<option value="failed">Failed</option>
</select>
<a href="{{ route('admin.transaksis.export') }}" class="btn btn-success btn-sm">
<i class="fas fa-file-excel me-1"></i>Export Excel
</a>
</div>
</div>
</div>
<div class="table-responsive">
<table class="data-table" id="ordersTable">
@ -608,6 +614,7 @@
<tbody>
@forelse($orders as $order)
<tr data-status="{{ $order->order_status }}"
data-payment="{{ $order->payment_status }}"
class="{{ $order->order_status === 'cancelled' ? 'row-cancelled' : '' }}">
<td><strong>{{ $order->invoice_number }}</strong></td>
<td>{{ $order->customer_name }}</td>
@ -704,13 +711,17 @@ function closeSidebar() {
// Filter orders
function filterOrders() {
const filterValue = document.getElementById('filterStatus').value.toLowerCase();
const filterStatus = document.getElementById('filterStatus').value.toLowerCase();
const filterPayment = document.getElementById('filterPayment').value.toLowerCase();
const rows = document.querySelectorAll('#ordersTable tbody tr[data-status]');
rows.forEach(row => {
const status = row.getAttribute('data-status');
row.style.display = (filterValue === '' || status === filterValue) ? '' : 'none';
const payment = row.getAttribute('data-payment');
const matchStatus = filterStatus === '' || status === filterStatus;
const matchPayment = filterPayment === '' || payment === filterPayment;
row.style.display = (matchStatus && matchPayment) ? '' : 'none';
});
}
}
// Update status via AJAX
function updateStatus(orderId, field, value, selectEl) {

View File

@ -600,7 +600,7 @@ class="form-control @error('name') is-invalid @enderror"
class="form-control @error('price') is-invalid @enderror"
name="price" id="price"
value="{{ old('price', $product->price) }}"
min="0" step="1000" placeholder="0" required>
min="0" step="1" placeholder="0" required>
</div>
@error('price')
<div class="invalid-feedback d-block">{{ $message }}</div>

View File

@ -486,11 +486,11 @@
<i class="fas fa-print"></i>
<span>Print</span>
</a>
<button type="button" class="btn-topbar btn-topbar-delete"
<!-- <button type="button" class="btn-topbar btn-topbar-delete"
data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>
<span>Hapus</span>
</button>
</button> -->
</div>
</header>
@ -747,7 +747,67 @@
</div>
</div>
@endif
{{-- Bukti Pembayaran & Tombol Approve --}}
@if($transaksi->payment_method !== 'cod')
<div class="info-card">
<div class="info-card-header">
<div class="info-card-header-left">
<div class="info-card-icon icon-green">
<i class="fas fa-receipt"></i>
</div>
<div>
<h3>Bukti Pembayaran</h3>
<p>Konfirmasi pembayaran pembeli</p>
</div>
</div>
</div>
<div class="info-card-body">
@if($transaksi->payment_proof)
<img src="{{ Storage::url($transaksi->payment_proof) }}"
alt="Bukti Bayar"
class="img-fluid rounded mb-3 w-100"
style="max-height:350px;object-fit:contain;border:1px solid #e5e7eb;">
@if($transaksi->payment_status === 'paid')
<div class="alert alert-success mb-0" style="font-size:0.84rem;">
<i class="fas fa-check-circle me-2"></i>
Dikonfirmasi lunas pada
<strong>{{ $transaksi->confirmed_at ? \Carbon\Carbon::parse($transaksi->confirmed_at)->format('d M Y H:i') : '-' }}</strong>
</div>
@elseif($transaksi->payment_status === 'waiting_confirmation')
<div class="alert alert-warning mb-3" style="font-size:0.84rem;">
<i class="fas fa-clock me-2"></i>
Menunggu konfirmasi admin
</div>
<form action="{{ route('admin.transaksis.approve', $transaksi->id) }}" method="POST">
@csrf
<button type="submit"
class="btn btn-success w-100 fw-bold"
onclick="return confirm('Konfirmasi pembayaran ini sebagai LUNAS?')"
style="border-radius:8px;">
<i class="fas fa-check-circle me-2"></i>Approve Tandai Lunas
</button>
</form>
@else
<form action="{{ route('admin.transaksis.approve', $transaksi->id) }}" method="POST">
@csrf
<button type="submit"
class="btn btn-success w-100 fw-bold"
onclick="return confirm('Konfirmasi pembayaran ini sebagai LUNAS?')"
style="border-radius:8px;">
<i class="fas fa-check-circle me-2"></i>Approve Tandai Lunas
</button>
</form>
@endif
@else
<div class="text-center py-3" style="color:#9ca3af;">
<i class="fas fa-image fa-2x mb-2 d-block"></i>
<span style="font-size:0.84rem;">Pembeli belum upload bukti pembayaran</span>
</div>
@endif
</div>
</div>
@endif
</div>
<!-- Akhir col-lg-4 -->

View File

@ -631,10 +631,10 @@
class="btn-action btn-view" title="Detail">
<i class="fas fa-eye"></i>
</a>
<a href="{{ route('admin.users.edit', $user) }}"
<!-- <a href="{{ route('admin.users.edit', $user) }}"
class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
</a> -->
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
style="display:inline;"
onsubmit="return confirm('Yakin ingin menghapus pengguna ini?')">
@ -704,9 +704,9 @@ class="btn-action btn-edit" title="Edit">
<a href="{{ route('admin.users.show', $user) }}" class="btn-action btn-view" title="Detail">
<i class="fas fa-eye"></i>
</a>
<a href="{{ route('admin.users.edit', $user) }}" class="btn-action btn-edit" title="Edit">
<!-- <a href="{{ route('admin.users.edit', $user) }}" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i> Edit
</a>
</a> -->
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
style="flex:0;"
onsubmit="return confirm('Yakin ingin menghapus pengguna ini?')">

View File

@ -154,9 +154,9 @@ class="form-control"
<div class="mb-3">
<label class="form-label">Nomor Telepon</label>
<input type="text" name="phone"
value="{{ old('phone', '+62') }}"
placeholder="+62812xxxxxxxx">
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror"
value="{{ old('phone') }}"
placeholder="081234567890">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>

View File

@ -620,8 +620,8 @@ class="form-control-custom"
<label class="form-label-custom">Nomor Telepon</label>
<input type="text" name="phone"
class="form-control-custom @error('phone') is-invalid @enderror"
value="{{ old('phone', $user->phone ?? '+62') }}"
placeholder="+62812xxxxxxxx">
value="{{ old('phone', $user->phone ?? '') }}"
placeholder="081234567890">
@error('phone')<div class="invalid-feedback-custom">{{ $message }}</div>@enderror
</div>
<div class="col-12">

View File

@ -396,10 +396,10 @@
<i class="fas fa-arrow-left"></i>
<span>Kembali</span>
</a>
<a href="{{ route('admin.users.edit', $user) }}" class="btn-topbar btn-topbar-edit">
<!-- <a href="{{ route('admin.users.edit', $user) }}" class="btn-topbar btn-topbar-edit">
<i class="fas fa-edit"></i>
<span>Edit</span>
</a>
</a> -->
@if($user->id !== auth()->id())
<button type="button" class="btn-topbar btn-topbar-delete"
data-bs-toggle="modal" data-bs-target="#deleteModal">
@ -548,14 +548,14 @@
</div>
</div>
<div class="info-card-body" style="display:flex;flex-direction:column;gap:10px;">
<a href="{{ route('admin.users.edit', $user) }}"
<!-- <a href="{{ route('admin.users.edit', $user) }}"
style="width:100%;padding:10px 16px;border-radius:9px;border:none;
background:linear-gradient(135deg,#ca8a04,#a16207);
color:white;font-weight:700;font-size:0.875rem;
display:flex;align-items:center;justify-content:center;gap:8px;
text-decoration:none;transition:all 0.2s;">
<i class="fas fa-edit"></i> Edit Pengguna
</a>
</a> -->
<a href="{{ route('admin.users') }}"
style="width:100%;padding:10px 16px;border-radius:9px;
border:1.5px solid #e5e7eb;background:#f9fafb;

View File

@ -98,17 +98,19 @@ class="form-control @error('email') is-invalid @enderror"
Nomor WhatsApp <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light fw-semibold text-success">+62</span>
<input type="tel"
class="form-control @error('phone') is-invalid @enderror"
id="phone" name="phone"
value="{{ old('phone') }}"
placeholder="8123456789"
placeholder="081234567890"
inputmode="numeric">
</div>
<div class="form-text text-muted">
</div>
<div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Masukkan nomor WhatsApp minimal 10 digit</small>
</div>
<!-- <div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Masukkan nomor tanpa angka 0 di depan</small>
</div>
</div> -->
@error('phone')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
@ -200,10 +202,10 @@ function validateInput() {
submitBtn.disabled = !valid;
} else {
const val = document.getElementById('phone').value.replace(/\D/g, '');
const valid = val.length >= 9 && val.length <= 13;
const valid = val.length >= 10 && val.length <= 15;
document.getElementById('phoneError').style.display = (val.length > 0 && !valid) ? 'block' : 'none';
submitBtn.disabled = !valid;
}
}
}
document.addEventListener('DOMContentLoaded', function () {

View File

@ -76,16 +76,16 @@ class="form-control @error('phone') is-invalid @enderror"
name="phone"
value="{{ old('phone') }}"
required
placeholder="+6281234567890">
placeholder="081234567890">
</div>
<div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Harus diawali +62, contoh: +6281234567890</small>
<small><i class="fas fa-info-circle"></i> Nomor telepon minimal 10 karakter</small>
</div>
@error('phone')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
<div id="phoneError" class="text-danger mt-1" style="display: none;">
<small>Nomor telepon harus diawali +62 dan hanya boleh berisi angka</small>
<small>Nomor telepon minimal 10 karakter</small>
</div>
</div>
@ -99,16 +99,16 @@ class="form-control @error('address') is-invalid @enderror"
name="address"
value="{{ old('address') }}"
required
placeholder="Jl. Contoh No. 123">
placeholder="Jl. Contoh No. 12, RT 01/RW 02, Desa Kademangan">
</div>
<div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Alamat minimal 12 karakter</small>
<small><i class="fas fa-info-circle"></i> Wajib lengkap: Nama Jalan · No. Rumah · RT/RW · Desa/Kelurahan</small>
</div>
@error('address')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
<div id="addressError" class="text-danger mt-1" style="display: none;">
<small>Alamat minimal harus 12 karakter</small>
<small>Alamat terlalu pendek, harap isi lengkap (minimal 20 karakter)</small>
</div>
</div>
@ -255,23 +255,18 @@ class="form-control"
phoneInput.addEventListener('input', function() {
const phoneError = document.getElementById('phoneError');
const numberPattern = /^[0-9]+$/;
// Remove non-numeric characters
if (!/^\+62[0-9]{9,15}$/.test(this.value) && this.value.length > 0) {
if (this.value.length < 10 && this.value.length > 0) {
phoneError.style.display = 'block';
this.classList.add('is-invalid');
} else {
phoneError.style.display = 'none';
this.classList.remove('is-invalid');
}
});
});
addressInput.addEventListener('input', function() {
const addressError = document.getElementById('addressError');
if (this.value.length < 12) {
if (this.value.length < 20) {
addressError.style.display = 'block';
this.classList.add('is-invalid');
} else {
@ -327,7 +322,7 @@ function checkPasswordMatch() {
let isValid = true;
// Check all validations
if (nameInput.value.length < 12) {
if (nameInput.value.length < 8) {
document.getElementById('nameError').style.display = 'block';
nameInput.classList.add('is-invalid');
isValid = false;
@ -340,13 +335,13 @@ function checkPasswordMatch() {
isValid = false;
}
if (!/^\+62[0-9]{9,15}$/.test(phoneInput.value)) {
if (phoneInput.value.length < 10) {
document.getElementById('phoneError').style.display = 'block';
phoneInput.classList.add('is-invalid');
isValid = false;
}
}
if (addressInput.value.length < 12) {
if (addressInput.value.length < 20) {
document.getElementById('addressError').style.display = 'block';
addressInput.classList.add('is-invalid');
isValid = false;

View File

@ -56,7 +56,7 @@
<label class="form-label">Nama Lengkap *</label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}"
value="{{ old('name', $prefill['name'] ?? '') }}"
placeholder="Minimal 8 karakter"
minlength="8" required>
<small class="text-muted">Minimal 8 karakter</small>
@ -69,10 +69,10 @@ class="form-control @error('name') is-invalid @enderror"
<label class="form-label">Nomor Telepon *</label>
<input type="text" name="phone"
class="form-control @error('phone') is-invalid @enderror"
value="{{ old('phone') }}"
placeholder="+6281234567890"
minlength="11" required>
<small class="text-muted">Harus diawali +62, contoh: +6281234567890</small>
value="{{ old('phone', $prefill['phone'] ?? '') }}"
placeholder="081234567890"
minlength="10" required>
<small class="text-muted">Nomor telepon minimal 10 karakter</small>
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
@ -81,7 +81,7 @@ class="form-control @error('phone') is-invalid @enderror"
<label class="form-label">Email *</label>
<input type="email" name="email"
class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}"
value="{{ old('email', $prefill['email'] ?? '') }}"
placeholder="contoh@email.com"
minlength="8" required>
<small class="text-muted">Minimal 8 karakter</small>
@ -101,29 +101,29 @@ class="form-control @error('email') is-invalid @enderror"
<label class="form-label">Kecamatan *</label>
<select name="city" id="kecamatan" class="form-control @error('city') is-invalid @enderror" required>
<option value="">-- Pilih Kecamatan --</option>
<option value="Bondowoso" {{ old('city') == 'Bondowoso' ? 'selected' : '' }}>Bondowoso (Pusat Kota)</option>
<option value="Grujugan" {{ old('city') == 'Grujugan' ? 'selected' : '' }}>Grujugan</option>
<option value="Jambesari Darus Sholah" {{ old('city') == 'Jambesari Darus Sholah' ? 'selected' : '' }}>Jambesari Darus Sholah</option>
<option value="Klabang" {{ old('city') == 'Klabang' ? 'selected' : '' }}>Klabang</option>
<option value="Tenggarang" {{ old('city') == 'Tenggarang' ? 'selected' : '' }}>Tenggarang</option>
<option value="Binakal" {{ old('city') == 'Binakal' ? 'selected' : '' }}>Binakal</option>
<option value="Prajekan" {{ old('city') == 'Prajekan' ? 'selected' : '' }}>Prajekan</option>
<option value="Botolinggo" {{ old('city') == 'Botolinggo' ? 'selected' : '' }}>Botolinggo</option>
<option value="Maesan" {{ old('city') == 'Maesan' ? 'selected' : '' }}>Maesan</option>
<option value="Tamanan" {{ old('city') == 'Tamanan' ? 'selected' : '' }}>Tamanan</option>
<option value="Wonosari" {{ old('city') == 'Wonosari' ? 'selected' : '' }}>Wonosari</option>
<option value="Pujer" {{ old('city') == 'Pujer' ? 'selected' : '' }}>Pujer</option>
<option value="Tlogosari" {{ old('city') == 'Tlogosari' ? 'selected' : '' }}>Tlogosari</option>
<option value="Sukosari" {{ old('city') == 'Sukosari' ? 'selected' : '' }}>Sukosari</option>
<option value="Sumberwringin" {{ old('city') == 'Sumberwringin' ? 'selected' : '' }}>Sumberwringin</option>
<option value="Tegalampel" {{ old('city') == 'Tegalampel' ? 'selected' : '' }}>Tegalampel</option>
<option value="Sempol" {{ old('city') == 'Sempol' ? 'selected' : '' }}>Sempol</option>
<option value="Pakem" {{ old('city') == 'Pakem' ? 'selected' : '' }}>Pakem</option>
<option value="Curahdami" {{ old('city') == 'Curahdami' ? 'selected' : '' }}>Curahdami</option>
<option value="Ijen" {{ old('city') == 'Ijen' ? 'selected' : '' }}>Ijen</option>
<option value="Tapen" {{ old('city') == 'Tapen' ? 'selected' : '' }}>Tapen</option>
<option value="Wringin" {{ old('city') == 'Wringin' ? 'selected' : '' }}>Wringin</option>
<option value="Taman Krocok" {{ old('city') == 'Taman Krocok' ? 'selected' : '' }}>Taman Krocok</option>
<option value="Bondowoso" {{ old('city', $prefill['city'] ?? '') == 'Bondowoso' ? 'selected' : '' }}>Bondowoso (Pusat Kota)</option>
<option value="Grujugan" {{ old('city', $prefill['city'] ?? '') == 'Grujugan' ? 'selected' : '' }}>Grujugan</option>
<option value="Jambesari Darus Sholah" {{ old('city', $prefill['city'] ?? '') == 'Jambesari Darus Sholah' ? 'selected' : '' }}>Jambesari Darus Sholah</option>
<option value="Klabang" {{ old('city', $prefill['city'] ?? '') == 'Klabang' ? 'selected' : '' }}>Klabang</option>
<option value="Tenggarang" {{ old('city', $prefill['city'] ?? '') == 'Tenggarang' ? 'selected' : '' }}>Tenggarang</option>
<option value="Binakal" {{ old('city', $prefill['city'] ?? '') == 'Binakal' ? 'selected' : '' }}>Binakal</option>
<option value="Prajekan" {{ old('city', $prefill['city'] ?? '') == 'Prajekan' ? 'selected' : '' }}>Prajekan</option>
<option value="Botolinggo" {{ old('city', $prefill['city'] ?? '') == 'Botolinggo' ? 'selected' : '' }}>Botolinggo</option>
<option value="Maesan" {{ old('city', $prefill['city'] ?? '') == 'Maesan' ? 'selected' : '' }}>Maesan</option>
<option value="Tamanan" {{ old('city', $prefill['city'] ?? '') == 'Tamanan' ? 'selected' : '' }}>Tamanan</option>
<option value="Wonosari" {{ old('city', $prefill['city'] ?? '') == 'Wonosari' ? 'selected' : '' }}>Wonosari</option>
<option value="Pujer" {{ old('city', $prefill['city'] ?? '') == 'Pujer' ? 'selected' : '' }}>Pujer</option>
<option value="Tlogosari" {{ old('city', $prefill['city'] ?? '') == 'Tlogosari' ? 'selected' : '' }}>Tlogosari</option>
<option value="Sukosari" {{ old('city', $prefill['city'] ?? '') == 'Sukosari' ? 'selected' : '' }}>Sukosari</option>
<option value="Sumberwringin" {{ old('city', $prefill['city'] ?? '') == 'Sumberwringin' ? 'selected' : '' }}>Sumberwringin</option>
<option value="Tegalampel" {{ old('city', $prefill['city'] ?? '') == 'Tegalampel' ? 'selected' : '' }}>Tegalampel</option>
<option value="Sempol" {{ old('city', $prefill['city'] ?? '') == 'Sempol' ? 'selected' : '' }}>Sempol</option>
<option value="Pakem" {{ old('city', $prefill['city'] ?? '') == 'Pakem' ? 'selected' : '' }}>Pakem</option>
<option value="Curahdami" {{ old('city', $prefill['city'] ?? '') == 'Curahdami' ? 'selected' : '' }}>Curahdami</option>
<option value="Ijen" {{ old('city', $prefill['city'] ?? '') == 'Ijen' ? 'selected' : '' }}>Ijen</option>
<option value="Tapen" {{ old('city', $prefill['city'] ?? '') == 'Tapen' ? 'selected' : '' }}>Tapen</option>
<option value="Wringin" {{ old('city', $prefill['city'] ?? '') == 'Wringin' ? 'selected' : '' }}>Wringin</option>
<option value="Taman Krocok" {{ old('city', $prefill['city'] ?? '') == 'Taman Krocok' ? 'selected' : '' }}>Taman Krocok</option>
</select>
@error('city')
<div class="invalid-feedback">{{ $message }}</div>
@ -148,7 +148,7 @@ class="form-control @error('email') is-invalid @enderror"
class="form-control @error('address') is-invalid @enderror"
rows="4"
placeholder="Contoh: Jl. Merdeka No. 12, RT 02/RW 05, Desa Kademangan, Arah dari pasar ke utara 200m"
minlength="20" required>{{ old('address') }}</textarea>
minlength="20" required>{{ old('address', $prefill['address'] ?? '') }}</textarea>
<small class="text-muted">
Wajib lengkap: Nama Jalan · No. Rumah · RT/RW · Desa/Kelurahan · Ancer-ancer
</small>
@ -163,7 +163,7 @@ class="form-control @error('address') is-invalid @enderror"
<label class="form-label">Kode Pos *</label>
<input type="text" name="postal_code"
class="form-control @error('postal_code') is-invalid @enderror"
value="{{ old('postal_code', '68200') }}"
value="{{ old('postal_code', $prefill['postal_code'] ?? '68200') }}"
placeholder="68200"
maxlength="5" required>
@error('postal_code')

View File

@ -359,15 +359,80 @@ class="border p-2 bg-white">
@endif
<div class="alert alert-info mt-3 mb-0">
<i class="fas fa-info-circle me-2"></i>
Setelah pembayaran berhasil, mohon konfirmasi melalui WhatsApp ke <strong>0813-3183-0561</strong> dengan menyertakan <strong>bukti pembayaran</strong> dan <strong>nomor invoice: {{ $transaksi->invoice_number }}</strong>
</div>
Setelah pembayaran, silakan <strong>upload bukti pembayaran</strong> di form di bawah ini. Admin akan mengkonfirmasi pembayaran Anda.
</div>
</div>
</div>
@endif
{{-- Card Tombol Aksi (tidak ikut print) --}}
<div class="card success-card mt-4">
<div class="card-body p-4 text-center">
{{-- Card Tombol Aksi (tidak ikut print) --}}
<div class="card success-card mt-4">
<div class="card-body p-4">
{{-- Flash message --}}
@if(session('success'))
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>{{ $errors->first() }}
</div>
@endif
{{-- Form Upload Bukti Bayar (hanya untuk non-COD & belum lunas) --}}
@if($transaksi->payment_method !== 'cod' && $transaksi->payment_status !== 'paid')
@if(!$transaksi->fresh()->payment_proof)
<div class="mb-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-upload me-2 text-success"></i>Upload Bukti Pembayaran
</h6>
<form action="{{ route('checkout.konfirmasi', $transaksi->id) }}"
method="POST"
enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label class="form-label text-muted small">
Upload foto/screenshot bukti transfer (JPG/PNG, maks 2MB)
</label>
<input type="file"
name="payment_proof"
class="form-control @error('payment_proof') is-invalid @enderror"
accept="image/jpg,image/jpeg,image/png"
required>
@error('payment_proof')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-paper-plane me-2"></i>Kirim Konfirmasi Pembayaran ke Admin
</button>
</form>
</div>
<hr>
@else
<div class="alert alert-info mb-4">
<i class="fas fa-clock me-2"></i>
Bukti pembayaran sudah dikirim. <strong>Menunggu konfirmasi admin.</strong>
</div>
@endif
@endif
@if($transaksi->payment_method === 'cod')
<div class="alert alert-secondary mb-4">
<i class="fas fa-truck me-2"></i>
Pembayaran COD dilakukan saat pesanan tiba. Status akan diperbarui oleh admin.
</div>
@endif
@if($transaksi->payment_status === 'paid')
<div class="alert alert-success mb-4">
<i class="fas fa-check-circle me-2"></i>
Pembayaran Anda telah <strong>dikonfirmasi lunas</strong> oleh admin!
</div>
@endif
<div class="row g-3">
<div class="col-md-6">
<a href="{{ route('home') }}" class="btn btn-outline-success w-100">
@ -380,20 +445,9 @@ class="border p-2 bg-white">
</button>
</div>
</div>
<div class="mt-3">
<a href="https://wa.me/6282313155053?text=Halo, saya ingin konfirmasi pembayaran dengan invoice {{ $transaksi->invoice_number }}"
target="_blank"
class="btn btn-success w-100">
<i class="fab fa-whatsapp me-2"></i>Konfirmasi via WhatsApp
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const transaksiId = {{ $transaksi->id }};
const statusBadge = document.getElementById('statusBadge');

View File

@ -362,43 +362,134 @@ class="card-img-top product-image"
</section>
</div>
<!-- Modal Konfirmasi Alamat -->
<div class="modal fade" id="addressModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="fas fa-map-marker-alt me-2"></i>Pilih Alamat Pengiriman
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="savedAddressSection">
<div class="alert alert-success">
<i class="fas fa-user-check me-2"></i>
<strong>Gunakan data yang tersimpan?</strong>
</div>
<div class="card border-success mb-3">
<div class="card-body">
<p class="mb-1"><i class="fas fa-user me-2 text-success"></i><span id="savedName">-</span></p>
<p class="mb-1"><i class="fas fa-phone me-2 text-success"></i><span id="savedPhone">-</span></p>
<p class="mb-1"><i class="fas fa-envelope me-2 text-success"></i><span id="savedEmail">-</span></p>
<p class="mb-0"><i class="fas fa-map-marker-alt me-2 text-success"></i><span id="savedAddress">-</span></p>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-success" id="useSavedAddressBtn">
<i class="fas fa-check-circle me-2"></i>Ya, Gunakan Data Ini
</button>
<button class="btn btn-outline-secondary" id="useNewAddressBtn">
<i class="fas fa-plus-circle me-2"></i>Gunakan Alamat Baru
</button>
</div>
</div>
<div id="noSavedAddressSection" style="display:none;">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Belum ada data tersimpan. Anda akan mengisi form checkout secara manual.
</div>
<div class="d-grid">
<button class="btn btn-success" id="continueNewBtn">
<i class="fas fa-arrow-right me-2"></i>Lanjut ke Checkout
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Cek status login dari Laravel (true/false)
const isLoggedIn = @json(auth()->check());
const loginUrl = "{{ route('login') }}";
let pendingProductId = null;
let pendingQtyKey = null;
let savedUserData = null;
function increaseQty(productId, maxStock) {
const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value);
if (value < maxStock) {
input.value = value + 1;
}
if (value < maxStock) input.value = value + 1;
}
function decreaseQty(productId) {
const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value);
if (value > 1) {
input.value = value - 1;
}
if (value > 1) input.value = value - 1;
}
function checkout(productId) {
function openAddressModal(productId, qtyKey) {
if (!isLoggedIn) {
window.location.href = loginUrl;
return;
}
const quantity = document.getElementById('qty-' + productId).value;
window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`;
pendingProductId = productId;
pendingQtyKey = qtyKey;
fetch("{{ route('checkout.last-address') }}")
.then(r => r.json())
.then(data => {
savedUserData = data;
const hasData = data.name || data.address;
if (hasData) {
document.getElementById('savedName').textContent = data.name || '-';
document.getElementById('savedPhone').textContent = data.phone || '-';
document.getElementById('savedEmail').textContent = data.email || '-';
document.getElementById('savedAddress').textContent =
[data.address, data.city, data.postal_code].filter(Boolean).join(', ') || '-';
document.getElementById('savedAddressSection').style.display = 'block';
document.getElementById('noSavedAddressSection').style.display = 'none';
} else {
document.getElementById('savedAddressSection').style.display = 'none';
document.getElementById('noSavedAddressSection').style.display = 'block';
}
new bootstrap.Modal(document.getElementById('addressModal')).show();
})
.catch(() => {
goToCheckout(false);
});
}
function goToCheckout(useSaved) {
const qty = document.getElementById('qty-' + pendingQtyKey).value;
let url = `/checkout?product_id=${pendingProductId}&quantity=${qty}`;
if (useSaved) url += '&use_saved=1';
const modalEl = document.getElementById('addressModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
window.location.href = url;
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('useSavedAddressBtn')?.addEventListener('click', () => goToCheckout(true));
document.getElementById('useNewAddressBtn')?.addEventListener('click', () => goToCheckout(false));
document.getElementById('continueNewBtn')?.addEventListener('click', () => goToCheckout(false));
});
function checkout(productId) {
openAddressModal(productId, productId);
}
function checkoutAll(productId, inputKey) {
if (!isLoggedIn) {
window.location.href = loginUrl;
return;
}
const quantity = document.getElementById('qty-' + inputKey).value;
window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`;
openAddressModal(productId, inputKey);
}
</script>
@endsection

View File

@ -13,8 +13,7 @@
use App\Http\Controllers\UserController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\AdminForgotPasswordController; // ← tambahkan ini
use App\Http\Controllers\AdminForgotPasswordController;
/*
|--------------------------------------------------------------------------
| Public Routes
@ -45,9 +44,16 @@
Route::get('/', [CheckoutController::class, 'index'])->name('index');
Route::post('/process', [CheckoutController::class, 'process'])->name('process');
Route::get('/success/{id}', [CheckoutController::class, 'success'])->name('success');
Route::get('/status/{id}', [CheckoutController::class, 'getStatus'])->name('status'); // ✅ hapus /checkout/
Route::get('/status/{id}', [CheckoutController::class, 'getStatus'])->name('status');
});
// ✅ Di LUAR group, sejajar dengan group di atas
Route::get('/checkout/last-address', [CheckoutController::class, 'getLastAddress'])
->middleware('auth')
->name('checkout.last-address');
/*
|--------------------------------------------------------------------------
| Authentication Routes
@ -206,3 +212,9 @@
Route::post('/reset-password', [ForgotPasswordController::class, 'resetPassword'])
->name('password.update');
// Konfirmasi pembayaran oleh pembeli (upload bukti)
// Route pembeli upload bukti bayar
Route::post('/checkout/konfirmasi/{id}', [CheckoutController::class, 'konfirmasiPembayaran'])->name('checkout.konfirmasi');
// Route admin approve pembayaran (taruh di group admin/auth)
Route::post('/admin/transaksis/{id}/approve-pembayaran', [TransaksiController::class, 'approvePembayaran'])->name('admin.transaksis.approve');