Enhance order and purchase management by implementing draft item functionality. Update OrderController and PurchaseController to handle draft items for users, allowing for better cart management. Refactor OrderService and PurchaseService to include methods for syncing and removing draft items. Update related request classes and models to support user associations. Modify frontend components to restore draft items and synchronize quantities, improving user experience during order and purchase creation.
This commit is contained in:
parent
b7f5c4e12e
commit
4c91834389
@ -35,13 +35,16 @@ public function index(Request $request): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('admin/manage/orders/Create', [
|
||||
'customers' => $this->orderService->customerOptions(),
|
||||
'catalog' => $this->orderService->catalogItems(),
|
||||
'catalog' => $this->orderService->catalogItems(user: $user),
|
||||
'channels' => OrderChannel::selectOptions(),
|
||||
'storePriceTypes' => $this->orderService->storePriceTypeOptions(),
|
||||
'draftItems' => $this->orderService->draftItemsForUser($user),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\OrderDraftItemRequest;
|
||||
use App\Http\Requests\Admin\Manage\OrderDraftResyncPricesRequest;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Services\Manage\OrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OrderDraftItemController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderService $orderService,
|
||||
) {}
|
||||
|
||||
public function store(OrderDraftItemRequest $request): JsonResponse
|
||||
{
|
||||
$item = $this->orderService->syncDraftItem($request->validated(), $request->user());
|
||||
|
||||
return response()->json(['item' => $item]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, ProductVariant $productVariant): JsonResponse
|
||||
{
|
||||
$this->orderService->removeDraftItem($request->user(), $productVariant);
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function resyncPrices(OrderDraftResyncPricesRequest $request): JsonResponse
|
||||
{
|
||||
$items = $this->orderService->resyncDraftPrices(
|
||||
$request->user(),
|
||||
$request->validated('price_type'),
|
||||
);
|
||||
|
||||
return response()->json(['items' => $items]);
|
||||
}
|
||||
}
|
||||
@ -32,11 +32,14 @@ public function index(Request $request): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('admin/manage/purchases/Create', [
|
||||
'suppliers' => $this->purchaseService->supplierOptions(),
|
||||
'catalog' => $this->purchaseService->catalogItems(),
|
||||
'catalog' => $this->purchaseService->catalogItems(user: $user),
|
||||
'draftItems' => $this->purchaseService->draftItemsForUser($user),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\PurchaseDraftItemRequest;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Manage\PurchaseService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PurchaseDraftItemController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PurchaseService $purchaseService,
|
||||
) {}
|
||||
|
||||
public function store(PurchaseDraftItemRequest $request): JsonResponse
|
||||
{
|
||||
$item = $this->purchaseService->syncDraftItem($request->validated(), $request->user());
|
||||
|
||||
return response()->json(['item' => $item]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, RawMaterialPrice $rawMaterialPrice): JsonResponse
|
||||
{
|
||||
$this->purchaseService->removeDraftItem($request->user(), $rawMaterialPrice);
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/Admin/Manage/OrderDraftItemRequest.php
Normal file
32
app/Http/Requests/Admin/Manage/OrderDraftItemRequest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\PriceType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class OrderDraftItemRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can(Permission::ORDERS_CREATE->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'product_variant_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'quantity' => ['required', 'integer', 'min:1'],
|
||||
'price_type' => ['required', Rule::enum(PriceType::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\PriceType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class OrderDraftResyncPricesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can(Permission::ORDERS_CREATE->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'price_type' => ['required', Rule::enum(PriceType::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -33,15 +33,17 @@ public function rules(): array
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
'marketplace_fee' => ['nullable', 'integer', 'min:0'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
];
|
||||
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.product_variant_id' => [
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
$rules['items'] = ['required', 'array', 'min:1'];
|
||||
$rules['items.*.product_variant_id'] = [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||
];
|
||||
];
|
||||
$rules['items.*.quantity'] = ['required', 'integer', 'min:1'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
30
app/Http/Requests/Admin/Manage/PurchaseDraftItemRequest.php
Normal file
30
app/Http/Requests/Admin/Manage/PurchaseDraftItemRequest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PurchaseDraftItemRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can(Permission::PURCHASES_CREATE->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'raw_material_price_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -25,20 +25,24 @@ public function authorize(): bool
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
$rules = [
|
||||
'supplier_id' => ['required', 'integer', Rule::exists('suppliers', 'id')->whereNull('deleted_at')],
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
'notes' => ['nullable', 'string', 'max:100'],
|
||||
...$this->photoRules(),
|
||||
];
|
||||
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.raw_material_price_id' => [
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
$rules['items'] = ['required', 'array', 'min:1'];
|
||||
$rules['items.*.raw_material_price_id'] = [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'items.*.quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||
...$this->photoRules(),
|
||||
];
|
||||
];
|
||||
$rules['items.*.quantity'] = ['required', 'numeric', 'decimal:0,4', 'gt:0'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -33,6 +33,11 @@ protected function casts(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
|
||||
@ -34,6 +34,11 @@ protected function casts(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function purchase(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Purchase::class);
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -97,11 +98,11 @@ public function storePriceTypeOptions(): array
|
||||
/**
|
||||
* @return Collection<int, Product>
|
||||
*/
|
||||
public function catalogItems(?Order $order = null): Collection
|
||||
public function catalogItems(?Order $order = null, ?User $user = null): Collection
|
||||
{
|
||||
$orderVariantIds = $order
|
||||
? $order->items()->pluck('product_variant_id')->all()
|
||||
: [];
|
||||
: ($user ? $this->draftItemsQuery($user)->pluck('product_variant_id')->all() : []);
|
||||
|
||||
return Product::query()
|
||||
->with([
|
||||
@ -155,6 +156,110 @@ public function findForEdit(Order $order): Order
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function draftItemsForUser(User $user): array
|
||||
{
|
||||
return $this->draftItemsQuery($user)
|
||||
->with([
|
||||
'productVariant.product:id,name',
|
||||
'productVariant.media',
|
||||
])
|
||||
->get()
|
||||
->map(fn (OrderItem $item) => $this->presentDraftItem($item))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function syncDraftItem(array $validated, User $user): array
|
||||
{
|
||||
$priceType = PriceType::from($validated['price_type']);
|
||||
$variant = ProductVariant::query()->findOrFail($validated['product_variant_id']);
|
||||
$price = ProductPrice::query()
|
||||
->where('variant_id', $variant->id)
|
||||
->where('type', $priceType)
|
||||
->first();
|
||||
|
||||
if ($price === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'product_variant_id' => 'Harga untuk tipe harga ini belum diatur.',
|
||||
]);
|
||||
}
|
||||
|
||||
$quantity = (int) $validated['quantity'];
|
||||
$unitPrice = (int) $price->price;
|
||||
$subtotal = $unitPrice * $quantity;
|
||||
|
||||
$item = OrderItem::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'product_variant_id' => $variant->id,
|
||||
'order_id' => null,
|
||||
],
|
||||
[
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'subtotal' => $subtotal,
|
||||
],
|
||||
);
|
||||
|
||||
$item->load([
|
||||
'productVariant.product:id,name',
|
||||
'productVariant.media',
|
||||
]);
|
||||
|
||||
return $this->presentDraftItem($item);
|
||||
}
|
||||
|
||||
public function removeDraftItem(User $user, ProductVariant $productVariant): void
|
||||
{
|
||||
OrderItem::query()
|
||||
->whereNull('order_id')
|
||||
->where('user_id', $user->id)
|
||||
->where('product_variant_id', $productVariant->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function resyncDraftPrices(User $user, string $priceTypeValue): array
|
||||
{
|
||||
$priceType = PriceType::from($priceTypeValue);
|
||||
|
||||
$items = $this->draftItemsQuery($user)
|
||||
->with([
|
||||
'productVariant.product:id,name',
|
||||
'productVariant.media',
|
||||
])
|
||||
->get();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$price = ProductPrice::query()
|
||||
->where('variant_id', $item->product_variant_id)
|
||||
->where('type', $priceType)
|
||||
->first();
|
||||
|
||||
if ($price === null) {
|
||||
$item->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$unitPrice = (int) $price->price;
|
||||
$item->unit_price = $unitPrice;
|
||||
$item->subtotal = $unitPrice * $item->quantity;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
return $this->draftItemsForUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
@ -162,8 +267,21 @@ public function create(array $validated, User $user): Order
|
||||
{
|
||||
return DB::transaction(function () use ($validated, $user): Order {
|
||||
$priceType = $this->resolvePriceType($validated['channel'], $validated['price_type']);
|
||||
$lineItems = $this->buildLineItems($validated['items'], $priceType);
|
||||
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
||||
|
||||
/** @var EloquentCollection<int, OrderItem> $draftItems */
|
||||
$draftItems = $this->draftItemsQuery($user)
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
if ($draftItems->isEmpty()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => 'Tambahkan minimal satu produk ke keranjang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->applyDraftPrices($draftItems, $priceType);
|
||||
|
||||
$subtotal = $draftItems->sum('subtotal');
|
||||
$discount = (int) ($validated['discount'] ?? 0);
|
||||
$marketplaceFee = (int) ($validated['marketplace_fee'] ?? 0);
|
||||
$netAmount = max($subtotal - $discount - $marketplaceFee, 0);
|
||||
@ -181,9 +299,10 @@ public function create(array $validated, User $user): Order
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
]);
|
||||
|
||||
foreach ($lineItems as $itemData) {
|
||||
$orderItem = $order->items()->create($itemData);
|
||||
$this->decrementStock($orderItem);
|
||||
foreach ($draftItems as $item) {
|
||||
$item->order_id = $order->id;
|
||||
$item->save();
|
||||
$this->decrementStock($item);
|
||||
}
|
||||
|
||||
return $order;
|
||||
@ -342,6 +461,57 @@ private function resolvePriceType(string $channel, string $priceType): PriceType
|
||||
return $priceTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<OrderItem>
|
||||
*/
|
||||
private function draftItemsQuery(User $user): Builder
|
||||
{
|
||||
return OrderItem::query()
|
||||
->whereNull('order_id')
|
||||
->where('user_id', $user->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function presentDraftItem(OrderItem $item): array
|
||||
{
|
||||
$variant = $item->productVariant;
|
||||
|
||||
return [
|
||||
'product_variant_id' => $item->product_variant_id,
|
||||
'product_name' => $variant?->product?->name ?? '',
|
||||
'variant_name' => $variant?->name ?? '',
|
||||
'quantity' => (string) $item->quantity,
|
||||
'unit_price' => $item->unit_price,
|
||||
'images' => $variant ? MediaPresenter::collection($variant, 'images') : [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EloquentCollection<int, OrderItem> $items
|
||||
*/
|
||||
private function applyDraftPrices(EloquentCollection $items, PriceType $priceType): void
|
||||
{
|
||||
foreach ($items as $index => $item) {
|
||||
$price = ProductPrice::query()
|
||||
->where('variant_id', $item->product_variant_id)
|
||||
->where('type', $priceType)
|
||||
->first();
|
||||
|
||||
if ($price === null) {
|
||||
throw ValidationException::withMessages([
|
||||
"items.{$index}.product_variant_id" => 'Harga untuk tipe harga ini belum diatur.',
|
||||
]);
|
||||
}
|
||||
|
||||
$unitPrice = (int) $price->price;
|
||||
$item->unit_price = $unitPrice;
|
||||
$item->subtotal = $unitPrice * $item->quantity;
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function decrementStock(OrderItem $item): void
|
||||
{
|
||||
ProductVariant::query()
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -81,11 +82,11 @@ public function supplierOptions(): array
|
||||
/**
|
||||
* @return Collection<int, RawMaterial>
|
||||
*/
|
||||
public function catalogItems(?Purchase $purchase = null): Collection
|
||||
public function catalogItems(?Purchase $purchase = null, ?User $user = null): Collection
|
||||
{
|
||||
$purchasePriceIds = $purchase
|
||||
? $purchase->items()->pluck('raw_material_price_id')->all()
|
||||
: [];
|
||||
: ($user ? $this->draftItemsQuery($user)->pluck('raw_material_price_id')->all() : []);
|
||||
|
||||
return RawMaterial::query()
|
||||
->with([
|
||||
@ -141,14 +142,84 @@ public function findForEdit(Purchase $purchase): Purchase
|
||||
return $purchase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function draftItemsForUser(User $user): array
|
||||
{
|
||||
return $this->draftItemsQuery($user)
|
||||
->with([
|
||||
'rawMaterialPrice.rawMaterial:id,name,unit',
|
||||
'rawMaterialPrice.media',
|
||||
])
|
||||
->get()
|
||||
->map(fn (PurchaseItem $item) => $this->presentDraftItem($item))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function syncDraftItem(array $validated, User $user): array
|
||||
{
|
||||
$price = RawMaterialPrice::query()
|
||||
->with('rawMaterial:id,name,unit')
|
||||
->findOrFail($validated['raw_material_price_id']);
|
||||
|
||||
$quantity = (float) $validated['quantity'];
|
||||
$unitPrice = (int) $price->price;
|
||||
$subtotal = (int) round($quantity * $unitPrice);
|
||||
|
||||
$item = PurchaseItem::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'raw_material_price_id' => $price->id,
|
||||
'purchase_id' => null,
|
||||
],
|
||||
[
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'subtotal' => $subtotal,
|
||||
],
|
||||
);
|
||||
|
||||
$item->load([
|
||||
'rawMaterialPrice.rawMaterial:id,name,unit',
|
||||
'rawMaterialPrice.media',
|
||||
]);
|
||||
|
||||
return $this->presentDraftItem($item);
|
||||
}
|
||||
|
||||
public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice): void
|
||||
{
|
||||
PurchaseItem::query()
|
||||
->whereNull('purchase_id')
|
||||
->where('user_id', $user->id)
|
||||
->where('raw_material_price_id', $rawMaterialPrice->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function create(array $validated, User $user): Purchase
|
||||
{
|
||||
return DB::transaction(function () use ($validated, $user): Purchase {
|
||||
$lineItems = $this->buildLineItems($validated['items']);
|
||||
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
||||
/** @var EloquentCollection<int, PurchaseItem> $draftItems */
|
||||
$draftItems = $this->draftItemsQuery($user)
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
if ($draftItems->isEmpty()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => 'Tambahkan minimal satu bahan baku ke keranjang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$subtotal = $draftItems->sum('subtotal');
|
||||
$discount = (int) ($validated['discount'] ?? 0);
|
||||
$total = max($subtotal - $discount, 0);
|
||||
|
||||
@ -161,9 +232,10 @@ public function create(array $validated, User $user): Purchase
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
]);
|
||||
|
||||
foreach ($lineItems as $itemData) {
|
||||
$purchaseItem = $purchase->items()->create($itemData);
|
||||
$this->incrementStock($purchaseItem);
|
||||
foreach ($draftItems as $item) {
|
||||
$item->purchase_id = $purchase->id;
|
||||
$item->save();
|
||||
$this->incrementStock($item);
|
||||
}
|
||||
|
||||
$this->syncPhotos($purchase, $validated);
|
||||
@ -268,6 +340,35 @@ private function syncPhotos(Purchase $purchase, array $validated): void
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<PurchaseItem>
|
||||
*/
|
||||
private function draftItemsQuery(User $user): Builder
|
||||
{
|
||||
return PurchaseItem::query()
|
||||
->whereNull('purchase_id')
|
||||
->where('user_id', $user->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function presentDraftItem(PurchaseItem $item): array
|
||||
{
|
||||
$price = $item->rawMaterialPrice;
|
||||
$rawMaterial = $price?->rawMaterial;
|
||||
|
||||
return [
|
||||
'raw_material_price_id' => $item->raw_material_price_id,
|
||||
'raw_material_name' => $rawMaterial?->name ?? '',
|
||||
'variant' => $price?->variant ?? '',
|
||||
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
|
||||
'quantity' => $item->quantity_input,
|
||||
'unit_price' => $item->unit_price,
|
||||
'images' => $price ? MediaPresenter::collection($price, 'images') : [],
|
||||
];
|
||||
}
|
||||
|
||||
private function incrementStock(PurchaseItem $item): void
|
||||
{
|
||||
RawMaterialPrice::query()
|
||||
|
||||
@ -11,7 +11,8 @@ public function up(): void
|
||||
Schema::create('purchase_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('purchase_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('purchase_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('raw_material_price_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->decimal('quantity', 18, 4);
|
||||
|
||||
@ -11,7 +11,8 @@ public function up(): void
|
||||
Schema::create('order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('order_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('product_variant_id')->constrained()->restrictOnDelete();
|
||||
|
||||
$table->unsignedInteger('quantity');
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||
import { formErrors } from '@/lib/form';
|
||||
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||
@ -52,11 +53,14 @@ const props = defineProps<{
|
||||
notes: string;
|
||||
items: OrderCartItem[];
|
||||
};
|
||||
draftItems?: OrderCartItem[];
|
||||
submitUrl: string;
|
||||
method: 'post' | 'put';
|
||||
submitLabel: string;
|
||||
}>();
|
||||
|
||||
const isCreateMode = computed(() => props.method === 'post');
|
||||
|
||||
const search = ref('');
|
||||
const cart = ref<OrderCartItem[]>([]);
|
||||
|
||||
@ -94,6 +98,17 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function populateDraftItems() {
|
||||
if (!isCreateMode.value || !props.draftItems?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
cart.value = props.draftItems.map((item) => ({ ...item }));
|
||||
toast.info('Keranjang dipulihkan dari data tersimpan.');
|
||||
}
|
||||
|
||||
populateDraftItems();
|
||||
|
||||
watch(
|
||||
() => form.channel,
|
||||
(channel) => {
|
||||
@ -107,6 +122,43 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.price_type,
|
||||
async (priceType) => {
|
||||
if (!isCreateMode.value || cart.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { items } = await apiFetch<{ items: OrderCartItem[] }>(
|
||||
'/admin/manage/orders/draft-items/resync-prices',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ price_type: priceType }),
|
||||
},
|
||||
);
|
||||
|
||||
cart.value = items.map((item) => ({ ...item }));
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui harga keranjang.');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function upsertCartItem(item: OrderCartItem) {
|
||||
const index = cart.value.findIndex(
|
||||
(cartItem) => cartItem.product_variant_id === item.product_variant_id,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
cart.value.push({ ...item });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cart.value[index] = { ...item };
|
||||
}
|
||||
|
||||
const filteredCatalog = computed(() => {
|
||||
const keyword = search.value.trim().toLowerCase();
|
||||
|
||||
@ -138,7 +190,20 @@ function lineSubtotal(item: OrderCartItem): number {
|
||||
return quantity * item.unit_price;
|
||||
}
|
||||
|
||||
function addToCart(product: OrderCatalogItem, variant: ProductVariantItem) {
|
||||
async function syncDraftItem(variantId: number, quantity: number) {
|
||||
const { item } = await apiFetch<{ item: OrderCartItem }>('/admin/manage/orders/draft-items', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
product_variant_id: variantId,
|
||||
quantity,
|
||||
price_type: form.price_type,
|
||||
}),
|
||||
});
|
||||
|
||||
upsertCartItem(item);
|
||||
}
|
||||
|
||||
async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem) {
|
||||
const price = getVariantPrice(variant);
|
||||
|
||||
if (!price) {
|
||||
@ -150,10 +215,20 @@ function addToCart(product: OrderCatalogItem, variant: ProductVariantItem) {
|
||||
const existing = cart.value.find(
|
||||
(item) => item.product_variant_id === variant.id,
|
||||
);
|
||||
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await syncDraftItem(variant.id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const currentQty = Number(existing.quantity) || 0;
|
||||
existing.quantity = String(currentQty + 1);
|
||||
existing.quantity = String(nextQty);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -168,16 +243,40 @@ function addToCart(product: OrderCatalogItem, variant: ProductVariantItem) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromCart(index: number) {
|
||||
async function removeFromCart(index: number) {
|
||||
const item = cart.value[index];
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await apiFetch(`/admin/manage/orders/draft-items/${item.product_variant_id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal menghapus item dari keranjang.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cart.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function adjustQuantity(index: number, delta: number) {
|
||||
async function adjustQuantity(index: number, delta: number) {
|
||||
const item = cart.value[index];
|
||||
const nextQty = (Number(item.quantity) || 0) + delta;
|
||||
|
||||
if (nextQty < 1) {
|
||||
removeFromCart(index);
|
||||
await removeFromCart(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await syncDraftItem(item.product_variant_id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@ -185,6 +284,27 @@ function adjustQuantity(index: number, delta: number) {
|
||||
item.quantity = String(nextQty);
|
||||
}
|
||||
|
||||
async function syncCartItemQuantity(index: number) {
|
||||
const item = cart.value[index];
|
||||
const nextQty = Number(item.quantity) || 0;
|
||||
|
||||
if (nextQty < 1) {
|
||||
await removeFromCart(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCreateMode.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await syncDraftItem(item.product_variant_id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||
}
|
||||
}
|
||||
|
||||
function buildFormData(): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
@ -202,10 +322,12 @@ function buildFormData(): FormData {
|
||||
formData.append('marketplace_fee', parseRupiah(form.marketplace_fee));
|
||||
formData.append('notes', form.notes);
|
||||
|
||||
cart.value.forEach((item, index) => {
|
||||
formData.append(`items[${index}][product_variant_id]`, String(item.product_variant_id));
|
||||
formData.append(`items[${index}][quantity]`, item.quantity);
|
||||
});
|
||||
if (props.method === 'put') {
|
||||
cart.value.forEach((item, index) => {
|
||||
formData.append(`items[${index}][product_variant_id]`, String(item.product_variant_id));
|
||||
formData.append(`items[${index}][quantity]`, item.quantity);
|
||||
});
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
@ -398,7 +520,7 @@ function submit() {
|
||||
<Minus class="size-3.5" />
|
||||
</Button>
|
||||
<Input v-model="item.quantity" type="number" min="1" step="1"
|
||||
class="h-8 text-center" />
|
||||
class="h-8 text-center" @change="syncCartItemQuantity(index)" />
|
||||
<Button type="button" variant="outline" size="icon"
|
||||
class="size-8 shrink-0" @click="adjustQuantity(index, 1)">
|
||||
<Plus class="size-3.5" />
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||
import { formErrors } from '@/lib/form';
|
||||
@ -51,11 +52,13 @@ const props = defineProps<{
|
||||
items: PurchaseCartItem[];
|
||||
photos?: MediaItem | null;
|
||||
};
|
||||
draftItems?: PurchaseCartItem[];
|
||||
submitUrl: string;
|
||||
method: 'post' | 'put';
|
||||
submitLabel: string;
|
||||
}>();
|
||||
|
||||
const isCreateMode = computed(() => props.method === 'post');
|
||||
const search = ref('');
|
||||
const cart = ref<PurchaseCartItem[]>([]);
|
||||
const existingPhotoId = ref<number | null>(null);
|
||||
@ -99,6 +102,31 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function populateDraftItems() {
|
||||
if (!isCreateMode.value || !props.draftItems?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
cart.value = props.draftItems.map((item) => ({ ...item }));
|
||||
toast.info('Keranjang dipulihkan dari data tersimpan.');
|
||||
}
|
||||
|
||||
populateDraftItems();
|
||||
|
||||
function upsertCartItem(item: PurchaseCartItem) {
|
||||
const index = cart.value.findIndex(
|
||||
(cartItem) => cartItem.raw_material_price_id === item.raw_material_price_id,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
cart.value.push({ ...item });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cart.value[index] = { ...item };
|
||||
}
|
||||
|
||||
type CatalogPrice = {
|
||||
id: number;
|
||||
variant: string;
|
||||
@ -134,14 +162,36 @@ function lineSubtotal(item: PurchaseCartItem): number {
|
||||
return Math.round(quantity * item.unit_price);
|
||||
}
|
||||
|
||||
function addToCart(rawMaterial: PurchaseCatalogItem, price: CatalogPrice) {
|
||||
async function syncDraftItem(priceId: number, quantity: number) {
|
||||
const { item } = await apiFetch<{ item: PurchaseCartItem }>('/admin/manage/purchases/draft-items', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
raw_material_price_id: priceId,
|
||||
quantity,
|
||||
}),
|
||||
});
|
||||
|
||||
upsertCartItem(item);
|
||||
}
|
||||
|
||||
async function addToCart(rawMaterial: PurchaseCatalogItem, price: CatalogPrice) {
|
||||
const existing = cart.value.find(
|
||||
(item) => item.raw_material_price_id === price.id,
|
||||
);
|
||||
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await syncDraftItem(price.id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const currentQty = Number(existing.quantity) || 0;
|
||||
existing.quantity = String(currentQty + 1);
|
||||
existing.quantity = String(nextQty);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -157,16 +207,40 @@ function addToCart(rawMaterial: PurchaseCatalogItem, price: CatalogPrice) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromCart(index: number) {
|
||||
async function removeFromCart(index: number) {
|
||||
const item = cart.value[index];
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await apiFetch(`/admin/manage/purchases/draft-items/${item.raw_material_price_id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal menghapus item dari keranjang.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cart.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function adjustQuantity(index: number, delta: number) {
|
||||
async function adjustQuantity(index: number, delta: number) {
|
||||
const item = cart.value[index];
|
||||
const nextQty = (Number(item.quantity) || 0) + delta;
|
||||
|
||||
if (nextQty <= 0) {
|
||||
removeFromCart(index);
|
||||
await removeFromCart(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateMode.value) {
|
||||
try {
|
||||
await syncDraftItem(item.raw_material_price_id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@ -174,6 +248,27 @@ function adjustQuantity(index: number, delta: number) {
|
||||
item.quantity = String(nextQty);
|
||||
}
|
||||
|
||||
async function syncCartItemQuantity(index: number) {
|
||||
const item = cart.value[index];
|
||||
const nextQty = Number(item.quantity) || 0;
|
||||
|
||||
if (nextQty <= 0) {
|
||||
await removeFromCart(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCreateMode.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await syncDraftItem(item.raw_material_price_id, nextQty);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||
}
|
||||
}
|
||||
|
||||
function buildFormData(): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
@ -185,10 +280,12 @@ function buildFormData(): FormData {
|
||||
formData.append('discount', parseRupiah(form.discount));
|
||||
formData.append('notes', form.notes);
|
||||
|
||||
cart.value.forEach((item, index) => {
|
||||
formData.append(`items[${index}][raw_material_price_id]`, String(item.raw_material_price_id));
|
||||
formData.append(`items[${index}][quantity]`, item.quantity);
|
||||
});
|
||||
if (props.method === 'put') {
|
||||
cart.value.forEach((item, index) => {
|
||||
formData.append(`items[${index}][raw_material_price_id]`, String(item.raw_material_price_id));
|
||||
formData.append(`items[${index}][quantity]`, item.quantity);
|
||||
});
|
||||
}
|
||||
|
||||
const removeMediaIds = [...form.remove_media_ids];
|
||||
|
||||
@ -353,7 +450,8 @@ function submit() {
|
||||
<Minus class="size-3.5" />
|
||||
</Button>
|
||||
<Input v-model="item.quantity" type="number" min="0.0001"
|
||||
step="0.0001" class="h-8 text-center" />
|
||||
step="0.0001" class="h-8 text-center"
|
||||
@change="syncCartItemQuantity(index)" />
|
||||
<Button type="button" variant="outline" size="icon"
|
||||
class="size-8 shrink-0" @click="adjustQuantity(index, 1)">
|
||||
<Plus class="size-3.5" />
|
||||
|
||||
36
resources/js/lib/api.ts
Normal file
36
resources/js/lib/api.ts
Normal file
@ -0,0 +1,36 @@
|
||||
function getXsrfToken(): string {
|
||||
const match = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='));
|
||||
|
||||
return match ? decodeURIComponent(match.split('=')[1] ?? '') : '';
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': getXsrfToken(),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({})) as {
|
||||
message?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
const firstValidationError = error.errors
|
||||
? Object.values(error.errors).flat()[0]
|
||||
: undefined;
|
||||
|
||||
throw new Error(firstValidationError ?? error.message ?? 'Permintaan gagal diproses.');
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
@ -4,13 +4,14 @@ import { ArrowLeft } from '@lucide/vue';
|
||||
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { EnumOption, OrderCatalogItem, SelectOption } from '@/types/order';
|
||||
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
|
||||
|
||||
defineProps<{
|
||||
customers: SelectOption[];
|
||||
catalog: OrderCatalogItem[];
|
||||
channels: EnumOption[];
|
||||
storePriceTypes: EnumOption[];
|
||||
draftItems: OrderCartItem[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -38,6 +39,7 @@ defineProps<{
|
||||
:catalog="catalog"
|
||||
:channels="channels"
|
||||
:store-price-types="storePriceTypes"
|
||||
:draft-items="draftItems"
|
||||
submit-url="/admin/manage/orders"
|
||||
method="post"
|
||||
submit-label="Simpan"
|
||||
|
||||
@ -4,11 +4,12 @@ import { ArrowLeft } from '@lucide/vue';
|
||||
import PurchasePosForm from '@/components/admin/manage/purchases/PurchasePosForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { PurchaseCatalogItem, SelectOption } from '@/types/purchase';
|
||||
import type { PurchaseCartItem, PurchaseCatalogItem, SelectOption } from '@/types/purchase';
|
||||
|
||||
defineProps<{
|
||||
suppliers: SelectOption[];
|
||||
catalog: PurchaseCatalogItem[];
|
||||
draftItems: PurchaseCartItem[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -34,6 +35,7 @@ defineProps<{
|
||||
<PurchasePosForm
|
||||
:suppliers="suppliers"
|
||||
:catalog="catalog"
|
||||
:draft-items="draftItems"
|
||||
submit-url="/admin/manage/purchases"
|
||||
method="post"
|
||||
submit-label="Simpan"
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
||||
use App\Http\Controllers\Admin\Manage\CuttingController;
|
||||
use App\Http\Controllers\Admin\Manage\OrderController;
|
||||
use App\Http\Controllers\Admin\Manage\OrderDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\PurchaseController;
|
||||
use App\Http\Controllers\Admin\Manage\PurchaseDraftItemController;
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
@ -168,6 +170,14 @@
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('create');
|
||||
|
||||
Route::post('draft-items', [PurchaseDraftItemController::class, 'store'])
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('draft-items.store');
|
||||
|
||||
Route::delete('draft-items/{rawMaterialPrice}', [PurchaseDraftItemController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('draft-items.destroy');
|
||||
|
||||
Route::post('/', [PurchaseController::class, 'store'])
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('store');
|
||||
@ -194,6 +204,18 @@
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('create');
|
||||
|
||||
Route::post('draft-items', [OrderDraftItemController::class, 'store'])
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('draft-items.store');
|
||||
|
||||
Route::put('draft-items/resync-prices', [OrderDraftItemController::class, 'resyncPrices'])
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('draft-items.resync-prices');
|
||||
|
||||
Route::delete('draft-items/{productVariant}', [OrderDraftItemController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('draft-items.destroy');
|
||||
|
||||
Route::post('/', [OrderController::class, 'store'])
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user