feat: require s3_keys for product and raw material variants, enhancing validation and user feedback in forms

This commit is contained in:
Yoga Pangestu 2026-07-13 11:28:48 +07:00
parent 9f253dc2d9
commit bb74fb49c8
12 changed files with 57 additions and 9 deletions

View File

@ -49,4 +49,14 @@ public function attributes(): array
...$this->productVariantAttributes(), ...$this->productVariantAttributes(),
]; ];
} }
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'variants.*.s3_keys.required' => 'Foto varian wajib diisi.',
];
}
} }

View File

@ -45,4 +45,14 @@ public function attributes(): array
...$this->rawMaterialPriceAttributes(), ...$this->rawMaterialPriceAttributes(),
]; ];
} }
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'prices.*.s3_keys.required' => 'Foto varian wajib diisi.',
];
}
} }

View File

@ -25,7 +25,7 @@ protected function productVariantRules(string $variantsKey = 'variants', ?int $p
"{$variantsKey}.*.name" => ['required', 'string', 'max:200'], "{$variantsKey}.*.name" => ['required', 'string', 'max:200'],
"{$variantsKey}.*.stock" => ['required', 'integer', 'min:0'], "{$variantsKey}.*.stock" => ['required', 'integer', 'min:0'],
"{$variantsKey}.*.retail_stock" => ['required', 'integer', 'min:0'], "{$variantsKey}.*.retail_stock" => ['required', 'integer', 'min:0'],
...$this->variantImageRules($variantsKey), ...$this->variantImageRules($variantsKey, required: true),
]; ];
if ($productId) { if ($productId) {

View File

@ -18,7 +18,7 @@ protected function rawMaterialPriceRules(string $pricesKey = 'prices', ?int $raw
"{$pricesKey}.*.variant" => ['required', 'string', 'max:200'], "{$pricesKey}.*.variant" => ['required', 'string', 'max:200'],
"{$pricesKey}.*.price" => ['required', 'integer', 'gt:0'], "{$pricesKey}.*.price" => ['required', 'integer', 'gt:0'],
"{$pricesKey}.*.stock" => ['required', 'numeric', 'decimal:0,4', 'min:0'], "{$pricesKey}.*.stock" => ['required', 'numeric', 'decimal:0,4', 'min:0'],
...$this->variantImageRules($pricesKey), ...$this->variantImageRules($pricesKey, required: true),
]; ];
if ($rawMaterialId) { if ($rawMaterialId) {

View File

@ -22,10 +22,12 @@ protected function photoRules(string $prefix = 'photos', int $max = 1): array
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
protected function variantImageRules(string $variantsKey = 'variants', int $max = 5): array protected function variantImageRules(string $variantsKey = 'variants', int $max = 5, bool $required = false): array
{ {
$s3KeysRule = $required ? 'required' : 'nullable';
return [ return [
"{$variantsKey}.*.s3_keys" => ['nullable', 'array', "max:{$max}"], "{$variantsKey}.*.s3_keys" => [$s3KeysRule, 'array', "max:{$max}"],
"{$variantsKey}.*.s3_keys.*" => ['required', 'string'], "{$variantsKey}.*.s3_keys.*" => ['required', 'string'],
"{$variantsKey}.*.remove_media_ids" => ['nullable', 'array'], "{$variantsKey}.*.remove_media_ids" => ['nullable', 'array'],
"{$variantsKey}.*.remove_media_ids.*" => ['integer'], "{$variantsKey}.*.remove_media_ids.*" => ['integer'],

View File

@ -77,7 +77,7 @@ const emit = defineEmits<{
<div> <div>
<MediaDropzone :id="`variant_images_${variant.client_id}`" :model-value="variant.media" <MediaDropzone :id="`variant_images_${variant.client_id}`" :model-value="variant.media"
label="Foto Varian" :max-files="5" :errors="variantErrors(variant.client_id, 'images')" label="Foto Varian" :max-files="5" :errors="variantErrors(variant.client_id, 's3_keys')"
@update:model-value="emit('update:media', $event)" /> @update:model-value="emit('update:media', $event)" />
</div> </div>
</FieldGroup> </FieldGroup>

View File

@ -210,7 +210,7 @@ function submit() {
<MediaDropzone :id="`variant_images_${editingVariantFormItem.client_id}`" <MediaDropzone :id="`variant_images_${editingVariantFormItem.client_id}`"
v-model="editingVariantFormItem.media" label="Foto Varian" :max-files="5" required v-model="editingVariantFormItem.media" label="Foto Varian" :max-files="5" required
:errors="formErrors(editForm, `variants.${editingVariantIndex}.images`)" /> :errors="formErrors(editForm, `variants.${editingVariantIndex}.s3_keys`)" />
</FieldSet> </FieldSet>
</FieldGroup> </FieldGroup>

View File

@ -167,7 +167,7 @@ function updatePrice(type: string, value: string) {
<div> <div>
<MediaDropzone :id="`variant_images_${variant.client_id}`" :model-value="variant.media" <MediaDropzone :id="`variant_images_${variant.client_id}`" :model-value="variant.media"
label="Foto Varian" :max-files="5" required :errors="variantErrors(variant.client_id, 'images')" label="Foto Varian" :max-files="5" required :errors="variantErrors(variant.client_id, 's3_keys')"
@update:model-value="emit('update:media', $event)" /> @update:model-value="emit('update:media', $event)" />
</div> </div>
</FieldGroup> </FieldGroup>

View File

@ -165,7 +165,7 @@ function submit() {
<MediaDropzone :id="`price_images_${editingPriceFormItem.client_id}`" <MediaDropzone :id="`price_images_${editingPriceFormItem.client_id}`"
v-model="editingPriceFormItem.media" label="Foto Varian" :max-files="5" required v-model="editingPriceFormItem.media" label="Foto Varian" :max-files="5" required
:errors="formErrors(editForm, `prices.${editingPriceIndex}.images`)" /> :errors="formErrors(editForm, `prices.${editingPriceIndex}.s3_keys`)" />
</FieldSet> </FieldSet>
</FieldGroup> </FieldGroup>

View File

@ -84,7 +84,7 @@ const emit = defineEmits<{
</FieldSet> </FieldSet>
<MediaDropzone :id="`price_images_${price.client_id}`" v-model="price.media" label="Foto Varian" <MediaDropzone :id="`price_images_${price.client_id}`" v-model="price.media" label="Foto Varian"
:max-files="5" required :errors="priceErrors(price.client_id, 'images')" /> :max-files="5" required :errors="priceErrors(price.client_id, 's3_keys')" />
</FieldGroup> </FieldGroup>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -395,6 +395,20 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock
->assertSessionHasErrors('variants.0.stock'); ->assertSessionHasErrors('variants.0.stock');
}); });
test('variant s3_keys is required', function () {
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE);
$category = Category::factory()->create();
$this->actingAs($user)
->post(route('admin.master.products.store'), [
'name' => 'Produk Baru',
'category_ids' => [$category->id],
'variants' => [['name' => 'All Size', 'stock' => 10, 'retail_stock' => 0, 'prices' => defaultPrices()]],
])
->assertSessionHasErrors('variants.0.s3_keys');
});
test('approving create request activates product with variants and categories', function () { test('approving create request activates product with variants and categories', function () {
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE);
$verifier = createProductVerifierUser(); $verifier = createProductVerifierUser();

View File

@ -382,6 +382,18 @@ function createRawMaterialVerifierUser(): User
->assertSessionHasErrors('prices.0.stock'); ->assertSessionHasErrors('prices.0.stock');
}); });
test('price s3_keys is required', function () {
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
$this->actingAs($user)
->post(route('admin.master.raw_materials.store'), [
'name' => 'Kain Sutra',
'unit' => RawMaterialUnit::METER->value,
'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => 10]],
])
->assertSessionHasErrors('prices.0.s3_keys');
});
test('creating raw material also creates prices', function () { test('creating raw material also creates prices', function () {
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE); $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);