From b5acd49443fc3b9be2031343af7436ddbf4da589 Mon Sep 17 00:00:00 2001 From: bayughazali Date: Wed, 20 May 2026 00:08:47 +0700 Subject: [PATCH] perbaikan responsive belum100% --- .../AdminForgotPasswordController.php | 160 +- app/Services/FonnteService.php | 23 + ..._05_19_151412_add_no_hp_to_users_table.php | 28 + resources/views/admin/cancellations.blade.php | 934 ++++++++--- resources/views/admin/dashboard.blade.php | 768 +++++++-- resources/views/admin/laporan.blade.php | 1425 +++++++++++------ resources/views/admin/orders.blade.php | 1318 ++++++++------- .../views/admin/products/create.blade.php | 999 +++++++----- resources/views/admin/products/edit.blade.php | 972 ++++++++--- .../views/admin/products/index.blade.php | 1193 +++++++------- .../views/admin/transaksis/edit.blade.php | 782 ++++++--- .../views/admin/transaksis/show.blade.php | 1017 ++++++++---- resources/views/admin/users.blade.php | 839 ++++++---- resources/views/admin/users/Edit.blade.php | 951 ++++++++--- resources/views/admin/users/Show.blade.php | 775 +++++++-- resources/views/admin/verify-otp.blade.php | 197 ++- resources/views/home.blade.php | 116 +- 17 files changed, 8489 insertions(+), 4008 deletions(-) create mode 100644 app/Services/FonnteService.php create mode 100644 database/migrations/2026_05_19_151412_add_no_hp_to_users_table.php diff --git a/app/Http/Controllers/AdminForgotPasswordController.php b/app/Http/Controllers/AdminForgotPasswordController.php index e9b26ee..05c9ec9 100644 --- a/app/Http/Controllers/AdminForgotPasswordController.php +++ b/app/Http/Controllers/AdminForgotPasswordController.php @@ -82,48 +82,52 @@ public function showVerifyOtpForm(Request $request) // ───────────────────────────────────────────────────────── // STEP 2 — Proses: verifikasi OTP // ───────────────────────────────────────────────────────── - public function verifyOtp(Request $request) - { - $request->validate([ - 'otp' => ['required', 'digits:6'], - ], [ - 'otp.required' => 'Kode OTP wajib diisi.', - 'otp.digits' => 'Kode OTP harus 6 digit angka.', - ]); + // STEP 2 — Proses: verifikasi OTP +public function verifyOtp(Request $request) +{ + $request->validate([ + 'otp' => ['required', 'digits:6'], + ], [ + 'otp.required' => 'Kode OTP wajib diisi.', + 'otp.digits' => 'Kode OTP harus 6 digit angka.', + ]); - $email = $request->session()->get('admin_reset_email'); + $email = $request->session()->get('admin_reset_email'); - if (!$email) { - return redirect()->route('admin.password.request') - ->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.'); - } - - $record = DB::table('password_reset_tokens')->where('email', $email)->first(); - - if (!$record) { - return back()->with('error', 'Kode OTP tidak ditemukan. Silakan kirim ulang.'); - } - - if (Carbon::now()->diffInMinutes(Carbon::parse($record->created_at)) > 10) { - DB::table('password_reset_tokens')->where('email', $email)->delete(); - return back()->with('error', 'Kode OTP sudah kadaluarsa. Silakan kirim ulang kode.'); - } - - if (!Hash::check($request->otp, $record->token)) { - return back()->with('error', 'Kode OTP salah. Periksa kembali kode yang Anda masukkan.'); - } - - $verifiedToken = Str::random(60); - DB::table('password_reset_tokens') - ->where('email', $email) - ->update(['token' => Hash::make($verifiedToken)]); - - $request->session()->put('admin_reset_token', $verifiedToken); - - return redirect()->route('admin.password.reset-form') - ->with('success', 'Kode berhasil diverifikasi! Silakan buat password baru.'); + if (!$email) { + return redirect()->route('admin.password.request') + ->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.'); } + $record = DB::table('password_reset_tokens')->where('email', $email)->first(); + + if (!$record) { + return back()->with('error', 'Kode OTP tidak ditemukan. Silakan kirim ulang.'); + } + + // ✅ CEK EXPIRED — hapus record & paksa user kirim ulang + if (Carbon::now()->diffInSeconds(Carbon::parse($record->created_at)) >= 180) { + DB::table('password_reset_tokens')->where('email', $email)->delete(); + return back()->with('otp_expired', true) // flag khusus untuk view + ->with('error', 'Kode OTP sudah kadaluarsa (3 menit). Silakan kirim ulang kode.'); + } + + if (!Hash::check($request->otp, $record->token)) { + return back() + ->with('error', 'Kode OTP salah. Waktu berkurang 30 detik.') + ->with('otp_wrong', true); // ← flag untuk kurangi timer di frontend +} + + $verifiedToken = Str::random(60); + DB::table('password_reset_tokens') + ->where('email', $email) + ->update(['token' => Hash::make($verifiedToken)]); + + $request->session()->put('admin_reset_token', $verifiedToken); + + return redirect()->route('admin.password.reset-form') + ->with('success', 'Kode berhasil diverifikasi! Silakan buat password baru.'); +} // ───────────────────────────────────────────────────────── // STEP 2 — Kirim ulang OTP // ───────────────────────────────────────────────────────── @@ -182,41 +186,53 @@ public function showResetForm(Request $request) // ───────────────────────────────────────────────────────── // STEP 3 — Proses: simpan password baru // ───────────────────────────────────────────────────────── - public function resetPassword(Request $request) - { - $request->validate([ - 'password' => ['required', 'min:8', 'confirmed', 'regex:/^\S+$/'], - 'password_confirmation' => ['required'], - ], [ - 'password.required' => 'Password baru wajib diisi.', - 'password.min' => 'Password minimal 8 karakter.', - 'password.confirmed' => 'Konfirmasi password tidak cocok.', - 'password.regex' => 'Password tidak boleh mengandung spasi.', - ]); + // ───────────────────────────────────────────────────────── +// STEP 3 — Proses: simpan password baru +// ───────────────────────────────────────────────────────── +public function resetPassword(Request $request) +{ + $request->validate([ + 'password' => ['required', 'min:8', 'confirmed', 'regex:/^\S+$/'], + 'password_confirmation' => ['required'], + ], [ + 'password.required' => 'Password baru wajib diisi.', + 'password.min' => 'Password minimal 8 karakter.', + 'password.confirmed' => 'Konfirmasi password tidak cocok.', + 'password.regex' => 'Password tidak boleh mengandung spasi.', + ]); - $email = $request->session()->get('admin_reset_email'); - $token = $request->session()->get('admin_reset_token'); + $email = $request->session()->get('admin_reset_email'); + $token = $request->session()->get('admin_reset_token'); - if (!$email || !$token) { - return redirect()->route('admin.password.request') - ->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.'); - } - - $record = DB::table('password_reset_tokens')->where('email', $email)->first(); - - if (!$record || !Hash::check($token, $record->token)) { - return redirect()->route('admin.password.request') - ->with('error', 'Token tidak valid. Silakan ulangi dari awal.'); - } - - User::where('email', $email) - ->where('is_admin', true) - ->update(['password' => Hash::make($request->password)]); - - DB::table('password_reset_tokens')->where('email', $email)->delete(); - $request->session()->forget(['admin_reset_email', 'admin_reset_token']); - - return redirect()->route('admin.login.form') - ->with('success', 'Password admin berhasil diubah! Silakan login dengan password baru.'); + if (!$email || !$token) { + return redirect()->route('admin.password.request') + ->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.'); } + + $record = DB::table('password_reset_tokens')->where('email', $email)->first(); + + if (!$record || !Hash::check($token, $record->token)) { + return redirect()->route('admin.password.request') + ->with('error', 'Token tidak valid. Silakan ulangi dari awal.'); + } + + // ✅ CEK PASSWORD LAMA — tolak jika sama dengan password sekarang + $admin = User::where('email', $email)->where('is_admin', true)->first(); + + if ($admin && Hash::check($request->password, $admin->password)) { + return back()->withErrors([ + 'password' => 'Password baru tidak boleh sama dengan password yang sedang digunakan.', + ])->withInput(); + } + + User::where('email', $email) + ->where('is_admin', true) + ->update(['password' => Hash::make($request->password)]); + + DB::table('password_reset_tokens')->where('email', $email)->delete(); + $request->session()->forget(['admin_reset_email', 'admin_reset_token']); + + return redirect()->route('admin.login.form') + ->with('success', 'Password admin berhasil diubah! Silakan login dengan password baru.'); +} } \ No newline at end of file diff --git a/app/Services/FonnteService.php b/app/Services/FonnteService.php new file mode 100644 index 0000000..8c5dcb9 --- /dev/null +++ b/app/Services/FonnteService.php @@ -0,0 +1,23 @@ + env('FONNTE_TOKEN'), + ])->post('https://api.fonnte.com/send', [ + 'target' => $nomor, + 'message' => "Kode OTP reset password Anda: *$otp*\n\nBerlaku selama 10 menit. Jangan berikan kode ini kepada siapapun.", + ]); + + return $response->json(); + } +} \ No newline at end of file diff --git a/database/migrations/2026_05_19_151412_add_no_hp_to_users_table.php b/database/migrations/2026_05_19_151412_add_no_hp_to_users_table.php new file mode 100644 index 0000000..7b3ca50 --- /dev/null +++ b/database/migrations/2026_05_19_151412_add_no_hp_to_users_table.php @@ -0,0 +1,28 @@ +string('no_hp')->nullable()->after('email'); + }); +} + + /** + * Reverse the migrations. + */ + public function down() +{ + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('no_hp'); + }); +} +}; diff --git a/resources/views/admin/cancellations.blade.php b/resources/views/admin/cancellations.blade.php index 04d4663..5074c43 100644 --- a/resources/views/admin/cancellations.blade.php +++ b/resources/views/admin/cancellations.blade.php @@ -3,323 +3,765 @@ - Pengajuan Pembatalan - Admin + Pengajuan Pembatalan - Bibit Cabai Admin + + - - Kembali ke Website - + -
- {{-- Sidebar --}} - + + +
+ + +
+
+ +
+

