Refactor product status management: replace is_active with status enum
- Updated the OwnerVerificationRequest model to change pendingToggleIsActive to pendingToggleStatus, reflecting the new status structure. - Refactored the Product model to replace is_active with status, utilizing the ProductStatus enum for better clarity and type safety. - Modified the ProductService to accommodate the new status field, including pagination and creation logic. - Adjusted the AnalysisService to filter products based on their status. - Updated the VerificationChangeFormatter to handle the new status field. - Created a migration to remove is_active from the products table and add status with appropriate default values. - Refactored the ProductSeeder to use the new status field. - Updated frontend components and types to reflect the changes from is_active to status. - Adjusted tests to ensure they validate the new status logic correctly.
This commit is contained in:
parent
f337e50fa2
commit
cf5b300895
23
app/Enums/ProductStatus.php
Normal file
23
app/Enums/ProductStatus.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\ProvidesEnumOptions;
|
||||||
|
|
||||||
|
enum ProductStatus: string
|
||||||
|
{
|
||||||
|
use ProvidesEnumOptions;
|
||||||
|
|
||||||
|
case DRAFT = 'draft';
|
||||||
|
case ACTIVE = 'active';
|
||||||
|
case INACTIVE = 'inactive';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::DRAFT => 'Draf',
|
||||||
|
self::ACTIVE => 'Aktif',
|
||||||
|
self::INACTIVE => 'Nonaktif',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,15 +31,15 @@ public function index(Request $request): Response
|
|||||||
{
|
{
|
||||||
$tableQuery = $this->parseDataTableQuery($request);
|
$tableQuery = $this->parseDataTableQuery($request);
|
||||||
$tableQuery['search_id'] = $request->string('search_id')->trim()->toString();
|
$tableQuery['search_id'] = $request->string('search_id')->trim()->toString();
|
||||||
$isActive = $request->string('is_active')->toString();
|
$status = $request->string('status')->toString();
|
||||||
$categoryId = $request->string('category_id')->toString();
|
$categoryId = $request->string('category_id')->toString();
|
||||||
$stockStatus = $request->string('stock_status')->toString();
|
$stockStatus = $request->string('stock_status')->toString();
|
||||||
|
|
||||||
return Inertia::render('admin/master/products/Index', [
|
return Inertia::render('admin/master/products/Index', [
|
||||||
'products' => $this->productService->paginateForIndex($tableQuery, $isActive, $categoryId, $stockStatus),
|
'products' => $this->productService->paginateForIndex($tableQuery, $status, $categoryId, $stockStatus),
|
||||||
'categories' => $this->categoryService->getSelectOptions(),
|
'categories' => $this->categoryService->getSelectOptions(),
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'status' => $status,
|
||||||
'category_id' => $categoryId,
|
'category_id' => $categoryId,
|
||||||
'stock_status' => $stockStatus,
|
'stock_status' => $stockStatus,
|
||||||
'search_id' => $tableQuery['search_id'],
|
'search_id' => $tableQuery['search_id'],
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Requests\Admin\Master;
|
namespace App\Http\Requests\Admin\Master;
|
||||||
|
|
||||||
use App\Enums\Permission;
|
use App\Enums\Permission;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Http\Requests\Concerns\HasProductVariantRules;
|
use App\Http\Requests\Concerns\HasProductVariantRules;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -32,6 +33,8 @@ public function rules(): array
|
|||||||
'category_ids' => ['required', 'array', 'min:1'],
|
'category_ids' => ['required', 'array', 'min:1'],
|
||||||
'category_ids.*' => ['integer', Rule::exists('categories', 'id')->whereNull('deleted_at')],
|
'category_ids.*' => ['integer', Rule::exists('categories', 'id')->whereNull('deleted_at')],
|
||||||
|
|
||||||
|
'status' => ['required', 'string', Rule::in(ProductStatus::values())],
|
||||||
|
|
||||||
...$this->productVariantRules(
|
...$this->productVariantRules(
|
||||||
productId: $this->route('product')?->id,
|
productId: $this->route('product')?->id,
|
||||||
imagesRequired: $this->isMethod('POST'),
|
imagesRequired: $this->isMethod('POST'),
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin;
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class ToggleStatusRequest extends FormRequest
|
class ToggleStatusRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@ -17,7 +19,7 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'is_active' => ['required', 'boolean'],
|
'status' => ['required', 'string', Rule::in([ProductStatus::ACTIVE->value, ProductStatus::INACTIVE->value])],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ public function rules(): array
|
|||||||
public function attributes(): array
|
public function attributes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'is_active' => 'aktif',
|
'status' => 'status',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,7 +150,7 @@ public static function mediaModuleName(): string
|
|||||||
return 'owner_verification_request';
|
return 'owner_verification_request';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pendingToggleIsActive(): ?bool
|
public function pendingToggleStatus(): ?string
|
||||||
{
|
{
|
||||||
if ($this->action !== OwnerVerificationAction::TOGGLE_STATUS) {
|
if ($this->action !== OwnerVerificationAction::TOGGLE_STATUS) {
|
||||||
return null;
|
return null;
|
||||||
@ -159,11 +159,11 @@ public function pendingToggleIsActive(): ?bool
|
|||||||
$payload = is_array($this->payload) ? $this->payload : [];
|
$payload = is_array($this->payload) ? $this->payload : [];
|
||||||
$new = $payload['new'] ?? null;
|
$new = $payload['new'] ?? null;
|
||||||
|
|
||||||
if (! is_array($new) || ! array_key_exists('is_active', $new)) {
|
if (! is_array($new) || ! array_key_exists('status', $new)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $new['is_active'];
|
return $new['status'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function priceImageCollection(int $index): string
|
public function priceImageCollection(int $index): string
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\OwnerVerificationStatus;
|
use App\Enums\OwnerVerificationStatus;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Concerns\HasPendingOwnerVerification;
|
use App\Models\Concerns\HasPendingOwnerVerification;
|
||||||
use App\Models\Concerns\InteractsWithActivityLog;
|
use App\Models\Concerns\InteractsWithActivityLog;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
@ -36,22 +37,28 @@ class Product extends Model
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'is_active' => 'boolean',
|
'status' => ProductStatus::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Scope (grouped by column, then alphabetical)
|
// 3. Scope (grouped by column, then alphabetical)
|
||||||
// Column Group: is_active
|
// Column Group: status
|
||||||
#[Scope]
|
#[Scope]
|
||||||
protected function active(Builder $query): void
|
protected function active(Builder $query): void
|
||||||
{
|
{
|
||||||
$query->where('is_active', true);
|
$query->where('status', ProductStatus::ACTIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Scope]
|
#[Scope]
|
||||||
protected function inactive(Builder $query): void
|
protected function inactive(Builder $query): void
|
||||||
{
|
{
|
||||||
$query->where('is_active', false);
|
$query->where('status', ProductStatus::INACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope]
|
||||||
|
protected function draft(Builder $query): void
|
||||||
|
{
|
||||||
|
$query->where('status', ProductStatus::DRAFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Attribute
|
// 4. Attribute
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\OwnerVerificationAction;
|
use App\Enums\OwnerVerificationAction;
|
||||||
use App\Enums\OwnerVerificationStatus;
|
use App\Enums\OwnerVerificationStatus;
|
||||||
use App\Enums\Permission;
|
use App\Enums\Permission;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\OwnerVerificationRequest;
|
use App\Models\OwnerVerificationRequest;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
@ -30,7 +31,7 @@ public function __construct(
|
|||||||
private readonly PushNotificationService $pushNotificationService,
|
private readonly PushNotificationService $pushNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginateForIndex(array $tableQuery, string $isActive, string $categoryId = '', string $stockStatus = ''): LengthAwarePaginator
|
public function paginateForIndex(array $tableQuery, string $status, string $categoryId = '', string $stockStatus = ''): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = Product::query()
|
$query = Product::query()
|
||||||
->with([
|
->with([
|
||||||
@ -50,7 +51,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
|||||||
$search = $tableQuery['search'];
|
$search = $tableQuery['search'];
|
||||||
$query->whereHas('variants', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
|
$query->whereHas('variants', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
|
||||||
})
|
})
|
||||||
->when($isActive !== '', fn (Builder $query) => $query->where('is_active', $isActive === '1'))
|
->when($status !== '', fn (Builder $query) => $query->where('status', $status))
|
||||||
->when($categoryId !== '', function (Builder $query) use ($categoryId): void {
|
->when($categoryId !== '', function (Builder $query) use ($categoryId): void {
|
||||||
$query->whereHas('categories', fn (Builder $query) => $query->where('categories.id', $categoryId));
|
$query->whereHas('categories', fn (Builder $query) => $query->where('categories.id', $categoryId));
|
||||||
})
|
})
|
||||||
@ -93,8 +94,8 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
|||||||
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||||
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
||||||
$product->setAttribute(
|
$product->setAttribute(
|
||||||
'display_is_active',
|
'display_status',
|
||||||
$pendingRequest?->pendingToggleIsActive() ?? $product->is_active,
|
$pendingRequest?->pendingToggleStatus() ?? $product->status->value,
|
||||||
);
|
);
|
||||||
|
|
||||||
return $product;
|
return $product;
|
||||||
@ -120,13 +121,14 @@ public function findForEdit(Product $product): Product
|
|||||||
public function create(array $validated, User $user): void
|
public function create(array $validated, User $user): void
|
||||||
{
|
{
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||||
|
$isDraft = ($validated['status'] ?? '') === ProductStatus::DRAFT->value;
|
||||||
|
|
||||||
$product = $this->runInTransaction(
|
$product = $this->runInTransaction(
|
||||||
function () use ($validated, $user, $isOwner): Product {
|
function () use ($validated, $user, $isOwner, $isDraft): Product {
|
||||||
$product = Product::create([
|
$product = Product::create([
|
||||||
'name' => $validated['name'],
|
'name' => $validated['name'],
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
'is_active' => $isOwner,
|
'status' => $isDraft ? ProductStatus::DRAFT : ($isOwner ? ProductStatus::ACTIVE : ProductStatus::INACTIVE),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$product->categories()->sync($validated['category_ids'] ?? []);
|
$product->categories()->sync($validated['category_ids'] ?? []);
|
||||||
@ -150,7 +152,7 @@ function () use ($validated, $user, $isOwner): Product {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $isOwner) {
|
if (! $isOwner && ! $isDraft) {
|
||||||
OwnerVerificationRequest::create([
|
OwnerVerificationRequest::create([
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
'action' => OwnerVerificationAction::CREATE,
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
'status' => OwnerVerificationStatus::PENDING,
|
||||||
@ -169,6 +171,16 @@ function () use ($validated, $user, $isOwner): Product {
|
|||||||
'Gagal membuat produk',
|
'Gagal membuat produk',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($isDraft) {
|
||||||
|
$this->notifyOwner(
|
||||||
|
'Simpan Draft Produk',
|
||||||
|
"Produk '{$validated['name']}' disimpan sebagai draft oleh {$user->profile?->full_name}.",
|
||||||
|
route('admin.master.products.index'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($isOwner) {
|
if ($isOwner) {
|
||||||
$this->cacheForgetByPattern('master:products:*');
|
$this->cacheForgetByPattern('master:products:*');
|
||||||
|
|
||||||
@ -193,10 +205,14 @@ function () use ($validated, $user, $isOwner): Product {
|
|||||||
public function update(Product $product, array $validated, User $user): void
|
public function update(Product $product, array $validated, User $user): void
|
||||||
{
|
{
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||||
|
$isDraft = $product->status === ProductStatus::DRAFT;
|
||||||
|
$changingToActive = ($validated['status'] ?? '') === ProductStatus::ACTIVE->value;
|
||||||
|
$canEditDirectly = $isOwner || ($isDraft && ! $changingToActive);
|
||||||
|
|
||||||
$this->runInTransaction(
|
$this->runInTransaction(
|
||||||
function () use ($validated, $product, $user, $isOwner): void {
|
function () use ($validated, $product, $user, $canEditDirectly): void {
|
||||||
if ($isOwner) {
|
|
||||||
|
if ($canEditDirectly) {
|
||||||
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
||||||
$this->applyPayloadToProduct($product, $payload);
|
$this->applyPayloadToProduct($product, $payload);
|
||||||
|
|
||||||
@ -253,6 +269,24 @@ function () use ($validated, $product, $user, $isOwner): void {
|
|||||||
'Gagal memperbarui produk',
|
'Gagal memperbarui produk',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($canEditDirectly) {
|
||||||
|
$this->cacheForgetByPattern('master:products:*');
|
||||||
|
$this->cacheForgetByPattern('prices:*');
|
||||||
|
$this->cacheForget('homepage:page_data');
|
||||||
|
|
||||||
|
$actionLabel = $isDraft && $changingToActive
|
||||||
|
? 'Publikasi Draft Produk'
|
||||||
|
: 'Ubah Produk';
|
||||||
|
|
||||||
|
$this->notifyOwner(
|
||||||
|
$actionLabel,
|
||||||
|
"Produk '{$product->name}' telah diperbarui oleh {$user->profile?->full_name}.",
|
||||||
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->cacheForgetByPattern('master:products:*');
|
$this->cacheForgetByPattern('master:products:*');
|
||||||
$this->cacheForgetByPattern('prices:*');
|
$this->cacheForgetByPattern('prices:*');
|
||||||
$this->cacheForget('homepage:page_data');
|
$this->cacheForget('homepage:page_data');
|
||||||
@ -370,16 +404,17 @@ function () use ($product, $user): void {
|
|||||||
public function toggleStatus(Product $product, array $validated, User $user): void
|
public function toggleStatus(Product $product, array $validated, User $user): void
|
||||||
{
|
{
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||||
|
$newStatus = ProductStatus::tryFrom($validated['status'] ?? '');
|
||||||
|
|
||||||
if ($isOwner) {
|
if ($isOwner) {
|
||||||
$product->update([
|
$product->update([
|
||||||
'is_active' => (bool) $validated['is_active'],
|
'status' => $newStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->cacheForgetByPattern('master:products:*');
|
$this->cacheForgetByPattern('master:products:*');
|
||||||
$this->cacheForget('homepage:page_data');
|
$this->cacheForget('homepage:page_data');
|
||||||
|
|
||||||
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
$statusLabel = $newStatus?->label() ?? 'unknown';
|
||||||
|
|
||||||
$this->notifyOwner(
|
$this->notifyOwner(
|
||||||
'Ubah Status Produk',
|
'Ubah Status Produk',
|
||||||
@ -391,7 +426,7 @@ public function toggleStatus(Product $product, array $validated, User $user): vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->runInTransaction(
|
$this->runInTransaction(
|
||||||
function () use ($product, $validated, $user): void {
|
function () use ($product, $user, $newStatus): void {
|
||||||
OwnerVerificationRequest::create([
|
OwnerVerificationRequest::create([
|
||||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
'status' => OwnerVerificationStatus::PENDING,
|
||||||
@ -401,11 +436,11 @@ function () use ($product, $validated, $user): void {
|
|||||||
'payload' => [
|
'payload' => [
|
||||||
'old' => [
|
'old' => [
|
||||||
'name' => $product->name,
|
'name' => $product->name,
|
||||||
'is_active' => $product->is_active,
|
'status' => $product->status->value,
|
||||||
],
|
],
|
||||||
'new' => [
|
'new' => [
|
||||||
'name' => $product->name,
|
'name' => $product->name,
|
||||||
'is_active' => (bool) $validated['is_active'],
|
'status' => $newStatus->value,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@ -413,7 +448,7 @@ function () use ($product, $validated, $user): void {
|
|||||||
'Gagal mengajukan perubahan status produk',
|
'Gagal mengajukan perubahan status produk',
|
||||||
);
|
);
|
||||||
|
|
||||||
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
$statusLabel = $newStatus?->label() ?? 'unknown';
|
||||||
|
|
||||||
$this->notifyForPendingRequest(
|
$this->notifyForPendingRequest(
|
||||||
$user,
|
$user,
|
||||||
@ -478,7 +513,7 @@ public function applyCreate(OwnerVerificationRequest $verificationRequest): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$product->update([
|
$product->update([
|
||||||
'is_active' => true,
|
'status' => ProductStatus::ACTIVE,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,10 +540,13 @@ private function applyPayloadToProduct(
|
|||||||
'description' => $payload['description'] ?? null,
|
'description' => $payload['description'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (array_key_exists('is_active', $payload)) {
|
if (array_key_exists('status', $payload)) {
|
||||||
$product->update([
|
$status = ProductStatus::tryFrom($payload['status']);
|
||||||
'is_active' => (bool) $payload['is_active'],
|
if ($status !== null) {
|
||||||
]);
|
$product->update([
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$product->categories()->sync($payload['category_ids'] ?? []);
|
$product->categories()->sync($payload['category_ids'] ?? []);
|
||||||
@ -609,8 +647,10 @@ public function applyToggleStatus(OwnerVerificationRequest $verificationRequest)
|
|||||||
|
|
||||||
$newPayload = $this->payloadNew($verificationRequest);
|
$newPayload = $this->payloadNew($verificationRequest);
|
||||||
|
|
||||||
|
$status = ProductStatus::tryFrom($newPayload['status'] ?? '');
|
||||||
|
|
||||||
$product->update([
|
$product->update([
|
||||||
'is_active' => (bool) ($newPayload['is_active'] ?? false),
|
'status' => $status ?? ProductStatus::INACTIVE,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -713,7 +753,7 @@ private function snapshotProduct(Product $product): array
|
|||||||
'name' => $product->name,
|
'name' => $product->name,
|
||||||
'description' => $product->description,
|
'description' => $product->description,
|
||||||
'category_ids' => $product->categories->pluck('id')->all(),
|
'category_ids' => $product->categories->pluck('id')->all(),
|
||||||
'is_active' => $product->is_active,
|
'status' => $product->status->value,
|
||||||
'variants' => $product->variants
|
'variants' => $product->variants
|
||||||
->map(fn (ProductVariant $variant) => [
|
->map(fn (ProductVariant $variant) => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
@ -744,6 +784,7 @@ private function buildPayloadFromValidated(array $validated): array
|
|||||||
return [
|
return [
|
||||||
'name' => $validated['name'],
|
'name' => $validated['name'],
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
|
'status' => $validated['status'] ?? ProductStatus::ACTIVE->value,
|
||||||
'category_ids' => $validated['category_ids'] ?? [],
|
'category_ids' => $validated['category_ids'] ?? [],
|
||||||
'variants' => collect($validated['variants'] ?? [])
|
'variants' => collect($validated['variants'] ?? [])
|
||||||
->map(fn (array $variantData) => [
|
->map(fn (array $variantData) => [
|
||||||
@ -820,7 +861,7 @@ private function copyRequestVariantImages(OwnerVerificationRequest $verification
|
|||||||
|
|
||||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||||
{
|
{
|
||||||
if (in_array($sort, ['name', 'slug', 'is_active'], true)) {
|
if (in_array($sort, ['name', 'slug', 'status'], true)) {
|
||||||
$query->orderBy($sort, $direction);
|
$query->orderBy($sort, $direction);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Enums\OrderStatus;
|
use App\Enums\OrderStatus;
|
||||||
use App\Enums\Permission;
|
use App\Enums\Permission;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
@ -646,6 +647,7 @@ public function getProductStock(): array
|
|||||||
{
|
{
|
||||||
$variants = ProductVariant::query()
|
$variants = ProductVariant::query()
|
||||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||||
|
->where('products.status', ProductStatus::ACTIVE)
|
||||||
->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,
|
||||||
@ -657,6 +659,8 @@ public function getProductStock(): array
|
|||||||
|
|
||||||
$totalValue = \DB::table('product_prices')
|
$totalValue = \DB::table('product_prices')
|
||||||
->join('product_variants', 'product_prices.variant_id', '=', 'product_variants.id')
|
->join('product_variants', 'product_prices.variant_id', '=', 'product_variants.id')
|
||||||
|
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||||
|
->where('products.status', ProductStatus::ACTIVE)
|
||||||
->where('product_prices.type', PriceType::HARGA_MODAL->value)
|
->where('product_prices.type', PriceType::HARGA_MODAL->value)
|
||||||
->whereNull('product_variants.deleted_at')
|
->whereNull('product_variants.deleted_at')
|
||||||
->whereNull('product_prices.deleted_at')
|
->whereNull('product_prices.deleted_at')
|
||||||
@ -664,8 +668,10 @@ public function getProductStock(): array
|
|||||||
->value('total');
|
->value('total');
|
||||||
|
|
||||||
$totalCategories = \DB::table('product_categories')
|
$totalCategories = \DB::table('product_categories')
|
||||||
->distinct('category_id')
|
->join('products', 'product_categories.product_id', '=', 'products.id')
|
||||||
->count('category_id');
|
->where('products.status', ProductStatus::ACTIVE)
|
||||||
|
->distinct('product_categories.category_id')
|
||||||
|
->count('product_categories.category_id');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'total_stock' => (int) ($variants->total_stock ?? 0),
|
'total_stock' => (int) ($variants->total_stock ?? 0),
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Support\OwnerVerification;
|
namespace App\Support\OwnerVerification;
|
||||||
|
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
|
|
||||||
class VerificationChangeFormatter
|
class VerificationChangeFormatter
|
||||||
{
|
{
|
||||||
@ -74,7 +75,7 @@ private static function label(string $field): string
|
|||||||
'has_photos' => 'Foto Bukti',
|
'has_photos' => 'Foto Bukti',
|
||||||
'unit' => 'Satuan',
|
'unit' => 'Satuan',
|
||||||
'unit_label' => 'Satuan',
|
'unit_label' => 'Satuan',
|
||||||
'is_active' => 'Status Aktif',
|
'status' => 'Status',
|
||||||
'variants' => 'Varian',
|
'variants' => 'Varian',
|
||||||
'prices' => 'Varian Harga',
|
'prices' => 'Varian Harga',
|
||||||
'quantity' => 'Perubahan Stok Ecer',
|
'quantity' => 'Perubahan Stok Ecer',
|
||||||
@ -107,8 +108,10 @@ private static function presentValue(string $field, mixed $value): mixed
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field === 'is_active') {
|
if ($field === 'status') {
|
||||||
return (bool) $value;
|
$status = ProductStatus::tryFrom((string) $value);
|
||||||
|
|
||||||
|
return $status?->label() ?? $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$marketplaceKeys = [
|
$marketplaceKeys = [
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -19,14 +20,14 @@ public function definition(): array
|
|||||||
'name' => Str::limit($name, 200, ''),
|
'name' => Str::limit($name, 200, ''),
|
||||||
'slug' => Str::slug($name),
|
'slug' => Str::slug($name),
|
||||||
'description' => fake()->optional()->paragraph(),
|
'description' => fake()->optional()->paragraph(),
|
||||||
'is_active' => true,
|
'status' => ProductStatus::ACTIVE,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inactive(): static
|
public function inactive(): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'is_active' => false,
|
'status' => ProductStatus::INACTIVE,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->string('status', 20)->default(ProductStatus::ACTIVE->value)->after('description');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('products')->where('is_active', true)->update(['status' => ProductStatus::ACTIVE->value]);
|
||||||
|
DB::table('products')->where('is_active', false)->update(['status' => ProductStatus::INACTIVE->value]);
|
||||||
|
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_active')->default(true)->after('description');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('products')->where('status', ProductStatus::ACTIVE->value)->update(['is_active' => true]);
|
||||||
|
DB::table('products')->whereIn('status', [ProductStatus::INACTIVE->value, ProductStatus::DRAFT->value])->update(['is_active' => false]);
|
||||||
|
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
@ -77,7 +78,7 @@ public function run(): void
|
|||||||
'name' => $productData['name'],
|
'name' => $productData['name'],
|
||||||
'slug' => str()->slug($productData['name']),
|
'slug' => str()->slug($productData['name']),
|
||||||
'description' => $productData['description'],
|
'description' => $productData['description'],
|
||||||
'is_active' => true,
|
'status' => ProductStatus::ACTIVE,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$product->categories()->sync(
|
$product->categories()->sync(
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
export const ProductStatus = {
|
||||||
|
DRAFT: 'draft',
|
||||||
|
ACTIVE: 'active',
|
||||||
|
INACTIVE: 'inactive',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** @deprecated Use ProductStatus instead */
|
||||||
export const ActiveStatus = {
|
export const ActiveStatus = {
|
||||||
ACTIVE: '1',
|
ACTIVE: '1',
|
||||||
INACTIVE: '0',
|
INACTIVE: '0',
|
||||||
|
|||||||
@ -80,6 +80,7 @@ const props = defineProps<{
|
|||||||
productStock: {
|
productStock: {
|
||||||
total_stock: number;
|
total_stock: number;
|
||||||
total_reject: number;
|
total_reject: number;
|
||||||
|
total_retail: number;
|
||||||
total_value: number;
|
total_value: number;
|
||||||
total_products: number;
|
total_products: number;
|
||||||
total_variants: number;
|
total_variants: number;
|
||||||
@ -674,28 +675,29 @@ watch([startDate, endDate], () => {
|
|||||||
]" />
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart"
|
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart"
|
||||||
main-label="Total Stok" :main-value="productStock.total_stock.toLocaleString('id-ID')
|
main-label="Total Stok"
|
||||||
" :sub-label="'Rp' +
|
:main-value="(
|
||||||
formatRupiah(productStock.total_value) +
|
productStock.total_stock +
|
||||||
(productStock.total_reject > 0
|
productStock.total_reject +
|
||||||
? ' (' + productStock.total_reject + ' reject)'
|
productStock.total_retail
|
||||||
: '')
|
).toLocaleString('id-ID')"
|
||||||
" :items="[
|
:sub-label="'Rp' + formatRupiah(productStock.total_value)"
|
||||||
|
:items="[
|
||||||
{
|
{
|
||||||
label: 'Produk',
|
label: 'Stok Bagus',
|
||||||
value: productStock.total_products.toLocaleString(
|
value: productStock.total_stock.toLocaleString(
|
||||||
'id-ID',
|
'id-ID',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Varian',
|
label: 'Stok Reject',
|
||||||
value: productStock.total_variants.toLocaleString(
|
value: productStock.total_reject.toLocaleString(
|
||||||
'id-ID',
|
'id-ID',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kategori',
|
label: 'Stok Ecer',
|
||||||
value: productStock.total_categories.toLocaleString(
|
value: productStock.total_retail.toLocaleString(
|
||||||
'id-ID',
|
'id-ID',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const initialData = computed(() => ({
|
|||||||
description: props.product.description ?? '',
|
description: props.product.description ?? '',
|
||||||
category_ids:
|
category_ids:
|
||||||
props.product.categories?.map((category) => category.id) ?? [],
|
props.product.categories?.map((category) => category.id) ?? [],
|
||||||
|
status: props.product.status ?? 'active',
|
||||||
variants: (props.product.variants ?? []).map((variant) => ({
|
variants: (props.product.variants ?? []).map((variant) => ({
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
name: variant.name,
|
name: variant.name,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
useDataTableQuery,
|
useDataTableQuery,
|
||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import { ActiveStatus } from '@/constants/active-status';
|
import { ProductStatus } from '@/constants/active-status';
|
||||||
import { StockStatus } from '@/constants/stock-status';
|
import { StockStatus } from '@/constants/stock-status';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, create } from '@/routes/admin/master/products';
|
import { index, create } from '@/routes/admin/master/products';
|
||||||
@ -23,7 +23,7 @@ const props = defineProps<{
|
|||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
direction?: 'asc' | 'desc';
|
direction?: 'asc' | 'desc';
|
||||||
is_active?: string;
|
status?: string;
|
||||||
category_id?: string;
|
category_id?: string;
|
||||||
stock_status?: string;
|
stock_status?: string;
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ const { query, setSearch, setFilter, resetFilters, syncFromServer } =
|
|||||||
useDataTableQuery({
|
useDataTableQuery({
|
||||||
url: index.url(),
|
url: index.url(),
|
||||||
initial: { ...props.filters },
|
initial: { ...props.filters },
|
||||||
filterKeys: ['is_active', 'category_id', 'stock_status'],
|
filterKeys: ['status', 'category_id', 'stock_status'],
|
||||||
});
|
});
|
||||||
|
|
||||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||||
@ -52,12 +52,13 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'is_active',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: ActiveStatus.ACTIVE, label: 'Aktif' },
|
{ value: ProductStatus.DRAFT, label: 'Draf' },
|
||||||
{ value: ActiveStatus.INACTIVE, label: 'Nonaktif' },
|
{ value: ProductStatus.ACTIVE, label: 'Aktif' },
|
||||||
|
{ value: ProductStatus.INACTIVE, label: 'Nonaktif' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -73,7 +74,7 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
|
|
||||||
const filterValues = computed(() => ({
|
const filterValues = computed(() => ({
|
||||||
category_id: query.value.category_id ?? '',
|
category_id: query.value.category_id ?? '',
|
||||||
is_active: query.value.is_active ?? '',
|
status: query.value.status ?? '',
|
||||||
stock_status: query.value.stock_status ?? '',
|
stock_status: query.value.stock_status ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { Plus, Save, Search, X } from '@lucide/vue';
|
import { FileText, Globe, Plus, Save, Search, X } from '@lucide/vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { useVariantList } from '@/composables/useVariantList';
|
import { useVariantList } from '@/composables/useVariantList';
|
||||||
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
|
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
|
||||||
@ -185,8 +187,16 @@ const form = useForm({
|
|||||||
name: props.initialData?.name ?? '',
|
name: props.initialData?.name ?? '',
|
||||||
description: props.initialData?.description ?? '',
|
description: props.initialData?.description ?? '',
|
||||||
category_ids: props.initialData?.category_ids ?? [],
|
category_ids: props.initialData?.category_ids ?? [],
|
||||||
|
status: props.initialData?.status ?? 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDraft = ref(props.initialData?.status === 'draft');
|
||||||
|
|
||||||
|
function toggleDraft(checked: boolean) {
|
||||||
|
isDraft.value = checked;
|
||||||
|
form.status = checked ? 'draft' : 'active';
|
||||||
|
}
|
||||||
|
|
||||||
const categoryError = computed(() => form.errors.category_ids);
|
const categoryError = computed(() => form.errors.category_ids);
|
||||||
|
|
||||||
function toggleCategory(categoryId: number, checked: boolean) {
|
function toggleCategory(categoryId: number, checked: boolean) {
|
||||||
@ -206,6 +216,7 @@ function buildFormData(): FormData {
|
|||||||
|
|
||||||
formData.append('name', form.name.trim());
|
formData.append('name', form.name.trim());
|
||||||
formData.append('description', form.description.trim());
|
formData.append('description', form.description.trim());
|
||||||
|
formData.append('status', form.status);
|
||||||
|
|
||||||
form.category_ids.forEach((categoryId) => {
|
form.category_ids.forEach((categoryId) => {
|
||||||
formData.append('category_ids[]', String(categoryId));
|
formData.append('category_ids[]', String(categoryId));
|
||||||
@ -299,16 +310,29 @@ function submit() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<Button type="button" variant="outline" @click="handleAddVariant">
|
<Button type="button" variant="outline" @click="handleAddVariant">
|
||||||
<Plus class="size-4" />
|
<Plus class="size-4" />
|
||||||
Tambah Varian
|
Tambah Varian
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="submit" :disabled="form.processing || isUploading">
|
<div class="flex flex-col items-end gap-3 sm:flex-row sm:items-center sm:gap-4">
|
||||||
<Save class="size-4" />
|
<div class="flex items-center gap-2">
|
||||||
{{ isUploading ? 'Mengunggah...' : form.processing ? 'Menyimpan...' : submitLabel }}
|
<Switch :id="'draft-toggle'" :model-value="isDraft" @update:model-value="toggleDraft" />
|
||||||
</Button>
|
<Label :for="'draft-toggle'" class="cursor-pointer">
|
||||||
|
<span class="flex items-center gap-1.5 text-sm whitespace-nowrap">
|
||||||
|
<FileText v-if="isDraft" class="size-4 text-muted-foreground" />
|
||||||
|
<Globe v-else class="size-4 text-muted-foreground" />
|
||||||
|
{{ isDraft ? 'Simpan sebagai Draft' : 'Publikasikan' }}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" :disabled="form.processing || isUploading" class="w-full sm:w-auto">
|
||||||
|
<Save class="size-4" />
|
||||||
|
{{ isUploading ? 'Mengunggah...' : form.processing ? 'Menyimpan...' : submitLabel }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { router } from '@inertiajs/vue3';
|
import { router } from '@inertiajs/vue3';
|
||||||
import { ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { ProductStatus } from '@/constants/active-status';
|
||||||
import { toggle_status } from '@/routes/admin/master/products';
|
import { toggle_status } from '@/routes/admin/master/products';
|
||||||
import type { ProductListItem } from '@/types/product';
|
import type { ProductListItem } from '@/types/product';
|
||||||
|
|
||||||
@ -14,38 +15,43 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
|
|
||||||
function resolveIsActive(product: ProductListItem): boolean {
|
function resolveStatus(product: ProductListItem): string {
|
||||||
return product.display_is_active ?? product.is_active;
|
return product.display_status ?? product.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActive = ref(resolveIsActive(props.product));
|
const currentStatus = ref(resolveStatus(props.product));
|
||||||
const processing = ref(false);
|
const processing = ref(false);
|
||||||
|
|
||||||
|
const isActive = computed(() => currentStatus.value === ProductStatus.ACTIVE);
|
||||||
|
const isDraft = computed(() => currentStatus.value === ProductStatus.DRAFT);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => resolveIsActive(props.product),
|
() => resolveStatus(props.product),
|
||||||
(value) => {
|
(value) => {
|
||||||
isActive.value = value;
|
currentStatus.value = value;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function toggleStatus(checked: boolean) {
|
function toggleStatus(checked: boolean) {
|
||||||
if (!can('products.toggle_status') || props.product.has_pending_request) {
|
if (!can('products.toggle_status') || props.product.has_pending_request || isDraft.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checked === isActive.value) {
|
const newStatus = checked ? ProductStatus.ACTIVE : ProductStatus.INACTIVE;
|
||||||
|
|
||||||
|
if (newStatus === currentStatus.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
isActive.value = checked;
|
currentStatus.value = newStatus;
|
||||||
|
|
||||||
router.patch(toggle_status.url(props.product.id), {
|
router.patch(toggle_status.url(props.product.id), {
|
||||||
is_active: checked,
|
status: newStatus,
|
||||||
}, {
|
}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: (errors: any) => {
|
onError: (errors: any) => {
|
||||||
isActive.value = resolveIsActive(props.product);
|
currentStatus.value = resolveStatus(props.product);
|
||||||
|
|
||||||
if (errors.system) {
|
if (errors.system) {
|
||||||
toast.error(errors.system);
|
toast.error(errors.system);
|
||||||
@ -57,7 +63,7 @@ function toggleStatus(checked: boolean) {
|
|||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
isActive.value = resolveIsActive(props.product);
|
currentStatus.value = resolveStatus(props.product);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -65,13 +71,18 @@ function toggleStatus(checked: boolean) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Switch
|
<template v-if="isDraft">
|
||||||
:model-value="isActive"
|
<Badge variant="secondary">Draf</Badge>
|
||||||
:disabled="processing || !can('products.toggle_status') || product.has_pending_request"
|
</template>
|
||||||
@update:model-value="toggleStatus"
|
<template v-else>
|
||||||
/>
|
<Switch
|
||||||
<Badge :variant="isActive ? 'default' : 'secondary'">
|
:model-value="isActive"
|
||||||
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
:disabled="processing || !can('products.toggle_status') || product.has_pending_request"
|
||||||
</Badge>
|
@update:model-value="toggleStatus"
|
||||||
|
/>
|
||||||
|
<Badge :variant="isActive ? 'default' : 'secondary'">
|
||||||
|
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export interface Product {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
is_active: boolean;
|
status: string;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
variants: Variant[];
|
variants: Variant[];
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ export interface ProductListItem extends Product {
|
|||||||
pending_request_action?: string;
|
pending_request_action?: string;
|
||||||
pending_request_action_label?: string;
|
pending_request_action_label?: string;
|
||||||
pending_request_submitted_by_name?: string;
|
pending_request_submitted_by_name?: string;
|
||||||
display_is_active?: boolean;
|
display_status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryOption {
|
export interface CategoryOption {
|
||||||
@ -102,6 +102,7 @@ export type ProductFormInitialData = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
category_ids?: number[];
|
category_ids?: number[];
|
||||||
|
status?: string;
|
||||||
variants?: Array<{
|
variants?: Array<{
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\PaymentType;
|
use App\Enums\PaymentType;
|
||||||
use App\Enums\Permission as PermissionEnum;
|
use App\Enums\Permission as PermissionEnum;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
@ -621,7 +622,7 @@ function setupOrderDraftItems(User $user): ProductVariant
|
|||||||
test('catalogItems excludes variants where all stock types are zero', function () {
|
test('catalogItems excludes variants where all stock types are zero', function () {
|
||||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
||||||
|
|
||||||
$product = Product::factory()->create(['is_active' => true]);
|
$product = Product::factory()->create(['status' => ProductStatus::ACTIVE]);
|
||||||
|
|
||||||
$variantWithStock = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 0]);
|
$variantWithStock = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 0]);
|
||||||
$variantAllZero = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 0]);
|
$variantAllZero = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 0]);
|
||||||
@ -643,7 +644,7 @@ function setupOrderDraftItems(User $user): ProductVariant
|
|||||||
test('catalogItems includes all-zero-stock variants already in order', function () {
|
test('catalogItems includes all-zero-stock variants already in order', function () {
|
||||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE);
|
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE);
|
||||||
|
|
||||||
$product = Product::factory()->create(['is_active' => true]);
|
$product = Product::factory()->create(['status' => ProductStatus::ACTIVE]);
|
||||||
$variantAllZero = ProductVariant::factory()->create([
|
$variantAllZero = ProductVariant::factory()->create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'stock' => 0,
|
'stock' => 0,
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use App\Enums\OwnerVerificationAction;
|
use App\Enums\OwnerVerificationAction;
|
||||||
use App\Enums\OwnerVerificationStatus;
|
use App\Enums\OwnerVerificationStatus;
|
||||||
use App\Enums\Permission as PermissionEnum;
|
use App\Enums\Permission as PermissionEnum;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\OwnerVerificationRequest;
|
use App\Models\OwnerVerificationRequest;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
@ -191,7 +192,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
createProductWithVariants();
|
createProductWithVariants();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('admin.master.products.index', ['is_active' => '1']))
|
->get(route('admin.master.products.index', ['status' => 'active']))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -243,6 +244,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->post(route('admin.master.products.store'), [
|
->post(route('admin.master.products.store'), [
|
||||||
'name' => 'Produk Baru',
|
'name' => 'Produk Baru',
|
||||||
|
'status' => 'active',
|
||||||
'description' => 'Deskripsi produk',
|
'description' => 'Deskripsi produk',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
@ -259,7 +261,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$this->assertDatabaseHas('products', [
|
$this->assertDatabaseHas('products', [
|
||||||
'name' => 'Produk Baru',
|
'name' => 'Produk Baru',
|
||||||
'is_active' => false,
|
'status' => ProductStatus::INACTIVE->value,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -421,6 +423,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'name' => 'Produk Lengkap',
|
'name' => 'Produk Lengkap',
|
||||||
'description' => 'Deskripsi lengkap',
|
'description' => 'Deskripsi lengkap',
|
||||||
'category_ids' => [$cat1->id, $cat2->id],
|
'category_ids' => [$cat1->id, $cat2->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantWithImage('S', 5),
|
variantWithImage('S', 5),
|
||||||
variantWithImage('M', 10),
|
variantWithImage('M', 10),
|
||||||
@ -430,14 +433,14 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$product = Product::where('name', 'Produk Lengkap')->first();
|
$product = Product::where('name', 'Produk Lengkap')->first();
|
||||||
expect($product)->not->toBeNull();
|
expect($product)->not->toBeNull();
|
||||||
expect($product->is_active)->toBeFalse();
|
expect($product->status)->toBe(ProductStatus::INACTIVE);
|
||||||
expect($product->categories)->toHaveCount(2);
|
expect($product->categories)->toHaveCount(2);
|
||||||
expect($product->variants)->toHaveCount(3);
|
expect($product->variants)->toHaveCount(3);
|
||||||
|
|
||||||
approveLatestOwnerVerificationRequest($verifier);
|
approveLatestOwnerVerificationRequest($verifier);
|
||||||
|
|
||||||
$product->refresh();
|
$product->refresh();
|
||||||
expect($product->is_active)->toBeTrue();
|
expect($product->status)->toBe(ProductStatus::ACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('approving create request auto-generates product slug', function () {
|
test('approving create request auto-generates product slug', function () {
|
||||||
@ -450,13 +453,14 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
->post(route('admin.master.products.store'), [
|
->post(route('admin.master.products.store'), [
|
||||||
'name' => 'Batik Modern Elegan',
|
'name' => 'Batik Modern Elegan',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [variantWithImage()],
|
'variants' => [variantWithImage()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('products', [
|
$this->assertDatabaseHas('products', [
|
||||||
'name' => 'Batik Modern Elegan',
|
'name' => 'Batik Modern Elegan',
|
||||||
'slug' => 'batik-modern-elegan',
|
'slug' => 'batik-modern-elegan',
|
||||||
'is_active' => false,
|
'status' => ProductStatus::INACTIVE->value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
approveLatestOwnerVerificationRequest($verifier);
|
approveLatestOwnerVerificationRequest($verifier);
|
||||||
@ -464,7 +468,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
$this->assertDatabaseHas('products', [
|
$this->assertDatabaseHas('products', [
|
||||||
'name' => 'Batik Modern Elegan',
|
'name' => 'Batik Modern Elegan',
|
||||||
'slug' => 'batik-modern-elegan',
|
'slug' => 'batik-modern-elegan',
|
||||||
'is_active' => true,
|
'status' => ProductStatus::ACTIVE->value,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -478,6 +482,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
->post(route('admin.master.products.store'), [
|
->post(route('admin.master.products.store'), [
|
||||||
'name' => 'Produk Ditolak',
|
'name' => 'Produk Ditolak',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [variantWithImage()],
|
'variants' => [variantWithImage()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -537,6 +542,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
->put(route('admin.master.products.update', $product), [
|
->put(route('admin.master.products.update', $product), [
|
||||||
'name' => 'Nama Diubah',
|
'name' => 'Nama Diubah',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||||
],
|
],
|
||||||
@ -563,6 +569,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'name' => 'Nama Diubah',
|
'name' => 'Nama Diubah',
|
||||||
'description' => 'Deskripsi baru',
|
'description' => 'Deskripsi baru',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantUpdateData($variant->id, 'New Variant', 20),
|
variantUpdateData($variant->id, 'New Variant', 20),
|
||||||
],
|
],
|
||||||
@ -592,6 +599,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'name' => 'Nama Diubah',
|
'name' => 'Nama Diubah',
|
||||||
'description' => 'Deskripsi baru',
|
'description' => 'Deskripsi baru',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantUpdateData($variant->id, 'New Variant', 20),
|
variantUpdateData($variant->id, 'New Variant', 20),
|
||||||
],
|
],
|
||||||
@ -676,6 +684,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
->put(route('admin.master.products.update', $product), [
|
->put(route('admin.master.products.update', $product), [
|
||||||
'name' => 'Updated Product',
|
'name' => 'Updated Product',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantUpdateData($variantToKeep->id, 'Kept Variant', 5),
|
variantUpdateData($variantToKeep->id, 'Kept Variant', 5),
|
||||||
],
|
],
|
||||||
@ -699,7 +708,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
])
|
])
|
||||||
->assertRedirect();
|
->assertRedirect();
|
||||||
|
|
||||||
@ -710,7 +719,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'status' => OwnerVerificationStatus::PENDING->value,
|
'status' => OwnerVerificationStatus::PENDING->value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($product->fresh()->is_active)->toBeTrue();
|
expect($product->fresh()->status)->toBe(ProductStatus::ACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('product index exposes display is active for pending toggle status request', function () {
|
test('product index exposes display is active for pending toggle status request', function () {
|
||||||
@ -720,11 +729,11 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
);
|
);
|
||||||
|
|
||||||
$product = createProductWithVariants();
|
$product = createProductWithVariants();
|
||||||
$product->update(['is_active' => true]);
|
$product->update(['status' => ProductStatus::ACTIVE]);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
])
|
])
|
||||||
->assertRedirect();
|
->assertRedirect();
|
||||||
|
|
||||||
@ -734,8 +743,8 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertInertia(fn ($page) => $page
|
$response->assertInertia(fn ($page) => $page
|
||||||
->where('products.data.0.id', $product->id)
|
->where('products.data.0.id', $product->id)
|
||||||
->where('products.data.0.is_active', true)
|
->where('products.data.0.status', 'active')
|
||||||
->where('products.data.0.display_is_active', false)
|
->where('products.data.0.display_status', 'inactive')
|
||||||
->where('products.data.0.has_pending_request', true)
|
->where('products.data.0.has_pending_request', true)
|
||||||
->where('products.data.0.pending_request_action', OwnerVerificationAction::TOGGLE_STATUS->value)
|
->where('products.data.0.pending_request_action', OwnerVerificationAction::TOGGLE_STATUS->value)
|
||||||
);
|
);
|
||||||
@ -749,19 +758,19 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
approveLatestOwnerVerificationRequest($verifier);
|
approveLatestOwnerVerificationRequest($verifier);
|
||||||
|
|
||||||
expect($product->fresh()->is_active)->toBeFalse();
|
expect($product->fresh()->status)->toBe(ProductStatus::INACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('guest cannot toggle product status', function () {
|
test('guest cannot toggle product status', function () {
|
||||||
$product = createProductWithVariants();
|
$product = createProductWithVariants();
|
||||||
|
|
||||||
$this->patch(route('admin.master.products.toggle_status', $product), [
|
$this->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
])->assertRedirect(route('login'));
|
])->assertRedirect(route('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -772,21 +781,21 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
])
|
])
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('is_active field is required', function () {
|
test('status field is required', function () {
|
||||||
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS);
|
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS);
|
||||||
|
|
||||||
$product = createProductWithVariants();
|
$product = createProductWithVariants();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => null,
|
'status' => null,
|
||||||
])
|
])
|
||||||
->assertSessionHasErrors('is_active');
|
->assertSessionHasErrors('status');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -893,14 +902,15 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
expect($product->fresh()->variants)->toHaveCount(3);
|
expect($product->fresh()->variants)->toHaveCount(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('product has is_active cast to boolean', function () {
|
test('product has status cast to enum', function () {
|
||||||
$product = Product::factory()->create(['is_active' => true]);
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
expect($product->is_active)->toBeTrue();
|
expect($product->status)->toBeInstanceOf(ProductStatus::class);
|
||||||
|
expect($product->status)->toBe(ProductStatus::ACTIVE);
|
||||||
|
|
||||||
$product->update(['is_active' => false]);
|
$product->update(['status' => ProductStatus::INACTIVE]);
|
||||||
|
|
||||||
expect($product->fresh()->is_active)->toBeFalse();
|
expect($product->fresh()->status)->toBe(ProductStatus::INACTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('product has total_stock_formatted accessor', function () {
|
test('product has total_stock_formatted accessor', function () {
|
||||||
@ -1022,6 +1032,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'name' => 'Produk Owner',
|
'name' => 'Produk Owner',
|
||||||
'description' => 'Deskripsi',
|
'description' => 'Deskripsi',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantWithImage('All Size', 10),
|
variantWithImage('All Size', 10),
|
||||||
],
|
],
|
||||||
@ -1030,7 +1041,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$product = Product::where('name', 'Produk Owner')->first();
|
$product = Product::where('name', 'Produk Owner')->first();
|
||||||
expect($product)->not->toBeNull();
|
expect($product)->not->toBeNull();
|
||||||
expect($product->is_active)->toBeTrue();
|
expect($product->status)->toBe(ProductStatus::ACTIVE);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('owner_verification_requests', [
|
$this->assertDatabaseMissing('owner_verification_requests', [
|
||||||
'subject_id' => $product->id,
|
'subject_id' => $product->id,
|
||||||
@ -1054,6 +1065,7 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
'name' => 'Produk Updated',
|
'name' => 'Produk Updated',
|
||||||
'description' => 'Deskripsi baru',
|
'description' => 'Deskripsi baru',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
|
'status' => 'active',
|
||||||
'variants' => [
|
'variants' => [
|
||||||
variantUpdateData($variant->id, 'Updated Variant', 25),
|
variantUpdateData($variant->id, 'Updated Variant', 25),
|
||||||
],
|
],
|
||||||
@ -1091,10 +1103,10 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
|
|||||||
|
|
||||||
$this->actingAs($owner)
|
$this->actingAs($owner)
|
||||||
->patch(route('admin.master.products.toggle_status', $product), [
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
'is_active' => false,
|
'status' => 'inactive',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($product->fresh()->is_active)->toBeFalse();
|
expect($product->fresh()->status)->toBe(ProductStatus::INACTIVE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user