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:
Yoga Pangestu 2026-06-28 22:57:07 +07:00
parent aeb7d1f292
commit 8f66502f25
32 changed files with 262 additions and 156 deletions

View File

@ -226,7 +226,7 @@ ### Controller
PurchaseDraftItemController.php PurchaseDraftItemController.php
Stock/ Stock/
StockController.php StockController.php
StockRetailController.php RetailStockController.php
StokOpnameController.php ← modul dengan 1 controller tetap di level parent StokOpnameController.php ← modul dengan 1 controller tetap di level parent
OwnerVerificationController.php OwnerVerificationController.php
``` ```

View File

@ -13,7 +13,7 @@ enum OwnerVerificationAction: string
case DELETE = 'delete'; case DELETE = 'delete';
case TOGGLE_STATUS = 'toggle_status'; case TOGGLE_STATUS = 'toggle_status';
case STOCK_VERIFY = 'stock_verify'; case STOCK_VERIFY = 'stock_verify';
case STOCK_RETAIL_TRANSFER = 'stock_retail_transfer'; case RETAIL_STOCK_TRANSFER = 'retail_stock_transfer';
public function label(): string public function label(): string
{ {
@ -23,7 +23,7 @@ public function label(): string
self::DELETE => 'Hapus', self::DELETE => 'Hapus',
self::TOGGLE_STATUS => 'Ubah Status', self::TOGGLE_STATUS => 'Ubah Status',
self::STOCK_VERIFY => 'Verifikasi Stok', self::STOCK_VERIFY => 'Verifikasi Stok',
self::STOCK_RETAIL_TRANSFER => 'Transfer Stok Ecer', self::RETAIL_STOCK_TRANSFER => 'Transfer Stok Ecer',
}; };
} }
} }

View File

@ -3,21 +3,21 @@
namespace App\Http\Controllers\Admin\Manage\Stock; namespace App\Http\Controllers\Admin\Manage\Stock;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\StockRetailTransferRequest; use App\Http\Requests\Admin\Manage\RetailStockTransferRequest;
use App\Services\Manage\StockRetailService; use App\Services\Manage\RetailStockService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class StockRetailController extends Controller class RetailStockController extends Controller
{ {
public function __construct( 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(); $validated = $request->validated();
$this->stockRetailService->transfer( $this->retailStockService->transfer(
$validated['product_variant_id'], $validated['product_variant_id'],
$validated['quantity'], $validated['quantity'],
$request->user(), $request->user(),

View File

@ -6,7 +6,7 @@
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class StockRetailTransferRequest extends FormRequest class RetailStockTransferRequest extends FormRequest
{ {
public function authorize(): bool public function authorize(): bool
{ {

View File

@ -42,7 +42,7 @@ public function rules(): array
], ],
'variants.*.name' => ['required', 'string', 'max:200'], 'variants.*.name' => ['required', 'string', 'max:200'],
'variants.*.stock' => ['required', 'integer', 'min:0'], 'variants.*.stock' => ['required', 'integer', 'min:0'],
'variants.*.stock_retail' => ['required', 'integer', 'min:0'], 'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
...$this->variantImageRules(), ...$this->variantImageRules(),
]; ];
} }
@ -60,7 +60,7 @@ public function attributes(): array
'variants' => 'Varian', 'variants' => 'Varian',
'variants.*.name' => 'Nama Varian', 'variants.*.name' => 'Nama Varian',
'variants.*.stock' => 'Stok', 'variants.*.stock' => 'Stok',
'variants.*.stock_retail' => 'Stok Ecer', 'variants.*.retail_stock' => 'Stok Ecer',
...$this->variantImageAttributes('variants', 'Foto Varian'), ...$this->variantImageAttributes('variants', 'Foto Varian'),
]; ];
} }

View File

@ -65,7 +65,7 @@ public function totalRejectStockFormatted(): Attribute
public function totalRetailStockFormatted(): Attribute public function totalRetailStockFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => number_format($this->variants->sum('stock_retail'), 0, ',', '.'), get: fn () => number_format($this->variants->sum('retail_stock'), 0, ',', '.'),
); );
} }

View File

