feat: Implement restock management, refactor warehouse views, and enhance stock activity logging for sales.
This commit is contained in:
parent
77d3d25cd9
commit
cc917ac946
@ -76,6 +76,11 @@ public function approveApproval(StockOpname $stockOpname): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$itemable = $item->itemable;
|
$itemable = $item->itemable;
|
||||||
|
|
||||||
|
if (! $itemable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$morphType = class_basename($itemable);
|
$morphType = class_basename($itemable);
|
||||||
|
|
||||||
$relationMap = [
|
$relationMap = [
|
||||||
@ -90,9 +95,11 @@ public function approveApproval(StockOpname $stockOpname): void
|
|||||||
|
|
||||||
$relationName = $relationMap[$morphType];
|
$relationName = $relationMap[$morphType];
|
||||||
|
|
||||||
$currentStock = $outlet->$relationName()
|
$currentPivot = $outlet->$relationName()
|
||||||
->where($itemable->getTable().'.id', $itemable->id)
|
->where($itemable->getTable().'.id', $itemable->id)
|
||||||
->first()?->pivot?->stock ?? 0;
|
->first();
|
||||||
|
|
||||||
|
$currentStock = $currentPivot?->pivot?->stock ?? 0;
|
||||||
|
|
||||||
$quantity = $item->qty_physical;
|
$quantity = $item->qty_physical;
|
||||||
|
|
||||||
@ -100,26 +107,13 @@ public function approveApproval(StockOpname $stockOpname): void
|
|||||||
$itemable->id => ['stock' => $quantity],
|
$itemable->id => ['stock' => $quantity],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
StockActivityLogService::log(
|
StockActivityLogService::stockOpname(
|
||||||
logName: 'Stock Opname',
|
stockOpname: $stockOpname,
|
||||||
event: 'Mengubah',
|
product: $itemable,
|
||||||
description: sprintf(
|
|
||||||
'Stok %s di %s diperbarui dari %s menjadi %s',
|
|
||||||
$itemable->name,
|
|
||||||
$outlet->name,
|
|
||||||
$currentStock,
|
|
||||||
$quantity
|
|
||||||
),
|
|
||||||
performedOn: $itemable,
|
|
||||||
outlet: $outlet,
|
|
||||||
quantityChange: $quantity - $currentStock,
|
quantityChange: $quantity - $currentStock,
|
||||||
previousStock: $currentStock,
|
previousStock: $currentStock,
|
||||||
newStock: $quantity,
|
newStock: $quantity,
|
||||||
extraProperties: [
|
note: $stockOpname->note
|
||||||
// Preserving 'itemable_id' pointing to StockOpnameItem id as per original code overwriting
|
|
||||||
'itemable_id' => $item->id ?? null,
|
|
||||||
'itemable_type' => $item->itemable_type,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Studio\Master;
|
|
||||||
|
|
||||||
use App\Livewire\Forms\Studio\Master\WarehouseForm;
|
|
||||||
use App\Models\Warehouse as WarehouseModel;
|
|
||||||
use App\Traits\Authorization\WithAuthorization;
|
|
||||||
use App\Traits\Components\WithCloseModal;
|
|
||||||
use App\Traits\Components\WithConfirmation;
|
|
||||||
use App\Traits\Components\WithToast;
|
|
||||||
use App\Traits\Notification\WithSubscribeNotification;
|
|
||||||
use App\Traits\Utilities\WithUpdatedData;
|
|
||||||
use Flux\Flux;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Livewire\Attributes\On;
|
|
||||||
use Livewire\Attributes\Title;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
#[Title('Gudang')]
|
|
||||||
class Warehouse extends Component
|
|
||||||
{
|
|
||||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
|
|
||||||
|
|
||||||
public WarehouseForm $form;
|
|
||||||
|
|
||||||
public string $method = 'create';
|
|
||||||
|
|
||||||
public string $modalTitle = '';
|
|
||||||
|
|
||||||
public function create(): void
|
|
||||||
{
|
|
||||||
$this->canOrAbort('create warehouse');
|
|
||||||
|
|
||||||
$this->form->store();
|
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
|
||||||
|
|
||||||
$this->toast('Gudang berhasil ditambahkan.');
|
|
||||||
|
|
||||||
Flux::modals()->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(): void
|
|
||||||
{
|
|
||||||
$this->canOrAbort('update warehouse');
|
|
||||||
|
|
||||||
$this->form->update();
|
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
|
||||||
|
|
||||||
$this->toast('Gudang berhasil diperbarui.');
|
|
||||||
|
|
||||||
Flux::modals()->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(WarehouseModel $warehouse): void
|
|
||||||
{
|
|
||||||
$this->canOrAbort('delete warehouse');
|
|
||||||
|
|
||||||
$this->form->warehouse = $warehouse;
|
|
||||||
|
|
||||||
$this->form->delete();
|
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
|
||||||
|
|
||||||
$this->toast('Gudang berhasil dihapus.');
|
|
||||||
|
|
||||||
Flux::modals()->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[On('modal:open')]
|
|
||||||
public function openModal(string $method, string $modalTitle, ?string $id = null): void
|
|
||||||
{
|
|
||||||
$this->resetValidation();
|
|
||||||
$this->resetErrorBag();
|
|
||||||
|
|
||||||
$this->method = $method;
|
|
||||||
$this->modalTitle = $modalTitle;
|
|
||||||
|
|
||||||
$id && $this->form->setWarehouse(WarehouseModel::byHashOrFail($id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
return view('livewire.studio.master.warehouse', [
|
|
||||||
'pageTitle' => 'Gudang',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -40,6 +40,11 @@ public function outlets(): BelongsToMany
|
|||||||
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function warehouses(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Warehouse::class)->withPivot('stock');
|
||||||
|
}
|
||||||
|
|
||||||
public function purchaseItems(): MorphMany
|
public function purchaseItems(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
||||||
@ -49,4 +54,9 @@ public function items(): MorphMany
|
|||||||
{
|
{
|
||||||
return $this->morphMany(OrderItem::class, 'orderable');
|
return $this->morphMany(OrderItem::class, 'orderable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restockItems(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(RestockItem::class, 'restockable');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,4 +99,9 @@ public function stockOpname(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(StockOpname::class);
|
return $this->hasMany(StockOpname::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restocks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Restock::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,11 @@ public function outlets(): BelongsToMany
|
|||||||
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function warehouses(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Warehouse::class)->withPivot('stock');
|
||||||
|
}
|
||||||
|
|
||||||
public function purchaseItems(): MorphMany
|
public function purchaseItems(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
||||||
@ -62,4 +67,9 @@ public function items(): MorphMany
|
|||||||
{
|
{
|
||||||
return $this->morphMany(OrderItem::class, 'orderable');
|
return $this->morphMany(OrderItem::class, 'orderable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restockItems(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(RestockItem::class, 'restockable');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,11 @@ public function outlets(): BelongsToMany
|
|||||||
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
return $this->belongsToMany(Outlet::class)->withPivot('stock');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function warehouses(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Warehouse::class)->withPivot('stock');
|
||||||
|
}
|
||||||
|
|
||||||
public function purchaseItems(): MorphMany
|
public function purchaseItems(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
return $this->morphMany(PurchaseItem::class, 'purchasable');
|
||||||
@ -49,4 +54,9 @@ public function items(): MorphMany
|
|||||||
{
|
{
|
||||||
return $this->morphMany(OrderItem::class, 'orderable');
|
return $this->morphMany(OrderItem::class, 'orderable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restockItems(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(RestockItem::class, 'restockable');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,167 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use App\Models\Purchase;
|
||||||
|
use App\Models\Restock;
|
||||||
|
use App\Models\StockOpname;
|
||||||
|
use App\Models\Warehouse;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class StockActivityLogService
|
class StockActivityLogService
|
||||||
{
|
{
|
||||||
|
public const LOG_PURCHASE = 'purchase';
|
||||||
|
|
||||||
|
public const LOG_DISTRIBUTION = 'distribution';
|
||||||
|
|
||||||
|
public const LOG_SALE = 'sale';
|
||||||
|
|
||||||
|
public const LOG_STOCK_OPNAME = 'stock_opname';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log stock related activity.
|
* Log purchase activity (Incoming to Warehouse).
|
||||||
*/
|
*/
|
||||||
public static function log(
|
public static function purchase(
|
||||||
|
Purchase $purchase,
|
||||||
|
Model $product,
|
||||||
|
int $quantity,
|
||||||
|
int $previousStock,
|
||||||
|
int $newStock,
|
||||||
|
?Model $causedBy = null
|
||||||
|
): void {
|
||||||
|
self::logWarehouse(
|
||||||
|
logName: self::LOG_PURCHASE,
|
||||||
|
event: 'increase',
|
||||||
|
description: "Purchase #{$purchase->hash_id}: Added {$quantity} stock to Warehouse.",
|
||||||
|
performedOn: $product,
|
||||||
|
warehouse: $purchase->warehouse,
|
||||||
|
quantityChange: $quantity,
|
||||||
|
previousStock: $previousStock,
|
||||||
|
newStock: $newStock,
|
||||||
|
extraProperties: [
|
||||||
|
'purchase_id' => $purchase->id,
|
||||||
|
'purchase_hash' => $purchase->hash_id,
|
||||||
|
],
|
||||||
|
causedBy: $causedBy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log distribution activity (Warehouse to Outlet).
|
||||||
|
* This logs two events: Out from Warehouse, In to Outlet.
|
||||||
|
*/
|
||||||
|
public static function distribution(
|
||||||
|
Restock $restock,
|
||||||
|
Model $product,
|
||||||
|
int $quantity,
|
||||||
|
int $warehousePreviousStock,
|
||||||
|
int $warehouseNewStock,
|
||||||
|
int $outletPreviousStock,
|
||||||
|
int $outletNewStock,
|
||||||
|
?Model $causedBy = null
|
||||||
|
): void {
|
||||||
|
// Log Warehouse Out
|
||||||
|
self::logWarehouse(
|
||||||
|
logName: self::LOG_DISTRIBUTION,
|
||||||
|
event: 'decrease',
|
||||||
|
description: "Restock #{$restock->hash_id}: Moved {$quantity} stock from Warehouse to Outlet.",
|
||||||
|
performedOn: $product,
|
||||||
|
warehouse: $restock->warehouse,
|
||||||
|
quantityChange: -1 * abs($quantity),
|
||||||
|
previousStock: $warehousePreviousStock,
|
||||||
|
newStock: $warehouseNewStock,
|
||||||
|
extraProperties: [
|
||||||
|
'restock_id' => $restock->id,
|
||||||
|
'restock_hash' => $restock->hash_id,
|
||||||
|
'target_outlet_id' => $restock->outlet_id,
|
||||||
|
],
|
||||||
|
causedBy: $causedBy
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log Outlet In
|
||||||
|
self::logOutlet(
|
||||||
|
logName: self::LOG_DISTRIBUTION,
|
||||||
|
event: 'increase',
|
||||||
|
description: "Restock #{$restock->hash_id}: Received {$quantity} stock from Warehouse.",
|
||||||
|
performedOn: $product,
|
||||||
|
outlet: $restock->outlet,
|
||||||
|
quantityChange: abs($quantity),
|
||||||
|
previousStock: $outletPreviousStock,
|
||||||
|
newStock: $outletNewStock,
|
||||||
|
extraProperties: [
|
||||||
|
'restock_id' => $restock->id,
|
||||||
|
'restock_hash' => $restock->hash_id,
|
||||||
|
'source_warehouse_id' => $restock->warehouse_id,
|
||||||
|
],
|
||||||
|
causedBy: $causedBy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log sale activity (Outlet to Customer).
|
||||||
|
*/
|
||||||
|
public static function sale(
|
||||||
|
Order $order,
|
||||||
|
Model $product,
|
||||||
|
int $quantity,
|
||||||
|
int $previousStock,
|
||||||
|
int $newStock,
|
||||||
|
?Model $causedBy = null
|
||||||
|
): void {
|
||||||
|
self::logOutlet(
|
||||||
|
logName: self::LOG_SALE,
|
||||||
|
event: 'decrease',
|
||||||
|
description: "Order #{$order->hash_id}: Sold {$quantity} items.",
|
||||||
|
performedOn: $product,
|
||||||
|
outlet: $order->outlet,
|
||||||
|
quantityChange: -1 * abs($quantity),
|
||||||
|
previousStock: $previousStock,
|
||||||
|
newStock: $newStock,
|
||||||
|
extraProperties: [
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_hash' => $order->hash_id,
|
||||||
|
],
|
||||||
|
causedBy: $causedBy ?? $order->user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log stock opname activity (Adjustment in Outlet).
|
||||||
|
*/
|
||||||
|
public static function stockOpname(
|
||||||
|
StockOpname $stockOpname,
|
||||||
|
Model $product,
|
||||||
|
int $quantityChange,
|
||||||
|
int $previousStock,
|
||||||
|
int $newStock,
|
||||||
|
?string $note = null,
|
||||||
|
?Model $causedBy = null
|
||||||
|
): void {
|
||||||
|
$event = $quantityChange > 0 ? 'increase' : 'decrease';
|
||||||
|
|
||||||
|
self::logOutlet(
|
||||||
|
logName: self::LOG_STOCK_OPNAME,
|
||||||
|
event: $event,
|
||||||
|
description: "Stock Opname #{$stockOpname->hash_id}: Adjusted stock by {$quantityChange}.",
|
||||||
|
performedOn: $product,
|
||||||
|
outlet: $stockOpname->outlet,
|
||||||
|
quantityChange: $quantityChange,
|
||||||
|
previousStock: $previousStock,
|
||||||
|
newStock: $newStock,
|
||||||
|
extraProperties: [
|
||||||
|
'stock_opname_id' => $stockOpname->id,
|
||||||
|
'stock_opname_hash' => $stockOpname->hash_id,
|
||||||
|
'note' => $note,
|
||||||
|
],
|
||||||
|
causedBy: $causedBy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log stock related activity for Outlet.
|
||||||
|
*/
|
||||||
|
public static function logOutlet(
|
||||||
string $logName,
|
string $logName,
|
||||||
string $event,
|
string $event,
|
||||||
string $description,
|
string $description,
|
||||||
@ -28,6 +181,42 @@ public static function log(
|
|||||||
'quantity_change' => $quantityChange,
|
'quantity_change' => $quantityChange,
|
||||||
'previous_stock' => $previousStock,
|
'previous_stock' => $previousStock,
|
||||||
'new_stock' => $newStock,
|
'new_stock' => $newStock,
|
||||||
|
'location_type' => 'outlet',
|
||||||
|
'location_name' => $outlet->name ?? 'Unknown Outlet',
|
||||||
|
], $extraProperties);
|
||||||
|
|
||||||
|
activity($logName)
|
||||||
|
->performedOn($performedOn)
|
||||||
|
->causedBy($causedBy)
|
||||||
|
->withProperties($properties)
|
||||||
|
->event($event)
|
||||||
|
->log($description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log stock related activity for Warehouse.
|
||||||
|
*/
|
||||||
|
public static function logWarehouse(
|
||||||
|
string $logName,
|
||||||
|
string $event,
|
||||||
|
string $description,
|
||||||
|
Model $performedOn,
|
||||||
|
Warehouse $warehouse,
|
||||||
|
int $quantityChange,
|
||||||
|
int $previousStock,
|
||||||
|
int $newStock,
|
||||||
|
array $extraProperties = [],
|
||||||
|
?Model $causedBy = null
|
||||||
|
): void {
|
||||||
|
$causedBy = $causedBy ?: auth()->user();
|
||||||
|
|
||||||
|
$properties = array_merge([
|
||||||
|
'warehouse_id' => $warehouse->getKey(),
|
||||||
|
'quantity_change' => $quantityChange,
|
||||||
|
'previous_stock' => $previousStock,
|
||||||
|
'new_stock' => $newStock,
|
||||||
|
'location_type' => 'warehouse',
|
||||||
|
'location_name' => $warehouse->name ?? 'Unknown Warehouse',
|
||||||
], $extraProperties);
|
], $extraProperties);
|
||||||
|
|
||||||
activity($logName)
|
activity($logName)
|
||||||
|
|||||||
@ -39,24 +39,26 @@ public function increaseOutletStock($outlet, $item): void
|
|||||||
'stock' => $currentStock + $quantity,
|
'stock' => $currentStock + $quantity,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
$currentStock = 0;
|
||||||
$outlet->{$relation}()->attach($item->orderable_id, [
|
$outlet->{$relation}()->attach($item->orderable_id, [
|
||||||
'stock' => $quantity,
|
'stock' => $quantity,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
$newStock = $currentStock + $quantity;
|
||||||
|
|
||||||
StockActivityLogService::log(
|
StockActivityLogService::logOutlet(
|
||||||
logName: 'Order',
|
logName: StockActivityLogService::LOG_SALE,
|
||||||
event: 'Menambah',
|
event: 'increase',
|
||||||
description: 'Stok '.$item->orderable->name.' di '.$outlet->name.' bertambah '.formatCurrencyNumber($quantity),
|
description: 'Stok '.$item->orderable->name.' di '.$outlet->name.' bertambah '.formatCurrencyNumber($quantity).' (Pembatalan/Edit Order)',
|
||||||
performedOn: $item->orderable,
|
performedOn: $item->orderable,
|
||||||
outlet: $outlet,
|
outlet: $outlet,
|
||||||
quantityChange: $quantity,
|
quantityChange: abs($quantity),
|
||||||
previousStock: $currentStock,
|
previousStock: $currentStock,
|
||||||
newStock: $currentStock + $quantity,
|
newStock: $newStock,
|
||||||
extraProperties: [
|
extraProperties: [
|
||||||
'orderable_id' => $item->orderable_id,
|
'orderable_id' => $item->orderable_id,
|
||||||
'orderable_type' => $item->orderable_type,
|
'orderable_type' => $item->orderable_type,
|
||||||
'purchase_id' => $item->purchase_id ?? null,
|
'order_id' => $item->order_id ?? null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -101,21 +103,33 @@ public function decreaseOutletStock($outlet, $item)
|
|||||||
'stock' => $newStock,
|
'stock' => $newStock,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
StockActivityLogService::log(
|
$order = $item->order ?? \App\Models\Order::find($item->order_id);
|
||||||
logName: 'Order',
|
|
||||||
event: 'Mengurangi',
|
if ($order) {
|
||||||
|
StockActivityLogService::sale(
|
||||||
|
order: $order,
|
||||||
|
product: $item->orderable,
|
||||||
|
quantity: $quantity,
|
||||||
|
previousStock: $currentStock,
|
||||||
|
newStock: $newStock
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
StockActivityLogService::logOutlet(
|
||||||
|
logName: StockActivityLogService::LOG_SALE,
|
||||||
|
event: 'decrease',
|
||||||
description: 'Stok '.$item->orderable->name.' di '.$outlet->name.' berkurang '.formatCurrencyNumber($quantity),
|
description: 'Stok '.$item->orderable->name.' di '.$outlet->name.' berkurang '.formatCurrencyNumber($quantity),
|
||||||
performedOn: $item->orderable,
|
performedOn: $item->orderable,
|
||||||
outlet: $outlet,
|
outlet: $outlet,
|
||||||
quantityChange: $quantity,
|
quantityChange: -1 * abs($quantity),
|
||||||
previousStock: $currentStock,
|
previousStock: $currentStock,
|
||||||
newStock: $currentStock - $quantity,
|
newStock: $newStock,
|
||||||
extraProperties: [
|
extraProperties: [
|
||||||
'orderable_id' => $item->orderable_id,
|
'orderable_id' => $item->orderable_id,
|
||||||
'orderable_type' => $item->orderable_type,
|
'orderable_type' => $item->orderable_type,
|
||||||
'purchase_id' => $item->purchase_id ?? null,
|
'order_id' => $item->order_id ?? null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ['status' => true];
|
return ['status' => true];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,11 @@ public function run(): void
|
|||||||
OutletSeeder::class,
|
OutletSeeder::class,
|
||||||
UserSeeder::class,
|
UserSeeder::class,
|
||||||
TierSeeder::class,
|
TierSeeder::class,
|
||||||
|
BrandSeeder::class,
|
||||||
|
CategorySeeder::class,
|
||||||
|
PerfumeSeeder::class,
|
||||||
|
ProductSeeder::class,
|
||||||
|
BottleSeeder::class,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$this->call([
|
$this->call([
|
||||||
|
|||||||
@ -226,6 +226,7 @@ public function run(): void
|
|||||||
'update outlet',
|
'update outlet',
|
||||||
'delete outlet',
|
'delete outlet',
|
||||||
|
|
||||||
|
'view user',
|
||||||
'create user',
|
'create user',
|
||||||
'update user',
|
'update user',
|
||||||
'delete user',
|
'delete user',
|
||||||
@ -267,7 +268,6 @@ public function run(): void
|
|||||||
'delete bottle',
|
'delete bottle',
|
||||||
'audit bottle',
|
'audit bottle',
|
||||||
|
|
||||||
'view payroll',
|
|
||||||
'manage adjustment',
|
'manage adjustment',
|
||||||
|
|
||||||
'delete order',
|
'delete order',
|
||||||
@ -379,7 +379,6 @@ public function run(): void
|
|||||||
'update bottle',
|
'update bottle',
|
||||||
'delete bottle',
|
'delete bottle',
|
||||||
|
|
||||||
'view payroll',
|
|
||||||
'manage adjustment',
|
'manage adjustment',
|
||||||
|
|
||||||
'delete order',
|
'delete order',
|
||||||
@ -388,6 +387,11 @@ public function run(): void
|
|||||||
'create purchase',
|
'create purchase',
|
||||||
'delete purchase',
|
'delete purchase',
|
||||||
|
|
||||||
|
'view restock',
|
||||||
|
'create restock',
|
||||||
|
'update restock',
|
||||||
|
'delete restock',
|
||||||
|
|
||||||
'view stock opname',
|
'view stock opname',
|
||||||
'manage stock opname',
|
'manage stock opname',
|
||||||
'show stock opname',
|
'show stock opname',
|
||||||
|
|||||||
213
resources/views/livewire/studio/manage/restock/form.blade.php
Normal file
213
resources/views/livewire/studio/manage/restock/form.blade.php
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<flux:main>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<flux:button href="{{ route('studio.manage.restock.index') }}" wire:navigate class="text-sm">
|
||||||
|
Kembali
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<flux:callout icon="megaphone" color="blue" class="mb-6">
|
||||||
|
<flux:callout.heading>Informasi Restock</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
Proses restock akan **mengurangi** stok di Gudang dan **menambah** stok di Outlet yang dipilih.
|
||||||
|
</flux:callout.text>
|
||||||
|
</flux:callout>
|
||||||
|
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
|
<div class="w-full lg:w-3/4 space-y-4">
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<flux:input label="Catatan" placeholder="Restock rutin mingguan" wire:model="form.note"
|
||||||
|
autocomplete="off" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>Tanggal Restock <span class="text-red-500 ms-1">*</span></flux:label>
|
||||||
|
<flux:date-picker with-today wire:model="form.restock_date" placeholder="Pilih Tanggal"
|
||||||
|
autocomplete="off" locale="id-ID" selectable-header />
|
||||||
|
<flux:error name="form.restock_date" />
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>Gudang <span class="text-red-500 ms-1">*</span></flux:label>
|
||||||
|
<flux:select variant="listbox" searchable placeholder="Pilih Gudang"
|
||||||
|
wire:model.live="form.warehouse_id">
|
||||||
|
@foreach ($warehouses as $key => $value)
|
||||||
|
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:error name="form.warehouse_id" />
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
||||||
|
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
|
||||||
|
wire:model.live="form.outlet_id">
|
||||||
|
@foreach ($outlets as $key => $value)
|
||||||
|
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:error name="form.outlet_id" />
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
|
||||||
|
@if ($form->warehouse_id && $form->outlet_id)
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<flux:select label="Parfum" variant="listbox" searchable placeholder="Cari parfum..."
|
||||||
|
wire:model="form.perfume_id">
|
||||||
|
@foreach ($perfumes as $key => $perfume)
|
||||||
|
<flux:select.option value="{{ $key }}">{{ $perfume }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:input label="Kuantitas" placeholder="500" wire:model="form.quantity_perfume"
|
||||||
|
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<flux:button variant="primary" color="zinc" wire:click="addItem('parfum')">
|
||||||
|
Tambah
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<flux:select label="Produk" variant="listbox" searchable placeholder="Cari produk..."
|
||||||
|
wire:model="form.product_id">
|
||||||
|
@foreach ($products as $key => $product)
|
||||||
|
<flux:select.option value="{{ $key }}">{{ $product }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:input label="Kuantitas" placeholder="10" wire:model="form.quantity_product"
|
||||||
|
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<flux:button variant="primary" color="zinc" wire:click="addItem('produk')">
|
||||||
|
Tambah
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<flux:select label="Botol" variant="listbox" searchable placeholder="Cari botol..."
|
||||||
|
wire:model="form.bottle_id">
|
||||||
|
@foreach ($bottles as $key => $bottle)
|
||||||
|
<flux:select.option value="{{ $key }}">{{ $bottle }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:input label="Kuantitas" placeholder="50" wire:model="form.quantity_bottle"
|
||||||
|
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<flux:button variant="primary" color="zinc" wire:click="addItem('botol')">
|
||||||
|
Tambah
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<flux:card class="p-6 text-center text-zinc-500 italic">
|
||||||
|
Silakan pilih Gudang Asal dan Outlet Tujuan terlebih dahulu untuk menampilkan daftar item.
|
||||||
|
</flux:card>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full lg:w-1/3 space-y-4">
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h3 class="text-sm font-medium">Gambar</h3>
|
||||||
|
<div class="dropzone-wrapper">
|
||||||
|
<livewire:dropzone wire:model="form.image" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1"
|
||||||
|
:key="'image'" :files="$form->image" />
|
||||||
|
@error('form.image')
|
||||||
|
<div class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
<flux:card class="space-y-6 p-6">
|
||||||
|
<h3 class="text-sm font-medium">Keranjang Restock</h3>
|
||||||
|
|
||||||
|
<div class="divide-y text-sm">
|
||||||
|
<flux:table>
|
||||||
|
<flux:table.columns>
|
||||||
|
<flux:table.column>Item</flux:table.column>
|
||||||
|
<flux:table.column>Qty</flux:table.column>
|
||||||
|
<flux:table.column>Aksi</flux:table.column>
|
||||||
|
</flux:table.columns>
|
||||||
|
|
||||||
|
<flux:table.rows>
|
||||||
|
@forelse($restockItems as $item)
|
||||||
|
<flux:table.row>
|
||||||
|
<flux:table.cell>
|
||||||
|
<flux:heading>
|
||||||
|
<div>{{ $item->restockable->name }}</div>
|
||||||
|
</flux:heading>
|
||||||
|
</flux:table.cell>
|
||||||
|
<flux:table.cell>
|
||||||
|
{{ formatCurrencyNumber($item->quantity, '') }}
|
||||||
|
</flux:table.cell>
|
||||||
|
<flux:table.cell class="space-x-2">
|
||||||
|
<flux:modal.trigger name="form-modal">
|
||||||
|
<flux:tooltip content="Ubah">
|
||||||
|
<flux:button variant="primary" color="yellow" icon="pencil-square"
|
||||||
|
size="sm"
|
||||||
|
wire:click="$dispatch('modal:open', {'item': '{{ $item->id }}'})">
|
||||||
|
</flux:button>
|
||||||
|
</flux:tooltip>
|
||||||
|
</flux:modal.trigger>
|
||||||
|
|
||||||
|
<flux:button variant="danger" icon="trash" size="sm"
|
||||||
|
wire:click="deleteItem('{{ $item->id }}')">
|
||||||
|
</flux:button>
|
||||||
|
</flux:table.cell>
|
||||||
|
</flux:table.row>
|
||||||
|
@empty
|
||||||
|
<flux:table.row>
|
||||||
|
<flux:table.cell colspan="3" class="text-center">
|
||||||
|
<span class="text-sm text-zinc-500 italic">Keranjang Kosong...</span>
|
||||||
|
</flux:table.cell>
|
||||||
|
</flux:table.row>
|
||||||
|
@endforelse
|
||||||
|
</flux:table.rows>
|
||||||
|
</flux:table>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start mt-4">
|
||||||
|
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
|
||||||
|
Simpan
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:modal name="form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
|
||||||
|
<div class="p-4 space-y-6">
|
||||||
|
<flux:heading size="lg">Ubah {{ $modalTitle }}</flux:heading>
|
||||||
|
|
||||||
|
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas" wire:model="form.quantity_edit"
|
||||||
|
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<flux:spacer />
|
||||||
|
<flux:button variant="primary" color="zinc" class="sm:w-auto cursor-pointer"
|
||||||
|
wire:click="updateItem">
|
||||||
|
Perbarui
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
|
</flux:main>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<flux:main>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||||
|
</div>
|
||||||
|
@can('create restock')
|
||||||
|
<div>
|
||||||
|
<flux:button href="{{ route('studio.manage.restock.create') }}" variant="primary" wire:navigate
|
||||||
|
class="text-sm">
|
||||||
|
Tambah
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<flux:callout icon="megaphone" color="blue" class="mb-6">
|
||||||
|
<flux:callout.heading>Informasi</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
Menghapus data restock otomatis mengembalikan stok gudang.
|
||||||
|
</flux:callout.text>
|
||||||
|
</flux:callout>
|
||||||
|
|
||||||
|
<livewire:datatable.manage.restocks-table />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('components.modals.confirmation', [
|
||||||
|
'modalName' => 'delete-confirmation',
|
||||||
|
'modalTitle' => 'Apakah Anda yakin?',
|
||||||
|
'modalMessage' => 'Data restock akan dihapus dari riwayat.',
|
||||||
|
'buttonVariant' => 'primary',
|
||||||
|
'buttonColor' => 'danger',
|
||||||
|
'buttonText' => 'Ya, Hapus',
|
||||||
|
])
|
||||||
|
</flux:main>
|
||||||
@ -22,7 +22,6 @@
|
|||||||
use App\Livewire\Studio\Finance\Payroll as PayrollComponent;
|
use App\Livewire\Studio\Finance\Payroll as PayrollComponent;
|
||||||
use App\Livewire\Studio\Information\Broadcast as BroadcastComponent;
|
use App\Livewire\Studio\Information\Broadcast as BroadcastComponent;
|
||||||
use App\Livewire\Studio\Information\Faq as FaqComponent;
|
use App\Livewire\Studio\Information\Faq as FaqComponent;
|
||||||
use App\Livewire\Studio\Information\PriceRequest as PriceRequestComponent;
|
|
||||||
use App\Livewire\Studio\Information\Testimonial as TestimonialComponent;
|
use App\Livewire\Studio\Information\Testimonial as TestimonialComponent;
|
||||||
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
|
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
|
||||||
use App\Livewire\Studio\Loyalty\RedemptionVerification;
|
use App\Livewire\Studio\Loyalty\RedemptionVerification;
|
||||||
@ -39,6 +38,8 @@
|
|||||||
use App\Livewire\Studio\Manage\Order\Show as OrderShow;
|
use App\Livewire\Studio\Manage\Order\Show as OrderShow;
|
||||||
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
|
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
|
||||||
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex;
|
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex;
|
||||||
|
use App\Livewire\Studio\Manage\Restock\Create as RestockCreate;
|
||||||
|
use App\Livewire\Studio\Manage\Restock\Index as RestockIndex;
|
||||||
use App\Livewire\Studio\Manage\StockOpname\Index as StockOpnameIndex;
|
use App\Livewire\Studio\Manage\StockOpname\Index as StockOpnameIndex;
|
||||||
use App\Livewire\Studio\Manage\StockOpname\Manage as StockOpnameManage;
|
use App\Livewire\Studio\Manage\StockOpname\Manage as StockOpnameManage;
|
||||||
use App\Livewire\Studio\Manage\StockOpname\Show as StockOpnameShow;
|
use App\Livewire\Studio\Manage\StockOpname\Show as StockOpnameShow;
|
||||||
@ -50,7 +51,8 @@
|
|||||||
use App\Livewire\Studio\Master\User\Create as UserCreate;
|
use App\Livewire\Studio\Master\User\Create as UserCreate;
|
||||||
use App\Livewire\Studio\Master\User\Edit as UserEdit;
|
use App\Livewire\Studio\Master\User\Edit as UserEdit;
|
||||||
use App\Livewire\Studio\Master\User\Index as UserIndex;
|
use App\Livewire\Studio\Master\User\Index as UserIndex;
|
||||||
use App\Livewire\Studio\Master\Warehouse as WarehouseComponent;
|
use App\Livewire\Studio\Master\Warehouse\Index as WarehouseIndex;
|
||||||
|
use App\Livewire\Studio\Master\Warehouse\Show as WarehouseShow;
|
||||||
use App\Livewire\Studio\Setting\Account as AccountComponent;
|
use App\Livewire\Studio\Setting\Account as AccountComponent;
|
||||||
use App\Livewire\Studio\Setting\Application as ApplicationComponent;
|
use App\Livewire\Studio\Setting\Application as ApplicationComponent;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@ -73,8 +75,9 @@
|
|||||||
Route::get('/{outlet}/edit', OutletEdit::class)->name('edit')->middleware('can:update outlet');
|
Route::get('/{outlet}/edit', OutletEdit::class)->name('edit')->middleware('can:update outlet');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('warehouse')->name('warehouse.')->group(function () {
|
Route::prefix('warehouses')->name('warehouse.')->group(function () {
|
||||||
Route::get('/', WarehouseComponent::class)->name('index')->middleware('can:view warehouse');
|
Route::get('/', WarehouseIndex::class)->name('index')->middleware('can:view warehouse');
|
||||||
|
Route::get('/{warehouse}/show', WarehouseShow::class)->name('show')->middleware('can:view warehouse');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('users')->name('user.')->group(function () {
|
Route::prefix('users')->name('user.')->group(function () {
|
||||||
@ -165,6 +168,11 @@
|
|||||||
Route::get('/create', PurchaseCreate::class)->name('create')->middleware('can:create purchase');
|
Route::get('/create', PurchaseCreate::class)->name('create')->middleware('can:create purchase');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::prefix('restocks')->name('restock.')->group(function () {
|
||||||
|
Route::get('/', RestockIndex::class)->name('index')->middleware('can:view restock');
|
||||||
|
Route::get('/create', RestockCreate::class)->name('create')->middleware('can:create restock');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('stock-opname')->name('stock_opname.')->group(function () {
|
Route::prefix('stock-opname')->name('stock_opname.')->group(function () {
|
||||||
Route::get('/', StockOpnameIndex::class)->name('index')->middleware('can:view stock opname');
|
Route::get('/', StockOpnameIndex::class)->name('index')->middleware('can:view stock opname');
|
||||||
Route::get('/{stockOpname}/show', StockOpnameShow::class)->name('show')->middleware('can:show stock opname');
|
Route::get('/{stockOpname}/show', StockOpnameShow::class)->name('show')->middleware('can:show stock opname');
|
||||||
@ -181,12 +189,6 @@
|
|||||||
Route::prefix('information')->name('studio.information.')->group(function () {
|
Route::prefix('information')->name('studio.information.')->group(function () {
|
||||||
Route::get('/broadcasts', BroadcastComponent::class)->name('broadcast.index')->middleware('can:view broadcast');
|
Route::get('/broadcasts', BroadcastComponent::class)->name('broadcast.index')->middleware('can:view broadcast');
|
||||||
|
|
||||||
Route::prefix('price-requests')->name('price_request.')->group(function () {
|
|
||||||
Route::get('/', PriceRequestComponent::class)->name('index')->middleware('can:view price request');
|
|
||||||
Route::patch('/{priceRequest}/approve', PriceRequestComponent::class)->name('approve')->middleware('can:approve price request');
|
|
||||||
Route::patch('/{priceRequest}/reject', PriceRequestComponent::class)->name('reject')->middleware('can:reject price request');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::prefix('faqs')->name('faq.')->group(function () {
|
Route::prefix('faqs')->name('faq.')->group(function () {
|
||||||
Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq');
|
Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user