Dashboard

+

Selamat datang di dashboard admin!

+
+
+ + + Kembali ke Website + +
+ + +
+ + +
+
+
{{ $stats['total_products'] }}
+
Total Produk
+ +
+
+
{{ $stats['active_products'] }}
+
Produk Aktif
+ +
+
+
{{ $stats['total_sold'] }}
+
Total Terjual
+ +
+
+
{{ $stats['low_stock'] }}
+
Stok Menipis
+ +
+
+ + +
+
+
📈 Grafik Pendapatan Harian (30 Hari Terakhir)
+ + Lihat Detail + +
+
+ @if(count($chartLabels) > 0) + + @else +
+ +

Belum Ada Data

+

Grafik pendapatan akan ditampilkan setelah ada transaksi

+ + Buka Laporan + +
+ @endif +
+
+ +
+
+ \ No newline at end of file diff --git a/resources/views/admin/orders.blade.php b/resources/views/admin/orders.blade.php index e7e47a8..0ec398b 100644 --- a/resources/views/admin/orders.blade.php +++ b/resources/views/admin/orders.blade.php @@ -1,603 +1,771 @@ - - - - - - Orders Management - Bibit Cabai Admin - - - - - - - - - - + .sidebar-header .brand-icon { font-size: 2rem; margin-bottom: 6px; display: block; } + .sidebar-header h3 { font-size: 1.35rem; font-weight: 700; margin-bottom: 2px; } + .sidebar-header .subtitle { opacity: 0.65; font-size: 0.78rem; margin-bottom: 6px; } + .sidebar-header .admin-name { + display: inline-block; + background: rgba(255,255,255,0.15); + padding: 3px 12px; + border-radius: 20px; + font-size: 0.78rem; + font-weight: 500; + } - - - - Kembali ke Website - + .sidebar-menu { padding: 12px 0; flex: 1; } -
- - + .menu-section-label { + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + opacity: 0.45; + padding: 12px 20px 6px; + } - -
-
- -

Orders Management

-

Kelola semua pesanan pelanggan di sini!

-
+ .menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + color: rgba(255,255,255,0.82); + text-decoration: none; + transition: all 0.2s; + border-left: 3px solid transparent; + font-size: 0.9rem; + } -
+ .menu-item i { width: 18px; font-size: 0.95rem; flex-shrink: 0; text-align: center; } - @if(session('success')) - - @endif + .menu-item:hover { + background: rgba(255,255,255,0.1); + color: white; + text-decoration: none; + border-left-color: rgba(255,255,255,0.5); + } - -
-
-
-
Total Orders
-
{{ $orders->total() }}
-
-
-
-
-
-
Pending Orders
-
{{ \App\Models\Transaksi::where('order_status', 'pending')->count() }}
-
-
-
-
-
-
Processing Orders
-
{{ \App\Models\Transaksi::where('order_status', 'processing')->count() }}
-
-
-
-
-
-
Delivered Orders
-
{{ \App\Models\Transaksi::where('order_status', 'delivered')->count() }}
-
-
-
-
+ .menu-item.active { + background: rgba(255,255,255,0.15); + color: white; + border-left-color: #fff; + font-weight: 600; + } - - + .btn-logout:hover { + background: rgba(255,255,255,0.22); + border-color: rgba(255,255,255,0.5); + color: white; + } -
-
+ .sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 1040; + backdrop-filter: blur(2px); + } + + .sidebar-overlay.show { display: block; } + + /* ===== MAIN CONTENT ===== */ + .main-content { + margin-left: var(--sidebar-width); + min-height: 100vh; + display: flex; + flex-direction: column; + } + + /* ===== TOPBAR ===== */ + .topbar { + background: white; + height: 64px; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 1px 0 #e5e7eb; + position: sticky; + top: 0; + z-index: 100; + } + + .topbar-left { display: flex; align-items: center; gap: 14px; } + + .hamburger-btn { + display: none; + background: none; + border: none; + color: #374151; + font-size: 1.25rem; + padding: 6px; + cursor: pointer; + border-radius: 6px; + transition: background 0.15s; + } + + .hamburger-btn:hover { background: #f3f4f6; } + + .topbar-breadcrumb { font-size: 0.8rem; color: #6b7280; margin-bottom: 2px; } + .topbar-breadcrumb a { color: var(--green-main); text-decoration: none; } + .topbar-title h2 { font-size: 1.1rem; font-weight: 700; color: #111827; margin: 0; } + .topbar-title p { font-size: 0.75rem; color: #6b7280; margin: 0; } + + .btn-back-website { + background: linear-gradient(135deg, #dc2626, #b91c1c); + color: white; + border: none; + padding: 9px 18px; + border-radius: 8px; + font-weight: 600; + font-size: 0.825rem; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 7px; + transition: all 0.2s; + white-space: nowrap; + } + + .btn-back-website:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(220,38,38,0.35); + color: white; + text-decoration: none; + } + + /* ===== CONTENT AREA ===== */ + .content-area { padding: 20px 24px 24px; flex: 1; } + + /* ===== STATS GRID ===== */ + .stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 20px; + } + + .stat-card { + background: white; + border-radius: 12px; + padding: 18px 16px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 10px rgba(0,0,0,0.04); + display: flex; + justify-content: space-between; + align-items: center; + border-left: 4px solid; + } + + .stat-card.primary { border-left-color: #2563eb; } + .stat-card.warning { border-left-color: #d97706; } + .stat-card.info { border-left-color: #0891b2; } + .stat-card.success { border-left-color: var(--green-main); } + + .stat-label { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 4px; + } + + .stat-card.primary .stat-label { color: #2563eb; } + .stat-card.warning .stat-label { color: #d97706; } + .stat-card.info .stat-label { color: #0891b2; } + .stat-card.success .stat-label { color: var(--green-main); } + + .stat-value { font-size: 1.5rem; font-weight: 800; color: #111827; } + .stat-icon { font-size: 1.75rem; color: #e5e7eb; } + + /* ===== TABLE CARD ===== */ + .table-card { + background: white; + border-radius: 14px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 10px rgba(0,0,0,0.04); + overflow: hidden; + } + + .table-card-header { + padding: 18px 20px; + border-bottom: 1px solid #f3f4f6; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + } + + .section-title { + font-size: 1rem; + font-weight: 700; + color: #111827; + display: flex; + align-items: center; + gap: 8px; + } + + .table-controls { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + } + + /* Table */ + .data-table { + width: 100%; + border-collapse: collapse; + } + + .data-table th, .data-table td { + padding: 11px 14px; + text-align: left; + border-bottom: 1px solid #f3f4f6; + font-size: 0.875rem; + } + + .data-table th { + background: #f9fafb; + font-weight: 600; + color: #6b7280; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; + } + + .data-table tbody tr:hover { background-color: #f9fafb; } + + /* Badges */ + .badge-status { + padding: 4px 10px; + border-radius: 12px; + font-size: 0.72rem; + font-weight: 600; + display: inline-block; + } + + .badge-pending { background: #fef3c7; color: #92400e; } + .badge-processing { background: #dbeafe; color: #1e40af; } + .badge-shipped { background: #e0f2fe; color: #075985; } + .badge-delivered { background: #dcfce7; color: #166534; } + .badge-cancelled { background: #fee2e2; color: #991b1b; } + .badge-paid { background: #dcfce7; color: #166534; } + .badge-failed { background: #fee2e2; color: #991b1b; } + + /* Selects with badge style */ + select.badge-status { + border: none; + cursor: pointer; + appearance: auto; + -webkit-appearance: auto; + } + + /* Action Buttons */ + .btn-action { + padding: 6px 10px; + border: none; + border-radius: 6px; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + display: inline-flex; + align-items: center; + } + + .btn-action:hover { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + text-decoration: none; + color: inherit; + } + + .btn-view { background: #0891b2; color: white; } + .btn-edit { background: #d97706; color: white; } + .btn-print { background: #6b7280; color: white; } + + /* Cancelled row */ + tr.row-cancelled { background-color: #f3f4f6 !important; opacity: 0.75; } + tr.row-cancelled td { color: #9ca3af !important; } + tr.row-cancelled strong, tr.row-cancelled .text-success { color: #9ca3af !important; } + tr.row-cancelled select, tr.row-cancelled .btn-action { pointer-events: none; opacity: 0.5; cursor: not-allowed; } + + /* Pagination */ + .pagination { + display: flex; + gap: 4px; + margin: 0; + padding: 0; + list-style: none; + flex-wrap: wrap; + } + + .pagination .page-item .page-link { + padding: 6px 12px; + border-radius: 6px; + border: 1px solid #e5e7eb; + color: var(--green-main); + font-size: 0.83rem; + text-decoration: none; + background: white; + } + + .pagination .page-item.active .page-link { + background: var(--green-main); + border-color: var(--green-main); + color: white; + } + + .pagination .page-item.disabled .page-link { color: #d1d5db; pointer-events: none; } + .pagination .page-item .page-link:hover { background: #f0fdf4; border-color: var(--green-main); } + + .table-footer { + padding: 14px 20px; + border-top: 1px solid #f3f4f6; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } + + /* Alert */ + .alert-success-custom { + background: #f0fdf4; + border: 1px solid #bbf7d0; + color: #166534; + border-radius: 10px; + padding: 12px 16px; + font-size: 0.875rem; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + } + + .fade-in { + animation: fadeIn 0.3s ease-in-out; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + + /* ===== RESPONSIVE ===== */ + @media (max-width: 1100px) { + .stats-grid { grid-template-columns: repeat(2, 1fr); } + } + + @media (max-width: 900px) { + .sidebar { transform: translateX(-100%); } + .sidebar.open { transform: translateX(0); box-shadow: 4px 0 24px rgba(0,0,0,0.2); } + .main-content { margin-left: 0; } + .hamburger-btn { display: flex; align-items: center; justify-content: center; } + } + + @media (max-width: 600px) { + .stats-grid { grid-template-columns: 1fr 1fr; gap: 10px; } + .stat-card { padding: 14px 12px; } + .stat-value { font-size: 1.3rem; } + .stat-icon { font-size: 1.4rem; } + .content-area { padding: 14px 14px 20px; } + .topbar { padding: 0 14px; } + .btn-back-website span { display: none; } + .btn-back-website { padding: 9px 12px; } + .table-card-header { flex-direction: column; align-items: flex-start; } + .table-controls { width: 100%; } + .table-controls .form-select, .table-controls a { flex: 1; } + .table-footer { flex-direction: column; align-items: flex-start; gap: 10px; } + .data-table th, .data-table td { padding: 9px 10px; font-size: 0.8rem; } + } + + @media (max-width: 380px) { + .stats-grid { grid-template-columns: 1fr; } + } + + + + + + + + +