@ -15,7 +15,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
#[Guarded(['id'])] #[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 class ProductVariant extends Model implements HasMedia
{ {
// 1. Use Trait // 1. Use Trait
@ -29,7 +29,7 @@ protected function casts(): array
return [ return [
'reject_stock' => 'integer', 'reject_stock' => 'integer',
'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( return Attribute::make(
get: fn () => number_format($this->stock_retail, 0, ',', '.'), get: fn () => number_format($this->retail_stock, 0, ',', '.'),
); );
} }

View File

@ -50,8 +50,10 @@ public function stockFormatted(): Attribute
return Attribute::make( return Attribute::make(
get: function () { get: function () {
$formatted = rtrim(rtrim(number_format((float) $this->stock, 4, ',', '.'), '0'), ','); $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}";
}, },
); );
} }

View File

@ -7,9 +7,9 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Guarded(['id'])] #[Guarded(['id'])]
class StockRetailHistory extends Model class RetailStockHistory extends Model
{ {
protected $table = 'stock_retail_histories'; protected $table = 'retail_stock_histories';
public $timestamps = false; public $timestamps = false;
@ -21,8 +21,8 @@ protected function casts(): array
'quantity' => 'integer', 'quantity' => 'integer',
'stock_after' => 'integer', 'stock_after' => 'integer',
'stock_before' => 'integer', 'stock_before' => 'integer',
'stock_retail_after' => 'integer', 'retail_stock_after' => 'integer',
'stock_retail_before' => 'integer', 'retail_stock_before' => 'integer',
]; ];
} }

View File

@ -117,7 +117,7 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
->with([ ->with([
'prices' => fn ($query) => $query 'prices' => fn ($query) => $query
->orderBy('created_at') ->orderBy('created_at')
->with(['rawMaterial:id,unit', 'media']), ->with('media'),
]) ])
->where(function (Builder $query) use ($selectedPriceIds): void { ->where(function (Builder $query) use ($selectedPriceIds): void {
$query->active(); $query->active();
@ -132,11 +132,10 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
->orderBy('name') ->orderBy('name')
->get() ->get()
->each(function (RawMaterial $rawMaterial): void { ->each(function (RawMaterial $rawMaterial): void {
$rawMaterial->prices->each(function (RawMaterialPrice $price): void { $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
$price->setAttribute( $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
'images', $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
MediaPresenter::collection($price, 'images'), $price->unsetRelation('rawMaterial');
);
}); });
}); });
} }

View File

@ -52,7 +52,7 @@ public function stockColumn(ProductStockQuality $quality): string
return match ($quality) { return match ($quality) {
ProductStockQuality::GOOD => 'stock', ProductStockQuality::GOOD => 'stock',
ProductStockQuality::REJECT => 'reject_stock', ProductStockQuality::REJECT => 'reject_stock',
ProductStockQuality::RETAIL => 'stock_retail', ProductStockQuality::RETAIL => 'retail_stock',
}; };
} }

View File

@ -36,7 +36,7 @@ public function __construct(
private readonly PurchaseService $purchaseService, private readonly PurchaseService $purchaseService,
private readonly PushNotificationService $pushNotificationService, private readonly PushNotificationService $pushNotificationService,
private readonly MarketplaceService $marketplaceService, private readonly MarketplaceService $marketplaceService,
private readonly StockRetailService $stockRetailService, private readonly RetailStockService $retailStockService,
) {} ) {}
public function hasPendingMarketplaceVerification(): bool public function hasPendingMarketplaceVerification(): bool
@ -283,7 +283,7 @@ private function rejectVerificationRequest(OwnerVerificationRequest $request): v
RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request), RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request),
Purchase::class => $this->purchaseService->rejectVerificationRequest($request), Purchase::class => $this->purchaseService->rejectVerificationRequest($request),
MarketplaceSettings::class => null, MarketplaceSettings::class => null,
ProductVariant::class => $this->stockRetailService->rejectStockRetailTransfer($request), ProductVariant::class => $this->retailStockService->rejectRetailStockTransfer($request),
default => throw ValidationException::withMessages([ default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.', 'subject_type' => 'Tipe data verifikasi tidak didukung.',
]), ]),
@ -297,7 +297,7 @@ private function applyVerificationRequest(OwnerVerificationRequest $request): vo
RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request), RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request),
Purchase::class => $this->purchaseService->applyVerificationRequest($request), Purchase::class => $this->purchaseService->applyVerificationRequest($request),
MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request), MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request),
ProductVariant::class => $this->stockRetailService->applyStockRetailTransfer($request), ProductVariant::class => $this->retailStockService->applyRetailStockTransfer($request),
default => throw ValidationException::withMessages([ default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.', 'subject_type' => 'Tipe data verifikasi tidak didukung.',
]), ]),

View File

@ -97,7 +97,7 @@ public function catalogItems(?Purchase $purchase = null, ?User $user = null): Co
->with([ ->with([
'prices' => fn ($query) => $query 'prices' => fn ($query) => $query
->orderBy('created_at') ->orderBy('created_at')
->with(['rawMaterial:id,unit', 'media']), ->with('media'),
]) ])
->where(function (Builder $query) use ($purchasePriceIds): void { ->where(function (Builder $query) use ($purchasePriceIds): void {
$query->active(); $query->active();
@ -112,11 +112,10 @@ public function catalogItems(?Purchase $purchase = null, ?User $user = null): Co
->orderBy('name') ->orderBy('name')
->get() ->get()
->each(function (RawMaterial $rawMaterial): void { ->each(function (RawMaterial $rawMaterial): void {
$rawMaterial->prices->each(function (RawMaterialPrice $price): void { $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
$price->setAttribute( $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
'images', $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
MediaPresenter::collection($price, 'images'), $price->unsetRelation('rawMaterial');
);
}); });
}); });
} }

View File

@ -7,14 +7,14 @@
use App\Enums\Permission; use App\Enums\Permission;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\ProductVariant; use App\Models\ProductVariant;
use App\Models\StockRetailHistory; use App\Models\RetailStockHistory;
use App\Models\User; use App\Models\User;
use App\Services\System\PushNotificationService; use App\Services\System\PushNotificationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class StockRetailService class RetailStockService
{ {
public function __construct( public function __construct(
private readonly PushNotificationService $pushNotificationService, private readonly PushNotificationService $pushNotificationService,
@ -47,7 +47,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
$existingPending = OwnerVerificationRequest::query() $existingPending = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class) ->where('subject_type', ProductVariant::class)
->where('subject_id', $variant->id) ->where('subject_id', $variant->id)
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER) ->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
->where('status', OwnerVerificationStatus::PENDING) ->where('status', OwnerVerificationStatus::PENDING)
->exists(); ->exists();
@ -59,7 +59,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
try { try {
$request = OwnerVerificationRequest::create([ $request = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::STOCK_RETAIL_TRANSFER, 'action' => OwnerVerificationAction::RETAIL_STOCK_TRANSFER,
'status' => OwnerVerificationStatus::PENDING, 'status' => OwnerVerificationStatus::PENDING,
'subject_type' => ProductVariant::class, 'subject_type' => ProductVariant::class,
'subject_id' => $variant->id, 'subject_id' => $variant->id,
@ -67,12 +67,12 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
'payload' => [ 'payload' => [
'old' => [ 'old' => [
'stock' => $variant->stock, 'stock' => $variant->stock,
'stock_retail' => $variant->stock_retail, 'retail_stock' => $variant->retail_stock,
], ],
'new' => [ 'new' => [
'quantity' => $quantity, 'quantity' => $quantity,
'stock' => $variant->stock - $quantity, 'stock' => $variant->stock - $quantity,
'stock_retail' => $variant->stock_retail + $quantity, 'retail_stock' => $variant->retail_stock + $quantity,
'notes' => $notes, 'notes' => $notes,
'variant_name' => $variant->name, 'variant_name' => $variant->name,
'product_name' => $variant->product?->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 ?? []; $payload = $request->payload ?? [];
$newData = $payload['new'] ?? []; $newData = $payload['new'] ?? [];
@ -138,26 +138,26 @@ private function executeTransfer(ProductVariant $variant, int $quantity, User $u
->firstOrFail(); ->firstOrFail();
$stockBefore = $variant->stock; $stockBefore = $variant->stock;
$stockRetailBefore = $variant->stock_retail; $retailStockBefore = $variant->retail_stock;
$variant->decrement('stock', $quantity); $variant->decrement('stock', $quantity);
$variant->increment('stock_retail', $quantity); $variant->increment('retail_stock', $quantity);
StockRetailHistory::create([ RetailStockHistory::create([
'product_variant_id' => $variant->id, 'product_variant_id' => $variant->id,
'user_id' => $user->id, 'user_id' => $user->id,
'quantity' => $quantity, 'quantity' => $quantity,
'stock_before' => $stockBefore, 'stock_before' => $stockBefore,
'stock_retail_before' => $stockRetailBefore, 'retail_stock_before' => $retailStockBefore,
'stock_after' => $stockBefore - $quantity, 'stock_after' => $stockBefore - $quantity,
'stock_retail_after' => $stockRetailBefore + $quantity, 'retail_stock_after' => $retailStockBefore + $quantity,
'notes' => $notes, 'notes' => $notes,
'created_at' => now(), '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. // No-op: nothing was changed yet, so nothing to rollback.
} }

View File

@ -74,12 +74,12 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
$pendingRequest = $product->pendingOwnerVerificationRequest; $pendingRequest = $product->pendingOwnerVerificationRequest;
// Also check for pending stock retail transfer on variants // Also check for pending retail stock transfer on variants
if ($pendingRequest === null) { if ($pendingRequest === null) {
$pendingRequest = OwnerVerificationRequest::query() $pendingRequest = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class) ->where('subject_type', ProductVariant::class)
->whereIn('subject_id', $product->variants->pluck('id')) ->whereIn('subject_id', $product->variants->pluck('id'))
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER) ->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
->pending() ->pending()
->latest() ->latest()
->first(); ->first();
@ -132,7 +132,7 @@ public function create(array $validated, User $user): void
$variant = $product->variants()->create([ $variant = $product->variants()->create([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'], 'retail_stock' => $variantData['retail_stock'],
]); ]);
$this->syncVariantImages($variant, $variantData, $index); $this->syncVariantImages($variant, $variantData, $index);
@ -195,7 +195,7 @@ public function update(Product $product, array $validated, User $user): void
$variant = $product->variants()->create([ $variant = $product->variants()->create([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'], 'retail_stock' => $variantData['retail_stock'],
]); ]);
$this->syncVariantImages($variant, $variantData, $index); $this->syncVariantImages($variant, $variantData, $index);
} }
@ -436,7 +436,7 @@ private function applyPayloadToProduct(
$variant->update([ $variant->update([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'], 'retail_stock' => $variantData['retail_stock'],
]); ]);
if ($verificationRequest !== null) { if ($verificationRequest !== null) {
@ -449,7 +449,7 @@ private function applyPayloadToProduct(
$variant = $product->variants()->create([ $variant = $product->variants()->create([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'], 'retail_stock' => $variantData['retail_stock'],
]); ]);
if ($verificationRequest !== null) { if ($verificationRequest !== null) {
@ -595,7 +595,7 @@ private function snapshotProduct(Product $product): array
'id' => $variant->id, 'id' => $variant->id,
'name' => $variant->name, 'name' => $variant->name,
'stock' => $variant->stock, 'stock' => $variant->stock,
'stock_retail' => $variant->stock_retail, 'retail_stock' => $variant->retail_stock,
]) ])
->all(), ->all(),
]); ]);
@ -623,7 +623,7 @@ private function buildPayloadFromValidated(array $validated): array
'id' => $variantData['id'] ?? null, 'id' => $variantData['id'] ?? null,
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'], 'retail_stock' => $variantData['retail_stock'],
'remove_media_ids' => $variantData['remove_media_ids'] ?? [], 'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
]) ])
->all(), ->all(),

View File

@ -35,7 +35,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
'pendingOwnerVerificationRequest', 'pendingOwnerVerificationRequest',
'prices' => fn ($query) => $query 'prices' => fn ($query) => $query
->orderBy('created_at') ->orderBy('created_at')
->with(['rawMaterial:id,unit', 'media']), ->with('media'),
]) ])
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search']; $search = $tableQuery['search'];
@ -73,11 +73,10 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
->paginate(25) ->paginate(25)
->withQueryString() ->withQueryString()
->through(function (RawMaterial $rawMaterial) { ->through(function (RawMaterial $rawMaterial) {
$rawMaterial->prices->each(function (RawMaterialPrice $price): void { $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
$price->setAttribute( $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
'images', $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
MediaPresenter::collection($price, 'images'), $price->unsetRelation('rawMaterial');
);
}); });
$pendingRequest = $rawMaterial->pendingOwnerVerificationRequest; $pendingRequest = $rawMaterial->pendingOwnerVerificationRequest;
@ -101,8 +100,10 @@ public function findForEdit(RawMaterial $rawMaterial): RawMaterial
'prices' => fn ($query) => $query->orderBy('created_at')->with('media'), '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('images', MediaPresenter::collection($price, 'images'));
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
$price->unsetRelation('rawMaterial');
}); });
return $rawMaterial; return $rawMaterial;

View File

@ -499,7 +499,7 @@ public function getProductStock(): array
->selectRaw(' ->selectRaw('
SUM(product_variants.stock) as total_stock, SUM(product_variants.stock) as total_stock,
SUM(product_variants.reject_stock) as total_reject, 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(product_variants.id) as total_variants,
COUNT(DISTINCT products.id) as total_products COUNT(DISTINCT products.id) as total_products
') ')

View File

@ -77,7 +77,7 @@ private static function label(string $field): string
'prices' => 'Varian Harga', 'prices' => 'Varian Harga',
'quantity' => 'Jumlah Transfer', 'quantity' => 'Jumlah Transfer',
'stock' => 'Stok Bagus', 'stock' => 'Stok Bagus',
'stock_retail' => 'Stok Ecer', 'retail_stock' => 'Stok Ecer',
'variant_name' => 'Varian', 'variant_name' => 'Varian',
'product_name' => 'Produk', 'product_name' => 'Produk',
'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform', 'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform',
@ -186,7 +186,7 @@ private static function presentValue(string $field, mixed $value): mixed
return implode(', ', $value); return implode(', ', $value);
} }
if (in_array($field, ['quantity', 'stock', 'stock_retail'], true)) { if (in_array($field, ['quantity', 'stock', 'retail_stock'], true)) {
return (int) $value; return (int) $value;
} }

View File

@ -16,7 +16,7 @@ public function up(): void
$table->string('name', 200); $table->string('name', 200);
$table->unsignedInteger('stock')->default(0); $table->unsignedInteger('stock')->default(0);
$table->unsignedInteger('reject_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('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();

View File

@ -8,7 +8,7 @@
{ {
public function up(): void public function up(): void
{ {
Schema::create('stock_retail_histories', function (Blueprint $table) { Schema::create('retail_stock_histories', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete(); $table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete();
@ -16,9 +16,9 @@ public function up(): void
$table->unsignedInteger('quantity'); $table->unsignedInteger('quantity');
$table->unsignedInteger('stock_before'); $table->unsignedInteger('stock_before');
$table->unsignedInteger('stock_retail_before'); $table->unsignedInteger('retail_stock_before');
$table->unsignedInteger('stock_after'); $table->unsignedInteger('stock_after');
$table->unsignedInteger('stock_retail_after'); $table->unsignedInteger('retail_stock_after');
$table->string('notes')->nullable(); $table->string('notes')->nullable();
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
@ -27,6 +27,6 @@ public function up(): void
public function down(): void public function down(): void
{ {
Schema::dropIfExists('stock_retail_histories'); Schema::dropIfExists('retail_stock_histories');
} }
}; };

View 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>

View File

@ -7,5 +7,6 @@ export { default as RowAdjustAction } from './RowAdjustAction.vue';
export { default as RowStatusAction } from './RowStatusAction.vue'; export { default as RowStatusAction } from './RowStatusAction.vue';
export { default as RowPrintAction } from './RowPrintAction.vue'; export { default as RowPrintAction } from './RowPrintAction.vue';
export { default as RowDetailAction } from './RowDetailAction.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 CreateButton } from './CreateButton.vue';
export { default as BackButton } from './BackButton.vue'; export { default as BackButton } from './BackButton.vue';

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowLeftRight, Save } from '@lucide/vue'; import { Save } from '@lucide/vue';
import { reactive, ref, watch } from 'vue'; import { computed, reactive, ref, watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { NumberInput } from '@/components/form/number-input'; import { NumberInput } from '@/components/form/number-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -18,9 +18,24 @@ import {
FieldLabel, FieldLabel,
FieldSet, FieldSet,
} from '@/components/ui/field'; } from '@/components/ui/field';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { apiFetch } from '@/lib/api'; 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 }); const open = defineModel<boolean>('open', { default: false });
@ -28,8 +43,9 @@ const props = defineProps<{
variantId: number; variantId: number;
variantName: string; variantName: string;
productName: string; productName: string;
stockBagus: number; goodStock: number;
stockRetail: number; retailStock: number;
variants?: VariantOption[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -39,6 +55,22 @@ const emit = defineEmits<{
const processing = ref(false); const processing = ref(false);
const errors = reactive<Record<string, string>>({}); 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({ const form = reactive({
quantity: '', quantity: '',
notes: '', notes: '',
@ -48,12 +80,28 @@ function resetForm() {
form.quantity = ''; form.quantity = '';
form.notes = ''; form.notes = '';
Object.keys(errors).forEach((key) => delete errors[key]); Object.keys(errors).forEach((key) => delete errors[key]);
if (hasMultipleVariants.value) {
selectedVariantId.value = 0;
}
} }
watch(open, (isOpen) => { watch(open, (isOpen) => {
if (isOpen) { if (isOpen) {
resetForm(); 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() { async function submit() {
@ -64,7 +112,7 @@ async function submit() {
await apiFetch(transfer.url(), { await apiFetch(transfer.url(), {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
product_variant_id: props.variantId, product_variant_id: submitVariantId.value,
quantity: Number(form.quantity), quantity: Number(form.quantity),
notes: form.notes || null, notes: form.notes || null,
}), }),
@ -108,14 +156,35 @@ function formatNumber(value: number): string {
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border p-3 space-y-1.5"> <div class="rounded-lg border p-3 space-y-1.5">
<p class="text-sm font-medium">{{ productName }}</p> <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"> <div class="flex justify-between text-xs pt-1">
<span class="text-muted-foreground">Stok Bagus</span> <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>
<div class="flex justify-between text-xs"> <div class="flex justify-between text-xs">
<span class="text-muted-foreground">Stok Ecer</span> <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>
</div> </div>
@ -131,7 +200,7 @@ function formatNumber(value: number): string {
<NumberInput <NumberInput
id="retail-quantity" id="retail-quantity"
v-model="form.quantity" v-model="form.quantity"
:max="stockBagus" :max="displayGoodStock"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
/> />
<FieldError <FieldError
@ -161,7 +230,7 @@ function formatNumber(value: number): string {
> >
Batal Batal
</Button> </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" /> <Save class="size-4" />
{{ processing ? 'Mengirim...' : 'Ajukan Transfer' }} {{ processing ? 'Mengirim...' : 'Ajukan Transfer' }}
</Button> </Button>

View File

@ -110,7 +110,7 @@ const emit = defineEmits<{
{{ variant.name }} {{ variant.name }}
</p> </p>
<p class="text-xs text-muted-foreground"> <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> <template v-else>
<span class="tabular-nums">Bagus: {{ variant.stock }}</span> <span class="tabular-nums">Bagus: {{ variant.stock }}</span>
<span class="mx-1">·</span> <span class="mx-1">·</span>

View File

@ -80,7 +80,7 @@ export function useOrderPosCart(options: {
} }
if (stockQuality === StockQuality.RETAIL) { if (stockQuality === StockQuality.RETAIL) {
return variant.stock_retail ?? 0; return variant.retail_stock ?? 0;
} }
return variant.stock; return variant.stock;

View File

@ -21,7 +21,7 @@ const initialData = computed(() => ({
id: variant.id, id: variant.id,
name: variant.name, name: variant.name,
stock: variant.stock, stock: variant.stock,
stock_retail: variant.stock_retail, retail_stock: variant.retail_stock,
images: variant.images ?? [], images: variant.images ?? [],
})), })),
})); }));

View File

@ -42,7 +42,7 @@ const {
client_id: createClientId(), client_id: createClientId(),
name: '', name: '',
stock: '0', stock: '0',
stock_retail: '0', retail_stock: '0',
media: createMediaUploadState(), media: createMediaUploadState(),
}), }),
() => { () => {
@ -51,7 +51,7 @@ const {
client_id: createClientId(), client_id: createClientId(),
name: '', name: '',
stock: '0', stock: '0',
stock_retail: '0', retail_stock: '0',
media: createMediaUploadState(), media: createMediaUploadState(),
}]; }];
} }
@ -61,7 +61,7 @@ const {
id: variant.id, id: variant.id,
name: variant.name ?? '', name: variant.name ?? '',
stock: variant.stock != null ? String(variant.stock) : '0', 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 ?? []), media: createMediaUploadState(variant.images ?? []),
})); }));
}, },
@ -100,7 +100,7 @@ function buildFormData(): FormData {
appendToFormData(formData, (formData, index, variant) => { appendToFormData(formData, (formData, index, variant) => {
formData.append(`variants[${index}][name]`, variant.name.trim()); 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]`, 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); appendMediaToFormData(formData, `variants[${index}]`, variant.media);
}, props.method); }, props.method);
@ -153,7 +153,7 @@ function submit() {
@remove="removeVariant(variant.client_id)" @remove="removeVariant(variant.client_id)"
@update:name="setVariantField(variant.client_id, 'name', $event)" @update:name="setVariantField(variant.client_id, 'name', $event)"
@update:stock="setVariantField(variant.client_id, 'stock', $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)" @update:media="setVariantField(variant.client_id, 'media', $event)"
/> />

View File

@ -30,7 +30,7 @@ const emit = defineEmits<{
remove: []; remove: [];
'update:name': [value: string]; 'update:name': [value: string];
'update:stock': [value: string]; 'update:stock': [value: string];
'update:stock-retail': [value: string]; 'update:retail-stock': [value: string];
'update:media': [value: MediaUploadState]; 'update:media': [value: MediaUploadState];
}>(); }>();
</script> </script>
@ -79,15 +79,15 @@ const emit = defineEmits<{
<FieldError :errors="variantErrors(variant.client_id, 'stock')" /> <FieldError :errors="variantErrors(variant.client_id, 'stock')" />
</Field> </Field>
<Field> <Field>
<FieldLabel :for="`variant_stock_retail_${variant.client_id}`" required> <FieldLabel :for="`variant_retail_stock_${variant.client_id}`" required>
Stok Ecer Stok Ecer
</FieldLabel> </FieldLabel>
<NumberInput <NumberInput
:id="`variant_stock_retail_${variant.client_id}`" :id="`variant_retail_stock_${variant.client_id}`"
:model-value="variant.stock_retail" :model-value="variant.retail_stock"
@update:model-value="emit('update:stock-retail', String($event))" @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> </Field>
</FieldSet> </FieldSet>

View File

@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowLeftRight } from '@lucide/vue'; import { computed } from 'vue';
import { computed, ref } from 'vue';
import { DataTableEmpty } from '@/components/data-table'; import { DataTableEmpty } from '@/components/data-table';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue'; import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue'; import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import StockRetailTransferModal from '@/components/modal/StockRetailTransferModal.vue';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { import {
Table, Table,
TableBody, TableBody,
@ -56,27 +53,6 @@ const paginationSummary = usePaginationSummary(() => props.pagination, showingCo
function rowNumber(index: number): number { function rowNumber(index: number): number {
return groupedTableRowNumber(props.firstItem, index); 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> </script>
<template> <template>
@ -141,12 +117,11 @@ function onTransferSubmitted() {
<TableHead>Stok Reject</TableHead> <TableHead>Stok Reject</TableHead>
<TableHead>Stok Ecer</TableHead> <TableHead>Stok Ecer</TableHead>
<TableHead>Harga</TableHead> <TableHead>Harga</TableHead>
<TableHead v-if="can('stocks.view')" class="w-[60px]">Aksi</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="!product.variants.length" :key="`${product.id}-empty`"> <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 Belum ada varian
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -164,7 +139,7 @@ function onTransferSubmitted() {
{{ variant.reject_stock_formatted }} {{ variant.reject_stock_formatted }}
</TableCell> </TableCell>
<TableCell class="tabular-nums text-blue-600"> <TableCell class="tabular-nums text-blue-600">
{{ variant.stock_retail_formatted }} {{ variant.retail_stock_formatted }}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div v-if="variant.prices?.length" class="space-y-0.5 text-xs"> <div v-if="variant.prices?.length" class="space-y-0.5 text-xs">
@ -184,17 +159,6 @@ function onTransferSubmitted() {
</div> </div>
<span v-else class="text-xs text-muted-foreground">Belum ada harga</span> <span v-else class="text-xs text-muted-foreground">Belum ada harga</span>
</TableCell> </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> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
@ -209,14 +173,4 @@ function onTransferSubmitted() {
:pagination-links="paginationLinks" :pagination-links="paginationLinks"
/> />
</div> </div>
<StockRetailTransferModal
v-model:open="transferModalOpen"
:variant-id="transferVariantId"
:variant-name="transferVariantName"
:product-name="transferProductName"
:stock-bagus="transferStockBagus"
:stock-retail="transferStockRetail"
@submitted="onTransferSubmitted"
/>
</template> </template>

View File

@ -1,5 +1,7 @@
<script setup lang="ts"> <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 OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { edit, destroy } from '@/routes/admin/master/products'; import { edit, destroy } from '@/routes/admin/master/products';
@ -10,6 +12,35 @@ const props = defineProps<{
}>(); }>();
const { can } = useCan(); 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> </script>
<template> <template>
@ -18,6 +49,12 @@ const { can } = useCan();
:id="product.pending_request_id" :title="product.name" subject-label="Produk" :id="product.pending_request_id" :title="product.name" subject-label="Produk"
:action-label="product.pending_request_action_label" /> :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)" <RowEditAction v-if="can('products.update')" :href="edit.url(product.id)"
:disabled="product.has_pending_request" :disabled="product.has_pending_request"
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'" /> :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.`" :description="`Pengajuan hapus produk ${product.name} akan dikirim ke owner untuk verifikasi.`"
error-message="Gagal mengajukan penghapusan produk." /> error-message="Gagal mengajukan penghapusan produk." />
</div> </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> </template>

View File

@ -43,8 +43,8 @@ export interface Variant {
stock_formatted: string; stock_formatted: string;
reject_stock: number; reject_stock: number;
reject_stock_formatted: string; reject_stock_formatted: string;
stock_retail: number; retail_stock: number;
stock_retail_formatted: string; retail_stock_formatted: string;
prices: Price[]; prices: Price[];
images: MediaItem[]; images: MediaItem[];
} }
@ -91,7 +91,7 @@ export interface ProductVariantFormItem {
id?: number; id?: number;
name: string; name: string;
stock: string | number; stock: string | number;
stock_retail: string | number; retail_stock: string | number;
media: MediaUploadState; media: MediaUploadState;
[key: string]: unknown; [key: string]: unknown;
} }
@ -104,7 +104,7 @@ export type ProductFormInitialData = {
id?: number; id?: number;
name?: string; name?: string;
stock?: number | string; stock?: number | string;
stock_retail?: number | string; retail_stock?: number | string;
images?: MediaItem[]; images?: MediaItem[];
}>; }>;
}; };

View File

@ -21,8 +21,8 @@
use App\Http\Controllers\Admin\Manage\OwnerVerificationController; use App\Http\Controllers\Admin\Manage\OwnerVerificationController;
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController; use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController;
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseDraftItemController; 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\StockController;
use App\Http\Controllers\Admin\Manage\Stock\StockRetailController;
use App\Http\Controllers\Admin\Manage\StokOpnameController; use App\Http\Controllers\Admin\Manage\StokOpnameController;
use App\Http\Controllers\Admin\Master\CategoryController; use App\Http\Controllers\Admin\Master\CategoryController;
use App\Http\Controllers\Admin\Master\CustomerController; use App\Http\Controllers\Admin\Master\CustomerController;
@ -357,10 +357,10 @@
->name('verify'); ->name('verify');
}); });
Route::prefix('stock-retail')->name('stock-retail.') Route::prefix('retail-stock')->name('retail-stock.')
->middleware('permission:'.Permission::STOCKS_VIEW->value) ->middleware('permission:'.Permission::STOCKS_VIEW->value)
->group(function () { ->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.') Route::prefix('stok-opnames')->name('stok-opnames.')