feat: implement retail stock transfer functionality with new controller, request validation, and service logic, while updating related models and UI components
This commit is contained in:
parent
aeb7d1f292
commit
8f66502f25
@ -226,7 +226,7 @@ ### Controller
|
||||
PurchaseDraftItemController.php
|
||||
Stock/
|
||||
StockController.php
|
||||
StockRetailController.php
|
||||
RetailStockController.php
|
||||
StokOpnameController.php ← modul dengan 1 controller tetap di level parent
|
||||
OwnerVerificationController.php
|
||||
```
|
||||
|
||||
@ -13,7 +13,7 @@ enum OwnerVerificationAction: string
|
||||
case DELETE = 'delete';
|
||||
case TOGGLE_STATUS = 'toggle_status';
|
||||
case STOCK_VERIFY = 'stock_verify';
|
||||
case STOCK_RETAIL_TRANSFER = 'stock_retail_transfer';
|
||||
case RETAIL_STOCK_TRANSFER = 'retail_stock_transfer';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@ -23,7 +23,7 @@ public function label(): string
|
||||
self::DELETE => 'Hapus',
|
||||
self::TOGGLE_STATUS => 'Ubah Status',
|
||||
self::STOCK_VERIFY => 'Verifikasi Stok',
|
||||
self::STOCK_RETAIL_TRANSFER => 'Transfer Stok Ecer',
|
||||
self::RETAIL_STOCK_TRANSFER => 'Transfer Stok Ecer',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,21 +3,21 @@
|
||||
namespace App\Http\Controllers\Admin\Manage\Stock;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\StockRetailTransferRequest;
|
||||
use App\Services\Manage\StockRetailService;
|
||||
use App\Http\Requests\Admin\Manage\RetailStockTransferRequest;
|
||||
use App\Services\Manage\RetailStockService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class StockRetailController extends Controller
|
||||
class RetailStockController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StockRetailService $stockRetailService,
|
||||
private readonly RetailStockService $retailStockService,
|
||||
) {}
|
||||
|
||||
public function transfer(StockRetailTransferRequest $request): JsonResponse
|
||||
public function transfer(RetailStockTransferRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$this->stockRetailService->transfer(
|
||||
$this->retailStockService->transfer(
|
||||
$validated['product_variant_id'],
|
||||
$validated['quantity'],
|
||||
$request->user(),
|
||||
@ -6,7 +6,7 @@
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StockRetailTransferRequest extends FormRequest
|
||||
class RetailStockTransferRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
@ -42,7 +42,7 @@ public function rules(): array
|
||||
],
|
||||
'variants.*.name' => ['required', 'string', 'max:200'],
|
||||
'variants.*.stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.stock_retail' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
|
||||
...$this->variantImageRules(),
|
||||
];
|
||||
}
|
||||
@ -60,7 +60,7 @@ public function attributes(): array
|
||||
'variants' => 'Varian',
|
||||
'variants.*.name' => 'Nama Varian',
|
||||
'variants.*.stock' => 'Stok',
|
||||
'variants.*.stock_retail' => 'Stok Ecer',
|
||||
'variants.*.retail_stock' => 'Stok Ecer',
|
||||
...$this->variantImageAttributes('variants', 'Foto Varian'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ public function totalRejectStockFormatted(): Attribute
|
||||
public function totalRetailStockFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => number_format($this->variants->sum('stock_retail'), 0, ',', '.'),
|
||||
get: fn () => number_format($this->variants->sum('retail_stock'), 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['stock_formatted', 'reject_stock_formatted', 'stock_retail_formatted'])]
|
||||
#[Appends(['stock_formatted', 'reject_stock_formatted', 'retail_stock_formatted'])]
|
||||
class ProductVariant extends Model implements HasMedia
|
||||
{
|
||||
// 1. Use Trait
|
||||
@ -29,7 +29,7 @@ protected function casts(): array
|
||||
return [
|
||||
'reject_stock' => 'integer',
|
||||
'stock' => 'integer',
|
||||
'stock_retail' => 'integer',
|
||||
'retail_stock' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -48,10 +48,10 @@ public function stockFormatted(): Attribute
|
||||
);
|
||||
}
|
||||
|
||||
public function stockRetailFormatted(): Attribute
|
||||
public function retailStockFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => number_format($this->stock_retail, 0, ',', '.'),
|
||||
get: fn () => number_format($this->retail_stock, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -50,8 +50,10 @@ public function stockFormatted(): Attribute
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$formatted = rtrim(rtrim(number_format((float) $this->stock, 4, ',', '.'), '0'), ',');
|
||||
$unitAbbreviation = $this->attributes['unit_abbreviation']
|
||||
?? $this->rawMaterial?->unit?->abbreviation();
|
||||
|
||||
return "{$formatted} {$this->rawMaterial->unit->abbreviation()}";
|
||||
return "{$formatted} {$unitAbbreviation}";
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class StockRetailHistory extends Model
|
||||
class RetailStockHistory extends Model
|
||||
{
|
||||
protected $table = 'stock_retail_histories';
|
||||
protected $table = 'retail_stock_histories';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
@ -21,8 +21,8 @@ protected function casts(): array
|
||||
'quantity' => 'integer',
|
||||
'stock_after' => 'integer',
|
||||
'stock_before' => 'integer',
|
||||
'stock_retail_after' => 'integer',
|
||||
'stock_retail_before' => 'integer',
|
||||
'retail_stock_after' => 'integer',
|
||||
'retail_stock_before' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
|
||||
->with([
|
||||
'prices' => fn ($query) => $query
|
||||
->orderBy('created_at')
|
||||
->with(['rawMaterial:id,unit', 'media']),
|
||||
->with('media'),
|
||||
])
|
||||
->where(function (Builder $query) use ($selectedPriceIds): void {
|
||||
$query->active();
|
||||
@ -132,11 +132,10 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->each(function (RawMaterial $rawMaterial): void {
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
|
||||
$price->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($price, 'images'),
|
||||
);
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
|
||||
$price->unsetRelation('rawMaterial');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ public function stockColumn(ProductStockQuality $quality): string
|
||||
return match ($quality) {
|
||||
ProductStockQuality::GOOD => 'stock',
|
||||
ProductStockQuality::REJECT => 'reject_stock',
|
||||
ProductStockQuality::RETAIL => 'stock_retail',
|
||||
ProductStockQuality::RETAIL => 'retail_stock',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ public function __construct(
|
||||
private readonly PurchaseService $purchaseService,
|
||||
private readonly PushNotificationService $pushNotificationService,
|
||||
private readonly MarketplaceService $marketplaceService,
|
||||
private readonly StockRetailService $stockRetailService,
|
||||
private readonly RetailStockService $retailStockService,
|
||||
) {}
|
||||
|
||||
public function hasPendingMarketplaceVerification(): bool
|
||||
@ -283,7 +283,7 @@ private function rejectVerificationRequest(OwnerVerificationRequest $request): v
|
||||
RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request),
|
||||
Purchase::class => $this->purchaseService->rejectVerificationRequest($request),
|
||||
MarketplaceSettings::class => null,
|
||||
ProductVariant::class => $this->stockRetailService->rejectStockRetailTransfer($request),
|
||||
ProductVariant::class => $this->retailStockService->rejectRetailStockTransfer($request),
|
||||
default => throw ValidationException::withMessages([
|
||||
'subject_type' => 'Tipe data verifikasi tidak didukung.',
|
||||
]),
|
||||
@ -297,7 +297,7 @@ private function applyVerificationRequest(OwnerVerificationRequest $request): vo
|
||||
RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request),
|
||||
Purchase::class => $this->purchaseService->applyVerificationRequest($request),
|
||||
MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request),
|
||||
ProductVariant::class => $this->stockRetailService->applyStockRetailTransfer($request),
|
||||
ProductVariant::class => $this->retailStockService->applyRetailStockTransfer($request),
|
||||
default => throw ValidationException::withMessages([
|
||||
'subject_type' => 'Tipe data verifikasi tidak didukung.',
|
||||
]),
|
||||
|
||||
@ -97,7 +97,7 @@ public function catalogItems(?Purchase $purchase = null, ?User $user = null): Co
|
||||
->with([
|
||||
'prices' => fn ($query) => $query
|
||||
->orderBy('created_at')
|
||||
->with(['rawMaterial:id,unit', 'media']),
|
||||
->with('media'),
|
||||
])
|
||||
->where(function (Builder $query) use ($purchasePriceIds): void {
|
||||
$query->active();
|
||||
@ -112,11 +112,10 @@ public function catalogItems(?Purchase $purchase = null, ?User $user = null): Co
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->each(function (RawMaterial $rawMaterial): void {
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
|
||||
$price->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($price, 'images'),
|
||||
);
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
|
||||
$price->unsetRelation('rawMaterial');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -7,14 +7,14 @@
|
||||
use App\Enums\Permission;
|
||||
use App\Models\OwnerVerificationRequest;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StockRetailHistory;
|
||||
use App\Models\RetailStockHistory;
|
||||
use App\Models\User;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class StockRetailService
|
||||
class RetailStockService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PushNotificationService $pushNotificationService,
|
||||
@ -47,7 +47,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
$existingPending = OwnerVerificationRequest::query()
|
||||
->where('subject_type', ProductVariant::class)
|
||||
->where('subject_id', $variant->id)
|
||||
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
|
||||
->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
|
||||
->where('status', OwnerVerificationStatus::PENDING)
|
||||
->exists();
|
||||
|
||||
@ -59,7 +59,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
|
||||
try {
|
||||
$request = OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::STOCK_RETAIL_TRANSFER,
|
||||
'action' => OwnerVerificationAction::RETAIL_STOCK_TRANSFER,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => ProductVariant::class,
|
||||
'subject_id' => $variant->id,
|
||||
@ -67,12 +67,12 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'stock' => $variant->stock,
|
||||
'stock_retail' => $variant->stock_retail,
|
||||
'retail_stock' => $variant->retail_stock,
|
||||
],
|
||||
'new' => [
|
||||
'quantity' => $quantity,
|
||||
'stock' => $variant->stock - $quantity,
|
||||
'stock_retail' => $variant->stock_retail + $quantity,
|
||||
'retail_stock' => $variant->retail_stock + $quantity,
|
||||
'notes' => $notes,
|
||||
'variant_name' => $variant->name,
|
||||
'product_name' => $variant->product?->name ?? '-',
|
||||
@ -110,7 +110,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
}
|
||||
}
|
||||
|
||||
public function applyStockRetailTransfer(OwnerVerificationRequest $request): void
|
||||
public function applyRetailStockTransfer(OwnerVerificationRequest $request): void
|
||||
{
|
||||
$payload = $request->payload ?? [];
|
||||
$newData = $payload['new'] ?? [];
|
||||
@ -138,26 +138,26 @@ private function executeTransfer(ProductVariant $variant, int $quantity, User $u
|
||||
->firstOrFail();
|
||||
|
||||
$stockBefore = $variant->stock;
|
||||
$stockRetailBefore = $variant->stock_retail;
|
||||
$retailStockBefore = $variant->retail_stock;
|
||||
|
||||
$variant->decrement('stock', $quantity);
|
||||
$variant->increment('stock_retail', $quantity);
|
||||
$variant->increment('retail_stock', $quantity);
|
||||
|
||||
StockRetailHistory::create([
|
||||
RetailStockHistory::create([
|
||||
'product_variant_id' => $variant->id,
|
||||
'user_id' => $user->id,
|
||||
'quantity' => $quantity,
|
||||
'stock_before' => $stockBefore,
|
||||
'stock_retail_before' => $stockRetailBefore,
|
||||
'retail_stock_before' => $retailStockBefore,
|
||||
'stock_after' => $stockBefore - $quantity,
|
||||
'stock_retail_after' => $stockRetailBefore + $quantity,
|
||||
'retail_stock_after' => $retailStockBefore + $quantity,
|
||||
'notes' => $notes,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function rejectStockRetailTransfer(OwnerVerificationRequest $request): void
|
||||
public function rejectRetailStockTransfer(OwnerVerificationRequest $request): void
|
||||
{
|
||||
// No-op: nothing was changed yet, so nothing to rollback.
|
||||
}
|
||||
@ -74,12 +74,12 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
||||
|
||||
$pendingRequest = $product->pendingOwnerVerificationRequest;
|
||||
|
||||
// Also check for pending stock retail transfer on variants
|
||||
// Also check for pending retail stock transfer on variants
|
||||
if ($pendingRequest === null) {
|
||||
$pendingRequest = OwnerVerificationRequest::query()
|
||||
->where('subject_type', ProductVariant::class)
|
||||
->whereIn('subject_id', $product->variants->pluck('id'))
|
||||
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
|
||||
->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
|
||||
->pending()
|
||||
->latest()
|
||||
->first();
|
||||
@ -132,7 +132,7 @@ public function create(array $validated, User $user): void
|
||||
$variant = $product->variants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
|
||||
$this->syncVariantImages($variant, $variantData, $index);
|
||||
@ -195,7 +195,7 @@ public function update(Product $product, array $validated, User $user): void
|
||||
$variant = $product->variants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
$this->syncVariantImages($variant, $variantData, $index);
|
||||
}
|
||||
@ -436,7 +436,7 @@ private function applyPayloadToProduct(
|
||||
$variant->update([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
|
||||
if ($verificationRequest !== null) {
|
||||
@ -449,7 +449,7 @@ private function applyPayloadToProduct(
|
||||
$variant = $product->variants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
|
||||
if ($verificationRequest !== null) {
|
||||
@ -595,7 +595,7 @@ private function snapshotProduct(Product $product): array
|
||||
'id' => $variant->id,
|
||||
'name' => $variant->name,
|
||||
'stock' => $variant->stock,
|
||||
'stock_retail' => $variant->stock_retail,
|
||||
'retail_stock' => $variant->retail_stock,
|
||||
])
|
||||
->all(),
|
||||
]);
|
||||
@ -623,7 +623,7 @@ private function buildPayloadFromValidated(array $validated): array
|
||||
'id' => $variantData['id'] ?? null,
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
|
||||
])
|
||||
->all(),
|
||||
|
||||
@ -35,7 +35,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
||||
'pendingOwnerVerificationRequest',
|
||||
'prices' => fn ($query) => $query
|
||||
->orderBy('created_at')
|
||||
->with(['rawMaterial:id,unit', 'media']),
|
||||
->with('media'),
|
||||
])
|
||||
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
||||
$search = $tableQuery['search'];
|
||||
@ -73,11 +73,10 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
||||
->paginate(25)
|
||||
->withQueryString()
|
||||
->through(function (RawMaterial $rawMaterial) {
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
|
||||
$price->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($price, 'images'),
|
||||
);
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
|
||||
$price->unsetRelation('rawMaterial');
|
||||
});
|
||||
|
||||
$pendingRequest = $rawMaterial->pendingOwnerVerificationRequest;
|
||||
@ -101,8 +100,10 @@ public function findForEdit(RawMaterial $rawMaterial): RawMaterial
|
||||
'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
|
||||
]);
|
||||
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
|
||||
$price->unsetRelation('rawMaterial');
|
||||
});
|
||||
|
||||
return $rawMaterial;
|
||||
|
||||
@ -499,7 +499,7 @@ public function getProductStock(): array
|
||||
->selectRaw('
|
||||
SUM(product_variants.stock) as total_stock,
|
||||
SUM(product_variants.reject_stock) as total_reject,
|
||||
SUM(product_variants.stock_retail) as total_retail,
|
||||
SUM(product_variants.retail_stock) as total_retail,
|
||||
COUNT(product_variants.id) as total_variants,
|
||||
COUNT(DISTINCT products.id) as total_products
|
||||
')
|
||||
|
||||
@ -77,7 +77,7 @@ private static function label(string $field): string
|
||||
'prices' => 'Varian Harga',
|
||||
'quantity' => 'Jumlah Transfer',
|
||||
'stock' => 'Stok Bagus',
|
||||
'stock_retail' => 'Stok Ecer',
|
||||
'retail_stock' => 'Stok Ecer',
|
||||
'variant_name' => 'Varian',
|
||||
'product_name' => 'Produk',
|
||||
'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform',
|
||||
@ -186,7 +186,7 @@ private static function presentValue(string $field, mixed $value): mixed
|
||||
return implode(', ', $value);
|
||||
}
|
||||
|
||||
if (in_array($field, ['quantity', 'stock', 'stock_retail'], true)) {
|
||||
if (in_array($field, ['quantity', 'stock', 'retail_stock'], true)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ public function up(): void
|
||||
$table->string('name', 200);
|
||||
$table->unsignedInteger('stock')->default(0);
|
||||
$table->unsignedInteger('reject_stock')->default(0);
|
||||
$table->unsignedInteger('stock_retail')->default(0);
|
||||
$table->unsignedInteger('retail_stock')->default(0);
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stock_retail_histories', function (Blueprint $table) {
|
||||
Schema::create('retail_stock_histories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete();
|
||||
@ -16,9 +16,9 @@ public function up(): void
|
||||
|
||||
$table->unsignedInteger('quantity');
|
||||
$table->unsignedInteger('stock_before');
|
||||
$table->unsignedInteger('stock_retail_before');
|
||||
$table->unsignedInteger('retail_stock_before');
|
||||
$table->unsignedInteger('stock_after');
|
||||
$table->unsignedInteger('stock_retail_after');
|
||||
$table->unsignedInteger('retail_stock_after');
|
||||
$table->string('notes')->nullable();
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
@ -27,6 +27,6 @@ public function up(): void
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_retail_histories');
|
||||
Schema::dropIfExists('retail_stock_histories');
|
||||
}
|
||||
};
|
||||
33
resources/js/components/button/RowTransferAction.vue
Normal file
33
resources/js/components/button/RowTransferAction.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftRight } from '@lucide/vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
defineProps<{
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
tooltipClass?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-8"
|
||||
:disabled="disabled"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<ArrowLeftRight class="size-4" />
|
||||
<span class="sr-only">{{ tooltip || 'Transfer Stok Ecer' }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :class="tooltipClass">{{ tooltip || 'Transfer Stok Ecer' }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
@ -7,5 +7,6 @@ export { default as RowAdjustAction } from './RowAdjustAction.vue';
|
||||
export { default as RowStatusAction } from './RowStatusAction.vue';
|
||||
export { default as RowPrintAction } from './RowPrintAction.vue';
|
||||
export { default as RowDetailAction } from './RowDetailAction.vue';
|
||||
export { default as RowTransferAction } from './RowTransferAction.vue';
|
||||
export { default as CreateButton } from './CreateButton.vue';
|
||||
export { default as BackButton } from './BackButton.vue';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftRight, Save } from '@lucide/vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { NumberInput } from '@/components/form/number-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -18,9 +18,24 @@ import {
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { transfer } from '@/routes/admin/manage/stock-retail';
|
||||
import { transfer } from '@/routes/admin/manage/retail-stock';
|
||||
|
||||
interface VariantOption {
|
||||
id: number;
|
||||
name: string;
|
||||
stock: number;
|
||||
retail_stock: number;
|
||||
}
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
@ -28,8 +43,9 @@ const props = defineProps<{
|
||||
variantId: number;
|
||||
variantName: string;
|
||||
productName: string;
|
||||
stockBagus: number;
|
||||
stockRetail: number;
|
||||
goodStock: number;
|
||||
retailStock: number;
|
||||
variants?: VariantOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -39,6 +55,22 @@ const emit = defineEmits<{
|
||||
const processing = ref(false);
|
||||
const errors = reactive<Record<string, string>>({});
|
||||
|
||||
const selectedVariantId = ref(0);
|
||||
|
||||
const hasMultipleVariants = computed(() => (props.variants?.length ?? 0) > 1);
|
||||
|
||||
const activeVariant = computed(() => {
|
||||
if (hasMultipleVariants.value && selectedVariantId.value > 0) {
|
||||
return props.variants?.find((v) => v.id === selectedVariantId.value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const displayVariantName = computed(() => activeVariant.value?.name ?? props.variantName);
|
||||
const displayGoodStock = computed(() => activeVariant.value?.stock ?? props.goodStock);
|
||||
const displayRetailStock = computed(() => activeVariant.value?.retail_stock ?? props.retailStock);
|
||||
|
||||
const form = reactive({
|
||||
quantity: '',
|
||||
notes: '',
|
||||
@ -48,12 +80,28 @@ function resetForm() {
|
||||
form.quantity = '';
|
||||
form.notes = '';
|
||||
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||
|
||||
if (hasMultipleVariants.value) {
|
||||
selectedVariantId.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
resetForm();
|
||||
|
||||
if (!hasMultipleVariants.value && props.variantId > 0) {
|
||||
selectedVariantId.value = props.variantId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const submitVariantId = computed(() => {
|
||||
if (hasMultipleVariants.value) {
|
||||
return selectedVariantId.value;
|
||||
}
|
||||
|
||||
return props.variantId;
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
@ -64,7 +112,7 @@ async function submit() {
|
||||
await apiFetch(transfer.url(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
product_variant_id: props.variantId,
|
||||
product_variant_id: submitVariantId.value,
|
||||
quantity: Number(form.quantity),
|
||||
notes: form.notes || null,
|
||||
}),
|
||||
@ -108,14 +156,35 @@ function formatNumber(value: number): string {
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border p-3 space-y-1.5">
|
||||
<p class="text-sm font-medium">{{ productName }}</p>
|
||||
<p class="text-xs text-muted-foreground">Varian: {{ variantName }}</p>
|
||||
|
||||
<Field v-if="hasMultipleVariants">
|
||||
<FieldLabel for="transfer-variant" required>Pilih Varian</FieldLabel>
|
||||
<Select :model-value="selectedVariantId ? String(selectedVariantId) : undefined" @update:model-value="selectedVariantId = Number($event)">
|
||||
<SelectTrigger id="transfer-variant" class="w-full">
|
||||
<SelectValue placeholder="Pilih varian" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="variant in variants"
|
||||
:key="variant.id"
|
||||
:value="String(variant.id)"
|
||||
>
|
||||
{{ variant.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<p v-if="!hasMultipleVariants" class="text-xs text-muted-foreground">Varian: {{ displayVariantName }}</p>
|
||||
<div class="flex justify-between text-xs pt-1">
|
||||
<span class="text-muted-foreground">Stok Bagus</span>
|
||||
<span class="font-medium tabular-nums">{{ formatNumber(stockBagus) }}</span>
|
||||
<span class="font-medium tabular-nums">{{ formatNumber(displayGoodStock) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-muted-foreground">Stok Ecer</span>
|
||||
<span class="font-medium tabular-nums text-blue-600">{{ formatNumber(stockRetail) }}</span>
|
||||
<span class="font-medium tabular-nums text-blue-600">{{ formatNumber(displayRetailStock) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -131,7 +200,7 @@ function formatNumber(value: number): string {
|
||||
<NumberInput
|
||||
id="retail-quantity"
|
||||
v-model="form.quantity"
|
||||
:max="stockBagus"
|
||||
:max="displayGoodStock"
|
||||
placeholder="Masukkan jumlah"
|
||||
/>
|
||||
<FieldError
|
||||
@ -161,7 +230,7 @@ function formatNumber(value: number): string {
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" :disabled="processing || !form.quantity || Number(form.quantity) < 1">
|
||||
<Button type="submit" :disabled="processing || !form.quantity || Number(form.quantity) < 1 || (hasMultipleVariants && !selectedVariantId)">
|
||||
<Save class="size-4" />
|
||||
{{ processing ? 'Mengirim...' : 'Ajukan Transfer' }}
|
||||
</Button>
|
||||
@ -110,7 +110,7 @@ const emit = defineEmits<{
|
||||
{{ variant.name }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
<span v-if="isCashierUser" class="tabular-nums">Ecer: {{ variant.stock_retail ?? 0 }}</span>
|
||||
<span v-if="isCashierUser" class="tabular-nums">Ecer: {{ variant.retail_stock ?? 0 }}</span>
|
||||
<template v-else>
|
||||
<span class="tabular-nums">Bagus: {{ variant.stock }}</span>
|
||||
<span class="mx-1">·</span>
|
||||
|
||||
@ -80,7 +80,7 @@ export function useOrderPosCart(options: {
|
||||
}
|
||||
|
||||
if (stockQuality === StockQuality.RETAIL) {
|
||||
return variant.stock_retail ?? 0;
|
||||
return variant.retail_stock ?? 0;
|
||||
}
|
||||
|
||||
return variant.stock;
|
||||
|
||||
@ -21,7 +21,7 @@ const initialData = computed(() => ({
|
||||
id: variant.id,
|
||||
name: variant.name,
|
||||
stock: variant.stock,
|
||||
stock_retail: variant.stock_retail,
|
||||
retail_stock: variant.retail_stock,
|
||||
images: variant.images ?? [],
|
||||
})),
|
||||
}));
|
||||
|
||||
@ -42,7 +42,7 @@ const {
|
||||
client_id: createClientId(),
|
||||
name: '',
|
||||
stock: '0',
|
||||
stock_retail: '0',
|
||||
retail_stock: '0',
|
||||
media: createMediaUploadState(),
|
||||
}),
|
||||
() => {
|
||||
@ -51,7 +51,7 @@ const {
|
||||
client_id: createClientId(),
|
||||
name: '',
|
||||
stock: '0',
|
||||
stock_retail: '0',
|
||||
retail_stock: '0',
|
||||
media: createMediaUploadState(),
|
||||
}];
|
||||
}
|
||||
@ -61,7 +61,7 @@ const {
|
||||
id: variant.id,
|
||||
name: variant.name ?? '',
|
||||
stock: variant.stock != null ? String(variant.stock) : '0',
|
||||
stock_retail: variant.stock_retail != null ? String(variant.stock_retail) : '0',
|
||||
retail_stock: variant.retail_stock != null ? String(variant.retail_stock) : '0',
|
||||
media: createMediaUploadState(variant.images ?? []),
|
||||
}));
|
||||
},
|
||||
@ -100,7 +100,7 @@ function buildFormData(): FormData {
|
||||
appendToFormData(formData, (formData, index, variant) => {
|
||||
formData.append(`variants[${index}][name]`, variant.name.trim());
|
||||
formData.append(`variants[${index}][stock]`, String(Number.parseInt(String(variant.stock), 10) || 0));
|
||||
formData.append(`variants[${index}][stock_retail]`, String(Number.parseInt(String(variant.stock_retail), 10) || 0));
|
||||
formData.append(`variants[${index}][retail_stock]`, String(Number.parseInt(String(variant.retail_stock), 10) || 0));
|
||||
appendMediaToFormData(formData, `variants[${index}]`, variant.media);
|
||||
}, props.method);
|
||||
|
||||
@ -153,7 +153,7 @@ function submit() {
|
||||
@remove="removeVariant(variant.client_id)"
|
||||
@update:name="setVariantField(variant.client_id, 'name', $event)"
|
||||
@update:stock="setVariantField(variant.client_id, 'stock', $event)"
|
||||
@update:stock-retail="setVariantField(variant.client_id, 'stock_retail', $event)"
|
||||
@update:retail-stock="setVariantField(variant.client_id, 'retail_stock', $event)"
|
||||
@update:media="setVariantField(variant.client_id, 'media', $event)"
|
||||
/>
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ const emit = defineEmits<{
|
||||
remove: [];
|
||||
'update:name': [value: string];
|
||||
'update:stock': [value: string];
|
||||
'update:stock-retail': [value: string];
|
||||
'update:retail-stock': [value: string];
|
||||
'update:media': [value: MediaUploadState];
|
||||
}>();
|
||||
</script>
|
||||
@ -79,15 +79,15 @@ const emit = defineEmits<{
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'stock')" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel :for="`variant_stock_retail_${variant.client_id}`" required>
|
||||
<FieldLabel :for="`variant_retail_stock_${variant.client_id}`" required>
|
||||
Stok Ecer
|
||||
</FieldLabel>
|
||||
<NumberInput
|
||||
:id="`variant_stock_retail_${variant.client_id}`"
|
||||
:model-value="variant.stock_retail"
|
||||
@update:model-value="emit('update:stock-retail', String($event))"
|
||||
:id="`variant_retail_stock_${variant.client_id}`"
|
||||
:model-value="variant.retail_stock"
|
||||
@update:model-value="emit('update:retail-stock', String($event))"
|
||||
/>
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'stock_retail')" />
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'retail_stock')" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftRight } from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { DataTableEmpty } from '@/components/data-table';
|
||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||
import StockRetailTransferModal from '@/components/modal/StockRetailTransferModal.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -56,27 +53,6 @@ const paginationSummary = usePaginationSummary(() => props.pagination, showingCo
|
||||
function rowNumber(index: number): number {
|
||||
return groupedTableRowNumber(props.firstItem, index);
|
||||
}
|
||||
|
||||
// Stock Retail Transfer Modal
|
||||
const transferModalOpen = ref(false);
|
||||
const transferVariantId = ref(0);
|
||||
const transferVariantName = ref('');
|
||||
const transferProductName = ref('');
|
||||
const transferStockBagus = ref(0);
|
||||
const transferStockRetail = ref(0);
|
||||
|
||||
function openTransferModal(product: ProductListItem, variant: Variant) {
|
||||
transferVariantId.value = variant.id;
|
||||
transferVariantName.value = variant.name;
|
||||
transferProductName.value = product.name;
|
||||
transferStockBagus.value = variant.stock;
|
||||
transferStockRetail.value = variant.stock_retail;
|
||||
transferModalOpen.value = true;
|
||||
}
|
||||
|
||||
function onTransferSubmitted() {
|
||||
// Stock won't change until owner approves, no need to update local data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -141,12 +117,11 @@ function onTransferSubmitted() {
|
||||
<TableHead>Stok Reject</TableHead>
|
||||
<TableHead>Stok Ecer</TableHead>
|
||||
<TableHead>Harga</TableHead>
|
||||
<TableHead v-if="can('stocks.view')" class="w-[60px]">Aksi</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!product.variants.length" :key="`${product.id}-empty`">
|
||||
<TableCell :colspan="can('stocks.view') ? 7 : 6" class="text-muted-foreground">
|
||||
<TableCell :colspan="6" class="text-muted-foreground">
|
||||
Belum ada varian
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -164,7 +139,7 @@ function onTransferSubmitted() {
|
||||
{{ variant.reject_stock_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums text-blue-600">
|
||||
{{ variant.stock_retail_formatted }}
|
||||
{{ variant.retail_stock_formatted }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div v-if="variant.prices?.length" class="space-y-0.5 text-xs">
|
||||
@ -184,17 +159,6 @@ function onTransferSubmitted() {
|
||||
</div>
|
||||
<span v-else class="text-xs text-muted-foreground">Belum ada harga</span>
|
||||
</TableCell>
|
||||
<TableCell v-if="can('stocks.view')">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
title="Transfer ke Stok Ecer"
|
||||
:disabled="product.has_pending_request"
|
||||
@click="openTransferModal(product, variant)"
|
||||
>
|
||||
<ArrowLeftRight class="size-3.5" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@ -209,14 +173,4 @@ function onTransferSubmitted() {
|
||||
:pagination-links="paginationLinks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StockRetailTransferModal
|
||||
v-model:open="transferModalOpen"
|
||||
:variant-id="transferVariantId"
|
||||
:variant-name="transferVariantName"
|
||||
:product-name="transferProductName"
|
||||
:stock-bagus="transferStockBagus"
|
||||
:stock-retail="transferStockRetail"
|
||||
@submitted="onTransferSubmitted"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||
import { ref } from 'vue';
|
||||
import { RowDeleteAction, RowEditAction, RowTransferAction } from '@/components/button';
|
||||
import RetailStockTransferModal from '@/components/modal/RetailStockTransferModal.vue';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { edit, destroy } from '@/routes/admin/master/products';
|
||||
@ -10,6 +12,35 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const transferModalOpen = ref(false);
|
||||
const transferVariantId = ref(0);
|
||||
const transferVariantName = ref('');
|
||||
const transferGoodStock = ref(0);
|
||||
const transferRetailStock = ref(0);
|
||||
|
||||
function openTransferModal(variantId: number, variantName: string, goodStock: number, retailStock: number) {
|
||||
transferVariantId.value = variantId;
|
||||
transferVariantName.value = variantName;
|
||||
transferGoodStock.value = goodStock;
|
||||
transferRetailStock.value = retailStock;
|
||||
transferModalOpen.value = true;
|
||||
}
|
||||
|
||||
function handleTransferClick() {
|
||||
const variants = props.product.variants ?? [];
|
||||
|
||||
if (variants.length === 1) {
|
||||
const v = variants[0];
|
||||
openTransferModal(v.id, v.name, v.stock, v.retail_stock);
|
||||
} else if (variants.length > 1) {
|
||||
transferVariantId.value = 0;
|
||||
transferVariantName.value = '';
|
||||
transferGoodStock.value = 0;
|
||||
transferRetailStock.value = 0;
|
||||
transferModalOpen.value = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -18,6 +49,12 @@ const { can } = useCan();
|
||||
:id="product.pending_request_id" :title="product.name" subject-label="Produk"
|
||||
:action-label="product.pending_request_action_label" />
|
||||
|
||||
<RowTransferAction
|
||||
v-if="can('stocks.view')"
|
||||
:disabled="product.has_pending_request"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Transfer Stok Ecer'"
|
||||
@click="handleTransferClick"
|
||||
/>
|
||||
<RowEditAction v-if="can('products.update')" :href="edit.url(product.id)"
|
||||
:disabled="product.has_pending_request"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'" />
|
||||
@ -27,4 +64,15 @@ const { can } = useCan();
|
||||
:description="`Pengajuan hapus produk ${product.name} akan dikirim ke owner untuk verifikasi.`"
|
||||
error-message="Gagal mengajukan penghapusan produk." />
|
||||
</div>
|
||||
|
||||
<RetailStockTransferModal
|
||||
v-model:open="transferModalOpen"
|
||||
:variant-id="transferVariantId"
|
||||
:variant-name="transferVariantName"
|
||||
:product-name="product.name"
|
||||
:good-stock="transferGoodStock"
|
||||
:retail-stock="transferRetailStock"
|
||||
:variants="(product.variants ?? []).map(v => ({ id: v.id, name: v.name, stock: v.stock, retail_stock: v.retail_stock }))"
|
||||
@submitted="() => {}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -43,8 +43,8 @@ export interface Variant {
|
||||
stock_formatted: string;
|
||||
reject_stock: number;
|
||||
reject_stock_formatted: string;
|
||||
stock_retail: number;
|
||||
stock_retail_formatted: string;
|
||||
retail_stock: number;
|
||||
retail_stock_formatted: string;
|
||||
prices: Price[];
|
||||
images: MediaItem[];
|
||||
}
|
||||
@ -91,7 +91,7 @@ export interface ProductVariantFormItem {
|
||||
id?: number;
|
||||
name: string;
|
||||
stock: string | number;
|
||||
stock_retail: string | number;
|
||||
retail_stock: string | number;
|
||||
media: MediaUploadState;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@ -104,7 +104,7 @@ export type ProductFormInitialData = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
stock?: number | string;
|
||||
stock_retail?: number | string;
|
||||
retail_stock?: number | string;
|
||||
images?: MediaItem[];
|
||||
}>;
|
||||
};
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
use App\Http\Controllers\Admin\Manage\OwnerVerificationController;
|
||||
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController;
|
||||
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\Stock\RetailStockController;
|
||||
use App\Http\Controllers\Admin\Manage\Stock\StockController;
|
||||
use App\Http\Controllers\Admin\Manage\Stock\StockRetailController;
|
||||
use App\Http\Controllers\Admin\Manage\StokOpnameController;
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
@ -357,10 +357,10 @@
|
||||
->name('verify');
|
||||
});
|
||||
|
||||
Route::prefix('stock-retail')->name('stock-retail.')
|
||||
Route::prefix('retail-stock')->name('retail-stock.')
|
||||
->middleware('permission:'.Permission::STOCKS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::post('/transfer', [StockRetailController::class, 'transfer'])->name('transfer');
|
||||
Route::post('/transfer', [RetailStockController::class, 'transfer'])->name('transfer');
|
||||
});
|
||||
|
||||
Route::prefix('stok-opnames')->name('stok-opnames.')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user