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(),
];
}
/**
* @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(),
];
}
/**
* @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}.*.stock" => ['required', 'integer', 'min:0'],
"{$variantsKey}.*.retail_stock" => ['required', 'integer', 'min:0'],
...$this->variantImageRules($variantsKey),
...$this->variantImageRules($variantsKey, required: true),
];
if ($productId) {

View File

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

View File

@ -22,10 +22,12 @@ protected function photoRules(string $prefix = 'photos', int $max = 1): array
/**
* @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 [
"{$variantsKey}.*.s3_keys" => ['nullable', 'array', "max:{$max}"],
"{$variantsKey}.*.s3_keys" => [$s3KeysRule, 'array', "max:{$max}"],
"{$variantsKey}.*.s3_keys.*" => ['required', 'string'],
"{$variantsKey}.*.remove_media_ids" => ['nullable', 'array'],
"{$variantsKey}.*.remove_media_ids.*" => ['integer'],

View File

@ -77,7 +77,7 @@ const emit = defineEmits<{
<div>
<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)" />
</div>
</FieldGroup>

View File

@ -210,7 +210,7 @@ function submit() {
<MediaDropzone :id="`variant_images_${editingVariantFormItem.client_id}`"
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>
</FieldGroup>

View File

@ -167,7 +167,7 @@ function updatePrice(type: string, value: string) {
<div>
<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)" />
</div>
</FieldGroup>

View File

@ -165,7 +165,7 @@ function submit() {
<MediaDropzone :id="`price_images_${editingPriceFormItem.client_id}`"
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>
</FieldGroup>

View File

@ -84,7 +84,7 @@ const emit = defineEmits<{
</FieldSet>
<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>
</CardContent>
</Card>

View File

@ -395,6 +395,20 @@ function variantUpdateData(int $id, string $name = 'Updated Variant', int $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 () {
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE);
$verifier = createProductVerifierUser();

View File

@ -382,6 +382,18 @@ function createRawMaterialVerifierUser(): User
->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 () {
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);