Compare commits
10 Commits
2fe3173817
...
482ce9f3b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482ce9f3b8 | ||
|
|
30fcfb4f20 | ||
|
|
f8529b2459 | ||
|
|
23cc07596c | ||
|
|
2d5d0b8eb1 | ||
|
|
3fcdaa7475 | ||
|
|
044dffefa8 | ||
|
|
b059d5a260 | ||
|
|
85e04dbf6a | ||
|
|
43d399f07a |
@ -36,7 +36,7 @@ public function create(): Response
|
||||
->whereNull('order_id')
|
||||
->latest()
|
||||
->get(),
|
||||
'orderStatus' => OrderStatus::options(),
|
||||
'orderStatus' => collect(OrderStatus::options())->filter(fn ($opt) => $opt['value'] !== OrderStatus::CANCELLED->value)->values()->toArray(),
|
||||
'orderChannels' => OrderChannel::options(),
|
||||
'paymentMethods' => PaymentMethod::options(),
|
||||
'priceTypes' => PriceType::options(),
|
||||
@ -79,7 +79,11 @@ public function store(OrderRequest $request): RedirectResponse
|
||||
'price_type' => $item['price_type'],
|
||||
]);
|
||||
|
||||
Product::find($item['product_id'])->decrement('stock', $item['qty']);
|
||||
$product = Product::find($item['product_id']);
|
||||
if ($product->stock < $item['qty']) {
|
||||
throw new \InvalidArgumentException("Stok produk {$product->name} tidak mencukupi. Sisa stok: {$product->stock}");
|
||||
}
|
||||
$product->decrement('stock', $item['qty']);
|
||||
}
|
||||
|
||||
OrderItem::where('user_id', auth()->id())
|
||||
@ -88,6 +92,8 @@ public function store(OrderRequest $request): RedirectResponse
|
||||
});
|
||||
|
||||
return redirect()->route('order.index')->with('success', 'Pesanan berhasil disimpan');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
LogHelper::logException($e, 'Failed to store order', [
|
||||
'customer_name' => $validated['customer_name'],
|
||||
@ -115,27 +121,66 @@ public function edit(Order $order): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Order $order): Response
|
||||
{
|
||||
$order->load(['items.product', 'user']);
|
||||
|
||||
return Inertia::render('admin/manage/order/show', [
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(OrderRequest $request, Order $order): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $order) {
|
||||
$totalItemsPrice = collect($validated['items'])->sum(fn ($item) => $item['qty'] * $item['price']);
|
||||
$cogs = collect($validated['items'])->sum(function ($item) {
|
||||
$isCancelled = $validated['order_status'] === OrderStatus::CANCELLED->value;
|
||||
$wasCancelled = ($order->order_status instanceof OrderStatus ? $order->order_status->value : $order->order_status) === OrderStatus::CANCELLED->value;
|
||||
|
||||
// 1. Jika status SEKARANG adalah GAGAL
|
||||
if ($isCancelled) {
|
||||
// Kembalikan stok jika sebelumnya TIDAK gagal
|
||||
if (! $wasCancelled) {
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock', $item->qty);
|
||||
}
|
||||
}
|
||||
|
||||
// Update metadata saja, jangan hapus item (sesuai request)
|
||||
$order->update([
|
||||
'customer_name' => $validated['customer_name'],
|
||||
'discount' => $validated['discount'],
|
||||
'payment' => $validated['payment'],
|
||||
'payment_method' => $validated['payment_method'],
|
||||
'order_status' => $validated['order_status'],
|
||||
'order_channel' => $validated['order_channel'],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Jika status SEKARANG TIDAK GAGAL
|
||||
// Kembalikan stok lama jika sebelumnya TIDAK gagal (karena kita akan mengganti item)
|
||||
if (! $wasCancelled) {
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock', $item->qty);
|
||||
}
|
||||
}
|
||||
|
||||
// Proses update item (hapus lama, buat baru)
|
||||
$order->items()->delete();
|
||||
|
||||
$items = $validated['items'] ?? [];
|
||||
$totalItemsPrice = collect($items)->sum(fn ($item) => $item['qty'] * $item['price']);
|
||||
$cogs = collect($items)->sum(function ($item) {
|
||||
$product = Product::find($item['product_id']);
|
||||
$purchasePrice = $product->prices()->where('price_type', PriceType::PURCHASE)->first()?->price ?? 0;
|
||||
|
||||
return $purchasePrice * $item['qty'];
|
||||
});
|
||||
|
||||
// Restore stock for old items
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock', $item->qty);
|
||||
}
|
||||
|
||||
$order->items()->delete();
|
||||
|
||||
$order->update([
|
||||
'customer_name' => $validated['customer_name'],
|
||||
'cogs' => $cogs,
|
||||
@ -147,7 +192,7 @@ public function update(OrderRequest $request, Order $order): RedirectResponse
|
||||
'order_channel' => $validated['order_channel'],
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
foreach ($items as $item) {
|
||||
$order->items()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'product_id' => $item['product_id'],
|
||||
@ -157,11 +202,18 @@ public function update(OrderRequest $request, Order $order): RedirectResponse
|
||||
'price_type' => $item['price_type'],
|
||||
]);
|
||||
|
||||
Product::find($item['product_id'])->decrement('stock', $item['qty']);
|
||||
// Potong stok (karena status sekarang bukan gagal)
|
||||
$product = Product::find($item['product_id']);
|
||||
if ($product->stock < $item['qty']) {
|
||||
throw new \InvalidArgumentException("Stok produk {$product->name} tidak mencukupi. Sisa stok: {$product->stock}");
|
||||
}
|
||||
$product->decrement('stock', $item['qty']);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('order.index')->with('success', 'Pesanan berhasil diperbarui');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
LogHelper::logException($e, 'Failed to update order', [
|
||||
'order_id' => $order->id,
|
||||
|
||||
@ -106,6 +106,9 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
||||
]);
|
||||
|
||||
foreach ($purchase->items as $item) {
|
||||
if ($item->product->stock < $item->quantity) {
|
||||
throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dibatalkan. Sisa stok: {$item->product->stock}");
|
||||
}
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
|
||||
@ -125,6 +128,8 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
||||
});
|
||||
|
||||
return redirect()->route('purchase.index')->with('success', 'Data berhasil diperbarui');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
LogHelper::logException($e, 'Failed to update purchase', [
|
||||
'purchase_id' => $purchase->id,
|
||||
@ -143,12 +148,17 @@ public function destroy(Purchase $purchase): RedirectResponse
|
||||
try {
|
||||
DB::transaction(function () use ($purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
if ($item->product->stock < $item->quantity) {
|
||||
throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dihapus. Sisa stok: {$item->product->stock}");
|
||||
}
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
$purchase->delete();
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Data berhasil dihapus');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
LogHelper::logException($e, 'Failed to delete purchase', [
|
||||
'purchase_id' => $purchase->id,
|
||||
@ -164,9 +174,12 @@ public function bulkDestroy(Request $request): RedirectResponse
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($ids) {
|
||||
$purchases = Purchase::with('items')->whereIn('id', $ids)->get();
|
||||
$purchases = Purchase::with('items.product')->whereIn('id', $ids)->get();
|
||||
foreach ($purchases as $purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
if ($item->product->stock < $item->quantity) {
|
||||
throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dihapus. Sisa stok: {$item->product->stock}");
|
||||
}
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
$purchase->delete();
|
||||
@ -174,6 +187,8 @@ public function bulkDestroy(Request $request): RedirectResponse
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Data terpilih berhasil dihapus');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
LogHelper::logException($e, 'Failed to bulk delete purchases', [
|
||||
'ids' => $ids,
|
||||
|
||||
@ -19,8 +19,7 @@ class UserController extends Controller
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/master/user/index', [
|
||||
'users' => User::with(['profile', 'roles'])->latest()->paginate(10),
|
||||
'roles' => Role::all(),
|
||||
'users' => User::with(['profile', 'roles'])->whereDoesntHave('roles', fn ($query) => $query->where('name', 'Developer'))->latest()->paginate(10),
|
||||
'defaultPassword' => config('auth.password_default', 'password'),
|
||||
]);
|
||||
}
|
||||
@ -28,7 +27,7 @@ public function index(): Response
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/master/user/create', [
|
||||
'roles' => Role::all(),
|
||||
'roles' => Role::where('name', '!=', 'Developer')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -41,7 +40,7 @@ public function store(UserRequest $request): RedirectResponse
|
||||
$user = User::create([
|
||||
'username' => $validated['username'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make(config('auth.password_default')),
|
||||
'password' => Hash::make(config('auth.password_default', 'password')),
|
||||
]);
|
||||
|
||||
$user->profile()->create([
|
||||
@ -79,7 +78,7 @@ public function edit(User $user): Response
|
||||
|
||||
return Inertia::render('admin/master/user/edit', [
|
||||
'user' => $user,
|
||||
'roles' => Role::all(),
|
||||
'roles' => Role::where('name', '!=', 'Developer')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\System\GeneralSettingRequest;
|
||||
use App\Models\GeneralSetting;
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@ -13,38 +14,48 @@ class GeneralSettingController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/system/settings/index', [
|
||||
'setting' => GeneralSetting::first(),
|
||||
]);
|
||||
return Inertia::render('admin/system/settings/index');
|
||||
}
|
||||
|
||||
public function update(GeneralSettingRequest $request): RedirectResponse
|
||||
public function update(GeneralSettingRequest $request, GeneralSettings $settings): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$setting = GeneralSetting::first();
|
||||
if ($setting) {
|
||||
$setting->update($validated);
|
||||
} else {
|
||||
$setting = GeneralSetting::create($validated);
|
||||
}
|
||||
$settings->site_name = $validated['site_name'];
|
||||
$settings->site_description = $validated['site_description'];
|
||||
$settings->site_address = $validated['site_address'];
|
||||
$settings->site_phone = $validated['site_phone'];
|
||||
|
||||
if ($request->hasFile('logo_light')) {
|
||||
$setting->addMediaFromRequest('logo_light')->toMediaCollection('logo_light');
|
||||
if ($settings->logo_light) {
|
||||
Storage::disk('public')->delete($settings->logo_light);
|
||||
}
|
||||
$settings->logo_light = $request->file('logo_light')->store('settings', 'public');
|
||||
}
|
||||
|
||||
if ($request->hasFile('logo_dark')) {
|
||||
$setting->addMediaFromRequest('logo_dark')->toMediaCollection('logo_dark');
|
||||
if ($settings->logo_dark) {
|
||||
Storage::disk('public')->delete($settings->logo_dark);
|
||||
}
|
||||
$settings->logo_dark = $request->file('logo_dark')->store('settings', 'public');
|
||||
}
|
||||
|
||||
if ($request->hasFile('icon_light')) {
|
||||
$setting->addMediaFromRequest('icon_light')->toMediaCollection('icon_light');
|
||||
if ($settings->icon_light) {
|
||||
Storage::disk('public')->delete($settings->icon_light);
|
||||
}
|
||||
$settings->icon_light = $request->file('icon_light')->store('settings', 'public');
|
||||
}
|
||||
|
||||
if ($request->hasFile('icon_dark')) {
|
||||
$setting->addMediaFromRequest('icon_dark')->toMediaCollection('icon_dark');
|
||||
if ($settings->icon_dark) {
|
||||
Storage::disk('public')->delete($settings->icon_dark);
|
||||
}
|
||||
$settings->icon_dark = $request->file('icon_dark')->store('settings', 'public');
|
||||
}
|
||||
|
||||
$settings->save();
|
||||
|
||||
return redirect()->back()->with('success', 'Pengaturan berhasil diperbarui');
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,9 +43,10 @@ public function __invoke(Request $request)
|
||||
'cogs' => $this->getStats(fn () => $applyFilter(Order::query())->sum('cogs')),
|
||||
'total_discount' => $this->getStats(fn () => $applyFilter(Order::query())->sum('discount')),
|
||||
'total_purchases' => $this->getStats(fn () => $applyFilter(Purchase::query())->sum('total')),
|
||||
'products_sold' => $this->getStats(fn () => $applyFilter(OrderItem::query())->sum('qty')),
|
||||
'products_sold' => $this->getStats(fn () => $applyFilter(OrderItem::query())->whereHas('order')->sum('qty')),
|
||||
'total_customers' => $this->getStats(fn () => $applyFilter(Order::query())->whereNotNull('customer_name')->distinct('customer_name')->count('customer_name')),
|
||||
'repeat_customers' => $this->getStats(fn () => $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('customer_name')
|
||||
->select('customer_name')
|
||||
->groupBy('customer_name')
|
||||
@ -76,6 +77,7 @@ public function __invoke(Request $request)
|
||||
|
||||
// Revenue vs Purchases per month
|
||||
$revenueByMonth = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select(
|
||||
DB::raw("$monthSelect as month"),
|
||||
DB::raw('SUM(total) as total')
|
||||
@ -84,6 +86,7 @@ public function __invoke(Request $request)
|
||||
->get();
|
||||
|
||||
$purchasesByMonth = $applyFilter(DB::table('purchases'))
|
||||
->whereNull('deleted_at')
|
||||
->select(
|
||||
DB::raw("$monthSelect as month"),
|
||||
DB::raw('SUM(total) as total')
|
||||
@ -105,6 +108,7 @@ public function __invoke(Request $request)
|
||||
});
|
||||
|
||||
$paymentMethods = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select('payment_method as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('payment_method')
|
||||
->get()
|
||||
@ -117,6 +121,7 @@ public function __invoke(Request $request)
|
||||
});
|
||||
|
||||
$orderStatuses = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select('order_status as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('order_status')
|
||||
->get()
|
||||
@ -129,6 +134,7 @@ public function __invoke(Request $request)
|
||||
});
|
||||
|
||||
$orderChannels = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select('order_channel as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('order_channel')
|
||||
->get()
|
||||
@ -142,13 +148,17 @@ public function __invoke(Request $request)
|
||||
|
||||
$topProducts = $applyFilter(DB::table('order_items'), 'order_items.created_at')
|
||||
->join('products', 'order_items.product_id', '=', 'products.id')
|
||||
->join('orders', 'order_items.order_id', '=', 'orders.id')
|
||||
->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
|
||||
->whereNull('orders.deleted_at')
|
||||
->whereNull('order_items.deleted_at')
|
||||
->groupBy('products.name')
|
||||
->orderByDesc('total')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
$topCustomers = $applyFilter(DB::table('orders'), 'orders.created_at')
|
||||
->whereNull('orders.deleted_at')
|
||||
->select('customer_name as name', DB::raw('SUM(total) as total'))
|
||||
->whereNotNull('customer_name')
|
||||
->groupBy('customer_name')
|
||||
@ -159,6 +169,7 @@ public function __invoke(Request $request)
|
||||
$hourSelect = $isSqlite ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)';
|
||||
|
||||
$salesByHourRaw = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select(
|
||||
DB::raw("$hourSelect as hour"),
|
||||
DB::raw('COUNT(*) as total')
|
||||
@ -177,6 +188,7 @@ public function __invoke(Request $request)
|
||||
|
||||
// Revenue & Profit & Volume per month
|
||||
$ordersByMonth = $applyFilter(DB::table('orders'))
|
||||
->whereNull('deleted_at')
|
||||
->select(
|
||||
DB::raw("$monthSelect as month"),
|
||||
DB::raw('SUM(total) as revenue'),
|
||||
@ -239,7 +251,9 @@ public function __invoke(Request $request)
|
||||
->join('products', 'order_items.product_id', '=', 'products.id')
|
||||
->join('category_product', 'products.id', '=', 'category_product.product_id')
|
||||
->join('categories', 'category_product.category_id', '=', 'categories.id')
|
||||
->join('orders', 'order_items.order_id', '=', 'orders.id')
|
||||
->select('categories.name', DB::raw('SUM(order_items.qty) as total'))
|
||||
->whereNull('orders.deleted_at')
|
||||
->whereNull('order_items.deleted_at')
|
||||
->groupBy('categories.id', 'categories.name')
|
||||
->orderByDesc('total')
|
||||
|
||||
@ -42,6 +42,7 @@ public function __invoke()
|
||||
|
||||
// Get sales by hour
|
||||
$todayOrders = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->whereDate('created_at', $today)
|
||||
->select(
|
||||
DB::raw("$hourSelect as hour"),
|
||||
@ -51,6 +52,7 @@ public function __invoke()
|
||||
->get();
|
||||
|
||||
$yesterdayOrders = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->whereDate('created_at', $yesterday)
|
||||
->select(
|
||||
DB::raw("$hourSelect as hour"),
|
||||
@ -71,6 +73,7 @@ public function __invoke()
|
||||
});
|
||||
|
||||
$paymentMethods = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->select('payment_method as name', DB::raw('COUNT(*) as total'))
|
||||
->whereDate('created_at', $today)
|
||||
->groupBy('payment_method')
|
||||
@ -84,6 +87,7 @@ public function __invoke()
|
||||
});
|
||||
|
||||
$orderStatuses = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->select('order_status as name', DB::raw('COUNT(*) as total'))
|
||||
->whereDate('created_at', $today)
|
||||
->groupBy('order_status')
|
||||
@ -97,6 +101,7 @@ public function __invoke()
|
||||
});
|
||||
|
||||
$orderChannels = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->select('order_channel as name', DB::raw('COUNT(*) as total'))
|
||||
->whereDate('created_at', $today)
|
||||
->groupBy('order_channel')
|
||||
@ -113,6 +118,8 @@ public function __invoke()
|
||||
->join('products', 'order_items.product_id', '=', 'products.id')
|
||||
->join('orders', 'order_items.order_id', '=', 'orders.id')
|
||||
->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
|
||||
->whereNull('orders.deleted_at')
|
||||
->whereNull('order_items.deleted_at')
|
||||
->whereDate('orders.created_at', $today)
|
||||
->groupBy('products.name')
|
||||
->orderByDesc('total')
|
||||
@ -120,6 +127,7 @@ public function __invoke()
|
||||
->get();
|
||||
|
||||
$topCustomers = DB::table('orders')
|
||||
->whereNull('deleted_at')
|
||||
->select('customer_name as name', DB::raw('SUM(total) as total'))
|
||||
->whereDate('created_at', $today)
|
||||
->whereNotNull('customer_name')
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\GeneralSetting;
|
||||
use App\Models\Product;
|
||||
|
||||
class HomepageController extends Controller
|
||||
@ -14,7 +13,6 @@ public function __invoke()
|
||||
$categorySlug = request('category');
|
||||
|
||||
return inertia('homepage', [
|
||||
'setting' => GeneralSetting::first(),
|
||||
'categories' => Category::active()->get(),
|
||||
'bestSellers' => Product::with(['categories' => fn ($q) => $q->active(), 'prices'])
|
||||
->when($search, function ($query, $search) {
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\GeneralSetting;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
@ -39,7 +38,6 @@ public function share(Request $request): array
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'setting' => GeneralSetting::first(),
|
||||
'auth' => [
|
||||
'user' => $request->user() ? $request->user()->load('profile') : null,
|
||||
'roles' => $request->user() ? $request->user()->getRoleNames() : [],
|
||||
|
||||
@ -27,20 +27,22 @@ public function authorize(): bool
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$isCancelled = $this->input('order_status') === OrderStatus::CANCELLED->value;
|
||||
|
||||
return [
|
||||
'customer_name' => ['required', 'string', 'max:100'],
|
||||
'subtotal' => ['required', 'integer', 'min:0'],
|
||||
'subtotal' => ['nullable', 'integer', 'min:0'],
|
||||
'discount' => ['required', 'integer', 'min:0'],
|
||||
'payment' => ['required', 'integer', 'min:0'],
|
||||
'payment_method' => ['required', Rule::enum(PaymentMethod::class)],
|
||||
'order_status' => ['required', Rule::enum(OrderStatus::class)],
|
||||
'order_channel' => ['required', Rule::enum(OrderChannel::class)],
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.product_id' => ['required', Rule::exists('products', 'id')->whereNull('deleted_at')],
|
||||
'items.*.qty' => ['required', 'integer', 'min:1'],
|
||||
'items.*.price' => ['required', 'integer', 'min:0'],
|
||||
'items.*.total' => ['required', 'integer', 'min:0'],
|
||||
'items.*.price_type' => ['required', Rule::enum(PriceType::class)],
|
||||
'items' => [$isCancelled ? 'nullable' : 'required', 'array', $isCancelled ? 'min:0' : 'min:1'],
|
||||
'items.*.product_id' => ['required_unless:order_status,cancelled', Rule::exists('products', 'id')->whereNull('deleted_at')],
|
||||
'items.*.qty' => ['required_unless:order_status,cancelled', 'integer', 'min:1'],
|
||||
'items.*.price' => ['required_unless:order_status,cancelled', 'integer', 'min:0'],
|
||||
'items.*.total' => ['required_unless:order_status,cancelled', 'integer', 'min:0'],
|
||||
'items.*.price_type' => ['required_unless:order_status,cancelled', Rule::enum(PriceType::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ public function rules(): array
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'base_salary' => ['required', 'integer', 'min:0'],
|
||||
'roles' => ['nullable', 'array'],
|
||||
'roles' => ['required', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Requests\Admin\System;
|
||||
|
||||
use App\Models\GeneralSetting;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
@ -23,16 +22,14 @@ public function authorize(): bool
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$settingExists = GeneralSetting::exists();
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'description' => ['required', 'string'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'logo_light' => [$settingExists ? 'nullable' : 'required', 'image', 'max:2048'],
|
||||
'site_name' => ['required', 'string', 'max:50'],
|
||||
'site_description' => ['required', 'string'],
|
||||
'site_address' => ['required', 'string'],
|
||||
'site_phone' => ['required', 'string', 'max:20'],
|
||||
'logo_light' => ['nullable', 'image', 'max:2048'],
|
||||
'logo_dark' => ['nullable', 'image', 'max:2048'],
|
||||
'icon_light' => [$settingExists ? 'nullable' : 'required', 'image', 'max:1024'],
|
||||
'icon_light' => ['nullable', 'image', 'max:1024'],
|
||||
'icon_dark' => ['nullable', 'image', 'max:1024'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['logo_light_url', 'logo_dark_url', 'icon_light_url', 'icon_dark_url'])]
|
||||
class GeneralSetting extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, InteractsWithMedia, LogsActivity;
|
||||
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('logo_light')
|
||||
->singleFile();
|
||||
|
||||
$this->addMediaCollection('logo_dark')
|
||||
->singleFile();
|
||||
|
||||
$this->addMediaCollection('icon_light')
|
||||
->singleFile();
|
||||
|
||||
$this->addMediaCollection('icon_dark')
|
||||
->singleFile();
|
||||
}
|
||||
|
||||
protected function logoLightUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMediaUrl('logo_light') ?: null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function logoDarkUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMediaUrl('logo_dark') ?: null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function iconLightUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMediaUrl('icon_light') ?: null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function iconDarkUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMediaUrl('icon_dark') ?: null,
|
||||
);
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'description', 'address', 'phone'])
|
||||
->logOnlyDirty()
|
||||
->useLogName('Pengaturan Umum');
|
||||
}
|
||||
|
||||
public function tapActivity(Activity $activity, string $eventName)
|
||||
{
|
||||
$activity->description = match ($eventName) {
|
||||
'created' => 'TAMBAH',
|
||||
'updated' => 'UBAH',
|
||||
'deleted' => 'HAPUS',
|
||||
default => $activity->description,
|
||||
};
|
||||
|
||||
if (isset($activity->properties['attributes'])) {
|
||||
$attributeMap = [
|
||||
'name' => 'Nama Aplikasi',
|
||||
'description' => 'Deskripsi',
|
||||
'address' => 'Alamat',
|
||||
'phone' => 'No. Telepon',
|
||||
];
|
||||
|
||||
$properties = $activity->properties->toArray();
|
||||
|
||||
$localizeValues = function ($attrs) use ($attributeMap) {
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $value) {
|
||||
$label = $attributeMap[$key] ?? $key;
|
||||
$newAttrs[$label] = $value;
|
||||
}
|
||||
|
||||
return $newAttrs;
|
||||
};
|
||||
|
||||
$properties['attributes'] = $localizeValues($properties['attributes']);
|
||||
|
||||
if (isset($properties['old'])) {
|
||||
$properties['old'] = $localizeValues($properties['old']);
|
||||
}
|
||||
|
||||
$activity->properties = collect($properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Models\Order;
|
||||
|
||||
class OrderObserver
|
||||
@ -40,8 +41,14 @@ public function updating(Order $order): void
|
||||
*/
|
||||
public function deleting(Order $order): void
|
||||
{
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock', $item->qty);
|
||||
$isCancelled = ($order->order_status instanceof OrderStatus ? $order->order_status->value : $order->order_status) === OrderStatus::CANCELLED->value;
|
||||
|
||||
if (! $isCancelled) {
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock', $item->qty);
|
||||
}
|
||||
}
|
||||
|
||||
$order->items()->delete();
|
||||
}
|
||||
}
|
||||
|
||||
75
app/Providers/ViewServiceProvider.php
Normal file
75
app/Providers/ViewServiceProvider.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* View Service Provider
|
||||
*
|
||||
* This provider handles global data sharing for both Inertia and Blade views.
|
||||
*/
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole() && ! $this->app->environment('testing')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Share settings with Inertia
|
||||
Inertia::share('setting', function () {
|
||||
try {
|
||||
$settings = app(GeneralSettings::class);
|
||||
|
||||
return [
|
||||
'site_name' => $settings->site_name,
|
||||
'site_description' => $settings->site_description,
|
||||
'site_address' => $settings->site_address,
|
||||
'site_phone' => $settings->site_phone,
|
||||
'logo_light_url' => $settings->logo_light ? Storage::url($settings->logo_light) : null,
|
||||
'logo_dark_url' => $settings->logo_dark ? Storage::url($settings->logo_dark) : null,
|
||||
'icon_light_url' => $settings->icon_light ? Storage::url($settings->icon_light) : null,
|
||||
'icon_dark_url' => $settings->icon_dark ? Storage::url($settings->icon_dark) : null,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Share settings with Blade views
|
||||
View::composer('*', function ($view) {
|
||||
try {
|
||||
$settings = app(GeneralSettings::class);
|
||||
$view->with('setting', [
|
||||
'site_name' => $settings->site_name,
|
||||
'site_description' => $settings->site_description,
|
||||
'site_address' => $settings->site_address,
|
||||
'site_phone' => $settings->site_phone,
|
||||
'logo_light_url' => $settings->logo_light ? Storage::url($settings->logo_light) : null,
|
||||
'logo_dark_url' => $settings->logo_dark ? Storage::url($settings->logo_dark) : null,
|
||||
'icon_light_url' => $settings->icon_light ? Storage::url($settings->icon_light) : null,
|
||||
'icon_dark_url' => $settings->icon_dark ? Storage::url($settings->icon_dark) : null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$view->with('setting', []);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
29
app/Settings/GeneralSettings.php
Normal file
29
app/Settings/GeneralSettings.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class GeneralSettings extends Settings
|
||||
{
|
||||
public string $site_name;
|
||||
|
||||
public string $site_description;
|
||||
|
||||
public string $site_address;
|
||||
|
||||
public string $site_phone;
|
||||
|
||||
public ?string $logo_light;
|
||||
|
||||
public ?string $logo_dark;
|
||||
|
||||
public ?string $icon_light;
|
||||
|
||||
public ?string $icon_dark;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'general';
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\FortifyServiceProvider;
|
||||
use App\Providers\ViewServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
FortifyServiceProvider::class,
|
||||
ViewServiceProvider::class,
|
||||
];
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"spatie/laravel-activitylog": "^4.12",
|
||||
"spatie/laravel-medialibrary": "^11.21",
|
||||
"spatie/laravel-permission": "^7.3",
|
||||
"spatie/laravel-settings": "^3.8",
|
||||
"spatie/laravel-sluggable": "^3.8"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
405
composer.lock
generated
405
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1e97290615923580b9a565671ca7bfae",
|
||||
"content-hash": "a42046c1187875d3d873b061a01fa885",
|
||||
"packages": [
|
||||
{
|
||||
"name": "archtechx/enums",
|
||||
@ -445,6 +445,54 @@
|
||||
},
|
||||
"time": "2024-07-08T12:26:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/deprecations.git",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<=7.5 || >=14"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||
"psr/log": "^1 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Deprecations\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||
"homepage": "https://www.doctrine-project.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||
},
|
||||
"time": "2026-02-07T07:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/inflector",
|
||||
"version": "2.1.0",
|
||||
@ -3976,6 +4024,117 @@
|
||||
},
|
||||
"time": "2025-09-24T15:06:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-2.x": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"phpDocumentor\\Reflection\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jaap van Otterdijk",
|
||||
"email": "opensource@ijaap.nl"
|
||||
}
|
||||
],
|
||||
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
|
||||
"homepage": "http://www.phpdoc.org",
|
||||
"keywords": [
|
||||
"FQSEN",
|
||||
"phpDocumentor",
|
||||
"phpdoc",
|
||||
"reflection",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
|
||||
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
|
||||
},
|
||||
"time": "2020-06-27T09:03:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/type-resolver",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
||||
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
|
||||
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/deprecations": "^1.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpdocumentor/reflection-common": "^2.0",
|
||||
"phpstan/phpdoc-parser": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-tokenizer": "*",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psalm/phar": "^4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-1.x": "1.x-dev",
|
||||
"dev-2.x": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"phpDocumentor\\Reflection\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "me@mikevanriel.com"
|
||||
}
|
||||
],
|
||||
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
|
||||
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
|
||||
},
|
||||
"time": "2026-01-06T21:53:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.5",
|
||||
@ -5316,6 +5475,91 @@
|
||||
],
|
||||
"time": "2026-04-07T15:19:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-settings",
|
||||
"version": "3.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-settings.git",
|
||||
"reference": "09b788ee96d205699420dedb8a6aa8c4c8af84fe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-settings/zipball/09b788ee96d205699420dedb8a6aa8c4c8af84fe",
|
||||
"reference": "09b788ee96d205699420dedb8a6aa8c4c8af84fe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/database": "^11.0|^12.0|^13.0",
|
||||
"php": "^8.2",
|
||||
"phpdocumentor/type-resolver": "^1.5|^2.0",
|
||||
"spatie/temporary-directory": "^1.3|^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-redis": "*",
|
||||
"mockery/mockery": "^1.4",
|
||||
"orchestra/testbench": "^9.0|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.0|^3.0|^4.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0",
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"spatie/laravel-data": "^2.0.0|^4.0.0",
|
||||
"spatie/pest-plugin-snapshots": "^2.0",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2|^5.0",
|
||||
"spatie/ray": "^1.36"
|
||||
},
|
||||
"suggest": {
|
||||
"spatie/data-transfer-object": "Allows for DTO casting to settings. (deprecated)"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\LaravelSettings\\LaravelSettingsServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\LaravelSettings\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ruben Van Assche",
|
||||
"email": "ruben@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Store your application settings",
|
||||
"homepage": "https://github.com/spatie/laravel-settings",
|
||||
"keywords": [
|
||||
"laravel-settings",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-settings/issues",
|
||||
"source": "https://github.com/spatie/laravel-settings/tree/3.8.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-28T07:05:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-sluggable",
|
||||
"version": "3.8.1",
|
||||
@ -8245,54 +8489,6 @@
|
||||
],
|
||||
"time": "2026-03-29T15:46:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/deprecations.git",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<=7.5 || >=14"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||
"psr/log": "^1 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Deprecations\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||
"homepage": "https://www.doctrine-project.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||
},
|
||||
"time": "2026-02-07T07:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.24.1",
|
||||
@ -9630,59 +9826,6 @@
|
||||
},
|
||||
"time": "2022-02-21T01:04:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-2.x": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"phpDocumentor\\Reflection\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jaap van Otterdijk",
|
||||
"email": "opensource@ijaap.nl"
|
||||
}
|
||||
],
|
||||
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
|
||||
"homepage": "http://www.phpdoc.org",
|
||||
"keywords": [
|
||||
"FQSEN",
|
||||
"phpDocumentor",
|
||||
"phpdoc",
|
||||
"reflection",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
|
||||
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
|
||||
},
|
||||
"time": "2020-06-27T09:03:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "6.0.3",
|
||||
@ -9748,64 +9891,6 @@
|
||||
},
|
||||
"time": "2026-03-18T20:49:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/type-resolver",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
||||
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
|
||||
"reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/deprecations": "^1.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpdocumentor/reflection-common": "^2.0",
|
||||
"phpstan/phpdoc-parser": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-tokenizer": "*",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psalm/phar": "^4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-1.x": "1.x-dev",
|
||||
"dev-2.x": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"phpDocumentor\\Reflection\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "me@mikevanriel.com"
|
||||
}
|
||||
],
|
||||
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
|
||||
"source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
|
||||
},
|
||||
"time": "2026-01-06T21:53:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "12.5.6",
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?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(config('settings.repositories.database.table') ?? 'settings', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
$table->string('group');
|
||||
$table->string('name');
|
||||
$table->boolean('locked')->default(false);
|
||||
$table->json('payload');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group', 'name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -16,7 +16,7 @@ public function up(): void
|
||||
$table->string('name', 100);
|
||||
$table->string('slug', 100);
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('stock')->default(0);
|
||||
$table->unsignedInteger('stock')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
<?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('general_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->text('description');
|
||||
$table->text('address');
|
||||
$table->string('phone', 20);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('general_settings');
|
||||
}
|
||||
};
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\GeneralSetting;
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class GeneralSettingSeeder extends Seeder
|
||||
@ -12,11 +12,11 @@ class GeneralSettingSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
GeneralSetting::updateOrCreate([
|
||||
'name' => 'VN Grup',
|
||||
'description' => 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya. Kami menyediakan berbagai macam daster dengan desain yang beragam, mulai dari yang sederhana dan klasik hingga yang modern dan trendi, cocok untuk berbagai kebutuhan, baik di rumah maupun saat bersantai di luar.',
|
||||
'address' => 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat',
|
||||
'phone' => '089679965828',
|
||||
]);
|
||||
$settings = app(GeneralSettings::class);
|
||||
$settings->site_name = 'VN Grup';
|
||||
$settings->site_description = 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya. Kami menyediakan berbagai macam daster dengan desain yang beragam, mulai dari yang sederhana dan klasik hingga yang modern dan trendi, cocok untuk berbagai kebutuhan, baik di rumah maupun saat bersantai di luar.';
|
||||
$settings->site_address = 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat';
|
||||
$settings->site_phone = '089679965828';
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->add('general.site_name', 'VN Grup');
|
||||
$this->migrator->add('general.site_description', 'VN Grup adalah Toko Baju Daster Online yang menghadirkan koleksi baju daster berkualitas tinggi untuk wanita yang mengutamakan kenyamanan tanpa mengorbankan gaya.');
|
||||
$this->migrator->add('general.site_address', 'Jl. Raya Pabuaran-Cipeunduy, Kec. Pabuaran, Kab. Subang, Jawa Barat');
|
||||
$this->migrator->add('general.site_phone', '089679965828');
|
||||
$this->migrator->add('general.logo_light', null);
|
||||
$this->migrator->add('general.logo_dark', null);
|
||||
$this->migrator->add('general.icon_light', null);
|
||||
$this->migrator->add('general.icon_dark', null);
|
||||
}
|
||||
};
|
||||
@ -67,39 +67,39 @@ @theme {
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--background: oklch(0.962 0.012 85.3);
|
||||
--foreground: oklch(0.334 0.082 245.2);
|
||||
--card: oklch(0.962 0.012 85.3);
|
||||
--card-foreground: oklch(0.334 0.082 245.2);
|
||||
--popover: oklch(0.962 0.012 85.3);
|
||||
--popover-foreground: oklch(0.334 0.082 245.2);
|
||||
--primary: oklch(0.334 0.082 245.2);
|
||||
--primary-foreground: oklch(0.962 0.012 85.3);
|
||||
--secondary: oklch(0.94 0.01 85);
|
||||
--secondary-foreground: oklch(0.334 0.082 245.2);
|
||||
--muted: oklch(0.94 0.01 85);
|
||||
--muted-foreground: oklch(0.45 0.05 245);
|
||||
--accent: oklch(0.94 0.01 85);
|
||||
--accent-foreground: oklch(0.334 0.082 245.2);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.87 0 0);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.334 0.082 245.2 / 0.1);
|
||||
--input: oklch(0.334 0.082 245.2 / 0.1);
|
||||
--ring: oklch(0.334 0.082 245.2 / 0.5);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.87 0 0);
|
||||
--sidebar: oklch(0.962 0.012 85.3);
|
||||
--sidebar-foreground: oklch(0.334 0.082 245.2);
|
||||
--sidebar-primary: oklch(0.334 0.082 245.2);
|
||||
--sidebar-primary-foreground: oklch(0.962 0.012 85.3);
|
||||
--sidebar-accent: oklch(0.334 0.082 245.2 / 0.05);
|
||||
--sidebar-accent-foreground: oklch(0.334 0.082 245.2);
|
||||
--sidebar-border: oklch(0.334 0.082 245.2 / 0.1);
|
||||
--sidebar-ring: oklch(0.334 0.082 245.2 / 0.5);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -145,4 +145,9 @@ @layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,20 +9,20 @@ export default function AppLogo() {
|
||||
{setting?.logo_light_url ? (
|
||||
<img
|
||||
src={setting.logo_light_url}
|
||||
alt={setting.name}
|
||||
alt={setting.site_name}
|
||||
className="max-h-8 w-auto dark:hidden"
|
||||
/>
|
||||
) : null}
|
||||
{setting?.logo_dark_url ? (
|
||||
<img
|
||||
src={setting.logo_dark_url}
|
||||
alt={setting.name}
|
||||
alt={setting.site_name}
|
||||
className="hidden max-h-8 w-auto dark:block"
|
||||
/>
|
||||
) : null}
|
||||
{!setting?.logo_light_url && !setting?.logo_dark_url && (
|
||||
<span className="truncate font-semibold tracking-tight">
|
||||
{setting?.name || 'VN Grup'}
|
||||
{setting?.site_name || 'VN Grup'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -147,7 +147,7 @@ export function AppSidebar() {
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<Link href={dashboard().url} prefetch>
|
||||
<Link href={dashboard().url}>
|
||||
<AppLogo />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@ -29,7 +29,7 @@ export function NavMain({
|
||||
isActive={item.isActive || isCurrentOrParentUrl(item.href)}
|
||||
tooltip={{ children: item.title }}
|
||||
>
|
||||
<Link href={item.href} prefetch>
|
||||
<Link href={item.href}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
|
||||
@ -180,7 +180,12 @@ return;
|
||||
|
||||
post(orderRoutes.store().url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -195,7 +195,12 @@ return;
|
||||
|
||||
patch(orderRoutes.update(order.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ import type { Order } from '@/types/order';
|
||||
import { ORDER_CHANNEL, ORDER_STATUS, PAYMENT_METHOD } from '@/types/order';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil, Printer, Trash2 } from 'lucide-react';
|
||||
import { FileText, Pencil, Printer, Trash2 } from 'lucide-react';
|
||||
|
||||
interface ColumnProps {
|
||||
onDelete: (order: Order) => void;
|
||||
@ -182,6 +182,22 @@ export const getColumns = ({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={orderRoutes.show(order.id).url}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-emerald-600 hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-950/20"
|
||||
>
|
||||
<FileText className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Invoice</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
194
resources/js/pages/admin/manage/order/show.tsx
Normal file
194
resources/js/pages/admin/manage/order/show.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { ArrowLeft, Mail } from 'lucide-react';
|
||||
import type { Order } from '@/types/order';
|
||||
import { ORDER_STATUS, PAYMENT_METHOD, ORDER_CHANNEL } from '@/types/order';
|
||||
import * as orderRoutes from '@/routes/order';
|
||||
import type { GeneralSetting } from '@/types/general-setting';
|
||||
|
||||
interface ShowProps {
|
||||
order: Order;
|
||||
setting: GeneralSetting;
|
||||
}
|
||||
|
||||
export default function OrderShow({ order, setting }: ShowProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 print:p-8">
|
||||
<Head title={`Invoice #${order.order_number}`} />
|
||||
|
||||
{/* Header / Actions */}
|
||||
<div className="relative z-10 flex items-center justify-between print:hidden">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href={orderRoutes.index().url}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
||||
Detail Pesanan
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Invoice #{order.order_number}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Actions removed as requested */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Card */}
|
||||
<Card className="relative mx-auto w-full max-w-4xl overflow-hidden border-none bg-card shadow-xl print:shadow-none print:border print:max-w-none">
|
||||
<div className="pointer-events-none absolute inset-0 bg-grid-slate-100 [mask-image:linear-gradient(0deg,#fff,rgba(255,255,255,0.6))] dark:bg-grid-slate-700/25 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))] print:hidden" />
|
||||
|
||||
<CardHeader className="relative space-y-6 pb-8 pt-10">
|
||||
<div className="flex flex-col justify-between gap-6 md:flex-row md:items-start">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{setting?.logo_light_url ? (
|
||||
<img src={setting.logo_light_url} alt="Logo" className="h-10 w-auto" />
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-2xl font-bold text-primary-foreground">
|
||||
{setting?.site_name?.charAt(0) || 'V'}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground">{setting?.site_name || 'VN Grup'}</h2>
|
||||
<p className="text-sm text-muted-foreground italic">Your Style, Our Passion</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-xs space-y-1 text-sm text-muted-foreground">
|
||||
<p>{setting?.site_address || 'Alamat Toko Belum Diatur'}</p>
|
||||
<p>{setting?.site_phone || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left md:text-right">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-3xl font-black uppercase tracking-tighter text-primary">INVOICE</h3>
|
||||
<p className="text-lg font-mono font-medium">#{order.order_number}</p>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>Tanggal Pesanan: <span className="font-medium text-foreground">{order.created_at_formatted}</span></p>
|
||||
<p>Channel: <span className="font-medium text-foreground">{ORDER_CHANNEL[order.order_channel] || order.order_channel}</span></p>
|
||||
<div className="flex items-center gap-2 md:justify-end">
|
||||
<span>Status:</span>
|
||||
<Badge variant={ORDER_STATUS[order.order_status]?.color ?? 'outline'}>
|
||||
{ORDER_STATUS[order.order_status]?.label ?? order.order_status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-primary/10" />
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Tagihan Untuk:</h4>
|
||||
<div className="space-y-1">
|
||||
<p className="text-lg font-bold text-foreground">{order.customer_name}</p>
|
||||
<p className="text-sm text-muted-foreground">Pelanggan Setia</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 text-left md:text-right">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Pembayaran:</h4>
|
||||
<div className="space-y-1">
|
||||
<p className="text-lg font-bold text-foreground">{PAYMENT_METHOD[order.payment_method] || order.payment_method}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="relative space-y-8 pb-12">
|
||||
<div className="rounded-xl border bg-card/50 overflow-hidden shadow-sm">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||
<TableHead>Produk</TableHead>
|
||||
<TableHead className="text-right">Harga</TableHead>
|
||||
<TableHead className="text-center">Jumlah</TableHead>
|
||||
<TableHead className="text-right">Total</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{order.items?.map((item, index) => (
|
||||
<TableRow key={item.id} className="hover:bg-muted/30">
|
||||
<TableCell className="text-center text-muted-foreground">{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium">{item.product?.name || 'Produk Tidak Diketahui'}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">{item.price_type}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{item.price_formatted}</TableCell>
|
||||
<TableCell className="text-center">{item.qty}</TableCell>
|
||||
<TableCell className="text-right font-medium">{item.total_formatted}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-between gap-8 md:flex-row">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="rounded-xl bg-muted/30 p-4 space-y-2 border border-dashed">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Catatan:</h4>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Terima kasih telah berbelanja di {setting?.site_name || 'VN Grup'}.
|
||||
Barang yang sudah dibeli tidak dapat ditukar atau dikembalikan kecuali ada perjanjian sebelumnya.
|
||||
Simpan invoice ini sebagai bukti pembelian yang sah.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Mail className="size-3" />
|
||||
<span>Dicatat oleh: <span className="font-medium text-foreground">{order.user?.name || '-'}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-80 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Subtotal</span>
|
||||
<span className="font-medium">{order.subtotal_formatted}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Potongan / Diskon</span>
|
||||
<span className="font-medium text-red-500">- {order.discount_formatted}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between items-center pt-1">
|
||||
<span className="text-lg font-bold">Total Tagihan</span>
|
||||
<span className="text-2xl font-black text-primary">{order.total_formatted}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center rounded-lg bg-primary/5 px-3 py-2 border border-primary/10 mt-4">
|
||||
<span className="text-xs font-bold uppercase text-primary/70">Sudah Dibayar</span>
|
||||
<span className="text-sm font-bold text-primary">{order.payment_formatted}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-10 text-center text-sm text-muted-foreground print:pt-20">
|
||||
<p className="font-medium">Semoga hari Anda menyenangkan!</p>
|
||||
<p className="text-[10px] mt-1 opacity-50">Generated by {setting?.site_name || 'VN Grup'} System</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OrderShow.layout = (page: any) => (
|
||||
<div className="min-h-screen bg-muted/20 dark:bg-muted/5">{page}</div>
|
||||
);
|
||||
@ -165,7 +165,12 @@ return;
|
||||
|
||||
post(purchaseRoutes.store().url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -170,7 +170,12 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
|
||||
|
||||
patch(purchaseRoutes.update(purchase.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -20,7 +20,12 @@ export function usePurchaseIndex() {
|
||||
if (purchaseToDelete) {
|
||||
router.delete(purchaseRoutes.destroy(purchaseToDelete.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsDeleteDialogOpen(false);
|
||||
setPurchaseToDelete(null);
|
||||
setRowSelection({});
|
||||
@ -35,7 +40,12 @@ export function usePurchaseIndex() {
|
||||
_method: 'DELETE'
|
||||
}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setRowsToDelete([]);
|
||||
setRowSelection({});
|
||||
|
||||
@ -113,7 +113,12 @@ export default function ProductCreate({ categories }: { categories: Category[] }
|
||||
post(productRoutes.store().url, {
|
||||
forceFormData: true,
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -165,7 +165,12 @@ export default function ProductEdit({ product, categories }: { product: Product,
|
||||
post(productRoutes.update(product.id).url, {
|
||||
forceFormData: true,
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -21,7 +21,12 @@ export function useProductIndex() {
|
||||
if (productToDelete) {
|
||||
router.delete(productRoutes.destroy(productToDelete.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsDeleteDialogOpen(false);
|
||||
setProductToDelete(null);
|
||||
setRowSelection({});
|
||||
@ -36,7 +41,12 @@ export function useProductIndex() {
|
||||
_method: 'DELETE'
|
||||
}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setRowsToDelete([]);
|
||||
setRowSelection({});
|
||||
@ -48,7 +58,14 @@ export function useProductIndex() {
|
||||
router.patch(productRoutes.toggleStatus(id).url, {}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
onSuccess: (response: any) => toast.success(response.props.flash.success),
|
||||
onSuccess: (response: any) => {
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -50,7 +50,12 @@ export default function UserCreate({ roles }: { roles: any[] }) {
|
||||
e.preventDefault();
|
||||
post(userRoutes.store().url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -384,7 +389,9 @@ export default function UserCreate({ roles }: { roles: any[] }) {
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="roles">Peran</Label>
|
||||
<Label htmlFor="roles" required>
|
||||
Peran
|
||||
</Label>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
|
||||
@ -51,7 +51,12 @@ export default function UserEdit({ user, roles }: { user: any, roles: any[] }) {
|
||||
e.preventDefault();
|
||||
post(userRoutes.update(user.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -237,7 +242,9 @@ export default function UserEdit({ user, roles }: { user: any, roles: any[] }) {
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="roles">Peran</Label>
|
||||
<Label htmlFor="roles" required>
|
||||
Peran
|
||||
</Label>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
|
||||
@ -22,7 +22,12 @@ export function useUserIndex() {
|
||||
if (userToReset) {
|
||||
router.patch(userRoutes.resetPassword(userToReset.id).url, {}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsResetPasswordOpen(false);
|
||||
setUserToReset(null);
|
||||
},
|
||||
@ -39,7 +44,12 @@ export function useUserIndex() {
|
||||
if (userToDelete) {
|
||||
router.delete(userRoutes.destroy(userToDelete.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsDeleteDialogOpen(false);
|
||||
setUserToDelete(null);
|
||||
setRowSelection({});
|
||||
@ -54,7 +64,12 @@ export function useUserIndex() {
|
||||
_method: 'DELETE'
|
||||
}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setRowsToDelete([]);
|
||||
setRowSelection({});
|
||||
@ -67,7 +82,12 @@ export function useUserIndex() {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -30,7 +30,12 @@ export default function RoleCreate({
|
||||
e.preventDefault();
|
||||
post(roleRoutes.store().url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -32,7 +32,12 @@ export default function RoleEdit({
|
||||
e.preventDefault();
|
||||
put(roleRoutes.update(role.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -20,7 +20,12 @@ export function useRoleIndex() {
|
||||
if (roleToDelete) {
|
||||
router.delete(roleRoutes.destroy(roleToDelete.id).url, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsDeleteDialogOpen(false);
|
||||
setRoleToDelete(null);
|
||||
setRowSelection({});
|
||||
@ -35,7 +40,12 @@ export function useRoleIndex() {
|
||||
_method: 'DELETE'
|
||||
}, {
|
||||
onSuccess: (response: any) => {
|
||||
toast.success(response.props.flash.success);
|
||||
const flash = response.props.flash;
|
||||
if (flash?.error) {
|
||||
toast.error(flash.error);
|
||||
} else if (flash?.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setRowsToDelete([]);
|
||||
setRowSelection({});
|
||||
|
||||
@ -17,10 +17,10 @@ export default function SettingIndex({
|
||||
setting: GeneralSetting | null;
|
||||
}) {
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
name: setting?.name || '',
|
||||
description: setting?.description || '',
|
||||
address: setting?.address || '',
|
||||
phone: setting?.phone || '',
|
||||
site_name: setting?.site_name || '',
|
||||
site_description: setting?.site_description || '',
|
||||
site_address: setting?.site_address || '',
|
||||
site_phone: setting?.site_phone || '',
|
||||
logo_light: null as File | null,
|
||||
logo_dark: null as File | null,
|
||||
icon_light: null as File | null,
|
||||
@ -107,41 +107,41 @@ export default function SettingIndex({
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Field>
|
||||
<Label htmlFor="phone" required>
|
||||
<Label htmlFor="site_phone" required>
|
||||
No. Telepon
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={data.phone || ''}
|
||||
id="site_phone"
|
||||
value={data.site_phone || ''}
|
||||
onChange={(e) =>
|
||||
setData('phone', e.target.value)
|
||||
setData('site_phone', e.target.value)
|
||||
}
|
||||
placeholder="0812xxxx"
|
||||
/>
|
||||
<FieldError
|
||||
error={errors.phone}
|
||||
error={errors.site_phone}
|
||||
label="No. Telepon"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="name" required>
|
||||
<Label htmlFor="site_name" required>
|
||||
Nama Aplikasi
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
id="site_name"
|
||||
name="site_name"
|
||||
value={data.site_name}
|
||||
onChange={(e) =>
|
||||
setData('name', e.target.value)
|
||||
setData('site_name', e.target.value)
|
||||
}
|
||||
autoComplete="off"
|
||||
placeholder="Contoh: VN Grup Dress"
|
||||
maxLength={100}
|
||||
/>
|
||||
<FieldError
|
||||
error={errors.name}
|
||||
error={errors.site_name}
|
||||
label="Nama Aplikasi"
|
||||
className="text-xs"
|
||||
/>
|
||||
@ -149,15 +149,15 @@ export default function SettingIndex({
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="description" required>
|
||||
<Label htmlFor="site_description" required>
|
||||
Deskripsi
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={data.description || ''}
|
||||
id="site_description"
|
||||
value={data.site_description || ''}
|
||||
onChange={(e) =>
|
||||
setData(
|
||||
'description',
|
||||
'site_description',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
@ -165,27 +165,27 @@ export default function SettingIndex({
|
||||
rows={4}
|
||||
/>
|
||||
<FieldError
|
||||
error={errors.description}
|
||||
error={errors.site_description}
|
||||
label="Deskripsi"
|
||||
className="mt-1 text-xs"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="address" required>
|
||||
<Label htmlFor="site_address" required>
|
||||
Alamat
|
||||
</Label>
|
||||
<Textarea
|
||||
id="address"
|
||||
value={data.address || ''}
|
||||
id="site_address"
|
||||
value={data.site_address || ''}
|
||||
onChange={(e) =>
|
||||
setData('address', e.target.value)
|
||||
setData('site_address', e.target.value)
|
||||
}
|
||||
placeholder="Alamat lengkap toko"
|
||||
rows={3}
|
||||
/>
|
||||
<FieldError
|
||||
error={errors.address}
|
||||
error={errors.site_address}
|
||||
label="Alamat"
|
||||
className="mt-1 text-xs"
|
||||
/>
|
||||
|
||||
@ -141,11 +141,11 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
{setting?.logo_light_url ? (
|
||||
<img
|
||||
src={setting.logo_light_url}
|
||||
alt={setting.name}
|
||||
alt={setting.site_name}
|
||||
className="max-h-8 w-auto"
|
||||
/>
|
||||
) : (
|
||||
setting?.name || 'VN Grup'
|
||||
setting?.site_name || 'VN Grup'
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -334,7 +334,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
<p className="mb-4 text-sm font-medium opacity-60">{formatCurrency(item.retail_price)}</p>
|
||||
|
||||
<a
|
||||
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.name || 'VN Grup'}, saya ingin memesan ${item.name} seharga ${formatCurrency(item.retail_price)}. Mohon info detail pembayarannya.`)}`}
|
||||
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.site_name || 'VN Grup'}, saya ingin memesan ${item.name} seharga ${formatCurrency(item.retail_price)}. Mohon info detail pembayarannya.`)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 border border-[#1e4a6d] px-6 py-2 text-[8px] font-bold uppercase tracking-widest transition-all hover:bg-[#1e4a6d] hover:text-white"
|
||||
@ -412,10 +412,10 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
{/* Brand Info */}
|
||||
<div className="lg:col-span-6">
|
||||
<div className="mb-6 flex items-center gap-2 text-xl font-bold uppercase tracking-tighter">
|
||||
{setting?.logo_light_url ? <img src={setting.logo_light_url} alt={setting.name} className="max-h-8" /> : (setting?.name || 'VN Grup')}
|
||||
{setting?.logo_light_url ? <img src={setting.logo_light_url} alt={setting.site_name} className="max-h-8" /> : (setting?.site_name || 'VN Grup')}
|
||||
</div>
|
||||
<p className="mb-8 text-sm leading-relaxed opacity-60">
|
||||
{setting?.description || 'Menghadirkan produk esensial yang tak lekang oleh waktu dengan fokus pada kualitas, keberlanjutan, dan gaya yang abadi.'}
|
||||
{setting?.site_description || 'Menghadirkan produk esensial yang tak lekang oleh waktu dengan fokus pada kualitas, keberlanjutan, dan gaya yang abadi.'}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Facebook size={18} className="cursor-pointer hover:opacity-50" />
|
||||
@ -443,13 +443,13 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
<Search size={14} className="rotate-45" />
|
||||
</div>
|
||||
<p className="leading-relaxed whitespace-pre-line">
|
||||
{setting?.address || 'Jl. Raya Utama No. 123, \n Kec. Lengkong, Kota Bandung, \n Jawa Barat 40262'}
|
||||
{setting?.site_address || 'Jl. Raya Utama No. 123, \n Kec. Lengkong, Kota Bandung, \n Jawa Barat 40262'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone size={14} />
|
||||
<a href={`https://wa.me/${(setting?.phone || '+628123456789').replace(/\D/g, '')}`} target="_blank" className="hover:underline">
|
||||
{setting?.phone || '+62 812 3456 789'}
|
||||
<a href={`https://wa.me/${(setting?.site_phone || '+628123456789').replace(/\D/g, '')}`} target="_blank" className="hover:underline">
|
||||
{setting?.site_phone || '+62 812 3456 789'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -458,7 +458,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="px-6 py-6 text-center text-[10px] font-bold uppercase tracking-widest lg:px-12 lg:text-left">
|
||||
<p className="opacity-50">© {new Date().getFullYear()} {setting?.name || 'VN Grup'}.</p>
|
||||
<p className="opacity-50">© {new Date().getFullYear()} {setting?.site_name || 'VN Grup'}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@ -564,7 +564,7 @@ export default function Homepage({ categories = [], bestSellers, topSellingProdu
|
||||
|
||||
<div className="mt-auto pt-8 border-t border-[#1e4a6d]/10">
|
||||
<a
|
||||
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.name || 'VN Grup'}, saya tertarik dengan produk ${selectedProduct?.name} seharga ${formatCurrency(selectedProduct?.retail_price || 0)}. Bisa tanya-tanya dulu?`)}`}
|
||||
href={`https://wa.me/628123456789?text=${encodeURIComponent(`Halo ${setting?.site_name || 'VN Grup'}, saya tertarik dengan produk ${selectedProduct?.name} seharga ${formatCurrency(selectedProduct?.retail_price || 0)}. Bisa tanya-tanya dulu?`)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full bg-[#1e4a6d] py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-white transition-all hover:bg-[#15344e] flex items-center justify-center gap-2"
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
export interface GeneralSetting {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
address: string | null;
|
||||
phone: string | null;
|
||||
site_name: string;
|
||||
site_description: string | null;
|
||||
site_address: string | null;
|
||||
site_phone: string | null;
|
||||
logo_light_url: string | null;
|
||||
logo_dark_url: string | null;
|
||||
icon_light_url: string | null;
|
||||
icon_dark_url: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@ -31,12 +31,8 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
@php
|
||||
$setting = \App\Models\GeneralSetting::first();
|
||||
@endphp
|
||||
|
||||
@if ($setting && $setting->icon_light_url)
|
||||
<link rel="icon" href="{{ $setting->icon_light_url }}" sizes="any">
|
||||
@if ($setting['icon_light_url'] ?? null)
|
||||
<link rel="icon" href="{{ $setting['icon_light_url'] }}" sizes="any">
|
||||
@else
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
@endif
|
||||
@ -47,7 +43,7 @@
|
||||
@viteReactRefresh
|
||||
@vite(['resources/css/app.css', 'resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
|
||||
<x-inertia::head>
|
||||
<title>{{ config('app.name', 'VN Grup') }}</title>
|
||||
<title>{{ $setting['site_name'] ?? config('app.name', 'VN Grup') }}</title>
|
||||
</x-inertia::head>
|
||||
</head>
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
Route::get('orders', [OrderController::class, 'index'])->name('order.index')->middleware('can:View:Order');
|
||||
Route::get('order/create', [OrderController::class, 'create'])->name('order.create')->middleware('can:Create:Order');
|
||||
Route::post('order/store', [OrderController::class, 'store'])->name('order.store')->middleware('can:Create:Order');
|
||||
Route::get('order/show/{order}', [OrderController::class, 'show'])->name('order.show')->middleware('can:View:Order');
|
||||
Route::get('order/edit/{order}', [OrderController::class, 'edit'])->name('order.edit')->middleware('can:Edit:Order');
|
||||
Route::patch('order/update/{order}', [OrderController::class, 'update'])->name('order.update')->middleware('can:Edit:Order');
|
||||
Route::delete('order/destroy/{order}', [OrderController::class, 'destroy'])->name('order.destroy')->middleware('can:Delete:Order');
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Product;
|
||||
use App\Settings\GeneralSettings;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
use function Pest\Laravel\assertDatabaseHas;
|
||||
@ -45,6 +46,13 @@
|
||||
'DeleteAny:Order',
|
||||
]);
|
||||
actingAs($user);
|
||||
|
||||
$settings = app(GeneralSettings::class);
|
||||
$settings->site_name = 'VN Grup';
|
||||
$settings->site_phone = '08123456789';
|
||||
$settings->site_address = 'Jl. Test No. 1';
|
||||
$settings->site_description = 'Test Description';
|
||||
$settings->save();
|
||||
});
|
||||
|
||||
it('can access order index page', function () {
|
||||
@ -71,6 +79,45 @@
|
||||
->component('admin/manage/order/create')
|
||||
->has('products')
|
||||
->has('cartItems')
|
||||
->where('orderStatus', function ($status) {
|
||||
return collect($status)->every(fn ($item) => $item['value'] !== OrderStatus::CANCELLED->value);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can access order show page', function () {
|
||||
$order = Order::factory()
|
||||
->has(OrderItem::factory()->count(2), 'items')
|
||||
->create();
|
||||
|
||||
get(route('order.show', $order))
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('admin/manage/order/show')
|
||||
->has('order', fn ($page) => $page
|
||||
->where('id', $order->id)
|
||||
->has('items', 2)
|
||||
->has('items.0.product')
|
||||
->has('user')
|
||||
->etc()
|
||||
)
|
||||
->has('setting')
|
||||
);
|
||||
});
|
||||
|
||||
it('can access order edit page', function () {
|
||||
$order = Order::factory()->create();
|
||||
|
||||
get(route('order.edit', $order))
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('admin/manage/order/edit')
|
||||
->has('order')
|
||||
->has('products')
|
||||
->has('orderStatus')
|
||||
->has('orderChannels')
|
||||
->has('paymentMethods')
|
||||
->has('priceTypes')
|
||||
);
|
||||
});
|
||||
|
||||
@ -169,6 +216,65 @@
|
||||
->assertJsonValidationErrors(['customer_name', 'items']);
|
||||
});
|
||||
|
||||
it('returns flash error when stock is insufficient on order store', function () {
|
||||
$product = Product::factory()->create(['stock' => 1]);
|
||||
|
||||
$data = [
|
||||
'customer_name' => 'John Doe',
|
||||
'subtotal' => 20000,
|
||||
'discount' => 0,
|
||||
'payment' => 20000,
|
||||
'payment_method' => PaymentMethod::CASH->value,
|
||||
'order_status' => OrderStatus::PENDING->value,
|
||||
'order_channel' => OrderChannel::STORE->value,
|
||||
'items' => [
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'qty' => 5, // lebih dari stok
|
||||
'price' => 10000,
|
||||
'total' => 50000,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
postJson(route('order.store'), $data)
|
||||
->assertRedirect()
|
||||
->assertSessionHas('error');
|
||||
|
||||
expect($product->fresh()->stock)->toBe(1);
|
||||
});
|
||||
|
||||
it('returns flash error when stock is insufficient on order update', function () {
|
||||
$product = Product::factory()->create(['stock' => 2]);
|
||||
$order = Order::factory()->create();
|
||||
|
||||
$newData = [
|
||||
'customer_name' => 'Updated Customer',
|
||||
'subtotal' => 30000,
|
||||
'discount' => 0,
|
||||
'payment' => 30000,
|
||||
'payment_method' => PaymentMethod::CASH->value,
|
||||
'order_status' => OrderStatus::PROCESSING->value,
|
||||
'order_channel' => OrderChannel::STORE->value,
|
||||
'items' => [
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'qty' => 10, // lebih dari stok
|
||||
'price' => 3000,
|
||||
'total' => 30000,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
patchJson(route('order.update', $order), $newData)
|
||||
->assertRedirect()
|
||||
->assertSessionHas('error');
|
||||
|
||||
expect($product->fresh()->stock)->toBe(2);
|
||||
});
|
||||
|
||||
it('can update an order', function () {
|
||||
$product1 = Product::factory()->create(['stock' => 10]);
|
||||
$product2 = Product::factory()->create(['stock' => 10]);
|
||||
@ -234,6 +340,54 @@
|
||||
expect($product2->fresh()->stock)->toBe(7);
|
||||
});
|
||||
|
||||
it('restores stock and preserves items when order status is changed to gagal', function () {
|
||||
$product = Product::factory()->create(['stock' => 10]);
|
||||
$order = Order::factory()->create(['order_status' => OrderStatus::PENDING->value]);
|
||||
|
||||
$item = $order->items()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'product_id' => $product->id,
|
||||
'qty' => 3,
|
||||
'price' => 1000,
|
||||
'total' => 3000,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
]);
|
||||
|
||||
$product->decrement('stock', 3);
|
||||
expect($product->fresh()->stock)->toBe(7);
|
||||
|
||||
$newData = [
|
||||
'customer_name' => $order->customer_name,
|
||||
'subtotal' => 3000,
|
||||
'discount' => 0,
|
||||
'payment' => 3000,
|
||||
'payment_method' => $order->payment_method,
|
||||
'order_status' => OrderStatus::CANCELLED->value, // Change to Gagal
|
||||
'order_channel' => $order->order_channel,
|
||||
'items' => [
|
||||
// Even if we send items, they shouldn't be deleted/updated based on my early return
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'qty' => 5, // different qty
|
||||
'price' => 1000,
|
||||
'total' => 5000,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
patchJson(route('order.update', $order), $newData)
|
||||
->assertRedirect();
|
||||
|
||||
// Stock should be restored (7 + 3 = 10)
|
||||
expect($product->fresh()->stock)->toBe(10);
|
||||
|
||||
// Items should be preserved (original item with qty 3, NOT deleted/recreated with qty 5)
|
||||
expect($order->items()->count())->toBe(1);
|
||||
expect($order->items()->first()->qty)->toBe(3);
|
||||
expect($order->items()->first()->id)->toBe($item->id); // Same ID means not deleted/recreated
|
||||
});
|
||||
|
||||
it('can delete an order and restore stock', function () {
|
||||
$product = Product::factory()->create(['stock' => 5]);
|
||||
$order = Order::factory()->create();
|
||||
@ -275,6 +429,12 @@
|
||||
actingAs(createUnauthorizedUser());
|
||||
});
|
||||
|
||||
it('cannot view an order without permission', function () {
|
||||
$order = Order::factory()->create();
|
||||
get(route('order.show', $order))
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
it('cannot store an order without permission', function () {
|
||||
postJson(route('order.store'), ['customer_name' => 'Unauthorized'])
|
||||
->assertStatus(403);
|
||||
|
||||
@ -139,6 +139,61 @@
|
||||
->assertJsonValidationErrors(['purchase_date', 'items']);
|
||||
});
|
||||
|
||||
it('returns flash error when stock is insufficient on purchase update', function () {
|
||||
$product = Product::factory()->create(['stock' => 0]);
|
||||
|
||||
$purchase = Purchase::create([
|
||||
'purchase_date' => now()->format('Y-m-d'),
|
||||
'note' => 'Old Note',
|
||||
'total' => 5000,
|
||||
]);
|
||||
|
||||
$purchase->items()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'product_id' => $product->id,
|
||||
'quantity' => 5,
|
||||
'unit_price' => 1000,
|
||||
'total_price' => 5000,
|
||||
]);
|
||||
|
||||
$newData = [
|
||||
'purchase_date' => now()->format('Y-m-d'),
|
||||
'note' => 'Updated Note',
|
||||
'items' => [
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'quantity' => 10,
|
||||
'unit_price' => 2000,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
patchJson(route('purchase.update', $purchase), $newData)
|
||||
->assertRedirect()
|
||||
->assertSessionHas('error');
|
||||
|
||||
expect($purchase->fresh()->note)->toBe('Old Note');
|
||||
});
|
||||
|
||||
it('returns flash error when stock is insufficient on purchase delete', function () {
|
||||
$product = Product::factory()->create(['stock' => 0]);
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$purchase->items()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'product_id' => $product->id,
|
||||
'quantity' => 5,
|
||||
'unit_price' => 1000,
|
||||
'total_price' => 5000,
|
||||
]);
|
||||
|
||||
deleteJson(route('purchase.destroy', $purchase))
|
||||
->assertRedirect()
|
||||
->assertSessionHas('error');
|
||||
|
||||
expect(Purchase::find($purchase->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('can access purchase edit page', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
|
||||
@ -50,17 +50,38 @@
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('admin/master/user/index')
|
||||
->has('users', fn ($page) => $page->has('data')->etc())
|
||||
->has('roles')
|
||||
->has('defaultPassword')
|
||||
);
|
||||
});
|
||||
|
||||
it('excludes users with Developer role from index', function () {
|
||||
$developerRole = Role::create(['name' => 'Developer']);
|
||||
$developer = User::factory()->create();
|
||||
$developer->assignRole($developerRole);
|
||||
|
||||
$regularUser = User::factory()->create();
|
||||
|
||||
get(route('user.index'))
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->has('users.data', fn ($data) => $data
|
||||
->each(fn ($user) => $user->where('id', fn ($id) => $id !== $developer->id)->etc())
|
||||
->etc()
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('can access user create page', function () {
|
||||
Role::create(['name' => 'Developer']);
|
||||
Role::create(['name' => 'Admin']);
|
||||
|
||||
get(route('user.create'))
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('admin/master/user/create')
|
||||
->has('roles')
|
||||
->has('roles', fn ($roles) => $roles
|
||||
->each(fn ($role) => $role->where('name', fn ($name) => $name !== 'Developer')->etc())
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@ -101,10 +122,12 @@
|
||||
it('validates user creation', function () {
|
||||
postJson(route('user.store'), [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username', 'email', 'nik', 'full_name', 'phone_number', 'address', 'birth_place', 'birth_date', 'base_salary']);
|
||||
->assertJsonValidationErrors(['username', 'email', 'nik', 'full_name', 'phone_number', 'address', 'birth_place', 'birth_date', 'base_salary', 'roles']);
|
||||
});
|
||||
|
||||
it('can access user edit page', function () {
|
||||
Role::create(['name' => 'Developer']);
|
||||
Role::create(['name' => 'Admin']);
|
||||
$user = User::factory()->create();
|
||||
|
||||
get(route('user.edit', $user))
|
||||
@ -112,7 +135,9 @@
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('admin/master/user/edit')
|
||||
->has('user')
|
||||
->has('roles')
|
||||
->has('roles', fn ($roles) => $roles
|
||||
->each(fn ($role) => $role->where('name', fn ($name) => $name !== 'Developer')->etc())
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GeneralSetting;
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
@ -49,19 +49,11 @@
|
||||
it('can update general settings with logo and icon variants', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
// Ensure a setting exists first for testing update logic in Controller
|
||||
$setting = GeneralSetting::create([
|
||||
'name' => 'Old Name',
|
||||
'description' => 'Old Description',
|
||||
'address' => 'Old Address',
|
||||
'phone' => '000',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'name' => 'VNGrup Dress',
|
||||
'description' => 'Toko baju premium',
|
||||
'address' => 'Jakarta, Indonesia',
|
||||
'phone' => '081234567890',
|
||||
'site_name' => 'VNGrup Dress',
|
||||
'site_description' => 'Toko baju premium',
|
||||
'site_address' => 'Jakarta, Indonesia',
|
||||
'site_phone' => '081234567890',
|
||||
'logo_light' => UploadedFile::fake()->image('logo_light.png'),
|
||||
'logo_dark' => UploadedFile::fake()->image('logo_dark.png'),
|
||||
'icon_light' => UploadedFile::fake()->image('icon_light.png'),
|
||||
@ -72,26 +64,34 @@
|
||||
->assertRedirect()
|
||||
->assertSessionHas('success');
|
||||
|
||||
assertDatabaseHas('general_settings', [
|
||||
'id' => $setting->id,
|
||||
'name' => 'VNGrup Dress',
|
||||
'phone' => '081234567890',
|
||||
assertDatabaseHas('settings', [
|
||||
'group' => 'general',
|
||||
'name' => 'site_name',
|
||||
'payload' => json_encode('VNGrup Dress'),
|
||||
]);
|
||||
|
||||
$setting->refresh();
|
||||
expect($setting->getFirstMediaUrl('logo_light'))->not->toBeEmpty();
|
||||
expect($setting->getFirstMediaUrl('logo_dark'))->not->toBeEmpty();
|
||||
expect($setting->getFirstMediaUrl('icon_light'))->not->toBeEmpty();
|
||||
expect($setting->getFirstMediaUrl('icon_dark'))->not->toBeEmpty();
|
||||
assertDatabaseHas('settings', [
|
||||
'group' => 'general',
|
||||
'name' => 'site_phone',
|
||||
'payload' => json_encode('081234567890'),
|
||||
]);
|
||||
|
||||
$settings = app(GeneralSettings::class);
|
||||
expect($settings->logo_light)->not->toBeNull();
|
||||
expect($settings->logo_dark)->not->toBeNull();
|
||||
expect($settings->icon_light)->not->toBeNull();
|
||||
expect($settings->icon_dark)->not->toBeNull();
|
||||
|
||||
Storage::disk('public')->assertExists($settings->logo_light);
|
||||
Storage::disk('public')->assertExists($settings->logo_dark);
|
||||
Storage::disk('public')->assertExists($settings->icon_light);
|
||||
Storage::disk('public')->assertExists($settings->icon_dark);
|
||||
});
|
||||
|
||||
it('validates settings update with new variants', function () {
|
||||
// If setting doesn't exist, logo_light and icon_light are required
|
||||
GeneralSetting::truncate();
|
||||
|
||||
postJson(route('system.settings.update'), [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'description', 'address', 'phone', 'logo_light', 'icon_light']);
|
||||
->assertJsonValidationErrors(['site_name', 'site_description', 'site_address', 'site_phone']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
});
|
||||
|
||||
it('cannot update settings without permission', function () {
|
||||
postJson(route('system.settings.update'), ['name' => 'Unauthorized'])
|
||||
postJson(route('system.settings.update'), ['site_name' => 'Unauthorized'])
|
||||
->assertStatus(403);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user