update fitur terbaru

This commit is contained in:
bayughazali 2026-05-19 14:58:50 +07:00
parent 51469f5520
commit b68eca055b
67 changed files with 9570 additions and 2192 deletions

View File

@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=mysql
# DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
# DB_PORT=3306 DB_PORT=3306
# DB_DATABASE=laravel DB_DATABASE=ta_bibit_cabai
# DB_USERNAME=root DB_USERNAME=root
# DB_PASSWORD= DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120

1
.gitignore vendored
View File

@ -22,3 +22,4 @@
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
Thumbs.db Thumbs.db
vendor.zip

View File

@ -98,20 +98,47 @@ public function login(Request $request)
/** /**
* Show the admin dashboard. * Show the admin dashboard.
*/ */
public function dashboard() public function dashboard()
{ {
$this->syncProductLabels();
// Total bibit terjual dari tabel transaksi_details
$totalSold = \App\Models\TransaksiDetail::whereHas('transaksi', function($q) {
$q->whereIn('payment_status', ['paid', 'lunas', 'success']);
})->sum('quantity');
// Jika masih 0, ambil semua tanpa filter payment_status
if ($totalSold == 0) {
$totalSold = \App\Models\TransaksiDetail::sum('quantity');
}
// Stok menipis = produk dengan stok > 0 dan stok < 1000
$lowStock = Product::where('stock', '>', 0)
->where('stock', '<', 1000)
->count();
$stats = [ $stats = [
'total_products' => Product::count(), 'total_products' => Product::count(),
'active_products' => Product::where('status', 'aktif')->count(), 'active_products' => Product::where('status', 'aktif')->count(),
'out_of_stock' => Product::where('stock', 0)->count(), 'total_sold' => number_format($totalSold, 0, ',', '.'),
'low_stock' => Product::where('stock', '>', 0)->where('stock', '<=', 5)->count(), 'low_stock' => $lowStock,
]; ];
$recent_products = Product::latest()->take(5)->get(); $recent_products = Product::latest()->take(5)->get();
return view('admin.dashboard', compact('stats', 'recent_products')); // Grafik pendapatan per hari (30 hari terakhir) - semua transaksi
} $salesChart = \App\Models\Transaksi::selectRaw('DATE(created_at) as date, SUM(total_amount) as total')
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date', 'asc')
->get();
$chartLabels = $salesChart->pluck('date')
->map(fn($d) => \Carbon\Carbon::parse($d)->format('d M'))
->toArray();
$chartData = $salesChart->pluck('total')->toArray();
return view('admin.dashboard', compact('stats', 'recent_products', 'chartLabels', 'chartData'));
}
/** /**
* Show orders management. * Show orders management.
*/ */
@ -129,7 +156,16 @@ public function orders()
*/ */
public function users() public function users()
{ {
return view('admin.users'); $users = User::orderBy('id', 'desc')->paginate(15);
$totalUsers = User::count();
$totalAdmins = User::where('is_admin', 1)->count();
$verifiedUsers = User::whereNotNull('email_verified_at')->count();
$activeUsers = User::where('created_at', '>=', now()->subDays(30))->count();
return view('admin.users', compact(
'users', 'totalUsers', 'totalAdmins', 'verifiedUsers', 'activeUsers'
));
} }
/** /**
@ -163,12 +199,130 @@ public function logout(Request $request)
return redirect('/') return redirect('/')
->with('success', 'Anda telah berhasil logout'); ->with('success', 'Anda telah berhasil logout');
} }
/**
* Show reports / laporan penjualan.
* Ganti method laporan() yang ada di AdminController dengan ini.
*/
public function laporan(Request $request)
{
$startDate = $request->input('start_date', now()->startOfMonth()->toDateString());
$endDate = $request->input('end_date', now()->toDateString());
// Tambahkan method ini ke dalam AdminController.php Anda // ── Query transaksi ───────────────────────────────────────────────
$query = \App\Models\Transaksi::with(['details.product'])
->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']);
// Filter order_status (bukan payment_status)
if ($request->filled('status')) {
$query->where('order_status', $request->status);
}
// Filter berdasarkan produk (lewat relasi details)
if ($request->filled('product_id')) {
$query->whereHas('details', function ($q) use ($request) {
$q->where('product_id', $request->product_id);
});
}
$orders = $query->orderBy('created_at', 'desc')->paginate(15);
// ── Ringkasan ─────────────────────────────────────────────────────
$summaryQuery = \App\Models\Transaksi::whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']);
if ($request->filled('status')) {
$summaryQuery->where('order_status', $request->status);
}
if ($request->filled('product_id')) {
$summaryQuery->whereHas('details', fn($q) => $q->where('product_id', $request->product_id));
}
$totalPendapatan = $summaryQuery->sum('total_amount');
$totalTransaksi = $summaryQuery->count();
$totalProdukKeluar = \App\Models\TransaksiDetail::whereHas('transaksi', function ($q) use ($startDate, $endDate, $request) {
$q->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']);
if ($request->filled('status')) {
$q->where('order_status', $request->status);
}
})
->when($request->filled('product_id'), fn($q) => $q->where('product_id', $request->product_id))
->sum('transaksi_details.quantity');
$summary = [
'total_pendapatan' => $totalPendapatan,
'total_transaksi' => $totalTransaksi,
'total_produk_keluar' => $totalProdukKeluar,
'rata_per_transaksi' => $totalTransaksi > 0 ? round($totalPendapatan / $totalTransaksi) : 0,
];
// ── Produk terlaris ───────────────────────────────────────────────
$productSales = \App\Models\TransaksiDetail::selectRaw('
transaksi_details.product_id,
products.name as product_name,
SUM(transaksi_details.quantity) as total_qty,
SUM(transaksi_details.subtotal) as total_revenue
')
->join('products', 'transaksi_details.product_id', '=', 'products.id')
->whereHas('transaksi', function ($q) use ($startDate, $endDate, $request) {
$q->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']);
if ($request->filled('status')) {
$q->where('order_status', $request->status);
}
})
->when($request->filled('product_id'), fn($q) => $q->where('transaksi_details.product_id', $request->product_id))
->groupBy('transaksi_details.product_id', 'products.name')
->orderByDesc('total_qty')
->get();
$topProducts = $productSales->take(7);
// ── Grafik harian ─────────────────────────────────────────────────
$salesChart = \App\Models\Transaksi::selectRaw('DATE(created_at) as date, SUM(total_amount) as total')
->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59'])
->when($request->filled('status'), fn($q) => $q->where('order_status', $request->status))
->when($request->filled('product_id'), fn($q) => $q->whereHas('details', fn($d) => $d->where('product_id', $request->product_id)))
->groupBy('date')
->orderBy('date', 'asc')
->get();
$chartLabels = $salesChart->pluck('date')->map(fn($d) => \Carbon\Carbon::parse($d)->format('d M'))->toArray();
$chartData = $salesChart->pluck('total')->toArray();
$products = \App\Models\Product::orderBy('name')->get();
return view('admin.laporan', compact(
'orders', 'summary', 'productSales', 'topProducts',
'chartLabels', 'chartData', 'products'
));
}
/** /**
* Display orders page with shipping and status information * Sync label produk berdasarkan total terjual dari transaksi nyata
*/ */
private function syncProductLabels()
{
// Ambil top 10 produk terlaris berdasarkan transaksi nyata
$bestSellingIds = \Illuminate\Support\Facades\DB::table('transaksi_details')
->select('product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity) as total_sold'))
->groupBy('product_id')
->orderByDesc('total_sold')
->take(10)
->pluck('product_id')
->toArray();
// Reset semua label dulu
Product::where('stock', '>', 0)
->whereNotIn('id', $bestSellingIds)
->update(['label' => 'tersedia']);
Product::where('stock', '<=', 0)
->update(['label' => 'habis']);
// Set label terlaris untuk top 10
if (!empty($bestSellingIds)) {
Product::whereIn('id', $bestSellingIds)
->where('stock', '>', 0)
->update(['label' => 'terlaris']);
}
}
} }

View File

@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Carbon\Carbon;
class AdminForgotPasswordController extends Controller
{
// ─────────────────────────────────────────────────────────
// STEP 1 — Tampilkan form input email
// ─────────────────────────────────────────────────────────
public function showForgotForm()
{
return view('admin.forgot-password');
}
// ─────────────────────────────────────────────────────────
// STEP 1 — Proses: validasi email & kirim OTP
// ─────────────────────────────────────────────────────────
public function sendOtp(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
], [
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
]);
$admin = User::where('email', $request->email)
->where('is_admin', true)
->first();
if (!$admin) {
return back()->withErrors([
'email' => 'Email tidak terdaftar sebagai akun admin.',
])->withInput();
}
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
DB::table('password_reset_tokens')->where('email', $request->email)->delete();
DB::table('password_reset_tokens')->insert([
'email' => $request->email,
'token' => Hash::make($otp),
'created_at' => Carbon::now(),
]);
Mail::send('emails.admin-otp', [
'otp' => $otp,
'name' => $admin->name ?? 'Admin',
], function ($message) use ($request) {
$message->to($request->email)
->subject('Kode Verifikasi Reset Password Admin - Bibit Cabai');
});
$request->session()->put('admin_reset_email', $request->email);
return redirect()->route('admin.password.verify-otp-form')
->with('success', 'Kode OTP telah dikirim ke email admin Anda. Berlaku 10 menit.');
}
// ─────────────────────────────────────────────────────────
// STEP 2 — Tampilkan form input OTP
// ─────────────────────────────────────────────────────────
public function showVerifyOtpForm(Request $request)
{
if (!$request->session()->has('admin_reset_email')) {
return redirect()->route('admin.password.request')
->with('error', 'Silakan masukkan email admin terlebih dahulu.');
}
return view('admin.verify-otp');
}
// ─────────────────────────────────────────────────────────
// 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');
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.');
}
// ─────────────────────────────────────────────────────────
// STEP 2 — Kirim ulang OTP
// ─────────────────────────────────────────────────────────
public function resendOtp(Request $request)
{
$email = $request->session()->get('admin_reset_email');
if (!$email) {
return redirect()->route('admin.password.request')
->with('error', 'Sesi tidak valid. Silakan masukkan email admin kembali.');
}
$admin = User::where('email', $email)
->where('is_admin', true)
->first();
if (!$admin) {
return redirect()->route('admin.password.request')
->with('error', 'Email tidak ditemukan.');
}
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
DB::table('password_reset_tokens')->where('email', $email)->delete();
DB::table('password_reset_tokens')->insert([
'email' => $email,
'token' => Hash::make($otp),
'created_at' => Carbon::now(),
]);
Mail::send('emails.admin-otp', [
'otp' => $otp,
'name' => $admin->name ?? 'Admin',
], function ($message) use ($email) {
$message->to($email)
->subject('Kode OTP Baru (Kirim Ulang) - Bibit Cabai Admin');
});
return back()->with('success', 'Kode OTP baru telah dikirim ke email admin Anda.');
}
// ─────────────────────────────────────────────────────────
// STEP 3 — Tampilkan form password baru
// ─────────────────────────────────────────────────────────
public function showResetForm(Request $request)
{
if (!$request->session()->has('admin_reset_token') ||
!$request->session()->has('admin_reset_email')) {
return redirect()->route('admin.password.request')
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
}
return view('admin.reset-password');
}
// ─────────────────────────────────────────────────────────
// 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');
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.');
}
}

View File

@ -0,0 +1,225 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Carbon\Carbon;
class ForgotPasswordController extends Controller
{
// ─────────────────────────────────────────────
// STEP 1 — Show form: enter email
// ─────────────────────────────────────────────
public function showForgotForm()
{
return view('auth.forgot-password');
}
// ─────────────────────────────────────────────
// STEP 1 — Handle: send OTP to email
// ─────────────────────────────────────────────
public function sendOtp(Request $request)
{
$request->validate([
'email' => ['required', 'email', 'regex:/@gmail\.com$/'],
], [
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
'email.regex' => 'Email harus menggunakan domain @gmail.com.',
]);
$user = User::where('email', $request->email)->first();
if (!$user) {
return back()->withErrors([
'email' => 'Email tidak ditemukan di sistem kami.',
])->withInput();
}
// Generate 6-digit OTP
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Delete any existing token for this email
DB::table('password_reset_tokens')->where('email', $request->email)->delete();
// Store hashed OTP + expiry (10 minutes)
DB::table('password_reset_tokens')->insert([
'email' => $request->email,
'token' => Hash::make($otp),
'created_at' => Carbon::now(),
]);
// Send OTP via email
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($message) use ($request) {
$message->to($request->email)
->subject('Kode Verifikasi Reset Password - Bibit Cabai Bondowoso');
});
// Store email in session for next steps
$request->session()->put('reset_email', $request->email);
return redirect()->route('password.verify-otp-form')
->with('success', 'Kode verifikasi telah dikirim ke email Anda. Berlaku selama 10 menit.');
}
// ─────────────────────────────────────────────
// STEP 2 — Show form: enter OTP
// ─────────────────────────────────────────────
public function showVerifyOtpForm(Request $request)
{
if (!$request->session()->has('reset_email')) {
return redirect()->route('password.request')
->with('error', 'Silakan masukkan email Anda terlebih dahulu.');
}
return view('auth.verify-otp');
}
// ─────────────────────────────────────────────
// STEP 2 — Handle: verify 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('reset_email');
if (!$email) {
return redirect()->route('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.');
}
// Check expiry: 10 minutes
$createdAt = Carbon::parse($record->created_at);
if (Carbon::now()->diffInMinutes($createdAt) > 10) {
DB::table('password_reset_tokens')->where('email', $email)->delete();
return back()->with('error', 'Kode OTP sudah kadaluarsa. Silakan kirim ulang kode.');
}
// Verify OTP
if (!Hash::check($request->otp, $record->token)) {
return back()->with('error', 'Kode OTP salah. Periksa kembali kode yang Anda masukkan.');
}
// Generate a verified token for the next step
$verifiedToken = Str::random(60);
DB::table('password_reset_tokens')
->where('email', $email)
->update(['token' => Hash::make($verifiedToken)]);
$request->session()->put('reset_token', $verifiedToken);
return redirect()->route('password.reset-form')
->with('success', 'Kode berhasil diverifikasi! Silakan buat password baru.');
}
// ─────────────────────────────────────────────
// STEP 2 — Handle: resend OTP
// ─────────────────────────────────────────────
public function resendOtp(Request $request)
{
$email = $request->session()->get('reset_email');
if (!$email) {
return redirect()->route('password.request')
->with('error', 'Sesi tidak valid. Silakan masukkan email Anda kembali.');
}
$user = User::where('email', $email)->first();
if (!$user) {
return redirect()->route('password.request')
->with('error', 'Email tidak ditemukan.');
}
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
DB::table('password_reset_tokens')->where('email', $email)->delete();
DB::table('password_reset_tokens')->insert([
'email' => $email,
'token' => Hash::make($otp),
'created_at' => Carbon::now(),
]);
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($message) use ($email) {
$message->to($email)
->subject('Kode Verifikasi Reset Password (Kirim Ulang) - Bibit Cabai Bondowoso');
});
return back()->with('success', 'Kode OTP baru telah dikirim ke email Anda.');
}
// ─────────────────────────────────────────────
// STEP 3 — Show form: enter new password
// ─────────────────────────────────────────────
public function showResetForm(Request $request)
{
if (!$request->session()->has('reset_token') || !$request->session()->has('reset_email')) {
return redirect()->route('password.request')
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
}
return view('auth.reset-password');
}
// ─────────────────────────────────────────────
// STEP 3 — Handle: save new password
// ─────────────────────────────────────────────
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('reset_email');
$token = $request->session()->get('reset_token');
if (!$email || !$token) {
return redirect()->route('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('password.request')
->with('error', 'Token tidak valid. Silakan ulangi dari awal.');
}
// Update password
User::where('email', $email)->update([
'password' => Hash::make($request->password),
]);
// Clean up
DB::table('password_reset_tokens')->where('email', $email)->delete();
$request->session()->forget(['reset_email', 'reset_token']);
return redirect()->route('login')
->with('success', 'Password berhasil diubah! Silakan login dengan password baru Anda.');
}
}

View File

@ -9,6 +9,8 @@
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Mail\AdminOrderNotification;
use Illuminate\Support\Facades\Mail;
class CheckoutController extends Controller class CheckoutController extends Controller
{ {
@ -46,20 +48,37 @@ public function index(Request $request)
} }
public function process(Request $request) public function process(Request $request)
{ {
$validated = $request->validate([ $request->validate([
'product_id' => 'required|exists:products,id', 'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1', 'quantity' => 'required|integer|min:1',
'name' => 'required|string|max:255', 'name' => 'required|string|min:8|max:255',
'phone' => 'required|string|max:20', 'phone' => ['required', 'string', 'min:11', 'regex:/^\+62[0-9]{8,}$/'],
'email' => 'required|email|max:255', 'email' => 'required|email|min:8|max:255',
'address' => 'required|string', 'address' => ['required', 'string', 'min:20',
'regex:/(?=.*[Jj]l\.?|.*[Jj]alan)(?=.*[Nn]o\.?)(?=.*[Rr][Tt])(?=.*[Rr][Ww])/'],
'city' => 'required|string|max:100', 'city' => 'required|string|max:100',
'postal_code' => 'required|string|max:10', 'postal_code' => 'required|string|max:10',
'payment_method' => 'required|in:qris,bri,dana,seabank,shopee,cod', // TAMBAHKAN 'qris' 'payment_method' => 'required|in:qris,bri,dana,seabank,shopee,cod',
'shipping_cost' => 'required|numeric|min:0', 'shipping_cost' => 'required|numeric|min:0',
'notes' => 'nullable|string|max:1000' 'notes' => 'nullable|string|max:1000',
]); ], [
'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',
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
'email.min' => 'Email minimal 8 karakter.',
'address.required' => 'Alamat lengkap wajib diisi.',
'address.min' => 'Alamat terlalu pendek, harap isi lengkap.',
'address.regex' => 'Alamat harus lengkap: Nama Jalan, No. Rumah, RT/RW. Contoh: Jl. Merdeka No. 12, RT 02/RW 05, Desa Kademangan',
'city.required' => 'Kecamatan wajib dipilih.',
'postal_code.required' => 'Kode pos wajib diisi.',
'payment_method.required'=> 'Metode pembayaran wajib dipilih.',
'shipping_cost.required' => 'Ongkos kirim belum dihitung, pilih kecamatan terlebih dahulu.',
]);
DB::beginTransaction(); DB::beginTransaction();
@ -74,9 +93,45 @@ public function process(Request $request)
throw new \Exception('Stok tidak mencukupi. Stok tersedia: ' . $product->stock); throw new \Exception('Stok tidak mencukupi. Stok tersedia: ' . $product->stock);
} }
$ongkirData = [
'Bondowoso' => 20000,
'Grujugan' => 30000,
'Jambesari Darus Sholah' => 250000,
'Klabang' => 10000,
'Tenggarang' => 12000,
'Binakal' => 12000,
'Prajekan' => 13000,
'Botolinggo' => 13000,
'Maesan' => 50000,
'Tamanan' => 40000,
'Wonosari' => 10000,
'Pujer' => 15000,
'Tlogosari' => 250000,
'Sukosari' => 18000,
'Sumberwringin' => 20000,
'Tegalampel' => 30000,
'Sempol' => 80000,
'Pakem' => 50000,
'Curahdami' => 40000,
'Ijen' => 100000,
'Tapen' => 10000,
'Wringin' => 300000,
'Taman Krocok' => 28000,
];
$subtotal = $product->price * $request->quantity; $subtotal = $product->price * $request->quantity;
$ongkir = 15000; $baseOngkir = $ongkirData[$request->city] ?? 15000;
$total = $subtotal + $ongkir; $ongkir = $baseOngkir;
$diskon = 0;
if ($request->quantity >= 1000) {
$ongkir = 0;
$diskon = $subtotal * 0.15;
} elseif ($request->quantity >= 800) {
$ongkir = 0;
}
$total = ($subtotal - $diskon) + $ongkir;
$invoiceNumber = 'INV-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -6)); $invoiceNumber = 'INV-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -6));
@ -86,11 +141,12 @@ public function process(Request $request)
$transaksi->customer_name = $request->name; $transaksi->customer_name = $request->name;
$transaksi->customer_phone = $request->phone; $transaksi->customer_phone = $request->phone;
$transaksi->customer_email = $request->email; $transaksi->customer_email = $request->email;
$transaksi->shipping_address = $request->address; $transaksi->shipping_address= $request->address;
$transaksi->province = $request->province; $transaksi->province = $request->province;
$transaksi->city = $request->city; $transaksi->city = $request->city;
$transaksi->postal_code = $request->postal_code; $transaksi->postal_code = $request->postal_code;
$transaksi->subtotal = $subtotal; $transaksi->subtotal = $subtotal;
$transaksi->discount = $diskon;
$transaksi->shipping_cost = $ongkir; $transaksi->shipping_cost = $ongkir;
$transaksi->total_amount = $total; $transaksi->total_amount = $total;
$transaksi->payment_method = $request->payment_method; $transaksi->payment_method = $request->payment_method;
@ -113,6 +169,14 @@ public function process(Request $request)
DB::commit(); DB::commit();
// Kirim notifikasi email ke admin
try {
Mail::to(config('mail.admin_email', 'bcom0508@gmail.com'))
->send(new AdminOrderNotification($transaksi->load('details')));
} catch (\Exception $mailError) {
Log::warning('Gagal kirim notifikasi email: ' . $mailError->getMessage());
}
return redirect()->route('checkout.success', ['id' => $transaksi->id]) return redirect()->route('checkout.success', ['id' => $transaksi->id])
->with('success', 'Pesanan berhasil dibuat!'); ->with('success', 'Pesanan berhasil dibuat!');
@ -129,7 +193,7 @@ public function process(Request $request)
->with('error', 'Terjadi kesalahan: ' . $e->getMessage()) ->with('error', 'Terjadi kesalahan: ' . $e->getMessage())
->withInput(); ->withInput();
} }
} }
public function success($id) public function success($id)
{ {

View File

@ -3,9 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Contact; use App\Models\Contact;
use App\Mail\ContactMail;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Mail;
class ContactController extends Controller class ContactController extends Controller
{ {
@ -15,17 +17,69 @@ public function index(): View
} }
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|string|max:255', 'name' => [
'email' => 'required|email|max:255', 'required',
'phone' => 'nullable|string|max:20', 'string',
'subject' => 'required|string|max:255', 'min:10',
'message' => 'required|string|max:1000', 'max:255',
],
'email' => [
'required',
'email',
'min:8',
'max:255',
'regex:/^[a-zA-Z0-9._%+\-]+@gmail\.com$/',
],
'phone' => [
'nullable',
'string',
'min:11',
'max:20',
'regex:/^[0-9]+$/',
],
'subject' => [
'required',
'string',
'min:8',
'max:255',
],
'message' => [
'required',
'string',
function ($attribute, $value, $fail) {
$wordCount = str_word_count($value);
if ($wordCount < 5) {
$fail('Pesan harus terdiri dari minimal 5 kata.');
}
},
],
], [
// Pesan error custom dalam Bahasa Indonesia
'name.required' => 'Nama lengkap wajib diisi.',
'name.min' => 'Nama lengkap minimal 10 karakter.',
'email.required' => 'Email wajib diisi.',
'email.min' => 'Email minimal 8 karakter.',
'email.email' => 'Format email tidak valid.',
'email.regex' => 'Email harus menggunakan @gmail.com.',
'phone.min' => 'No. telepon minimal 11 karakter.',
'phone.regex' => 'No. telepon hanya boleh berisi angka.',
'subject.required' => 'Subjek wajib diisi.',
'subject.min' => 'Subjek minimal 8 karakter.',
'message.required' => 'Pesan wajib diisi.',
]); ]);
// Simpan ke database
Contact::create($validated); Contact::create($validated);
// Kirim email
Mail::to('bayualghozali86@gmail.com')->send(new ContactMail($validated));
return redirect()->back()->with('success', 'Pesan Anda telah terkirim! Kami akan segera merespons.'); return redirect()->back()->with('success', 'Pesan Anda telah terkirim! Kami akan segera merespons.');
} }
} }

View File

@ -4,19 +4,41 @@
use App\Models\Product; use App\Models\Product;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class HomeController extends Controller class HomeController extends Controller
{ {
public function index() public function index()
{ {
// Get featured products (best selling or marked as featured) $featuredProducts = DB::table('transaksi_details')
$featuredProducts = Product::active() ->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id')
->orderByDesc('sold') // Order by most sold ->join('products', 'transaksi_details.product_id', '=', 'products.id')
->take(6) ->where('products.status', 'aktif')
->select(
'products.id',
'products.name',
'products.image',
'products.price',
'products.stock',
DB::raw('SUM(transaksi_details.quantity) as total_sold'),
DB::raw('COUNT(DISTINCT transaksis.id) as total_orders')
)
->groupBy(
'products.id',
'products.name',
'products.image',
'products.price',
'products.stock'
)
->orderByDesc('total_sold')
->take(2)
->get(); ->get();
return view('home', compact('featuredProducts')); // Ambil semua produk aktif
} $allProducts = Product::active()->orderByDesc('created_at')->get();
return view('home', compact('featuredProducts', 'allProducts'));
}
public function bestSellingProducts() public function bestSellingProducts()
{ {

View File

@ -92,6 +92,7 @@ public function store(Request $request)
// Auto set label based on stock // Auto set label based on stock
$product->updateLabelBasedOnStock(); $product->updateLabelBasedOnStock();
$product->save(); $product->save();
$this->syncLabelAfterSave($product);
return redirect()->route('admin.products.index') return redirect()->route('admin.products.index')
->with('success', 'Produk berhasil ditambahkan!'); ->with('success', 'Produk berhasil ditambahkan!');
@ -167,6 +168,7 @@ public function update(Request $request, Product $product)
// Auto update label based on new stock // Auto update label based on new stock
$product->updateLabelBasedOnStock(); $product->updateLabelBasedOnStock();
$product->save(); $product->save();
$this->syncLabelAfterSave($product);
return redirect()->route('admin.products.index') return redirect()->route('admin.products.index')
->with('success', 'Produk berhasil diupdate!'); ->with('success', 'Produk berhasil diupdate!');
@ -195,7 +197,12 @@ public function destroy(Product $product)
return redirect()->route('admin.products.index') return redirect()->route('admin.products.index')
->with('success', 'Produk berhasil dihapus'); ->with('success', 'Produk berhasil dihapus');
} }
// Tampilkan detail produk untuk user (public)
public function publicShow($id)
{
$product = Product::where('status', 'aktif')->findOrFail($id);
return view('products.show', compact('product'));
}
// NEW: Method untuk debugging gambar // NEW: Method untuk debugging gambar
public function debugImage($id) public function debugImage($id)
{ {
@ -212,4 +219,27 @@ public function debugImage($id)
return response()->json($debug); return response()->json($debug);
} }
/**
* Cek apakah produk ini termasuk top 10 terlaris, jika iya set label terlaris
*/
private function syncLabelAfterSave(Product $product)
{
if ($product->stock <= 0) {
return; // Sudah di-handle updateLabelBasedOnStock
}
$bestSellingIds = \Illuminate\Support\Facades\DB::table('transaksi_details')
->select('product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity) as total_sold'))
->groupBy('product_id')
->orderByDesc('total_sold')
->take(10)
->pluck('product_id')
->toArray();
if (in_array($product->id, $bestSellingIds)) {
$product->label = 'terlaris';
$product->save();
}
}
} }

View File

@ -7,33 +7,15 @@
use App\Models\Transaksi; use App\Models\Transaksi;
use App\Models\TransaksiDetail; use App\Models\TransaksiDetail;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ProductRecommendationController extends Controller class ProductRecommendationController extends Controller
{ {
/**
* VERSI UNTUK DEBUGGING
* Menampilkan produk terlaris dengan informasi lengkap untuk debugging
*/
public function index() public function index()
{ {
// Log untuk debugging // Query produk terlaris dari SEMUA status transaksi
Log::info('🔍 Mengakses halaman produk terlaris');
// Cek total transaksi
$totalTransaksi = Transaksi::count();
$totalDelivered = Transaksi::where('order_status', 'delivered')->count();
$totalDetails = TransaksiDetail::count();
Log::info("📊 Total Transaksi: {$totalTransaksi}");
Log::info("✅ Delivered: {$totalDelivered}");
Log::info("📦 Detail: {$totalDetails}");
// Query produk terlaris HANYA dari transaksi delivered
$bestSellingProducts = DB::table('transaksi_details') $bestSellingProducts = DB::table('transaksi_details')
->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id') ->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id')
->join('products', 'transaksi_details.product_id', '=', 'products.id') ->join('products', 'transaksi_details.product_id', '=', 'products.id')
->where('transaksis.order_status', 'delivered')
->where('products.status', 'aktif') ->where('products.status', 'aktif')
->select( ->select(
'products.id', 'products.id',
@ -55,72 +37,21 @@ public function index()
->take(10) ->take(10)
->get(); ->get();
Log::info("🏆 Produk terlaris ditemukan: " . $bestSellingProducts->count());
// FALLBACK: Jika tidak ada produk delivered, tampilkan dari semua status
if ($bestSellingProducts->isEmpty()) {
Log::warning('⚠️ Tidak ada produk dari transaksi delivered, menggunakan semua status');
$bestSellingProducts = DB::table('transaksi_details')
->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id')
->join('products', 'transaksi_details.product_id', '=', 'products.id')
// TANPA FILTER STATUS - untuk testing
->where('products.status', 'aktif')
->select(
'products.id',
'products.name',
'products.image',
'products.price',
'products.stock',
DB::raw('SUM(transaksi_details.quantity) as total_sold'),
DB::raw('COUNT(DISTINCT transaksis.id) as total_orders')
)
->groupBy(
'products.id',
'products.name',
'products.image',
'products.price',
'products.stock'
)
->orderByDesc('total_sold')
->take(10)
->get();
Log::info("🔄 Produk dari semua status: " . $bestSellingProducts->count());
}
// Hitung statistik // Hitung statistik
$statistics = [ $statistics = [
'total_products_sold' => DB::table('transaksi_details') 'total_products_sold' => DB::table('transaksi_details')->sum('quantity'),
->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id')
->where('transaksis.order_status', 'delivered')
->sum('transaksi_details.quantity'),
'total_completed_orders' => Transaksi::where('order_status', 'delivered')->count(), 'total_completed_orders' => Transaksi::where('order_status', 'delivered')->count(),
'most_popular_product' => $bestSellingProducts->first() 'most_popular_product' => $bestSellingProducts->first()
]; ];
// Debug info return view('products.best-selling', compact('bestSellingProducts', 'statistics'));
$debugInfo = [
'total_transaksi' => $totalTransaksi,
'total_delivered' => $totalDelivered,
'total_details' => $totalDetails,
'products_found' => $bestSellingProducts->count()
];
return view('products.best-selling', compact('bestSellingProducts', 'statistics', 'debugInfo'));
} }
/**
* Method untuk update status transaksi ke delivered (untuk testing)
*/
public function updateToDelivered(Request $request) public function updateToDelivered(Request $request)
{ {
$transaksiIds = $request->input('ids', []); $transaksiIds = $request->input('ids', []);
if (empty($transaksiIds)) { if (empty($transaksiIds)) {
// Update semua transaksi yang ada detail
$transaksiIds = TransaksiDetail::distinct() $transaksiIds = TransaksiDetail::distinct()
->pluck('transaksi_id') ->pluck('transaksi_id')
->toArray(); ->toArray();
@ -136,9 +67,6 @@ public function updateToDelivered(Request $request)
]); ]);
} }
/**
* Method untuk cek status sistem
*/
public function checkStatus() public function checkStatus()
{ {
$status = [ $status = [

View File

@ -0,0 +1,201 @@
<?php
namespace App\Http\Controllers;
use App\Models\Transaksi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
/**
* Halaman profil pengguna
*/
public function index()
{
$user = auth()->user();
return view('profile.index', compact('user'));
}
/**
* Update profil pengguna
*/
public function update(Request $request)
{
$user = auth()->user();
$request->validate([
'name' => ['required', 'string', 'min:8', 'regex:/^[a-zA-Z\s]+$/'],
'phone' => ['required', 'string', 'min:11', 'regex:/^(\+62|08)[0-9]+$/'],
'address' => ['required', 'string', 'min:12'],
], [
'name.required' => 'Nama lengkap wajib diisi.',
'name.min' => 'Nama lengkap minimal 8 karakter.',
'name.regex' => 'Nama hanya boleh menggunakan huruf dan spasi.',
'phone.required' => 'Nomor telepon wajib diisi.',
'phone.min' => 'Nomor telepon minimal 11 karakter.',
'phone.regex' => 'Nomor telepon harus diawali +62 atau 08 dan hanya boleh berisi angka.',
'address.required' => 'Alamat wajib diisi.',
'address.min' => 'Alamat minimal 12 karakter.',
]);
$user->update([
'name' => $request->name,
'phone' => $request->phone,
'address' => $request->address,
]);
return back()->with('success', 'Profil berhasil diperbarui.');
}
/**
* Update password pengguna
*/
public function updatePassword(Request $request)
{
$user = auth()->user();
$request->validate([
'current_password' => ['required'],
'password' => ['required', 'confirmed', 'min:8', 'regex:/^(?=.*[a-zA-Z])(?=.*[0-9])\S+$/'],
], [
'current_password.required' => 'Password lama wajib diisi.',
'password.required' => 'Password baru wajib diisi.',
'password.min' => 'Password baru minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'password.regex' => 'Password harus terdiri dari huruf dan angka, tanpa spasi.',
]);
if (!Hash::check($request->current_password, $user->password)) {
return back()->withErrors([
'current_password' => 'Password lama yang kamu masukkan tidak sesuai.'
])->withInput();
}
$user->update(['password' => Hash::make($request->password)]);
return back()->with('success', 'Password berhasil diperbarui.');
}
/**
* Halaman daftar pesanan pengguna
*/
public function orders(Request $request)
{
$user = auth()->user();
$status = $request->get('status', 'all');
$query = Transaksi::with(['details.product', 'cancellation']) // ← tambah cancellation
->where('user_id', $user->id)
->orderBy('created_at', 'desc');
if ($status !== 'all') {
$query->where('order_status', $status);
}
$orders = $query->paginate(10);
$totalOrders = Transaksi::where('user_id', $user->id)->count();
$pendingOrders = Transaksi::where('user_id', $user->id)->where('order_status', 'pending')->count();
$processOrders = Transaksi::where('user_id', $user->id)->where('order_status', 'processing')->count();
$shippedOrders = Transaksi::where('user_id', $user->id)->where('order_status', 'shipped')->count();
$doneOrders = Transaksi::where('user_id', $user->id)->where('order_status', 'delivered')->count();
$cancelOrders = Transaksi::where('user_id', $user->id)->where('order_status', 'cancelled')->count();
return view('profile.orders', compact(
'orders', 'status',
'totalOrders', 'pendingOrders', 'processOrders',
'shippedOrders', 'doneOrders', 'cancelOrders'
));
}
/**
* Detail pesanan dengan relasi cancellation
*/
public function orderDetail($id)
{
$user = auth()->user();
$order = Transaksi::with(['details.product', 'cancellation']) // ← tambah cancellation
->where('user_id', $user->id)
->findOrFail($id);
return view('profile.order-detail', compact('order'));
}
// Tambahkan di ProfileController.php — setelah method orderDetail()
/**
* Tampilkan form batalkan pesanan
*/
public function cancelForm($id)
{
$user = auth()->user();
$transaksi = Transaksi::with(['details.product', 'cancellation'])
->where('user_id', $user->id)
->findOrFail($id);
if (!$transaksi->canBeCancelled()) {
return redirect()->route('orders.detail', $id)
->with('error', 'Pesanan ini tidak dapat dibatalkan.');
}
$reasons = \App\Models\OrderCancellation::reasons();
return view('profile.cancel', compact('transaksi', 'reasons'));
}
/**
* Simpan pengajuan pembatalan
*/
public function cancelStore(Request $request, $id)
{
$user = auth()->user();
$transaksi = Transaksi::with('cancellation')
->where('user_id', $user->id)
->findOrFail($id);
if (!$transaksi->canBeCancelled()) {
return redirect()->route('orders.detail', $id)
->with('error', 'Pesanan ini tidak dapat dibatalkan.');
}
$request->validate([
'reason' => ['required', 'string'],
'description' => ['nullable', 'string', 'max:500'],
], [
'reason.required' => 'Pilih alasan pembatalan terlebih dahulu.',
]);
// ── COD + pending → langsung cancel tanpa persetujuan admin ──
if ($transaksi->isAutoCancel()) {
$transaksi->update(['order_status' => 'cancelled']);
// Simpan catatan pembatalan (opsional, untuk riwayat)
\App\Models\OrderCancellation::create([
'transaksi_id' => $transaksi->id,
'user_id' => $user->id,
'reason' => $request->reason,
'description' => $request->description,
'status' => 'approved', // langsung approved
'admin_note' => 'Dibatalkan otomatis (COD, belum diproses)',
'reviewed_at' => now(),
]);
return redirect()->route('orders.my')
->with('success', 'Pesanan berhasil dibatalkan.');
}
// ── Selain itu → kirim pengajuan ke admin ──
\App\Models\OrderCancellation::create([
'transaksi_id' => $transaksi->id,
'user_id' => $user->id,
'reason' => $request->reason,
'description' => $request->description,
'status' => 'pending',
]);
return redirect()->route('orders.detail', $id)
->with('success', 'Pengajuan pembatalan berhasil dikirim. Tunggu konfirmasi admin.');
}
}

View File

@ -4,9 +4,14 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Transaksi; use App\Models\Transaksi;
use App\Models\OrderCancellation;
use App\Models\Product; use App\Models\Product;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Mail\OrderShippedNotification;
use App\Notifications\OrderStatusChanged;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class TransaksiController extends Controller class TransaksiController extends Controller
{ {
@ -93,45 +98,50 @@ public function edit($id)
* UPDATED: Menambahkan validasi dan update untuk customer info dan shipping * UPDATED: Menambahkan validasi dan update untuk customer info dan shipping
*/ */
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$request->validate([ $request->validate([
'customer_name' => 'required|string|max:255',
'customer_phone' => 'required|string|max:20',
'customer_email' => 'nullable|email|max:255',
'shipping_address' => 'required|string',
'city' => 'required|string|max:100',
'province' => 'required|string|max:100',
'postal_code' => 'required|string|max:10',
'order_status' => 'required|in:pending,processing,shipped,delivered,cancelled', 'order_status' => 'required|in:pending,processing,shipped,delivered,cancelled',
'payment_status' => 'required|in:pending,paid,failed', 'payment_status' => 'required|in:pending,paid,failed',
'tracking_number' => 'nullable|string|max:100', 'tracking_number'=> 'nullable|string|max:100',
'notes' => 'nullable|string' 'notes' => 'nullable|string',
]); ]);
try { try {
$transaksi = Transaksi::findOrFail($id); $transaksi = Transaksi::findOrFail($id);
$oldStatus = $transaksi->order_status;
$updateData = [ $updateData = [
'customer_name' => $request->customer_name,
'customer_phone' => $request->customer_phone,
'customer_email' => $request->customer_email,
'shipping_address' => $request->shipping_address,
'city' => $request->city,
'province' => $request->province,
'postal_code' => $request->postal_code,
'order_status' => $request->order_status, 'order_status' => $request->order_status,
'payment_status' => $request->payment_status, 'payment_status' => $request->payment_status,
'tracking_number' => $request->tracking_number, 'tracking_number' => $request->tracking_number,
'notes' => $request->notes 'notes' => $request->notes,
// Field customer & shipping TIDAK diupdate
'customer_name' => $transaksi->customer_name,
'customer_phone' => $transaksi->customer_phone,
'customer_email' => $transaksi->customer_email,
'shipping_address'=> $transaksi->shipping_address,
'city' => $transaksi->city,
'province' => $transaksi->province,
'postal_code' => $transaksi->postal_code,
]; ];
// If payment status changed to paid, set paid_at
if ($request->payment_status === 'paid' && $transaksi->payment_status !== 'paid') { if ($request->payment_status === 'paid' && $transaksi->payment_status !== 'paid') {
$updateData['paid_at'] = now(); $updateData['paid_at'] = now();
} }
$transaksi->update($updateData); $transaksi->update($updateData);
// Kirim notifikasi jika order_status berubah
if ($transaksi->user_id && $request->order_status !== $oldStatus) {
try {
$transaksi->user->notify(new OrderStatusChanged($transaksi, $request->order_status));
} catch (\Exception $notifError) {
Log::warning('Gagal simpan notifikasi: ' . $notifError->getMessage());
}
}
return redirect() return redirect()
->route('admin.transaksis.show', $id) ->route('admin.transaksis.show', $id)
->with('success', 'Transaksi berhasil diupdate!'); ->with('success', 'Transaksi berhasil diupdate!');
@ -142,57 +152,72 @@ public function update(Request $request, $id)
->with('error', 'Terjadi kesalahan: ' . $e->getMessage()) ->with('error', 'Terjadi kesalahan: ' . $e->getMessage())
->withInput(); ->withInput();
} }
} }
/** /**
* Update order status via AJAX or regular request. * Update order status via AJAX or regular request.
* UPDATED: Menerima field 'order_status' selain 'status' * UPDATED: Menerima field 'order_status' selain 'status'
*/ */
public function updateStatus(Request $request, $id) public function updateStatus(Request $request, $id)
{ {
// Accept both 'status' and 'order_status' field names
$statusField = $request->has('order_status') ? 'order_status' : 'status';
$request->validate([
$statusField => 'required|in:pending,processing,shipped,delivered,cancelled',
]);
try { try {
$transaksi = Transaksi::findOrFail($id); $transaksi = Transaksi::with('details')->findOrFail($id);
$newStatus = $request->input($statusField);
// Use model method if exists, otherwise direct update // Handle order_status
if (method_exists($transaksi, 'updateOrderStatus')) { if ($request->has('order_status')) {
$transaksi->updateOrderStatus($newStatus); $request->validate([
} else { 'order_status' => 'required|in:pending,processing,shipped,delivered,cancelled',
$transaksi->update(['order_status' => $newStatus]);
}
if ($request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Status berhasil diupdate!'
]); ]);
}
return redirect() $newStatus = $request->order_status;
->back() $oldStatus = $transaksi->order_status;
->with('success', 'Status berhasil diupdate!');
} catch (\Exception $e) { // Jika sudah cancelled, tidak bisa diubah
if ($request->ajax()) { if (in_array($oldStatus, ['cancelled', 'delivered'])) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Gagal update status: ' . $e->getMessage() 'message' => 'Pesanan yang sudah selesai atau dibatalkan tidak dapat diubah kembali.'
], 500); ], 422);
} }
return redirect() $transaksi->update(['order_status' => $newStatus]);
->back()
->with('error', 'Gagal update status: ' . $e->getMessage()); if ($newStatus === 'shipped' && $oldStatus !== 'shipped') {
try {
Mail::to($transaksi->customer_email)
->send(new OrderShippedNotification($transaksi));
} catch (\Exception $mailError) {
Log::warning('Gagal kirim email shipped: ' . $mailError->getMessage());
} }
} }
if ($transaksi->user_id) {
try {
$transaksi->user->notify(new OrderStatusChanged($transaksi, $newStatus));
} catch (\Exception $notifError) {
Log::warning('Gagal simpan notifikasi: ' . $notifError->getMessage());
}
}
}
// Handle payment_status
if ($request->has('payment_status')) {
$request->validate([
'payment_status' => 'required|in:pending,paid,failed',
]);
$updateData = ['payment_status' => $request->payment_status];
if ($request->payment_status === 'paid' && $transaksi->payment_status !== 'paid') {
$updateData['paid_at'] = now();
}
$transaksi->update($updateData);
}
return response()->json(['success' => true, 'message' => 'Status berhasil diupdate!']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/** /**
* Update payment status. * Update payment status.
*/ */
@ -409,4 +434,72 @@ public function printInvoice($id)
$transaksi = Transaksi::with(['details.product', 'user'])->findOrFail($id); $transaksi = Transaksi::with(['details.product', 'user'])->findOrFail($id);
return view('admin.transaksis.print', compact('transaksi')); return view('admin.transaksis.print', compact('transaksi'));
} }
// ← tambah di bagian use atas
/**
* Daftar pengajuan pembatalan (Admin)
*/
public function cancellations()
{
$cancellations = OrderCancellation::with(['transaksi', 'user'])
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected')")
->orderBy('created_at', 'desc')
->paginate(15);
return view('admin.cancellations', compact('cancellations'));
}
/**
* Setujui pengajuan pembatalan
*/
public function approveCancellation(Request $request, $id)
{
$cancellation = OrderCancellation::with('transaksi')->findOrFail($id);
$cancellation->update([
'status' => 'approved',
'admin_note' => $request->admin_note ?? 'Pengajuan pembatalan disetujui oleh admin.',
'reviewed_by' => auth()->id(),
'reviewed_at' => now(),
]);
// Kembalikan stok produk
foreach ($cancellation->transaksi->details as $detail) {
$product = Product::find($detail->product_id);
if ($product) {
$product->increment('stock', $detail->quantity);
}
}
// Update status pesanan jadi cancelled
$cancellation->transaksi->update(['order_status' => 'cancelled']);
return back()->with('success', 'Pengajuan disetujui. Pesanan dibatalkan & stok dikembalikan.');
}
/**
* Tolak pengajuan pembatalan
*/
public function rejectCancellation(Request $request, $id)
{
$request->validate([
'admin_note' => ['required', 'string', 'max:500'],
], [
'admin_note.required' => 'Alasan penolakan wajib diisi.',
]);
$cancellation = OrderCancellation::findOrFail($id);
$cancellation->update([
'status' => 'rejected',
'admin_note' => $request->admin_note,
'reviewed_by' => auth()->id(),
'reviewed_at' => now(),
]);
return back()->with('success', 'Pengajuan pembatalan berhasil ditolak.');
}
} }

View File

@ -15,51 +15,20 @@ class UserController extends Controller
* Display a listing of the users. * Display a listing of the users.
*/ */
public function index() public function index()
{ {
try {
// Ambil semua users dari database dengan pagination
$users = User::orderBy('id', 'desc')->paginate(15); $users = User::orderBy('id', 'desc')->paginate(15);
// Hitung statistik
$totalUsers = User::count(); $totalUsers = User::count();
$totalAdmins = User::where('is_admin', 1)->count(); $totalAdmins = User::where('is_admin', 1)->count();
$verifiedUsers = User::whereNotNull('email_verified_at')->count(); $verifiedUsers = User::whereNotNull('email_verified_at')->count();
$activeUsers = User::where('created_at', '>=', now()->subDays(30))->count(); $activeUsers = User::where('created_at', '>=', now()->subDays(30))->count();
// dd($users->count(), $totalUsers);
// Debug: Log jumlah users return view('admin.users', compact(
\Log::info('Total users in database: ' . $totalUsers); 'users', 'totalUsers', 'totalAdmins', 'verifiedUsers', 'activeUsers'
\Log::info('Users retrieved: ' . $users->count()); ));
}
// Data yang akan dikirim ke view
$data = [
'users' => $users,
'totalUsers' => $totalUsers,
'totalAdmins' => $totalAdmins,
'verifiedUsers' => $verifiedUsers,
'activeUsers' => $activeUsers
];
// Coba cari view di lokasi yang berbeda
if (view()->exists('admin.users.index')) {
return view('admin.users.index', $data);
}
return view('admin.users', $data);
} catch (\Exception $e) {
\Log::error('Error in UserController@index: ' . $e->getMessage());
// Jika error, kirim data kosong
return view('admin.users', [
'users' => collect()->paginate(15),
'totalUsers' => 0,
'totalAdmins' => 0,
'verifiedUsers' => 0,
'activeUsers' => 0,
'error' => $e->getMessage()
]);
}
}
/** /**
* Show the form for creating a new user. * Show the form for creating a new user.
@ -73,28 +42,60 @@ public function create()
* Store a newly created user in database. * Store a newly created user in database.
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$validated = $request->validate([ $request->validate([
'name' => ['required', 'string', 'max:255'], 'name' => [
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'required', 'string', 'min:8',
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'regex:/^[a-zA-Z\s]+$/' // hanya huruf dan spasi
'phone' => ['nullable', 'string', 'max:20'], ],
'address' => ['nullable', 'string'], 'email' => [
'is_admin' => ['boolean'] 'required', 'string', 'min:8', 'email', 'max:255', 'unique:users',
'regex:/^[^\s]+@gmail\.com$/' // tidak boleh spasi, harus @gmail.com
],
'password' => [
'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
],
'address' => ['required', 'string', 'min:10'],
], [
'name.required' => 'Nama lengkap wajib diisi.',
'name.min' => 'Nama minimal 8 karakter.',
'name.regex' => 'Nama hanya boleh menggunakan huruf dan spasi.',
'email.required' => 'Email wajib diisi.',
'email.min' => 'Email minimal 8 karakter.',
'email.unique' => 'Email sudah digunakan.',
'email.regex' => 'Email tidak boleh mengandung spasi dan harus menggunakan @gmail.com.',
'password.required' => 'Password wajib diisi.',
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed'=> 'Konfirmasi password tidak cocok.',
'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.',
'address.required' => 'Alamat wajib diisi.',
'address.min' => 'Alamat minimal 10 karakter.',
]); ]);
$user = User::create([ User::create([
'name' => $validated['name'], 'name' => $request->name,
'email' => $validated['email'], 'email' => $request->email,
'password' => Hash::make($validated['password']), 'password' => Hash::make($request->password),
'phone' => $validated['phone'] ?? null, 'phone' => $request->phone,
'address' => $validated['address'] ?? null, 'address' => $request->address,
'is_admin' => $request->has('is_admin') ? 1 : 0, 'is_admin' => $request->has('is_admin') ? 1 : 0,
]); ]);
return redirect()->route('admin.users.index') return redirect()->route('admin.users')
->with('success', 'Pengguna berhasil ditambahkan'); ->with('success', 'Pengguna berhasil ditambahkan');
} }
/** /**
* Display the specified user. * Display the specified user.
@ -110,60 +111,107 @@ public function show(User $user)
* Show the form for editing the specified user. * Show the form for editing the specified user.
*/ */
public function edit(User $user) public function edit(User $user)
{ {
return view('admin.users.edit', [ // Hanya boleh edit akun sendiri
'user' => $user if ($user->id !== auth()->id()) {
]); return redirect()->route('admin.users')
->with('error', 'Anda tidak dapat mengedit akun pengguna lain.');
} }
return view('admin.users.edit', compact('user'));
}
/** /**
* Update the specified user in database. * Update the specified user in database.
*/ */
public function update(Request $request, User $user) public function update(Request $request, User $user)
{ {
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'], // Hanya boleh update akun sendiri
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email,'.$user->id], if ($user->id !== auth()->id()) {
'phone' => ['nullable', 'string', 'max:20'], return redirect()->route('admin.users')
'address' => ['nullable', 'string'], ->with('error', 'Anda tidak dapat mengedit akun pengguna lain.');
'is_admin' => ['boolean'] }
$request->validate([
'name' => [
'required', 'string', 'min:8',
'regex:/^[a-zA-Z\s]+$/'
],
'email' => [
'required', 'string', 'min:8', 'max:255',
'unique:users,email,' . $user->id,
'regex:/^[^\s]+@gmail\.com$/'
],
'phone' => [
'required', 'string', 'min:12',
'regex:/^(\+62)[0-9]+$/'
],
'address' => ['required', 'string', 'min:10'],
], [
'name.required' => 'Nama lengkap wajib diisi.',
'name.min' => 'Nama minimal 8 karakter.',
'name.regex' => 'Nama hanya boleh menggunakan huruf dan spasi.',
'email.required' => 'Email wajib diisi.',
'email.min' => 'Email minimal 8 karakter.',
'email.unique' => 'Email sudah digunakan.',
'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.',
'address.required' => 'Alamat wajib diisi.',
'address.min' => 'Alamat minimal 10 karakter.',
]); ]);
$updateData = [ $data = [
'name' => $validated['name'], 'name' => $request->name,
'email' => $validated['email'], 'email' => $request->email,
'phone' => $validated['phone'] ?? null, 'phone' => $request->phone,
'address' => $validated['address'] ?? null, 'address' => $request->address,
'is_admin' => $request->has('is_admin') ? 1 : 0, 'is_admin' => $request->has('is_admin') ? 1 : 0,
]; ];
// Update password jika diisi
if ($request->filled('password')) { if ($request->filled('password')) {
$request->validate([ $request->validate([
'password' => ['confirmed', Rules\Password::defaults()] 'password' => [
'confirmed', 'min:8',
'regex:/^(?=.*[a-zA-Z])(?=.*[0-9])\S+$/'
],
], [
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'password.regex' => 'Password tidak boleh mengandung spasi dan harus terdiri dari huruf dan angka.',
]); ]);
$updateData['password'] = Hash::make($request->password); $data['password'] = Hash::make($request->password);
} }
$user->update($updateData); $user->update($data);
return redirect()->route('admin.users.index') return redirect()->route('admin.users')
->with('success', 'Pengguna berhasil diperbarui'); ->with('success', 'Pengguna berhasil diperbarui');
} }
/** /**
* Remove the specified user from database. * Remove the specified user from database.
*/ */
public function destroy(User $user) public function destroy(User $user)
{ {
// Prevent deleting yourself // 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) { if (auth()->id() === $user->id) {
return back()->with('error', 'Anda tidak dapat menghapus akun sendiri'); return back()->with('error', 'Anda tidak dapat menghapus akun sendiri.');
} }
$user->delete(); $user->delete();
return redirect()->route('admin.users.index') return redirect()->route('admin.users')
->with('success', 'Pengguna berhasil dihapus'); ->with('success', 'Pengguna berhasil dihapus');
} }
} }

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsAdmin
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check() || !auth()->user()->is_admin) {
abort(403, 'Akses ditolak. Anda bukan admin.');
}
return $next($request);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Mail;
use App\Models\Transaksi;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class AdminOrderNotification extends Mailable
{
use Queueable, SerializesModels;
public Transaksi $transaksi;
public function __construct(Transaksi $transaksi)
{
$this->transaksi = $transaksi;
}
public function envelope(): Envelope
{
return new Envelope(
subject: '🛒 Pesanan Baru #' . $this->transaksi->invoice_number,
);
}
public function content(): Content
{
return new Content(
view: 'emails.admin-order-notification',
);
}
}

36
app/Mail/ContactMail.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ContactMail extends Mailable
{
use Queueable, SerializesModels;
public array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function envelope(): Envelope
{
return new Envelope(
replyTo: [$this->data['email']],
subject: '[Contact Us] ' . $this->data['subject'],
);
}
public function content(): Content
{
return new Content(
view: 'emails.contact',
);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShippedNotification extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct()
{
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Order Shipped Notification',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'view.name',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class OrderCancellation extends Model
{
protected $fillable = [
'transaksi_id',
'user_id',
'reason',
'description',
'status',
'admin_note',
'reviewed_by',
'reviewed_at',
];
protected $casts = [
'reviewed_at' => 'datetime',
];
public static function reasons(): array
{
return [
'Ingin mengubah produk / varian' => 'Ingin mengubah produk / varian',
'Ingin mengubah alamat pengiriman' => 'Ingin mengubah alamat pengiriman',
'Menemukan harga lebih murah' => 'Menemukan harga lebih murah',
'Tidak jadi membeli' => 'Tidak jadi membeli',
'Proses pengiriman terlalu lama' => 'Proses pengiriman terlalu lama',
'Lainnya' => 'Lainnya',
];
}
public function transaksi()
{
return $this->belongsTo(Transaksi::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function reviewer()
{
return $this->belongsTo(User::class, 'reviewed_by');
}
public function isPending(): bool { return $this->status === 'pending'; }
public function isApproved(): bool { return $this->status === 'approved'; }
public function isRejected(): bool { return $this->status === 'rejected'; }
}

View File

@ -91,16 +91,14 @@ public function getLabelBadgeClassAttribute()
// NEW: Method untuk mendapatkan label berdasarkan stok // NEW: Method untuk mendapatkan label berdasarkan stok
public function updateLabelBasedOnStock() public function updateLabelBasedOnStock()
{ {
if ($this->stock <= 0) { if ($this->stock <= 0) {
$this->label = 'habis'; $this->label = 'habis';
} elseif ($this->stock > 1000) {
$this->label = 'terlaris';
} else { } else {
$this->label = 'tersedia'; $this->label = 'tersedia';
} }
return $this; return $this;
} }
// Scope untuk produk aktif // Scope untuk produk aktif
public function scopeActive($query) public function scopeActive($query)
@ -126,4 +124,9 @@ public function scopeByCategory($query, $category)
{ {
return $query->where('category', $category); return $query->where('category', $category);
} }
// Relasi ke transaksi details untuk menghitung total terjual
public function transaksiDetails()
{
return $this->hasMany(TransaksiDetail::class);
}
} }

View File

@ -34,11 +34,11 @@ class Transaksi extends Model
protected $casts = [ protected $casts = [
'paid_at' => 'datetime', 'paid_at' => 'datetime',
'subtotal' => 'decimal:2', 'subtotal' => 'decimal:2',
'shipping_cost' => 'decimal:2', 'shipping_cost'=> 'decimal:2',
'total_amount' => 'decimal:2', 'total_amount' => 'decimal:2',
]; ];
// Relationships // ── Relationships ────────────────────────────────────────────
public function user() public function user()
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
@ -49,7 +49,12 @@ public function details()
return $this->hasMany(TransaksiDetail::class); return $this->hasMany(TransaksiDetail::class);
} }
// Scopes public function cancellation()
{
return $this->hasOne(OrderCancellation::class);
}
// ── Scopes ───────────────────────────────────────────────────
public function scopePending($query) public function scopePending($query)
{ {
return $query->where('order_status', 'pending'); return $query->where('order_status', 'pending');
@ -85,27 +90,57 @@ public function scopeUnpaid($query)
return $query->where('payment_status', 'pending'); return $query->where('payment_status', 'pending');
} }
// Methods // ── Methods ──────────────────────────────────────────────────
public function canBeCancelled()
{ /**
return in_array($this->order_status, ['pending', 'processing']); * Cek apakah pesanan bisa dibatalkan.
* Syarat: status pending/processing DAN belum ada pengajuan cancel.
*/
public function canBeCancelled(): bool
{
$this->loadMissing('cancellation');
// Sudah shipped/delivered/cancelled → tidak bisa dibatalkan
if (in_array($this->order_status, ['shipped', 'delivered', 'cancelled'])) {
return false;
} }
public function markAsPaid() // Sudah ada pengajuan cancel → tidak bisa duplikat
if ($this->cancellation !== null) {
return false;
}
return true; // ← ini yang hilang
}
public function isAutoCancel(): bool
{
return $this->payment_method === 'cod'
&& $this->order_status === 'pending';
}
public function hasPendingCancellation(): bool
{
$this->loadMissing('cancellation');
return $this->cancellation !== null
&& $this->cancellation->status === 'pending';
}
public function markAsPaid(): bool
{ {
return $this->update([ return $this->update([
'payment_status' => 'paid', 'payment_status' => 'paid',
'paid_at' => now() 'paid_at' => now(),
]); ]);
} }
public function updateOrderStatus($status) public function updateOrderStatus(string $status): bool
{ {
return $this->update(['order_status' => $status]); return $this->update(['order_status' => $status]);
} }
// Accessor // ── Accessors ────────────────────────────────────────────────
public function getPaymentMethodLabelAttribute() public function getPaymentMethodLabelAttribute(): string
{ {
return $this->payment_method === 'transfer' ? 'Transfer Bank' : 'COD'; return $this->payment_method === 'transfer' ? 'Transfer Bank' : 'COD';
} }

View File

@ -0,0 +1,44 @@
<?php
namespace App\Notifications;
use App\Models\Transaksi;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\DatabaseMessage;
class OrderStatusChanged extends Notification
{
use Queueable;
public Transaksi $transaksi;
public string $newStatus;
public function __construct(Transaksi $transaksi, string $newStatus)
{
$this->transaksi = $transaksi;
$this->newStatus = $newStatus;
}
public function via($notifiable): array
{
return ['database'];
}
public function toDatabase($notifiable): array
{
$messages = [
'processing' => '🔄 Pesanan Anda sedang diproses',
'shipped' => '📦 Pesanan Anda sedang dalam pengiriman',
'delivered' => '✅ Pesanan Anda telah diterima',
'cancelled' => '❌ Pesanan Anda dibatalkan',
];
return [
'invoice' => $this->transaksi->invoice_number,
'status' => $this->newStatus,
'message' => $messages[$this->newStatus] ?? 'Status pesanan diperbarui',
'transaksi_id'=> $this->transaksi->id,
];
}
}

View File

@ -18,7 +18,7 @@ public function register(): void
* Bootstrap any application services. * Bootstrap any application services.
*/ */
public function boot(): void public function boot(): void
{ {
// \Illuminate\Pagination\Paginator::useBootstrapFive();
} }
} }

View File

@ -3,6 +3,7 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\IsAdmin; // tambahkan ini
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -10,9 +11,12 @@
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware) {
// // Daftarkan alias middleware di sini
$middleware->alias([
'is_admin' => IsAdmin::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions) {
// //
})->create(); })->create();

View File

@ -15,6 +15,7 @@
*/ */
'default' => env('MAIL_MAILER', 'log'), 'default' => env('MAIL_MAILER', 'log'),
'admin_email' => env('ADMIN_EMAIL', 'bayualghozali86@gmail.com'), // ← tambahkan di sini
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -0,0 +1,28 @@
<?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('transaksis', function (Blueprint $table) {
$table->decimal('discount', 15, 2)->default(0)->after('subtotal');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('transaksis', function (Blueprint $table) {
//
});
}
};

View File

@ -0,0 +1,29 @@
<?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::create('order_cancellations', function (Blueprint $table) {
$table->id();
$table->foreignId('transaksi_id')->constrained('transaksis')->onDelete('cascade');
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('reason');
$table->text('description')->nullable();
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->text('admin_note')->nullable();
$table->foreignId('reviewed_by')->nullable()->constrained('users')->onDelete('set null');
$table->timestamp('reviewed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('order_cancellations');
}
};

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(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@ -0,0 +1,28 @@
<?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(): void
{
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};

BIN
public/images/bara f1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pengajuan Pembatalan - Admin</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #f8f9fa; }
.admin-container { display: flex; min-height: 100vh; }
.sidebar {
width: 280px; background: linear-gradient(180deg, #28a745, #20c997);
color: white; min-height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000;
}
.sidebar-header { padding: 25px 20px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sidebar-header h3 { font-size: 1.8rem; font-weight: bold; margin-bottom: 5px; }
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.sidebar-menu { padding: 20px 0; }
.menu-item {
display: block; padding: 15px 25px; color: white;
text-decoration: none; transition: all 0.3s; border-left: 4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background: rgba(255,255,255,0.2); color: white;
border-left-color: #fff; transform: translateX(5px);
}
.menu-item i { width: 20px; margin-right: 10px; }
.main-content { margin-left: 280px; flex: 1; }
.admin-header {
background: white; padding: 20px 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-bottom: 1px solid #dee2e6;
}
.content-area { padding: 30px; }
.badge-status { padding: 4px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
.badge-pending { background: #fff3cd; color: #856404; }
.badge-approved { background: #d4edda; color: #155724; }
.badge-rejected { background: #f8d7da; color: #721c24; }
.card { border: none; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.card-header { background: white; border-bottom: 2px solid #f8f9fa; border-radius: 12px 12px 0 0 !important; }
.back-button {
position: fixed; top: 20px; right: 20px; z-index: 1100;
background: linear-gradient(45deg, #dc3545, #c82333);
color: white; border: none; padding: 12px 20px;
border-radius: 25px; font-weight: 600; text-decoration: none; transition: all 0.3s;
}
.back-button:hover { color: white; transform: translateY(-2px); }
.table th { background: #f8f9fa; font-weight: 600; color: #495057; font-size: 0.85rem; }
.table td { vertical-align: middle; }
.action-btn { padding: 5px 12px; border-radius: 6px; font-size: 0.8rem; font-weight: 600; border: none; cursor: pointer; }
.action-btn.btn-success { background-color: #28a745 !important; color: white !important; opacity: 1 !important; }
.action-btn.btn-danger { background-color: #dc3545 !important; color: white !important; opacity: 1 !important; }
</style>
</head>
<body>
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
{{-- Sidebar --}}
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item">
<i class="fas fa-tachometer-alt"></i>Dashboard
</a>
<a href="{{ route('admin.products.index') }}" class="menu-item">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
<a href="{{ route('admin.cancellations') }}" class="menu-item active">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan
</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
{{-- Main Content --}}
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Pengajuan Pembatalan</li>
</ol>
</nav>
<h2>Pengajuan Pembatalan Pesanan</h2>
<p class="mb-0">Kelola semua permintaan pembatalan dari pelanggan.</p>
</div>
<div class="content-area">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Stats --}}
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card p-3 border-start border-warning border-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-warning fw-bold small">MENUNGGU REVIEW</div>
<div class="fs-3 fw-bold">{{ $cancellations->where('status','pending')->count() }}</div>
</div>
<i class="fas fa-clock fa-2x text-muted"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 border-start border-success border-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-success fw-bold small">DISETUJUI</div>
<div class="fs-3 fw-bold">{{ $cancellations->where('status','approved')->count() }}</div>
</div>
<i class="fas fa-check-circle fa-2x text-muted"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 border-start border-danger border-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-danger fw-bold small">DITOLAK</div>
<div class="fs-3 fw-bold">{{ $cancellations->where('status','rejected')->count() }}</div>
</div>
<i class="fas fa-times-circle fa-2x text-muted"></i>
</div>
</div>
</div>
</div>
{{-- Tabel --}}
<div class="card">
<div class="card-header py-3">
<h6 class="mb-0 fw-bold text-success">
<i class="fas fa-times-circle me-2"></i>
Daftar Pengajuan Pembatalan ({{ $cancellations->total() }})
</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="ps-3">Invoice</th>
<th>Pelanggan</th>
<th>Alasan</th>
<th>Keterangan</th>
<th>Tgl Pengajuan</th>
<th>Status</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($cancellations as $item)
<tr>
<td class="ps-3">
<strong class="d-block">{{ $item->transaksi->invoice_number ?? '-' }}</strong>
<small class="text-muted">
Rp{{ number_format($item->transaksi->total_amount ?? 0, 0, ',', '.') }}
</small>
</td>
<td>
<strong>{{ $item->user->name ?? '-' }}</strong><br>
<small class="text-muted">{{ $item->user->email ?? '-' }}</small>
</td>
<td style="max-width:180px;">
<small>{{ $item->reason }}</small>
</td>
<td style="max-width:200px;">
<small class="text-muted">{{ $item->description ?? '-' }}</small>
</td>
<td>
<small>{{ $item->created_at->format('d M Y') }}</small><br>
<small class="text-muted">{{ $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 class="text-muted">{{ $item->reviewed_at->format('d M Y') }}</small>
@endif
</td>
<td class="text-center">
@if($item->status === 'pending')
{{-- Tombol Setujui --}}
<form action="{{ route('admin.cancellations.approve', $item->id) }}" method="POST"
class="d-inline"
onsubmit="return confirm('Setujui pembatalan pesanan {{ $item->transaksi->invoice_number ?? '' }}?')">
@csrf
<textarea name="admin_note" style="display:none"> disetujui</textarea>
<button type="submit" class="action-btn btn-success text-white me-1">
<i class="fas fa-check me-1"></i>Setujui
</button>
</form>
{{-- Tombol Tolak --}}
<button type="button"
class="action-btn btn-danger text-white"
onclick="showRejectModal({{ $item->id }}, '{{ $item->transaksi->invoice_number ?? '' }}')">
<i class="fas fa-times me-1"></i>Tolak
</button>
@else
@if($item->status === 'approved')
<span class="action-btn btn-success">Disetujui</span>
@elseif($item->status === 'rejected')
<span class="action-btn btn-danger">Ditolak</span>
@else
<span class="text-muted">-</span>
@endif
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3 d-block"></i>
<p class="text-muted">Belum ada pengajuan pembatalan</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Pagination --}}
@if($cancellations->hasPages())
<div class="d-flex justify-content-between align-items-center p-3 border-top">
<small class="text-muted">
Menampilkan {{ $cancellations->firstItem() }} - {{ $cancellations->lastItem() }}
dari {{ $cancellations->total() }} pengajuan
</small>
{{ $cancellations->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
</div>
{{-- Modal Tolak --}}
<div class="modal fade" id="rejectModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:420px;">
<div class="modal-content border-0" style="border-radius:14px; overflow:hidden;">
<div class="modal-header bg-danger text-white">
<h6 class="modal-title fw-bold mb-0">
<i class="fas fa-times-circle me-2"></i>Tolak Pengajuan
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="rejectForm" method="POST">
@csrf
<div class="modal-body p-4">
<p class="mb-3">Pesanan: <strong id="rejectInvoice"></strong></p>
<label class="form-label fw-semibold">
Alasan Penolakan <span class="text-danger">*</span>
</label>
<textarea name="admin_note" class="form-control" rows="3"
placeholder="Jelaskan alasan penolakan kepada pelanggan..."
required maxlength="500"></textarea>
<small class="text-muted">Wajib diisi. Maks. 500 karakter.</small>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-danger fw-bold">
<i class="fas fa-times me-1"></i>Tolak Pengajuan
</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function showRejectModal(id, invoice) {
document.getElementById('rejectInvoice').textContent = invoice;
document.getElementById('rejectForm').action = `/admin/cancellations/${id}/reject`;
new bootstrap.Modal(document.getElementById('rejectModal')).show();
}
</script>
</body>
</html>

View File

@ -5,217 +5,94 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Admin - Bibit Cabai</title> <title>Dashboard Admin - Bibit Cabai</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style> <style>
* { * { margin: 0; padding: 0; box-sizing: border-box; }
margin: 0; body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
padding: 0; .admin-container { display: flex; min-height: 100vh; }
box-sizing: border-box;
}
body { /* Sidebar */
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; .sidebar { width: 280px; background: linear-gradient(180deg, #28a745, #20c997); color: white; min-height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000; transition: all 0.3s ease; }
background: #f8f9fa; .sidebar-header { padding: 25px 20px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1); }
} .sidebar-header h3 { font-size: 1.8rem; font-weight: bold; margin-bottom: 5px; }
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.admin-container { .sidebar-menu { padding: 20px 0; }
display: flex; .menu-item { display: block; padding: 15px 25px; color: white; text-decoration: none; transition: all 0.3s; border-left: 4px solid transparent; cursor: pointer; }
min-height: 100vh; .menu-item:hover, .menu-item.active { background: rgba(255,255,255,0.1); color: white; text-decoration: none; border-left-color: #fff; transform: translateX(5px); }
} .menu-item i { width: 20px; margin-right: 10px; }
/* Sidebar Styles */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #28a745, #20c997);
color: white;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
transition: all 0.3s ease;
}
.sidebar-header {
padding: 25px 20px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 {
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 5px;
}
.sidebar-header p {
opacity: 0.8;
font-size: 0.9rem;
}
.sidebar-menu {
padding: 20px 0;
}
.menu-item {
display: block;
padding: 15px 25px;
color: white;
text-decoration: none;
transition: all 0.3s;
border-left: 4px solid transparent;
cursor: pointer;
}
.menu-item:hover, .menu-item.active {
background: rgba(255,255,255,0.1);
color: white;
text-decoration: none;
border-left-color: #fff;
transform: translateX(5px);
}
.menu-item i {
width: 20px;
margin-right: 10px;
}
/* Main Content */ /* Main Content */
.main-content { .main-content { margin-left: 280px; flex: 1; min-height: 100vh; }
margin-left: 280px; .admin-header { background: white; padding: 20px 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-bottom: 1px solid #dee2e6; }
flex: 1; .content-area { padding: 30px; }
min-height: 100vh;
}
.admin-header {
background: white;
padding: 20px 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: 1px solid #dee2e6;
}
.content-area {
padding: 30px;
}
/* Stats Cards */ /* Stats Cards */
.stats-grid { .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 25px; margin-bottom: 40px; }
display: grid; .stat-card { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border-left: 5px solid; transition: transform 0.3s ease; }
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); .stat-card:hover { transform: translateY(-5px); }
gap: 25px;
margin-bottom: 40px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
border-left: 5px solid;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.products { border-left-color: #28a745; } .stat-card.products { border-left-color: #28a745; }
.stat-card.orders { border-left-color: #007bff; } .stat-card.orders { border-left-color: #007bff; }
.stat-card.revenue { border-left-color: #ffc107; } .stat-card.revenue { border-left-color: #ffc107; }
.stat-card.users { border-left-color: #6f42c1; } .stat-card.users { border-left-color: #6f42c1; }
.stat-number { font-size: 2.5rem; font-weight: bold; margin-bottom: 8px; }
.stat-number { .stat-label { color: #6c757d; font-size: 1rem; font-weight: 500; }
font-size: 2.5rem; .stat-icon { float: right; font-size: 2.5rem; opacity: 0.3; margin-top: -10px; }
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
color: #6c757d;
font-size: 1rem;
font-weight: 500;
}
.stat-icon {
float: right;
font-size: 2.5rem;
opacity: 0.3;
margin-top: -10px;
}
/* Back Button */ /* Back Button */
.back-button { .back-button { position: fixed; top: 20px; right: 20px; z-index: 1100; background: linear-gradient(45deg, #dc3545, #c82333); color: white; border: none; padding: 12px 20px; border-radius: 25px; font-weight: 600; transition: all 0.3s ease; text-decoration: none; }
position: fixed; .back-button:hover { background: linear-gradient(45deg, #c82333, #a71e2a); transform: translateY(-2px); color: white; text-decoration: none; }
top: 20px;
right: 20px; /* Chart Card Header */
z-index: 1100; .chart-card-header {
background: linear-gradient(45deg, #dc3545, #c82333); display: flex;
color: white; align-items: center;
border: none; justify-content: space-between;
padding: 12px 20px; flex-wrap: wrap;
border-radius: 25px; gap: 10px;
font-weight: 600;
transition: all 0.3s ease;
text-decoration: none;
} }
.back-button:hover { /* Lihat Detail Button */
background: linear-gradient(45deg, #c82333, #a71e2a); .btn-lihat-detail {
transform: translateY(-2px); background: rgba(255,255,255,0.2);
color: white; color: white;
border: 2px solid rgba(255,255,255,0.6);
padding: 7px 18px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.btn-lihat-detail:hover {
background: white;
color: #28a745;
border-color: white;
text-decoration: none;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
} }
/* Empty State */ /* Empty State */
.empty-state { .empty-state { text-align: center; padding: 60px 20px; color: #6c757d; }
text-align: center; .empty-state i { font-size: 4rem; margin-bottom: 20px; opacity: 0.3; }
padding: 60px 20px; .empty-state h4 { font-size: 1.5rem; margin-bottom: 10px; }
color: #6c757d; .empty-state p { font-size: 1rem; margin-bottom: 20px; }
}
.empty-state i {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.3;
}
.empty-state h4 {
font-size: 1.5rem;
margin-bottom: 10px;
}
.empty-state p {
font-size: 1rem;
margin-bottom: 20px;
}
/* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .sidebar { width: 100%; position: relative; min-height: auto; }
width: 100%; .main-content { margin-left: 0; }
position: relative; .stats-grid { grid-template-columns: 1fr; }
min-height: auto;
}
.main-content {
margin-left: 0;
}
.stats-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>
</head> </head>
<body> <body>
<!-- Back Button -->
<a href="{{ route('home') }}" class="back-button"> <a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website <i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a> </a>
@ -237,16 +114,20 @@
</a> </a>
<a href="{{ route('admin.orders') }}" class="menu-item {{ request()->routeIs('admin.orders') ? 'active' : '' }}"> <a href="{{ route('admin.orders') }}" class="menu-item {{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="fas fa-shopping-cart"></i>Pesanan <i class="fas fa-shopping-cart"></i>Pesanan
</a>
{{-- TAMBAHKAN DI SINI --}}
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a> </a>
<a href="{{ route('admin.users') }}" class="menu-item {{ request()->routeIs('admin.users') ? 'active' : '' }}"> <a href="{{ route('admin.users') }}" class="menu-item {{ request()->routeIs('admin.users') ? 'active' : '' }}">
<i class="fas fa-users"></i>Pengguna <i class="fas fa-users"></i>Pengguna
</a> </a>
<a href="{{ route('admin.reports') }}" class="menu-item {{ request()->routeIs('admin.reports') ? 'active' : '' }}"> <a href="{{ route('admin.laporan') }}" class="menu-item {{ request()->routeIs('admin.laporan') ? 'active' : '' }}">
<i class="fas fa-chart-line"></i>Laporan <i class="fas fa-chart-line"></i>Laporan
</a> </a>
<a href="{{ route('admin.settings') }}" class="menu-item {{ request()->routeIs('admin.settings') ? 'active' : '' }}"> <!-- <a href="{{ route('admin.settings') }}" class="menu-item {{ request()->routeIs('admin.settings') ? 'active' : '' }}">
<i class="fas fa-cog"></i>Pengaturan <i class="fas fa-cog"></i>Pengaturan
</a> </a> -->
<div class="mt-4 px-3"> <div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}"> <form method="POST" action="{{ route('admin.logout') }}">
@csrf @csrf
@ -268,42 +149,58 @@
<div class="content-area"> <div class="content-area">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card products"> <div class="stat-card products">
<div class="stat-number text-success">{{ $totalProducts ?? 0 }}</div> <div class="stat-number text-success">{{ $stats['total_products'] }}</div>
<div class="stat-label">Total Produk</div> <div class="stat-label">Total Produk</div>
<i class="fas fa-seedling stat-icon text-success"></i> <i class="fas fa-seedling stat-icon text-success"></i>
</div> </div>
<div class="stat-card orders"> <div class="stat-card orders">
<div class="stat-number text-primary">{{ $activeProducts ?? 0 }}</div> <div class="stat-number text-primary">{{ $stats['active_products'] }}</div>
<div class="stat-label">Produk Aktif</div> <div class="stat-label">Produk Aktif</div>
<i class="fas fa-check-circle stat-icon text-primary"></i> <i class="fas fa-check-circle stat-icon text-primary"></i>
</div> </div>
<div class="stat-card revenue"> <div class="stat-card revenue">
<div class="stat-number text-warning">{{ $totalSold ?? 0 }}</div> <div class="stat-number text-warning">{{ $stats['total_sold'] }}</div>
<div class="stat-label">Total Terjual</div> <div class="stat-label">Total Terjual</div>
<i class="fas fa-chart-line stat-icon text-warning"></i> <i class="fas fa-chart-line stat-icon text-warning"></i>
</div> </div>
<div class="stat-card users"> <div class="stat-card users">
<div class="stat-number text-info">{{ $lowStock ?? 0 }}</div> <div class="stat-number text-info">{{ $stats['low_stock'] }}</div>
<div class="stat-label">Stok Menipis</div> <div class="stat-label">Stok Menipis</div>
<i class="fas fa-exclamation-triangle stat-icon text-info"></i> <i class="fas fa-exclamation-triangle stat-icon text-info"></i>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-4"> <div class="col-12 mb-4">
<div class="card"> <div class="card">
<div class="card-header bg-success text-white"> <div class="card-header bg-success text-white">
<h5 class="mb-0">📈 Grafik Penjualan</h5> <div class="chart-card-header">
<h5 class="mb-0">📈 Grafik Pendapatan Harian (30 Hari Terakhir)</h5>
{{-- Tombol Lihat Detail mengarah ke halaman laporan --}}
<a href="{{ route('admin.laporan') }}" class="btn-lihat-detail">
<i class="fas fa-external-link-alt"></i> Lihat Detail
</a>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
@if(count($chartLabels) > 0)
<canvas id="salesChart" height="80"></canvas>
@else
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
<h4>Belum Ada Data</h4> <h4>Belum Ada Data</h4>
<p>Grafik penjualan akan ditampilkan setelah ada transaksi</p> <p>Grafik pendapatan akan ditampilkan setelah ada transaksi</p>
<a href="{{ route('admin.laporan') }}" class="btn btn-success">
<i class="fas fa-chart-bar me-2"></i>Buka Laporan
</a>
</div>
@endif
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="row">
<div class="col-md-6 mb-4"> <div class="col-md-6 mb-4">
<div class="card"> <div class="card">
<div class="card-header bg-info text-white"> <div class="card-header bg-info text-white">
@ -321,9 +218,54 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> -->
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
@if(count($chartLabels) > 0)
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: {!! json_encode($chartLabels) !!},
datasets: [{
label: 'Penjualan (Rp)',
data: {!! json_encode($chartData) !!},
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
borderWidth: 2,
pointBackgroundColor: '#28a745',
pointRadius: 4,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return 'Rp ' + Number(context.parsed.y).toLocaleString('id-ID');
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return 'Rp ' + Number(value).toLocaleString('id-ID');
}
}
}
}
}
});
@endif
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lupa Password Admin - TA Bibit Cabai</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.btn-primary-custom {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border: none;
border-radius: 25px;
color: white;
font-weight: 600;
}
.btn-primary-custom:hover {
background: linear-gradient(135deg, #0d7377 0%, #2dd654 100%);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(17, 153, 142, 0.3);
color: white;
}
.btn-primary-custom:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
cursor: not-allowed;
}
.form-control:focus {
border-color: #11998e;
box-shadow: 0 0 0 0.2rem rgba(17, 153, 142, 0.25);
}
/* Step Indicator */
.step-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28px;
}
.step-circle {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 13px;
}
.step-circle.active { background: #11998e; color: #fff; }
.step-circle.done { background: #11998e; color: #fff; }
.step-circle.idle { background: #dee2e6; color: #888; }
.step-label { font-size: 11px; white-space: nowrap; margin-top: 4px; }
.step-label.active { color: #11998e; font-weight: 700; }
.step-label.idle { color: #aaa; }
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-line { height: 2px; width: 36px; margin: 0 6px 18px; }
.step-line.done { background: #11998e; }
.step-line.idle { background: #dee2e6; }
.icon-box {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin: 0 auto 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-4 col-lg-4">
<div class="card login-card">
<div class="card-body p-5">
{{-- Icon --}}
<div class="icon-box">🔑</div>
<div class="text-center mb-3">
<h4 class="fw-bold text-dark mb-1">Lupa Password Admin</h4>
<p class="text-muted small">Masukkan email admin yang terdaftar</p>
</div>
{{-- Step Indicator --}}
<div class="step-wrapper">
<div class="step-item">
<div class="step-circle active">1</div>
<div class="step-label active">Email</div>
</div>
<div class="step-line idle"></div>
<div class="step-item">
<div class="step-circle idle">2</div>
<div class="step-label idle">Kode OTP</div>
</div>
<div class="step-line idle"></div>
<div class="step-item">
<div class="step-circle idle">3</div>
<div class="step-label idle">Password Baru</div>
</div>
</div>
{{-- Error --}}
@if ($errors->any())
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<small>
@foreach ($errors->all() as $error)
<div>⚠️ {{ $error }}</div>
@endforeach
</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
@if (session('error'))
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<small>⚠️ {{ session('error') }}</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Form --}}
<form method="POST" action="{{ route('admin.password.email') }}" id="forgotForm" novalidate>
@csrf
<div class="mb-4">
<label for="email" class="form-label fw-semibold">Email Admin</label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
id="email"
name="email"
value="{{ old('email') }}"
placeholder="admin@example.com"
autofocus
required>
<div class="form-text text-muted small">
Hanya akun admin yang terdaftar di sistem
</div>
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div id="emailError" class="text-danger mt-1" style="display:none;">
<small>⚠️ Format email tidak valid</small>
</div>
</div>
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary-custom py-2 fw-bold" id="submitBtn" disabled>
📨 Kirim Kode Verifikasi
</button>
</div>
</form>
<div class="text-center">
<a href="{{ route('admin.login') }}" class="text-decoration-none small" style="color:#11998e;">
Kembali ke Login Admin
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const emailInput = document.getElementById('email');
const submitBtn = document.getElementById('submitBtn');
const emailError = document.getElementById('emailError');
function validateEmail(val) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) && !/\s/.test(val);
}
emailInput.addEventListener('input', function () {
const val = this.value.trim();
if (val.length > 0 && !validateEmail(val)) {
emailError.style.display = 'block';
this.classList.add('is-invalid');
submitBtn.disabled = true;
} else {
emailError.style.display = 'none';
this.classList.remove('is-invalid');
submitBtn.disabled = val.length === 0;
}
});
emailInput.addEventListener('keydown', function (e) {
if (e.key === ' ') e.preventDefault();
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,612 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laporan Penjualan - Bibit Cabai</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
.admin-container { display: flex; min-height: 100vh; }
/* ===== SIDEBAR (sama persis dengan dashboard) ===== */
.sidebar { width: 280px; background: linear-gradient(180deg, #28a745, #20c997); color: white; min-height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000; transition: all 0.3s ease; }
.sidebar-header { padding: 25px 20px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sidebar-header h3 { font-size: 1.8rem; font-weight: bold; margin-bottom: 5px; }
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.sidebar-menu { padding: 20px 0; }
.menu-item { display: block; padding: 15px 25px; color: white; text-decoration: none; transition: all 0.3s; border-left: 4px solid transparent; cursor: pointer; }
.menu-item:hover, .menu-item.active { background: rgba(255,255,255,0.1); color: white; text-decoration: none; border-left-color: #fff; transform: translateX(5px); }
.menu-item i { width: 20px; margin-right: 10px; }
/* ===== MAIN CONTENT ===== */
.main-content { margin-left: 280px; flex: 1; min-height: 100vh; }
.admin-header { background: white; padding: 20px 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-bottom: 1px solid #dee2e6; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.admin-header-left h2 { margin-bottom: 2px; }
.admin-header-left p { margin: 0; color: #6c757d; font-size: 0.9rem; }
.content-area { padding: 30px; }
/* Back Button */
.back-button { position: fixed; top: 20px; right: 20px; z-index: 1100; background: linear-gradient(45deg, #dc3545, #c82333); color: white; border: none; padding: 12px 20px; border-radius: 25px; font-weight: 600; transition: all 0.3s ease; text-decoration: none; }
.back-button:hover { background: linear-gradient(45deg, #c82333, #a71e2a); transform: translateY(-2px); color: white; text-decoration: none; }
/* ===== FILTER BAR ===== */
.filter-bar {
background: white;
border-radius: 12px;
padding: 20px 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 28px;
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 16px;
}
.filter-bar .form-label { font-weight: 600; color: #495057; font-size: 0.85rem; margin-bottom: 6px; }
.filter-bar .form-control, .filter-bar .form-select {
border-radius: 8px;
border: 1.5px solid #dee2e6;
font-size: 0.9rem;
padding: 9px 13px;
transition: border-color 0.2s;
}
.filter-bar .form-control:focus, .filter-bar .form-select:focus {
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40,167,69,0.12);
}
.btn-filter {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
border: none;
padding: 10px 22px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s;
}
.btn-filter:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(40,167,69,0.35); color: white; }
.btn-export {
background: white;
color: #28a745;
border: 2px solid #28a745;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-export:hover { background: #28a745; color: white; text-decoration: none; }
/* ===== RINGKASAN CARDS ===== */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-bottom: 28px;
}
.summary-card {
background: white;
border-radius: 12px;
padding: 22px 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
border-top: 4px solid;
transition: transform 0.25s;
}
.summary-card:hover { transform: translateY(-4px); }
.summary-card.pendapatan { border-top-color: #28a745; }
.summary-card.transaksi { border-top-color: #007bff; }
.summary-card.produk { border-top-color: #fd7e14; }
.summary-card.rata { border-top-color: #6f42c1; }
.summary-val { font-size: 1.8rem; font-weight: 700; line-height: 1.2; }
.summary-val.green { color: #28a745; }
.summary-val.blue { color: #007bff; }
.summary-val.orange { color: #fd7e14; }
.summary-val.purple { color: #6f42c1; }
.summary-lbl { color: #6c757d; font-size: 0.88rem; font-weight: 500; margin-top: 4px; }
.summary-icon { float: right; font-size: 2rem; opacity: 0.18; margin-top: -6px; }
/* ===== CHART SECTION ===== */
.section-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 28px;
overflow: hidden;
}
.section-header {
padding: 18px 22px;
font-weight: 700;
font-size: 1rem;
display: flex;
align-items: center;
gap: 10px;
color: white;
}
.section-header.green { background: linear-gradient(135deg, #28a745, #20c997); }
.section-header.blue { background: linear-gradient(135deg, #007bff, #0056b3); }
.section-header.orange { background: linear-gradient(135deg, #fd7e14, #e55a00); }
.section-body { padding: 22px; }
/* ===== TABEL ===== */
.table-responsive { border-radius: 8px; overflow: hidden; }
.table-laporan { font-size: 0.9rem; margin: 0; }
.table-laporan thead th {
background: #f1f5f9;
color: #495057;
font-weight: 700;
border-bottom: 2px solid #dee2e6;
padding: 13px 15px;
white-space: nowrap;
}
.table-laporan tbody td { padding: 12px 15px; vertical-align: middle; border-bottom: 1px solid #f0f0f0; }
.table-laporan tbody tr:hover { background: #f8fff9; }
.table-laporan tbody tr:last-child td { border-bottom: none; }
/* Badge status */
.badge-status { padding: 5px 12px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
.badge-selesai { background: #d4edda; color: #155724; }
.badge-proses { background: #cce5ff; color: #004085; }
.badge-batal { background: #f8d7da; color: #721c24; }
.badge-kirim { background: #fff3cd; color: #856404; }
/* Progress bar produk */
.progress-thin { height: 6px; border-radius: 10px; background: #e9ecef; }
.progress-fill-green { background: linear-gradient(90deg, #28a745, #20c997); }
.progress-fill-orange { background: linear-gradient(90deg, #fd7e14, #ffc107); }
/* Empty state */
.empty-state { text-align: center; padding: 50px 20px; color: #adb5bd; }
.empty-state i { font-size: 3.5rem; margin-bottom: 15px; display: block; }
/* Pagination */
.page-link { color: #28a745; border-color: #dee2e6; }
.page-item.active .page-link { background-color: #28a745; border-color: #28a745; }
.page-link:hover { color: #155724; border-color: #28a745; }
@media (max-width: 768px) {
.sidebar { width: 100%; position: relative; min-height: auto; }
.main-content { margin-left: 0; }
.summary-grid { grid-template-columns: 1fr 1fr; }
.filter-bar { flex-direction: column; align-items: stretch; }
}
</style>
</head>
<body>
<!-- Back Button -->
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
<!-- ===== SIDEBAR ===== -->
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin User' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="fas fa-tachometer-alt"></i>Dashboard
</a>
<a href="{{ route('admin.products.index') }}" class="menu-item {{ request()->routeIs('admin.products.*') ? 'active' : '' }}">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item {{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
{{-- TAMBAHKAN DI SINI --}}
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item {{ request()->routeIs('admin.users') ? 'active' : '' }}">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item {{ request()->routeIs('admin.laporan') ? 'active' : '' }}">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item {{ request()->routeIs('admin.settings') ? 'active' : '' }}">
<i class="fas fa-cog"></i>Pengaturan
</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100"
onclick="return confirm('Apakah Anda yakin ingin logout?')">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
<!-- ===== MAIN CONTENT ===== -->
<div class="main-content">
<div class="admin-header">
<div class="admin-header-left">
<h2>📊 Laporan Penjualan</h2>
<p>Detail pendapatan, transaksi, dan produk yang keluar</p>
</div>
<a href="{{ route('admin.laporan.export') }}" class="btn-export">
<i class="fas fa-file-excel"></i> Export Excel
</a>
</div>
<div class="content-area">
{{-- ===== FILTER BAR ===== --}}
<div class="filter-bar">
<form method="GET" action="{{ route('admin.laporan') }}" style="display:contents">
<div>
<label class="form-label">Dari Tanggal</label>
<input type="date" name="start_date" class="form-control"
value="{{ request('start_date', now()->startOfMonth()->toDateString()) }}"
style="min-width:160px">
</div>
<div>
<label class="form-label">Sampai Tanggal</label>
<input type="date" name="end_date" class="form-control"
value="{{ request('end_date', now()->toDateString()) }}"
style="min-width:160px">
</div>
<div>
<label class="form-label">Status Pesanan</label>
<select name="status" class="form-select" style="min-width:160px">
<option value="">Semua Status</option>
<option value="delivered" {{ request('status')=='delivered' ? 'selected':'' }}>Selesai</option>
<option value="processing" {{ request('status')=='processing' ? 'selected':'' }}>Diproses</option>
<option value="shipped" {{ request('status')=='shipped' ? 'selected':'' }}>Dikirim</option>
<option value="cancelled" {{ request('status')=='cancelled' ? 'selected':'' }}>Dibatal</option>
<option value="pending" {{ request('status')=='pending' ? 'selected':'' }}>Pending</option>
</select>
</div>
<div>
<label class="form-label">Produk</label>
<select name="product_id" class="form-select" style="min-width:180px">
<option value="">Semua Produk</option>
@foreach($products ?? [] as $product)
<option value="{{ $product->id }}" {{ request('product_id')==$product->id ? 'selected':'' }}>
{{ $product->name }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-filter">
<i class="fas fa-search me-1"></i> Tampilkan
</button>
<a href="{{ route('admin.laporan') }}" class="btn btn-outline-secondary" style="border-radius:8px;padding:10px 18px;font-size:.9rem">
<i class="fas fa-redo me-1"></i> Reset
</a>
</form>
</div>
{{-- ===== RINGKASAN ===== --}}
<div class="summary-grid">
<div class="summary-card pendapatan">
<i class="fas fa-wallet summary-icon text-success"></i>
<div class="summary-val green">Rp {{ number_format($summary['total_pendapatan'] ?? 0, 0, ',', '.') }}</div>
<div class="summary-lbl">Total Pendapatan</div>
</div>
<div class="summary-card transaksi">
<i class="fas fa-receipt summary-icon text-primary"></i>
<div class="summary-val blue">{{ number_format($summary['total_transaksi'] ?? 0) }}</div>
<div class="summary-lbl">Total Transaksi</div>
</div>
<div class="summary-card produk">
<i class="fas fa-box-open summary-icon text-warning"></i>
<div class="summary-val orange">{{ number_format($summary['total_produk_keluar'] ?? 0) }}</div>
<div class="summary-lbl">Produk Terjual (unit)</div>
</div>
<div class="summary-card rata">
<i class="fas fa-calculator summary-icon text-purple"></i>
<div class="summary-val purple">Rp {{ number_format($summary['rata_per_transaksi'] ?? 0, 0, ',', '.') }}</div>
<div class="summary-lbl">Rata-rata per Transaksi</div>
</div>
</div>
{{-- ===== GRAFIK PENDAPATAN ===== --}}
<div class="section-card">
<div class="section-header green">
<i class="fas fa-chart-area"></i> Grafik Pendapatan Harian
</div>
<div class="section-body">
@if(isset($chartLabels) && count($chartLabels) > 0)
<canvas id="revenueChart" height="90"></canvas>
@else
<div class="empty-state">
<i class="fas fa-chart-area"></i>
<p>Belum ada data grafik untuk periode ini.</p>
</div>
@endif
</div>
</div>
<div class="row g-4 mb-4">
{{-- ===== GRAFIK PRODUK TERLARIS ===== --}}
<div class="col-lg-5">
<div class="section-card mb-0 h-100">
<div class="section-header orange">
<i class="fas fa-fire"></i> Produk Terlaris
</div>
<div class="section-body">
@if(isset($topProducts) && count($topProducts) > 0)
<canvas id="topProductChart" height="220"></canvas>
@else
<div class="empty-state">
<i class="fas fa-seedling"></i>
<p>Belum ada data produk terlaris.</p>
</div>
@endif
</div>
</div>
</div>
{{-- ===== TABEL PRODUK KELUAR ===== --}}
<div class="col-lg-7">
<div class="section-card mb-0 h-100">
<div class="section-header blue">
<i class="fas fa-boxes"></i> Detail Produk Keluar
</div>
<div class="section-body p-0">
@if(isset($productSales) && count($productSales) > 0)
<div class="table-responsive">
<table class="table table-laporan">
<thead>
<tr>
<th>#</th>
<th>Nama Produk</th>
<th class="text-center">Terjual (unit)</th>
<th class="text-end">Pendapatan</th>
<th>Proporsi</th>
</tr>
</thead>
<tbody>
@foreach($productSales as $i => $item)
<tr>
<td class="text-muted">{{ $i + 1 }}</td>
<td>
<strong>{{ $item->product_name }}</strong>
<br><small class="text-muted">{{ $item->category ?? '-' }}</small>
</td>
<td class="text-center">
<span class="badge bg-light text-dark border fw-bold">{{ number_format($item->total_qty) }}</span>
</td>
<td class="text-end fw-bold text-success">
Rp {{ number_format($item->total_revenue, 0, ',', '.') }}
</td>
<td style="min-width:100px">
@php
$pct = $summary['total_produk_keluar'] > 0
? round($item->total_qty / $summary['total_produk_keluar'] * 100)
: 0;
@endphp
<div class="d-flex align-items-center gap-2">
<div class="progress progress-thin flex-grow-1">
<div class="progress-fill-green" style="width:{{ $pct }}%;height:100%;border-radius:10px"></div>
</div>
<small class="text-muted">{{ $pct }}%</small>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="empty-state">
<i class="fas fa-box-open"></i>
<p>Belum ada data produk untuk periode ini.</p>
</div>
@endif
</div>
</div>
</div>
</div>
{{-- ===== TABEL DETAIL TRANSAKSI ===== --}}
<div class="section-card">
<div class="section-header green">
<i class="fas fa-list-alt"></i> Riwayat Transaksi Detail
</div>
<div class="section-body p-0">
@if(isset($orders) && $orders->count() > 0)
<div class="table-responsive">
<table class="table table-laporan">
<thead>
<tr>
<th>No. Order</th>
<th>Tanggal</th>
<th>Pelanggan</th>
<th>Produk</th>
<th class="text-center">Qty</th>
<th class="text-end">Total</th>
<th class="text-center">Status</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach($orders as $order)
<tr>
<td><code class="text-success">#{{ $order->invoice_number ?? $order->id }}</code></td>
<td class="text-muted" style="white-space:nowrap">
{{ \Carbon\Carbon::parse($order->created_at)->format('d M Y') }}<br>
<small>{{ \Carbon\Carbon::parse($order->created_at)->format('H:i') }}</small>
</td>
<td>
<strong>{{ $order->customer_name ?? $order->user->name ?? 'Guest' }}</strong><br>
<small class="text-muted">{{ $order->customer_email ?? $order->user->email ?? '-' }}</small>
</td>
<td>
{{-- Relasi di Transaksi adalah 'details', bukan 'items' --}}
@forelse($order->details ?? [] as $detail)
<div>{{ $detail->product->name ?? '-' }}</div>
@empty
<span class="text-muted">-</span>
@endforelse
</td>
<td class="text-center">
@forelse($order->details ?? [] as $detail)
<div>{{ $detail->quantity }}</div>
@empty
<span class="text-muted">-</span>
@endforelse
</td>
<td class="text-end fw-bold text-success" style="white-space:nowrap">
Rp {{ number_format($order->total_amount ?? 0, 0, ',', '.') }}
</td>
<td class="text-center">
{{-- Status sesuai nilai enum di DB: pending, processing, shipped, delivered, cancelled --}}
@php $orderStatus = $order->order_status ?? 'pending'; @endphp
@if($orderStatus == 'delivered')
<span class="badge-status badge-selesai"> Selesai</span>
@elseif($orderStatus == 'shipped')
<span class="badge-status badge-kirim">🚚 Dikirim</span>
@elseif($orderStatus == 'cancelled')
<span class="badge-status badge-batal"> Batal</span>
@elseif($orderStatus == 'processing')
<span class="badge-status badge-proses"> Diproses</span>
@else
<span class="badge-status" style="background:#e9ecef;color:#6c757d"> Pending</span>
@endif
{{-- Payment status --}}
<br>
@php $payStatus = $order->payment_status ?? 'pending'; @endphp
@if($payStatus == 'paid')
<small class="text-success fw-bold">💳 Paid</small>
@elseif($payStatus == 'failed')
<small class="text-danger fw-bold"> Failed</small>
@else
<small class="text-warning fw-bold"> Pending</small>
@endif
</td>
<td class="text-center">
<a href="{{ route('admin.transaksis.show', $order->id) }}"
class="btn btn-sm btn-outline-success" title="Lihat Detail">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Pagination --}}
@if($orders->hasPages())
<div class="d-flex justify-content-between align-items-center px-4 py-3 border-top">
<small class="text-muted">
Menampilkan {{ $orders->firstItem() }}{{ $orders->lastItem() }} dari {{ $orders->total() }} transaksi
</small>
{{ $orders->appends(request()->query())->links() }}
</div>
@endif
@else
<div class="empty-state">
<i class="fas fa-receipt"></i>
<p>Belum ada transaksi untuk periode yang dipilih.</p>
</div>
@endif
</div>
</div>
</div>{{-- end content-area --}}
</div>{{-- end main-content --}}
</div>{{-- end admin-container --}}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
// ===== GRAFIK PENDAPATAN HARIAN =====
@if(isset($chartLabels) && count($chartLabels) > 0)
const ctxRevenue = document.getElementById('revenueChart').getContext('2d');
new Chart(ctxRevenue, {
type: 'bar',
data: {
labels: {!! json_encode($chartLabels) !!},
datasets: [
{
type: 'line',
label: 'Tren',
data: {!! json_encode($chartData) !!},
borderColor: '#20c997',
backgroundColor: 'transparent',
borderWidth: 2,
pointRadius: 3,
tension: 0.4,
order: 1
},
{
type: 'bar',
label: 'Pendapatan (Rp)',
data: {!! json_encode($chartData) !!},
backgroundColor: 'rgba(40,167,69,0.15)',
borderColor: '#28a745',
borderWidth: 1.5,
borderRadius: 6,
order: 2
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true, position: 'top' },
tooltip: {
callbacks: {
label: ctx => ctx.dataset.label + ': Rp ' + Number(ctx.parsed.y).toLocaleString('id-ID')
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: '#f0f0f0' },
ticks: { callback: v => 'Rp ' + Number(v).toLocaleString('id-ID') }
},
x: { grid: { display: false } }
}
}
});
@endif
// ===== GRAFIK PRODUK TERLARIS (Doughnut) =====
@if(isset($topProducts) && count($topProducts) > 0)
const ctxProd = document.getElementById('topProductChart').getContext('2d');
new Chart(ctxProd, {
type: 'doughnut',
data: {
labels: {!! json_encode($topProducts->pluck('product_name')) !!},
datasets: [{
data: {!! json_encode($topProducts->pluck('total_qty')) !!},
backgroundColor: ['#28a745','#20c997','#007bff','#fd7e14','#6f42c1','#dc3545','#ffc107'],
borderWidth: 2,
borderColor: '#fff',
hoverOffset: 8
}]
},
options: {
responsive: true,
cutout: '60%',
plugins: {
legend: { position: 'bottom', labels: { padding: 12, font: { size: 12 } } },
tooltip: {
callbacks: {
label: ctx => ctx.label + ': ' + Number(ctx.parsed).toLocaleString('id-ID') + ' unit'
}
}
}
}
});
@endif
</script>
</body>
</html>

View File

@ -205,16 +205,19 @@
</a> </a>
<a href="{{ route('admin.orders') }}" class="menu-item {{ request()->routeIs('admin.orders') ? 'active' : '' }}"> <a href="{{ route('admin.orders') }}" class="menu-item {{ request()->routeIs('admin.orders') ? 'active' : '' }}">
<i class="fas fa-shopping-cart"></i>Pesanan <i class="fas fa-shopping-cart"></i>Pesanan
</a>
<a href="{{ route('admin.cancellations') }}" class="menu-item active">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a> </a>
<a href="{{ route('admin.users') }}" class="menu-item {{ request()->routeIs('admin.users') ? 'active' : '' }}"> <a href="{{ route('admin.users') }}" class="menu-item {{ request()->routeIs('admin.users') ? 'active' : '' }}">
<i class="fas fa-users"></i>Pengguna <i class="fas fa-users"></i>Pengguna
</a> </a>
<a href="{{ route('admin.reports') }}" class="menu-item {{ request()->routeIs('admin.reports') ? 'active' : '' }}"> <a href="{{ route('admin.laporan') }}" class="menu-item {{ request()->routeIs('admin.laporan') ? 'active' : '' }}">
<i class="fas fa-chart-line"></i>Laporan <i class="fas fa-chart-line"></i>Laporan
</a> </a>
<a href="{{ route('admin.settings') }}" class="menu-item {{ request()->routeIs('admin.settings') ? 'active' : '' }}"> <!-- <a href="{{ route('admin.settings') }}" class="menu-item {{ request()->routeIs('admin.settings') ? 'active' : '' }}">
<i class="fas fa-cog"></i>Pengaturan <i class="fas fa-cog"></i>Pengaturan
</a> </a> -->
<div class="mt-4 px-3"> <div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}"> <form method="POST" action="{{ route('admin.logout') }}">
@csrf @csrf

View File

@ -116,7 +116,12 @@ class="btn btn-link position-absolute end-0 top-50 translate-middle-y pe-3"
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="text-end mb-3">
<a href="{{ route('admin.password.request') }}"
class="text-decoration-none small" style="color: #11998e;">
Lupa Password?
</a>
</div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-login py-3 fw-bold"> <button type="submit" class="btn btn-login py-3 fw-bold">
🌿 Masuk ke Dashboard 🌿 Masuk ke Dashboard
@ -133,9 +138,9 @@ class="btn btn-link position-absolute end-0 top-50 translate-middle-y pe-3"
{{-- Debug Info (Hapus di production) --}} {{-- Debug Info (Hapus di production) --}}
@if (config('app.debug')) @if (config('app.debug'))
<div class="mt-4 p-2 bg-light rounded small"> <div class="mt-4 p-2 bg-light rounded small">
<strong>Debug Info:</strong><br> <!-- <strong>Debug Info:</strong><br>
Route: {{ Route::currentRouteName() }}<br> Route: {{ Route::currentRouteName() }}<br>
Method: {{ request()->method() }}<br> Method: {{ request()->method() }}<br> -->
@if(request()->method() === 'POST') @if(request()->method() === 'POST')
Data: {{ json_encode(request()->only(['email'])) }} Data: {{ json_encode(request()->only(['email'])) }}
@endif @endif

View File

@ -1,269 +1,603 @@
@extends('layouts.app') <!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Orders Management - Bibit Cabai Admin</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
@section('title', 'Orders Management') <!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
@section('content') <!-- Font Awesome -->
<div class="container-fluid"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <style>
<h1 class="h3 mb-0 text-gray-800">Orders Management</h1> * { margin: 0; padding: 0; box-sizing: border-box; }
<div>
<a href="{{ route('admin.transaksis.export') }}" class="btn btn-success"> body {
<i class="fas fa-file-excel"></i> Export Excel font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f8f9fa;
}
.admin-container {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #28a745, #20c997);
color: white;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.sidebar-header {
padding: 25px 20px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 {
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 5px;
}
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.sidebar-menu { padding: 20px 0; }
.menu-item {
display: block;
padding: 15px 25px;
color: white;
text-decoration: none;
transition: all 0.3s;
border-left: 4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background: rgba(255,255,255,0.2);
color: white;
text-decoration: none;
border-left-color: #fff;
transform: translateX(5px);
}
.menu-item i { width: 20px; margin-right: 10px; }
/* Main Content */
.main-content {
margin-left: 280px;
flex: 1;
min-height: 100vh;
}
.admin-header {
background: white;
padding: 20px 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: 1px solid #dee2e6;
}
.content-area { padding: 30px; }
.page-header {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
margin-bottom: 30px;
}
.breadcrumb { background: transparent; padding: 0; margin-bottom: 10px; }
.breadcrumb-item a { color: #28a745; text-decoration: none; }
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid;
}
.stat-card.primary { border-left-color: #007bff; }
.stat-card.warning { border-left-color: #ffc107; }
.stat-card.info { border-left-color: #17a2b8; }
.stat-card.success { border-left-color: #28a745; }
.stat-label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: 5px;
}
.stat-card.primary .stat-label { color: #007bff; }
.stat-card.warning .stat-label { color: #ffc107; }
.stat-card.info .stat-label { color: #17a2b8; }
.stat-card.success .stat-label { color: #28a745; }
.stat-value { font-size: 1.5rem; font-weight: 700; color: #333; }
.stat-icon { font-size: 2rem; color: #dee2e6; }
/* Table */
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.data-table th, .data-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.data-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
font-size: 0.85rem;
}
.data-table tbody tr:hover { background-color: #f8f9fc; }
/* Badges */
.badge-status {
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-pending { background: #fff3cd; color: #856404; }
.badge-processing { background: #d1ecf1; color: #0c5460; }
.badge-shipped { background: #cce5ff; color: #004085; }
.badge-delivered { background: #d4edda; color: #155724; }
.badge-cancelled { background: #f8d7da; color: #721c24; }
.badge-paid { background: #d4edda; color: #155724; }
.badge-failed { background: #f8d7da; color: #721c24; }
/* Action Buttons */
.btn-action {
padding: 6px 10px;
border: none;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-action:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
text-decoration: none;
color: inherit;
}
.btn-view { background: #17a2b8; color: white; }
.btn-edit { background: #ffc107; color: #212529; }
.btn-print { background: #6c757d; color: white; }
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f8f9fa;
}
.section-title { font-size: 1.3rem; font-weight: 600; color: #333; }
/* Back Button */
.back-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 1100;
background: linear-gradient(45deg, #dc3545, #c82333);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
font-weight: 600;
transition: all 0.3s;
text-decoration: none;
}
.back-button:hover {
background: linear-gradient(45deg, #c82333, #a71e2a);
transform: translateY(-2px);
color: white;
text-decoration: none;
}
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Pagination Fix */
.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 #dee2e6;
color: #28a745;
font-size: 0.85rem;
text-decoration: none;
background: white;
}
.pagination .page-item.active .page-link {
background: #28a745;
border-color: #28a745;
color: white;
}
.pagination .page-item.disabled .page-link {
color: #adb5bd;
pointer-events: none;
}
.pagination .page-item .page-link:hover {
background: #e9f7ef;
border-color: #28a745;
color: #28a745;
}
/* Cancelled row styling */
tr.row-cancelled {
background-color: #e9ecef !important;
opacity: 0.75;
}
tr.row-cancelled td {
color: #6c757d !important;
}
tr.row-cancelled strong,
tr.row-cancelled .text-success {
color: #6c757d !important;
}
tr.row-cancelled select,
tr.row-cancelled .btn-action {
pointer-events: none;
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.sidebar { width: 100%; position: relative; }
.main-content { margin-left: 0; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<!-- Back Button -->
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a> </a>
<div class="admin-container">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin User' }}</small>
</div> </div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item">
<i class="fas fa-tachometer-alt"></i>Dashboard
</a>
<a href="{{ route('admin.products.index') }}" class="menu-item">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item active">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
{{-- TAMBAHKAN DI SINI --}}
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan
</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('admin.dashboard') }}">
<i class="fas fa-home"></i> Dashboard
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Pesanan</li>
</ol>
</nav>
<h2>Orders Management</h2>
<p class="mb-0">Kelola semua pesanan pelanggan di sini!</p>
</div> </div>
<!-- Statistics Cards --> <div class="content-area fade-in">
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Orders
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ $orders->total() }}
</div>
</div>
<div class="col-auto">
<i class="fas fa-shopping-cart fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4"> @if(session('success'))
<div class="card border-left-warning shadow h-100 py-2"> <div class="alert alert-success alert-dismissible fade show" role="alert">
<div class="card-body"> <i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<div class="row no-gutters align-items-center"> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Pending Orders
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ \App\Models\Transaksi::where('order_status', 'pending')->count() }}
</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div> </div>
@endif
<div class="col-xl-3 col-md-6 mb-4"> <!-- Stats Cards -->
<div class="card border-left-info shadow h-100 py-2"> <div class="stats-grid">
<div class="card-body"> <div class="stat-card primary">
<div class="row no-gutters align-items-center"> <div>
<div class="col mr-2"> <div class="stat-label">Total Orders</div>
<div class="text-xs font-weight-bold text-info text-uppercase mb-1"> <div class="stat-value">{{ $orders->total() }}</div>
Processing Orders
</div> </div>
<div class="h5 mb-0 font-weight-bold text-gray-800"> <div class="stat-icon"><i class="fas fa-shopping-cart"></i></div>
{{ \App\Models\Transaksi::where('order_status', 'processing')->count() }}
</div> </div>
<div class="stat-card warning">
<div>
<div class="stat-label">Pending Orders</div>
<div class="stat-value">{{ \App\Models\Transaksi::where('order_status', 'pending')->count() }}</div>
</div> </div>
<div class="col-auto"> <div class="stat-icon"><i class="fas fa-clock"></i></div>
<i class="fas fa-box fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Delivered Orders
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ \App\Models\Transaksi::where('order_status', 'delivered')->count() }}
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div> </div>
<div class="stat-card info">
<div>
<div class="stat-label">Processing Orders</div>
<div class="stat-value">{{ \App\Models\Transaksi::where('order_status', 'processing')->count() }}</div>
</div> </div>
<div class="stat-icon"><i class="fas fa-box"></i></div>
</div> </div>
<div class="stat-card success">
<div>
<div class="stat-label">Delivered Orders</div>
<div class="stat-value">{{ \App\Models\Transaksi::where('order_status', 'delivered')->count() }}</div>
</div> </div>
<div class="stat-icon"><i class="fas fa-check-circle"></i></div>
</div> </div>
</div> </div>
<!-- Orders Table --> <!-- Orders Table -->
<div class="card shadow mb-4"> <div class="page-header">
<div class="card-header py-3 d-flex justify-content-between align-items-center"> <div class="section-header">
<h6 class="m-0 font-weight-bold text-primary">Orders List</h6> <h3 class="section-title">
<div class="d-flex gap-2"> <i class="fas fa-shopping-cart text-success me-2"></i>
<!-- Filter by Status --> Daftar Pesanan ({{ $orders->total() }})
<select class="form-control form-control-sm" id="filterStatus" onchange="filterOrders()"> </h3>
<option value="">All Status</option> <div class="d-flex gap-2 align-items-center">
<select class="form-select form-select-sm" id="filterStatus" onchange="filterOrders()" style="width: 160px;">
<option value="">Semua Status</option>
<option value="pending">Pending</option> <option value="pending">Pending</option>
<option value="processing">Processing</option> <option value="processing">Processing</option>
<option value="shipped">Shipped</option> <option value="shipped">Shipped</option>
<option value="delivered">Delivered</option> <option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option> <option value="cancelled">Cancelled</option>
</select> </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>
<div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered table-hover" id="ordersTable" width="100%" cellspacing="0"> <table class="data-table" id="ordersTable">
<thead class="thead-light"> <thead>
<tr> <tr>
<th>Invoice Number</th> <th>Invoice Number</th>
<th>Customer</th> <th>Customer</th>
<th>Phone</th> <th>Phone</th>
<th>Shipping Address</th>
<th>City</th>
<th>Province</th>
<th>Postal Code</th>
<th>Order Status</th> <th>Order Status</th>
<th>Payment Status</th> <th>Payment Status</th>
<th>Metode Pembayaran</th>
<th>Total</th> <th>Total</th>
<th>Order Date</th> <th>Order Date</th>
<th>Actions</th> <th>Aksi</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($orders as $order) @forelse($orders as $order)
<tr data-status="{{ $order->order_status }}"> <tr data-status="{{ $order->order_status }}"
<td> class="{{ $order->order_status === 'cancelled' ? 'row-cancelled' : '' }}">
<strong>{{ $order->invoice_number }}</strong> <td><strong>{{ $order->invoice_number }}</strong></td>
</td>
<td>{{ $order->customer_name }}</td> <td>{{ $order->customer_name }}</td>
<td>{{ $order->customer_phone }}</td> <td>{{ $order->customer_phone }}</td>
<td> <td>
<small>{{ \Illuminate\Support\Str::limit($order->shipping_address, 30) }}</small> <select class="badge-status badge-{{ $order->order_status }} border-0 cursor-pointer"
style="cursor:pointer; appearance:auto;"
onchange="updateStatus({{ $order->id }}, 'order_status', this.value, this)">
<option value="pending" {{ $order->order_status == 'pending' ? 'selected' : '' }}>Pending</option>
<option value="processing" {{ $order->order_status == 'processing' ? 'selected' : '' }}>Processing</option>
<option value="shipped" {{ $order->order_status == 'shipped' ? 'selected' : '' }}>Shipped</option>
<option value="delivered" {{ $order->order_status == 'delivered' ? 'selected' : '' }}>Delivered</option>
<option value="cancelled" {{ $order->order_status == 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</select>
@if($order->cancellation && $order->cancellation->status === 'pending')
<br><span class="badge-status badge-warning mt-1" style="font-size:0.7rem;">
Minta Batal
</span>
@endif
</td> </td>
<td>{{ $order->city }}</td>
<td>{{ $order->province }}</td>
<td>{{ $order->postal_code }}</td>
<td> <td>
@php <select class="badge-status badge-{{ $order->payment_status }} border-0"
$statusClass = [ style="cursor:pointer; appearance:auto;"
'pending' => 'warning', onchange="updateStatus({{ $order->id }}, 'payment_status', this.value, this)">
'processing' => 'info', <option value="pending" {{ $order->payment_status == 'pending' ? 'selected' : '' }}>Pending</option>
'shipped' => 'primary', <option value="paid" {{ $order->payment_status == 'paid' ? 'selected' : '' }}>Paid</option>
'delivered' => 'success', <option value="failed" {{ $order->payment_status == 'failed' ? 'selected' : '' }}>Failed</option>
'cancelled' => 'danger' </select>
][$order->order_status] ?? 'secondary'; </td>
@endphp <td>
<span class="badge badge-{{ $statusClass }}"> <span class="badge-status" style="background:#ced4da;color:#212529;">
{{ ucfirst($order->order_status) }} {{ ucwords(str_replace('_', ' ', $order->payment_method ?? '-')) }}
</span> </span>
</td> </td>
<td><strong class="text-success">Rp {{ number_format($order->total_amount, 0, ',', '.') }}</strong></td>
<td><small>{{ $order->created_at->format('d M Y H:i') }}</small></td>
<td> <td>
@php <div class="d-flex gap-1">
$paymentClass = [
'pending' => 'warning',
'paid' => 'success',
'failed' => 'danger'
][$order->payment_status] ?? 'secondary';
@endphp
<span class="badge badge-{{ $paymentClass }}">
{{ ucfirst($order->payment_status) }}
</span>
</td>
<td>
<strong>Rp {{ number_format($order->total_amount, 0, ',', '.') }}</strong>
</td>
<td>
<small>{{ $order->created_at->format('d M Y H:i') }}</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{{ route('admin.transaksis.show', $order->id) }}" <a href="{{ route('admin.transaksis.show', $order->id) }}"
class="btn btn-info btn-sm" class="btn-action btn-view" title="View Details">
title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{{ route('admin.transaksis.edit', $order->id) }}" <a href="{{ route('admin.transaksis.edit', $order->id) }}"
class="btn btn-warning btn-sm" class="btn-action btn-edit" title="Edit">
title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a href="{{ route('admin.transaksis.print', $order->id) }}" <a href="{{ route('admin.transaksis.print', $order->id) }}"
class="btn btn-secondary btn-sm" class="btn-action btn-print" title="Print Invoice" target="_blank">
title="Print Invoice"
target="_blank">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</a> </a>
</div> </div>
</td> </td>
</tr> </tr>
@empty @empty
<tr>
<td colspan="12" class="text-center py-4">
<i class="fas fa-inbox fa-3x text-gray-300 mb-3"></i>
<p class="text-gray-500">No orders found</p>
</td>
</tr>
@endforelse @endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-3"> <div class="d-flex justify-content-between align-items-center mt-4">
<div> <small class="text-muted">
Showing {{ $orders->firstItem() ?? 0 }} to {{ $orders->lastItem() ?? 0 }} Menampilkan {{ $orders->firstItem() ?? 0 }} sampai {{ $orders->lastItem() ?? 0 }}
of {{ $orders->total() }} orders dari {{ $orders->total() }} pesanan
</small>
<div>{{ $orders->links() }}</div>
</div> </div>
<div> </div>
{{ $orders->links() }}
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
@push('styles') <!-- Bootstrap JS -->
<style> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
.gap-2 {
gap: 0.5rem;
}
.table-hover tbody tr:hover {
background-color: #f8f9fc;
}
.btn-group-sm .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.badge {
font-size: 0.75rem;
padding: 0.35em 0.65em;
}
</style>
@endpush
@push('scripts')
<script> <script>
function filterOrders() { function filterOrders() {
const filterValue = document.getElementById('filterStatus').value.toLowerCase(); const filterValue = document.getElementById('filterStatus').value.toLowerCase();
const table = document.getElementById('ordersTable'); const rows = document.querySelectorAll('#ordersTable tbody tr[data-status]');
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr'); rows.forEach(row => {
const status = row.getAttribute('data-status');
row.style.display = (filterValue === '' || status === filterValue) ? '' : 'none';
});
}
for (let i = 0; i < rows.length; i++) { function updateStatus(orderId, field, value, selectEl) {
const status = rows[i].getAttribute('data-status'); // Jika status sudah cancelled, tidak bisa diubah
if (field === 'order_status') {
const row = selectEl.closest('tr');
const currentStatus = row.getAttribute('data-status');
if (currentStatus === 'cancelled' || currentStatus === 'delivered') {
alert('Pesanan yang sudah selesai atau dibatalkan tidak dapat diubah kembali.');
selectEl.value = currentStatus;
return;
}
}
if (filterValue === '' || status === filterValue) { const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
rows[i].style.display = '';
fetch(`/admin/orders/${orderId}/update-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ [field]: value })
})
.then(res => res.json())
.then(data => {
if (data.success) {
// Update warna badge sesuai nilai baru
const badgeClasses = ['badge-pending','badge-processing','badge-shipped','badge-delivered','badge-cancelled','badge-paid','badge-failed'];
selectEl.classList.remove(...badgeClasses);
selectEl.classList.add('badge-' + value);
// Update data-status di row jika yang diubah adalah order_status
if (field === 'order_status') {
const row = selectEl.closest('tr');
row.setAttribute('data-status', value);
// Tambahkan/hapus class abu-abu jika status cancelled
if (value === 'cancelled') {
row.classList.add('row-cancelled');
} else { } else {
rows[i].style.display = 'none'; row.classList.remove('row-cancelled');
} }
} }
// Tampilkan notifikasi kecil
showToast('Status berhasil diupdate!');
} else {
alert('Gagal update: ' + data.message);
}
})
.catch(() => alert('Terjadi kesalahan koneksi.'));
}
function showToast(msg) {
const toast = document.createElement('div');
toast.innerText = msg;
toast.style.cssText = 'position:fixed;bottom:20px;right:20px;background:#28a745;color:white;padding:10px 20px;border-radius:8px;z-index:9999;font-size:14px;';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
} }
</script> </script>
@endpush </body>
@endsection </html>

View File

@ -237,12 +237,12 @@
<a class="menu-item" href="{{ route('admin.users') }}"> <a class="menu-item" href="{{ route('admin.users') }}">
<i class="fas fa-users"></i>Pengguna <i class="fas fa-users"></i>Pengguna
</a> </a>
<a class="menu-item" href="{{ route('admin.reports') }}"> <a class="menu-item" href="{{ route('admin.laporan') }}">
<i class="fas fa-chart-line"></i>Laporan <i class="fas fa-chart-line"></i>Laporan
</a> </a>
<a class="menu-item" href="{{ route('admin.settings') }}"> <!-- <a class="menu-item" href="{{ route('admin.settings') }}">
<i class="fas fa-cog"></i>Pengaturan <i class="fas fa-cog"></i>Pengaturan
</a> </a> -->
<div class="mt-4 px-3"> <div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}"> <form method="POST" action="{{ route('admin.logout') }}">
@csrf @csrf

View File

@ -411,16 +411,20 @@
</a> </a>
<a href="{{ route('admin.orders') }}" class="menu-item"> <a href="{{ route('admin.orders') }}" class="menu-item">
<i class="fas fa-shopping-cart"></i>Pesanan <i class="fas fa-shopping-cart"></i>Pesanan
</a>
{{-- TAMBAHKAN DI SINI --}}
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a> </a>
<a href="{{ route('admin.users') }}" class="menu-item"> <a href="{{ route('admin.users') }}" class="menu-item">
<i class="fas fa-users"></i>Pengguna <i class="fas fa-users"></i>Pengguna
</a> </a>
<a href="{{ route('admin.reports') }}" class="menu-item"> <a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan <i class="fas fa-chart-line"></i>Laporan
</a> </a>
<a href="{{ route('admin.settings') }}" class="menu-item"> <!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan <i class="fas fa-cog"></i>Pengaturan
</a> </a> -->
<div class="mt-4 px-3"> <div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}"> <form method="POST" action="{{ route('admin.logout') }}">
@csrf @csrf

View File

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Password Admin - TA Bibit Cabai</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.btn-primary-custom {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border: none;
border-radius: 25px;
color: white;
font-weight: 600;
}
.btn-primary-custom:hover {
background: linear-gradient(135deg, #0d7377 0%, #2dd654 100%);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(17, 153, 142, 0.3);
color: white;
}
.btn-primary-custom:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
}
.form-control:focus {
border-color: #11998e;
box-shadow: 0 0 0 0.2rem rgba(17, 153, 142, 0.25);
}
.step-wrapper {
display: flex; align-items: center; justify-content: center; margin-bottom: 28px;
}
.step-circle {
width: 36px; height: 36px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px;
}
.step-circle.active { background: #11998e; color: #fff; }
.step-circle.done { background: #11998e; color: #fff; }
.step-circle.idle { background: #dee2e6; color: #888; }
.step-label { font-size: 11px; white-space: nowrap; margin-top: 4px; }
.step-label.active { color: #11998e; font-weight: 700; }
.step-label.done { color: #11998e; }
.step-label.idle { color: #aaa; }
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-line { height: 2px; width: 36px; margin: 0 6px 18px; }
.step-line.done { background: #11998e; }
.step-line.idle { background: #dee2e6; }
.icon-box {
width: 64px; height: 64px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 28px;
margin: 0 auto 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-4 col-lg-4">
<div class="card login-card">
<div class="card-body p-5">
<div class="icon-box">🔓</div>
<div class="text-center mb-3">
<h4 class="fw-bold text-dark mb-1">Password Baru Admin</h4>
<p class="text-muted small">Buat password baru yang kuat untuk akun admin</p>
</div>
{{-- Step Indicator --}}
<div class="step-wrapper">
<div class="step-item">
<div class="step-circle done"></div>
<div class="step-label done">Email</div>
</div>
<div class="step-line done"></div>
<div class="step-item">
<div class="step-circle done"></div>
<div class="step-label done">Kode OTP</div>
</div>
<div class="step-line done"></div>
<div class="step-item">
<div class="step-circle active">3</div>
<div class="step-label active">Password Baru</div>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<small>
@foreach ($errors->all() as $error)
<div>⚠️ {{ $error }}</div>
@endforeach
</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
@if (session('error'))
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<small>⚠️ {{ session('error') }}</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
<form method="POST" action="{{ route('admin.password.update') }}" id="resetForm" novalidate>
@csrf
<input type="hidden" name="token" value="{{ session('admin_reset_token') }}">
<input type="hidden" name="email" value="{{ session('admin_reset_email') }}">
{{-- New Password --}}
<div class="mb-3">
<label for="password" class="form-label fw-semibold">Password Baru</label>
<div class="input-group">
<input type="password"
class="form-control @error('password') is-invalid @enderror"
id="password"
name="password"
placeholder="Minimal 8 karakter"
required>
<button class="btn btn-outline-secondary" type="button" id="togglePw1">👁️</button>
</div>
<div class="form-text text-muted small"> Minimal 8 karakter, tanpa spasi</div>
@error('password')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
<div id="pwError" class="text-danger small mt-1" style="display:none;">
⚠️ Password minimal 8 karakter dan tidak boleh mengandung spasi
</div>
</div>
{{-- Strength Bar --}}
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">Kekuatan password:</small>
<small id="strengthLabel" class="fw-semibold text-secondary"></small>
</div>
<div class="progress" style="height:6px;border-radius:3px;">
<div id="strengthBar" class="progress-bar bg-secondary" style="width:0%;transition:width .3s;"></div>
</div>
</div>
{{-- Confirm Password --}}
<div class="mb-4">
<label for="password_confirmation" class="form-label fw-semibold">Konfirmasi Password</label>
<div class="input-group">
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
id="password_confirmation"
name="password_confirmation"
placeholder="Ulangi password baru"
required>
<button class="btn btn-outline-secondary" type="button" id="togglePw2">👁️</button>
</div>
@error('password_confirmation')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
<div id="confirmError" class="text-danger small mt-1" style="display:none;">
⚠️ Password tidak cocok
</div>
<div id="confirmOk" class="text-success small mt-1" style="display:none;">
Password cocok
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary-custom py-2 fw-bold" id="submitBtn" disabled>
💾 Simpan Password Baru
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const pw = document.getElementById('password');
const conf = document.getElementById('password_confirmation');
const btn = document.getElementById('submitBtn');
const bar = document.getElementById('strengthBar');
const label = document.getElementById('strengthLabel');
const pwErr = document.getElementById('pwError');
const cErr = document.getElementById('confirmError');
const cOk = document.getElementById('confirmOk');
// Toggle visibility
document.getElementById('togglePw1').addEventListener('click', () => {
pw.type = pw.type === 'password' ? 'text' : 'password';
document.getElementById('togglePw1').textContent = pw.type === 'password' ? '👁️' : '🙈';
});
document.getElementById('togglePw2').addEventListener('click', () => {
conf.type = conf.type === 'password' ? 'text' : 'password';
document.getElementById('togglePw2').textContent = conf.type === 'password' ? '👁️' : '🙈';
});
// Strength checker
function strength(v) {
let s = 0;
if (v.length >= 8) s++;
if (v.length >= 12) s++;
if (/[A-Z]/.test(v)) s++;
if (/[0-9]/.test(v)) s++;
if (/[^A-Za-z0-9]/.test(v)) s++;
return s;
}
pw.addEventListener('input', function () {
const v = this.value;
const invalid = v.length > 0 && (v.length < 8 || /\s/.test(v));
pwErr.style.display = invalid ? 'block' : 'none';
this.classList.toggle('is-invalid', invalid);
// Strength bar
if (v.length === 0) {
bar.style.width = '0%'; bar.className = 'progress-bar bg-secondary';
label.textContent = '—'; label.className = 'fw-semibold text-secondary';
} else {
const lvls = [
{ w:'20%', c:'bg-danger', l:'Sangat Lemah', lc:'text-danger' },
{ w:'40%', c:'bg-warning', l:'Lemah', lc:'text-warning' },
{ w:'60%', c:'bg-info', l:'Sedang', lc:'text-info' },
{ w:'80%', c:'bg-primary', l:'Kuat', lc:'text-primary' },
{ w:'100%',c:'bg-success', l:'Sangat Kuat', lc:'text-success' },
];
const lvl = lvls[Math.min(strength(v) - 1, 4)] || lvls[0];
bar.style.width = lvl.w; bar.className = `progress-bar ${lvl.c}`;
label.textContent = lvl.l; label.className = `fw-semibold ${lvl.lc}`;
}
checkConf(); checkBtn();
});
conf.addEventListener('input', function () { checkConf(); checkBtn(); });
function checkConf() {
if (conf.value.length === 0) {
cErr.style.display = 'none'; cOk.style.display = 'none';
conf.classList.remove('is-valid', 'is-invalid');
} else if (conf.value !== pw.value) {
cErr.style.display = 'block'; cOk.style.display = 'none';
conf.classList.add('is-invalid'); conf.classList.remove('is-valid');
} else {
cErr.style.display = 'none'; cOk.style.display = 'block';
conf.classList.remove('is-invalid'); conf.classList.add('is-valid');
}
}
function checkBtn() {
const pwOk = pw.value.length >= 8 && !/\s/.test(pw.value);
const confOk = conf.value === pw.value && conf.value.length > 0;
btn.disabled = !(pwOk && confOk);
}
[pw, conf].forEach(i => i.addEventListener('keydown', e => { if (e.key === ' ') e.preventDefault(); }));
});
</script>
</body>
</html>

View File

@ -1,37 +1,155 @@
@extends('layouts.app') <!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Order - Bibit Cabai Admin</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
.admin-container { display: flex; min-height: 100vh; }
@section('title', 'Edit Order') .sidebar {
width: 280px; background: linear-gradient(180deg, #28a745, #20c997);
color: white; min-height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000;
}
.sidebar-header {
padding: 25px 20px; text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 { font-size: 1.8rem; font-weight: bold; margin-bottom: 5px; }
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.sidebar-menu { padding: 20px 0; }
.menu-item {
display: block; padding: 15px 25px; color: white;
text-decoration: none; transition: all 0.3s; border-left: 4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background: rgba(255,255,255,0.2); color: white;
border-left-color: #fff; transform: translateX(5px); text-decoration: none;
}
.menu-item i { width: 20px; margin-right: 10px; }
@section('content') .main-content { margin-left: 280px; flex: 1; min-height: 100vh; }
<div class="container-fluid"> .admin-header {
<!-- Page Header --> background: white; padding: 20px 30px;
<div class="d-flex justify-content-between align-items-center mb-4"> box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-bottom: 1px solid #dee2e6;
<div> }
<h1 class="h3 mb-0 text-gray-800">Edit Order</h1> .content-area { padding: 30px; }
.breadcrumb { background: transparent; padding: 0; margin-bottom: 10px; }
.breadcrumb-item a { color: #28a745; text-decoration: none; }
.back-button {
position: fixed; top: 20px; right: 20px; z-index: 1100;
background: linear-gradient(45deg, #dc3545, #c82333);
color: white; border: none; padding: 12px 20px;
border-radius: 25px; font-weight: 600; text-decoration: none; transition: all 0.3s;
}
.back-button:hover { color: white; transform: translateY(-2px); }
.fade-in { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.sidebar { width: 100%; position: relative; }
.main-content { margin-left: 0; }
}
</style>
</head>
<body>
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
{{-- Sidebar --}}
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin User' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item">
<i class="fas fa-tachometer-alt"></i>Dashboard
</a>
<a href="{{ route('admin.products.index') }}" class="menu-item">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item active">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan -->
</a>
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
{{-- Main Content --}}
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent p-0 mb-0"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li> <li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.orders') }}">Orders</a></li> <li class="breadcrumb-item"><a href="{{ route('admin.orders') }}">Pesanan</a></li>
<li class="breadcrumb-item active">Edit {{ $transaksi->invoice_number }}</li> <li class="breadcrumb-item active">Edit {{ $transaksi->invoice_number }}</li>
</ol> </ol>
</nav> </nav>
<div class="d-flex justify-content-between align-items-center">
<div>
<h2>Edit Order</h2>
<p class="mb-0">{{ $transaksi->invoice_number }}</p>
</div> </div>
<a href="{{ route('admin.transaksis.show', $transaksi->id) }}" class="btn btn-secondary"> <a href="{{ route('admin.transaksis.show', $transaksi->id) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back <i class="fas fa-arrow-left me-1"></i> Back
</a> </a>
</div> </div>
</div>
<div class="content-area fade-in">
@if ($errors->any()) @if ($errors->any())
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show">
<strong>Error!</strong> Please fix the following issues: <strong>Error!</strong>
<ul class="mb-0 mt-2"> <ul class="mb-0 mt-2">
@foreach ($errors->all() as $error) @foreach ($errors->all() as $error)
<li>{{ $error }}</li> <li>{{ $error }}</li>
@endforeach @endforeach
</ul> </ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<span aria-hidden="true">&times;</span> </div>
</button> @endif
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
@endif @endif
@ -40,45 +158,50 @@
@method('PUT') @method('PUT')
<div class="row"> <div class="row">
<!-- Order Details -->
{{-- Kolom Kiri --}}
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3"> {{-- Order Details --}}
<h6 class="m-0 font-weight-bold text-primary">Order Details</h6> <div class="card shadow mb-4" style="border-radius:12px;">
<div class="card-header py-3" style="background:#f8f9fa; border-radius:12px 12px 0 0;">
<h6 class="m-0 fw-bold text-primary">Order Details</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="mb-3">
<label for="invoice_number">Invoice Number</label> <label class="form-label fw-semibold">Invoice Number</label>
<input type="text" class="form-control" id="invoice_number" name="invoice_number" <input type="text" class="form-control bg-light"
value="{{ old('invoice_number', $transaksi->invoice_number) }}" readonly> value="{{ $transaksi->invoice_number }}" readonly>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="mb-3">
<label for="order_status">Order Status <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Order Status <span class="text-danger">*</span></label>
<select class="form-control @error('order_status') is-invalid @enderror" <select class="form-select @error('order_status') is-invalid @enderror"
id="order_status" name="order_status" required> name="order_status" required>
<option value="pending" {{ old('order_status', $transaksi->order_status) == 'pending' ? 'selected' : '' }}>Pending</option> @foreach(['pending','processing','shipped','delivered','cancelled'] as $s)
<option value="processing" {{ old('order_status', $transaksi->order_status) == 'processing' ? 'selected' : '' }}>Processing</option> <option value="{{ $s }}"
<option value="shipped" {{ old('order_status', $transaksi->order_status) == 'shipped' ? 'selected' : '' }}>Shipped</option> {{ old('order_status', $transaksi->order_status) == $s ? 'selected' : '' }}>
<option value="delivered" {{ old('order_status', $transaksi->order_status) == 'delivered' ? 'selected' : '' }}>Delivered</option> {{ ucfirst($s) }}
<option value="cancelled" {{ old('order_status', $transaksi->order_status) == 'cancelled' ? 'selected' : '' }}>Cancelled</option> </option>
@endforeach
</select> </select>
@error('order_status') @error('order_status')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="mb-3">
<label for="payment_status">Payment Status <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Payment Status <span class="text-danger">*</span></label>
<select class="form-control @error('payment_status') is-invalid @enderror" <select class="form-select @error('payment_status') is-invalid @enderror"
id="payment_status" name="payment_status" required> name="payment_status" required>
<option value="pending" {{ old('payment_status', $transaksi->payment_status) == 'pending' ? 'selected' : '' }}>Pending</option> @foreach(['pending','paid','failed'] as $p)
<option value="paid" {{ old('payment_status', $transaksi->payment_status) == 'paid' ? 'selected' : '' }}>Paid</option> <option value="{{ $p }}"
<option value="failed" {{ old('payment_status', $transaksi->payment_status) == 'failed' ? 'selected' : '' }}>Failed</option> {{ old('payment_status', $transaksi->payment_status) == $p ? 'selected' : '' }}>
{{ ucfirst($p) }}
</option>
@endforeach
</select> </select>
@error('payment_status') @error('payment_status')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@ -86,23 +209,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3">
<div class="form-group"> <label class="form-label fw-semibold">Tracking Number</label>
<label for="tracking_number">Tracking Number</label>
<input type="text" class="form-control @error('tracking_number') is-invalid @enderror" <input type="text" class="form-control @error('tracking_number') is-invalid @enderror"
id="tracking_number" name="tracking_number" name="tracking_number"
value="{{ old('tracking_number', $transaksi->tracking_number) }}" value="{{ old('tracking_number', $transaksi->tracking_number) }}"
placeholder="Enter tracking number"> placeholder="Enter tracking number">
@error('tracking_number') @error('tracking_number')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="mb-3">
<div class="form-group"> <label class="form-label fw-semibold">Notes</label>
<label for="notes">Notes</label>
<textarea class="form-control @error('notes') is-invalid @enderror" <textarea class="form-control @error('notes') is-invalid @enderror"
id="notes" name="notes" rows="3" name="notes" rows="3"
placeholder="Enter any notes about this order">{{ old('notes', $transaksi->notes) }}</textarea> placeholder="Enter any notes...">{{ old('notes', $transaksi->notes) }}</textarea>
@error('notes') @error('notes')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
@ -110,149 +231,141 @@
</div> </div>
</div> </div>
<!-- Customer Information --> {{-- Customer Information (readonly) --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4" style="border-radius:12px;">
<div class="card-header py-3"> <div class="card-header py-3" style="background:#f8f9fa; border-radius:12px 12px 0 0;">
<h6 class="m-0 font-weight-bold text-primary">Customer Information</h6> <h6 class="m-0 fw-bold text-primary">
Customer Information
<small class="text-muted fw-normal ms-2" style="font-size:0.75rem;">
<i class="fas fa-lock"></i> Tidak dapat diubah
</small>
</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="mb-3">
<label for="customer_name">Customer Name <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Customer Name</label>
<input type="text" class="form-control @error('customer_name') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="customer_name" name="customer_name" value="{{ $transaksi->customer_name }}" readonly>
value="{{ old('customer_name', $transaksi->customer_name) }}" required>
@error('customer_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="mb-3">
<label for="customer_phone">Phone <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Phone</label>
<input type="text" class="form-control @error('customer_phone') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="customer_phone" name="customer_phone" value="{{ $transaksi->customer_phone }}" readonly>
value="{{ old('customer_phone', $transaksi->customer_phone) }}" required>
@error('customer_phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="mb-3">
<label for="customer_email">Email</label> <label class="form-label fw-semibold">Email</label>
<input type="email" class="form-control @error('customer_email') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="customer_email" name="customer_email" value="{{ $transaksi->customer_email ?? '-' }}" readonly>
value="{{ old('customer_email', $transaksi->customer_email) }}">
@error('customer_email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Shipping Information --> {{-- Shipping Information (readonly) --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4" style="border-radius:12px;">
<div class="card-header py-3"> <div class="card-header py-3" style="background:#f8f9fa; border-radius:12px 12px 0 0;">
<h6 class="m-0 font-weight-bold text-primary">Shipping Information</h6> <h6 class="m-0 fw-bold text-primary">
Shipping Information
<small class="text-muted fw-normal ms-2" style="font-size:0.75rem;">
<i class="fas fa-lock"></i> Tidak dapat diubah
</small>
</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="mb-3">
<label for="shipping_address">Shipping Address <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Shipping Address</label>
<textarea class="form-control @error('shipping_address') is-invalid @enderror" <textarea class="form-control bg-light" rows="3"
id="shipping_address" name="shipping_address" rows="3" required>{{ old('shipping_address', $transaksi->shipping_address) }}</textarea> readonly>{{ $transaksi->shipping_address }}</textarea>
@error('shipping_address')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="mb-3">
<label for="city">City <span class="text-danger">*</span></label> <label class="form-label fw-semibold">City</label>
<input type="text" class="form-control @error('city') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="city" name="city" value="{{ $transaksi->city }}" readonly>
value="{{ old('city', $transaksi->city) }}" required>
@error('city')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="mb-3">
<label for="province">Province <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Province</label>
<input type="text" class="form-control @error('province') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="province" name="province" value="{{ $transaksi->province }}" readonly>
value="{{ old('province', $transaksi->province) }}" required>
@error('province')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="mb-3">
<label for="postal_code">Postal Code <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Postal Code</label>
<input type="text" class="form-control @error('postal_code') is-invalid @enderror" <input type="text" class="form-control bg-light"
id="postal_code" name="postal_code" value="{{ $transaksi->postal_code }}" readonly>
value="{{ old('postal_code', $transaksi->postal_code) }}" required>
@error('postal_code')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Order Summary --> </div>
{{-- Akhir col-lg-8 --}}
{{-- Kolom Kanan --}}
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3"> {{-- Order Summary --}}
<h6 class="m-0 font-weight-bold text-primary">Order Summary</h6> <div class="card shadow mb-4" style="border-radius:12px;">
<div class="card-header py-3" style="background:#f8f9fa; border-radius:12px 12px 0 0;">
<h6 class="m-0 fw-bold text-primary">Order Summary</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="text-primary">{{ $transaksi->invoice_number }}</h5> <h5 class="text-primary">{{ $transaksi->invoice_number }}</h5>
<p class="text-muted mb-3">{{ $transaksi->created_at->format('d M Y, H:i') }}</p> <p class="text-muted mb-3">{{ $transaksi->created_at->format('d M Y, H:i') }}</p>
<table class="table table-sm table-borderless"> <table class="table table-sm table-borderless">
<tbody> <tbody>
@foreach($transaksi->details as $detail) @foreach($transaksi->details as $detail)
<tr> <tr>
<td>{{ $detail->product->name ?? 'N/A' }}</td> <td>{{ $detail->product->name ?? 'N/A' }}</td>
<td class="text-right">x{{ $detail->quantity }}</td> <td class="text-end">x{{ $detail->quantity }}</td>
<td class="text-right">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td> <td class="text-end">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
<tfoot class="border-top"> <tfoot class="border-top">
<tr> <tr>
<th colspan="2">Total:</th> <th colspan="2">Total:</th>
<th class="text-right">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</th> <th class="text-end">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
</div> </div>
<!-- Action Buttons --> {{-- Action Buttons --}}
<div class="card shadow"> <div class="card shadow" style="border-radius:12px;">
<div class="card-body"> <div class="card-body">
<button type="submit" class="btn btn-primary btn-block"> <button type="submit" class="btn btn-primary w-100 mb-2">
<i class="fas fa-save"></i> Update Order <i class="fas fa-save me-1"></i> Update Order
</button> </button>
<a href="{{ route('admin.transaksis.show', $transaksi->id) }}" class="btn btn-secondary btn-block"> <a href="{{ route('admin.transaksis.show', $transaksi->id) }}"
<i class="fas fa-times"></i> Cancel class="btn btn-secondary w-100">
<i class="fas fa-times me-1"></i> Cancel
</a> </a>
</div> </div>
</div> </div>
</div> </div>
{{-- Akhir col-lg-4 --}}
</div> </div>
</form> </form>
</div>
</div>
</div> </div>
@endsection
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,259 +1,471 @@
@extends('layouts.app') <!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Details - Bibit Cabai Admin</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f8f9fa; }
.admin-container { display: flex; min-height: 100vh; }
@section('title', 'Order Details') /* ===== SIDEBAR ===== */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #28a745, #20c997);
color: white;
min-height: 100vh;
position: fixed;
top: 0; left: 0;
z-index: 1000;
}
.sidebar-header {
padding: 25px 20px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 { font-size: 1.8rem; font-weight: bold; margin-bottom: 5px; }
.sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
.sidebar-menu { padding: 20px 0; }
.menu-item {
display: block;
padding: 15px 25px;
color: white;
text-decoration: none;
transition: all 0.3s;
border-left: 4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background: rgba(255,255,255,0.2);
color: white;
text-decoration: none;
border-left-color: #fff;
transform: translateX(5px);
}
.menu-item i { width: 20px; margin-right: 10px; }
@section('content') /* ===== MAIN CONTENT ===== */
<div class="container-fluid"> .main-content { margin-left: 280px; flex: 1; min-height: 100vh; }
<!-- Page Header --> .admin-header {
<div class="d-flex justify-content-between align-items-center mb-4"> background: white;
<div> padding: 20px 30px;
<h1 class="h3 mb-0 text-gray-800">Order Details</h1> box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: 1px solid #dee2e6;
}
.content-area { padding: 30px; }
.breadcrumb { background: transparent; padding: 0; margin-bottom: 10px; }
.breadcrumb-item a { color: #28a745; text-decoration: none; }
/* ===== BACK BUTTON ===== */
.back-button {
position: fixed;
top: 20px; right: 20px;
z-index: 1100;
background: linear-gradient(45deg, #dc3545, #c82333);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s;
}
.back-button:hover {
background: linear-gradient(45deg, #c82333, #a71e2a);
color: white;
transform: translateY(-2px);
}
/* ===== FADE IN ===== */
.fade-in { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== CARD ===== */
.card { border: none; border-radius: 12px; }
.card-header {
background: #f8f9fa !important;
border-radius: 12px 12px 0 0 !important;
border-bottom: 1px solid #dee2e6;
}
/* ===== BADGES ===== */
.badge-status {
padding: 5px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-pending { background: #fff3cd; color: #856404; }
.badge-processing { background: #d1ecf1; color: #0c5460; }
.badge-shipped { background: #cce5ff; color: #004085; }
.badge-delivered { background: #d4edda; color: #155724; }
.badge-cancelled { background: #f8d7da; color: #721c24; }
.badge-paid { background: #d4edda; color: #155724; }
.badge-failed { background: #f8d7da; color: #721c24; }
/* ===== INFO TABLE ===== */
.info-table td { padding: 6px 0; border: none; vertical-align: top; }
.info-table td:first-child { font-weight: 600; width: 40%; color: #495057; }
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.sidebar { width: 100%; position: relative; }
.main-content { margin-left: 0; }
}
</style>
</head>
<body>
{{-- Back to Website --}}
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
{{-- ===== SIDEBAR ===== --}}
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin User' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item">
<i class="fas fa-tachometer-alt"></i>Dashboard
</a>
<a href="{{ route('admin.products.index') }}" class="menu-item">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item active">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan
</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
{{-- ===== MAIN CONTENT ===== --}}
<div class="main-content">
{{-- Header --}}
<div class="admin-header">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent p-0 mb-0"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li> <li class="breadcrumb-item">
<li class="breadcrumb-item"><a href="{{ route('admin.orders') }}">Orders</a></li> <a href="{{ route('admin.dashboard') }}"><i class="fas fa-home"></i> Dashboard</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.orders') }}">Pesanan</a>
</li>
<li class="breadcrumb-item active">{{ $transaksi->invoice_number }}</li> <li class="breadcrumb-item active">{{ $transaksi->invoice_number }}</li>
</ol> </ol>
</nav> </nav>
<div class="d-flex justify-content-between align-items-center">
<div>
<h2>Order Details</h2>
<p class="mb-0 text-muted">{{ $transaksi->invoice_number }}</p>
</div> </div>
<div class="btn-group" role="group"> <div class="d-flex gap-2">
<a href="{{ route('admin.orders') }}" class="btn btn-secondary"> <a href="{{ route('admin.orders') }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left"></i> Back to Orders <i class="fas fa-arrow-left me-1"></i> Back to Orders
</a> </a>
<a href="{{ route('admin.transaksis.edit', $transaksi->id) }}" class="btn btn-warning"> <a href="{{ route('admin.transaksis.edit', $transaksi->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit me-1"></i> Edit
</a> </a>
<a href="{{ route('admin.transaksis.print', $transaksi->id) }}" class="btn btn-info" target="_blank"> <a href="{{ route('admin.transaksis.print', $transaksi->id) }}" class="btn btn-info btn-sm" target="_blank">
<i class="fas fa-print"></i> Print Invoice <i class="fas fa-print me-1"></i> Print Invoice
</a> </a>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal"> <button type="button" class="btn btn-danger btn-sm"
<i class="fas fa-trash"></i> Delete data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash me-1"></i> Delete
</button> </button>
</div> </div>
</div> </div>
</div>
{{-- Content --}}
<div class="content-area fade-in">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="row"> <div class="row">
<!-- Order Information -->
{{-- ===== KOLOM KIRI (8/12) ===== --}}
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Invoice & Status Card -->
{{-- Order Information & Status --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center"> <div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Order Information</h6> <h6 class="m-0 fw-bold text-primary">
<div> <i class="fas fa-file-alt me-2"></i>Order Information
@php </h6>
$statusClass = [ <span class="badge-status badge-{{ $transaksi->order_status }}">
'pending' => 'warning',
'processing' => 'info',
'shipped' => 'primary',
'delivered' => 'success',
'cancelled' => 'danger'
][$transaksi->order_status] ?? 'secondary';
@endphp
<span class="badge badge-{{ $statusClass }} badge-lg">
{{ ucfirst($transaksi->order_status) }} {{ ucfirst($transaksi->order_status) }}
</span> </span>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row">
{{-- Invoice Details --}}
<div class="col-md-6"> <div class="col-md-6">
<h6 class="text-primary mb-3">Invoice Details</h6> <h6 class="text-primary mb-3">Invoice Details</h6>
<table class="table table-sm table-borderless"> <table class="info-table table table-sm table-borderless">
<tr> <tr>
<td width="40%"><strong>Invoice Number:</strong></td> <td>Invoice Number:</td>
<td>{{ $transaksi->invoice_number }}</td> <td><strong>{{ $transaksi->invoice_number }}</strong></td>
</tr> </tr>
<tr> <tr>
<td><strong>Order Date:</strong></td> <td>Order Date:</td>
<td>{{ $transaksi->created_at->format('d M Y, H:i') }}</td> <td>{{ $transaksi->created_at->format('d M Y, H:i') }}</td>
</tr> </tr>
<tr> <tr>
<td><strong>Payment Status:</strong></td> <td>Payment Status:</td>
<td> <td>
@php <span class="badge-status badge-{{ $transaksi->payment_status }}">
$paymentClass = [
'pending' => 'warning',
'paid' => 'success',
'failed' => 'danger'
][$transaksi->payment_status] ?? 'secondary';
@endphp
<span class="badge badge-{{ $paymentClass }}">
{{ ucfirst($transaksi->payment_status) }} {{ ucfirst($transaksi->payment_status) }}
</span> </span>
</td> </td>
</tr> </tr>
@if($transaksi->tracking_number)
<tr>
<td>Tracking Number:</td>
<td><code>{{ $transaksi->tracking_number }}</code></td>
</tr>
@endif
</table> </table>
</div> </div>
{{-- Update Status --}}
<div class="col-md-6"> <div class="col-md-6">
<h6 class="text-primary mb-3">Update Status</h6> <h6 class="text-primary mb-3">Update Status</h6>
<form action="{{ route('admin.transaksis.update-status', $transaksi->id) }}" method="POST">
<form action="{{ route('admin.transaksis.update-status', $transaksi->id) }}" method="POST" class="mb-3">
@csrf @csrf
<div class="form-group"> <label class="form-label fw-semibold small">Order Status</label>
<label for="order_status">Order Status</label> <div class="input-group input-group-sm">
<select name="order_status" id="order_status" class="form-control"> <select name="order_status" class="form-select" id="order_status">
<option value="pending" {{ $transaksi->order_status == 'pending' ? 'selected' : '' }}>Pending</option> @foreach(['pending','processing','shipped','delivered','cancelled'] as $s)
<option value="processing" {{ $transaksi->order_status == 'processing' ? 'selected' : '' }}>Processing</option> <option value="{{ $s }}" {{ $transaksi->order_status == $s ? 'selected' : '' }}>
<option value="shipped" {{ $transaksi->order_status == 'shipped' ? 'selected' : '' }}>Shipped</option> {{ ucfirst($s) }}
<option value="delivered" {{ $transaksi->order_status == 'delivered' ? 'selected' : '' }}>Delivered</option> </option>
<option value="cancelled" {{ $transaksi->order_status == 'cancelled' ? 'selected' : '' }}>Cancelled</option> @endforeach
</select> </select>
</div> <button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-primary btn-sm"> <i class="fas fa-save"></i>
<i class="fas fa-save"></i> Update Order Status
</button> </button>
</div>
</form> </form>
<form action="{{ route('admin.transaksis.update-payment-status', $transaksi->id) }}" method="POST" class="mt-3"> <form action="{{ route('admin.transaksis.update-payment-status', $transaksi->id) }}" method="POST">
@csrf @csrf
<div class="form-group"> <label class="form-label fw-semibold small">Payment Status</label>
<label for="payment_status">Payment Status</label> <div class="input-group input-group-sm">
<select name="payment_status" id="payment_status" class="form-control"> <select name="payment_status" class="form-select" id="payment_status">
<option value="pending" {{ $transaksi->payment_status == 'pending' ? 'selected' : '' }}>Pending</option> @foreach(['pending','paid','failed'] as $p)
<option value="paid" {{ $transaksi->payment_status == 'paid' ? 'selected' : '' }}>Paid</option> <option value="{{ $p }}" {{ $transaksi->payment_status == $p ? 'selected' : '' }}>
<option value="failed" {{ $transaksi->payment_status == 'failed' ? 'selected' : '' }}>Failed</option> {{ ucfirst($p) }}
</option>
@endforeach
</select> </select>
</div> <button type="submit" class="btn btn-success">
<button type="submit" class="btn btn-success btn-sm"> <i class="fas fa-save"></i>
<i class="fas fa-save"></i> Update Payment Status
</button> </button>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Order Items --> {{-- Order Items --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Order Items</h6> <h6 class="m-0 fw-bold text-primary">
<i class="fas fa-box me-2"></i>Order Items
</h6>
</div> </div>
<div class="card-body"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered"> <table class="table table-bordered mb-0">
<thead class="thead-light"> <thead class="table-light">
<tr> <tr>
<th>#</th> <th>#</th>
<th>Product</th> <th>Product</th>
<th>Price</th> <th class="text-center">Price</th>
<th>Quantity</th> <th class="text-center">Qty</th>
<th>Subtotal</th> <th class="text-end">Subtotal</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($transaksi->details as $index => $detail) @foreach($transaksi->details as $index => $detail)
<tr> <tr>
<td>{{ $index + 1 }}</td> <td>{{ $index + 1 }}</td>
<td> <td><strong>{{ $detail->product->name ?? 'Product Not Found' }}</strong></td>
<strong>{{ $detail->product->name ?? 'Product Not Found' }}</strong> <td class="text-center">Rp {{ number_format($detail->price, 0, ',', '.') }}</td>
</td> <td class="text-center">{{ $detail->quantity }}</td>
<td>Rp {{ number_format($detail->price, 0, ',', '.') }}</td> <td class="text-end">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
<td>{{ $detail->quantity }}</td>
<td>Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
<tfoot> <tfoot class="table-light">
<tr> <tr>
<th colspan="4" class="text-right">Total Amount:</th> <th colspan="4" class="text-end">Total Amount:</th>
<th>Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</th> <th class="text-end text-success">
Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}
</th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Customer & Shipping Info --> </div>
{{-- Akhir col-lg-8 --}}
{{-- ===== KOLOM KANAN (4/12) ===== --}}
<div class="col-lg-4"> <div class="col-lg-4">
<!-- Customer Information -->
{{-- Customer Information --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Customer Information</h6> <h6 class="m-0 fw-bold text-primary">
<i class="fas fa-user me-2"></i>Customer Information
</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="table table-sm table-borderless"> <table class="info-table table table-sm table-borderless">
<tr> <tr>
<td><i class="fas fa-user text-primary"></i></td> <td><i class="fas fa-user text-success me-1"></i> Name:</td>
<td><strong>Name:</strong></td>
<td>{{ $transaksi->customer_name }}</td> <td>{{ $transaksi->customer_name }}</td>
</tr> </tr>
<tr> <tr>
<td><i class="fas fa-phone text-primary"></i></td> <td><i class="fas fa-phone text-success me-1"></i> Phone:</td>
<td><strong>Phone:</strong></td>
<td>{{ $transaksi->customer_phone }}</td> <td>{{ $transaksi->customer_phone }}</td>
</tr> </tr>
<tr> <tr>
<td><i class="fas fa-envelope text-primary"></i></td> <td><i class="fas fa-envelope text-success me-1"></i> Email:</td>
<td><strong>Email:</strong></td>
<td>{{ $transaksi->customer_email ?? '-' }}</td> <td>{{ $transaksi->customer_email ?? '-' }}</td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
<!-- Shipping Information --> {{-- Shipping Information --}}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Shipping Information</h6> <h6 class="m-0 fw-bold text-primary">
<i class="fas fa-truck me-2"></i>Shipping Information
</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="mb-2"><strong>Shipping Address:</strong></p> <p class="fw-semibold small text-muted mb-1">Shipping Address</p>
<address class="mb-3"> <address class="mb-3" style="line-height:1.7;">
{{ $transaksi->shipping_address }}<br> {{ $transaksi->shipping_address }}<br>
{{ $transaksi->city }}, {{ $transaksi->province }}<br> {{ $transaksi->city }}, {{ $transaksi->province }}<br>
{{ $transaksi->postal_code }} {{ $transaksi->postal_code }}
</address> </address>
@if($transaksi->shipping_method) @if($transaksi->shipping_method)
<p class="mb-1"><strong>Shipping Method:</strong></p> <p class="fw-semibold small text-muted mb-1">Shipping Method</p>
<p>{{ $transaksi->shipping_method }}</p> <p class="mb-3">{{ $transaksi->shipping_method }}</p>
@endif @endif
@if($transaksi->tracking_number) @if($transaksi->tracking_number)
<p class="mb-1"><strong>Tracking Number:</strong></p> <p class="fw-semibold small text-muted mb-1">Tracking Number</p>
<p><code>{{ $transaksi->tracking_number }}</code></p> <p class="mb-0"><code class="bg-light px-2 py-1 rounded">{{ $transaksi->tracking_number }}</code></p>
@endif @endif
</div> </div>
</div> </div>
<!-- Additional Notes --> {{-- Notes --}}
@if($transaksi->notes) @if($transaksi->notes)
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Notes</h6> <h6 class="m-0 fw-bold text-primary">
<i class="fas fa-sticky-note me-2"></i>Notes
</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="mb-0">{{ $transaksi->notes }}</p> <p class="mb-0">{{ $transaksi->notes }}</p>
</div> </div>
</div> </div>
@endif @endif
</div>
</div>
</div>
<!-- Delete Confirmation Modal --> </div>
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true"> {{-- Akhir col-lg-4 --}}
<div class="modal-dialog" role="document">
</div>
</div>
{{-- Akhir content-area --}}
</div>
{{-- Akhir main-content --}}
</div>
{{-- Akhir admin-container --}}
{{-- ===== DELETE MODAL ===== --}}
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-danger text-white"> <div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5> <h5 class="modal-title" id="deleteModalLabel">
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close"> <i class="fas fa-exclamation-triangle me-2"></i>Confirm Delete
<span aria-hidden="true">&times;</span> </h5>
</button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Are you sure you want to delete this order?</p> <p>Are you sure you want to delete this order?</p>
<p><strong>Invoice: {{ $transaksi->invoice_number }}</strong></p> <p><strong>Invoice: {{ $transaksi->invoice_number }}</strong></p>
<p class="text-danger"><i class="fas fa-exclamation-triangle"></i> This action cannot be undone!</p> <p class="text-danger mb-0">
<i class="fas fa-exclamation-triangle me-1"></i> This action cannot be undone!
</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form action="{{ route('admin.transaksis.destroy', $transaksi->id) }}" method="POST" style="display: inline;"> <form action="{{ route('admin.transaksis.destroy', $transaksi->id) }}" method="POST" class="d-inline">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="btn btn-danger"> <button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i> Delete Order <i class="fas fa-trash me-1"></i> Delete Order
</button> </button>
</form> </form>
</div> </div>
@ -261,37 +473,24 @@
</div> </div>
</div> </div>
@push('styles') <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<style>
.badge-lg {
font-size: 1rem;
padding: 0.5rem 1rem;
}
address {
line-height: 1.6;
}
.breadcrumb-item + .breadcrumb-item::before {
content: "";
}
</style>
@endpush
@push('scripts')
<script> <script>
// Auto-submit form when status changes // Auto-confirm saat status berubah
document.getElementById('order_status').addEventListener('change', function() { document.getElementById('order_status').addEventListener('change', function () {
if(confirm('Update order status?')) { if (confirm('Update order status to "' + this.value + '"?')) {
this.form.submit(); this.form.submit();
} else {
this.form.reset();
} }
}); });
document.getElementById('payment_status').addEventListener('change', function() { document.getElementById('payment_status').addEventListener('change', function () {
if(confirm('Update payment status?')) { if (confirm('Update payment status to "' + this.value + '"?')) {
this.form.submit(); this.form.submit();
} else {
this.form.reset();
} }
}); });
</script> </script>
@endpush </body>
@endsection </html>

View File

@ -4,392 +4,388 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<title>Kelola Pengguna - Admin</title> <title>Kelola Pengguna - Bibit Cabai Admin</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style> <style>
* { * { margin: 0; padding: 0; box-sizing: border-box; }
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: #f8f9fa;
}
.admin-container {
display: flex;
min-height: 100vh; min-height: 100vh;
padding: 20px;
} }
.container { /* Sidebar */
max-width: 1400px; .sidebar {
margin: 0 auto; width: 280px;
background: linear-gradient(180deg, #28a745, #20c997);
color: white;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
} }
.header { .sidebar-header {
background: white; padding: 25px 20px;
padding: 25px 30px; text-align: center;
border-radius: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);
margin-bottom: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
} }
.header h1 { .sidebar-header h3 {
color: #333; font-size: 1.8rem;
font-size: 28px; font-weight: bold;
margin-bottom: 5px;
} }
.header-actions { .sidebar-header p { opacity: 0.8; font-size: 0.9rem; }
display: flex;
gap: 10px;
}
.btn { .sidebar-menu { padding: 20px 0; }
padding: 10px 20px;
border: none; .menu-item {
border-radius: 5px; display: block;
cursor: pointer; padding: 15px 25px;
font-size: 14px; color: white;
text-decoration: none; text-decoration: none;
transition: all 0.3s; transition: all 0.3s;
display: inline-block; border-left: 4px solid transparent;
} }
.btn-primary { .menu-item:hover, .menu-item.active {
background: #667eea; background: rgba(255,255,255,0.2);
color: white; color: white;
text-decoration: none;
border-left-color: #fff;
transform: translateX(5px);
} }
.btn-primary:hover { .menu-item i { width: 20px; margin-right: 10px; }
background: #5568d3;
/* Main Content */
.main-content {
margin-left: 280px;
flex: 1;
min-height: 100vh;
} }
.btn-danger { .admin-header {
background: #e74c3c; background: white;
color: white; padding: 20px 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: 1px solid #dee2e6;
} }
.btn-danger:hover { .content-area { padding: 30px; }
background: #c0392b;
.page-header {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
margin-bottom: 30px;
} }
.alert { .breadcrumb { background: transparent; padding: 0; margin-bottom: 10px; }
padding: 15px 20px; .breadcrumb-item a { color: #28a745; text-decoration: none; }
border-radius: 5px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-success { /* Stats Cards */
background: #d4edda; .stats-grid {
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 20px; gap: 20px;
margin-bottom: 25px; margin-bottom: 30px;
} }
.stat-card { .stat-card {
background: white; background: white;
border-radius: 12px;
padding: 20px; padding: 20px;
border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-card h3 {
color: #666;
font-size: 14px;
margin-bottom: 10px;
text-transform: uppercase;
}
.stat-card .number {
color: #667eea;
font-size: 32px;
font-weight: bold;
}
.content {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.search-bar {
margin-bottom: 20px;
display: flex; display: flex;
gap: 10px; justify-content: space-between;
align-items: center;
border-left: 4px solid;
}
.stat-card.primary { border-left-color: #007bff; }
.stat-card.warning { border-left-color: #ffc107; }
.stat-card.info { border-left-color: #17a2b8; }
.stat-card.success { border-left-color: #28a745; }
.stat-label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: 5px;
}
.stat-card.primary .stat-label { color: #007bff; }
.stat-card.warning .stat-label { color: #ffc107; }
.stat-card.info .stat-label { color: #17a2b8; }
.stat-card.success .stat-label { color: #28a745; }
.stat-value { font-size: 1.5rem; font-weight: 700; color: #333; }
.stat-icon { font-size: 2rem; color: #dee2e6; }
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f8f9fa;
}
.section-title { font-size: 1.3rem; font-weight: 600; color: #333; }
/* Table */
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.data-table th, .data-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.data-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
font-size: 0.85rem;
}
.data-table tbody tr:hover { background-color: #f8f9fc; }
/* Badges */
.badge-role {
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-admin { background: #cce5ff; color: #004085; }
.badge-user { background: #d4edda; color: #155724; }
.badge-verified { background: #d4edda; color: #155724; }
.badge-unverified { background: #e2e3e5; color: #383d41; }
/* Action Buttons */
.btn-action {
padding: 6px 10px;
border: none;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center; align-items: center;
} }
.search-bar input { .btn-action:hover {
flex: 1; transform: translateY(-1px);
padding: 12px 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
border: 2px solid #e0e0e0; text-decoration: none;
border-radius: 5px; color: inherit;
font-size: 14px;
transition: border-color 0.3s;
} }
.search-bar input:focus { .btn-view { background: #17a2b8; color: white; }
outline: none; .btn-edit { background: #ffc107; color: #212529; }
border-color: #667eea; .btn-delete { background: #dc3545; color: white; }
}
.search-bar button { /* Back Button */
background: #667eea; .back-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 1100;
background: linear-gradient(45deg, #dc3545, #c82333);
color: white; color: white;
padding: 12px 25px;
border: none; border: none;
border-radius: 5px; padding: 12px 20px;
cursor: pointer; border-radius: 25px;
font-size: 14px;
transition: background 0.3s;
}
.search-bar button:hover {
background: #5568d3;
}
.table-responsive {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
thead {
background: #f8f9fa;
}
th {
padding: 15px;
text-align: left;
font-weight: 600; font-weight: 600;
color: #333;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
color: #666;
}
tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-admin {
background: #667eea;
color: white;
}
.badge-user {
background: #27ae60;
color: white;
}
.badge-verified {
background: #2ecc71;
color: white;
}
.badge-unverified {
background: #95a5a6;
color: white;
}
.action-btn {
padding: 6px 12px;
margin: 0 3px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s; transition: all 0.3s;
text-decoration: none; text-decoration: none;
display: inline-block;
} }
.btn-edit { .back-button:hover {
background: #3498db; background: linear-gradient(45deg, #c82333, #a71e2a);
transform: translateY(-2px);
color: white; color: white;
}
.btn-edit:hover {
background: #2980b9;
}
.btn-delete {
background: #e74c3c;
color: white;
}
.btn-delete:hover {
background: #c0392b;
}
.btn-view {
background: #95a5a6;
color: white;
}
.btn-view:hover {
background: #7f8c8d;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.pagination a, .pagination span {
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
color: #667eea;
text-decoration: none; text-decoration: none;
transition: all 0.3s;
} }
.pagination a:hover { .fade-in {
background: #667eea; animation: fadeIn 0.3s ease-in-out;
color: white;
} }
.pagination .active { @keyframes fadeIn {
background: #667eea; from { opacity: 0; transform: translateY(10px); }
color: white; to { opacity: 1; transform: translateY(0); }
border-color: #667eea;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state i {
font-size: 48px;
margin-bottom: 10px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.header { .sidebar { width: 100%; position: relative; }
flex-direction: column; .main-content { margin-left: 0; }
gap: 15px; .stats-grid { grid-template-columns: repeat(2, 1fr); }
text-align: center;
}
.header-actions {
flex-direction: column;
width: 100%;
}
.btn {
width: 100%;
}
table {
font-size: 12px;
}
th, td {
padding: 10px 5px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <!-- Back Button -->
<!-- Header --> <a href="{{ route('home') }}" class="back-button">
<div class="header"> <i class="fas fa-arrow-left me-2"></i>Kembali ke Website
<div> </a>
<h1>👥 Kelola Pengguna</h1>
<p style="color: #666; margin-top: 5px;">Dashboard Admin - TA Bibit Cabai</p> <div class="admin-container">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin User' }}</small>
</div> </div>
<div class="header-actions"> <div class="sidebar-menu">
<a href="{{ route('admin.users.create') }}" class="btn btn-primary">+ Tambah Pengguna</a> <a href="{{ route('admin.dashboard') }}" class="menu-item">
<a href="{{ route('admin.dashboard') }}" class="btn btn-primary"> Dashboard</a> <i class="fas fa-tachometer-alt"></i>Dashboard
<form method="POST" action="{{ route('logout') }}" style="display: inline;"> </a>
<a href="{{ route('admin.products.index') }}" class="menu-item">
<i class="fas fa-seedling"></i>Kelola Produk
</a>
<a href="{{ route('admin.orders') }}" class="menu-item">
<i class="fas fa-shopping-cart"></i>Pesanan
</a>
{{-- TAMBAHKAN DI SINI --}}
<a href="{{ route('admin.cancellations') }}" class="menu-item">
<i class="fas fa-times-circle"></i>Pengajuan Batal
</a>
<a href="{{ route('admin.users') }}" class="menu-item active">
<i class="fas fa-users"></i>Pengguna
</a>
<a href="{{ route('admin.laporan') }}" class="menu-item">
<i class="fas fa-chart-line"></i>Laporan
</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item">
<i class="fas fa-cog"></i>Pengaturan
</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf @csrf
<button type="submit" class="btn btn-danger">Logout</button> <button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form> </form>
</div> </div>
</div> </div>
</nav>
<!-- Main Content -->
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('admin.dashboard') }}">
<i class="fas fa-home"></i> Dashboard
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Pengguna</li>
</ol>
</nav>
<h2>Kelola Pengguna</h2>
<p class="mb-0">Kelola semua pengguna terdaftar di sini!</p>
</div>
<div class="content-area fade-in">
<!-- Alert Messages -->
@if(session('success')) @if(session('success'))
<div class="alert alert-success"> <div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }} <i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
@endif @endif
@if(session('error')) @if(session('error'))
<div class="alert alert-error"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session('error') }} <i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
@endif @endif
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="stats"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card primary">
<h3>Total Pengguna</h3> <div>
<div class="number">{{ $totalUsers ?? 0 }}</div> <div class="stat-label">Total Pengguna</div>
<div class="stat-value">{{ $totalUsers ?? 0 }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-icon"><i class="fas fa-users"></i></div>
<h3>Admin</h3>
<div class="number">{{ $totalAdmins ?? 0 }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-card warning">
<h3>Email Terverifikasi</h3> <div>
<div class="number">{{ $verifiedUsers ?? 0 }}</div> <div class="stat-label">Admin</div>
<div class="stat-value">{{ $totalAdmins ?? 0 }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-icon"><i class="fas fa-user-shield"></i></div>
<h3>Pengguna Aktif (30 Hari)</h3>
<div class="number">{{ $activeUsers ?? 0 }}</div>
</div> </div>
<div class="stat-card success">
<div>
<div class="stat-label">Email Terverifikasi</div>
<div class="stat-value">{{ $verifiedUsers ?? 0 }}</div>
</div>
<div class="stat-icon"><i class="fas fa-check-circle"></i></div>
</div>
<div class="stat-card info">
<div>
<div class="stat-label">Aktif (30 Hari)</div>
<div class="stat-value">{{ $activeUsers ?? 0 }}</div>
</div>
<div class="stat-icon"><i class="fas fa-user-clock"></i></div>
</div> </div>
<!-- Main Content -->
<div class="content">
<!-- Search Bar -->
<div class="search-bar">
<input type="text" id="searchInput" placeholder="🔍 Cari pengguna berdasarkan nama, email, atau nomor telepon...">
<button onclick="searchUsers()">Cari</button>
</div> </div>
<!-- Users Table --> <!-- Users Table -->
<div class="page-header">
<div class="section-header">
<h3 class="section-title">
<i class="fas fa-users text-success me-2"></i>
Daftar Pengguna ({{ $totalUsers ?? 0 }})
</h3>
<a href="{{ route('admin.users.create') }}" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-1"></i>Tambah Pengguna
</a>
</div>
<!-- Search -->
<div class="mb-3">
<div class="input-group" style="max-width: 400px;">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="searchInput"
placeholder="Cari nama, email, atau telepon...">
</div>
</div>
@if(isset($users) && $users->count() > 0) @if(isset($users) && $users->count() > 0)
<div class="table-responsive"> <div class="table-responsive">
<table id="usersTable"> <table class="data-table" id="usersTable">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
@ -397,7 +393,7 @@
<th>Email</th> <th>Email</th>
<th>Telepon</th> <th>Telepon</th>
<th>Alamat</th> <th>Alamat</th>
<th>Status</th> <th>Role</th>
<th>Verifikasi</th> <th>Verifikasi</th>
<th>Aksi</th> <th>Aksi</th>
</tr> </tr>
@ -405,33 +401,47 @@
<tbody> <tbody>
@foreach($users as $user) @foreach($users as $user)
<tr> <tr>
<td>{{ $user->id }}</td> <td>{{ ($users->currentPage() - 1) * $users->perPage() + $loop->iteration }}</td>
<td><strong>{{ $user->name }}</strong></td> <td><strong>{{ $user->name }}</strong></td>
<td>{{ $user->email }}</td> <td>{{ $user->email }}</td>
<td>{{ $user->phone ?? '-' }}</td> <td>{{ $user->phone ?? '-' }}</td>
<td>{{ Str::limit($user->address ?? '-', 30) }}</td> <td><small>{{ \Illuminate\Support\Str::limit($user->address ?? '-', 30) }}</small></td>
<td> <td>
@if($user->is_admin == 1) @if($user->is_admin == 1)
<span class="badge badge-admin">Admin</span> <span class="badge-role badge-admin">Admin</span>
@else @else
<span class="badge badge-user">User</span> <span class="badge-role badge-user">User</span>
@endif @endif
</td> </td>
<td> <td>
@if($user->email_verified_at) @if($user->email_verified_at)
<span class="badge badge-verified"> Terverifikasi</span> <span class="badge-role badge-verified">
<i class="fas fa-check me-1"></i>Terverifikasi
</span>
@else @else
<span class="badge badge-unverified">Belum Verifikasi</span> <span class="badge-role badge-unverified">Belum Verifikasi</span>
@endif @endif
</td> </td>
<td> <td>
<a href="{{ route('admin.users.show', $user) }}" class="action-btn btn-view">Detail</a> <div class="d-flex gap-1">
<a href="{{ route('admin.users.edit', $user) }}" class="action-btn btn-edit">Edit</a> <a href="{{ route('admin.users.show', $user) }}"
<form method="POST" action="{{ route('admin.users.destroy', $user) }}" style="display: inline;" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pengguna ini?');"> 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">
<i class="fas fa-edit"></i>
</a>
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
style="display: inline;"
onsubmit="return confirm('Yakin ingin menghapus pengguna ini?')">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="action-btn btn-delete">Hapus</button> <button type="submit" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
@endforeach @endforeach
@ -440,52 +450,39 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
@if(isset($users)) <div class="d-flex justify-content-between align-items-center mt-4">
<div class="pagination"> <small class="text-muted">
{{ $users->links() }} Menampilkan {{ $users->firstItem() }} sampai {{ $users->lastItem() }}
dari {{ $users->total() }} pengguna
</small>
<div>{{ $users->links() }}</div>
</div> </div>
@endif
@else @else
<div class="empty-state"> <div class="text-center py-5">
<div>📭</div> <i class="fas fa-users fa-3x text-muted mb-3 d-block"></i>
<h3>Tidak ada data pengguna</h3> <h5 class="text-muted">Belum ada pengguna terdaftar</h5>
<p>Belum ada pengguna terdaftar dalam sistem</p> <p class="text-muted">Mulai dengan menambahkan pengguna baru.</p>
</div> </div>
@endif @endif
</div> </div>
</div>
</div>
</div> </div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Search function // Live search
function searchUsers() { document.getElementById('searchInput').addEventListener('keyup', function() {
const input = document.getElementById('searchInput'); const filter = this.value.toLowerCase();
const filter = input.value.toLowerCase(); const rows = document.querySelectorAll('#usersTable tbody tr');
const table = document.getElementById('usersTable'); rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (!table) return; row.style.display = text.includes(filter) ? '' : 'none';
});
const tr = table.getElementsByTagName('tr'); });
for (let i = 1; i < tr.length; i++) {
let td = tr[i].getElementsByTagName('td');
let found = false;
for (let j = 0; j < td.length; j++) {
if (td[j]) {
let txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toLowerCase().indexOf(filter) > -1) {
found = true;
break;
}
}
}
tr[i].style.display = found ? '' : 'none';
}
}
// Search on input
document.getElementById('searchInput')?.addEventListener('keyup', searchUsers);
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tambah Pengguna - Bibit Cabai Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',sans-serif; background:#f8f9fa; }
.admin-container { display:flex; min-height:100vh; }
.sidebar {
width:280px; background:linear-gradient(180deg,#28a745,#20c997);
color:white; min-height:100vh; position:fixed; top:0; left:0; z-index:1000;
}
.sidebar-header { padding:25px 20px; text-align:center; border-bottom:1px solid rgba(255,255,255,0.1); }
.sidebar-header h3 { font-size:1.8rem; font-weight:bold; margin-bottom:5px; }
.sidebar-menu { padding:20px 0; }
.menu-item {
display:block; padding:15px 25px; color:white; text-decoration:none;
transition:all 0.3s; border-left:4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background:rgba(255,255,255,0.2); color:white;
border-left-color:#fff; transform:translateX(5px);
}
.menu-item i { width:20px; margin-right:10px; }
.main-content { margin-left:280px; flex:1; }
.admin-header { background:white; padding:20px 30px; box-shadow:0 2px 4px rgba(0,0,0,0.1); }
.content-area { padding:30px; }
.form-card { background:white; border-radius:12px; padding:35px; box-shadow:0 4px 6px rgba(0,0,0,0.05); max-width:700px; }
.form-label { font-weight:600; color:#495057; }
.form-control:focus { border-color:#28a745; box-shadow:0 0 0 0.2rem rgba(40,167,69,0.25); }
.btn-save { background:linear-gradient(45deg,#28a745,#20c997); border:none; color:white; padding:12px 30px; border-radius:8px; font-weight:600; }
.btn-save:hover { opacity:0.9; color:white; }
.breadcrumb { background:transparent; padding:0; margin-bottom:10px; }
.breadcrumb-item a { color:#28a745; text-decoration:none; }
.back-button {
position:fixed; top:20px; right:20px; z-index:1100;
background:linear-gradient(45deg,#dc3545,#c82333);
color:white; border:none; padding:12px 20px;
border-radius:25px; font-weight:600; text-decoration:none; transition:all 0.3s;
}
.back-button:hover { transform:translateY(-2px); color:white; }
.section-divider { border-top:2px solid #f8f9fa; margin:25px 0; }
</style>
</head>
<body>
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item"><i class="fas fa-tachometer-alt"></i>Dashboard</a>
<a href="{{ route('admin.products.index') }}" class="menu-item"><i class="fas fa-seedling"></i>Kelola Produk</a>
<a href="{{ route('admin.orders') }}" class="menu-item"><i class="fas fa-shopping-cart"></i>Pesanan</a>
<a href="{{ route('admin.users') }}" class="menu-item active"><i class="fas fa-users"></i>Pengguna</a>
<a href="{{ route('admin.reports') }}" class="menu-item"><i class="fas fa-chart-line"></i>Laporan</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item"><i class="fas fa-cog"></i>Pengaturan</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.users') }}">Pengguna</a></li>
<li class="breadcrumb-item active">Tambah Pengguna</li>
</ol>
</nav>
<h2>Tambah Pengguna Baru</h2>
<p class="mb-0">Isi form berikut untuk menambah pengguna baru.</p>
</div>
<div class="content-area">
<div class="form-card">
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.users.store') }}">
@csrf
{{-- Informasi Dasar --}}
<h5 class="mb-3 text-success"><i class="fas fa-user me-2"></i>Informasi Dasar</h5>
<div class="mb-3">
<label class="form-label">Nama Lengkap <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}" placeholder="Masukkan nama lengkap" required>
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}" placeholder="contoh@email.com" required>
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Password <span class="text-danger">*</span></label>
<div class="input-group">
<input type="password" name="password" id="password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Minimal 8 karakter" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('password', 'eye1')">
<i class="fas fa-eye" id="eye1"></i>
</button>
</div>
@error('password')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Konfirmasi Password <span class="text-danger">*</span></label>
<div class="input-group">
<input type="password" name="password_confirmation" id="password_confirmation"
class="form-control"
placeholder="Ulangi password" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('password_confirmation', 'eye2')">
<i class="fas fa-eye" id="eye2"></i>
</button>
</div>
</div>
</div>
<div class="section-divider"></div>
<h5 class="mb-3 text-success"><i class="fas fa-address-card me-2"></i>Informasi Tambahan</h5>
<div class="mb-3">
<label class="form-label">Nomor Telepon</label>
<input type="text" name="phone"
value="{{ old('phone', '+62') }}"
placeholder="+62812xxxxxxxx">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label">Alamat</label>
<textarea name="address" class="form-control @error('address') is-invalid @enderror"
rows="4" style="min-height: 100px; resize: vertical;"
placeholder="Contoh: Jl. Mawar No. 10, RT 02/RW 03, Kelurahan Sumbersari, Jember">{{ old('address') }}</textarea>
@error('address')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="section-divider"></div>
<h5 class="mb-3 text-success"><i class="fas fa-shield-alt me-2"></i>Hak Akses</h5>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_admin" id="is_admin"
{{ old('is_admin') ? 'checked' : '' }}>
<label class="form-check-label" for="is_admin">
<strong>Jadikan Admin</strong>
<small class="text-muted d-block">Admin memiliki akses penuh ke dashboard.</small>
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-save">
<i class="fas fa-save me-2"></i>Simpan Pengguna
</button>
<a href="{{ route('admin.users') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function togglePassword(fieldId, iconId) {
const field = document.getElementById(fieldId);
const icon = document.getElementById(iconId);
if (field.type === 'password') {
field.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
field.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Pengguna - Bibit Cabai Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',sans-serif; background:#f8f9fa; }
.admin-container { display:flex; min-height:100vh; }
.sidebar {
width:280px; background:linear-gradient(180deg,#28a745,#20c997);
color:white; min-height:100vh; position:fixed; top:0; left:0; z-index:1000;
}
.sidebar-header { padding:25px 20px; text-align:center; border-bottom:1px solid rgba(255,255,255,0.1); }
.sidebar-header h3 { font-size:1.8rem; font-weight:bold; margin-bottom:5px; }
.sidebar-menu { padding:20px 0; }
.menu-item {
display:block; padding:15px 25px; color:white; text-decoration:none;
transition:all 0.3s; border-left:4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background:rgba(255,255,255,0.2); color:white;
border-left-color:#fff; transform:translateX(5px);
}
.menu-item i { width:20px; margin-right:10px; }
.main-content { margin-left:280px; flex:1; }
.admin-header { background:white; padding:20px 30px; box-shadow:0 2px 4px rgba(0,0,0,0.1); }
.content-area { padding:30px; }
.form-card { background:white; border-radius:12px; padding:35px; box-shadow:0 4px 6px rgba(0,0,0,0.05); max-width:700px; }
.form-label { font-weight:600; color:#495057; }
.form-control:focus { border-color:#28a745; box-shadow:0 0 0 0.2rem rgba(40,167,69,0.25); }
.btn-save { background:linear-gradient(45deg,#28a745,#20c997); border:none; color:white; padding:12px 30px; border-radius:8px; font-weight:600; }
.btn-save:hover { opacity:0.9; color:white; }
.breadcrumb { background:transparent; padding:0; margin-bottom:10px; }
.breadcrumb-item a { color:#28a745; text-decoration:none; }
.back-button {
position:fixed; top:20px; right:20px; z-index:1100;
background:linear-gradient(45deg,#dc3545,#c82333);
color:white; border:none; padding:12px 20px;
border-radius:25px; font-weight:600; text-decoration:none; transition:all 0.3s;
}
.back-button:hover { transform:translateY(-2px); color:white; }
.section-divider { border-top:2px solid #f8f9fa; margin:25px 0; }
.user-info-badge {
background:#f8f9fa; border-radius:8px; padding:12px 15px;
margin-bottom:20px; font-size:0.85rem; color:#6c757d;
}
</style>
</head>
<body>
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item"><i class="fas fa-tachometer-alt"></i>Dashboard</a>
<a href="{{ route('admin.products.index') }}" class="menu-item"><i class="fas fa-seedling"></i>Kelola Produk</a>
<a href="{{ route('admin.orders') }}" class="menu-item"><i class="fas fa-shopping-cart"></i>Pesanan</a>
<a href="{{ route('admin.users') }}" class="menu-item active"><i class="fas fa-users"></i>Pengguna</a>
<a href="{{ route('admin.reports') }}" class="menu-item"><i class="fas fa-chart-line"></i>Laporan</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item"><i class="fas fa-cog"></i>Pengaturan</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.users') }}">Pengguna</a></li>
<li class="breadcrumb-item active">Edit Pengguna</li>
</ol>
</nav>
<h2>Edit Pengguna</h2>
<p class="mb-0">Perbarui informasi pengguna <strong>{{ $user->name }}</strong>.</p>
</div>
<div class="content-area">
<div class="form-card">
<div class="user-info-badge">
<i class="fas fa-info-circle me-2 text-info"></i>
ID Pengguna: <strong>#{{ $user->id }}</strong> &nbsp;|&nbsp;
Terdaftar: <strong>{{ $user->created_at->format('d M Y') }}</strong>
@if($user->email_verified_at)
&nbsp;|&nbsp; <span class="text-success"><i class="fas fa-check-circle"></i> Email Terverifikasi</span>
@else
&nbsp;|&nbsp; <span class="text-muted"><i class="fas fa-clock"></i> Belum Verifikasi</span>
@endif
</div>
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.users.update', $user) }}">
@csrf
@method('PUT')
<h5 class="mb-3 text-success"><i class="fas fa-user me-2"></i>Informasi Dasar</h5>
<div class="mb-3">
<label class="form-label">Nama Lengkap <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $user->name) }}" required>
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
value="{{ old('email', $user->email) }}" required>
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Password Baru</label>
<div class="input-group">
<input type="password" name="password" id="password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Minimal 8 karakter">
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('password', 'eye1')">
<i class="fas fa-eye" id="eye1"></i>
</button>
</div>
@error('password')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Konfirmasi Password Baru</label>
<div class="input-group">
<input type="password" name="password_confirmation" id="password_confirmation"
class="form-control"
placeholder="Ulangi password baru">
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('password_confirmation', 'eye2')">
<i class="fas fa-eye" id="eye2"></i>
</button>
</div>
</div>
</div>
<div class="section-divider"></div>
<h5 class="mb-3 text-success"><i class="fas fa-address-card me-2"></i>Informasi Tambahan</h5>
<div class="mb-3">
<label class="form-label">Nomor Telepon</label>
<input type="text" name="phone"
value="{{ old('phone', '+62') }}"
placeholder="+62812xxxxxxxx">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label">Alamat</label>
<textarea name="address" class="form-control @error('address') is-invalid @enderror"
rows="4" style="min-height: 100px; resize: vertical;">{{ old('address', $user->address) }}</textarea>
@error('address')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="section-divider"></div>
<h5 class="mb-3 text-success"><i class="fas fa-shield-alt me-2"></i>Hak Akses</h5>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_admin" id="is_admin"
{{ old('is_admin', $user->is_admin) ? 'checked' : '' }}
{{ $user->id === auth()->id() ? 'disabled' : '' }}>
<label class="form-check-label" for="is_admin">
<strong>Jadikan Admin</strong>
@if($user->id === auth()->id())
<small class="text-danger d-block">Anda tidak dapat mengubah role akun sendiri.</small>
@else
<small class="text-muted d-block">Admin memiliki akses penuh ke dashboard.</small>
@endif
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-save">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
<a href="{{ route('admin.users') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Batal
</a>
<a href="{{ route('admin.users.show', $user) }}" class="btn btn-outline-info ms-auto">
<i class="fas fa-eye me-2"></i>Lihat Detail
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function togglePassword(fieldId, iconId) {
const field = document.getElementById(fieldId);
const icon = document.getElementById(iconId);
if (field.type === 'password') {
field.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
field.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detail Pengguna - Bibit Cabai Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',sans-serif; background:#f8f9fa; }
.admin-container { display:flex; min-height:100vh; }
.sidebar {
width:280px; background:linear-gradient(180deg,#28a745,#20c997);
color:white; min-height:100vh; position:fixed; top:0; left:0; z-index:1000;
}
.sidebar-header { padding:25px 20px; text-align:center; border-bottom:1px solid rgba(255,255,255,0.1); }
.sidebar-header h3 { font-size:1.8rem; font-weight:bold; margin-bottom:5px; }
.sidebar-menu { padding:20px 0; }
.menu-item {
display:block; padding:15px 25px; color:white; text-decoration:none;
transition:all 0.3s; border-left:4px solid transparent;
}
.menu-item:hover, .menu-item.active {
background:rgba(255,255,255,0.2); color:white;
border-left-color:#fff; transform:translateX(5px);
}
.menu-item i { width:20px; margin-right:10px; }
.main-content { margin-left:280px; flex:1; }
.admin-header { background:white; padding:20px 30px; box-shadow:0 2px 4px rgba(0,0,0,0.1); }
.content-area { padding:30px; }
.detail-card { background:white; border-radius:12px; padding:35px; box-shadow:0 4px 6px rgba(0,0,0,0.05); max-width:700px; }
.avatar-circle {
width:80px; height:80px; border-radius:50%;
background:linear-gradient(135deg,#28a745,#20c997);
display:flex; align-items:center; justify-content:center;
font-size:2rem; color:white; font-weight:bold; margin-bottom:15px;
}
.info-row { display:flex; padding:14px 0; border-bottom:1px solid #f8f9fa; }
.info-row:last-child { border-bottom:none; }
.info-label { width:160px; font-weight:600; color:#6c757d; font-size:0.9rem; flex-shrink:0; }
.info-value { color:#333; flex:1; }
.badge-role { padding:5px 12px; border-radius:12px; font-size:0.8rem; font-weight:600; }
.badge-admin { background:#cce5ff; color:#004085; }
.badge-user { background:#d4edda; color:#155724; }
.badge-verified { background:#d4edda; color:#155724; }
.badge-unverified { background:#e2e3e5; color:#383d41; }
.breadcrumb { background:transparent; padding:0; margin-bottom:10px; }
.breadcrumb-item a { color:#28a745; text-decoration:none; }
.back-button {
position:fixed; top:20px; right:20px; z-index:1100;
background:linear-gradient(45deg,#dc3545,#c82333);
color:white; border:none; padding:12px 20px;
border-radius:25px; font-weight:600; text-decoration:none; transition:all 0.3s;
}
.back-button:hover { transform:translateY(-2px); color:white; }
</style>
</head>
<body>
<a href="{{ route('home') }}" class="back-button">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Website
</a>
<div class="admin-container">
<nav class="sidebar">
<div class="sidebar-header">
<h3>🌱 Bibit Cabai</h3>
<p>Admin Dashboard</p>
<small class="text-light">{{ Auth::user()->name ?? 'Admin' }}</small>
</div>
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}" class="menu-item"><i class="fas fa-tachometer-alt"></i>Dashboard</a>
<a href="{{ route('admin.products.index') }}" class="menu-item"><i class="fas fa-seedling"></i>Kelola Produk</a>
<a href="{{ route('admin.orders') }}" class="menu-item"><i class="fas fa-shopping-cart"></i>Pesanan</a>
<a href="{{ route('admin.users') }}" class="menu-item active"><i class="fas fa-users"></i>Pengguna</a>
<a href="{{ route('admin.reports') }}" class="menu-item"><i class="fas fa-chart-line"></i>Laporan</a>
<!-- <a href="{{ route('admin.settings') }}" class="menu-item"><i class="fas fa-cog"></i>Pengaturan</a> -->
<div class="mt-4 px-3">
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit" class="btn btn-outline-light btn-sm w-100">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</button>
</form>
</div>
</div>
</nav>
<div class="main-content">
<div class="admin-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.users') }}">Pengguna</a></li>
<li class="breadcrumb-item active">Detail Pengguna</li>
</ol>
</nav>
<h2>Detail Pengguna</h2>
<p class="mb-0">Informasi lengkap pengguna.</p>
</div>
<div class="content-area">
<div class="detail-card">
{{-- Avatar & Nama --}}
<div class="text-center mb-4">
<div class="avatar-circle mx-auto">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<h4 class="mb-1">{{ $user->name }}</h4>
@if($user->is_admin)
<span class="badge-role badge-admin"><i class="fas fa-shield-alt me-1"></i>Admin</span>
@else
<span class="badge-role badge-user"><i class="fas fa-user me-1"></i>User</span>
@endif
</div>
{{-- Info Detail --}}
<div class="info-row">
<div class="info-label"><i class="fas fa-hashtag me-2 text-muted"></i>ID</div>
<div class="info-value">#{{ $user->id }}</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-envelope me-2 text-muted"></i>Email</div>
<div class="info-value">{{ $user->email }}</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-check-circle me-2 text-muted"></i>Verifikasi</div>
<div class="info-value">
@if($user->email_verified_at)
<span class="badge-role badge-verified">
<i class="fas fa-check me-1"></i>Terverifikasi
</span>
<small class="text-muted ms-2">{{ $user->email_verified_at->format('d M Y') }}</small>
@else
<span class="badge-role badge-unverified">Belum Verifikasi</span>
@endif
</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-phone me-2 text-muted"></i>Telepon</div>
<div class="info-value">{{ $user->phone ?? '-' }}</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-map-marker-alt me-2 text-muted"></i>Alamat</div>
<div class="info-value">{{ $user->address ?? '-' }}</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-calendar me-2 text-muted"></i>Bergabung</div>
<div class="info-value">{{ $user->created_at->format('d M Y, H:i') }}</div>
</div>
<div class="info-row">
<div class="info-label"><i class="fas fa-sync me-2 text-muted"></i>Diperbarui</div>
<div class="info-value">{{ $user->updated_at->format('d M Y, H:i') }}</div>
</div>
{{-- Action Buttons --}}
<div class="d-flex gap-2 mt-4">
<a href="{{ route('admin.users.edit', $user) }}" class="btn btn-warning">
<i class="fas fa-edit me-2"></i>Edit Pengguna
</a>
<a href="{{ route('admin.users') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
@if($user->id !== auth()->id())
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
class="ms-auto" onsubmit="return confirm('Yakin ingin menghapus pengguna ini?')">
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Hapus
</button>
</form>
@endif
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verifikasi Kode Admin - TA Bibit Cabai</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.btn-primary-custom {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border: none;
border-radius: 25px;
color: white;
font-weight: 600;
}
.btn-primary-custom:hover {
background: linear-gradient(135deg, #0d7377 0%, #2dd654 100%);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(17, 153, 142, 0.3);
color: white;
}
.btn-primary-custom:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
}
.form-control:focus {
border-color: #11998e;
box-shadow: 0 0 0 0.2rem rgba(17, 153, 142, 0.25);
}
.step-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28px;
}
.step-circle {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px;
}
.step-circle.active { background: #11998e; color: #fff; }
.step-circle.done { background: #11998e; color: #fff; }
.step-circle.idle { background: #dee2e6; color: #888; }
.step-label { font-size: 11px; white-space: nowrap; margin-top: 4px; }
.step-label.active { color: #11998e; font-weight: 700; }
.step-label.done { color: #11998e; }
.step-label.idle { color: #aaa; }
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-line { height: 2px; width: 36px; margin: 0 6px 18px; }
.step-line.done { background: #11998e; }
.step-line.idle { background: #dee2e6; }
/* OTP Boxes */
.otp-box {
width: 46px !important;
height: 54px !important;
padding: 0 !important;
border-radius: 10px !important;
border: 2px solid #dee2e6 !important;
font-size: 22px !important;
font-weight: 700 !important;
text-align: center;
transition: border-color 0.2s, box-shadow 0.2s;
}
.otp-box:focus {
border-color: #11998e !important;
box-shadow: 0 0 0 3px rgba(17,153,142,0.2) !important;
outline: none !important;
}
.otp-box.filled {
border-color: #11998e !important;
background-color: #f0fff4 !important;
}
.email-badge {
background: #f0fff4;
border: 1px solid #11998e33;
border-left: 3px solid #11998e;
border-radius: 8px;
padding: 10px 14px;
}
.icon-box {
width: 64px; height: 64px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 28px;
margin: 0 auto 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-4 col-lg-4">
<div class="card login-card">
<div class="card-body p-5">
<div class="icon-box">🛡️</div>
<div class="text-center mb-3">
<h4 class="fw-bold text-dark mb-1">Verifikasi Kode OTP</h4>
<p class="text-muted small">Masukkan kode 6 digit yang dikirim ke email</p>
</div>
{{-- Step Indicator --}}
<div class="step-wrapper">
<div class="step-item">
<div class="step-circle done"></div>
<div class="step-label done">Email</div>
</div>
<div class="step-line done"></div>
<div class="step-item">
<div class="step-circle active">2</div>
<div class="step-label active">Kode OTP</div>
</div>
<div class="step-line idle"></div>
<div class="step-item">
<div class="step-circle idle">3</div>
<div class="step-label idle">Password Baru</div>
</div>
</div>
@if (session('success'))
<div class="alert alert-success alert-dismissible fade show py-2" role="alert">
<small> {{ session('success') }}</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
@if (session('error'))
<div class="alert alert-danger alert-dismissible fade show py-2" role="alert">
<small>⚠️ {{ session('error') }}</small>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Email Target Badge --}}
<div class="email-badge mb-4 d-flex align-items-center gap-2">
<span style="font-size:20px;">📧</span>
<div>
<div class="fw-semibold text-dark small">Kode dikirim ke:</div>
<div class="text-muted" style="font-size:13px;">{{ session('admin_reset_email', 'email admin') }}</div>
</div>
</div>
<form method="POST" action="{{ route('admin.password.verify-otp') }}" id="otpForm" novalidate>
@csrf
<div class="mb-3">
<label class="form-label fw-semibold text-center d-block">
Kode Verifikasi (6 digit)
</label>
{{-- OTP Boxes --}}
<div class="d-flex justify-content-center gap-2 mb-2" id="otpBoxes">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
</div>
<input type="hidden" name="otp" id="otpHidden">
@error('otp')
<div class="text-danger text-center mt-1 small">{{ $message }}</div>
@enderror
</div>
{{-- Countdown Timer --}}
<div class="text-center mb-4">
<div id="timerWrapper">
<p class="text-muted small mb-1">Kode berlaku selama:</p>
<span id="countdown" class="badge fs-6 px-3 py-2" style="background:#11998e;">10:00</span>
</div>
<div id="expiredWrapper" style="display:none;">
<span class="badge bg-danger fs-6 px-3 py-2">
Kode sudah kadaluarsa
</span>
</div>
</div>
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary-custom py-2 fw-bold" id="submitBtn" disabled>
Verifikasi Kode
</button>
</div>
</form>
<hr class="my-3">
{{-- Resend --}}
<div class="text-center">
<p class="text-muted small mb-2">Tidak menerima kode?</p>
<form method="POST" action="{{ route('admin.password.resend-otp') }}">
@csrf
<button type="submit" class="btn btn-outline-secondary btn-sm" id="resendBtn" disabled>
🔄 Kirim Ulang Kode
</button>
</form>
<div class="mt-2">
<a href="{{ route('admin.password.request') }}" class="text-decoration-none small text-muted">
Ganti email
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const boxes = document.querySelectorAll('.otp-box');
const otpHidden = document.getElementById('otpHidden');
const submitBtn = document.getElementById('submitBtn');
const resendBtn = document.getElementById('resendBtn');
const countdown = document.getElementById('countdown');
const timerWrap = document.getElementById('timerWrapper');
const expiredWrap= document.getElementById('expiredWrapper');
// ---- OTP input logic ----
boxes.forEach((box, i) => {
box.addEventListener('input', function () {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value) {
this.classList.add('filled');
if (i < boxes.length - 1) boxes[i + 1].focus();
} else {
this.classList.remove('filled');
}
updateHidden();
});
box.addEventListener('keydown', function (e) {
if (e.key === 'Backspace' && !this.value && i > 0) {
boxes[i - 1].focus();
boxes[i - 1].value = '';
boxes[i - 1].classList.remove('filled');
updateHidden();
}
});
box.addEventListener('paste', function (e) {
e.preventDefault();
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
pasted.split('').slice(0, 6).forEach((ch, idx) => {
if (boxes[idx]) { boxes[idx].value = ch; boxes[idx].classList.add('filled'); }
});
updateHidden();
boxes[Math.min(pasted.length, 5)].focus();
});
});
function updateHidden() {
const otp = Array.from(boxes).map(b => b.value).join('');
otpHidden.value = otp;
submitBtn.disabled = otp.length !== 6;
}
// ---- Countdown 10 min ----
let secs = 10 * 60;
const timer = setInterval(() => {
secs--;
const m = String(Math.floor(secs / 60)).padStart(2, '0');
const s = String(secs % 60).padStart(2, '0');
countdown.textContent = `${m}:${s}`;
if (secs <= 60) { countdown.style.background = '#ffc107'; countdown.style.color = '#000'; }
if (secs <= 0) {
clearInterval(timer);
timerWrap.style.display = 'none';
expiredWrap.style.display = 'block';
submitBtn.disabled = true;
resendBtn.disabled = false;
}
}, 1000);
// Enable resend after 60s
setTimeout(() => { resendBtn.disabled = false; }, 60000);
});
</script>
</body>
</html>

View File

@ -0,0 +1,155 @@
@extends('layouts.app')
@section('title', 'Lupa Password - Shop Bibit Cabai Bondowoso')
@section('content')
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card border-0 shadow-lg">
<div class="card-header bg-success text-white text-center py-4">
<h3 class="mb-0">
<i class="fas fa-key me-2"></i>Lupa Password
</h3>
<p class="mb-0 mt-2 opacity-75 small">Masukkan email terdaftar Anda</p>
</div>
<div class="card-body p-4">
{{-- Step Indicator --}}
<div class="d-flex align-items-center justify-content-center mb-4">
<div class="step-item active">
<div class="step-circle bg-success text-white">1</div>
<div class="step-label text-success fw-bold">Email</div>
</div>
<div class="step-line bg-secondary mx-2"></div>
<div class="step-item">
<div class="step-circle bg-secondary text-white">2</div>
<div class="step-label text-secondary">Kode OTP</div>
</div>
<div class="step-line bg-secondary mx-2"></div>
<div class="step-item">
<div class="step-circle bg-secondary text-white">3</div>
<div class="step-label text-secondary">Password Baru</div>
</div>
</div>
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<p class="text-muted text-center mb-4">
<i class="fas fa-info-circle text-success me-1"></i>
Kami akan mengirim kode verifikasi ke email Anda untuk mereset password.
</p>
<form method="POST" action="{{ route('password.email') }}" id="forgotForm">
@csrf
<div class="mb-4">
<label for="email" class="form-label fw-semibold">
Email <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="fas fa-envelope text-success"></i>
</span>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
id="email"
name="email"
value="{{ old('email') }}"
required
autofocus
placeholder="contoh@gmail.com">
</div>
<div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Gunakan email @gmail.com yang terdaftar</small>
</div>
@error('email')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
<div id="emailError" class="text-danger mt-1" style="display: none;">
<small>Email harus menggunakan domain @gmail.com</small>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
<i class="fas fa-paper-plane me-2"></i>Kirim Kode Verifikasi
</button>
</div>
</form>
<hr class="my-4">
<div class="text-center">
<a href="{{ route('login') }}" class="text-success text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Kembali ke Halaman Login
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.step-circle {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.step-label {
font-size: 11px;
white-space: nowrap;
}
.step-line {
height: 2px;
width: 40px;
margin-bottom: 18px;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
const emailInput = document.getElementById('email');
const submitBtn = document.getElementById('submitBtn');
const emailError = document.getElementById('emailError');
emailInput.addEventListener('input', function () {
const val = this.value;
const gmailPattern = /@gmail\.com$/;
const hasSpaces = /\s/.test(val);
if (val.length > 0 && (!gmailPattern.test(val) || hasSpaces)) {
emailError.style.display = 'block';
this.classList.add('is-invalid');
submitBtn.disabled = true;
} else {
emailError.style.display = 'none';
this.classList.remove('is-invalid');
submitBtn.disabled = val.length === 0;
}
});
emailInput.addEventListener('keydown', function (e) {
if (e.key === ' ') e.preventDefault();
});
});
</script>
@endsection

View File

@ -74,7 +74,11 @@ class="form-control @error('password') is-invalid @enderror"
<small>Password minimal 8 karakter dan tidak boleh mengandung spasi</small> <small>Password minimal 8 karakter dan tidak boleh mengandung spasi</small>
</div> </div>
</div> </div>
<div class="text-end mb-3">
<a href="{{ route('password.request') }}" class="text-success text-decoration-none small">
<i class="fas fa-question-circle me-1"></i>Lupa Password?
</a>
</div>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input @error('remember') is-invalid @enderror" id="remember" name="remember" required> <input type="checkbox" class="form-check-input @error('remember') is-invalid @enderror" id="remember" name="remember" required>
<label class="form-check-label" for="remember"> <label class="form-check-label" for="remember">

View File

@ -0,0 +1,285 @@
@extends('layouts.app')
@section('title', 'Reset Password - Shop Bibit Cabai Bondowoso')
@section('content')
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card border-0 shadow-lg">
<div class="card-header bg-success text-white text-center py-4">
<h3 class="mb-0">
<i class="fas fa-lock-open me-2"></i>Buat Password Baru
</h3>
<p class="mb-0 mt-2 opacity-75 small">Buat password yang kuat untuk akun Anda</p>
</div>
<div class="card-body p-4">
{{-- Step Indicator --}}
<div class="d-flex align-items-center justify-content-center mb-4">
<div class="step-item">
<div class="step-circle bg-success text-white">
<i class="fas fa-check" style="font-size:12px;"></i>
</div>
<div class="step-label text-success fw-bold">Email</div>
</div>
<div class="step-line bg-success mx-2"></div>
<div class="step-item">
<div class="step-circle bg-success text-white">
<i class="fas fa-check" style="font-size:12px;"></i>
</div>
<div class="step-label text-success fw-bold">Kode OTP</div>
</div>
<div class="step-line bg-success mx-2"></div>
<div class="step-item active">
<div class="step-circle bg-success text-white">3</div>
<div class="step-label text-success fw-bold">Password Baru</div>
</div>
</div>
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<p class="text-muted text-center mb-4">
<i class="fas fa-check-circle text-success me-1"></i>
Email terverifikasi. Silakan buat password baru Anda.
</p>
<form method="POST" action="{{ route('password.update') }}" id="resetForm">
@csrf
{{-- Hidden token from session --}}
<input type="hidden" name="token" value="{{ session('reset_token') }}">
<input type="hidden" name="email" value="{{ session('reset_email') }}">
{{-- New Password --}}
<div class="mb-3">
<label for="password" class="form-label fw-semibold">
Password Baru <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="fas fa-lock text-success"></i>
</span>
<input type="password"
class="form-control @error('password') is-invalid @enderror"
id="password"
name="password"
required
placeholder="Minimal 8 karakter">
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="fas fa-eye" id="eyeIcon1"></i>
</button>
</div>
<div class="form-text text-muted">
<small><i class="fas fa-info-circle"></i> Minimal 8 karakter, tanpa spasi</small>
</div>
@error('password')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
<div id="passwordError" class="text-danger mt-1" style="display:none;">
<small>Password minimal 8 karakter dan tidak boleh mengandung spasi</small>
</div>
</div>
{{-- Password Strength Bar --}}
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">Kekuatan password:</small>
<small id="strengthLabel" class="fw-semibold text-secondary"></small>
</div>
<div class="progress" style="height: 6px; border-radius: 3px;">
<div id="strengthBar" class="progress-bar bg-secondary"
role="progressbar" style="width: 0%"></div>
</div>
</div>
{{-- Confirm Password --}}
<div class="mb-4">
<label for="password_confirmation" class="form-label fw-semibold">
Konfirmasi Password <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="fas fa-lock text-success"></i>
</span>
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
id="password_confirmation"
name="password_confirmation"
required
placeholder="Ulangi password baru">
<button class="btn btn-outline-secondary" type="button" id="toggleConfirm">
<i class="fas fa-eye" id="eyeIcon2"></i>
</button>
</div>
@error('password_confirmation')
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
@enderror
<div id="confirmError" class="text-danger mt-1" style="display:none;">
<small>Password tidak cocok</small>
</div>
<div id="confirmSuccess" class="text-success mt-1" style="display:none;">
<small><i class="fas fa-check-circle me-1"></i>Password cocok</small>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
<i class="fas fa-save me-2"></i>Simpan Password Baru
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.step-circle {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.step-label {
font-size: 11px;
white-space: nowrap;
}
.step-line {
height: 2px;
width: 40px;
margin-bottom: 18px;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
const passwordInput = document.getElementById('password');
const confirmInput = document.getElementById('password_confirmation');
const submitBtn = document.getElementById('submitBtn');
const strengthBar = document.getElementById('strengthBar');
const strengthLabel = document.getElementById('strengthLabel');
// Toggle password visibility
document.getElementById('togglePassword').addEventListener('click', function () {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
document.getElementById('eyeIcon1').classList.toggle('fa-eye');
document.getElementById('eyeIcon1').classList.toggle('fa-eye-slash');
});
document.getElementById('toggleConfirm').addEventListener('click', function () {
const type = confirmInput.getAttribute('type') === 'password' ? 'text' : 'password';
confirmInput.setAttribute('type', type);
document.getElementById('eyeIcon2').classList.toggle('fa-eye');
document.getElementById('eyeIcon2').classList.toggle('fa-eye-slash');
});
// Password strength checker
function checkStrength(pwd) {
let score = 0;
if (pwd.length >= 8) score++;
if (pwd.length >= 12) score++;
if (/[A-Z]/.test(pwd)) score++;
if (/[0-9]/.test(pwd)) score++;
if (/[^A-Za-z0-9]/.test(pwd)) score++;
return score;
}
passwordInput.addEventListener('input', function () {
const val = this.value;
const hasSpaces = /\s/.test(val);
const passwordError = document.getElementById('passwordError');
if (val.length > 0 && (val.length < 8 || hasSpaces)) {
passwordError.style.display = 'block';
this.classList.add('is-invalid');
} else {
passwordError.style.display = 'none';
this.classList.remove('is-invalid');
}
// Strength bar
if (val.length === 0) {
strengthBar.style.width = '0%';
strengthBar.className = 'progress-bar bg-secondary';
strengthLabel.textContent = '—';
strengthLabel.className = 'fw-semibold text-secondary';
} else {
const score = checkStrength(val);
const levels = [
{ pct: '20%', cls: 'bg-danger', label: 'Sangat Lemah', labelCls: 'text-danger' },
{ pct: '40%', cls: 'bg-warning', label: 'Lemah', labelCls: 'text-warning' },
{ pct: '60%', cls: 'bg-info', label: 'Sedang', labelCls: 'text-info' },
{ pct: '80%', cls: 'bg-primary', label: 'Kuat', labelCls: 'text-primary' },
{ pct: '100%', cls: 'bg-success', label: 'Sangat Kuat', labelCls: 'text-success' },
];
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
strengthBar.style.width = lvl.pct;
strengthBar.className = `progress-bar ${lvl.cls}`;
strengthLabel.textContent = lvl.label;
strengthLabel.className = `fw-semibold ${lvl.labelCls}`;
}
validateConfirm();
checkFormValidity();
});
confirmInput.addEventListener('input', function () {
validateConfirm();
checkFormValidity();
});
function validateConfirm() {
const confirmError = document.getElementById('confirmError');
const confirmSuccess = document.getElementById('confirmSuccess');
if (confirmInput.value.length === 0) {
confirmError.style.display = 'none';
confirmSuccess.style.display = 'none';
confirmInput.classList.remove('is-invalid', 'is-valid');
} else if (passwordInput.value !== confirmInput.value) {
confirmError.style.display = 'block';
confirmSuccess.style.display = 'none';
confirmInput.classList.add('is-invalid');
confirmInput.classList.remove('is-valid');
} else {
confirmError.style.display = 'none';
confirmSuccess.style.display = 'block';
confirmInput.classList.remove('is-invalid');
confirmInput.classList.add('is-valid');
}
}
function checkFormValidity() {
const pwValid = passwordInput.value.length >= 8 && !/\s/.test(passwordInput.value);
const confirmValid = confirmInput.value === passwordInput.value && confirmInput.value.length > 0;
submitBtn.disabled = !(pwValid && confirmValid);
}
// Prevent spaces
[passwordInput, confirmInput].forEach(input => {
input.addEventListener('keydown', function (e) {
if (e.key === ' ') e.preventDefault();
});
});
});
</script>
@endsection

View File

@ -0,0 +1,268 @@
@extends('layouts.app')
@section('title', 'Verifikasi Kode - Shop Bibit Cabai Bondowoso')
@section('content')
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card border-0 shadow-lg">
<div class="card-header bg-success text-white text-center py-4">
<h3 class="mb-0">
<i class="fas fa-shield-alt me-2"></i>Verifikasi Kode
</h3>
<p class="mb-0 mt-2 opacity-75 small">Masukkan kode yang dikirim ke email Anda</p>
</div>
<div class="card-body p-4">
{{-- Step Indicator --}}
<div class="d-flex align-items-center justify-content-center mb-4">
<div class="step-item">
<div class="step-circle bg-success text-white">
<i class="fas fa-check" style="font-size:12px;"></i>
</div>
<div class="step-label text-success fw-bold">Email</div>
</div>
<div class="step-line bg-success mx-2"></div>
<div class="step-item active">
<div class="step-circle bg-success text-white">2</div>
<div class="step-label text-success fw-bold">Kode OTP</div>
</div>
<div class="step-line bg-secondary mx-2"></div>
<div class="step-item">
<div class="step-circle bg-secondary text-white">3</div>
<div class="step-label text-secondary">Password Baru</div>
</div>
</div>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Info email target --}}
<div class="alert alert-light border-start border-success border-3 mb-4">
<div class="d-flex align-items-center">
<i class="fas fa-envelope text-success me-3 fs-5"></i>
<div>
<div class="fw-semibold text-dark">Kode dikirim ke:</div>
<div class="text-muted small">{{ session('reset_email', 'email Anda') }}</div>
</div>
</div>
</div>
<form method="POST" action="{{ route('password.verify-otp') }}" id="otpForm">
@csrf
<div class="mb-4">
<label class="form-label fw-semibold text-center d-block">
Kode Verifikasi (6 digit) <span class="text-danger">*</span>
</label>
{{-- OTP Input Boxes --}}
<div class="d-flex justify-content-center gap-2 mb-2" id="otpBoxes">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
<input type="text" class="otp-box form-control text-center fw-bold fs-4"
maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="off">
</div>
{{-- Hidden input to store full OTP --}}
<input type="hidden" name="otp" id="otpHidden">
@error('otp')
<div class="text-danger text-center mt-1"><small>{{ $message }}</small></div>
@enderror
</div>
{{-- Countdown Timer --}}
<div class="text-center mb-4">
<div id="timerWrapper">
<p class="text-muted small mb-1">Kode berlaku selama:</p>
<span id="countdown" class="badge bg-success fs-6 px-3 py-2">10:00</span>
</div>
<div id="expiredWrapper" style="display:none;">
<span class="badge bg-danger fs-6 px-3 py-2">
<i class="fas fa-times-circle me-1"></i>Kode sudah kadaluarsa
</span>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
<i class="fas fa-check-circle me-2"></i>Verifikasi Kode
</button>
</div>
</form>
<hr class="my-4">
{{-- Resend OTP --}}
<div class="text-center">
<p class="text-muted small mb-2">Tidak menerima kode?</p>
<form method="POST" action="{{ route('password.resend-otp') }}" id="resendForm">
@csrf
<button type="submit" class="btn btn-outline-success btn-sm" id="resendBtn" disabled>
<i class="fas fa-redo me-1"></i>Kirim Ulang Kode
</button>
</form>
<div class="mt-2">
<a href="{{ route('password.request') }}" class="text-muted text-decoration-none small">
<i class="fas fa-arrow-left me-1"></i>Ganti email
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.step-circle {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.step-label {
font-size: 11px;
white-space: nowrap;
}
.step-line {
height: 2px;
width: 40px;
margin-bottom: 18px;
}
.otp-box {
width: 48px !important;
height: 56px !important;
padding: 0 !important;
border-radius: 10px !important;
border: 2px solid #dee2e6 !important;
transition: border-color 0.2s, box-shadow 0.2s;
}
.otp-box:focus {
border-color: #198754 !important;
box-shadow: 0 0 0 3px rgba(25,135,84,0.2) !important;
}
.otp-box.filled {
border-color: #198754 !important;
background-color: #f0fff4 !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
const boxes = document.querySelectorAll('.otp-box');
const otpHidden = document.getElementById('otpHidden');
const submitBtn = document.getElementById('submitBtn');
const resendBtn = document.getElementById('resendBtn');
const countdownEl = document.getElementById('countdown');
const timerWrapper = document.getElementById('timerWrapper');
const expiredWrapper = document.getElementById('expiredWrapper');
// ---- OTP box logic ----
boxes.forEach((box, index) => {
box.addEventListener('input', function () {
this.value = this.value.replace(/[^0-9]/g, '');
if (this.value.length === 1) {
this.classList.add('filled');
if (index < boxes.length - 1) boxes[index + 1].focus();
} else {
this.classList.remove('filled');
}
updateHidden();
});
box.addEventListener('keydown', function (e) {
if (e.key === 'Backspace' && this.value === '' && index > 0) {
boxes[index - 1].focus();
boxes[index - 1].value = '';
boxes[index - 1].classList.remove('filled');
updateHidden();
}
});
// Handle paste
box.addEventListener('paste', function (e) {
e.preventDefault();
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
pasted.split('').slice(0, 6).forEach((char, i) => {
if (boxes[i]) {
boxes[i].value = char;
boxes[i].classList.add('filled');
}
});
updateHidden();
const lastFilled = Math.min(pasted.length, 5);
boxes[lastFilled].focus();
});
});
function updateHidden() {
const otp = Array.from(boxes).map(b => b.value).join('');
otpHidden.value = otp;
submitBtn.disabled = otp.length !== 6;
}
// ---- Countdown Timer: 10 minutes ----
let seconds = 10 * 60;
function formatTime(s) {
const m = Math.floor(s / 60).toString().padStart(2, '0');
const sec = (s % 60).toString().padStart(2, '0');
return `${m}:${sec}`;
}
const timer = setInterval(function () {
seconds--;
countdownEl.textContent = formatTime(seconds);
if (seconds <= 60) {
countdownEl.classList.replace('bg-success', 'bg-warning');
countdownEl.classList.add('text-dark');
}
if (seconds <= 0) {
clearInterval(timer);
timerWrapper.style.display = 'none';
expiredWrapper.style.display = 'block';
submitBtn.disabled = true;
// Enable resend after expiry
resendBtn.disabled = false;
}
}, 1000);
// Enable resend after 60s
setTimeout(() => { resendBtn.disabled = false; }, 60000);
});
</script>
@endsection

View File

@ -54,8 +54,12 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Nama Lengkap *</label> <label class="form-label">Nama Lengkap *</label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror" <input type="text" name="name"
value="{{ old('name') }}" required> class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}"
placeholder="Minimal 8 karakter"
minlength="8" required>
<small class="text-muted">Minimal 8 karakter</small>
@error('name') @error('name')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
@ -63,17 +67,24 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Nomor Telepon *</label> <label class="form-label">Nomor Telepon *</label>
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror" <input type="text" name="phone"
value="{{ old('phone') }}" placeholder="08123456789" required> 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>
@error('phone') @error('phone')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Email *</label> <label class="form-label">Email *</label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror" <input type="email" name="email"
value="{{ old('email') }}" required> class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}"
placeholder="contoh@email.com"
minlength="8" required>
<small class="text-muted">Minimal 8 karakter</small>
@error('email') @error('email')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
@ -132,9 +143,15 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Alamat Lengkap (Nama Jalan, No Rumah, RT/RW, Desa) *</label> <label class="form-label">Alamat Lengkap *</label>
<textarea name="address" class="form-control @error('address') is-invalid @enderror" <textarea name="address"
rows="3" placeholder="Contoh: Jl. Merdeka No. 123, RT 02/RW 05, Desa Kademangan" required>{{ old('address') }}</textarea> 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>
<small class="text-muted">
Wajib lengkap: Nama Jalan · No. Rumah · RT/RW · Desa/Kelurahan · Ancer-ancer
</small>
@error('address') @error('address')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
@ -144,8 +161,11 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Kode Pos *</label> <label class="form-label">Kode Pos *</label>
<input type="text" name="postal_code" class="form-control @error('postal_code') is-invalid @enderror" <input type="text" name="postal_code"
value="{{ old('postal_code', '68200') }}" placeholder="68200" required> class="form-control @error('postal_code') is-invalid @enderror"
value="{{ old('postal_code', '68200') }}"
placeholder="68200"
maxlength="5" required>
@error('postal_code') @error('postal_code')
<div class="invalid-feedback">{{ $message }}</div> <div class="invalid-feedback">{{ $message }}</div>
@enderror @enderror
@ -390,18 +410,17 @@ class="border p-2 bg-white"> -->
setTimeout(() => { setTimeout(() => {
const baseOngkir = ongkirData[kecamatan] || 15000; const baseOngkir = ongkirData[kecamatan] || 15000;
let totalOngkir = baseOngkir; let totalOngkir = baseOngkir;
let detail = `Base ongkir: Rp ${formatRupiah(baseOngkir)}`; let detail = `Ongkir ke ${kecamatan}: Rp ${formatRupiah(baseOngkir)}`;
if (quantity > 5) { // Gratis ongkir >= 800 bibit
const extraBibit = quantity - 5; if (quantity >= 800) {
const extraCost = extraBibit * 5000;
totalOngkir += extraCost;
detail += ` + Tambahan ${extraBibit} bibit: Rp ${formatRupiah(extraCost)}`;
}
if (quantity >= 100) {
totalOngkir = 0; totalOngkir = 0;
detail = '🎉 GRATIS ONGKIR untuk pembelian 100+ bibit!'; detail = '🎉 GRATIS ONGKIR untuk pembelian 800+ bibit!';
}
// >= 1000 bibit: gratis ongkir + diskon 15%
if (quantity >= 1000) {
totalOngkir = 0;
detail = '🎉 GRATIS ONGKIR + Diskon 15% untuk pembelian 1000+ bibit!';
} }
document.getElementById('ongkirKecamatan').textContent = kecamatan; document.getElementById('ongkirKecamatan').textContent = kecamatan;
@ -466,9 +485,40 @@ class="border p-2 bg-white"> -->
}); });
function updateTotal(ongkir) { function updateTotal(ongkir) {
const total = subtotal + ongkir; // Hitung diskon
let diskon = 0;
let diskonLabel = '';
if (quantity >= 1000) {
diskon = subtotal * 0.15;
diskonLabel = '🎉 Diskon 15% untuk pembelian 1000+ bibit!';
}
const finalTotal = (subtotal - diskon) + ongkir;
document.getElementById('ongkirDisplay').textContent = 'Rp ' + formatRupiah(ongkir); document.getElementById('ongkirDisplay').textContent = 'Rp ' + formatRupiah(ongkir);
document.getElementById('totalDisplay').textContent = 'Rp ' + formatRupiah(total); document.getElementById('totalDisplay').textContent = 'Rp ' + formatRupiah(finalTotal);
// Tampilkan/sembunyikan baris diskon
let diskonEl = document.getElementById('diskonRow');
if (!diskonEl && diskon > 0) {
// Buat baris diskon jika belum ada
const ongkirRow = document.querySelector('.total-section hr');
const rowHtml = `
<div class="d-flex justify-content-between mb-2 text-danger" id="diskonRow">
<span>${diskonLabel}</span>
<span><strong>-Rp ${formatRupiah(diskon)}</strong></span>
</div>`;
ongkirRow.insertAdjacentHTML('beforebegin', rowHtml);
} else if (diskonEl) {
if (diskon > 0) {
diskonEl.style.display = 'flex';
diskonEl.querySelector('span').textContent = diskonLabel;
diskonEl.querySelector('strong').textContent = '-Rp ' + formatRupiah(diskon);
} else {
diskonEl.style.display = 'none';
}
}
} }
function formatRupiah(angka) { function formatRupiah(angka) {

View File

@ -58,6 +58,75 @@
.copy-btn:hover { .copy-btn:hover {
transform: scale(1.1); transform: scale(1.1);
} }
@media print {
@page {
size: A4;
margin: 10mm;
}
body * {
visibility: hidden;
height: 0;
overflow: hidden;
}
#print-area,
#print-area * {
visibility: visible;
height: auto;
overflow: visible;
}
#print-area {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 10px;
}
#print-area .card {
box-shadow: none !important;
border: 1px solid #dee2e6 !important;
margin-top: 8px !important;
border-radius: 8px !important;
}
#print-area .card-body {
padding: 10px 14px !important;
}
#print-area h5 {
font-size: 0.9rem !important;
margin-bottom: 8px !important;
}
#print-area .order-detail-row {
padding: 5px 0 !important;
}
#print-area p,
#print-area span,
#print-area td,
#print-area small {
font-size: 0.8rem !important;
}
#print-area img {
width: 50px !important;
height: 50px !important;
}
#print-area hr {
margin: 5px 0 !important;
}
#print-area .mb-2,
#print-area .mb-3 {
margin-bottom: 4px !important;
}
}
</style> </style>
@endsection @endsection
@ -65,15 +134,15 @@
<div class="container my-5"> <div class="container my-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
{{-- Card Pesanan Berhasil (tidak ikut print) --}}
<div class="card success-card"> <div class="card success-card">
<div class="card-body p-5 text-center"> <div class="card-body p-5 text-center">
<div class="success-icon"> <div class="success-icon">
<i class="fas fa-check fa-3x text-white"></i> <i class="fas fa-check fa-3x text-white"></i>
</div> </div>
<h2 class="fw-bold text-success mb-3">Pesanan Berhasil!</h2> <h2 class="fw-bold text-success mb-3">Pesanan Berhasil!</h2>
<p class="text-muted mb-4">Terima kasih atas pesanan Anda. Pesanan Anda sedang diproses.</p> <p class="text-muted mb-4">Terima kasih atas pesanan Anda. Pesanan Anda sedang diproses.</p>
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">
<i class="fas fa-info-circle me-2"></i> <i class="fas fa-info-circle me-2"></i>
Nomor Invoice: <strong>{{ $transaksi->invoice_number }}</strong> Nomor Invoice: <strong>{{ $transaksi->invoice_number }}</strong>
@ -81,27 +150,27 @@
</div> </div>
</div> </div>
<!-- Detail Pesanan --> {{-- ===== MULAI AREA PRINT ===== --}}
<div id="print-area">
{{-- Card Detail Pesanan --}}
<div class="card success-card mt-4"> <div class="card success-card mt-4">
<div class="card-body p-4"> <div class="card-body p-4">
<h5 class="fw-bold mb-4"> <h5 class="fw-bold mb-4">
<i class="fas fa-clipboard-list me-2 text-success"></i>Detail Pesanan <i class="fas fa-clipboard-list me-2 text-success"></i>Detail Pesanan
</h5> </h5>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Invoice:</strong></div> <div class="col-6"><strong>Invoice:</strong></div>
<div class="col-6 text-end">{{ $transaksi->invoice_number }}</div> <div class="col-6 text-end">{{ $transaksi->invoice_number }}</div>
</div> </div>
</div> </div>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Tanggal:</strong></div> <div class="col-6"><strong>Tanggal:</strong></div>
<div class="col-6 text-end">{{ $transaksi->created_at->format('d M Y H:i') }}</div> <div class="col-6 text-end">{{ $transaksi->created_at->format('d M Y H:i') }}</div>
</div> </div>
</div> </div>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Status Pesanan:</strong></div> <div class="col-6"><strong>Status Pesanan:</strong></div>
@ -130,34 +199,33 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Data Pembeli --> </div>
</div>
{{-- Card Data Pembeli --}}
<div class="card success-card mt-4"> <div class="card success-card mt-4">
<div class="card-body p-4"> <div class="card-body p-4">
<h5 class="fw-bold mb-4"> <h5 class="fw-bold mb-4">
<i class="fas fa-user me-2 text-success"></i>Data Pembeli <i class="fas fa-user me-2 text-success"></i>Data Pembeli
</h5> </h5>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Nama:</strong></div> <div class="col-6"><strong>Nama:</strong></div>
<div class="col-6 text-end">{{ $transaksi->customer_name }}</div> <div class="col-6 text-end">{{ $transaksi->customer_name }}</div>
</div> </div>
</div> </div>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Telepon:</strong></div> <div class="col-6"><strong>Telepon:</strong></div>
<div class="col-6 text-end">{{ $transaksi->customer_phone }}</div> <div class="col-6 text-end">{{ $transaksi->customer_phone }}</div>
</div> </div>
</div> </div>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-6"><strong>Email:</strong></div> <div class="col-6"><strong>Email:</strong></div>
<div class="col-6 text-end">{{ $transaksi->customer_email }}</div> <div class="col-6 text-end">{{ $transaksi->customer_email }}</div>
</div> </div>
</div> </div>
<div class="order-detail-row"> <div class="order-detail-row">
<div class="row"> <div class="row">
<div class="col-12"><strong>Alamat Pengiriman:</strong></div> <div class="col-12"><strong>Alamat Pengiriman:</strong></div>
@ -170,13 +238,12 @@
</div> </div>
</div> </div>
<!-- Produk --> {{-- Card Produk yang Dipesan --}}
<div class="card success-card mt-4"> <div class="card success-card mt-4">
<div class="card-body p-4"> <div class="card-body p-4">
<h5 class="fw-bold mb-4"> <h5 class="fw-bold mb-4">
<i class="fas fa-box me-2 text-success"></i>Produk yang Dipesan <i class="fas fa-box me-2 text-success"></i>Produk yang Dipesan
</h5> </h5>
@foreach($transaksi->details as $detail) @foreach($transaksi->details as $detail)
<div class="d-flex align-items-center mb-3 p-3" style="background: #f8f9fa; border-radius: 10px;"> <div class="d-flex align-items-center mb-3 p-3" style="background: #f8f9fa; border-radius: 10px;">
<img src="{{ $detail->product->image_url ?? 'https://via.placeholder.com/80x80/28a745/ffffff?text=Bibit' }}" <img src="{{ $detail->product->image_url ?? 'https://via.placeholder.com/80x80/28a745/ffffff?text=Bibit' }}"
@ -192,21 +259,16 @@ class="rounded me-3"
</div> </div>
</div> </div>
@endforeach @endforeach
<hr> <hr>
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span>Subtotal</span> <span>Subtotal</span>
<span>Rp {{ number_format($transaksi->subtotal, 0, ',', '.') }}</span> <span>Rp {{ number_format($transaksi->subtotal, 0, ',', '.') }}</span>
</div> </div>
<div class="d-flex justify-content-between mb-3"> <div class="d-flex justify-content-between mb-3">
<span>Ongkos Kirim</span> <span>Ongkos Kirim</span>
<span>Rp {{ number_format($transaksi->shipping_cost, 0, ',', '.') }}</span> <span>Rp {{ number_format($transaksi->shipping_cost, 0, ',', '.') }}</span>
</div> </div>
<hr> <hr>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5 class="fw-bold">Total</h5> <h5 class="fw-bold">Total</h5>
<h5 class="fw-bold text-success">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</h5> <h5 class="fw-bold text-success">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</h5>
@ -214,33 +276,28 @@ class="rounded me-3"
</div> </div>
</div> </div>
<!-- Informasi Transfer --> </div>
<!-- Informasi Transfer --> {{-- ===== AKHIR AREA PRINT ===== --}}
@if(in_array($transaksi->payment_method, ['qris', 'bri', 'dana', 'seabank', 'shopee']))
<div class="card success-card mt-4"> {{-- Card Informasi Transfer (tidak ikut print) --}}
@if(in_array($transaksi->payment_method, ['qris', 'bri', 'dana', 'seabank', 'shopee']))
<div class="card success-card mt-4">
<div class="card-body p-4"> <div class="card-body p-4">
<h5 class="fw-bold mb-4"> <h5 class="fw-bold mb-4">
<i class="fas fa-university me-2 text-success"></i>Informasi Pembayaran <i class="fas fa-university me-2 text-success"></i>Informasi Pembayaran
</h5> </h5>
<div class="alert alert-warning"> <div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-exclamation-triangle me-2"></i>
Silakan lakukan pembayaran sesuai metode yang Anda pilih Silakan lakukan pembayaran sesuai metode yang Anda pilih
</div> </div>
@if($transaksi->payment_method == 'qris') @if($transaksi->payment_method == 'qris')
<!-- QRIS Info -->
<div class="payment-info"> <div class="payment-info">
<p class="mb-1"><strong>📌 Data QRIS:</strong></p> <p class="mb-1"><strong>📌 Data QRIS:</strong></p>
<p class="mb-1">Nama: <strong>ACHMAD BAYU AL GHOZALI</strong></p> <p class="mb-1">Nama: <strong>ACHMAD BAYU AL GHOZALI</strong></p>
<p class="mb-0">NMID: <strong>ID1025440155548</strong></p> <p class="mb-0">NMID: <strong>ID1025440155548</strong></p>
</div> </div>
</div>
<div class="text-center my-3"> <div class="text-center my-3">
<!-- QR Code DANA Anda -->
<div id="qrCodeContainer" class="bg-white p-3 d-inline-block rounded shadow-sm"> <div id="qrCodeContainer" class="bg-white p-3 d-inline-block rounded shadow-sm">
<!-- PENTING: Ganti src di bawah dengan QR Code QRIS Dana Anda -->
<!-- Opsi 1: Jika Anda punya file QR Code lokal -->
<img src="{{ asset('images/qris.jpg') }}" <img src="{{ asset('images/qris.jpg') }}"
alt="QRIS DANA" alt="QRIS DANA"
style="width: 400px; height: 400px;" style="width: 400px; height: 400px;"
@ -258,7 +315,7 @@ class="border p-2 bg-white">
Scan QR Code dengan aplikasi pembayaran digital apa saja yang mendukung QRIS Scan QR Code dengan aplikasi pembayaran digital apa saja yang mendukung QRIS
</small> </small>
</div> </div>
</div>
@elseif($transaksi->payment_method == 'bri') @elseif($transaksi->payment_method == 'bri')
<div class="bank-account"> <div class="bank-account">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
@ -272,52 +329,43 @@ class="border p-2 bg-white">
<p class="mb-0">No. Rekening: <strong>1234-5678-9012-3456</strong></p> <p class="mb-0">No. Rekening: <strong>1234-5678-9012-3456</strong></p>
<p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p> <p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p>
</div> </div>
@elseif($transaksi->payment_method == 'dana') @elseif($transaksi->payment_method == 'dana')
<div class="bank-account"> <div class="bank-account">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<div> <div><strong>💳 DANA</strong></div>
<strong>💳 DANA</strong>
</div>
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i> <i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i>
</div> </div>
<p class="mb-0">Nomor DANA: <strong>0812-3456-7890</strong></p> <p class="mb-0">Nomor DANA: <strong>0812-3456-7890</strong></p>
<p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p> <p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p>
</div> </div>
@elseif($transaksi->payment_method == 'seabank') @elseif($transaksi->payment_method == 'seabank')
<div class="bank-account"> <div class="bank-account">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<div> <div><strong>🏦 SeaBank</strong></div>
<strong>🏦 SeaBank</strong>
</div>
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('901234567890')" title="Copy"></i> <i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('901234567890')" title="Copy"></i>
</div> </div>
<p class="mb-0">No. Rekening: <strong>901234567890</strong></p> <p class="mb-0">No. Rekening: <strong>901234567890</strong></p>
<p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p> <p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p>
</div> </div>
@elseif($transaksi->payment_method == 'shopee') @elseif($transaksi->payment_method == 'shopee')
<div class="bank-account"> <div class="bank-account">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<div> <div><strong>🛒 ShopeePay</strong></div>
<strong>🛒 ShopeePay</strong>
</div>
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i> <i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i>
</div> </div>
<p class="mb-0">Nomor ShopeePay: <strong>0812-3456-7890</strong></p> <p class="mb-0">Nomor ShopeePay: <strong>0812-3456-7890</strong></p>
<p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p> <p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p>
</div> </div>
@endif @endif
<div class="alert alert-info mt-3 mb-0"> <div class="alert alert-info mt-3 mb-0">
<i class="fas fa-info-circle me-2"></i> <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> 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> </div>
</div> </div>
</div> </div>
@endif @endif
<!-- Actions -->
{{-- Card Tombol Aksi (tidak ikut print) --}}
<div class="card success-card mt-4"> <div class="card success-card mt-4">
<div class="card-body p-4 text-center"> <div class="card-body p-4 text-center">
<div class="row g-3"> <div class="row g-3">
@ -328,11 +376,10 @@ class="border p-2 bg-white">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<button onclick="window.print()" class="btn btn-success w-100"> <button onclick="window.print()" class="btn btn-success w-100">
<i class="fas fa-print me-2"></i>Cetak Invoice <i class="fas fa-print me-2"></i>Cetak Bukti Checkout
</button> </button>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<a href="https://wa.me/6282313155053?text=Halo, saya ingin konfirmasi pembayaran dengan invoice {{ $transaksi->invoice_number }}" <a href="https://wa.me/6282313155053?text=Halo, saya ingin konfirmasi pembayaran dengan invoice {{ $transaksi->invoice_number }}"
target="_blank" target="_blank"
@ -342,6 +389,7 @@ class="btn btn-success w-100">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -22,21 +22,21 @@
<div class="contact-info"> <div class="contact-info">
<div class="mb-4"> <div class="mb-4">
<h5><i class="fas fa-map-marker-alt text-success me-2"></i>Alamat</h5> <h5><i class="fas fa-map-marker-alt text-success me-2"></i>Alamat</h5>
<p class="text-muted">Jl. Raya Bondowoso No. 123<br>Bondowoso, Jawa Timur 68211</p> <p class="text-muted">Jurang sapi, tapen, Bondowoso<br>Masuk paping sebelah toko</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h5><i class="fas fa-phone text-success me-2"></i>Telepon</h5> <h5><i class="fas fa-phone text-success me-2"></i>Telepon</h5>
<p class="text-muted"> <p class="text-muted">
<a href="tel:+62338421234" class="text-decoration-none">+62 338 421 234</a> <a href="tel:081331830561" class="text-decoration-none">081331830561</a>
</p> </p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h5><i class="fas fa-envelope text-success me-2"></i>Email</h5> <h5><i class="fas fa-envelope text-success me-2"></i>Email</h5>
<p class="text-muted"> <p class="text-muted">
<a href="mailto:info@bibitcabaibondowoso.com" class="text-decoration-none"> <a href="mailto:bibitcabai@gmail.com" class="text-decoration-none">
info@bibitcabaibondowoso.com bibitcabai@gmail.com
</a> </a>
</p> </p>
</div> </div>
@ -44,8 +44,8 @@
<div class="mb-4"> <div class="mb-4">
<h5><i class="fab fa-whatsapp text-success me-2"></i>WhatsApp</h5> <h5><i class="fab fa-whatsapp text-success me-2"></i>WhatsApp</h5>
<p class="text-muted"> <p class="text-muted">
<a href="https://wa.me/6281234567890" class="text-decoration-none" target="_blank"> <a href="https://wa.me/081331830561" class="text-decoration-none" target="_blank">
+62 812 3456 7890 081331830561
</a> </a>
</p> </p>
</div> </div>
@ -163,7 +163,7 @@ class="form-control @error('subject') is-invalid @enderror"
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="ratio ratio-21x9"> <div class="ratio ratio-21x9">
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3949.3962!2d113.8221!3d-7.9138!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zN8KwNTQnNDkuNyJTIDExM8KwNDknMTkuNiJF!5e0!3m2!1sen!2sid!4v1640000000000!5m2!1sen!2sid" <iframe src="https://maps.google.com/maps?q=-7.894878,113.909934&z=17&output=embed&hl=id"
style="border:0;" style="border:0;"
allowfullscreen="" allowfullscreen=""
loading="lazy" loading="lazy"

View File

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 600px; margin: auto; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { background: #2e7d32; color: white; padding: 20px 30px; }
.header h1 { margin: 0; font-size: 22px; }
.header p { margin: 5px 0 0; opacity: 0.85; font-size: 14px; }
.body { padding: 30px; }
.invoice-badge { display: inline-block; background: #e8f5e9; color: #2e7d32; font-weight: bold; padding: 6px 16px; border-radius: 20px; font-size: 14px; margin-bottom: 20px; }
.section { margin-bottom: 24px; }
.section-title { font-size: 13px; font-weight: bold; text-transform: uppercase; color: #888; letter-spacing: 1px; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 6px; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; }
.info-label { color: #555; }
.info-value { font-weight: bold; color: #222; text-align: right; max-width: 60%; }
.total-box { background: #f1f8e9; border: 2px solid #a5d6a7; border-radius: 8px; padding: 16px 20px; margin-top: 10px; }
.total-row { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 14px; }
.total-final { display: flex; justify-content: space-between; font-size: 18px; font-weight: bold; color: #2e7d32; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #a5d6a7; }
.badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.badge-pending { background: #fff3cd; color: #856404; }
.badge-payment { background: #d1ecf1; color: #0c5460; }
.cta { text-align: center; margin-top: 24px; }
.cta a { background: #2e7d32; color: white; padding: 12px 30px; border-radius: 6px; text-decoration: none; font-weight: bold; font-size: 15px; display: inline-block; }
.footer { background: #f9f9f9; padding: 16px 30px; text-align: center; font-size: 12px; color: #aaa; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🛒 Ada Pesanan Baru!</h1>
<p>{{ $transaksi->created_at->format('d F Y, H:i') }} WIB</p>
</div>
<div class="body">
<div class="invoice-badge">{{ $transaksi->invoice_number }}</div>
<!-- Info Pelanggan -->
<div class="section">
<div class="section-title">👤 Data Pelanggan</div>
<div class="info-row">
<span class="info-label">Nama</span>
<span class="info-value">{{ $transaksi->customer_name }}</span>
</div>
<div class="info-row">
<span class="info-label">Telepon</span>
<span class="info-value">{{ $transaksi->customer_phone }}</span>
</div>
<div class="info-row">
<span class="info-label">Email</span>
<span class="info-value">{{ $transaksi->customer_email }}</span>
</div>
</div>
<!-- Alamat Pengiriman -->
<div class="section">
<div class="section-title">📦 Alamat Pengiriman</div>
<div class="info-row">
<span class="info-label">Alamat</span>
<span class="info-value">{{ $transaksi->shipping_address }}</span>
</div>
<div class="info-row">
<span class="info-label">Kecamatan</span>
<span class="info-value">{{ $transaksi->city }}</span>
</div>
<div class="info-row">
<span class="info-label">Provinsi</span>
<span class="info-value">{{ $transaksi->province ?? 'Jawa Timur' }}</span>
</div>
<div class="info-row">
<span class="info-label">Kode Pos</span>
<span class="info-value">{{ $transaksi->postal_code }}</span>
</div>
</div>
<!-- Detail Produk -->
<div class="section">
<div class="section-title">🌶️ Detail Produk</div>
@foreach($transaksi->details as $detail)
<div class="info-row">
<span class="info-label">{{ $detail->product_name }}</span>
<span class="info-value">{{ $detail->quantity }} bibit × Rp {{ number_format($detail->price, 0, ',', '.') }}</span>
</div>
@endforeach
</div>
<!-- Ringkasan Pembayaran -->
<div class="section">
<div class="section-title">💳 Ringkasan Pembayaran</div>
<div class="total-box">
<div class="total-row">
<span>Subtotal</span>
<span>Rp {{ number_format($transaksi->subtotal, 0, ',', '.') }}</span>
</div>
@if($transaksi->discount > 0)
<div class="total-row" style="color: #e53935;">
<span>Diskon (15%)</span>
<span>-Rp {{ number_format($transaksi->discount, 0, ',', '.') }}</span>
</div>
@endif
<div class="total-row">
<span>Ongkos Kirim</span>
<span>{{ $transaksi->shipping_cost == 0 ? 'GRATIS' : 'Rp ' . number_format($transaksi->shipping_cost, 0, ',', '.') }}</span>
</div>
<div class="total-final">
<span>Total Pembayaran</span>
<span>Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</span>
</div>
</div>
<div style="margin-top: 12px;">
<div class="info-row">
<span class="info-label">Metode Pembayaran</span>
<span class="info-value">
<span class="badge badge-payment">
{{ strtoupper($transaksi->payment_method) }}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">Status Pesanan</span>
<span class="info-value">
<span class="badge badge-pending"> PENDING</span>
</span>
</div>
</div>
</div>
@if($transaksi->notes)
<div class="section">
<div class="section-title">📝 Catatan Pelanggan</div>
<p style="font-size: 14px; color: #444; background: #f9f9f9; padding: 10px; border-radius: 6px;">{{ $transaksi->notes }}</p>
</div>
@endif
<div class="cta">
<a href="{{ url('/admin/transaksi/' . $transaksi->id) }}">
Lihat Detail di Dashboard
</a>
</div>
</div>
<div class="footer">
Shop Bibit Cabai Bondowoso · Notifikasi otomatis, tidak perlu dibalas.
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="id">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body style="margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:30px 0;">
<tr><td align="center">
<table width="520" cellpadding="0" cellspacing="0"
style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.08);">
{{-- Header --}}
<tr>
<td style="background:linear-gradient(135deg,#11998e 0%,#38ef7d 100%);padding:28px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">🌿 Admin - Bibit Cabai Bondowoso</h1>
<p style="color:rgba(255,255,255,0.85);margin:6px 0 0;font-size:13px;">Reset Password Administrator</p>
</td>
</tr>
{{-- Body --}}
<tr>
<td style="padding:32px 36px;">
<p style="color:#333;font-size:15px;margin:0 0 14px;">
Halo, <strong>{{ $name }}</strong>!
</p>
<p style="color:#555;font-size:13px;line-height:1.6;margin:0 0 22px;">
Kami menerima permintaan reset password untuk akun <strong>admin</strong> Anda.
Gunakan kode berikut:
</p>
{{-- OTP --}}
<div style="text-align:center;margin:0 0 24px;">
<div style="display:inline-block;background:#f0fff4;border:2px dashed #11998e;
border-radius:12px;padding:18px 36px;">
<p style="color:#11998e;font-size:40px;font-weight:bold;
letter-spacing:12px;margin:0;font-family:monospace;">{{ $otp }}</p>
</div>
<p style="color:#888;font-size:12px;margin:10px 0 0;">
Kode berlaku <strong>10 menit</strong>
</p>
</div>
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:12px 16px;margin-bottom:16px;">
<p style="color:#856404;font-size:12px;margin:0;">
⚠️ <strong>Keamanan:</strong> Jangan bagikan kode ini kepada siapapun.
Jika Anda tidak meminta reset password, segera amankan akun Anda.
</p>
</div>
</td>
</tr>
{{-- Footer --}}
<tr>
<td style="background:#f8f9fa;padding:16px 36px;text-align:center;border-top:1px solid #eee;">
<p style="color:#aaa;font-size:11px;margin:0;">
© {{ date('Y') }} Shop Bibit Cabai Bondowoso Admin System
</p>
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; padding: 20px; }
.container { background: #fff; max-width: 600px; margin: auto; border-radius: 8px; padding: 30px; }
.header { background: #198754; color: white; padding: 20px; border-radius: 6px; text-align: center; margin: -30px -30px 25px; }
.label { font-weight: bold; color: #198754; }
.row { margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 15px; }
.message-box { background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #198754; }
.footer { text-align: center; color: #aaa; font-size: 12px; margin-top: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>🌶️ Shop Bibit Cabai Bondowoso</h2>
<p>Pesan Masuk dari Contact Us</p>
</div>
<div class="row"><span class="label">👤 Nama:</span><p>{{ $data['name'] }}</p></div>
<div class="row"><span class="label">📧 Email:</span><p>{{ $data['email'] }}</p></div>
<div class="row"><span class="label">📱 No. Telepon:</span><p>{{ $data['phone'] ?? '-' }}</p></div>
<div class="row"><span class="label">📌 Subjek:</span><p>{{ $data['subject'] }}</p></div>
<div class="row">
<span class="label">💬 Pesan:</span>
<div class="message-box">{{ $data['message'] }}</div>
</div>
<div class="footer">Email dikirim otomatis dari website Shop Bibit Cabai Bondowoso</div>
</div>
</body>
</html>

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 600px; margin: auto; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { background: linear-gradient(135deg, #28a745, #20c997); color: white; padding: 30px; text-align: center; }
.header h1 { margin: 0; font-size: 26px; }
.header p { margin: 8px 0 0; opacity: 0.9; }
.icon { font-size: 50px; margin-bottom: 10px; }
.body { padding: 30px; }
.greeting { font-size: 16px; color: #333; margin-bottom: 20px; }
.tracking-box { background: #e8f5e9; border: 2px dashed #28a745; border-radius: 10px; padding: 20px; text-align: center; margin: 20px 0; }
.tracking-box .label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 1px; }
.tracking-box .number { font-size: 22px; font-weight: bold; color: #28a745; font-family: monospace; margin-top: 6px; }
.section { margin-bottom: 24px; }
.section-title { font-size: 13px; font-weight: bold; text-transform: uppercase; color: #888; letter-spacing: 1px; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 6px; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; }
.info-label { color: #555; }
.info-value { font-weight: bold; color: #222; text-align: right; }
.total-box { background: #f1f8e9; border: 2px solid #a5d6a7; border-radius: 8px; padding: 16px 20px; }
.total-final { display: flex; justify-content: space-between; font-size: 18px; font-weight: bold; color: #28a745; }
.steps { margin: 20px 0; }
.step { display: flex; align-items: flex-start; margin-bottom: 14px; }
.step-num { background: #28a745; color: white; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: bold; flex-shrink: 0; margin-right: 12px; margin-top: 2px; }
.step-text { font-size: 14px; color: #444; }
.footer { background: #f9f9f9; padding: 16px 30px; text-align: center; font-size: 12px; color: #aaa; border-top: 1px solid #eee; }
.whatsapp-btn { display: inline-block; background: #25D366; color: white; padding: 12px 28px; border-radius: 25px; text-decoration: none; font-weight: bold; margin-top: 16px; font-size: 15px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="icon">📦</div>
<h1>Pesanan Sedang Dikirim!</h1>
<p>{{ $transaksi->created_at->format('d F Y') }}</p>
</div>
<div class="body">
<p class="greeting">
Halo <strong>{{ $transaksi->customer_name }}</strong>, kabar baik! 🎉<br>
Pesanan Anda dengan invoice <strong>{{ $transaksi->invoice_number }}</strong>
sudah dalam perjalanan menuju alamat Anda.
</p>
{{-- Tracking Number --}}
@if($transaksi->tracking_number)
<div class="tracking-box">
<div class="label">Nomor Resi Pengiriman</div>
<div class="number">{{ $transaksi->tracking_number }}</div>
<div style="font-size:12px; color:#666; margin-top:8px;">
Simpan nomor resi ini untuk melacak paket Anda
</div>
</div>
@endif
{{-- Detail Pesanan --}}
<div class="section">
<div class="section-title">🌶️ Detail Pesanan</div>
@foreach($transaksi->details as $detail)
<div class="info-row">
<span class="info-label">{{ $detail->product_name }}</span>
<span class="info-value">{{ $detail->quantity }} bibit</span>
</div>
@endforeach
</div>
{{-- Alamat Pengiriman --}}
<div class="section">
<div class="section-title">📍 Dikirim ke</div>
<div style="font-size:14px; color:#444; line-height:1.8;">
{{ $transaksi->shipping_address }}<br>
Kec. {{ $transaksi->city }}, {{ $transaksi->province }}<br>
Kode Pos: {{ $transaksi->postal_code }}
</div>
</div>
{{-- Total --}}
<div class="total-box">
<div class="total-final">
<span>Total Pembayaran</span>
<span>Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</span>
</div>
</div>
{{-- Instruksi --}}
<div class="section" style="margin-top:24px;">
<div class="section-title">📋 Apa yang harus dilakukan?</div>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div class="step-text">Pantau pengiriman menggunakan nomor resi di atas</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-text">Pastikan ada orang di rumah untuk menerima paket</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text">Periksa kondisi paket saat diterima sebelum menandatangani</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-text">Jika ada kendala, hubungi kami via WhatsApp</div>
</div>
</div>
</div>
<div style="text-align:center;">
<a href="https://wa.me/62081331830561?text=Halo, saya ingin menanyakan pesanan {{ $transaksi->invoice_number }}"
class="whatsapp-btn">
💬 Hubungi via WhatsApp
</a>
</div>
</div>
<div class="footer">
Shop Bibit Cabai Bondowoso · 081331830561<br>
Email ini dikirim otomatis, tidak perlu dibalas.
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kode Verifikasi Reset Password</title>
</head>
<body style="margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:30px 0;">
<tr>
<td align="center">
<table width="560" cellpadding="0" cellspacing="0"
style="background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.08);">
{{-- Header --}}
<tr>
<td style="background:#198754;padding:30px;text-align:center;">
<h1 style="color:#ffffff;margin:0;font-size:22px;">🌱 Bibit Cabai Bondowoso</h1>
<p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">
Reset Password
</p>
</td>
</tr>
{{-- Body --}}
<tr>
<td style="padding:36px 40px;">
<p style="color:#333;font-size:16px;margin:0 0 16px;">
Halo, <strong>{{ $name }}</strong>!
</p>
<p style="color:#555;font-size:14px;line-height:1.6;margin:0 0 24px;">
Kami menerima permintaan reset password untuk akun Anda.
Gunakan kode berikut untuk memverifikasi identitas Anda:
</p>
{{-- OTP Box --}}
<div style="text-align:center;margin:0 0 28px;">
<div style="display:inline-block;background:#f0fff4;border:2px dashed #198754;
border-radius:12px;padding:20px 40px;">
<p style="color:#198754;font-size:42px;font-weight:bold;
letter-spacing:12px;margin:0;font-family:monospace;">
{{ $otp }}
</p>
</div>
<p style="color:#888;font-size:13px;margin:12px 0 0;">
Kode ini berlaku selama <strong>10 menit</strong>
</p>
</div>
<p style="color:#555;font-size:14px;line-height:1.6;margin:0 0 12px;">
Jika Anda tidak meminta reset password, abaikan email ini.
Akun Anda tetap aman.
</p>
</td>
</tr>
{{-- Footer --}}
<tr>
<td style="background:#f8f9fa;padding:20px 40px;text-align:center;border-top:1px solid #eee;">
<p style="color:#aaa;font-size:12px;margin:0;">
© {{ date('Y') }} Shop Bibit Cabai Bondowoso. Semua hak dilindungi.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -73,7 +73,7 @@
</a> </a>
</div> </div>
<div class="col-lg-6 text-center"> <div class="col-lg-6 text-center">
<img src="{{ asset('images/cabai-hero.jpg') }}" <img src="{{ asset('images/bara f1.jpeg') }}"
alt="Bibit Cabai" alt="Bibit Cabai"
class="img-fluid hero-image" class="img-fluid hero-image"
onerror="this.src='https://via.placeholder.com/400x300/28a745/ffffff?text=Bibit+Cabai+Hero'"> onerror="this.src='https://via.placeholder.com/400x300/28a745/ffffff?text=Bibit+Cabai+Hero'">
@ -102,7 +102,7 @@ class="img-fluid hero-image"
@endif @endif
<div class="overflow-hidden"> <div class="overflow-hidden">
<img src="{{ $product->image_url }}" <img src="{{ $product->image ? asset('storage/' . $product->image) : 'https://via.placeholder.com/300x250/28a745/ffffff?text=' . urlencode($product->name) }}"
class="card-img-top product-image" class="card-img-top product-image"
alt="{{ $product->name }}" alt="{{ $product->name }}"
loading="lazy" loading="lazy"
@ -111,7 +111,7 @@ class="card-img-top product-image"
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<h5 class="card-title">{{ $product->name }}</h5> <h5 class="card-title">{{ $product->name }}</h5>
<div class="price-text mb-3">{{ $product->formatted_price }}</div> <div class="price-text mb-3">Rp {{ number_format($product->price, 0, ',', '.') }}</div>
@if($product->total_sold > 0) @if($product->total_sold > 0)
<small class="text-muted mb-3"> <small class="text-muted mb-3">
@ -148,6 +148,10 @@ class="card-img-top product-image"
<i class="fas fa-shopping-cart me-1"></i> <i class="fas fa-shopping-cart me-1"></i>
Beli Sekarang Beli Sekarang
</button> </button>
<a href="{{ route('products.show', $product->id) }}" class="btn btn-outline-success w-100 mt-2">
<i class="fas fa-eye me-1"></i>
Lihat Detail Produk
</a>
@else @else
<button class="btn btn-secondary w-100" disabled> <button class="btn btn-secondary w-100" disabled>
<i class="fas fa-ban me-1"></i> <i class="fas fa-ban me-1"></i>
@ -194,7 +198,77 @@ class="card-img-top product-image"
@endif @endif
</div> </div>
</section> </section>
<!-- Semua Produk Section -->
<section class="py-5 bg-white">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold text-success">SEMUA PRODUK</h2>
<p class="lead text-muted">Pilihan lengkap bibit cabai kami</p>
</div>
<div class="row g-4">
@foreach($allProducts as $product)
<div class="col-lg-4 col-md-6">
<div class="card product-card h-100 position-relative">
<div class="overflow-hidden">
<img src="{{ $product->image ? asset('storage/' . $product->image) : 'https://via.placeholder.com/300x250/28a745/ffffff?text=' . urlencode($product->name) }}"
class="card-img-top product-image"
alt="{{ $product->name }}"
loading="lazy">
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ $product->name }}</h5>
<div class="price-text mb-3">Rp {{ number_format($product->price, 0, ',', '.') }}</div>
<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center mb-3">
<small class="text-muted">
<i class="fas fa-boxes me-1"></i>
Stok: {{ number_format($product->stock) }}
</small>
@if($product->stock > 0)
<span class="badge bg-success">Tersedia</span>
@else
<span class="badge bg-danger">Habis</span>
@endif
</div>
@if($product->stock > 0)
<div class="input-group mb-3">
<button class="btn btn-outline-success" type="button" onclick="decreaseQty('all-{{ $product->id }}')">
<i class="fas fa-minus"></i>
</button>
<input type="number" class="form-control quantity-input" id="qty-all-{{ $product->id }}" value="1" min="1" max="{{ $product->stock }}">
<button class="btn btn-outline-success" type="button" onclick="increaseQty('all-{{ $product->id }}', {{ $product->stock }})">
<i class="fas fa-plus"></i>
</button>
</div>
<button class="btn btn-success w-100" onclick="checkoutAll({{ $product->id }}, 'all-{{ $product->id }}')">
<i class="fas fa-shopping-cart me-1"></i>
Beli Sekarang
</button>
<a href="{{ route('products.show', $product->id) }}" class="btn btn-outline-success w-100 mt-2">
<i class="fas fa-eye me-1"></i>
Lihat Detail Produk
</a>
@else
<button class="btn btn-secondary w-100" disabled>
<i class="fas fa-ban me-1"></i>
Stok Habis
</button>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</section>
<!-- Features Section -->
<!-- Features Section --> <!-- Features Section -->
<section class="py-5 bg-white"> <section class="py-5 bg-white">
<div class="container"> <div class="container">
@ -214,10 +288,10 @@ class="card-img-top product-image"
<i class="fas fa-truck fa-2x"></i> <i class="fas fa-truck fa-2x"></i>
</div> </div>
<h5>Pengiriman Cepat</h5> <h5>Pengiriman Cepat</h5>
<p class="text-muted">Pengiriman ke seluruh Indonesia dengan packaging aman.</p> <p class="text-muted">Pengiriman ke seluruh kabupaten Bondowoso dengan packaging aman.</p>
</div> </div>
</div> </div>
<div class="col-lg-3 col-md-6"> <!-- <div class="col-lg-3 col-md-6">
<div class="text-center"> <div class="text-center">
<div class="bg-success text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <div class="bg-success text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">
<i class="fas fa-medal fa-2x"></i> <i class="fas fa-medal fa-2x"></i>
@ -225,7 +299,7 @@ class="card-img-top product-image"
<h5>Garansi Tumbuh</h5> <h5>Garansi Tumbuh</h5>
<p class="text-muted">Garansi bibit tumbuh atau uang kembali 100%.</p> <p class="text-muted">Garansi bibit tumbuh atau uang kembali 100%.</p>
</div> </div>
</div> </div> -->
<div class="col-lg-3 col-md-6"> <div class="col-lg-3 col-md-6">
<div class="text-center"> <div class="text-center">
<div class="bg-success text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <div class="bg-success text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">
@ -241,6 +315,10 @@ class="card-img-top product-image"
</div> </div>
<script> <script>
// Cek status login dari Laravel (true/false)
const isLoggedIn = @json(auth()->check());
const loginUrl = "{{ route('login') }}";
function increaseQty(productId, maxStock) { function increaseQty(productId, maxStock) {
const input = document.getElementById('qty-' + productId); const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value); let value = parseInt(input.value);
@ -258,8 +336,21 @@ function decreaseQty(productId) {
} }
function checkout(productId) { function checkout(productId) {
if (!isLoggedIn) {
window.location.href = loginUrl;
return;
}
const quantity = document.getElementById('qty-' + productId).value; const quantity = document.getElementById('qty-' + productId).value;
window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`; window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`;
} }
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}`;
}
</script> </script>
@endsection @endsection

View File

@ -97,7 +97,8 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
@auth @auth
<!-- Admin Dashboard Access --> {{-- Tampilkan Admin Panel HANYA jika is_admin = 1 --}}
@if(Auth::user()->is_admin)
<li class="nav-item me-2"> <li class="nav-item me-2">
<a class="nav-link admin-link admin-notification px-3 py-2 rounded" <a class="nav-link admin-link admin-notification px-3 py-2 rounded"
href="{{ route('admin.dashboard') }}" href="{{ route('admin.dashboard') }}"
@ -106,32 +107,35 @@
<span class="badge admin-badge">NEW</span> <span class="badge admin-badge">NEW</span>
</a> </a>
</li> </li>
@endif
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user me-1"></i>{{ Auth::user()->name }} <i class="fas fa-user me-1"></i>{{ Auth::user()->name }}
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
@if(Auth::user()->is_admin)
<li> <li>
<a class="dropdown-item admin-access" href="{{ route('admin.dashboard') }}"> <a class="dropdown-item admin-access" href="{{ route('admin.dashboard') }}">
<i class="fas fa-tachometer-alt me-2"></i>Dashboard Admin <i class="fas fa-tachometer-alt me-2"></i>Dashboard Admin
</a> </a>
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@endif
<li> <li>
<a class="dropdown-item" href="#"> <a class="dropdown-item" href="{{ route('profile') }}">
<i class="fas fa-user-circle me-2"></i>Profil Saya <i class="fas fa-user-circle me-2"></i>Profil Saya
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="#"> <a class="dropdown-item" href="{{ route('orders.my') }}">
<i class="fas fa-box me-2"></i>Pesanan Saya <i class="fas fa-box me-2"></i>Pesanan Saya
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="#"> <!-- <a class="dropdown-item" href="#">
<i class="fas fa-cog me-2"></i>Pengaturan <i class="fas fa-cog me-2"></i>Pengaturan
</a> </a> -->
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li> <li>
@ -170,6 +174,7 @@
</div> </div>
</nav> </nav>
<!-- Alert Messages -->
<!-- Alert Messages --> <!-- Alert Messages -->
@if(session('success')) @if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">
@ -185,9 +190,29 @@
</div> </div>
@endif @endif
<!-- Notifikasi Pesanan Customer -->
@auth
@php
$notifTerkirim = auth()->user()->unreadNotifications
->filter(fn($n) => isset($n->data['status']) && in_array($n->data['status'], ['processing', 'shipped', 'delivered', 'cancelled']))
->count();
@endphp
@if($notifTerkirim > 0)
<div id="notif-pesanan" data-count="{{ $notifTerkirim }}" class="alert alert-warning alert-dismissible fade show mb-0" role="alert">
<div class="container">
<i class="fas fa-bell me-2"></i>
Anda memiliki <strong>{{ $notifTerkirim }}</strong> update status pesanan baru.
<a href="{{ route('orders.my') }}" class="alert-link">Lihat Pesanan Saya</a>
<button type="button" class="btn-close" onclick="tutupNotif('notif_pesanan', 'notif-pesanan')"></button>
</div>
</div>
@endif
@endauth
<!-- Admin Welcome Banner (for authenticated users) --> <!-- Admin Welcome Banner (for authenticated users) -->
@auth @auth
<div class="alert alert-info alert-dismissible fade show mb-0" role="alert" style="background: linear-gradient(135deg, #17a2b8, #20c997); border: none;"> @if(Auth::user()->is_admin)
<div id="notif-welcome" class="alert alert-info alert-dismissible fade show mb-0" role="alert" style="background: linear-gradient(135deg, #17a2b8, #20c997); border: none;">
<div class="container"> <div class="container">
<div class="d-flex align-items-center justify-content-between flex-wrap"> <div class="d-flex align-items-center justify-content-between flex-wrap">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@ -198,11 +223,13 @@
<a href="{{ route('admin.dashboard') }}" class="btn btn-light btn-sm me-2"> <a href="{{ route('admin.dashboard') }}" class="btn btn-light btn-sm me-2">
<i class="fas fa-external-link-alt me-1"></i>Buka Dashboard <i class="fas fa-external-link-alt me-1"></i>Buka Dashboard
</a> </a>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" onclick="tutupNotif('notif_welcome', 'notif-welcome')"></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
@endif
@endauth @endauth
<!-- Main Content --> <!-- Main Content -->
@ -219,9 +246,11 @@
<p>Platform terbaik untuk kebutuhan bibit dan tanaman Anda.</p> <p>Platform terbaik untuk kebutuhan bibit dan tanaman Anda.</p>
<div class="mt-3"> <div class="mt-3">
@auth @auth
@if(Auth::user()->is_admin)
<a href="{{ route('admin.dashboard') }}" class="btn btn-outline-success btn-sm"> <a href="{{ route('admin.dashboard') }}" class="btn btn-outline-success btn-sm">
<i class="fas fa-tools me-1"></i>Admin Panel <i class="fas fa-tools me-1"></i>Admin Panel
</a> </a>
@endif
@else @else
<a href="{{ route('admin.login.form') }}" class="btn btn-outline-success btn-sm"> <a href="{{ route('admin.login.form') }}" class="btn btn-outline-success btn-sm">
<i class="fas fa-tools me-1"></i>Admin Panel <i class="fas fa-tools me-1"></i>Admin Panel
@ -231,16 +260,25 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h5>Hubungi Kami</h5> <h5>Hubungi Kami</h5>
<p><i class="fas fa-envelope me-2"></i>info@tabibit.com</p> <p>
<p><i class="fas fa-phone me-2"></i>+62 123 456 789</p> <a href="https://wa.me/62123456789" target="_blank" class="text-white text-decoration-none">
<p><i class="fas fa-shield-alt me-2"></i>Admin: admin@tabibit.com</p> <i class="fab fa-whatsapp me-2 text-success"></i>+62 123 456 789
</div> </a>
</p>
<p>
<a href="mailto:info@tabibit.com" class="text-white text-decoration-none">
<i class="fas fa-envelope me-2 text-success"></i>info@tabibit.com
</a>
</p>
</div>
</div> </div>
<hr> <hr>
<div class="text-center"> <div class="text-center">
<p>&copy; {{ date('Y') }} Bibit Cabai. All rights reserved. | <p>&copy; {{ date('Y') }} Bibit Cabai. All rights reserved. |
@auth @auth
@if(Auth::user()->is_admin)
<a href="{{ route('admin.dashboard') }}" class="text-success text-decoration-none">Admin Panel</a> <a href="{{ route('admin.dashboard') }}" class="text-success text-decoration-none">Admin Panel</a>
@endif
@else @else
<a href="{{ route('admin.login.form') }}" class="text-success text-decoration-none">Admin Panel</a> <a href="{{ route('admin.login.form') }}" class="text-success text-decoration-none">Admin Panel</a>
@endauth @endauth
@ -254,30 +292,52 @@
<!-- Custom Scripts --> <!-- Custom Scripts -->
<script> <script>
// Auto-hide alerts
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert:not(.alert-info)'); // Notif pesanan - cek berdasarkan jumlah notif
alerts.forEach(function(alert) { var pesananEl = document.getElementById('notif-pesanan');
setTimeout(function() { if (pesananEl) {
if (alert.parentNode) { var count = pesananEl.getAttribute('data-count');
const bsAlert = new bootstrap.Alert(alert); var savedCount = localStorage.getItem('notif_pesanan_count');
bsAlert.close(); if (savedCount === count) {
pesananEl.style.display = 'none';
} }
}
// Notif welcome - pakai sessionStorage (reset setiap login baru)
if (sessionStorage.getItem('notif_welcome') === 'ditutup') {
var el = document.getElementById('notif-welcome');
if (el) el.style.display = 'none';
}
// Auto-hide semua notif setelah 5 detik
setTimeout(function() {
['notif-pesanan', 'notif-welcome'].forEach(function(elId) {
var el = document.getElementById(elId);
if (el && el.style.display !== 'none') {
el.style.transition = 'opacity 0.8s ease';
el.style.opacity = '0';
setTimeout(function() { el.style.display = 'none'; }, 800);
}
});
}, 5000); }, 5000);
}); });
// Hide admin banner after 10 seconds function tutupNotif(key, elId) {
const adminBanner = document.querySelector('.alert-info'); if (key === 'notif_pesanan') {
if (adminBanner) { var el = document.getElementById(elId);
setTimeout(function() { var count = el ? el.getAttribute('data-count') : '0';
if (adminBanner.parentNode) { localStorage.setItem('notif_pesanan_count', count);
const bsAlert = new bootstrap.Alert(adminBanner); } else {
bsAlert.close(); sessionStorage.setItem(key, 'ditutup'); // ganti localStorage -> sessionStorage
} }
}, 10000); var el = document.getElementById(elId);
if (el) {
el.style.transition = 'opacity 0.3s';
el.style.opacity = '0';
setTimeout(function() { el.style.display = 'none'; }, 300);
} }
}); }
</script> </script>
@yield('scripts') @yield('scripts')
</body> </body>

View File

@ -121,6 +121,10 @@ class="form-control quantity-input text-center"
<i class="fas fa-shopping-cart me-1"></i> <i class="fas fa-shopping-cart me-1"></i>
Beli Sekarang Beli Sekarang
</button> </button>
<a href="{{ route('products.show', $product->id) }}" class="btn btn-outline-success w-100 mt-2">
<i class="fas fa-eye me-1"></i>
Lihat Detail Produk
</a>
@else @else
<button class="btn btn-secondary w-100" disabled> <button class="btn btn-secondary w-100" disabled>
<i class="fas fa-ban me-1"></i> <i class="fas fa-ban me-1"></i>
@ -150,6 +154,59 @@ class="form-control quantity-input text-center"
</div> </div>
@endif @endif
</div> </div>
{{-- Script dipindah ke dalam @section('content') agar terbaca oleh browser --}}
<script>
const isLoggedIn = @json(auth()->check());
const loginUrl = "{{ route('login') }}";
function increaseQty(productId, maxStock) {
const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value) || 1;
if (value < maxStock) {
input.value = value + 1;
} else {
alert('Stok maksimal: ' + maxStock);
}
}
function decreaseQty(productId) {
const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value) || 1;
if (value > 1) {
input.value = value - 1;
}
}
function checkout(productId) {
if (!isLoggedIn) {
window.location.href = loginUrl;
return;
}
const input = document.getElementById('qty-' + productId);
if (!input) {
alert('Terjadi kesalahan, coba refresh halaman!');
return;
}
const quantity = parseInt(input.value) || 1;
const max = parseInt(input.getAttribute('max'));
if (isNaN(quantity) || quantity < 1) {
alert('Jumlah minimal 1!');
return;
}
if (quantity > max) {
alert('Jumlah melebihi stok tersedia (' + max + ')!');
return;
}
window.location.href = '/checkout?product_id=' + productId + '&quantity=' + quantity;
}
</script>
@endsection @endsection
@push('styles') @push('styles')
@ -244,28 +301,3 @@ class="form-control quantity-input text-center"
} }
</style> </style>
@endpush @endpush
@push('scripts')
<script>
function increaseQty(productId, maxStock) {
const input = document.getElementById('qty-' + productId);
let value = parseInt(input.value);
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;
}
}
function checkout(productId) {
const quantity = document.getElementById('qty-' + productId).value;
window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`;
}
</script>
@endpush

View File

@ -0,0 +1,156 @@
@extends('layouts.app')
@section('title', $product->name . ' - Shop Bibit Cabai Bondowoso')
@section('styles')
<style>
.product-detail-image {
width: 100%;
max-height: 450px;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.price-text {
font-size: 2rem;
font-weight: bold;
color: #28a745;
}
.info-card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 15px rgba(0,0,0,0.08);
}
.quantity-input {
width: 80px;
text-align: center;
}
.badge-stock {
font-size: 0.9rem;
padding: 6px 14px;
border-radius: 20px;
}
</style>
@endsection
@section('content')
<div class="container py-5">
{{-- Breadcrumb --}}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('home') }}" class="text-success">Home</a></li>
<li class="breadcrumb-item active">{{ $product->name }}</li>
</ol>
</nav>
<div class="row g-5">
{{-- Gambar Produk --}}
<div class="col-lg-6">
<img src="{{ $product->image ? asset('storage/' . $product->image) : 'https://via.placeholder.com/500x450/28a745/ffffff?text=' . urlencode($product->name) }}"
alt="{{ $product->name }}"
class="product-detail-image"
onerror="this.src='https://via.placeholder.com/500x450/28a745/ffffff?text={{ urlencode($product->name) }}'">
</div>
{{-- Info Produk --}}
<div class="col-lg-6">
<h1 class="fw-bold mb-2">{{ $product->name }}</h1>
@if($product->category)
<span class="badge bg-success-subtle text-success border border-success mb-3">
{{ $product->category }}
</span>
@endif
<div class="price-text mb-3">
Rp {{ number_format($product->price, 0, ',', '.') }}
</div>
{{-- Status Stok --}}
<div class="d-flex align-items-center gap-3 mb-4">
<span class="text-muted">
<i class="fas fa-boxes me-1"></i>Stok: <strong>{{ number_format($product->stock) }}</strong>
</span>
@if($product->stock > 0)
<span class="badge bg-success badge-stock">Tersedia</span>
@else
<span class="badge bg-danger badge-stock">Stok Habis</span>
@endif
</div>
{{-- Total Terjual --}}
@if(isset($product->sold) && $product->sold > 0)
<p class="text-muted mb-4">
<i class="fas fa-chart-line me-1 text-success"></i>
Terjual: <strong>{{ number_format($product->sold) }} bibit</strong>
</p>
@endif
{{-- Aksi Beli --}}
@if($product->stock > 0)
<div class="card info-card p-4 mb-4">
<label class="form-label fw-semibold">Jumlah</label>
<div class="input-group mb-3" style="max-width: 200px;">
<button class="btn btn-outline-success" type="button" onclick="decreaseQty()">
<i class="fas fa-minus"></i>
</button>
<input type="number" class="form-control quantity-input" id="qty-detail"
value="1" min="1" max="{{ $product->stock }}">
<button class="btn btn-outline-success" type="button" onclick="increaseQty({{ $product->stock }})">
<i class="fas fa-plus"></i>
</button>
</div>
<button class="btn btn-success btn-lg w-100" onclick="checkout({{ $product->id }})">
<i class="fas fa-shopping-cart me-2"></i>Beli Sekarang
</button>
</div>
@else
<button class="btn btn-secondary btn-lg w-100 mb-4" disabled>
<i class="fas fa-ban me-2"></i>Stok Habis
</button>
@endif
{{-- Deskripsi --}}
@if($product->description)
<div class="card info-card p-4">
<h5 class="fw-bold mb-3"><i class="fas fa-info-circle me-2 text-success"></i>Deskripsi Produk</h5>
<p class="text-muted mb-0" style="line-height: 1.8;">{{ $product->description }}</p>
</div>
@endif
</div>
</div>
{{-- Tombol Kembali --}}
<div class="mt-5">
<a href="{{ route('home') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Beranda
</a>
</div>
</div>
<script>
const isLoggedIn = @json(auth()->check());
const loginUrl = "{{ route('login') }}";
function increaseQty(maxStock) {
const input = document.getElementById('qty-detail');
if (parseInt(input.value) < maxStock) input.value = parseInt(input.value) + 1;
}
function decreaseQty() {
const input = document.getElementById('qty-detail');
if (parseInt(input.value) > 1) input.value = parseInt(input.value) - 1;
}
function checkout(productId) {
if (!isLoggedIn) {
window.location.href = loginUrl;
return;
}
const quantity = document.getElementById('qty-detail').value;
window.location.href = `/checkout?product_id=${productId}&quantity=${quantity}`;
}
</script>
@endsection

View File

@ -0,0 +1,297 @@
@extends('layouts.app')
@section('title', 'Batalkan Pesanan - Bibit Cabai')
@section('styles')
<style>
body { background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
.page-hero {
background: linear-gradient(135deg, #28a745, #20c997);
padding: 35px 0; color: white;
}
.cancel-wrap {
max-width: 680px; margin: 0 auto; padding: 30px 15px;
}
.detail-card {
background: white; border-radius: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
padding: 25px; margin-bottom: 20px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: #333;
margin-bottom: 18px; padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.order-summary-box {
background: #f8f9fa; border-radius: 12px;
padding: 16px 18px; margin-bottom: 22px;
border-left: 4px solid #dc3545;
}
.invoice-num { font-size: 0.8rem; color: #6c757d; font-family: monospace; }
.product-img {
width: 48px; height: 48px; border-radius: 8px;
object-fit: cover; border: 1px solid #e9ecef; flex-shrink: 0;
}
.product-placeholder {
width: 48px; height: 48px; border-radius: 8px;
background: #f0f0f0; display: flex; flex-shrink: 0;
align-items: center; justify-content: center; color: #aaa;
}
.reason-list { display: flex; flex-direction: column; gap: 10px; }
.reason-item input[type="radio"] { display: none; }
.reason-label {
display: flex; align-items: center; gap: 14px;
padding: 13px 16px; border-radius: 10px;
border: 2px solid #e9ecef; cursor: pointer;
transition: all 0.18s; font-size: 0.92rem; background: white;
user-select: none;
}
.reason-label:hover { border-color: #dc3545; background: #fff8f8; }
.reason-item input[type="radio"]:checked + .reason-label {
border-color: #dc3545; background: #fff0f0;
color: #c0392b; font-weight: 600;
}
.radio-dot {
width: 20px; height: 20px; border-radius: 50%;
border: 2px solid #dee2e6; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
transition: all 0.18s;
}
.reason-item input[type="radio"]:checked + .reason-label .radio-dot {
border-color: #dc3545; background: #dc3545;
}
.reason-item input[type="radio"]:checked + .reason-label .radio-dot::after {
content: ''; width: 8px; height: 8px;
border-radius: 50%; background: white; display: block;
}
.warning-box {
background: #fff3cd; border: 1px solid #ffc107;
border-radius: 10px; padding: 13px 16px;
color: #856404; font-size: 0.87rem; line-height: 1.5;
}
.btn-submit-cancel {
width: 100%; padding: 13px;
background: #dc3545; color: white; border: none;
border-radius: 10px; font-weight: 700; font-size: 1rem;
transition: all 0.2s; cursor: pointer; letter-spacing: 0.3px;
}
.btn-submit-cancel:hover {
background: #c82333;
box-shadow: 0 4px 14px rgba(220,53,69,0.35);
transform: translateY(-1px);
}
.btn-kembali {
display: block; text-align: center; margin-top: 13px;
color: #6c757d; font-size: 0.88rem; text-decoration: none;
}
.btn-kembali:hover { color: #333; text-decoration: underline; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-5px); }
80% { transform: translateX(5px); }
}
</style>
@endsection
@section('content')
<div class="page-hero">
<div class="container d-flex align-items-center gap-3">
<a href="{{ route('orders.detail', $transaksi->id) }}" class="btn btn-outline-light btn-sm">
<i class="fas fa-arrow-left"></i>
</a>
<div>
<h5 class="mb-0"><i class="fas fa-times-circle me-2"></i>Batalkan Pesanan</h5>
<small class="font-monospace">{{ $transaksi->invoice_number }}</small>
</div>
</div>
</div>
<div class="cancel-wrap">
<div class="detail-card">
<div class="section-title">
<i class="fas fa-receipt me-2 text-danger"></i>Pesanan yang Akan Dibatalkan
</div>
<div class="order-summary-box">
<div class="d-flex justify-content-between mb-2">
<span class="invoice-num"><i class="fas fa-receipt me-1"></i>{{ $transaksi->invoice_number }}</span>
<small class="text-muted">{{ $transaksi->created_at->format('d M Y, H:i') }}</small>
</div>
@foreach($transaksi->details->take(2) as $detail)
<div class="d-flex align-items-center gap-3 mb-2">
@if($detail->product && $detail->product->image)
<img src="{{ asset('storage/' . $detail->product->image) }}" class="product-img" alt="">
@else
<div class="product-placeholder"><i class="fas fa-seedling"></i></div>
@endif
<div class="flex-grow-1">
<div class="fw-bold" style="font-size:0.88rem;">{{ $detail->product_name }}</div>
<small class="text-muted">{{ $detail->quantity }} × Rp{{ number_format($detail->price, 0, ',', '.') }}</small>
</div>
<div class="fw-bold text-danger" style="font-size:0.88rem;">
Rp{{ number_format($detail->subtotal, 0, ',', '.') }}
</div>
</div>
@endforeach
@if($transaksi->details->count() > 2)
<small class="text-muted">
<i class="fas fa-plus me-1"></i>{{ $transaksi->details->count() - 2 }} produk lainnya
</small>
@endif
<div class="d-flex justify-content-between border-top pt-2 mt-2">
<span class="fw-bold">Total Pembayaran</span>
<span class="fw-bold text-danger">Rp{{ number_format($transaksi->total_amount, 0, ',', '.') }}</span>
</div>
</div>
<div class="warning-box mb-4">
<i class="fas fa-exclamation-triangle me-2"></i>
@if($transaksi->isAutoCancel())
<strong>Perhatian!</strong> Pesanan ini menggunakan <strong>COD</strong> dan belum diproses,
sehingga akan <strong>langsung dibatalkan</strong> tanpa perlu menunggu konfirmasi admin.
@else
<strong>Perhatian!</strong> Pengajuan pembatalan ini akan dikonfirmasi oleh admin terlebih dahulu.
Pesanan <strong>tidak langsung dibatalkan</strong> kamu akan mendapat notifikasi setelah admin merespons.
@endif
</div>
<form action="{{ route('orders.cancel.store', $transaksi->id) }}" method="POST" id="cancelForm">
@csrf
<div class="section-title">
<i class="fas fa-question-circle me-2 text-danger"></i>Pilih Alasan Pembatalan
</div>
@error('reason')
<div class="alert alert-danger py-2 mb-3" style="border-radius:8px; font-size:0.87rem;">
<i class="fas fa-exclamation-circle me-1"></i>{{ $message }}
</div>
@enderror
<div class="reason-list mb-4">
@foreach($reasons as $value => $label)
<div class="reason-item">
<input type="radio" name="reason"
id="reason_{{ $loop->index }}"
value="{{ $value }}"
{{ old('reason') == $value ? 'checked' : '' }}>
<label class="reason-label" for="reason_{{ $loop->index }}">
<span class="radio-dot"></span>
{{ $label }}
</label>
</div>
@endforeach
</div>
<div class="mb-4">
<label class="form-label fw-semibold" style="font-size:0.92rem;">
Keterangan Tambahan
<span class="text-muted fw-normal">(opsional)</span>
</label>
<textarea name="description"
class="form-control @error('description') is-invalid @enderror"
rows="3" maxlength="500"
placeholder="Ceritakan lebih detail alasanmu (opsional)...">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="text-muted">Maks. 500 karakter</small>
</div>
<button type="button" class="btn-submit-cancel" id="btnSubmitCancel">
<i class="fas fa-paper-plane me-2"></i>Kirim Pengajuan Pembatalan
</button>
</form>
<a href="{{ route('orders.detail', $transaksi->id) }}" class="btn-kembali">
<i class="fas fa-arrow-left me-1"></i>Batal, kembali ke detail pesanan
</a>
</div>
</div>
{{-- Modal Konfirmasi --}}
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:400px;">
<div class="modal-content border-0" style="border-radius:16px; overflow:hidden;">
<div class="modal-body text-center p-4">
<div class="mb-3" style="font-size:2.8rem;">😔</div>
<h5 class="fw-bold mb-2">Yakin ingin membatalkan?</h5>
<p class="text-muted mb-4" style="font-size:0.88rem; line-height:1.5;">
@if($transaksi->isAutoCancel())
Pesanan COD ini akan <strong>langsung dibatalkan</strong> sekarang juga.
@else
Pengajuan akan dikirim ke admin untuk ditinjau.
Pesanan tidak langsung dibatalkan sampai admin menyetujui.
@endif
</p>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-outline-secondary flex-fill"
data-bs-dismiss="modal">
Tidak Jadi
</button>
<button type="button"
class="btn btn-danger flex-fill"
id="btnConfirmSubmit">
Ya, Ajukan
</button>
</div>
</div>
</div>
</div>
</div>
{{-- Script langsung di dalam section content --}}
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('btnSubmitCancel').addEventListener('click', function () {
const selected = document.querySelector('input[name="reason"]:checked');
if (!selected) {
const list = document.querySelector('.reason-list');
list.style.animation = 'none';
list.offsetHeight;
list.style.animation = 'shake 0.4s ease';
alert('Pilih salah satu alasan pembatalan terlebih dahulu.');
return;
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
modal.show();
});
document.getElementById('btnConfirmSubmit').addEventListener('click', function () {
document.getElementById('cancelForm').submit();
});
});
</script>
@endsection

View File

@ -0,0 +1,245 @@
@extends('layouts.app')
@section('title', 'Profil Saya - Bibit Cabai')
@section('styles')
<style>
body { background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
.profile-hero {
background: linear-gradient(135deg, #28a745, #20c997);
padding: 40px 0 80px;
color: white;
}
.avatar-circle {
width: 90px; height: 90px; border-radius: 50%;
background: rgba(255,255,255,0.3);
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; font-weight: bold; color: white;
border: 4px solid rgba(255,255,255,0.5);
margin: 0 auto 15px;
}
.profile-card {
background: white;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
margin-top: -50px;
padding: 30px;
}
.nav-profile .nav-link {
color: #6c757d;
border-radius: 8px;
padding: 10px 16px;
font-weight: 500;
}
.nav-profile .nav-link.active,
.nav-profile .nav-link:hover {
background: #e8f5e9;
color: #28a745;
}
.nav-profile .nav-link i { width: 20px; }
.form-control:focus {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40,167,69,0.2);
}
.btn-green {
background: linear-gradient(45deg, #28a745, #20c997);
border: none; color: white; font-weight: 600;
padding: 10px 25px; border-radius: 8px;
}
.btn-green:hover { opacity: 0.9; color: white; }
.section-title {
font-size: 1.1rem; font-weight: 700;
color: #333; margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.info-badge {
background: #f0fff4; border: 1px solid #b7ebc8;
border-radius: 8px; padding: 10px 15px;
font-size: 0.85rem; color: #2d6a4f;
margin-bottom: 20px;
}
</style>
@endsection
@section('content')
{{-- Hero --}}
<div class="profile-hero text-center">
<div class="avatar-circle">{{ strtoupper(substr(auth()->user()->name, 0, 1)) }}</div>
<h4 class="mb-1">{{ auth()->user()->name }}</h4>
<small>{{ auth()->user()->email }}</small>
</div>
<div class="container py-4">
<div class="row g-4">
{{-- Sidebar Menu --}}
<div class="col-md-3">
<div class="profile-card p-0 overflow-hidden">
<div class="p-3 border-bottom">
<small class="text-muted text-uppercase fw-bold" style="font-size:0.7rem;">Menu</small>
</div>
<nav class="nav flex-column nav-profile p-2">
<a href="{{ route('profile') }}" class="nav-link active">
<i class="fas fa-user me-2"></i>Profil Saya
</a>
<a href="{{ route('orders.my') }}" class="nav-link">
<i class="fas fa-shopping-bag me-2"></i>Pesanan Saya
</a>
<hr class="my-2">
<a href="{{ route('home') }}" class="nav-link text-muted">
<i class="fas fa-home me-2"></i>Kembali ke Beranda
</a>
</nav>
</div>
</div>
{{-- Konten --}}
<div class="col-md-9">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Form Edit Profil --}}
<div class="profile-card mb-4">
<div class="section-title"><i class="fas fa-user-edit me-2 text-success"></i>Edit Profil</div>
<div class="info-badge">
<i class="fas fa-info-circle me-2"></i>
Email tidak dapat diubah melalui halaman ini. Email harus menggunakan <strong>
<form method="POST" action="{{ route('profile.update') }}">
@csrf
<div class="mb-3">
<label class="form-label fw-bold">Nama Lengkap</label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', auth()->user()->name) }}" required>
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-bold">Email</label>
<input type="email" class="form-control bg-light" value="{{ auth()->user()->email }}" disabled>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Nomor Telepon</label>
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror"
value="{{ old('phone', auth()->user()->phone ?? '+62') }}"
placeholder="+62812xxxxxxxx">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-bold">Alamat</label>
<textarea name="address" rows="4"
class="form-control @error('address') is-invalid @enderror"
style="min-height:100px; resize:vertical;"
placeholder="Contoh: Jl. Mawar No. 10, RT 02/RW 03, Kelurahan Sumbersari, Jember">{{ old('address', auth()->user()->address) }}</textarea>
@error('address')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<button type="submit" class="btn btn-green">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</form>
</div>
{{-- Form Ganti Password --}}
<div class="profile-card">
<div class="section-title"><i class="fas fa-lock me-2 text-success"></i>Ganti Password</div>
<form method="POST" action="{{ route('profile.password') }}">
@csrf
<div class="mb-3">
<label class="form-label fw-bold">Password Lama</label>
<div class="input-group">
<input type="password" name="current_password" id="cur_pass"
class="form-control @error('current_password') is-invalid @enderror"
placeholder="Masukkan password lama" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('cur_pass','eye0')">
<i class="fas fa-eye" id="eye0"></i>
</button>
@error('current_password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Password Baru</label>
<div class="input-group">
<input type="password" name="password" id="new_pass"
class="form-control @error('password') is-invalid @enderror"
placeholder="Minimal 8 karakter" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('new_pass','eye1')">
<i class="fas fa-eye" id="eye1"></i>
</button>
</div>
@error('password')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Konfirmasi Password</label>
<div class="input-group">
<input type="password" name="password_confirmation" id="conf_pass"
class="form-control" placeholder="Ulangi password baru" required>
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword('conf_pass','eye2')">
<i class="fas fa-eye" id="eye2"></i>
</button>
</div>
</div>
</div>
<button type="submit" class="btn btn-green">
<i class="fas fa-key me-2"></i>Ganti Password
</button>
</form>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function togglePassword(fieldId, iconId) {
const field = document.getElementById(fieldId);
const icon = document.getElementById(iconId);
if (field.type === 'password') {
field.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
field.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
@endsection

View File

@ -0,0 +1,444 @@
@extends('layouts.app')
@section('title', 'Detail Pesanan - Bibit Cabai')
@section('styles')
<style>
body { background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
.page-hero {
background: linear-gradient(135deg, #28a745, #20c997);
padding: 35px 0; color: white;
}
.detail-card {
background: white; border-radius: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
padding: 25px; margin-bottom: 20px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: #333;
margin-bottom: 18px; padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.order-status {
padding: 6px 16px; border-radius: 20px;
font-size: 0.85rem; font-weight: 600; display: inline-block;
}
.status-pending { background: #fff3cd; color: #856404; }
.status-processing { background: #cce5ff; color: #004085; }
.status-shipped { background: #d1ecf1; color: #0c5460; }
.status-delivered { background: #d4edda; color: #155724; }
.status-cancelled { background: #f8d7da; color: #721c24; }
.payment-paid { background: #d4edda; color: #155724; }
.payment-pending { background: #fff3cd; color: #856404; }
.info-row { display: flex; padding: 10px 0; border-bottom: 1px solid #f8f9fa; }
.info-row:last-child { border-bottom: none; }
.info-label { width: 160px; color: #6c757d; font-size: 0.9rem; flex-shrink: 0; }
.info-value { flex: 1; font-weight: 500; color: #333; }
.product-img {
width: 60px; height: 60px; border-radius: 10px;
object-fit: cover; border: 1px solid #e9ecef;
}
.product-img-placeholder {
width: 60px; height: 60px; border-radius: 10px;
background: #f0f0f0; display: flex;
align-items: center; justify-content: center;
color: #aaa; font-size: 1.3rem;
}
.timeline { position: relative; padding-left: 25px; }
.timeline::before {
content: ''; position: absolute; left: 8px; top: 0; bottom: 0;
width: 2px; background: #e9ecef;
}
.timeline-item { position: relative; margin-bottom: 20px; }
.timeline-item::before {
content: ''; position: absolute; left: -21px; top: 4px;
width: 12px; height: 12px; border-radius: 50%;
background: #dee2e6; border: 2px solid white;
}
.timeline-item.done::before { background: #28a745; }
.timeline-item.cancelled::before { background: #dc3545; }
.total-row {
display: flex; justify-content: space-between;
padding: 8px 0; font-size: 0.9rem;
}
.total-row.grand {
font-size: 1.1rem; font-weight: 700;
color: #28a745; border-top: 2px solid #f0f0f0;
padding-top: 12px; margin-top: 4px;
}
/* ── Card pembatalan ── */
.cancel-action-card {
border-radius: 14px; padding: 20px;
border: 1.5px dashed #f5c6cb;
background: #fff5f5; margin-bottom: 20px;
}
.cancel-status-card {
border-radius: 14px; padding: 20px;
margin-bottom: 20px;
}
.cancel-status-card.pending { background: #fffbf0; border: 1.5px solid #ffc107; }
.cancel-status-card.approved { background: #fff5f5; border: 1.5px solid #dc3545; }
.cancel-status-card.rejected { background: #f0fff4; border: 1.5px solid #28a745; }
.cancel-status-badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 14px; border-radius: 20px;
font-size: 0.78rem; font-weight: 700;
margin-bottom: 12px;
}
.cancel-status-badge.pending { background: #fff3cd; color: #856404; }
.cancel-status-badge.approved { background: #f8d7da; color: #721c24; }
.cancel-status-badge.rejected { background: #d4edda; color: #155724; }
.cancel-detail-row {
display: flex; gap: 10px; font-size: 0.87rem;
padding: 6px 0; border-bottom: 1px solid rgba(0,0,0,0.05);
}
.cancel-detail-row:last-child { border-bottom: none; }
.cancel-detail-label { width: 120px; color: #6c757d; flex-shrink: 0; }
.cancel-detail-value { flex: 1; color: #333; font-weight: 500; }
.admin-note-box {
border-radius: 8px; padding: 10px 14px;
font-size: 0.85rem; margin-top: 8px;
}
.admin-note-box.approved { background: #fff0f0; border-left: 3px solid #dc3545; }
.admin-note-box.rejected { background: #f0fff4; border-left: 3px solid #28a745; }
/* Tombol Batalkan */
.btn-batalkan {
display: inline-flex; align-items: center; gap: 8px;
background: #dc3545; color: white;
border: none; padding: 10px 22px; border-radius: 10px;
font-weight: 600; font-size: 0.9rem;
transition: all 0.2s; text-decoration: none; cursor: pointer;
}
.btn-batalkan:hover {
background: #c82333; color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220,53,69,0.3);
text-decoration: none;
}
</style>
@endsection
@section('content')
<div class="page-hero">
<div class="container d-flex align-items-center gap-3">
<a href="{{ route('orders.my') }}" class="btn btn-outline-light btn-sm">
<i class="fas fa-arrow-left"></i>
</a>
<div>
<h5 class="mb-0">Detail Pesanan</h5>
<small class="font-monospace">{{ $order->invoice_number }}</small>
</div>
</div>
</div>
<div class="container py-4">
{{-- Flash Message --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert" style="border-radius:12px;">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert" style="border-radius:12px;">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="row g-4">
{{-- Kolom Kiri --}}
<div class="col-md-8">
{{-- Status Pesanan --}}
<div class="detail-card">
<div class="section-title"><i class="fas fa-info-circle me-2 text-success"></i>Status Pesanan</div>
<div class="d-flex gap-3 mb-4 flex-wrap">
<div>
<small class="text-muted d-block">Status Pesanan</small>
<span class="order-status status-{{ $order->order_status }}">
@switch($order->order_status)
@case('pending') <i class="fas fa-clock me-1"></i>Menunggu Konfirmasi @break
@case('processing') <i class="fas fa-cog me-1"></i>Sedang Diproses @break
@case('shipped') <i class="fas fa-truck me-1"></i>Sedang Dikirim @break
@case('delivered') <i class="fas fa-check-circle me-1"></i>Pesanan Selesai @break
@case('cancelled') <i class="fas fa-times-circle me-1"></i>Dibatalkan @break
@endswitch
</span>
</div>
<div>
<small class="text-muted d-block">Status Pembayaran</small>
<span class="order-status {{ $order->payment_status === 'paid' ? 'payment-paid' : 'payment-pending' }}">
@if($order->payment_status === 'paid')
<i class="fas fa-check me-1"></i>Lunas
@else
<i class="fas fa-hourglass me-1"></i>Belum Dibayar
@endif
</span>
</div>
</div>
@php
$statuses = ['pending', 'processing', 'shipped', 'delivered'];
$currentIdx = array_search($order->order_status, $statuses);
$isCancelled = $order->order_status === 'cancelled';
@endphp
<div class="timeline">
@if($isCancelled)
<div class="timeline-item done">
<strong>Pesanan Diterima</strong>
<div class="text-muted" style="font-size:0.82rem;">{{ $order->created_at->format('d M Y, H:i') }}</div>
</div>
<div class="timeline-item cancelled">
<strong class="text-danger">Pesanan Dibatalkan</strong>
@if($order->cancellation && $order->cancellation->reviewed_at)
<div class="text-muted" style="font-size:0.82rem;">{{ $order->cancellation->reviewed_at->format('d M Y, H:i') }}</div>
@endif
</div>
@else
<div class="timeline-item {{ $currentIdx >= 0 ? 'done' : '' }}">
<strong>Pesanan Diterima</strong>
<div class="text-muted" style="font-size:0.82rem;">{{ $order->created_at->format('d M Y, H:i') }}</div>
</div>
<div class="timeline-item {{ $currentIdx >= 1 ? 'done' : '' }}">
<strong>Sedang Diproses</strong>
<div class="text-muted" style="font-size:0.82rem;">Admin sedang mempersiapkan pesanan</div>
</div>
<div class="timeline-item {{ $currentIdx >= 2 ? 'done' : '' }}">
<strong>Dikirim</strong>
<div class="text-muted" style="font-size:0.82rem;">Pesanan dalam perjalanan</div>
</div>
<div class="timeline-item {{ $currentIdx >= 3 ? 'done' : '' }}">
<strong>Selesai</strong>
<div class="text-muted" style="font-size:0.82rem;">Pesanan telah diterima</div>
</div>
@endif
</div>
</div>
{{-- ── TOMBOL BATALKAN / STATUS PENGAJUAN ── --}}
@php
$isCancellable = in_array($order->order_status, ['pending','processing'])
&& !$order->cancellation;
$cancellation = $order->cancellation;
@endphp
{{-- A) Bisa dibatalkan tampilkan tombol --}}
@if($isCancellable)
<div class="cancel-action-card">
<div class="d-flex align-items-start gap-3">
<div style="font-size:1.6rem; line-height:1;">⚠️</div>
<div class="flex-grow-1">
<div class="fw-bold mb-1" style="font-size:0.95rem;">Ingin membatalkan pesanan ini?</div>
<p class="text-muted mb-3" style="font-size:0.85rem; line-height:1.5;">
Pengajuan pembatalan akan dikirim ke admin untuk ditinjau terlebih dahulu.
Pesanan <strong>tidak langsung dibatalkan</strong> sampai admin menyetujui.
</p>
<a href="{{ route('orders.cancel', $order->id) }}" class="btn-batalkan">
<i class="fas fa-times-circle"></i>Batalkan Pesanan
</a>
</div>
</div>
</div>
@endif
{{-- B) Sudah ada pengajuan tampilkan status --}}
@if($cancellation)
<div class="cancel-status-card {{ $cancellation->status }}">
<div class="cancel-status-badge {{ $cancellation->status }}">
@if($cancellation->status === 'pending')
<i class="fas fa-clock"></i> Pengajuan Pembatalan Menunggu Konfirmasi Admin
@elseif($cancellation->status === 'approved')
<i class="fas fa-check-circle"></i> Pembatalan Disetujui
@else
<i class="fas fa-times-circle"></i> Pengajuan Pembatalan Ditolak
@endif
</div>
<div class="cancel-detail-row">
<span class="cancel-detail-label">Alasan</span>
<span class="cancel-detail-value">{{ $cancellation->reason }}</span>
</div>
@if($cancellation->description)
<div class="cancel-detail-row">
<span class="cancel-detail-label">Keterangan</span>
<span class="cancel-detail-value">{{ $cancellation->description }}</span>
</div>
@endif
<div class="cancel-detail-row">
<span class="cancel-detail-label">Diajukan</span>
<span class="cancel-detail-value">{{ $cancellation->created_at->format('d M Y, H:i') }}</span>
</div>
@if($cancellation->status === 'pending')
<p class="text-muted mt-2 mb-0" style="font-size:0.82rem;">
<i class="fas fa-info-circle me-1"></i>
Admin akan segera meninjau permintaanmu. Harap tunggu konfirmasi.
</p>
@endif
@if($cancellation->admin_note)
<div class="admin-note-box {{ $cancellation->status }}">
<small class="text-muted d-block mb-1 fw-bold">Catatan dari Admin:</small>
{{ $cancellation->admin_note }}
</div>
@endif
@if($cancellation->status === 'rejected')
<p class="text-muted mt-2 mb-0" style="font-size:0.82rem;">
<i class="fas fa-info-circle me-1"></i>
Pesanan tetap berjalan seperti biasa. Silakan tunggu pengiriman pesananmu.
</p>
@endif
</div>
@endif
{{-- Produk Dipesan --}}
<div class="detail-card">
<div class="section-title"><i class="fas fa-seedling me-2 text-success"></i>Produk Dipesan</div>
@foreach($order->details as $detail)
<div class="d-flex align-items-center gap-3 mb-3 pb-3 border-bottom">
@if($detail->product && $detail->product->image)
<img src="{{ asset('storage/' . $detail->product->image) }}"
class="product-img" alt="{{ $detail->product_name }}">
@else
<div class="product-img-placeholder">
<i class="fas fa-seedling"></i>
</div>
@endif
<div class="flex-grow-1">
<div class="fw-bold">{{ $detail->product_name }}</div>
<small class="text-muted">{{ $detail->quantity }} x Rp{{ number_format($detail->price, 0, ',', '.') }}</small>
</div>
<div class="text-end fw-bold">
Rp{{ number_format($detail->subtotal, 0, ',', '.') }}
</div>
</div>
@endforeach
<div class="mt-3">
<div class="total-row">
<span class="text-muted">Subtotal</span>
<span>Rp{{ number_format($order->subtotal, 0, ',', '.') }}</span>
</div>
<div class="total-row">
<span class="text-muted">Ongkos Kirim</span>
<span>Rp{{ number_format($order->shipping_cost, 0, ',', '.') }}</span>
</div>
<div class="total-row grand">
<span>Total Pembayaran</span>
<span>Rp{{ number_format($order->total_amount, 0, ',', '.') }}</span>
</div>
</div>
</div>
</div>
{{-- Kolom Kanan --}}
<div class="col-md-4">
<div class="detail-card">
<div class="section-title"><i class="fas fa-map-marker-alt me-2 text-success"></i>Alamat Pengiriman</div>
<div class="info-row">
<div class="info-label">Nama</div>
<div class="info-value">{{ $order->customer_name }}</div>
</div>
<div class="info-row">
<div class="info-label">Telepon</div>
<div class="info-value">{{ $order->customer_phone }}</div>
</div>
<div class="info-row">
<div class="info-label">Alamat</div>
<div class="info-value">{{ $order->shipping_address }}</div>
</div>
<div class="info-row">
<div class="info-label">Kecamatan</div>
<div class="info-value">{{ $order->city }}</div>
</div>
<div class="info-row">
<div class="info-label">Provinsi</div>
<div class="info-value">{{ $order->province }}</div>
</div>
<div class="info-row">
<div class="info-label">Kode Pos</div>
<div class="info-value">{{ $order->postal_code }}</div>
</div>
</div>
<div class="detail-card">
<div class="section-title"><i class="fas fa-credit-card me-2 text-success"></i>Info Pembayaran</div>
<div class="info-row">
<div class="info-label">Metode</div>
<div class="info-value">
@switch($order->payment_method)
@case('transfer') Transfer Bank @break
@case('bri') Transfer BRI @break
@case('qris') QRIS @break
@case('cod') COD (Bayar di Tempat) @break
@default {{ $order->payment_method }}
@endswitch
</div>
</div>
<div class="info-row">
<div class="info-label">Status</div>
<div class="info-value">
@if($order->payment_status === 'paid')
<span class="text-success fw-bold"><i class="fas fa-check me-1"></i>Lunas</span>
@else
<span class="text-warning fw-bold"><i class="fas fa-hourglass me-1"></i>Belum Dibayar</span>
@endif
</div>
</div>
@if($order->paid_at)
<div class="info-row">
<div class="info-label">Dibayar</div>
<div class="info-value">{{ $order->paid_at->format('d M Y, H:i') }}</div>
</div>
@endif
@if($order->notes)
<div class="info-row">
<div class="info-label">Catatan</div>
<div class="info-value">{{ $order->notes }}</div>
</div>
@endif
</div>
<a href="{{ route('orders.my') }}" class="btn btn-outline-success w-100">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Pesanan
</a>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,335 @@
@extends('layouts.app')
@section('title', 'Pesanan Saya - Bibit Cabai')
@section('styles')
<style>
body { background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
.page-hero {
background: linear-gradient(135deg, #28a745, #20c997);
padding: 35px 0;
color: white;
}
.nav-profile .nav-link {
color: #6c757d; border-radius: 8px;
padding: 10px 16px; font-weight: 500;
}
.nav-profile .nav-link.active,
.nav-profile .nav-link:hover {
background: #e8f5e9; color: #28a745;
}
.nav-profile .nav-link i { width: 20px; }
.profile-card {
background: white; border-radius: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
padding: 25px;
}
.filter-tabs .nav-link {
color: #6c757d; border-radius: 20px;
padding: 6px 16px; font-size: 0.85rem; font-weight: 500;
}
.filter-tabs .nav-link.active { background: #28a745; color: white; }
.filter-tabs .nav-link:hover:not(.active) { background: #f0f0f0; }
.order-card {
border: 1px solid #e9ecef; border-radius: 12px;
padding: 20px; margin-bottom: 16px;
transition: box-shadow 0.2s;
}
.order-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
/* ── Pesanan pending/processing punya border merah tipis kalau bisa dibatalkan ── */
.order-card.cancellable {
border-color: #f5c6cb;
}
.order-status {
padding: 4px 12px; border-radius: 20px;
font-size: 0.75rem; font-weight: 600;
}
.status-pending { background: #fff3cd; color: #856404; }
.status-processing { background: #cce5ff; color: #004085; }
.status-shipped { background: #d1ecf1; color: #0c5460; }
.status-delivered { background: #d4edda; color: #155724; }
.status-cancelled { background: #f8d7da; color: #721c24; }
.payment-paid { background: #d4edda; color: #155724; }
.payment-pending { background: #fff3cd; color: #856404; }
/* Badge pengajuan pembatalan sedang pending */
.cancel-badge {
background: #fff3cd; color: #856404;
border: 1px dashed #ffc107;
padding: 3px 10px; border-radius: 20px;
font-size: 0.72rem; font-weight: 600;
}
.product-img {
width: 55px; height: 55px; border-radius: 8px;
object-fit: cover; border: 1px solid #e9ecef;
}
.product-img-placeholder {
width: 55px; height: 55px; border-radius: 8px;
background: #f0f0f0; display: flex;
align-items: center; justify-content: center;
color: #aaa; font-size: 1.2rem;
}
.stat-mini {
text-align: center; padding: 15px;
border-radius: 10px; background: #f8f9fa;
}
.stat-mini .num { font-size: 1.5rem; font-weight: 700; color: #28a745; }
.stat-mini .lbl { font-size: 0.75rem; color: #6c757d; }
.invoice-num { font-size: 0.8rem; color: #6c757d; font-family: monospace; }
/* Tombol batalkan */
.btn-cancel-order {
font-size: 0.8rem; padding: 5px 14px;
border-radius: 20px; border: 1.5px solid #dc3545;
color: #dc3545; background: transparent;
font-weight: 600; transition: all 0.2s; text-decoration: none;
display: inline-flex; align-items: center; gap: 5px;
}
.btn-cancel-order:hover {
background: #dc3545; color: white;
text-decoration: none;
}
</style>
@endsection
@section('content')
<div class="page-hero">
<div class="container">
<h4 class="mb-0"><i class="fas fa-shopping-bag me-2"></i>Pesanan Saya</h4>
<small>Pantau status semua pesanan kamu di sini</small>
</div>
</div>
<div class="container py-4">
<div class="row g-4">
{{-- Sidebar --}}
<div class="col-md-3">
<div class="profile-card p-0 overflow-hidden mb-3">
<div class="p-3 border-bottom">
<small class="text-muted text-uppercase fw-bold" style="font-size:0.7rem;">Menu</small>
</div>
<nav class="nav flex-column nav-profile p-2">
<a href="{{ route('profile') }}" class="nav-link">
<i class="fas fa-user me-2"></i>Profil Saya
</a>
<a href="{{ route('orders.my') }}" class="nav-link active">
<i class="fas fa-shopping-bag me-2"></i>Pesanan Saya
</a>
<hr class="my-2">
<a href="{{ route('home') }}" class="nav-link text-muted">
<i class="fas fa-home me-2"></i>Kembali ke Beranda
</a>
</nav>
</div>
{{-- Statistik Mini --}}
<div class="profile-card">
<small class="text-muted text-uppercase fw-bold d-block mb-3" style="font-size:0.7rem;">Ringkasan</small>
<div class="row g-2">
<div class="col-6">
<div class="stat-mini">
<div class="num">{{ $totalOrders }}</div>
<div class="lbl">Total</div>
</div>
</div>
<div class="col-6">
<div class="stat-mini">
<div class="num">{{ $pendingOrders }}</div>
<div class="lbl">Menunggu</div>
</div>
</div>
<div class="col-6">
<div class="stat-mini">
<div class="num">{{ $shippedOrders }}</div>
<div class="lbl">Dikirim</div>
</div>
</div>
<div class="col-6">
<div class="stat-mini">
<div class="num">{{ $doneOrders }}</div>
<div class="lbl">Selesai</div>
</div>
</div>
</div>
</div>
</div>
{{-- Konten Pesanan --}}
<div class="col-md-9">
<div class="profile-card">
{{-- Flash Message --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert" style="border-radius:10px;">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
{{-- Filter Tabs --}}
<div class="d-flex flex-wrap gap-2 mb-4">
<nav class="nav filter-tabs flex-wrap gap-1">
<a href="{{ route('orders.my') }}"
class="nav-link {{ $status === 'all' ? 'active' : '' }}">
Semua ({{ $totalOrders }})
</a>
<a href="{{ route('orders.my', ['status' => 'pending']) }}"
class="nav-link {{ $status === 'pending' ? 'active' : '' }}">
Menunggu ({{ $pendingOrders }})
</a>
<a href="{{ route('orders.my', ['status' => 'processing']) }}"
class="nav-link {{ $status === 'processing' ? 'active' : '' }}">
Diproses ({{ $processOrders }})
</a>
<a href="{{ route('orders.my', ['status' => 'shipped']) }}"
class="nav-link {{ $status === 'shipped' ? 'active' : '' }}">
Dikirim ({{ $shippedOrders }})
</a>
<a href="{{ route('orders.my', ['status' => 'delivered']) }}"
class="nav-link {{ $status === 'delivered' ? 'active' : '' }}">
Selesai ({{ $doneOrders }})
</a>
<a href="{{ route('orders.my', ['status' => 'cancelled']) }}"
class="nav-link {{ $status === 'cancelled' ? 'active' : '' }}">
Dibatalkan ({{ $cancelOrders }})
</a>
</nav>
</div>
{{-- Daftar Pesanan --}}
@forelse($orders as $order)
@php
$isCancellable = $order->canBeCancelled();
$hasPending = $order->cancellation && $order->cancellation->status === 'pending';
@endphp
<div class="order-card {{ $isCancellable ? 'cancellable' : '' }}">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<div class="invoice-num mb-1">
<i class="fas fa-receipt me-1"></i>{{ $order->invoice_number }}
</div>
<small class="text-muted">{{ $order->created_at->format('d M Y, H:i') }}</small>
</div>
<div class="d-flex gap-2 flex-wrap justify-content-end align-items-center">
{{-- Badge pengajuan sedang menunggu --}}
@if($hasPending)
<span class="cancel-badge">
<i class="fas fa-clock me-1"></i>Menunggu Konfirmasi Batal
</span>
@endif
<span class="order-status status-{{ $order->order_status }}">
@switch($order->order_status)
@case('pending') <i class="fas fa-clock me-1"></i>Menunggu @break
@case('processing') <i class="fas fa-cog me-1"></i>Diproses @break
@case('shipped') <i class="fas fa-truck me-1"></i>Dikirim @break
@case('delivered') <i class="fas fa-check-circle me-1"></i>Selesai @break
@case('cancelled') <i class="fas fa-times-circle me-1"></i>Dibatalkan @break
@endswitch
</span>
<span class="order-status {{ $order->payment_status === 'paid' ? 'payment-paid' : 'payment-pending' }}">
@if($order->payment_status === 'paid')
<i class="fas fa-check me-1"></i>Lunas
@else
<i class="fas fa-hourglass me-1"></i>Belum Bayar
@endif
</span>
</div>
</div>
<div class="mb-3">
@foreach($order->details->take(2) as $detail)
<div class="d-flex align-items-center gap-3 mb-2">
@if($detail->product && $detail->product->image)
<img src="{{ asset('storage/' . $detail->product->image) }}"
class="product-img" alt="{{ $detail->product_name }}">
@else
<div class="product-img-placeholder">
<i class="fas fa-seedling"></i>
</div>
@endif
<div class="flex-grow-1">
<div class="fw-bold" style="font-size:0.9rem;">{{ $detail->product_name }}</div>
<small class="text-muted">{{ $detail->quantity }} x Rp{{ number_format($detail->price, 0, ',', '.') }}</small>
</div>
<div class="text-end">
<small class="fw-bold">Rp{{ number_format($detail->subtotal, 0, ',', '.') }}</small>
</div>
</div>
@endforeach
@if($order->details->count() > 2)
<small class="text-muted">
<i class="fas fa-plus me-1"></i>{{ $order->details->count() - 2 }} produk lainnya
</small>
@endif
</div>
<div class="d-flex justify-content-between align-items-center pt-3 border-top">
<div>
<small class="text-muted">Total Pembayaran</small>
<div class="fw-bold text-success" style="font-size:1.1rem;">
Rp{{ number_format($order->total_amount, 0, ',', '.') }}
</div>
</div>
<div class="d-flex gap-2 align-items-center">
{{-- Tombol Batalkan Pesanan --}}
@if($isCancellable)
<a href="{{ route('orders.cancel', $order->id) }}" class="btn-cancel-order">
<i class="fas fa-times-circle"></i>Batalkan
</a>
@endif
<a href="{{ route('orders.detail', $order->id) }}"
class="btn btn-outline-success btn-sm">
<i class="fas fa-eye me-1"></i>Lihat Detail
</a>
</div>
</div>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-shopping-bag fa-3x text-muted mb-3 d-block"></i>
<h6 class="text-muted">Belum ada pesanan</h6>
<a href="{{ route('home') }}" class="btn btn-success mt-2">
<i class="fas fa-seedling me-2"></i>Mulai Belanja
</a>
</div>
@endforelse
@if($orders->hasPages())
<div class="d-flex justify-content-center mt-4">
{{ $orders->appends(['status' => $status])->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
@endsection

View File

@ -11,6 +11,9 @@
use App\Http\Controllers\CheckoutController; use App\Http\Controllers\CheckoutController;
use App\Http\Controllers\TransaksiController; use App\Http\Controllers\TransaksiController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\AdminForgotPasswordController; // ← tambahkan ini
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -21,10 +24,12 @@
// Homepage // Homepage
Route::get('/', [HomeController::class, 'index'])->name('home'); Route::get('/', [HomeController::class, 'index'])->name('home');
// Produk terlaris // Produk terlaris
Route::get('/produk-terlaris', [ProductRecommendationController::class, 'index']) Route::get('/produk-terlaris', [ProductRecommendationController::class, 'index'])
->name('products.best-selling'); ->name('products.best-selling');
// Detail produk (public)
Route::get('/produk/{id}', [ProductController::class, 'publicShow'])->name('products.show');
// Contact Us // Contact Us
Route::get('/contact', [ContactController::class, 'index'])->name('contact'); Route::get('/contact', [ContactController::class, 'index'])->name('contact');
@ -67,6 +72,23 @@
Route::get('/best-selling-products', [ProductRecommendationController::class, 'apiGetBestSelling']) Route::get('/best-selling-products', [ProductRecommendationController::class, 'apiGetBestSelling'])
->name('best-selling-products'); ->name('best-selling-products');
}); });
Route::middleware('auth')->group(function () {
// Profil
Route::get('/profile', [ProfileController::class, 'index'])->name('profile');
Route::post('/profile/update', [ProfileController::class, 'update'])->name('profile.update');
Route::post('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
// Pesanan saya
Route::get('/pesanan-saya', [ProfileController::class, 'orders'])->name('orders.my');
Route::get('/pesanan-saya/{id}', [ProfileController::class, 'orderDetail'])->name('orders.detail');
Route::get('/orders/{id}/cancel', [ProfileController::class, 'cancelForm'])
->name('orders.cancel'); // ← nama harus orders.cancel
Route::post('/orders/{id}/cancel', [ProfileController::class, 'cancelStore'])
->name('orders.cancel.store'); // ← nama harus orders.cancel.store
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -76,27 +98,40 @@
Route::prefix('admin')->name('admin.')->group(function () { Route::prefix('admin')->name('admin.')->group(function () {
// Admin login routes (accessible to guests)
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
Route::get('/login', function () { Route::get('/login', function () {
return view('admin.login'); return view('admin.login');
})->name('login.form'); })->name('login.form');
Route::post('/login', [AdminController::class, 'login'])->name('login'); Route::post('/login', [AdminController::class, 'login'])->name('login');
Route::prefix('password')->name('password.')->group(function () { // ← ganti 'admin' jadi 'password'
Route::get('/forgot-password', [AdminForgotPasswordController::class, 'showForgotForm'])->name('request');
Route::post('/forgot-password', [AdminForgotPasswordController::class, 'sendOtp'])->name('email');
Route::get('/verify-otp', [AdminForgotPasswordController::class, 'showVerifyOtpForm'])->name('verify-otp-form');
Route::post('/verify-otp', [AdminForgotPasswordController::class, 'verifyOtp'])->name('verify-otp');
Route::post('/resend-otp', [AdminForgotPasswordController::class, 'resendOtp'])->name('resend-otp');
Route::get('/reset-password', [AdminForgotPasswordController::class, 'showResetForm'])->name('reset-form');
Route::post('/reset-password', [AdminForgotPasswordController::class, 'resetPassword'])->name('update');
}); });
});
// Protected admin routes (requires authentication) // Protected admin routes (requires authentication)
Route::middleware(['auth', 'web'])->group(function () { Route::middleware(['auth', 'is_admin'])->group(function () {
// Dashboard // Dashboard
Route::get('/dashboard', function () { Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
$totalProducts = \App\Models\Product::count(); // Di dalam group admin (bersama route lainnya):
$activeProducts = \App\Models\Product::where('status', 'aktif')->count(); Route::get('/laporan', [AdminController::class, 'laporan'])->name('laporan');
$totalSold = \App\Models\Product::sum('sold') ?? 0; Route::get('/laporan/export', function() {
$lowStock = \App\Models\Product::where('stock', '<=', 10)->count(); return redirect()->route('admin.laporan')->with('info', 'Fitur export sedang dalam pengembangan.');
})->name('laporan.export');
Route::get('/transaksis/{id}', [TransaksiController::class, 'show'])->name('transaksis.show');
// nama lengkapnya: admin.transaksis.show
return view('admin.dashboard', compact('totalProducts', 'activeProducts', 'totalSold', 'lowStock')); // HAPUS line 136 yang lama:
})->name('dashboard'); // Route::get('/admin/reports', [AdminController::class, 'reports'])->name('admin.reports');
// Product CRUD Routes // Product CRUD Routes
Route::get('/products', [ProductController::class, 'index'])->name('products.index'); Route::get('/products', [ProductController::class, 'index'])->name('products.index');
@ -118,9 +153,16 @@
Route::post('/transaksis/{id}/cancel', [TransaksiController::class, 'cancel'])->name('transaksis.cancel'); Route::post('/transaksis/{id}/cancel', [TransaksiController::class, 'cancel'])->name('transaksis.cancel');
Route::get('/transaksis-export', [TransaksiController::class, 'export'])->name('transaksis.export'); Route::get('/transaksis-export', [TransaksiController::class, 'export'])->name('transaksis.export');
Route::get('/transaksis/{id}/print', [TransaksiController::class, 'printInvoice'])->name('transaksis.print'); Route::get('/transaksis/{id}/print', [TransaksiController::class, 'printInvoice'])->name('transaksis.print');
Route::post('/orders/{id}/update-status', [TransaksiController::class, 'updateStatus'])->name('orders.updateStatus');
// Cancellation Admin Routes
Route::get('/cancellations', [TransaksiController::class, 'cancellations'])
->name('cancellations');
Route::post('/cancellations/{id}/approve', [TransaksiController::class, 'approveCancellation'])
->name('cancellations.approve');
Route::post('/cancellations/{id}/reject', [TransaksiController::class, 'rejectCancellation'])
->name('cancellations.reject');
// Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/create', [UserController::class, 'create'])->name('users.create'); Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
Route::post('/users', [UserController::class, 'store'])->name('users.store'); Route::post('/users', [UserController::class, 'store'])->name('users.store');
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show'); Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
@ -128,7 +170,6 @@
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update'); Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy'); Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
// Other admin routes // Other admin routes
Route::get('/orders', [AdminController::class, 'orders'])->name('orders'); Route::get('/orders', [AdminController::class, 'orders'])->name('orders');
Route::get('/users', [AdminController::class, 'users'])->name('users'); Route::get('/users', [AdminController::class, 'users'])->name('users');
@ -138,6 +179,29 @@
// Admin logout // Admin logout
Route::post('/logout', [AdminController::class, 'logout'])->name('logout'); Route::post('/logout', [AdminController::class, 'logout'])->name('logout');
}); });
}); // ← Penutup admin group yang benar (HANYA SATU) }); // ← Penutup admin group yang benar (HANYA SATU)
//lupa password
Route::get('/forgot-password', [ForgotPasswordController::class, 'showForgotForm'])
->name('password.request');
Route::post('/forgot-password', [ForgotPasswordController::class, 'sendOtp'])
->name('password.email');
Route::get('/verify-otp', [ForgotPasswordController::class, 'showVerifyOtpForm'])
->name('password.verify-otp-form');
Route::post('/verify-otp', [ForgotPasswordController::class, 'verifyOtp'])
->name('password.verify-otp');
Route::post('/resend-otp', [ForgotPasswordController::class, 'resendOtp'])
->name('password.resend-otp');
Route::get('/reset-password', [ForgotPasswordController::class, 'showResetForm'])
->name('password.reset-form');
Route::post('/reset-password', [ForgotPasswordController::class, 'resetPassword'])
->name('password.update');