first commit
This commit is contained in:
commit
51469f5520
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
// File: app/Http/Controllers/Admin/AdminController.php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show login form.
|
||||
*/
|
||||
public function showLoginForm()
|
||||
{
|
||||
return view('admin.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login request.
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
// Debug: Log request data
|
||||
Log::info('Login attempt', [
|
||||
'method' => $request->method(),
|
||||
'all_data' => $request->all(),
|
||||
'only_email' => $request->only(['email']),
|
||||
'email_value' => $request->input('email'),
|
||||
'has_email' => $request->has('email'),
|
||||
'filled_email' => $request->filled('email'),
|
||||
]);
|
||||
|
||||
// Validasi dengan pesan error yang jelas
|
||||
$validated = $request->validate([
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email:rfc,dns',
|
||||
'max:255'
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:6'
|
||||
],
|
||||
], [
|
||||
'email.required' => 'Email wajib diisi',
|
||||
'email.string' => 'Email harus berupa teks',
|
||||
'email.email' => 'Format email tidak valid',
|
||||
'email.max' => 'Email terlalu panjang',
|
||||
'password.required' => 'Password wajib diisi',
|
||||
'password.string' => 'Password harus berupa teks',
|
||||
'password.min' => 'Password minimal 6 karakter',
|
||||
]);
|
||||
|
||||
// Debug: Log validated data
|
||||
Log::info('Validated data', $validated);
|
||||
|
||||
$credentials = [
|
||||
'email' => $validated['email'],
|
||||
'password' => $validated['password']
|
||||
];
|
||||
|
||||
// Cek apakah user dengan email ini ada
|
||||
$user = User::where('email', $credentials['email'])->first();
|
||||
|
||||
if (!$user) {
|
||||
Log::warning('User not found', ['email' => $credentials['email']]);
|
||||
return back()->withErrors([
|
||||
'email' => 'Akun admin dengan email ini tidak ditemukan',
|
||||
])->withInput($request->only('email'));
|
||||
}
|
||||
|
||||
// Coba login
|
||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
Log::info('Login successful', ['user_id' => Auth::id()]);
|
||||
|
||||
return redirect()
|
||||
->intended(route('admin.dashboard'))
|
||||
->with('success', 'Selamat datang, ' . Auth::user()->name . '!');
|
||||
}
|
||||
|
||||
Log::warning('Login failed - wrong credentials', ['email' => $credentials['email']]);
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'Email atau password salah. Silakan coba lagi.',
|
||||
])->withInput($request->only('email'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the admin dashboard.
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$stats = [
|
||||
'total_products' => Product::count(),
|
||||
'active_products' => Product::where('status', 'aktif')->count(),
|
||||
'out_of_stock' => Product::where('stock', 0)->count(),
|
||||
'low_stock' => Product::where('stock', '>', 0)->where('stock', '<=', 5)->count(),
|
||||
];
|
||||
|
||||
$recent_products = Product::latest()->take(5)->get();
|
||||
|
||||
return view('admin.dashboard', compact('stats', 'recent_products'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show orders management.
|
||||
*/
|
||||
public function orders()
|
||||
{
|
||||
$orders = \App\Models\Transaksi::with(['details.product'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(15);
|
||||
|
||||
return view('admin.orders', compact('orders'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show users management.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return view('admin.users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show reports.
|
||||
*/
|
||||
public function reports()
|
||||
{
|
||||
return view('admin.reports');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show settings.
|
||||
*/
|
||||
public function settings()
|
||||
{
|
||||
return view('admin.settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout admin.
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Log::info('Admin logout', ['user_id' => Auth::id()]);
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/')
|
||||
->with('success', 'Anda telah berhasil logout');
|
||||
}
|
||||
|
||||
// Tambahkan method ini ke dalam AdminController.php Anda
|
||||
|
||||
/**
|
||||
* Display orders page with shipping and status information
|
||||
*/
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use App\Models\User;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
public function showLoginForm(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function login(Request $request): RedirectResponse
|
||||
{
|
||||
// Validasi input dengan semua requirement
|
||||
$request->validate([
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'regex:/@gmail\.com$/',
|
||||
'regex:/^\S+$/', // Tidak boleh mengandung spasi
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'min:8',
|
||||
'regex:/^\S+$/', // Tidak boleh mengandung spasi
|
||||
],
|
||||
'remember' => 'required|accepted',
|
||||
], [
|
||||
'email.required' => 'Email wajib diisi.',
|
||||
'email.email' => 'Format email tidak valid.',
|
||||
'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.',
|
||||
'password.required' => 'Password wajib diisi.',
|
||||
'password.min' => 'Password minimal harus 8 karakter.',
|
||||
'password.regex' => 'Password tidak boleh mengandung spasi.',
|
||||
'remember.required' => 'Anda harus mencentang "Ingat saya".',
|
||||
'remember.accepted' => 'Anda harus mencentang "Ingat saya".',
|
||||
]);
|
||||
|
||||
// Ambil credentials yang sudah divalidasi
|
||||
$credentials = [
|
||||
'email' => $request->email,
|
||||
'password' => $request->password,
|
||||
];
|
||||
|
||||
// Cari user dengan email yang diberikan dan pastikan bukan admin
|
||||
$user = User::where('email', $credentials['email'])
|
||||
->where(function ($query) {
|
||||
$query->where('is_admin', false)
|
||||
->orWhereNull('is_admin');
|
||||
})
|
||||
->first();
|
||||
|
||||
// Verifikasi apakah user ada dan password cocok
|
||||
if (!$user) {
|
||||
return back()->withErrors([
|
||||
'email' => 'Email tidak terdaftar atau Anda mencoba login admin di halaman user.',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
// Verifikasi password
|
||||
if (!Hash::check($credentials['password'], $user->password)) {
|
||||
return back()->withErrors([
|
||||
'password' => 'Password yang Anda masukkan salah.',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
// Login berhasil
|
||||
Auth::login($user, $request->boolean('remember'));
|
||||
$request->session()->regenerate();
|
||||
|
||||
// Pastikan session admin tidak ada
|
||||
session()->forget('is_admin');
|
||||
|
||||
return redirect()->intended('/')->with('success', 'Selamat datang, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/')->with('success', 'Anda telah berhasil logout.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
public function showRegistrationForm(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function register(Request $request): RedirectResponse
|
||||
{
|
||||
// Validasi dasar terlebih dahulu
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|min:12|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users|regex:/@gmail\.com$/|regex:/^\S+$/',
|
||||
'phone' => 'required|string|regex:/^[0-9]+$/|min:12|max:20|regex:/^\S+$/',
|
||||
'address' => 'required|string|min:12|max:500',
|
||||
'password' => 'required|string|min:8|confirmed|regex:/^\S+$/',
|
||||
'agree' => 'required|accepted',
|
||||
], [
|
||||
'name.required' => 'Nama wajib diisi.',
|
||||
'name.min' => 'Nama minimal harus 12 karakter.',
|
||||
'email.required' => 'Email wajib diisi.',
|
||||
'email.email' => 'Format email tidak valid.',
|
||||
'email.unique' => 'Email sudah digunakan.',
|
||||
'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.',
|
||||
'phone.required' => 'Nomor telepon wajib diisi.',
|
||||
'phone.regex' => 'Nomor telepon hanya boleh berisi angka dan tidak boleh mengandung spasi.',
|
||||
'phone.min' => 'Nomor telepon minimal harus 12 karakter.',
|
||||
'address.required' => 'Alamat wajib diisi.',
|
||||
'address.min' => 'Alamat minimal harus 12 karakter.',
|
||||
'password.required' => 'Password wajib diisi.',
|
||||
'password.min' => 'Password minimal harus 8 karakter.',
|
||||
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
||||
'password.regex' => 'Password tidak boleh mengandung spasi.',
|
||||
'agree.required' => 'Anda harus menyetujui syarat dan ketentuan.',
|
||||
'agree.accepted' => 'Anda harus menyetujui syarat dan ketentuan.',
|
||||
]);
|
||||
|
||||
// Validasi password familiar
|
||||
$validator->after(function ($validator) use ($request) {
|
||||
if ($request->has('password') && $this->isCommonPassword($request->password)) {
|
||||
$validator->errors()->add('password', 'Password yang Anda gunakan terlalu umum dan sering digunakan. Silakan pilih password yang lebih unik untuk keamanan akun Anda.');
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$validated = $validator->validated();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'address' => $validated['address'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'is_admin' => false, // Pastikan user biasa bukan admin
|
||||
]);
|
||||
|
||||
// Arahkan ke login dengan pesan sukses
|
||||
return redirect()->route('login')->with('success', 'Registrasi berhasil! Silakan login dengan akun Anda.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah password termasuk password yang umum/familiar
|
||||
*/
|
||||
private function isCommonPassword(string $password): bool
|
||||
{
|
||||
$commonPasswords = [
|
||||
// Password numerik umum
|
||||
'12345678', '123456789', '1234567890',
|
||||
'87654321', '11111111', '22222222', '33333333', '44444444', '55555555',
|
||||
'66666666', '77777777', '88888888', '99999999', '00000000',
|
||||
|
||||
// Password alfabetik umum
|
||||
'password', 'Password', 'PASSWORD', 'password123', 'Password123',
|
||||
'qwerty123', 'qwertyui', 'asdfghjk', 'zxcvbnm123',
|
||||
'abcd1234', 'admin123', 'user1234', 'welcome123',
|
||||
|
||||
// Password Indonesia umum
|
||||
'indonesia', 'Indonesia', 'garuda123', 'nusantara',
|
||||
'jakarta123', 'bali2023', 'surabaya', 'bandung123',
|
||||
'password1', 'rahasia123', 'bismillah',
|
||||
|
||||
// Kombinasi tanggal umum
|
||||
'01011990', '01011995', '01012000', '17081945',
|
||||
'12345abc', 'abc12345', '123qwe123', 'qwe12345',
|
||||
|
||||
// Pattern keyboard umum
|
||||
'qwerty12', 'asdf1234', 'zxcv1234', '1qaz2wsx',
|
||||
'1q2w3e4r', 'qazwsxed', 'plokijuh',
|
||||
|
||||
// Password dengan nama umum
|
||||
'admin1234', 'guest1234', 'test1234', 'demo1234',
|
||||
'sample123', 'example123', 'default123',
|
||||
|
||||
// Password sosial media umum
|
||||
'facebook', 'instagram', 'whatsapp', 'twitter123',
|
||||
'gmail123', 'yahoo123', 'google123',
|
||||
|
||||
// Password gaming umum
|
||||
'minecraft', 'roblox123', 'fortnite', 'pubg1234',
|
||||
'mobilelegend', 'freefire', 'valorant',
|
||||
|
||||
// Password device umum
|
||||
'android123', 'iphone123', 'samsung123', 'nokia1234',
|
||||
|
||||
// Tahun umum
|
||||
'20232023', '20242024', '19901990', '20002000',
|
||||
];
|
||||
|
||||
return in_array(strtolower($password), array_map('strtolower', $commonPasswords));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Transaksi;
|
||||
use App\Models\TransaksiDetail;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckoutController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$productId = $request->get('product_id');
|
||||
$quantity = $request->get('quantity', 1);
|
||||
|
||||
if (!$productId) {
|
||||
return redirect()->route('home')->with('error', 'Produk tidak ditemukan');
|
||||
}
|
||||
|
||||
$product = Product::find($productId);
|
||||
|
||||
if (!$product) {
|
||||
return redirect()->route('home')->with('error', 'Produk tidak ditemukan');
|
||||
}
|
||||
|
||||
if ($product->stock < $quantity) {
|
||||
return redirect()->back()->with('error', 'Stok tidak mencukupi. Stok tersedia: ' . $product->stock);
|
||||
}
|
||||
|
||||
$subtotal = $product->price * $quantity;
|
||||
$ongkir = 15000;
|
||||
$total = $subtotal + $ongkir;
|
||||
|
||||
// Panggil view checkout.index (form checkout)
|
||||
return view('checkout.index', compact('product', 'quantity', 'subtotal', 'ongkir', 'total'));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Checkout Index Error: ' . $e->getMessage());
|
||||
return redirect()->route('home')->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function process(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'name' => 'required|string|max:255',
|
||||
'phone' => 'required|string|max:20',
|
||||
'email' => 'required|email|max:255',
|
||||
'address' => 'required|string',
|
||||
'city' => 'required|string|max:100',
|
||||
'postal_code' => 'required|string|max:10',
|
||||
'payment_method' => 'required|in:qris,bri,dana,seabank,shopee,cod', // TAMBAHKAN 'qris'
|
||||
'shipping_cost' => 'required|numeric|min:0',
|
||||
'notes' => 'nullable|string|max:1000'
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$product = Product::lockForUpdate()->find($request->product_id);
|
||||
|
||||
if (!$product) {
|
||||
throw new \Exception('Produk tidak ditemukan');
|
||||
}
|
||||
|
||||
if ($product->stock < $request->quantity) {
|
||||
throw new \Exception('Stok tidak mencukupi. Stok tersedia: ' . $product->stock);
|
||||
}
|
||||
|
||||
$subtotal = $product->price * $request->quantity;
|
||||
$ongkir = 15000;
|
||||
$total = $subtotal + $ongkir;
|
||||
|
||||
$invoiceNumber = 'INV-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -6));
|
||||
|
||||
$transaksi = new Transaksi();
|
||||
$transaksi->invoice_number = $invoiceNumber;
|
||||
$transaksi->user_id = Auth::id();
|
||||
$transaksi->customer_name = $request->name;
|
||||
$transaksi->customer_phone = $request->phone;
|
||||
$transaksi->customer_email = $request->email;
|
||||
$transaksi->shipping_address = $request->address;
|
||||
$transaksi->province = $request->province;
|
||||
$transaksi->city = $request->city;
|
||||
$transaksi->postal_code = $request->postal_code;
|
||||
$transaksi->subtotal = $subtotal;
|
||||
$transaksi->shipping_cost = $ongkir;
|
||||
$transaksi->total_amount = $total;
|
||||
$transaksi->payment_method = $request->payment_method;
|
||||
$transaksi->payment_status = 'pending';
|
||||
$transaksi->order_status = 'pending';
|
||||
$transaksi->notes = $request->notes;
|
||||
$transaksi->save();
|
||||
|
||||
$detail = new TransaksiDetail();
|
||||
$detail->transaksi_id = $transaksi->id;
|
||||
$detail->product_id = $product->id;
|
||||
$detail->product_name = $product->name;
|
||||
$detail->quantity = $request->quantity;
|
||||
$detail->price = $product->price;
|
||||
$detail->subtotal = $subtotal;
|
||||
$detail->save();
|
||||
|
||||
$product->stock = $product->stock - $request->quantity;
|
||||
$product->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('checkout.success', ['id' => $transaksi->id])
|
||||
->with('success', 'Pesanan berhasil dibuat!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error('Checkout Process Error', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', 'Terjadi kesalahan: ' . $e->getMessage())
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function success($id)
|
||||
{
|
||||
try {
|
||||
$transaksi = Transaksi::with(['details.product'])->find($id);
|
||||
|
||||
if (!$transaksi) {
|
||||
return redirect()->route('home')
|
||||
->with('error', 'Transaksi tidak ditemukan');
|
||||
}
|
||||
|
||||
// Panggil view checkout.success (halaman sukses)
|
||||
return view('checkout.success', compact('transaksi'));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Success Page Error', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
return redirect()->route('home')
|
||||
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contact;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('contact');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'subject' => 'required|string|max:255',
|
||||
'message' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
Contact::create($validated);
|
||||
|
||||
return redirect()->back()->with('success', 'Pesan Anda telah terkirim! Kami akan segera merespons.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Get featured products (best selling or marked as featured)
|
||||
$featuredProducts = Product::active()
|
||||
->orderByDesc('sold') // Order by most sold
|
||||
->take(6)
|
||||
->get();
|
||||
|
||||
return view('home', compact('featuredProducts'));
|
||||
}
|
||||
|
||||
public function bestSellingProducts()
|
||||
{
|
||||
$products = Product::active()
|
||||
->where('sold', '>', 0)
|
||||
->orderByDesc('sold')
|
||||
->paginate(12);
|
||||
|
||||
return view('products.best-selling', compact('products'));
|
||||
}
|
||||
|
||||
public function allProducts()
|
||||
{
|
||||
$products = Product::active()
|
||||
->orderByDesc('created_at')
|
||||
->paginate(12);
|
||||
|
||||
return view('products.all', compact('products'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
// Tampilkan daftar produk (Admin) - UPDATED dengan search dan filter
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Product::orderBy('created_at', 'desc');
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$query->search($request->search);
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if ($request->filled('category')) {
|
||||
$query->byCategory($request->category);
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$products = $query->paginate(10);
|
||||
|
||||
// Append query parameters to pagination links
|
||||
$products->appends($request->query());
|
||||
|
||||
return view('admin.products.index', compact('products'));
|
||||
}
|
||||
|
||||
// Form tambah produk
|
||||
public function create()
|
||||
{
|
||||
return view('admin.products.create');
|
||||
}
|
||||
|
||||
// Simpan produk baru - UPDATED dengan auto label
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category' => 'required|string|max:100',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'stock' => 'required|integer|min:0',
|
||||
'status' => 'required|in:aktif,nonaktif',
|
||||
'description' => 'nullable|string',
|
||||
'image' => 'nullable|image|mimes:jpg,png,jpeg,gif|max:4048'
|
||||
], [
|
||||
'name.required' => 'Nama produk wajib diisi',
|
||||
'category.required' => 'Kategori wajib dipilih',
|
||||
'price.required' => 'Harga wajib diisi',
|
||||
'price.numeric' => 'Harga harus berupa angka',
|
||||
'stock.required' => 'Stok wajib diisi',
|
||||
'stock.integer' => 'Stok harus berupa angka bulat',
|
||||
'status.required' => 'Status wajib dipilih',
|
||||
'image.image' => 'File harus berupa gambar',
|
||||
'image.max' => 'Ukuran gambar maksimal 4MB',
|
||||
'image.mimes' => 'Format gambar harus JPG, PNG, JPEG, atau GIF'
|
||||
]);
|
||||
|
||||
$data = $request->only([
|
||||
'name',
|
||||
'category',
|
||||
'price',
|
||||
'stock',
|
||||
'status',
|
||||
'description'
|
||||
]);
|
||||
|
||||
// Upload gambar jika ada
|
||||
if ($request->hasFile('image')) {
|
||||
$gambar = $request->file('image');
|
||||
$filename = time() . '_' . $gambar->getClientOriginalName();
|
||||
$path = $gambar->storeAs('products', $filename, 'public');
|
||||
$data['image'] = $path;
|
||||
}
|
||||
|
||||
// Set default values
|
||||
$data['sold'] = 0;
|
||||
|
||||
try {
|
||||
$product = Product::create($data);
|
||||
|
||||
// Auto set label based on stock
|
||||
$product->updateLabelBasedOnStock();
|
||||
$product->save();
|
||||
|
||||
return redirect()->route('admin.products.index')
|
||||
->with('success', 'Produk berhasil ditambahkan!');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->withErrors(['error' => $e->getMessage()])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
// Tampilkan detail produk
|
||||
// public function show($id)
|
||||
// {
|
||||
// $product = Product::findOrFail($id);
|
||||
// return view('admin.products.show', compact('product'));
|
||||
// }
|
||||
|
||||
// Form edit produk
|
||||
public function edit(Product $product)
|
||||
{
|
||||
return view('admin.products.edit', compact('product'));
|
||||
}
|
||||
|
||||
// Ganti method update yang lama dengan yang ini:
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category' => 'required|string|max:100',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'stock' => 'required|integer|min:0',
|
||||
'status' => 'required|in:aktif,nonaktif',
|
||||
'description' => 'nullable|string',
|
||||
'image' => 'nullable|image|mimes:jpg,png,jpeg,gif|max:2048'
|
||||
], [
|
||||
'name.required' => 'Nama produk wajib diisi',
|
||||
'category.required' => 'Kategori wajib dipilih',
|
||||
'price.required' => 'Harga wajib diisi',
|
||||
'price.numeric' => 'Harga harus berupa angka',
|
||||
'stock.required' => 'Stok wajib diisi',
|
||||
'stock.integer' => 'Stok harus berupa angka bulat',
|
||||
'status.required' => 'Status wajib dipilih',
|
||||
'image.image' => 'File harus berupa gambar',
|
||||
'image.max' => 'Ukuran gambar maksimal 2MB',
|
||||
'image.mimes' => 'Format gambar harus JPG, PNG, JPEG, atau GIF'
|
||||
]);
|
||||
|
||||
$data = $request->only([
|
||||
'name',
|
||||
'category',
|
||||
'price',
|
||||
'stock',
|
||||
'status',
|
||||
'description'
|
||||
]);
|
||||
|
||||
// Upload gambar baru jika ada
|
||||
if ($request->hasFile('image')) {
|
||||
// Hapus gambar lama jika ada
|
||||
if ($product->image && Storage::disk('public')->exists($product->image)) {
|
||||
Storage::disk('public')->delete($product->image);
|
||||
}
|
||||
|
||||
$image = $request->file('image');
|
||||
$filename = time() . '_' . $image->getClientOriginalName();
|
||||
$path = $image->storeAs('products', $filename, 'public');
|
||||
$data['image'] = $path;
|
||||
}
|
||||
|
||||
try {
|
||||
$product->update($data);
|
||||
|
||||
// Auto update label based on new stock
|
||||
$product->updateLabelBasedOnStock();
|
||||
$product->save();
|
||||
|
||||
return redirect()->route('admin.products.index')
|
||||
->with('success', 'Produk berhasil diupdate!');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->withErrors(['error' => 'Terjadi kesalahan saat mengupdate produk'])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
// Juga ubah method show dan destroy untuk konsistensi:
|
||||
public function show(Product $product)
|
||||
{
|
||||
return view('admin.products.show', compact('product'));
|
||||
}
|
||||
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
// Hapus gambar dari storage sebelum menghapus record
|
||||
if ($product->image && Storage::disk('public')->exists($product->image)) {
|
||||
Storage::disk('public')->delete($product->image);
|
||||
}
|
||||
|
||||
$product->delete();
|
||||
|
||||
return redirect()->route('admin.products.index')
|
||||
->with('success', 'Produk berhasil dihapus');
|
||||
}
|
||||
|
||||
// NEW: Method untuk debugging gambar
|
||||
public function debugImage($id)
|
||||
{
|
||||
$product = Product::findOrFail($id);
|
||||
|
||||
$debug = [
|
||||
'product_id' => $product->id,
|
||||
'image_field' => $product->image,
|
||||
'image_url' => $product->image_url,
|
||||
'storage_path' => $product->image ? storage_path('app/public/' . $product->image) : null,
|
||||
'file_exists' => $product->image ? Storage::disk('public')->exists($product->image) : false,
|
||||
'public_path' => $product->image ? public_path('storage/' . $product->image) : null,
|
||||
];
|
||||
|
||||
return response()->json($debug);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Product;
|
||||
use App\Models\Transaksi;
|
||||
use App\Models\TransaksiDetail;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProductRecommendationController extends Controller
|
||||
{
|
||||
/**
|
||||
* VERSI UNTUK DEBUGGING
|
||||
* Menampilkan produk terlaris dengan informasi lengkap untuk debugging
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Log untuk debugging
|
||||
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')
|
||||
->join('transaksis', 'transaksi_details.transaksi_id', '=', 'transaksis.id')
|
||||
->join('products', 'transaksi_details.product_id', '=', 'products.id')
|
||||
->where('transaksis.order_status', 'delivered')
|
||||
->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 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
|
||||
$statistics = [
|
||||
'total_products_sold' => DB::table('transaksi_details')
|
||||
->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(),
|
||||
|
||||
'most_popular_product' => $bestSellingProducts->first()
|
||||
];
|
||||
|
||||
// Debug info
|
||||
$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)
|
||||
{
|
||||
$transaksiIds = $request->input('ids', []);
|
||||
|
||||
if (empty($transaksiIds)) {
|
||||
// Update semua transaksi yang ada detail
|
||||
$transaksiIds = TransaksiDetail::distinct()
|
||||
->pluck('transaksi_id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$updated = Transaksi::whereIn('id', $transaksiIds)
|
||||
->update(['order_status' => 'delivered']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "{$updated} transaksi berhasil diupdate ke delivered",
|
||||
'updated' => $updated
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method untuk cek status sistem
|
||||
*/
|
||||
public function checkStatus()
|
||||
{
|
||||
$status = [
|
||||
'total_transaksi' => Transaksi::count(),
|
||||
'total_delivered' => Transaksi::where('order_status', 'delivered')->count(),
|
||||
'total_pending' => Transaksi::where('order_status', 'pending')->count(),
|
||||
'total_processing' => Transaksi::where('order_status', 'processing')->count(),
|
||||
'total_details' => TransaksiDetail::count(),
|
||||
'transaksi_dengan_detail' => TransaksiDetail::distinct()->count('transaksi_id'),
|
||||
'status_breakdown' => Transaksi::select('order_status', DB::raw('count(*) as total'))
|
||||
->groupBy('order_status')
|
||||
->get(),
|
||||
];
|
||||
|
||||
return response()->json($status);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Transaksi;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TransaksiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of transactions.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Transaksi::with(['details.product', 'user'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// Filter by status
|
||||
if ($request->has('status') && $request->status != '') {
|
||||
$query->where('order_status', $request->status);
|
||||
}
|
||||
|
||||
// Filter by payment status
|
||||
if ($request->has('payment_status') && $request->payment_status != '') {
|
||||
$query->where('payment_status', $request->payment_status);
|
||||
}
|
||||
|
||||
// Filter by payment method
|
||||
if ($request->has('payment_method') && $request->payment_method != '') {
|
||||
$query->where('payment_method', $request->payment_method);
|
||||
}
|
||||
|
||||
// Search by invoice or customer name
|
||||
if ($request->has('search') && $request->search != '') {
|
||||
$search = $request->search;
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%")
|
||||
->orWhere('customer_phone', 'like', "%{$search}%")
|
||||
->orWhere('customer_email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if ($request->has('start_date') && $request->start_date != '') {
|
||||
$query->whereDate('created_at', '>=', $request->start_date);
|
||||
}
|
||||
if ($request->has('end_date') && $request->end_date != '') {
|
||||
$query->whereDate('created_at', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$transaksis = $query->paginate(20);
|
||||
|
||||
// Statistics
|
||||
$stats = [
|
||||
'total' => Transaksi::count(),
|
||||
'pending' => Transaksi::pending()->count(),
|
||||
'processing' => Transaksi::processing()->count(),
|
||||
'shipped' => Transaksi::shipped()->count(),
|
||||
'delivered' => Transaksi::delivered()->count(),
|
||||
'cancelled' => Transaksi::cancelled()->count(),
|
||||
'unpaid' => Transaksi::unpaid()->count(),
|
||||
'paid' => Transaksi::paid()->count(),
|
||||
'total_revenue' => Transaksi::paid()->sum('total_amount'),
|
||||
];
|
||||
|
||||
return view('admin.transaksis.index', compact('transaksis', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified transaction.
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$transaksi = Transaksi::with(['details.product', 'user'])->findOrFail($id);
|
||||
return view('admin.transaksis.show', compact('transaksi'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified transaction.
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$transaksi = Transaksi::with('details.product')->findOrFail($id);
|
||||
return view('admin.transaksis.edit', compact('transaksi'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified transaction in storage.
|
||||
* UPDATED: Menambahkan validasi dan update untuk customer info dan shipping
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$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',
|
||||
'payment_status' => 'required|in:pending,paid,failed',
|
||||
'tracking_number' => 'nullable|string|max:100',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
$transaksi = Transaksi::findOrFail($id);
|
||||
|
||||
$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,
|
||||
'payment_status' => $request->payment_status,
|
||||
'tracking_number' => $request->tracking_number,
|
||||
'notes' => $request->notes
|
||||
];
|
||||
|
||||
// If payment status changed to paid, set paid_at
|
||||
if ($request->payment_status === 'paid' && $transaksi->payment_status !== 'paid') {
|
||||
$updateData['paid_at'] = now();
|
||||
}
|
||||
|
||||
$transaksi->update($updateData);
|
||||
|
||||
return redirect()
|
||||
->route('admin.transaksis.show', $id)
|
||||
->with('success', 'Transaksi berhasil diupdate!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', 'Terjadi kesalahan: ' . $e->getMessage())
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order status via AJAX or regular request.
|
||||
* UPDATED: Menerima field 'order_status' selain 'status'
|
||||
*/
|
||||
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 {
|
||||
$transaksi = Transaksi::findOrFail($id);
|
||||
$newStatus = $request->input($statusField);
|
||||
|
||||
// Use model method if exists, otherwise direct update
|
||||
if (method_exists($transaksi, 'updateOrderStatus')) {
|
||||
$transaksi->updateOrderStatus($newStatus);
|
||||
} else {
|
||||
$transaksi->update(['order_status' => $newStatus]);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Status berhasil diupdate!'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', 'Status berhasil diupdate!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal update status: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', 'Gagal update status: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment status.
|
||||
*/
|
||||
public function updatePaymentStatus(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'payment_status' => 'required|in:pending,paid,failed',
|
||||
]);
|
||||
|
||||
try {
|
||||
$transaksi = Transaksi::findOrFail($id);
|
||||
|
||||
if ($request->payment_status === 'paid') {
|
||||
// Use model method if exists
|
||||
if (method_exists($transaksi, 'markAsPaid')) {
|
||||
$transaksi->markAsPaid();
|
||||
} else {
|
||||
$transaksi->update([
|
||||
'payment_status' => 'paid',
|
||||
'paid_at' => now()
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$transaksi->update([
|
||||
'payment_status' => $request->payment_status
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Status pembayaran berhasil diupdate!'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', 'Status pembayaran berhasil diupdate!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal update status pembayaran: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', 'Gagal update status pembayaran: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the transaction.
|
||||
*/
|
||||
public function cancel($id)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$transaksi = Transaksi::with('details')->findOrFail($id);
|
||||
|
||||
// Check if can be cancelled (use model method if exists)
|
||||
if (method_exists($transaksi, 'canBeCancelled') && !$transaksi->canBeCancelled()) {
|
||||
throw new \Exception('Transaksi tidak dapat dibatalkan');
|
||||
}
|
||||
|
||||
// Return stock
|
||||
foreach ($transaksi->details as $detail) {
|
||||
$product = Product::find($detail->product_id);
|
||||
if ($product) {
|
||||
$product->increment('stock', $detail->quantity);
|
||||
}
|
||||
}
|
||||
|
||||
// Update status
|
||||
$transaksi->update([
|
||||
'order_status' => 'cancelled',
|
||||
'payment_status' => 'failed'
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', 'Transaksi berhasil dibatalkan dan stok dikembalikan!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', 'Gagal membatalkan transaksi: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified transaction from storage.
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$transaksi = Transaksi::with('details')->findOrFail($id);
|
||||
|
||||
// If order is not cancelled, return stock first
|
||||
if ($transaksi->order_status !== 'cancelled') {
|
||||
foreach ($transaksi->details as $detail) {
|
||||
$product = Product::find($detail->product_id);
|
||||
if ($product) {
|
||||
$product->increment('stock', $detail->quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete transaction (details will be deleted automatically due to cascade)
|
||||
$transaksi->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('admin.orders') // Redirect ke halaman orders
|
||||
->with('success', 'Transaksi berhasil dihapus!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', 'Gagal menghapus transaksi: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export transactions to CSV.
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$query = Transaksi::with('details.product')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// Apply same filters as index
|
||||
if ($request->has('status') && $request->status != '') {
|
||||
$query->where('order_status', $request->status);
|
||||
}
|
||||
if ($request->has('payment_status') && $request->payment_status != '') {
|
||||
$query->where('payment_status', $request->payment_status);
|
||||
}
|
||||
if ($request->has('start_date') && $request->start_date != '') {
|
||||
$query->whereDate('created_at', '>=', $request->start_date);
|
||||
}
|
||||
if ($request->has('end_date') && $request->end_date != '') {
|
||||
$query->whereDate('created_at', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$transaksis = $query->get();
|
||||
|
||||
$filename = 'transaksi_' . date('Y-m-d_His') . '.csv';
|
||||
|
||||
header('Content-Type: text/csv');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// Header - UPDATED dengan info shipping
|
||||
fputcsv($output, [
|
||||
'Invoice',
|
||||
'Tanggal',
|
||||
'Customer',
|
||||
'Email',
|
||||
'Phone',
|
||||
'Shipping Address',
|
||||
'City',
|
||||
'Province',
|
||||
'Postal Code',
|
||||
'Total',
|
||||
'Payment Method',
|
||||
'Payment Status',
|
||||
'Order Status',
|
||||
'Tracking Number'
|
||||
]);
|
||||
|
||||
// Data - UPDATED dengan info shipping
|
||||
foreach ($transaksis as $transaksi) {
|
||||
fputcsv($output, [
|
||||
$transaksi->invoice_number,
|
||||
$transaksi->created_at->format('Y-m-d H:i:s'),
|
||||
$transaksi->customer_name,
|
||||
$transaksi->customer_email,
|
||||
$transaksi->customer_phone,
|
||||
$transaksi->shipping_address,
|
||||
$transaksi->city,
|
||||
$transaksi->province,
|
||||
$transaksi->postal_code,
|
||||
$transaksi->total_amount,
|
||||
$transaksi->payment_method_label ?? $transaksi->payment_method,
|
||||
$transaksi->payment_status,
|
||||
$transaksi->order_status,
|
||||
$transaksi->tracking_number ?? '-'
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print invoice.
|
||||
*/
|
||||
public function printInvoice($id)
|
||||
{
|
||||
$transaksi = Transaksi::with(['details.product', 'user'])->findOrFail($id);
|
||||
return view('admin.transaksis.print', compact('transaksi'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the users.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// Ambil semua users dari database dengan pagination
|
||||
$users = User::orderBy('id', 'desc')->paginate(15);
|
||||
|
||||
// Hitung statistik
|
||||
$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();
|
||||
|
||||
// Debug: Log jumlah users
|
||||
\Log::info('Total users in database: ' . $totalUsers);
|
||||
\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.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('admin.users.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created user in database.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'address' => ['nullable', 'string'],
|
||||
'is_admin' => ['boolean']
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'address' => $validated['address'] ?? null,
|
||||
'is_admin' => $request->has('is_admin') ? 1 : 0,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', 'Pengguna berhasil ditambahkan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified user.
|
||||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
return view('admin.users.show', [
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified user.
|
||||
*/
|
||||
public function edit(User $user)
|
||||
{
|
||||
return view('admin.users.edit', [
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified user in database.
|
||||
*/
|
||||
public function update(Request $request, User $user)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email,'.$user->id],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'address' => ['nullable', 'string'],
|
||||
'is_admin' => ['boolean']
|
||||
]);
|
||||
|
||||
$updateData = [
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'address' => $validated['address'] ?? null,
|
||||
'is_admin' => $request->has('is_admin') ? 1 : 0,
|
||||
];
|
||||
|
||||
// Update password jika diisi
|
||||
if ($request->filled('password')) {
|
||||
$request->validate([
|
||||
'password' => ['confirmed', Rules\Password::defaults()]
|
||||
]);
|
||||
$updateData['password'] = Hash::make($request->password);
|
||||
}
|
||||
|
||||
$user->update($updateData);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', 'Pengguna berhasil diperbarui');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified user from database.
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
// Prevent deleting yourself
|
||||
if (auth()->id() === $user->id) {
|
||||
return back()->with('error', 'Anda tidak dapat menghapus akun sendiri');
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', 'Pengguna berhasil dihapus');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Contact extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'subject',
|
||||
'message',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'customer_name',
|
||||
'customer_email',
|
||||
'customer_phone',
|
||||
'customer_address',
|
||||
'total_amount',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'total_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function orderDetails(): HasMany
|
||||
{
|
||||
return $this->hasMany(OrderDetail::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OrderDetail extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
'unit_price',
|
||||
'subtotal'
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => 'integer',
|
||||
'unit_price' => 'decimal:2',
|
||||
'subtotal' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'category',
|
||||
'price',
|
||||
'stock',
|
||||
'status',
|
||||
'description',
|
||||
'image',
|
||||
'label',
|
||||
'weight',
|
||||
'care_instructions',
|
||||
'season',
|
||||
'harvest_time',
|
||||
'sold'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'stock' => 'integer',
|
||||
'weight' => 'integer',
|
||||
'harvest_time' => 'integer',
|
||||
'sold' => 'integer'
|
||||
];
|
||||
|
||||
// Accessor untuk format harga - DIPERBAIKI
|
||||
public function getFormattedPriceAttribute()
|
||||
{
|
||||
return 'Rp ' . number_format($this->price, 0, ',', '.');
|
||||
}
|
||||
|
||||
// NEW: Accessor untuk URL gambar
|
||||
public function getImageUrlAttribute()
|
||||
{
|
||||
if ($this->image) {
|
||||
// Cek apakah file gambar ada di storage
|
||||
if (Storage::disk('public')->exists($this->image)) {
|
||||
return asset('storage/' . $this->image);
|
||||
}
|
||||
}
|
||||
|
||||
// Return placeholder jika gambar tidak ada
|
||||
return 'https://via.placeholder.com/60x60?text=No+Image&color=6c757d&background=f8f9fa';
|
||||
}
|
||||
|
||||
// Accessor untuk status badge
|
||||
public function getStatusBadgeAttribute()
|
||||
{
|
||||
return $this->status === 'aktif'
|
||||
? '<span class="badge bg-success">Aktif</span>'
|
||||
: '<span class="badge bg-danger">Non-aktif</span>';
|
||||
}
|
||||
|
||||
// NEW: Accessor untuk status badge class (untuk styling CSS)
|
||||
public function getStatusBadgeClassAttribute()
|
||||
{
|
||||
return $this->status === 'aktif' ? 'status-badge success' : 'status-badge danger';
|
||||
}
|
||||
|
||||
// NEW: Accessor untuk label badge class
|
||||
public function getLabelBadgeClassAttribute()
|
||||
{
|
||||
if (!$this->label) {
|
||||
return 'label-badge bg-secondary';
|
||||
}
|
||||
|
||||
switch (strtolower($this->label)) {
|
||||
case 'terlaris':
|
||||
return 'label-badge bg-danger';
|
||||
case 'tersedia':
|
||||
return 'label-badge bg-success';
|
||||
case 'habis':
|
||||
return 'label-badge bg-secondary';
|
||||
case 'baru':
|
||||
return 'label-badge bg-primary';
|
||||
default:
|
||||
return 'label-badge bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Method untuk mendapatkan label berdasarkan stok
|
||||
public function updateLabelBasedOnStock()
|
||||
{
|
||||
if ($this->stock <= 0) {
|
||||
$this->label = 'habis';
|
||||
} elseif ($this->stock > 1000) {
|
||||
$this->label = 'terlaris';
|
||||
} else {
|
||||
$this->label = 'tersedia';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Scope untuk produk aktif
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'aktif');
|
||||
}
|
||||
|
||||
// Scope untuk produk dengan stok rendah - DIPERBAIKI
|
||||
public function scopeLowStock($query, $threshold = 10)
|
||||
{
|
||||
return $query->where('stock', '<=', $threshold);
|
||||
}
|
||||
|
||||
// NEW: Scope untuk search
|
||||
public function scopeSearch($query, $term)
|
||||
{
|
||||
return $query->where('name', 'like', "%{$term}%")
|
||||
->orWhere('description', 'like', "%{$term}%");
|
||||
}
|
||||
|
||||
// NEW: Scope untuk filter kategori
|
||||
public function scopeByCategory($query, $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Transaksi extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'transaksis';
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_number',
|
||||
'user_id',
|
||||
'customer_name',
|
||||
'customer_phone',
|
||||
'customer_email',
|
||||
'shipping_address',
|
||||
'province',
|
||||
'city',
|
||||
'postal_code',
|
||||
'subtotal',
|
||||
'shipping_cost',
|
||||
'total_amount',
|
||||
'payment_method',
|
||||
'payment_status',
|
||||
'order_status',
|
||||
'notes',
|
||||
'paid_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'paid_at' => 'datetime',
|
||||
'subtotal' => 'decimal:2',
|
||||
'shipping_cost' => 'decimal:2',
|
||||
'total_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function details()
|
||||
{
|
||||
return $this->hasMany(TransaksiDetail::class);
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('order_status', 'pending');
|
||||
}
|
||||
|
||||
public function scopeProcessing($query)
|
||||
{
|
||||
return $query->where('order_status', 'processing');
|
||||
}
|
||||
|
||||
public function scopeShipped($query)
|
||||
{
|
||||
return $query->where('order_status', 'shipped');
|
||||
}
|
||||
|
||||
public function scopeDelivered($query)
|
||||
{
|
||||
return $query->where('order_status', 'delivered');
|
||||
}
|
||||
|
||||
public function scopeCancelled($query)
|
||||
{
|
||||
return $query->where('order_status', 'cancelled');
|
||||
}
|
||||
|
||||
public function scopePaid($query)
|
||||
{
|
||||
return $query->where('payment_status', 'paid');
|
||||
}
|
||||
|
||||
public function scopeUnpaid($query)
|
||||
{
|
||||
return $query->where('payment_status', 'pending');
|
||||
}
|
||||
|
||||
// Methods
|
||||
public function canBeCancelled()
|
||||
{
|
||||
return in_array($this->order_status, ['pending', 'processing']);
|
||||
}
|
||||
|
||||
public function markAsPaid()
|
||||
{
|
||||
return $this->update([
|
||||
'payment_status' => 'paid',
|
||||
'paid_at' => now()
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateOrderStatus($status)
|
||||
{
|
||||
return $this->update(['order_status' => $status]);
|
||||
}
|
||||
|
||||
// Accessor
|
||||
public function getPaymentMethodLabelAttribute()
|
||||
{
|
||||
return $this->payment_method === 'transfer' ? 'Transfer Bank' : 'COD';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TransaksiDetail extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'transaksi_details';
|
||||
|
||||
protected $fillable = [
|
||||
'transaksi_id',
|
||||
'product_id',
|
||||
'product_name',
|
||||
'quantity',
|
||||
'price',
|
||||
'subtotal'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'price' => 'decimal:2',
|
||||
'subtotal' => 'decimal:2',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function transaksi()
|
||||
{
|
||||
return $this->belongsTo(Transaksi::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'address',
|
||||
'password',
|
||||
'is_admin', // Tambahkan ini
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_admin' => 'boolean', // Tambahkan ini
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ProductRecommendationService
|
||||
{
|
||||
/**
|
||||
* Mendapatkan produk terlaris berdasarkan frekuensi pembelian
|
||||
*/
|
||||
public function getBestSellingProducts(int $limit = 6): Collection
|
||||
{
|
||||
return Cache::remember('best_selling_products', 3600, function () use ($limit) {
|
||||
return Product::select([
|
||||
'products.id',
|
||||
'products.name',
|
||||
'products.price',
|
||||
'products.image',
|
||||
'products.stock',
|
||||
DB::raw('COALESCE(SUM(order_details.quantity), 0) as total_sold'),
|
||||
DB::raw('COALESCE(COUNT(DISTINCT order_details.order_id), 0) as total_orders')
|
||||
])
|
||||
->leftJoin('order_details', 'products.id', '=', 'order_details.product_id')
|
||||
->leftJoin('orders', function($join) {
|
||||
$join->on('order_details.order_id', '=', 'orders.id')
|
||||
->where('orders.status', '=', 'completed');
|
||||
})
|
||||
->where('products.is_active', true)
|
||||
->where('products.stock', '>', 0)
|
||||
->groupBy([
|
||||
'products.id',
|
||||
'products.name',
|
||||
'products.price',
|
||||
'products.image',
|
||||
'products.stock'
|
||||
])
|
||||
->orderBy('total_sold', 'desc')
|
||||
->orderBy('total_orders', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan statistik penjualan untuk dashboard
|
||||
*/
|
||||
public function getSalesStatistics(): array
|
||||
{
|
||||
return Cache::remember('sales_statistics', 1800, function () {
|
||||
return [
|
||||
'total_products_sold' => DB::table('order_details')
|
||||
->join('orders', 'order_details.order_id', '=', 'orders.id')
|
||||
->where('orders.status', 'completed')
|
||||
->sum('order_details.quantity'),
|
||||
|
||||
'total_completed_orders' => DB::table('orders')
|
||||
->where('status', 'completed')
|
||||
->count(),
|
||||
|
||||
'most_popular_product' => Product::select([
|
||||
'products.name',
|
||||
DB::raw('SUM(order_details.quantity) as total_sold')
|
||||
])
|
||||
->join('order_details', 'products.id', '=', 'order_details.product_id')
|
||||
->join('orders', 'order_details.order_id', '=', 'orders.id')
|
||||
->where('orders.status', 'completed')
|
||||
->groupBy('products.id', 'products.name')
|
||||
->orderBy('total_sold', 'desc')
|
||||
->first()
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache ketika ada transaksi baru
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget('best_selling_products');
|
||||
Cache::forget('sales_statistics');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::snake((string) env('APP_NAME', 'laravel')).'_session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('phone')->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?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('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('category');
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->integer('stock');
|
||||
$table->integer('sold')->default(0);
|
||||
$table->string('image')->nullable();
|
||||
$table->enum('status', ['aktif', 'nonaktif'])->default('aktif');
|
||||
$table->enum('label', ['terlaris', 'tersedia'])->default('tersedia');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?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::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('customer_name');
|
||||
$table->string('customer_email');
|
||||
$table->string('customer_phone');
|
||||
$table->text('customer_address');
|
||||
$table->decimal('total_amount', 10, 2);
|
||||
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('orders');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?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::create('order_details', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('order_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('product_id')->constrained()->onDelete('cascade');
|
||||
$table->integer('quantity');
|
||||
$table->decimal('unit_price', 10, 2);
|
||||
$table->decimal('subtotal', 10, 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('order_details');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?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('contacts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email');
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('subject');
|
||||
$table->text('message');
|
||||
$table->enum('status', ['pending', 'read', 'replied'])->default('pending');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('contacts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?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::table('users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('users', 'is_admin')) {
|
||||
$table->boolean('is_admin')->default(false)->after('email_verified_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'is_admin')) {
|
||||
$table->dropColumn('is_admin');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?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('transaksis', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('invoice_number')->unique();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('customer_name');
|
||||
$table->string('customer_phone');
|
||||
$table->string('customer_email');
|
||||
$table->text('shipping_address');
|
||||
$table->string('province');
|
||||
$table->string('city');
|
||||
$table->string('postal_code');
|
||||
$table->decimal('subtotal', 12, 2);
|
||||
$table->decimal('shipping_cost', 12, 2)->default(0);
|
||||
$table->decimal('total_amount', 12, 2);
|
||||
$table->enum('payment_method', ['transfer', 'cod']);
|
||||
$table->enum('payment_status', ['pending', 'paid', 'failed'])->default('pending');
|
||||
$table->enum('order_status', ['pending', 'processing', 'shipped', 'delivered', 'cancelled'])->default('pending');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['invoice_number']);
|
||||
$table->index(['payment_status']);
|
||||
$table->index(['order_status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('transaksis');
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('transaksi_details', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('transaksi_id');
|
||||
$table->unsignedBigInteger('product_id');
|
||||
$table->string('product_name');
|
||||
$table->integer('quantity');
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->decimal('subtotal', 10, 2);
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign keys
|
||||
$table->foreign('transaksi_id')->references('id')->on('transaksis')->onDelete('cascade');
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('transaksi_details');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\Product;
|
||||
|
||||
class ProductSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeders.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$products = [
|
||||
[
|
||||
'name' => 'Bibit Cabai Merah Super',
|
||||
'description' => 'Premium Quality - Bibit cabai merah unggul dengan hasil panen melimpah. Tahan terhadap hama dan penyakit, cocok untuk berbagai cuaca.',
|
||||
'category' => 'Sayuran',
|
||||
'price' => 15000,
|
||||
'stock' => 100,
|
||||
'sold' => 250,
|
||||
'status' => 'aktif',
|
||||
'label' => 'terlaris',
|
||||
'image' => null // You can add image path here
|
||||
],
|
||||
[
|
||||
'name' => 'Bibit Cabai Hijau Jumbo',
|
||||
'description' => 'Varietas cabai hijau dengan ukuran besar dan rasa pedas yang pas. Ideal untuk masakan rumahan dan komersial.',
|
||||
'category' => 'Sayuran',
|
||||
'price' => 12000,
|
||||
'stock' => 85,
|
||||
'sold' => 180,
|
||||
'status' => 'aktif',
|
||||
'label' => 'terlaris',
|
||||
'image' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Bibit Cabai Rawit Super Pedas',
|
||||
'description' => 'Cabai rawit dengan tingkat kepedasan tinggi. Cocok untuk penggemar makanan pedas dan industri bumbu.',
|
||||
'category' => 'Sayuran',
|
||||
'price' => 8000,
|
||||
'stock' => 150,
|
||||
'sold' => 320,
|
||||
'status' => 'aktif',
|
||||
'label' => 'terlaris',
|
||||
'image' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Bibit Cabai Keriting Premium',
|
||||
'description' => 'Cabai keriting dengan tekstur unik dan rasa yang khas. Sangat diminati pasar lokal dan ekspor.',
|
||||
'category' => 'Sayuran',
|
||||
'price' => 18000,
|
||||
'stock' => 75,
|
||||
'sold' => 120,
|
||||
'status' => 'aktif',
|
||||
'label' => 'tersedia',
|
||||
'image' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Bibit Cabai Hias Ornamental',
|
||||
'description' => 'Cabai hias dengan warna-warni menarik. Cocok untuk dekorasi taman dan dapat dikonsumsi.',
|
||||
'category' => 'Hias',
|
||||
'price' => 25000,
|
||||
'stock' => 50,
|
||||
'sold' => 45,
|
||||
'status' => 'aktif',
|
||||
'label' => 'tersedia',
|
||||
'image' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Bibit Cabai Paprika Mini',
|
||||
'description' => 'Paprika mini dengan rasa manis dan warna cerah. Kaya vitamin dan cocok untuk salad.',
|
||||
'category' => 'Sayuran',
|
||||
'price' => 22000,
|
||||
'stock' => 60,
|
||||
'sold' => 90,
|
||||
'status' => 'aktif',
|
||||
'label' => 'tersedia',
|
||||
'image' => null
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($products as $product) {
|
||||
Product::create($product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.8.2",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^1.2.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.2.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import './bootstrap';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<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 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Stats Cards */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
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.orders { border-left-color: #007bff; }
|
||||
.stat-card.revenue { border-left-color: #ffc107; }
|
||||
.stat-card.users { border-left-color: #6f42c1; }
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 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>
|
||||
|
||||
<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>
|
||||
<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.reports') }}" class="menu-item {{ request()->routeIs('admin.reports') ? '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">
|
||||
<h2>Dashboard</h2>
|
||||
<p class="mb-0">Selamat datang di dashboard admin!</p>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card products">
|
||||
<div class="stat-number text-success">{{ $totalProducts ?? 0 }}</div>
|
||||
<div class="stat-label">Total Produk</div>
|
||||
<i class="fas fa-seedling stat-icon text-success"></i>
|
||||
</div>
|
||||
<div class="stat-card orders">
|
||||
<div class="stat-number text-primary">{{ $activeProducts ?? 0 }}</div>
|
||||
<div class="stat-label">Produk Aktif</div>
|
||||
<i class="fas fa-check-circle stat-icon text-primary"></i>
|
||||
</div>
|
||||
<div class="stat-card revenue">
|
||||
<div class="stat-number text-warning">{{ $totalSold ?? 0 }}</div>
|
||||
<div class="stat-label">Total Terjual</div>
|
||||
<i class="fas fa-chart-line stat-icon text-warning"></i>
|
||||
</div>
|
||||
<div class="stat-card users">
|
||||
<div class="stat-number text-info">{{ $lowStock ?? 0 }}</div>
|
||||
<div class="stat-label">Stok Menipis</div>
|
||||
<i class="fas fa-exclamation-triangle stat-icon text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">📈 Grafik Penjualan</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<h4>Belum Ada Data</h4>
|
||||
<p>Grafik penjualan akan ditampilkan setelah ada transaksi</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">⚡ Aktivitas Terbaru</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-clock"></i>
|
||||
<h4>Belum Ada Aktivitas</h4>
|
||||
<p>Aktivitas terbaru akan ditampilkan di sini</p>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Admin Dashboard') - Bibit Cabai</title>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<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 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 {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-left-color: #fff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.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 */
|
||||
.breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: #28a745;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
color: #20c997;
|
||||
}
|
||||
|
||||
/* Animation for transitions */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 12px 12px 0 0 !important;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@yield('additional_styles')
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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>
|
||||
<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.reports') }}" class="menu-item {{ request()->routeIs('admin.reports') ? '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">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@yield('additional_scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - TA Bibit Cabal</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-login {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-login: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;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #11998e;
|
||||
box-shadow: 0 0 0 0.2rem rgba(17, 153, 142, 0.25);
|
||||
}
|
||||
.text-primary {
|
||||
color: #11998e !important;
|
||||
}
|
||||
.btn-link {
|
||||
color: #11998e;
|
||||
}
|
||||
.btn-link:hover {
|
||||
color: #0d7377;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<div class="card login-card">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold text-dark">Admin Login</h3>
|
||||
<p class="text-muted">TA Bibit Cabal</p>
|
||||
</div>
|
||||
|
||||
{{-- Error Messages --}}
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Success Messages --}}
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Login Form --}}
|
||||
<form method="POST" action="{{ route('admin.login') }}" novalidate>
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Admin</label>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="contoh: admin@example.com"
|
||||
autocomplete="email"
|
||||
required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="position-relative">
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Masukkan password"
|
||||
autocomplete="current-password"
|
||||
required>
|
||||
<button type="button"
|
||||
class="btn btn-link position-absolute end-0 top-50 translate-middle-y pe-3"
|
||||
onclick="togglePassword()">
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-login py-3 fw-bold">
|
||||
🌿 Masuk ke Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{{ url('/') }}" class="text-decoration-none" style="color: #11998e;">
|
||||
← Kembali ke Beranda
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Debug Info (Hapus di production) --}}
|
||||
@if (config('app.debug'))
|
||||
<div class="mt-4 p-2 bg-light rounded small">
|
||||
<strong>Debug Info:</strong><br>
|
||||
Route: {{ Route::currentRouteName() }}<br>
|
||||
Method: {{ request()->method() }}<br>
|
||||
@if(request()->method() === 'POST')
|
||||
Data: {{ json_encode(request()->only(['email'])) }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function togglePassword() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const button = event.target;
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
button.textContent = '🙈';
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
button.textContent = '👁️';
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Log form data saat submit
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
const formData = new FormData(this);
|
||||
console.log('Form Data:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(key + ': "' + value + '"');
|
||||
}
|
||||
});
|
||||
|
||||
// Clear validation errors saat user mulai mengetik
|
||||
document.getElementById('email').addEventListener('input', function() {
|
||||
this.classList.remove('is-invalid');
|
||||
const feedback = this.nextElementSibling;
|
||||
if (feedback && feedback.classList.contains('invalid-feedback')) {
|
||||
feedback.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('password').addEventListener('input', function() {
|
||||
this.classList.remove('is-invalid');
|
||||
const feedback = this.nextElementSibling;
|
||||
if (feedback && feedback.classList.contains('invalid-feedback')) {
|
||||
feedback.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Orders Management')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Orders Management</h1>
|
||||
<div>
|
||||
<a href="{{ route('admin.transaksis.export') }}" class="btn btn-success">
|
||||
<i class="fas fa-file-excel"></i> Export Excel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<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">
|
||||
<div class="card border-left-warning 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-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 class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info 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-info text-uppercase mb-1">
|
||||
Processing Orders
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
{{ \App\Models\Transaksi::where('order_status', 'processing')->count() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Orders List</h6>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Filter by Status -->
|
||||
<select class="form-control form-control-sm" id="filterStatus" onchange="filterOrders()">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="shipped">Shipped</option>
|
||||
<option value="delivered">Delivered</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="ordersTable" width="100%" cellspacing="0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Invoice Number</th>
|
||||
<th>Customer</th>
|
||||
<th>Phone</th>
|
||||
<th>Shipping Address</th>
|
||||
<th>City</th>
|
||||
<th>Province</th>
|
||||
<th>Postal Code</th>
|
||||
<th>Order Status</th>
|
||||
<th>Payment Status</th>
|
||||
<th>Total</th>
|
||||
<th>Order Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($orders as $order)
|
||||
<tr data-status="{{ $order->order_status }}">
|
||||
<td>
|
||||
<strong>{{ $order->invoice_number }}</strong>
|
||||
</td>
|
||||
<td>{{ $order->customer_name }}</td>
|
||||
<td>{{ $order->customer_phone }}</td>
|
||||
<td>
|
||||
<small>{{ \Illuminate\Support\Str::limit($order->shipping_address, 30) }}</small>
|
||||
</td>
|
||||
<td>{{ $order->city }}</td>
|
||||
<td>{{ $order->province }}</td>
|
||||
<td>{{ $order->postal_code }}</td>
|
||||
<td>
|
||||
@php
|
||||
$statusClass = [
|
||||
'pending' => 'warning',
|
||||
'processing' => 'info',
|
||||
'shipped' => 'primary',
|
||||
'delivered' => 'success',
|
||||
'cancelled' => 'danger'
|
||||
][$order->order_status] ?? 'secondary';
|
||||
@endphp
|
||||
<span class="badge badge-{{ $statusClass }}">
|
||||
{{ ucfirst($order->order_status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$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) }}"
|
||||
class="btn btn-info btn-sm"
|
||||
title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('admin.transaksis.edit', $order->id) }}"
|
||||
class="btn btn-warning btn-sm"
|
||||
title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{{ route('admin.transaksis.print', $order->id) }}"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Print Invoice"
|
||||
target="_blank">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@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
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div>
|
||||
Showing {{ $orders->firstItem() ?? 0 }} to {{ $orders->lastItem() ?? 0 }}
|
||||
of {{ $orders->total() }} orders
|
||||
</div>
|
||||
<div>
|
||||
{{ $orders->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.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>
|
||||
function filterOrders() {
|
||||
const filterValue = document.getElementById('filterStatus').value.toLowerCase();
|
||||
const table = document.getElementById('ordersTable');
|
||||
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const status = rows[i].getAttribute('data-status');
|
||||
|
||||
if (filterValue === '' || status === filterValue) {
|
||||
rows[i].style.display = '';
|
||||
} else {
|
||||
rows[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tambah Produk - Bibit Cabai</title>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<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 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: #28a745;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 200px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Animation for transitions */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Form Controls */
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(45deg, #28a745, #20c997);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: linear-gradient(45deg, #20c997, #17a2b8);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Back Button -->
|
||||
<a href="{{ route('admin.products.index') }}" class="back-button">
|
||||
<i class="fas fa-arrow-left me-2"></i>Kembali ke Produk
|
||||
</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">Admin User</small>
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<a class="menu-item" href="{{ route('admin.dashboard') }}">
|
||||
<i class="fas fa-tachometer-alt"></i>Dashboard
|
||||
</a>
|
||||
<a class="menu-item active" href="{{ route('admin.products.index') }}">
|
||||
<i class="fas fa-seedling"></i>Kelola Produk
|
||||
</a>
|
||||
<a class="menu-item" href="{{ route('admin.orders') }}">
|
||||
<i class="fas fa-shopping-cart"></i>Pesanan
|
||||
</a>
|
||||
<a class="menu-item" href="{{ route('admin.users') }}">
|
||||
<i class="fas fa-users"></i>Pengguna
|
||||
</a>
|
||||
<a class="menu-item" href="{{ route('admin.reports') }}">
|
||||
<i class="fas fa-chart-line"></i>Laporan
|
||||
</a>
|
||||
<a class="menu-item" href="{{ route('admin.settings') }}">
|
||||
<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">
|
||||
<h2>Tambah Produk Baru</h2>
|
||||
<p class="mb-0">Tambah produk baru ke dalam sistem bibit cabai</p>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="form-container fade-in">
|
||||
<div class="form-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Kelola Produk</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Tambah Produk</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h3><i class="fas fa-plus text-success me-2"></i>Tambah Produk Baru</h3>
|
||||
<p class="text-muted mb-0">Lengkapi formulir di bawah ini untuk menambah produk baru</p>
|
||||
</div>
|
||||
|
||||
<!-- Display Validation Errors -->
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Display Success Message -->
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.products.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<!-- Nama Produk - DIPERBAIKI: name attribute -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Nama Produk <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-seedling"></i></span>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror"
|
||||
id="name" name="name" value="{{ old('name') }}"
|
||||
placeholder="Masukkan nama produk" required>
|
||||
</div>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Kategori - DIPERBAIKI: name attribute -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="category" class="form-label">Kategori <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-list"></i></span>
|
||||
<select class="form-select @error('category') is-invalid @enderror"
|
||||
id="category" name="category" required>
|
||||
<option value="">Pilih Kategori</option>
|
||||
<option value="Sayuran" {{ old('category') == 'Sayuran' ? 'selected' : '' }}>Sayuran</option>
|
||||
<option value="Buah" {{ old('category') == 'Buah' ? 'selected' : '' }}>Buah</option>
|
||||
<option value="Herbal" {{ old('category') == 'Herbal' ? 'selected' : '' }}>Herbal</option>
|
||||
<option value="Hias" {{ old('category') == 'Hias' ? 'selected' : '' }}>Hias</option>
|
||||
</select>
|
||||
</div>
|
||||
@error('category')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Harga - DIPERBAIKI: name attribute -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="price" class="form-label">Harga <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Rp</span>
|
||||
<input type="number" class="form-control @error('price') is-invalid @enderror"
|
||||
id="price" name="price" value="{{ old('price') }}"
|
||||
min="0" step="1000" placeholder="0" required>
|
||||
</div>
|
||||
@error('price')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Stok - DIPERBAIKI: name attribute -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="stock" class="form-label">Stok <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-boxes"></i></span>
|
||||
<input type="number" class="form-control @error('stock') is-invalid @enderror"
|
||||
id="stock" name="stock" value="{{ old('stock') }}"
|
||||
min="0" placeholder="0" required>
|
||||
</div>
|
||||
@error('stock')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="status" class="form-label">Status <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-toggle-on"></i></span>
|
||||
<select class="form-select @error('status') is-invalid @enderror"
|
||||
id="status" name="status" required>
|
||||
<option value="">Pilih Status</option>
|
||||
<option value="aktif" {{ old('status') == 'aktif' ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="nonaktif" {{ old('status') == 'nonaktif' ? 'selected' : '' }}>Non-aktif</option>
|
||||
</select>
|
||||
</div>
|
||||
@error('status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Deskripsi - DIPERBAIKI: name attribute -->
|
||||
<div class="col-12 mb-3">
|
||||
<label for="description" class="form-label">Deskripsi Produk</label>
|
||||
<textarea class="form-control @error('description') is-invalid @enderror"
|
||||
id="description" name="description" rows="4"
|
||||
placeholder="Masukkan deskripsi lengkap produk...">{{ old('description') }}</textarea>
|
||||
<small class="form-text text-muted">Jelaskan keunggulan dan cara perawatan bibit</small>
|
||||
@error('description')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Image Upload - DIPERBAIKI: name attribute -->
|
||||
<div class="col-12 mb-4">
|
||||
<label for="image" class="form-label">Gambar Produk</label>
|
||||
<input type="file" class="form-control @error('image') is-invalid @enderror"
|
||||
id="image" name="image" accept="image/*" onchange="previewImage(this)">
|
||||
<small class="form-text text-muted">Format: JPG, PNG, GIF. Maksimal 2MB. Disarankan ukuran 500x500 pixel.</small>
|
||||
@error('image')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
<!-- Image Preview -->
|
||||
<div id="imagePreview" class="mt-3" style="display: none;">
|
||||
<p class="mb-2"><strong>Preview Gambar:</strong></p>
|
||||
<img id="preview" src="" alt="Preview" class="image-preview">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Info -->
|
||||
<div class="alert alert-info d-flex align-items-center" role="alert">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<div>
|
||||
<strong>Informasi:</strong> Pastikan semua data yang dimasukkan sudah benar sebelum menyimpan.
|
||||
Data produk akan langsung tampil di website setelah disimpan.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<a href="{{ route('admin.products.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-2"></i>Batal
|
||||
</a>
|
||||
<button type="reset" class="btn btn-outline-warning">
|
||||
<i class="fas fa-undo me-2"></i>Reset Form
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save me-2"></i>Simpan Produk
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
// Preview image function
|
||||
function previewImage(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
// Check file size (2MB limit)
|
||||
if (input.files[0].size > 2 * 1024 * 1024) {
|
||||
alert('Ukuran file terlalu besar! Maksimal 2MB.');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('preview').src = e.target.result;
|
||||
document.getElementById('imagePreview').style.display = 'block';
|
||||
}
|
||||
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
} else {
|
||||
document.getElementById('imagePreview').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation enhancement
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const inputs = form.querySelectorAll('input[required], select[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
if (this.value.trim() === '') {
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
if (this.classList.contains('is-invalid') && this.value.trim() !== '') {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Price formatting - DIPERBAIKI: id yang benar
|
||||
const priceInput = document.getElementById('price');
|
||||
priceInput.addEventListener('input', function() {
|
||||
// Remove non-numeric characters except decimal point
|
||||
this.value = this.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
||||
// Auto-focus first input - DIPERBAIKI: id yang benar
|
||||
document.getElementById('name').focus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Edit Produk')
|
||||
|
||||
@section('content')
|
||||
<!-- Back Button -->
|
||||
<a href="{{ route('admin.products.index') }}" class="back-button">
|
||||
<i class="fas fa-arrow-left me-2"></i>Kembali ke Daftar Produk
|
||||
</a>
|
||||
<!-- 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">
|
||||
<a href="{{ route('admin.products.index') }}">Kelola Produk</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Edit Produk</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h2>Edit Produk</h2>
|
||||
<p class="mb-0">Edit informasi produk {{ $product->name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="content-area fade-in">
|
||||
<!-- Display Validation Errors -->
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-edit text-warning me-2"></i>
|
||||
Form Edit Produk
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.products.update', $product) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<!-- Nama Produk -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Nama Produk <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror"
|
||||
id="name" name="name" value="{{ old('name', $product->name) }}" required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Kategori -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="category" class="form-label">Kategori <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('category') is-invalid @enderror" id="category" name="category" required>
|
||||
<option value="">Pilih Kategori</option>
|
||||
<option value="Sayuran" {{ old('category', $product->category) === 'Sayuran' ? 'selected' : '' }}>Sayuran</option>
|
||||
<option value="Buah" {{ old('category', $product->category) === 'Buah' ? 'selected' : '' }}>Buah</option>
|
||||
<option value="Herbal" {{ old('category', $product->category) === 'Herbal' ? 'selected' : '' }}>Herbal</option>
|
||||
<option value="Hias" {{ old('category', $product->category) === 'Hias' ? 'selected' : '' }}>Hias</option>
|
||||
</select>
|
||||
@error('category')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Harga -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="price" class="form-label">Harga <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Rp</span>
|
||||
<input type="number" class="form-control @error('price') is-invalid @enderror"
|
||||
id="price" name="price" value="{{ old('price', $product->price) }}"
|
||||
min="0" step="0.01" required>
|
||||
</div>
|
||||
@error('price')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Stok -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="stock" class="form-label">Stok <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control @error('stock') is-invalid @enderror"
|
||||
id="stock" name="stock" value="{{ old('stock', $product->stock) }}"
|
||||
min="0" required>
|
||||
@error('stock')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="status" class="form-label">Status <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('status') is-invalid @enderror" id="status" name="status" required>
|
||||
<option value="aktif" {{ old('status', $product->status) === 'aktif' ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="nonaktif" {{ old('status', $product->status) === 'nonaktif' ? 'selected' : '' }}>Non-aktif</option>
|
||||
</select>
|
||||
@error('status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Label -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="label" class="form-label">Label</label>
|
||||
<select class="form-select @error('label') is-invalid @enderror" id="label" name="label">
|
||||
<option value="">Tidak Ada Label</option>
|
||||
<option value="baru" {{ old('label', $product->label) === 'baru' ? 'selected' : '' }}>Baru</option>
|
||||
<option value="populer" {{ old('label', $product->label) === 'populer' ? 'selected' : '' }}>Populer</option>
|
||||
<option value="diskon" {{ old('label', $product->label) === 'diskon' ? 'selected' : '' }}>Diskon</option>
|
||||
<option value="terbatas" {{ old('label', $product->label) === 'terbatas' ? 'selected' : '' }}>Terbatas</option>
|
||||
</select>
|
||||
@error('label')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Deskripsi -->
|
||||
<div class="col-12 mb-3">
|
||||
<label for="description" class="form-label">Deskripsi <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control @error('description') is-invalid @enderror"
|
||||
id="description" name="description" rows="4" required>{{ old('description', $product->description) }}</textarea>
|
||||
@error('description')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Gambar Saat Ini -->
|
||||
@if($product->image_url)
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label">Gambar Saat Ini</label>
|
||||
<div class="mb-3">
|
||||
<img src="{{ $product->image_url }}" alt="{{ $product->name }}"
|
||||
class="img-thumbnail" style="max-width: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Upload Gambar Baru -->
|
||||
<div class="col-12 mb-3">
|
||||
<label for="image" class="form-label">Gambar Produk (Opsional)</label>
|
||||
<input type="file" class="form-control @error('image') is-invalid @enderror"
|
||||
id="image" name="image" accept="image/*">
|
||||
<div class="form-text">Format: JPG, PNG, JPEG. Maksimal 2MB. Kosongkan jika tidak ingin mengubah gambar.</div>
|
||||
@error('image')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.products.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Kembali
|
||||
</a>
|
||||
<div>
|
||||
<button type="reset" class="btn btn-outline-warning me-2">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save me-2"></i>Update Produk
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.back-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1100;
|
||||
background: linear-gradient(45deg, #ffc107, #e0a800);
|
||||
color: #212529;
|
||||
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, #e0a800, #c69500);
|
||||
transform: translateY(-2px);
|
||||
color: #212529;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 12px 12px 0 0 !important;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,746 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kelola Produk - Bibit Cabai Admin</title>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<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 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 {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-left-color: #fff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Product Image */
|
||||
.product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-details h6 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-details small {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-badge.danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.label-badge {
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.label-badge.bg-success { background: #28a745 !important; }
|
||||
.label-badge.bg-danger { background: #dc3545 !important; }
|
||||
.label-badge.bg-warning { background: #ffc107 !important; color: #212529 !important; }
|
||||
.label-badge.bg-secondary { background: #6c757d !important; }
|
||||
|
||||
.stock-badge {
|
||||
background: #20c997;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-view { background: #17a2b8; color: white; }
|
||||
.btn-edit { background: #ffc107; color: #212529; }
|
||||
.btn-delete { background: #dc3545; color: white; }
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: #28a745;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item a:hover {
|
||||
color: #20c997;
|
||||
}
|
||||
|
||||
/* Animation for transitions */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-backdrop.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
transform: scale(0.8);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-backdrop.show .modal-content {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
</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">
|
||||
<i class="fas fa-tachometer-alt"></i>Dashboard
|
||||
</a>
|
||||
<a href="{{ route('admin.products.index') }}" class="menu-item active">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<!-- 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">Kelola Produk</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h2>Kelola Produk</h2>
|
||||
<p class="mb-0">Kelola semua produk bibit cabai Anda di sini!</p>
|
||||
</div>
|
||||
|
||||
<div class="content-area fade-in">
|
||||
<!-- Display Success/Error Messages -->
|
||||
@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
|
||||
|
||||
<div class="page-header">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-seedling text-success me-2"></i>
|
||||
Daftar Produk ({{ $products->total() ?? 0 }})
|
||||
</h3>
|
||||
<a href="{{ route('admin.products.create') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>Tambah Produk
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Form -->
|
||||
<form method="GET" action="{{ route('admin.products.index') }}" id="filterForm">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" name="search"
|
||||
value="{{ request('search') }}" placeholder="Cari produk..."
|
||||
id="searchProduct">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" name="category" id="filterCategory">
|
||||
<option value="">Semua Kategori</option>
|
||||
<option value="Sayuran" {{ request('category') === 'Sayuran' ? 'selected' : '' }}>Sayuran</option>
|
||||
<option value="Buah" {{ request('category') === 'Buah' ? 'selected' : '' }}>Buah</option>
|
||||
<option value="Herbal" {{ request('category') === 'Herbal' ? 'selected' : '' }}>Herbal</option>
|
||||
<option value="Hias" {{ request('category') === 'Hias' ? 'selected' : '' }}>Hias</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" name="status" id="filterStatus">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="aktif" {{ request('status') === 'aktif' ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="nonaktif" {{ request('status') === 'nonaktif' ? 'selected' : '' }}>Non-aktif</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Products Table -->
|
||||
@if($products && $products->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="data-table" id="productsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Kategori</th>
|
||||
<th>Harga</th>
|
||||
<th>Stok</th>
|
||||
<th>Status</th>
|
||||
<th>Label</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="productsTableBody">
|
||||
@foreach($products as $product)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="product-info">
|
||||
<img src="{{ $product->image_url }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="product-image"
|
||||
onerror="this.src='https://via.placeholder.com/60x60?text=No+Image'">
|
||||
<div class="product-details">
|
||||
<h6>{{ $product->name }}</h6>
|
||||
<small class="text-muted">{{ Str::limit($product->description, 30) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="category-badge">{{ $product->category }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong class="text-success">{{ $product->formatted_price }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="stock-badge">{{ number_format($product->stock) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="{{ $product->status_badge_class }}">
|
||||
{{ ucfirst($product->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label-badge {{ $product->label_badge_class }}">
|
||||
{{ ucfirst($product->label) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<!-- <div class="action-buttons">
|
||||
<a href="{{ route('admin.products.show', $product) }}"
|
||||
class="btn-action btn-view" title="Lihat">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a> -->
|
||||
<a href="{{ route('admin.products.edit', $product) }}"
|
||||
class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn-action btn-delete"
|
||||
onclick="deleteProduct({{ $product->id }}, '{{ $product->name }}')"
|
||||
title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div>
|
||||
<small class="text-muted">
|
||||
Menampilkan {{ $products->firstItem() }} sampai {{ $products->lastItem() }}
|
||||
dari {{ $products->total() }} produk
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
{{ $products->appends(request()->query())->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-seedling"></i>
|
||||
<h4>Belum Ada Produk</h4>
|
||||
<p>Mulai dengan menambahkan produk bibit cabai pertama Anda!</p>
|
||||
<a href="{{ route('admin.products.create') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>Tambah Produk Pertama
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="deleteModal" class="modal-backdrop">
|
||||
<div class="modal-content">
|
||||
<h4 class="text-danger mb-3">
|
||||
<i class="fas fa-exclamation-triangle"></i> Konfirmasi Hapus
|
||||
</h4>
|
||||
<p class="mb-2">Apakah Anda yakin ingin menghapus produk:</p>
|
||||
<p class="mb-4"><strong id="productNameToDelete"></strong></p>
|
||||
<p class="text-muted small mb-4">Aksi ini tidak dapat dibatalkan.</p>
|
||||
<div class="d-flex gap-2 justify-content-center">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteModal()">Batal</button>
|
||||
<button class="btn btn-danger" onclick="confirmDelete()">Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Form (Hidden) -->
|
||||
<form id="deleteForm" method="POST" style="display: none;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
</form>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Global variables for delete functionality
|
||||
let productToDelete = null;
|
||||
let productNameToDelete = '';
|
||||
|
||||
// Auto-submit form when filters change
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchProduct');
|
||||
const categoryFilter = document.getElementById('filterCategory');
|
||||
const statusFilter = document.getElementById('filterStatus');
|
||||
let searchTimeout;
|
||||
|
||||
// Debounced search
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Immediate filter change
|
||||
if (categoryFilter) {
|
||||
categoryFilter.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-hide alerts after 5 seconds
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
if (bsAlert) {
|
||||
bsAlert.close();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete product function with modal
|
||||
function deleteProduct(id, name) {
|
||||
productToDelete = id;
|
||||
productNameToDelete = name;
|
||||
|
||||
document.getElementById('productNameToDelete').textContent = name;
|
||||
|
||||
const modal = document.getElementById('deleteModal');
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
// Close delete modal
|
||||
function closeDeleteModal() {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
modal.classList.remove('show');
|
||||
productToDelete = null;
|
||||
productNameToDelete = '';
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
function confirmDelete() {
|
||||
if (productToDelete) {
|
||||
const form = document.getElementById('deleteForm');
|
||||
form.action = `/admin/products/${productToDelete}`;
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Escape key to close modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Image error handling
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const images = document.querySelectorAll('.product-image');
|
||||
images.forEach(function(img) {
|
||||
img.addEventListener('error', function() {
|
||||
this.src = 'https://via.placeholder.com/60x60?text=No+Image&color=6c757d&background=f8f9fa';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Success message auto-hide
|
||||
setTimeout(function() {
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
successAlert.style.transition = 'opacity 0.5s';
|
||||
successAlert.style.opacity = '0';
|
||||
setTimeout(function() {
|
||||
successAlert.remove();
|
||||
}, 500);
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Edit Order')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-gray-800">Edit Order</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb bg-transparent p-0 mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.orders') }}">Orders</a></li>
|
||||
<li class="breadcrumb-item active">Edit {{ $transaksi->invoice_number }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<a href="{{ route('admin.transaksis.show', $transaksi->id) }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Error!</strong> Please fix the following issues:
|
||||
<ul class="mb-0 mt-2">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.transaksis.update', $transaksi->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<!-- Order Details -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Order Details</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="invoice_number">Invoice Number</label>
|
||||
<input type="text" class="form-control" id="invoice_number" name="invoice_number"
|
||||
value="{{ old('invoice_number', $transaksi->invoice_number) }}" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="order_status">Order Status <span class="text-danger">*</span></label>
|
||||
<select class="form-control @error('order_status') is-invalid @enderror"
|
||||
id="order_status" name="order_status" required>
|
||||
<option value="pending" {{ old('order_status', $transaksi->order_status) == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="processing" {{ old('order_status', $transaksi->order_status) == 'processing' ? 'selected' : '' }}>Processing</option>
|
||||
<option value="shipped" {{ old('order_status', $transaksi->order_status) == 'shipped' ? 'selected' : '' }}>Shipped</option>
|
||||
<option value="delivered" {{ old('order_status', $transaksi->order_status) == 'delivered' ? 'selected' : '' }}>Delivered</option>
|
||||
<option value="cancelled" {{ old('order_status', $transaksi->order_status) == 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
</select>
|
||||
@error('order_status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="payment_status">Payment Status <span class="text-danger">*</span></label>
|
||||
<select class="form-control @error('payment_status') is-invalid @enderror"
|
||||
id="payment_status" name="payment_status" required>
|
||||
<option value="pending" {{ old('payment_status', $transaksi->payment_status) == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="paid" {{ old('payment_status', $transaksi->payment_status) == 'paid' ? 'selected' : '' }}>Paid</option>
|
||||
<option value="failed" {{ old('payment_status', $transaksi->payment_status) == 'failed' ? 'selected' : '' }}>Failed</option>
|
||||
</select>
|
||||
@error('payment_status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tracking_number">Tracking Number</label>
|
||||
<input type="text" class="form-control @error('tracking_number') is-invalid @enderror"
|
||||
id="tracking_number" name="tracking_number"
|
||||
value="{{ old('tracking_number', $transaksi->tracking_number) }}"
|
||||
placeholder="Enter tracking number">
|
||||
@error('tracking_number')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea class="form-control @error('notes') is-invalid @enderror"
|
||||
id="notes" name="notes" rows="3"
|
||||
placeholder="Enter any notes about this order">{{ old('notes', $transaksi->notes) }}</textarea>
|
||||
@error('notes')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Information -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Customer Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="customer_name">Customer Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('customer_name') is-invalid @enderror"
|
||||
id="customer_name" name="customer_name"
|
||||
value="{{ old('customer_name', $transaksi->customer_name) }}" required>
|
||||
@error('customer_name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="customer_phone">Phone <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('customer_phone') is-invalid @enderror"
|
||||
id="customer_phone" name="customer_phone"
|
||||
value="{{ old('customer_phone', $transaksi->customer_phone) }}" required>
|
||||
@error('customer_phone')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="customer_email">Email</label>
|
||||
<input type="email" class="form-control @error('customer_email') is-invalid @enderror"
|
||||
id="customer_email" name="customer_email"
|
||||
value="{{ old('customer_email', $transaksi->customer_email) }}">
|
||||
@error('customer_email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Information -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Shipping Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="shipping_address">Shipping Address <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control @error('shipping_address') is-invalid @enderror"
|
||||
id="shipping_address" name="shipping_address" rows="3" required>{{ old('shipping_address', $transaksi->shipping_address) }}</textarea>
|
||||
@error('shipping_address')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="city">City <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('city') is-invalid @enderror"
|
||||
id="city" name="city"
|
||||
value="{{ old('city', $transaksi->city) }}" required>
|
||||
@error('city')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="province">Province <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('province') is-invalid @enderror"
|
||||
id="province" name="province"
|
||||
value="{{ old('province', $transaksi->province) }}" required>
|
||||
@error('province')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="postal_code">Postal Code <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('postal_code') is-invalid @enderror"
|
||||
id="postal_code" name="postal_code"
|
||||
value="{{ old('postal_code', $transaksi->postal_code) }}" required>
|
||||
@error('postal_code')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Summary -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Order Summary</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="text-primary">{{ $transaksi->invoice_number }}</h5>
|
||||
<p class="text-muted mb-3">{{ $transaksi->created_at->format('d M Y, H:i') }}</p>
|
||||
|
||||
<table class="table table-sm table-borderless">
|
||||
<tbody>
|
||||
@foreach($transaksi->details as $detail)
|
||||
<tr>
|
||||
<td>{{ $detail->product->name ?? 'N/A' }}</td>
|
||||
<td class="text-right">x{{ $detail->quantity }}</td>
|
||||
<td class="text-right">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot class="border-top">
|
||||
<tr>
|
||||
<th colspan="2">Total:</th>
|
||||
<th class="text-right">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-save"></i> Update Order
|
||||
</button>
|
||||
<a href="{{ route('admin.transaksis.show', $transaksi->id) }}" class="btn btn-secondary btn-block">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice {{ $transaksi->invoice_number }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.invoice-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.invoice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 3px solid #4e73df;
|
||||
}
|
||||
|
||||
.company-info h1 {
|
||||
color: #4e73df;
|
||||
font-size: 28px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.company-info p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-title h2 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.invoice-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
font-size: 16px;
|
||||
color: #4e73df;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.detail-section p {
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-section strong {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.items-table thead {
|
||||
background: #4e73df;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.items-table tbody tr:last-child td {
|
||||
border-bottom: 2px solid #4e73df;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.total-section {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.total-row span:first-child {
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.total-row span:last-child {
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.grand-total {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4e73df;
|
||||
padding-top: 10px;
|
||||
border-top: 2px solid #4e73df;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-pending { background: #ffc107; color: white; }
|
||||
.status-processing { background: #17a2b8; color: white; }
|
||||
.status-shipped { background: #007bff; color: white; }
|
||||
.status-delivered { background: #28a745; color: white; }
|
||||
.status-cancelled { background: #dc3545; color: white; }
|
||||
.status-paid { background: #28a745; color: white; }
|
||||
.status-failed { background: #dc3545; color: white; }
|
||||
|
||||
.invoice-footer {
|
||||
margin-top: 50px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #4e73df;
|
||||
}
|
||||
|
||||
.notes h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #4e73df;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.invoice-container {
|
||||
border: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.print-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px 20px;
|
||||
background: #4e73df;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.print-button:hover {
|
||||
background: #224abe;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="window.print()" class="print-button no-print">
|
||||
🖨️ Print Invoice
|
||||
</button>
|
||||
|
||||
<div class="invoice-container">
|
||||
<!-- Header -->
|
||||
<div class="invoice-header">
|
||||
<div class="company-info">
|
||||
<h1>🌱 TA Bibit Cabai</h1>
|
||||
<p>Jl. Contoh Alamat No. 123</p>
|
||||
<p>Jember, Jawa Timur 68100</p>
|
||||
<p>Phone: (0331) 123-4567</p>
|
||||
<p>Email: info@tabibitcabai.com</p>
|
||||
</div>
|
||||
<div class="invoice-title">
|
||||
<h2>INVOICE</h2>
|
||||
<p><strong>{{ $transaksi->invoice_number }}</strong></p>
|
||||
<p>Date: {{ $transaksi->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<div class="invoice-details">
|
||||
<div class="detail-section">
|
||||
<h3>Bill To:</h3>
|
||||
<p><strong>Name:</strong> {{ $transaksi->customer_name }}</p>
|
||||
<p><strong>Phone:</strong> {{ $transaksi->customer_phone }}</p>
|
||||
@if($transaksi->customer_email)
|
||||
<p><strong>Email:</strong> {{ $transaksi->customer_email }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3>Ship To:</h3>
|
||||
<p>{{ $transaksi->shipping_address }}</p>
|
||||
<p>{{ $transaksi->city }}, {{ $transaksi->province }}</p>
|
||||
<p>{{ $transaksi->postal_code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<p>
|
||||
<strong>Order Status:</strong>
|
||||
<span class="status-badge status-{{ $transaksi->order_status }}">
|
||||
{{ ucfirst($transaksi->order_status) }}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Payment Status:</strong>
|
||||
<span class="status-badge status-{{ $transaksi->payment_status }}">
|
||||
{{ ucfirst($transaksi->payment_status) }}
|
||||
</span>
|
||||
</p>
|
||||
@if($transaksi->tracking_number)
|
||||
<p style="margin-top: 10px;">
|
||||
<strong>Tracking Number:</strong> <code>{{ $transaksi->tracking_number }}</code>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Items Table -->
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">#</th>
|
||||
<th width="45%">Product</th>
|
||||
<th width="15%" class="text-right">Price</th>
|
||||
<th width="10%" class="text-center">Qty</th>
|
||||
<th width="25%" class="text-right">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($transaksi->details as $index => $detail)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<strong>{{ $detail->product->name ?? 'Product Not Found' }}</strong>
|
||||
@if($detail->product && $detail->product->description)
|
||||
<br><small style="color: #666;">{{ \Illuminate\Support\Str::limit($detail->product->description, 80) }}</small>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-right">Rp {{ number_format($detail->price, 0, ',', '.') }}</td>
|
||||
<td class="text-center">{{ $detail->quantity }}</td>
|
||||
<td class="text-right">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Total Section -->
|
||||
<div class="total-section">
|
||||
<div class="total-row">
|
||||
<span>Subtotal:</span>
|
||||
<span>Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
@if($transaksi->shipping_cost ?? 0 > 0)
|
||||
<div class="total-row">
|
||||
<span>Shipping Cost:</span>
|
||||
<span>Rp {{ number_format($transaksi->shipping_cost, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="total-row grand-total">
|
||||
<span>TOTAL:</span>
|
||||
<span>Rp {{ number_format($transaksi->total_amount + ($transaksi->shipping_cost ?? 0), 0, ',', '.') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
@if($transaksi->notes)
|
||||
<div class="notes">
|
||||
<h4>Notes:</h4>
|
||||
<p>{{ $transaksi->notes }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="invoice-footer">
|
||||
<p><strong>Thank you for your business!</strong></p>
|
||||
<p>This is a computer-generated invoice and does not require a signature.</p>
|
||||
<p>For any inquiries, please contact us at info@tabibitcabai.com</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto print when page loads (optional)
|
||||
// window.onload = function() { window.print(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Order Details')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-gray-800">Order Details</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb bg-transparent p-0 mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.orders') }}">Orders</a></li>
|
||||
<li class="breadcrumb-item active">{{ $transaksi->invoice_number }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ route('admin.orders') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Orders
|
||||
</a>
|
||||
<a href="{{ route('admin.transaksis.edit', $transaksi->id) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="{{ route('admin.transaksis.print', $transaksi->id) }}" class="btn btn-info" target="_blank">
|
||||
<i class="fas fa-print"></i> Print Invoice
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Order Information -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Invoice & Status Card -->
|
||||
<div class="card shadow mb-4">
|
||||
<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>
|
||||
<div>
|
||||
@php
|
||||
$statusClass = [
|
||||
'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) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary mb-3">Invoice Details</h6>
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td width="40%"><strong>Invoice Number:</strong></td>
|
||||
<td>{{ $transaksi->invoice_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Order Date:</strong></td>
|
||||
<td>{{ $transaksi->created_at->format('d M Y, H:i') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Payment Status:</strong></td>
|
||||
<td>
|
||||
@php
|
||||
$paymentClass = [
|
||||
'pending' => 'warning',
|
||||
'paid' => 'success',
|
||||
'failed' => 'danger'
|
||||
][$transaksi->payment_status] ?? 'secondary';
|
||||
@endphp
|
||||
<span class="badge badge-{{ $paymentClass }}">
|
||||
{{ ucfirst($transaksi->payment_status) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary mb-3">Update Status</h6>
|
||||
<form action="{{ route('admin.transaksis.update-status', $transaksi->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="order_status">Order Status</label>
|
||||
<select name="order_status" id="order_status" class="form-control">
|
||||
<option value="pending" {{ $transaksi->order_status == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="processing" {{ $transaksi->order_status == 'processing' ? 'selected' : '' }}>Processing</option>
|
||||
<option value="shipped" {{ $transaksi->order_status == 'shipped' ? 'selected' : '' }}>Shipped</option>
|
||||
<option value="delivered" {{ $transaksi->order_status == 'delivered' ? 'selected' : '' }}>Delivered</option>
|
||||
<option value="cancelled" {{ $transaksi->order_status == 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-save"></i> Update Order Status
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ route('admin.transaksis.update-payment-status', $transaksi->id) }}" method="POST" class="mt-3">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="payment_status">Payment Status</label>
|
||||
<select name="payment_status" id="payment_status" class="form-control">
|
||||
<option value="pending" {{ $transaksi->payment_status == 'pending' ? 'selected' : '' }}>Pending</option>
|
||||
<option value="paid" {{ $transaksi->payment_status == 'paid' ? 'selected' : '' }}>Paid</option>
|
||||
<option value="failed" {{ $transaksi->payment_status == 'failed' ? 'selected' : '' }}>Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-save"></i> Update Payment Status
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Order Items</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Product</th>
|
||||
<th>Price</th>
|
||||
<th>Quantity</th>
|
||||
<th>Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($transaksi->details as $index => $detail)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<strong>{{ $detail->product->name ?? 'Product Not Found' }}</strong>
|
||||
</td>
|
||||
<td>Rp {{ number_format($detail->price, 0, ',', '.') }}</td>
|
||||
<td>{{ $detail->quantity }}</td>
|
||||
<td>Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4" class="text-right">Total Amount:</th>
|
||||
<th>Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer & Shipping Info -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Customer Information -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Customer Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td><i class="fas fa-user text-primary"></i></td>
|
||||
<td><strong>Name:</strong></td>
|
||||
<td>{{ $transaksi->customer_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="fas fa-phone text-primary"></i></td>
|
||||
<td><strong>Phone:</strong></td>
|
||||
<td>{{ $transaksi->customer_phone }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="fas fa-envelope text-primary"></i></td>
|
||||
<td><strong>Email:</strong></td>
|
||||
<td>{{ $transaksi->customer_email ?? '-' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Information -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Shipping Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2"><strong>Shipping Address:</strong></p>
|
||||
<address class="mb-3">
|
||||
{{ $transaksi->shipping_address }}<br>
|
||||
{{ $transaksi->city }}, {{ $transaksi->province }}<br>
|
||||
{{ $transaksi->postal_code }}
|
||||
</address>
|
||||
|
||||
@if($transaksi->shipping_method)
|
||||
<p class="mb-1"><strong>Shipping Method:</strong></p>
|
||||
<p>{{ $transaksi->shipping_method }}</p>
|
||||
@endif
|
||||
|
||||
@if($transaksi->tracking_number)
|
||||
<p class="mb-1"><strong>Tracking Number:</strong></p>
|
||||
<p><code>{{ $transaksi->tracking_number }}</code></p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Notes -->
|
||||
@if($transaksi->notes)
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Notes</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">{{ $transaksi->notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this order?</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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
<form action="{{ route('admin.transaksis.destroy', $transaksi->id) }}" method="POST" style="display: inline;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete Order
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<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>
|
||||
// Auto-submit form when status changes
|
||||
document.getElementById('order_status').addEventListener('change', function() {
|
||||
if(confirm('Update order status?')) {
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('payment_status').addEventListener('change', function() {
|
||||
if(confirm('Update payment status?')) {
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Kelola Pengguna - Admin</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 25px 30px;
|
||||
border-radius: 10px;
|
||||
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 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
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;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.search-bar input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.search-bar button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
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;
|
||||
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;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #3498db;
|
||||
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;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
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) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>👥 Kelola Pengguna</h1>
|
||||
<p style="color: #666; margin-top: 5px;">Dashboard Admin - TA Bibit Cabai</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-primary">+ Tambah Pengguna</a>
|
||||
<a href="{{ route('admin.dashboard') }}" class="btn btn-primary">← Dashboard</a>
|
||||
<form method="POST" action="{{ route('logout') }}" style="display: inline;">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-danger">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
✓ {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-error">
|
||||
✗ {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<h3>Total Pengguna</h3>
|
||||
<div class="number">{{ $totalUsers ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Admin</h3>
|
||||
<div class="number">{{ $totalAdmins ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Email Terverifikasi</h3>
|
||||
<div class="number">{{ $verifiedUsers ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Pengguna Aktif (30 Hari)</h3>
|
||||
<div class="number">{{ $activeUsers ?? 0 }}</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>
|
||||
|
||||
<!-- Users Table -->
|
||||
@if(isset($users) && $users->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nama</th>
|
||||
<th>Email</th>
|
||||
<th>Telepon</th>
|
||||
<th>Alamat</th>
|
||||
<th>Status</th>
|
||||
<th>Verifikasi</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td>{{ $user->id }}</td>
|
||||
<td><strong>{{ $user->name }}</strong></td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td>{{ $user->phone ?? '-' }}</td>
|
||||
<td>{{ Str::limit($user->address ?? '-', 30) }}</td>
|
||||
<td>
|
||||
@if($user->is_admin == 1)
|
||||
<span class="badge badge-admin">Admin</span>
|
||||
@else
|
||||
<span class="badge badge-user">User</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($user->email_verified_at)
|
||||
<span class="badge badge-verified">✓ Terverifikasi</span>
|
||||
@else
|
||||
<span class="badge badge-unverified">Belum Verifikasi</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.users.show', $user) }}" class="action-btn btn-view">Detail</a>
|
||||
<a href="{{ route('admin.users.edit', $user) }}" class="action-btn btn-edit">Edit</a>
|
||||
<form method="POST" action="{{ route('admin.users.destroy', $user) }}" style="display: inline;" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pengguna ini?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn btn-delete">Hapus</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if(isset($users))
|
||||
<div class="pagination">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<div>📭</div>
|
||||
<h3>Tidak ada data pengguna</h3>
|
||||
<p>Belum ada pengguna terdaftar dalam sistem</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Search function
|
||||
function searchUsers() {
|
||||
const input = document.getElementById('searchInput');
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById('usersTable');
|
||||
|
||||
if (!table) return;
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Login - 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-seedling me-2"></i>Masuk ke Akun Anda
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
@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
|
||||
|
||||
<form method="POST" action="{{ route('login') }}" id="loginForm">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
required
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
placeholder="contoh@gmail.com">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Harus menggunakan email @gmail.com</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 dan tidak boleh mengandung spasi</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="Minimal 8 karakter">
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye" id="eyeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Password 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>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input @error('remember') is-invalid @enderror" id="remember" name="remember" required>
|
||||
<label class="form-check-label" for="remember">
|
||||
Ingat saya <span class="text-danger">*</span>
|
||||
</label>
|
||||
@error('remember')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="rememberError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Anda harus mencentang "Ingat saya" untuk melanjutkan</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Masuk
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">Belum punya akun?
|
||||
<a href="{{ route('register') }}" class="text-success text-decoration-none fw-bold">
|
||||
Daftar di sini
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('loginForm');
|
||||
const emailInput = document.getElementById('email');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const rememberCheckbox = document.getElementById('remember');
|
||||
const togglePassword = document.getElementById('togglePassword');
|
||||
const eyeIcon = document.getElementById('eyeIcon');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Toggle password visibility
|
||||
togglePassword.addEventListener('click', function() {
|
||||
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
passwordInput.setAttribute('type', type);
|
||||
eyeIcon.classList.toggle('fa-eye');
|
||||
eyeIcon.classList.toggle('fa-eye-slash');
|
||||
});
|
||||
|
||||
// Real-time validation for email
|
||||
emailInput.addEventListener('input', function() {
|
||||
const emailError = document.getElementById('emailError');
|
||||
const gmailPattern = /@gmail\.com$/;
|
||||
const hasSpaces = /\s/.test(this.value);
|
||||
|
||||
if ((!gmailPattern.test(this.value) || hasSpaces) && this.value.length > 0) {
|
||||
emailError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
emailError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
checkFormValidity();
|
||||
});
|
||||
|
||||
// Real-time validation for password
|
||||
passwordInput.addEventListener('input', function() {
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
const hasSpaces = /\s/.test(this.value);
|
||||
|
||||
if (this.value.length < 8 || hasSpaces) {
|
||||
passwordError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
passwordError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
checkFormValidity();
|
||||
});
|
||||
|
||||
// Real-time validation for remember checkbox
|
||||
rememberCheckbox.addEventListener('change', function() {
|
||||
const rememberError = document.getElementById('rememberError');
|
||||
if (!this.checked) {
|
||||
rememberError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
rememberError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
checkFormValidity();
|
||||
});
|
||||
|
||||
// Check form validity and enable/disable submit button
|
||||
function checkFormValidity() {
|
||||
const emailValid = emailInput.value.length > 0 &&
|
||||
/@gmail\.com$/.test(emailInput.value) &&
|
||||
!/\s/.test(emailInput.value);
|
||||
const passwordValid = passwordInput.value.length >= 8 &&
|
||||
!/\s/.test(passwordInput.value);
|
||||
const rememberValid = rememberCheckbox.checked;
|
||||
|
||||
if (emailValid && passwordValid && rememberValid) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('btn-secondary');
|
||||
submitBtn.classList.add('btn-success');
|
||||
} else {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.remove('btn-success');
|
||||
submitBtn.classList.add('btn-secondary');
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent spaces in email and password fields
|
||||
emailInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
passwordInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Form validation before submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
// Check email validation
|
||||
const gmailPattern = /@gmail\.com$/;
|
||||
const emailHasSpaces = /\s/.test(emailInput.value);
|
||||
if (!gmailPattern.test(emailInput.value) || emailHasSpaces) {
|
||||
document.getElementById('emailError').style.display = 'block';
|
||||
emailInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Check password validation
|
||||
const passwordHasSpaces = /\s/.test(passwordInput.value);
|
||||
if (passwordInput.value.length < 8 || passwordHasSpaces) {
|
||||
document.getElementById('passwordError').style.display = 'block';
|
||||
passwordInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Check remember checkbox
|
||||
if (!rememberCheckbox.checked) {
|
||||
document.getElementById('rememberError').style.display = 'block';
|
||||
rememberCheckbox.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
// Scroll to first error
|
||||
const firstError = document.querySelector('.is-invalid');
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize form state
|
||||
checkFormValidity();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Register - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<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-user-plus me-2"></i>Daftar Akun Baru
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ route('register') }}" id="registerForm">
|
||||
@csrf
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="name" class="form-label">Nama Lengkap <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-user"></i></span>
|
||||
<input type="text"
|
||||
class="form-control @error('name') is-invalid @enderror"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
required
|
||||
autocomplete="name"
|
||||
placeholder="Minimal 12 karakter">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Nama minimal harus 12 karakter</small>
|
||||
</div>
|
||||
@error('name')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="nameError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Nama minimal harus 12 karakter</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="contoh@gmail.com">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Harus menggunakan email @gmail.com</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="col-md-6">
|
||||
<label for="phone" class="form-label">No. Telepon <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-phone"></i></span>
|
||||
<input type="tel"
|
||||
class="form-control @error('phone') is-invalid @enderror"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value="{{ old('phone') }}"
|
||||
required
|
||||
placeholder="081234567890">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Hanya angka, minimal 12 karakter</small>
|
||||
</div>
|
||||
@error('phone')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="phoneError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Nomor telepon harus angka dan minimal 12 karakter</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="address" class="form-label">Alamat <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-map-marker-alt"></i></span>
|
||||
<input type="text"
|
||||
class="form-control @error('address') is-invalid @enderror"
|
||||
id="address"
|
||||
name="address"
|
||||
value="{{ old('address') }}"
|
||||
required
|
||||
placeholder="Jl. Contoh No. 123">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Alamat minimal 12 karakter</small>
|
||||
</div>
|
||||
@error('address')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="addressError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Alamat minimal harus 12 karakter</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password" class="form-label">Password <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="Minimal 8 karakter">
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye" id="eyeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Password minimal 8 karakter</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 harus 8 karakter</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password_confirmation" class="form-label">Konfirmasi Password <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="Ulangi password">
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePasswordConfirmation">
|
||||
<i class="fas fa-eye" id="eyeIconConfirm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="passwordMatchError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Konfirmasi password tidak cocok</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input @error('agree') is-invalid @enderror" id="agree" name="agree" required>
|
||||
<label class="form-check-label" for="agree">
|
||||
Saya setuju dengan <a href="#" class="text-success">syarat dan ketentuan</a> <span class="text-danger">*</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('agree')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="agreeError" class="text-danger mt-1" style="display: none;">
|
||||
<small>Anda harus menyetujui syarat dan ketentuan</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success btn-lg" id="submitBtn">
|
||||
<i class="fas fa-user-plus me-2"></i>Daftar Sekarang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">Sudah punya akun?
|
||||
<a href="{{ route('login') }}" class="text-success text-decoration-none fw-bold">
|
||||
Masuk di sini
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('registerForm');
|
||||
const nameInput = document.getElementById('name');
|
||||
const emailInput = document.getElementById('email');
|
||||
const phoneInput = document.getElementById('phone');
|
||||
const addressInput = document.getElementById('address');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const passwordConfirmInput = document.getElementById('password_confirmation');
|
||||
const togglePassword = document.getElementById('togglePassword');
|
||||
const togglePasswordConfirmation = document.getElementById('togglePasswordConfirmation');
|
||||
const eyeIcon = document.getElementById('eyeIcon');
|
||||
const eyeIconConfirm = document.getElementById('eyeIconConfirm');
|
||||
const agreeCheckbox = document.getElementById('agree');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Toggle password visibility
|
||||
togglePassword.addEventListener('click', function() {
|
||||
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
passwordInput.setAttribute('type', type);
|
||||
eyeIcon.classList.toggle('fa-eye');
|
||||
eyeIcon.classList.toggle('fa-eye-slash');
|
||||
});
|
||||
|
||||
togglePasswordConfirmation.addEventListener('click', function() {
|
||||
const type = passwordConfirmInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
passwordConfirmInput.setAttribute('type', type);
|
||||
eyeIconConfirm.classList.toggle('fa-eye');
|
||||
eyeIconConfirm.classList.toggle('fa-eye-slash');
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
nameInput.addEventListener('input', function() {
|
||||
const nameError = document.getElementById('nameError');
|
||||
if (this.value.length < 12) {
|
||||
nameError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
nameError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
emailInput.addEventListener('input', function() {
|
||||
const emailError = document.getElementById('emailError');
|
||||
const gmailPattern = /@gmail\.com$/;
|
||||
if (!gmailPattern.test(this.value) && this.value.length > 0) {
|
||||
emailError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
emailError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
phoneInput.addEventListener('input', function() {
|
||||
const phoneError = document.getElementById('phoneError');
|
||||
const numberPattern = /^[0-9]+$/;
|
||||
|
||||
// Remove non-numeric characters
|
||||
this.value = this.value.replace(/[^0-9]/g, '');
|
||||
|
||||
if (!numberPattern.test(this.value) || this.value.length < 12) {
|
||||
phoneError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
phoneError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
addressInput.addEventListener('input', function() {
|
||||
const addressError = document.getElementById('addressError');
|
||||
if (this.value.length < 12) {
|
||||
addressError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
addressError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
passwordInput.addEventListener('input', function() {
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
if (this.value.length < 8) {
|
||||
passwordError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
passwordError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
checkPasswordMatch();
|
||||
});
|
||||
|
||||
passwordConfirmInput.addEventListener('input', function() {
|
||||
checkPasswordMatch();
|
||||
});
|
||||
|
||||
agreeCheckbox.addEventListener('change', function() {
|
||||
const agreeError = document.getElementById('agreeError');
|
||||
if (!this.checked) {
|
||||
agreeError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
submitBtn.disabled = true;
|
||||
} else {
|
||||
agreeError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function checkPasswordMatch() {
|
||||
const passwordMatchError = document.getElementById('passwordMatchError');
|
||||
if (passwordInput.value !== passwordConfirmInput.value && passwordConfirmInput.value.length > 0) {
|
||||
passwordMatchError.style.display = 'block';
|
||||
passwordConfirmInput.classList.add('is-invalid');
|
||||
return false;
|
||||
} else {
|
||||
passwordMatchError.style.display = 'none';
|
||||
passwordConfirmInput.classList.remove('is-invalid');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation before submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
// Check all validations
|
||||
if (nameInput.value.length < 12) {
|
||||
document.getElementById('nameError').style.display = 'block';
|
||||
nameInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
const gmailPattern = /@gmail\.com$/;
|
||||
if (!gmailPattern.test(emailInput.value)) {
|
||||
document.getElementById('emailError').style.display = 'block';
|
||||
emailInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
const numberPattern = /^[0-9]+$/;
|
||||
if (!numberPattern.test(phoneInput.value) || phoneInput.value.length < 12) {
|
||||
document.getElementById('phoneError').style.display = 'block';
|
||||
phoneInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (addressInput.value.length < 12) {
|
||||
document.getElementById('addressError').style.display = 'block';
|
||||
addressInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (passwordInput.value.length < 8) {
|
||||
document.getElementById('passwordError').style.display = 'block';
|
||||
passwordInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (passwordInput.value !== passwordConfirmInput.value) {
|
||||
document.getElementById('passwordMatchError').style.display = 'block';
|
||||
passwordConfirmInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!agreeCheckbox.checked) {
|
||||
document.getElementById('agreeError').style.display = 'block';
|
||||
agreeCheckbox.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
// Scroll to first error
|
||||
const firstError = document.querySelector('.is-invalid');
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize submit button state
|
||||
submitBtn.disabled = !agreeCheckbox.checked;
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,479 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Checkout - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.ongkir-info {
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
padding: 15px;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ongkir-loading {
|
||||
display: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-section {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h4 class="mb-0"><i class="fas fa-shopping-cart me-2"></i>Form Checkout</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Informasi Produk -->
|
||||
<div class="alert alert-info">
|
||||
<h5>{{ $product->name }}</h5>
|
||||
<p class="mb-1">Harga: Rp {{ number_format($product->price, 0, ',', '.') }} /bibit</p>
|
||||
<p class="mb-0">Jumlah: <strong>{{ $quantity }} bibit</strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Form Checkout -->
|
||||
<form action="{{ route('checkout.process') }}" method="POST" id="checkoutForm">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $product->id }}">
|
||||
<input type="hidden" name="quantity" value="{{ $quantity }}">
|
||||
<input type="hidden" name="shipping_cost" id="shipping_cost_input" value="0">
|
||||
|
||||
<h5 class="mb-3">Data Pembeli</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nama Lengkap *</label>
|
||||
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
|
||||
value="{{ old('name') }}" required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nomor Telepon *</label>
|
||||
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror"
|
||||
value="{{ old('phone') }}" placeholder="08123456789" required>
|
||||
@error('phone')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email *</label>
|
||||
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
|
||||
value="{{ old('email') }}" required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 mt-4">Alamat Pengiriman</h5>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Pengiriman hanya untuk wilayah Bondowoso</strong>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kecamatan *</label>
|
||||
<select name="city" id="kecamatan" class="form-control @error('city') is-invalid @enderror" required>
|
||||
<option value="">-- Pilih Kecamatan --</option>
|
||||
<option value="Bondowoso" {{ old('city') == 'Bondowoso' ? 'selected' : '' }}>Bondowoso (Pusat Kota)</option>
|
||||
<option value="Grujugan" {{ old('city') == 'Grujugan' ? 'selected' : '' }}>Grujugan</option>
|
||||
<option value="Jambesari Darus Sholah" {{ old('city') == 'Jambesari Darus Sholah' ? 'selected' : '' }}>Jambesari Darus Sholah</option>
|
||||
<option value="Klabang" {{ old('city') == 'Klabang' ? 'selected' : '' }}>Klabang</option>
|
||||
<option value="Tenggarang" {{ old('city') == 'Tenggarang' ? 'selected' : '' }}>Tenggarang</option>
|
||||
<option value="Binakal" {{ old('city') == 'Binakal' ? 'selected' : '' }}>Binakal</option>
|
||||
<option value="Prajekan" {{ old('city') == 'Prajekan' ? 'selected' : '' }}>Prajekan</option>
|
||||
<option value="Botolinggo" {{ old('city') == 'Botolinggo' ? 'selected' : '' }}>Botolinggo</option>
|
||||
<option value="Maesan" {{ old('city') == 'Maesan' ? 'selected' : '' }}>Maesan</option>
|
||||
<option value="Tamanan" {{ old('city') == 'Tamanan' ? 'selected' : '' }}>Tamanan</option>
|
||||
<option value="Wonosari" {{ old('city') == 'Wonosari' ? 'selected' : '' }}>Wonosari</option>
|
||||
<option value="Pujer" {{ old('city') == 'Pujer' ? 'selected' : '' }}>Pujer</option>
|
||||
<option value="Tlogosari" {{ old('city') == 'Tlogosari' ? 'selected' : '' }}>Tlogosari</option>
|
||||
<option value="Sukosari" {{ old('city') == 'Sukosari' ? 'selected' : '' }}>Sukosari</option>
|
||||
<option value="Sumberwringin" {{ old('city') == 'Sumberwringin' ? 'selected' : '' }}>Sumberwringin</option>
|
||||
<option value="Tegalampel" {{ old('city') == 'Tegalampel' ? 'selected' : '' }}>Tegalampel</option>
|
||||
<option value="Sempol" {{ old('city') == 'Sempol' ? 'selected' : '' }}>Sempol</option>
|
||||
<option value="Pakem" {{ old('city') == 'Pakem' ? 'selected' : '' }}>Pakem</option>
|
||||
<option value="Curahdami" {{ old('city') == 'Curahdami' ? 'selected' : '' }}>Curahdami</option>
|
||||
<option value="Ijen" {{ old('city') == 'Ijen' ? 'selected' : '' }}>Ijen</option>
|
||||
<option value="Tapen" {{ old('city') == 'Tapen' ? 'selected' : '' }}>Tapen</option>
|
||||
<option value="Wringin" {{ old('city') == 'Wringin' ? 'selected' : '' }}>Wringin</option>
|
||||
<option value="Taman Krocok" {{ old('city') == 'Taman Krocok' ? 'selected' : '' }}>Taman Krocok</option>
|
||||
</select>
|
||||
@error('city')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
<div class="ongkir-loading mt-2">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>Menghitung ongkir...
|
||||
</div>
|
||||
|
||||
<div class="ongkir-info" id="ongkirInfo">
|
||||
<i class="fas fa-truck me-2"></i>
|
||||
<strong>Ongkir ke <span id="ongkirKecamatan"></span>:
|
||||
<span id="ongkirAmount" class="text-success">Rp 0</span>
|
||||
</strong>
|
||||
<div id="ongkirDetail" class="small mt-1 text-muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Alamat Lengkap (Nama Jalan, No Rumah, RT/RW, Desa) *</label>
|
||||
<textarea name="address" class="form-control @error('address') is-invalid @enderror"
|
||||
rows="3" placeholder="Contoh: Jl. Merdeka No. 123, RT 02/RW 05, Desa Kademangan" required>{{ old('address') }}</textarea>
|
||||
@error('address')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="province" value="Jawa Timur">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kode Pos *</label>
|
||||
<input type="text" name="postal_code" class="form-control @error('postal_code') is-invalid @enderror"
|
||||
value="{{ old('postal_code', '68200') }}" placeholder="68200" required>
|
||||
@error('postal_code')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 mt-4">Metode Pembayaran</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pilih Metode Pembayaran *</label>
|
||||
<select name="payment_method" id="paymentMethod" class="form-control @error('payment_method') is-invalid @enderror" required>
|
||||
<option value="">-- Pilih Metode Pembayaran --</option>
|
||||
<option value="qris" {{ old('payment_method') == 'qris' ? 'selected' : '' }}>
|
||||
📱 QRIS (Scan & Pay)
|
||||
</option>
|
||||
<option value="bri" {{ old('payment_method') == 'bri' ? 'selected' : '' }}>
|
||||
🏦 Transfer Bank BRI
|
||||
</option>
|
||||
<option value="dana" {{ old('payment_method') == 'dana' ? 'selected' : '' }}>
|
||||
💳 DANA
|
||||
</option>
|
||||
<option value="seabank" {{ old('payment_method') == 'seabank' ? 'selected' : '' }}>
|
||||
🏦 SeaBank
|
||||
</option>
|
||||
<option value="shopee" {{ old('payment_method') == 'shopee' ? 'selected' : '' }}>
|
||||
🛒 ShopeePay
|
||||
</option>
|
||||
<option value="cod" {{ old('payment_method') == 'cod' ? 'selected' : '' }}>
|
||||
💵 COD (Cash on Delivery)
|
||||
</option>
|
||||
</select>
|
||||
@error('payment_method')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Info Pembayaran -->
|
||||
<div id="paymentInfo" style="display: none;">
|
||||
<!-- QRIS Info -->
|
||||
<!-- Bagian QRIS Info - Ganti bagian ini di checkout.blade.php Anda -->
|
||||
|
||||
<!-- QRIS Info -->
|
||||
<div id="qrisInfo" class="alert alert-info" style="display: none;">
|
||||
<h6><strong>📱 Pembayaran via QRIS</strong></h6>
|
||||
|
||||
<!-- Info NMID -->
|
||||
<div class="alert alert-light border-1 mb-3">
|
||||
<small class="text-muted d-block mb-1"><strong>Data QRIS Anda:</strong></small>
|
||||
<p class="mb-1">Nama: <strong>ACHMAD BAYU AL GHOZALI</strong></p>
|
||||
<p class="mb-0">NMID: <strong>ID1025440155548</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="text-center my-3">
|
||||
<!-- QR Code DANA Anda -->
|
||||
<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') }}"
|
||||
alt="QRIS DANA"
|
||||
style="width: 250px; height: 250px;"
|
||||
class="border p-2 bg-white">
|
||||
|
||||
<!-- Opsi 2: Jika ingin generate dinamis, gunakan NMID Anda -->
|
||||
<!-- Uncomment jika ingin menggunakan -->
|
||||
<!-- <img src="https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=00020101021226670016COM.NOBUBANK.WWW01189360050300000898740214545400008987400303UME51440014ID.CO.QRIS.WWW0215ID10200000000150303UME5204481253033605802ID5925ACHMAD BAYU AL GHOZALI6010Bondowoso61056820062070703A016304"
|
||||
alt="QRIS DANA"
|
||||
style="width: 250px; height: 250px;"
|
||||
class="border p-2 bg-white"> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-1"><strong>Scan QR Code dengan aplikasi berikut:</strong></p>
|
||||
<div class="d-flex justify-content-center gap-2 flex-wrap mt-2">
|
||||
<span class="badge bg-primary">GoPay</span>
|
||||
<span class="badge bg-info">OVO</span>
|
||||
<span class="badge bg-success">DANA</span>
|
||||
<span class="badge bg-warning text-dark">ShopeePay</span>
|
||||
<span class="badge bg-danger">LinkAja</span>
|
||||
<span class="badge bg-secondary">Mobile Banking</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="alert alert-warning mb-0">
|
||||
<strong>⚠️ Penting:</strong>
|
||||
<small class="d-block mt-2">
|
||||
<strong>Jika QR Code tidak muncul:</strong><br>
|
||||
1. Download QR Code QRIS dari aplikasi DANA Anda<br>
|
||||
2. Upload ke folder <code>public/images/qris-dana.png</code> di server<br>
|
||||
3. Atau gunakan opsi 2 di komentar kode di atas
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<small class="text-muted d-block mt-3">
|
||||
<strong>Cara Bayar:</strong><br>
|
||||
1. Buka aplikasi pembayaran digital Anda (GoPay, OVO, DANA, dll)<br>
|
||||
2. Pilih menu "Scan QRIS" atau "Scan QR"<br>
|
||||
3. Arahkan kamera ke QR Code di atas<br>
|
||||
4. Nominal akan otomatis terisi sesuai total pesanan<br>
|
||||
5. Tekan Bayar dan ikuti instruksi aplikasi<br>
|
||||
6. Konfirmasi pembayaran akan langsung diterima
|
||||
</small>
|
||||
</div>
|
||||
<!-- BRI Info -->
|
||||
<div id="briInfo" class="alert alert-info" style="display: none;">
|
||||
<h6><strong>🏦 Transfer ke Rekening BRI</strong></h6>
|
||||
<p class="mb-1">No. Rekening: <strong>1234-5678-9012-3456</strong></p>
|
||||
<p class="mb-1">Atas Nama: <strong>Shop Bibit Cabai Bondowoso</strong></p>
|
||||
<small class="text-muted">Silakan transfer sesuai total pembayaran yang tertera</small>
|
||||
</div>
|
||||
|
||||
<!-- DANA Info -->
|
||||
<div id="danaInfo" class="alert alert-info" style="display: none;">
|
||||
<h6><strong>💳 Transfer ke DANA</strong></h6>
|
||||
<p class="mb-1">Nomor DANA: <strong>0813-3183-0561</strong></p>
|
||||
<p class="mb-1">Atas Nama: <strong>Shop Bibit Cabai Bondowoso</strong></p>
|
||||
<small class="text-muted">Buka aplikasi DANA → Transfer → Masukkan nomor di atas</small>
|
||||
</div>
|
||||
|
||||
<!-- SeaBank Info -->
|
||||
<div id="seabankInfo" class="alert alert-info" style="display: none;">
|
||||
<h6><strong>🏦 Transfer ke SeaBank</strong></h6>
|
||||
<p class="mb-1">No. Rekening: <strong>901234567890</strong></p>
|
||||
<p class="mb-1">Atas Nama: <strong>Shop Bibit Cabai Bondowoso</strong></p>
|
||||
<small class="text-muted">Transfer melalui aplikasi SeaBank atau mobile banking</small>
|
||||
</div>
|
||||
|
||||
<!-- ShopeePay Info -->
|
||||
<div id="shopeeInfo" class="alert alert-info" style="display: none;">
|
||||
<h6><strong>🛒 Transfer ke ShopeePay</strong></h6>
|
||||
<p class="mb-1">Nomor ShopeePay: <strong>0813-3183-0561</strong></p>
|
||||
<p class="mb-1">Atas Nama: <strong>Shop Bibit Cabai Bondowoso</strong></p>
|
||||
<small class="text-muted">Buka aplikasi Shopee → ShopeePay → Transfer → Ke Pengguna</small>
|
||||
</div>
|
||||
|
||||
<!-- COD Info -->
|
||||
<div id="codInfo" class="alert alert-warning" style="display: none;">
|
||||
<h6><strong>💵 Cash on Delivery (COD)</strong></h6>
|
||||
<p class="mb-1">✅ Bayar tunai saat barang diterima</p>
|
||||
<p class="mb-1">✅ Pastikan uang pas sesuai total pembayaran</p>
|
||||
<small class="text-muted">Pembayaran dilakukan langsung kepada kurir saat pengiriman</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Konfirmasi Pembayaran:</strong> Setelah pembayaran berhasil, mohon konfirmasi melalui WhatsApp ke <strong>081331830561</strong> dengan menyertakan bukti pembayaran dan nomor invoice.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Catatan (Opsional)</label>
|
||||
<textarea name="notes" class="form-control" rows="2"
|
||||
placeholder="Tambahkan catatan jika ada">{{ old('notes') }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Ringkasan Total -->
|
||||
<div class="total-section">
|
||||
<h5 class="mb-3"><i class="fas fa-receipt me-2"></i>Ringkasan Pembayaran</h5>
|
||||
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Subtotal ({{ $quantity }} bibit)</span>
|
||||
<span><strong id="subtotalDisplay">Rp {{ number_format($product->price * $quantity, 0, ',', '.') }}</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Ongkos Kirim</span>
|
||||
<span><strong id="ongkirDisplay">Rp 0</strong></span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<h5 class="mb-0">Total Pembayaran</h5>
|
||||
<h5 class="mb-0 text-success"><strong id="totalDisplay">Rp {{ number_format($product->price * $quantity, 0, ',', '.') }}</strong></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-check-circle me-2"></i>Proses Checkout
|
||||
</button>
|
||||
<a href="{{ route('home') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const kecamatanSelect = document.getElementById('kecamatan');
|
||||
const ongkirInfo = document.getElementById('ongkirInfo');
|
||||
const ongkirLoading = document.querySelector('.ongkir-loading');
|
||||
const quantity = {{ $quantity }};
|
||||
const subtotal = {{ $product->price * $quantity }};
|
||||
|
||||
// Data ongkir per kecamatan
|
||||
const 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
|
||||
};
|
||||
|
||||
// Handler untuk perhitungan ongkir
|
||||
kecamatanSelect.addEventListener('change', function() {
|
||||
const kecamatan = this.value;
|
||||
|
||||
if (!kecamatan) {
|
||||
ongkirInfo.style.display = 'none';
|
||||
updateTotal(0);
|
||||
return;
|
||||
}
|
||||
|
||||
ongkirLoading.style.display = 'block';
|
||||
ongkirInfo.style.display = 'none';
|
||||
|
||||
setTimeout(() => {
|
||||
const baseOngkir = ongkirData[kecamatan] || 15000;
|
||||
let totalOngkir = baseOngkir;
|
||||
let detail = `Base ongkir: Rp ${formatRupiah(baseOngkir)}`;
|
||||
|
||||
if (quantity > 5) {
|
||||
const extraBibit = quantity - 5;
|
||||
const extraCost = extraBibit * 5000;
|
||||
totalOngkir += extraCost;
|
||||
detail += ` + Tambahan ${extraBibit} bibit: Rp ${formatRupiah(extraCost)}`;
|
||||
}
|
||||
|
||||
if (quantity >= 100) {
|
||||
totalOngkir = 0;
|
||||
detail = '🎉 GRATIS ONGKIR untuk pembelian 100+ bibit!';
|
||||
}
|
||||
|
||||
document.getElementById('ongkirKecamatan').textContent = kecamatan;
|
||||
document.getElementById('ongkirAmount').textContent = 'Rp ' + formatRupiah(totalOngkir);
|
||||
document.getElementById('ongkirDetail').textContent = detail;
|
||||
document.getElementById('shipping_cost_input').value = totalOngkir;
|
||||
|
||||
ongkirLoading.style.display = 'none';
|
||||
ongkirInfo.style.display = 'block';
|
||||
|
||||
updateTotal(totalOngkir);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Handler untuk metode pembayaran
|
||||
const paymentMethod = document.getElementById('paymentMethod');
|
||||
const paymentInfo = document.getElementById('paymentInfo');
|
||||
const qrisInfo = document.getElementById('qrisInfo');
|
||||
const briInfo = document.getElementById('briInfo');
|
||||
const danaInfo = document.getElementById('danaInfo');
|
||||
const seabankInfo = document.getElementById('seabankInfo');
|
||||
const shopeeInfo = document.getElementById('shopeeInfo');
|
||||
const codInfo = document.getElementById('codInfo');
|
||||
|
||||
paymentMethod.addEventListener('change', function() {
|
||||
const method = this.value;
|
||||
|
||||
// Sembunyikan semua info
|
||||
paymentInfo.style.display = 'none';
|
||||
qrisInfo.style.display = 'none';
|
||||
briInfo.style.display = 'none';
|
||||
danaInfo.style.display = 'none';
|
||||
seabankInfo.style.display = 'none';
|
||||
shopeeInfo.style.display = 'none';
|
||||
codInfo.style.display = 'none';
|
||||
|
||||
// Tampilkan info sesuai metode yang dipilih
|
||||
if (method) {
|
||||
paymentInfo.style.display = 'block';
|
||||
|
||||
switch(method) {
|
||||
case 'qris':
|
||||
qrisInfo.style.display = 'block';
|
||||
break;
|
||||
case 'bri':
|
||||
briInfo.style.display = 'block';
|
||||
break;
|
||||
case 'dana':
|
||||
danaInfo.style.display = 'block';
|
||||
break;
|
||||
case 'seabank':
|
||||
seabankInfo.style.display = 'block';
|
||||
break;
|
||||
case 'shopee':
|
||||
shopeeInfo.style.display = 'block';
|
||||
break;
|
||||
case 'cod':
|
||||
codInfo.style.display = 'block';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateTotal(ongkir) {
|
||||
const total = subtotal + ongkir;
|
||||
document.getElementById('ongkirDisplay').textContent = 'Rp ' + formatRupiah(ongkir);
|
||||
document.getElementById('totalDisplay').textContent = 'Rp ' + formatRupiah(total);
|
||||
}
|
||||
|
||||
function formatRupiah(angka) {
|
||||
return angka.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Pesanan Berhasil - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.success-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 30px;
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.success-card {
|
||||
border: none;
|
||||
box-shadow: 0 5px 25px rgba(0,0,0,0.1);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.order-detail-row {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.order-detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bank-account {
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #28a745;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card success-card">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="success-icon">
|
||||
<i class="fas fa-check fa-3x text-white"></i>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="alert alert-success" role="alert">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Nomor Invoice: <strong>{{ $transaksi->invoice_number }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Pesanan -->
|
||||
<div class="card success-card mt-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-4">
|
||||
<i class="fas fa-clipboard-list me-2 text-success"></i>Detail Pesanan
|
||||
</h5>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Invoice:</strong></div>
|
||||
<div class="col-6 text-end">{{ $transaksi->invoice_number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Status Pesanan:</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="badge bg-warning text-dark">Menunggu Pembayaran</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Metode Pembayaran:</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
@if($transaksi->payment_method == 'qris')
|
||||
<i class="fas fa-qrcode me-1"></i>QRIS
|
||||
@elseif($transaksi->payment_method == 'bri')
|
||||
<i class="fas fa-university me-1"></i>Transfer Bank BRI
|
||||
@elseif($transaksi->payment_method == 'dana')
|
||||
<i class="fas fa-wallet me-1"></i>DANA
|
||||
@elseif($transaksi->payment_method == 'seabank')
|
||||
<i class="fas fa-university me-1"></i>SeaBank
|
||||
@elseif($transaksi->payment_method == 'shopee')
|
||||
<i class="fas fa-shopping-bag me-1"></i>ShopeePay
|
||||
@else
|
||||
<i class="fas fa-money-bill-wave me-1"></i>COD
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data Pembeli -->
|
||||
<div class="card success-card mt-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-4">
|
||||
<i class="fas fa-user me-2 text-success"></i>Data Pembeli
|
||||
</h5>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Nama:</strong></div>
|
||||
<div class="col-6 text-end">{{ $transaksi->customer_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Telepon:</strong></div>
|
||||
<div class="col-6 text-end">{{ $transaksi->customer_phone }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-6"><strong>Email:</strong></div>
|
||||
<div class="col-6 text-end">{{ $transaksi->customer_email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-row">
|
||||
<div class="row">
|
||||
<div class="col-12"><strong>Alamat Pengiriman:</strong></div>
|
||||
<div class="col-12 mt-2 text-muted">
|
||||
{{ $transaksi->shipping_address }}<br>
|
||||
{{ $transaksi->city }}, {{ $transaksi->province }} {{ $transaksi->postal_code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Produk -->
|
||||
<div class="card success-card mt-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-4">
|
||||
<i class="fas fa-box me-2 text-success"></i>Produk yang Dipesan
|
||||
</h5>
|
||||
|
||||
@foreach($transaksi->details as $detail)
|
||||
<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' }}"
|
||||
alt="{{ $detail->product_name }}"
|
||||
class="rounded me-3"
|
||||
style="width: 80px; height: 80px; object-fit: cover;">
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">{{ $detail->product_name }}</h6>
|
||||
<p class="text-muted mb-0">{{ number_format($detail->quantity) }} bibit × Rp {{ number_format($detail->price, 0, ',', '.') }}</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<strong class="text-success">Rp {{ number_format($detail->subtotal, 0, ',', '.') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Subtotal</span>
|
||||
<span>Rp {{ number_format($transaksi->subtotal, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span>Ongkos Kirim</span>
|
||||
<span>Rp {{ number_format($transaksi->shipping_cost, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<h5 class="fw-bold">Total</h5>
|
||||
<h5 class="fw-bold text-success">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informasi Transfer -->
|
||||
<!-- Informasi Transfer -->
|
||||
@if(in_array($transaksi->payment_method, ['qris', 'bri', 'dana', 'seabank', 'shopee']))
|
||||
<div class="card success-card mt-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-4">
|
||||
<i class="fas fa-university me-2 text-success"></i>Informasi Pembayaran
|
||||
</h5>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Silakan lakukan pembayaran sesuai metode yang Anda pilih
|
||||
</div>
|
||||
|
||||
@if($transaksi->payment_method == 'qris')
|
||||
<!-- QRIS Info -->
|
||||
<div class="payment-info">
|
||||
<p class="mb-1"><strong>📌 Data QRIS:</strong></p>
|
||||
<p class="mb-1">Nama: <strong>ACHMAD BAYU AL GHOZALI</strong></p>
|
||||
<p class="mb-0">NMID: <strong>ID1025440155548</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center my-3">
|
||||
<!-- QR Code DANA Anda -->
|
||||
<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') }}"
|
||||
alt="QRIS DANA"
|
||||
style="width: 400px; height: 400px;"
|
||||
class="border p-2 bg-white">
|
||||
<p class="mb-2"><strong>Total Pembayaran:</strong></p>
|
||||
<h4 class="text-success mb-3">Rp {{ number_format($transaksi->total_amount, 0, ',', '.') }}</h4>
|
||||
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
||||
<span class="badge bg-primary">GoPay</span>
|
||||
<span class="badge bg-info">OVO</span>
|
||||
<span class="badge bg-success">DANA</span>
|
||||
<span class="badge bg-warning text-dark">ShopeePay</span>
|
||||
<span class="badge bg-danger">LinkAja</span>
|
||||
</div>
|
||||
<small class="d-block mt-3 text-muted">
|
||||
Scan QR Code dengan aplikasi pembayaran digital apa saja yang mendukung QRIS
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@elseif($transaksi->payment_method == 'bri')
|
||||
<div class="bank-account">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/2/2e/BRI_2020.svg"
|
||||
alt="BRI" style="height: 30px;" class="me-2">
|
||||
<strong>Bank BRI</strong>
|
||||
</div>
|
||||
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('1234567890123456')" title="Copy"></i>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@elseif($transaksi->payment_method == 'dana')
|
||||
<div class="bank-account">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>💳 DANA</strong>
|
||||
</div>
|
||||
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@elseif($transaksi->payment_method == 'seabank')
|
||||
<div class="bank-account">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>🏦 SeaBank</strong>
|
||||
</div>
|
||||
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('901234567890')" title="Copy"></i>
|
||||
</div>
|
||||
<p class="mb-0">No. Rekening: <strong>901234567890</strong></p>
|
||||
<p class="mb-0 text-muted">a.n. Shop Bibit Cabai Bondowoso</p>
|
||||
</div>
|
||||
|
||||
@elseif($transaksi->payment_method == 'shopee')
|
||||
<div class="bank-account">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>🛒 ShopeePay</strong>
|
||||
</div>
|
||||
<i class="fas fa-copy copy-btn text-success" onclick="copyToClipboard('081234567890')" title="Copy"></i>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="alert alert-info mt-3 mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Setelah pembayaran berhasil, mohon konfirmasi melalui WhatsApp ke <strong>0813-3183-0561</strong> dengan menyertakan <strong>bukti pembayaran</strong> dan <strong>nomor invoice: {{ $transaksi->invoice_number }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<!-- Actions -->
|
||||
<div class="card success-card mt-4">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<a href="{{ route('home') }}" class="btn btn-outline-success w-100">
|
||||
<i class="fas fa-home me-2"></i>Kembali ke Beranda
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button onclick="window.print()" class="btn btn-success w-100">
|
||||
<i class="fas fa-print me-2"></i>Cetak Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="https://wa.me/6282313155053?text=Halo, saya ingin konfirmasi pembayaran dengan invoice {{ $transaksi->invoice_number }}"
|
||||
target="_blank"
|
||||
class="btn btn-success w-100">
|
||||
<i class="fab fa-whatsapp me-2"></i>Konfirmasi via WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert('Nomor rekening berhasil disalin!');
|
||||
}, function(err) {
|
||||
console.error('Gagal menyalin: ', err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Contact Us - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-5">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4 fw-bold text-success">Contact Us</h1>
|
||||
<p class="lead text-muted">Hubungi kami untuk informasi lebih lanjut tentang bibit cabai berkualitas</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<!-- Contact Information -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="text-success mb-4">
|
||||
<i class="fas fa-store me-2"></i>Shop Bibit Cabai Bondowoso
|
||||
</h3>
|
||||
|
||||
<div class="contact-info">
|
||||
<div class="mb-4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5><i class="fas fa-phone text-success me-2"></i>Telepon</h5>
|
||||
<p class="text-muted">
|
||||
<a href="tel:+62338421234" class="text-decoration-none">+62 338 421 234</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5><i class="fas fa-envelope text-success me-2"></i>Email</h5>
|
||||
<p class="text-muted">
|
||||
<a href="mailto:info@bibitcabaibondowoso.com" class="text-decoration-none">
|
||||
info@bibitcabaibondowoso.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5><i class="fab fa-whatsapp text-success me-2"></i>WhatsApp</h5>
|
||||
<p class="text-muted">
|
||||
<a href="https://wa.me/6281234567890" class="text-decoration-none" target="_blank">
|
||||
+62 812 3456 7890
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5><i class="fas fa-clock text-success me-2"></i>Jam Operasional</h5>
|
||||
<p class="text-muted mb-1">Senin - Jumat: 08:00 - 17:00</p>
|
||||
<p class="text-muted mb-1">Sabtu: 08:00 - 15:00</p>
|
||||
<p class="text-muted">Minggu: Tutup</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="text-success mb-4">
|
||||
<i class="fas fa-paper-plane me-2"></i>Kirim Pesan
|
||||
</h3>
|
||||
|
||||
@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
|
||||
|
||||
<form action="{{ route('contact.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="name" class="form-label">Nama Lengkap <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control @error('name') is-invalid @enderror"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="phone" class="form-label">No. Telepon</label>
|
||||
<input type="tel"
|
||||
class="form-control @error('phone') is-invalid @enderror"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value="{{ old('phone') }}">
|
||||
@error('phone')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="subject" class="form-label">Subjek <span class="text-danger">*</span></label>
|
||||
<input type="text"
|
||||
class="form-control @error('subject') is-invalid @enderror"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value="{{ old('subject') }}"
|
||||
required>
|
||||
@error('subject')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="message" class="form-label">Pesan <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control @error('message') is-invalid @enderror"
|
||||
id="message"
|
||||
name="message"
|
||||
rows="5"
|
||||
required>{{ old('message') }}</textarea>
|
||||
@error('message')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-success btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i>Kirim Pesan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Section (Optional) -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-map me-2"></i>Lokasi Kami</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<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"
|
||||
style="border:0;"
|
||||
allowfullscreen=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Home - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.product-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.product-card:hover .product-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.badge-bestseller {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
z-index: 2;
|
||||
box-shadow: 0 2px 10px rgba(238, 90, 36, 0.3);
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid p-0">
|
||||
<!-- Hero Section -->
|
||||
<section class="bg-success text-white py-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="display-4 fw-bold mb-4">Bibit Cabai Berkualitas</h1>
|
||||
<p class="lead mb-4">Dapatkan bibit cabai terbaik dari Bondowoso untuk hasil panen yang optimal.</p>
|
||||
<a href="{{ route('products.best-selling') }}" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-fire me-2"></i>Lihat Produk Terlaris
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 text-center">
|
||||
<img src="{{ asset('images/cabai-hero.jpg') }}"
|
||||
alt="Bibit Cabai"
|
||||
class="img-fluid hero-image"
|
||||
onerror="this.src='https://via.placeholder.com/400x300/28a745/ffffff?text=Bibit+Cabai+Hero'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Produk Terlaris Section -->
|
||||
<section class="py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="display-5 fw-bold text-success">BIBIT TERLARIS</h2>
|
||||
<p class="lead text-muted">Produk pilihan petani Bondowoso</p>
|
||||
</div>
|
||||
|
||||
@if($featuredProducts->count() > 0)
|
||||
<div class="row g-4">
|
||||
@foreach($featuredProducts as $product)
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card product-card h-100 position-relative">
|
||||
@if($product->total_sold > 0)
|
||||
<div class="badge-bestseller">
|
||||
<i class="fas fa-fire"></i> Terlaris
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<img src="{{ $product->image_url }}"
|
||||
class="card-img-top product-image"
|
||||
alt="{{ $product->name }}"
|
||||
loading="lazy"
|
||||
onerror="this.src='https://via.placeholder.com/300x250/28a745/ffffff?text={{ urlencode($product->name) }}'">
|
||||
</div>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">{{ $product->name }}</h5>
|
||||
<div class="price-text mb-3">{{ $product->formatted_price }}</div>
|
||||
|
||||
@if($product->total_sold > 0)
|
||||
<small class="text-muted mb-3">
|
||||
<i class="fas fa-chart-line me-1"></i>
|
||||
Terjual: {{ number_format($product->total_sold) }} bibit
|
||||
</small>
|
||||
@endif
|
||||
|
||||
<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({{ $product->id }})">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control quantity-input" id="qty-{{ $product->id }}" value="1" min="1" max="{{ $product->stock }}">
|
||||
<button class="btn btn-outline-success" type="button" onclick="increaseQty({{ $product->id }}, {{ $product->stock }})">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success w-100" onclick="checkout({{ $product->id }})">
|
||||
<i class="fas fa-shopping-cart me-1"></i>
|
||||
Beli Sekarang
|
||||
</button>
|
||||
@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 class="text-center mt-5">
|
||||
<a href="{{ route('products.best-selling') }}" class="btn btn-outline-success btn-lg">
|
||||
Lihat Semua Produk Terlaris <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Belum ada data penjualan. Produk terlaris akan muncul setelah ada transaksi.
|
||||
</div>
|
||||
|
||||
<!-- Sample Products Placeholder -->
|
||||
<div class="row g-4 mt-4">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card product-card h-100">
|
||||
<img src="https://via.placeholder.com/300x250/28a745/ffffff?text=Bibit+Cabai+Merah"
|
||||
class="card-img-top product-image"
|
||||
alt="Sample Product">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Bibit Cabai Merah (Sample)</h5>
|
||||
<div class="price-text mb-3">Rp 15.000</div>
|
||||
<button class="btn btn-secondary w-100" disabled>
|
||||
<i class="fas fa-shopping-cart me-1"></i>Sample Product
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="py-5 bg-white">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<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;">
|
||||
<i class="fas fa-seedling fa-2x"></i>
|
||||
</div>
|
||||
<h5>Bibit Berkualitas</h5>
|
||||
<p class="text-muted">Bibit pilihan dengan kualitas terjamin dan hasil optimal.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<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;">
|
||||
<i class="fas fa-truck fa-2x"></i>
|
||||
</div>
|
||||
<h5>Pengiriman Cepat</h5>
|
||||
<p class="text-muted">Pengiriman ke seluruh Indonesia dengan packaging aman.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<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;">
|
||||
<i class="fas fa-medal fa-2x"></i>
|
||||
</div>
|
||||
<h5>Garansi Tumbuh</h5>
|
||||
<p class="text-muted">Garansi bibit tumbuh atau uang kembali 100%.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<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;">
|
||||
<i class="fas fa-headset fa-2x"></i>
|
||||
</div>
|
||||
<h5>Support 24/7</h5>
|
||||
<p class="text-muted">Tim support siap membantu Anda kapan saja.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'TA-Bibit')</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">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<style>
|
||||
.admin-link {
|
||||
background: linear-gradient(45deg, #28a745, #20c997) !important;
|
||||
border: none !important;
|
||||
font-weight: 600 !important;
|
||||
color: white !important;
|
||||
transition: all 0.3s ease !important;
|
||||
box-shadow: 0 2px 10px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
.admin-link:hover {
|
||||
background: linear-gradient(45deg, #20c997, #17a2b8) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.admin-notification {
|
||||
position: relative;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
background: linear-gradient(45deg, #fd7e14, #e83e8c);
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 6px;
|
||||
margin-left: 5px;
|
||||
animation: blink 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.dropdown-item.admin-access {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.dropdown-item.admin-access:hover {
|
||||
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
|
||||
color: white !important;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@yield('styles')
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="{{ route('home') }}">
|
||||
<i class="fas fa-seedling me-2"></i>Bibit Cabai
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{{ route('home') }}">Beranda</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{{ route('products.best-selling') }}">Produk Terlaris</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{{ route('contact') }}">Hubungi Kami</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
@auth
|
||||
<!-- Admin Dashboard Access -->
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link admin-link admin-notification px-3 py-2 rounded"
|
||||
href="{{ route('admin.dashboard') }}"
|
||||
title="Dashboard Admin">
|
||||
<i class="fas fa-crown me-1"></i>Admin Panel
|
||||
<span class="badge admin-badge">NEW</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<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">
|
||||
<i class="fas fa-user me-1"></i>{{ Auth::user()->name }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item admin-access" href="{{ route('admin.dashboard') }}">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard Admin
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-user-circle me-2"></i>Profil Saya
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-box me-2"></i>Pesanan Saya
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-cog me-2"></i>Pengaturan
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{{ route('logout') }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>Keluar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@else
|
||||
<!-- Admin Access for Non-Authenticated Users -->
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link admin-link px-3 py-2 rounded"
|
||||
href="{{ route('admin.login.form') }}"
|
||||
title="Login Admin">
|
||||
<i class="fas fa-user-shield me-1"></i>Admin Login
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{{ route('login') }}">
|
||||
<i class="fas fa-sign-in-alt me-1"></i>Login
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{{ route('register') }}">
|
||||
<i class="fas fa-user-plus me-1"></i>Register
|
||||
</a>
|
||||
</li>
|
||||
@endauth
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
@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" aria-label="Close"></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" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Admin Welcome Banner (for authenticated users) -->
|
||||
@auth
|
||||
<div 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="d-flex align-items-center justify-content-between flex-wrap">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span>Selamat datang, <strong>{{ Auth::user()->name }}</strong>! Akses dashboard admin untuk mengelola produk dan laporan.</span>
|
||||
</div>
|
||||
<div class="mt-2 mt-sm-0">
|
||||
<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
|
||||
</a>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-dark text-white py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-seedling me-2"></i>Bibit Cabai</h5>
|
||||
<p>Platform terbaik untuk kebutuhan bibit dan tanaman Anda.</p>
|
||||
<div class="mt-3">
|
||||
@auth
|
||||
<a href="{{ route('admin.dashboard') }}" class="btn btn-outline-success btn-sm">
|
||||
<i class="fas fa-tools me-1"></i>Admin Panel
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('admin.login.form') }}" class="btn btn-outline-success btn-sm">
|
||||
<i class="fas fa-tools me-1"></i>Admin Panel
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Hubungi Kami</h5>
|
||||
<p><i class="fas fa-envelope me-2"></i>info@tabibit.com</p>
|
||||
<p><i class="fas fa-phone me-2"></i>+62 123 456 789</p>
|
||||
<p><i class="fas fa-shield-alt me-2"></i>Admin: admin@tabibit.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<p>© {{ date('Y') }} Bibit Cabai. All rights reserved. |
|
||||
@auth
|
||||
<a href="{{ route('admin.dashboard') }}" class="text-success text-decoration-none">Admin Panel</a>
|
||||
@else
|
||||
<a href="{{ route('admin.login.form') }}" class="text-success text-decoration-none">Admin Panel</a>
|
||||
@endauth
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom Scripts -->
|
||||
<script>
|
||||
// Auto-hide alerts
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const alerts = document.querySelectorAll('.alert:not(.alert-info)');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
if (alert.parentNode) {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Hide admin banner after 10 seconds
|
||||
const adminBanner = document.querySelector('.alert-info');
|
||||
if (adminBanner) {
|
||||
setTimeout(function() {
|
||||
if (adminBanner.parentNode) {
|
||||
const bsAlert = new bootstrap.Alert(adminBanner);
|
||||
bsAlert.close();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@yield('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Produk Terlaris - Shop Bibit Cabai Bondowoso')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-5">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4 fw-bold text-success">
|
||||
<i class="fas fa-fire text-warning me-3"></i>BIBIT TERLARIS
|
||||
</h1>
|
||||
<p class="lead text-muted">Bibit cabai paling diminati petani di Bondowoso</p>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
@if(isset($statistics) && $statistics['total_products_sold'] > 0)
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card text-center">
|
||||
<i class="fas fa-seedling fa-2x mb-3"></i>
|
||||
<h3>{{ number_format($statistics['total_products_sold']) }}</h3>
|
||||
<p class="mb-0">Total Bibit Terjual</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card text-center">
|
||||
<i class="fas fa-shopping-bag fa-2x mb-3"></i>
|
||||
<h3>{{ number_format($statistics['total_completed_orders']) }}</h3>
|
||||
<p class="mb-0">Pesanan Selesai</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card text-center">
|
||||
<i class="fas fa-crown fa-2x mb-3"></i>
|
||||
<h3>{{ $statistics['most_popular_product']->name ?? 'Belum Ada' }}</h3>
|
||||
<p class="mb-0">Produk Terpopuler</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Products Grid -->
|
||||
@if($bestSellingProducts->count() > 0)
|
||||
<div class="row g-4">
|
||||
@foreach($bestSellingProducts as $index => $product)
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card product-card h-100 shadow-sm">
|
||||
<!-- Ranking Badge untuk Top 3 -->
|
||||
@if($index < 3)
|
||||
<div class="position-absolute top-0 start-0 m-2" style="z-index: 10;">
|
||||
<span class="badge badge-ranking">
|
||||
<i class="fas fa-trophy me-1"></i>#{{ $index + 1 }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Badge Terjual -->
|
||||
<div class="position-absolute top-0 end-0 m-2" style="z-index: 10;">
|
||||
<span class="badge bg-danger">
|
||||
<i class="fas fa-fire"></i> {{ number_format($product->total_sold) }} terjual
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Product Image -->
|
||||
<img src="{{ asset('storage/' . $product->image) }}"
|
||||
class="card-img-top"
|
||||
alt="{{ $product->name }}"
|
||||
style="height: 250px; object-fit: cover;">
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Product Name -->
|
||||
<h5 class="card-title mb-2">{{ $product->name }}</h5>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="price-tag mb-3">
|
||||
Rp {{ number_format($product->price, 0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<!-- Stock Info -->
|
||||
<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>
|
||||
<span class="badge {{ $product->stock > 0 ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ $product->stock > 0 ? 'Tersedia' : 'Habis' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Sales Statistics -->
|
||||
<div class="sales-info mb-3 p-2 bg-light rounded">
|
||||
<small class="d-flex justify-content-between">
|
||||
<span><i class="fas fa-chart-line me-1 text-success"></i>{{ number_format($product->total_sold) }} terjual</span>
|
||||
<span><i class="fas fa-users me-1 text-primary"></i>{{ number_format($product->total_orders) }} pembeli</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Form -->
|
||||
<div class="mt-auto">
|
||||
@if($product->stock > 0)
|
||||
<div class="input-group mb-3">
|
||||
<button class="btn btn-outline-success"
|
||||
type="button"
|
||||
onclick="decreaseQty({{ $product->id }})">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="form-control quantity-input text-center"
|
||||
id="qty-{{ $product->id }}"
|
||||
value="1"
|
||||
min="1"
|
||||
max="{{ $product->stock }}">
|
||||
<button class="btn btn-outline-success"
|
||||
type="button"
|
||||
onclick="increaseQty({{ $product->id }}, {{ $product->stock }})">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success w-100"
|
||||
onclick="checkout({{ $product->id }})">
|
||||
<i class="fas fa-shopping-cart me-1"></i>
|
||||
Beli Sekarang
|
||||
</button>
|
||||
@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>
|
||||
@else
|
||||
<!-- No Data State -->
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-chart-line fa-5x text-muted"></i>
|
||||
</div>
|
||||
<h3 class="text-muted mb-3">Belum Ada Produk Terlaris</h3>
|
||||
<p class="text-muted mb-4">
|
||||
Produk terlaris akan muncul setelah ada transaksi yang selesai.<br>
|
||||
Saat ini belum ada produk yang terjual.
|
||||
</p>
|
||||
<a href="{{ route('home') }}" class="btn btn-success">
|
||||
<i class="fas fa-home me-2"></i>Kembali ke Beranda
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.product-card {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
.badge-ranking {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stats-card i {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stats-card p {
|
||||
color: #6c757d;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
border-left: 3px solid #28a745;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quantity-input::-webkit-outer-spin-button,
|
||||
.quantity-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quantity-input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.display-4 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@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
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\ProductRecommendationController;
|
||||
use App\Http\Controllers\ContactController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\RegisterController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\CheckoutController;
|
||||
use App\Http\Controllers\TransaksiController;
|
||||
use App\Http\Controllers\UserController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Public Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Homepage
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
|
||||
// Produk terlaris
|
||||
Route::get('/produk-terlaris', [ProductRecommendationController::class, 'index'])
|
||||
->name('products.best-selling');
|
||||
|
||||
|
||||
// Contact Us
|
||||
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
|
||||
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Checkout Routes - PINDAH KE SINI (di luar admin group)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::prefix('checkout')->name('checkout.')->group(function () {
|
||||
Route::get('/', [CheckoutController::class, 'index'])->name('index');
|
||||
Route::post('/process', [CheckoutController::class, 'process'])->name('process');
|
||||
Route::get('/success/{id}', [CheckoutController::class, 'success'])->name('success');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/login', [LoginController::class, 'login']);
|
||||
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register');
|
||||
Route::post('/register', [RegisterController::class, 'register']);
|
||||
});
|
||||
|
||||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::prefix('api')->name('api.')->group(function () {
|
||||
Route::get('/best-selling-products', [ProductRecommendationController::class, 'apiGetBestSelling'])
|
||||
->name('best-selling-products');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
|
||||
// Admin login routes (accessible to guests)
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', function () {
|
||||
return view('admin.login');
|
||||
})->name('login.form');
|
||||
|
||||
Route::post('/login', [AdminController::class, 'login'])->name('login');
|
||||
});
|
||||
|
||||
// Protected admin routes (requires authentication)
|
||||
Route::middleware(['auth', 'web'])->group(function () {
|
||||
|
||||
// Dashboard
|
||||
Route::get('/dashboard', function () {
|
||||
$totalProducts = \App\Models\Product::count();
|
||||
$activeProducts = \App\Models\Product::where('status', 'aktif')->count();
|
||||
$totalSold = \App\Models\Product::sum('sold') ?? 0;
|
||||
$lowStock = \App\Models\Product::where('stock', '<=', 10)->count();
|
||||
|
||||
return view('admin.dashboard', compact('totalProducts', 'activeProducts', 'totalSold', 'lowStock'));
|
||||
})->name('dashboard');
|
||||
|
||||
// Product CRUD Routes
|
||||
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
||||
Route::get('/products/create', [ProductController::class, 'create'])->name('products.create');
|
||||
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
|
||||
Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
|
||||
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
|
||||
Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update');
|
||||
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.delete');
|
||||
|
||||
// Transaksi Management Routes
|
||||
Route::get('/transaksis', [TransaksiController::class, 'index'])->name('transaksis.index');
|
||||
Route::get('/transaksis/{id}', [TransaksiController::class, 'show'])->name('transaksis.show');
|
||||
Route::get('/transaksis/{id}/edit', [TransaksiController::class, 'edit'])->name('transaksis.edit');
|
||||
Route::put('/transaksis/{id}', [TransaksiController::class, 'update'])->name('transaksis.update');
|
||||
Route::delete('/transaksis/{id}', [TransaksiController::class, 'destroy'])->name('transaksis.destroy');
|
||||
Route::post('/transaksis/{id}/update-status', [TransaksiController::class, 'updateStatus'])->name('transaksis.update-status');
|
||||
Route::post('/transaksis/{id}/update-payment-status', [TransaksiController::class, 'updatePaymentStatus'])->name('transaksis.update-payment-status');
|
||||
Route::post('/transaksis/{id}/cancel', [TransaksiController::class, 'cancel'])->name('transaksis.cancel');
|
||||
Route::get('/transaksis-export', [TransaksiController::class, 'export'])->name('transaksis.export');
|
||||
Route::get('/transaksis/{id}/print', [TransaksiController::class, 'printInvoice'])->name('transaksis.print');
|
||||
|
||||
|
||||
Route::get('/users', [UserController::class, 'index'])->name('users.index');
|
||||
Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
|
||||
Route::post('/users', [UserController::class, 'store'])->name('users.store');
|
||||
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
|
||||
Route::get('/users/{user}/edit', [UserController::class, 'edit'])->name('users.edit');
|
||||
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
|
||||
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
|
||||
|
||||
|
||||
// Other admin routes
|
||||
Route::get('/orders', [AdminController::class, 'orders'])->name('orders');
|
||||
Route::get('/users', [AdminController::class, 'users'])->name('users');
|
||||
Route::get('/reports', [AdminController::class, 'reports'])->name('reports');
|
||||
Route::get('/settings', [AdminController::class, 'settings'])->name('settings');
|
||||
|
||||
// Admin logout
|
||||
Route::post('/logout', [AdminController::class, 'logout'])->name('logout');
|
||||
});
|
||||
}); // ← Penutup admin group yang benar (HANYA SATU)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue