feat: update ProductRequest and related components to conditionally handle price fields based on user roles; enhance form behavior for better variant price management
This commit is contained in:
parent
78e30c2aba
commit
47289d158e
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\Role;
|
||||
use App\Http\Requests\Concerns\ValidatesMediaUploads;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -25,7 +26,9 @@ public function authorize(): bool
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
$isAdminBahanBaku = $this->user()?->hasRole(Role::ADMIN_BAHAN_BAKU->value) ?? false;
|
||||
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:200'],
|
||||
'description' => ['nullable', 'string'],
|
||||
|
||||
@ -43,17 +46,22 @@ public function rules(): array
|
||||
'variants.*.name' => ['required', 'string', 'max:200'],
|
||||
'variants.*.stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices' => ['required', 'array'],
|
||||
'variants.*.prices.distributor' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.agent' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.sub_agent' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.grosir' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.retail' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.tiktok' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.shopee' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.prices.harga_modal' => ['required', 'integer', 'min:0'],
|
||||
...$this->variantImageRules(),
|
||||
];
|
||||
|
||||
if (! $isAdminBahanBaku) {
|
||||
$rules['variants.*.prices'] = ['required', 'array'];
|
||||
$rules['variants.*.prices.distributor'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.agent'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.sub_agent'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.grosir'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.retail'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.tiktok'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.shopee'] = ['required', 'integer', 'min:0'];
|
||||
$rules['variants.*.prices.harga_modal'] = ['required', 'integer', 'min:0'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -138,11 +138,13 @@ public function create(array $validated, User $user): void
|
||||
|
||||
$this->syncVariantImages($variant, $variantData, $index);
|
||||
|
||||
foreach ($variantData['prices'] as $type => $priceValue) {
|
||||
$variant->prices()->create([
|
||||
'type' => $type,
|
||||
'price' => $priceValue,
|
||||
]);
|
||||
if (! empty($variantData['prices'])) {
|
||||
foreach ($variantData['prices'] as $type => $priceValue) {
|
||||
$variant->prices()->create([
|
||||
'type' => $type,
|
||||
'price' => $priceValue,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,16 @@ import { computed, ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { useVariantList } from '@/composables/useVariantList';
|
||||
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
|
||||
import type { CategoryOption, ProductFormInitialData, ProductVariantFormItem } from '@/types/product';
|
||||
import ProductInfoSection from './ProductInfoSection.vue';
|
||||
import ProductVariantSection from './ProductVariantSection.vue';
|
||||
|
||||
const { hasRole } = useCan();
|
||||
const showPrices = !hasRole('admin-bahan-baku');
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
categories: CategoryOption[];
|
||||
@ -156,7 +160,7 @@ function buildFormData(): FormData {
|
||||
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}][retail_stock]`, String(Number.parseInt(String(variant.retail_stock), 10) || 0));
|
||||
if (variant.prices) {
|
||||
if (variant.prices && showPrices) {
|
||||
Object.entries(variant.prices as Record<string, string>).forEach(([type, value]) => {
|
||||
formData.append(`variants[${index}][prices][${type}]`, String(Number.parseInt(value, 10) || 0));
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Trash2, Copy, Clipboard, Check } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { NumberInput } from '@/components/form/number-input';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||
@ -14,10 +14,14 @@ import {
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
import { PRICE_TYPES, PRICE_TYPE_LABELS } from '@/types/product';
|
||||
import type { ProductVariantFormItem } from '@/types/product';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
|
||||
const { hasRole } = useCan();
|
||||
const showPrices = !hasRole('admin-bahan-baku');
|
||||
|
||||
const props = defineProps<{
|
||||
form: {
|
||||
@ -83,14 +87,8 @@ function updatePrice(type: string, value: string) {
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-start justify-between gap-4">
|
||||
<CardTitle>Varian {{ index + 1 }}</CardTitle>
|
||||
<Button
|
||||
v-if="canRemove"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive size-8"
|
||||
@click="emit('remove')"
|
||||
>
|
||||
<Button v-if="canRemove" type="button" variant="outline" size="icon"
|
||||
class="text-destructive hover:text-destructive size-8" @click="emit('remove')">
|
||||
<Trash2 class="size-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
@ -101,75 +99,53 @@ function updatePrice(type: string, value: string) {
|
||||
<FieldLabel :for="`variant_name_${variant.client_id}`" required>
|
||||
Nama Varian
|
||||
</FieldLabel>
|
||||
<Input
|
||||
:id="`variant_name_${variant.client_id}`"
|
||||
:model-value="variant.name"
|
||||
type="text"
|
||||
placeholder="Masukkan nama varian"
|
||||
:maxlength="FIELD_LIMITS.variantName"
|
||||
@update:model-value="emit('update:name', String($event))"
|
||||
/>
|
||||
<Input :id="`variant_name_${variant.client_id}`" :model-value="variant.name" type="text"
|
||||
placeholder="Masukkan nama varian" :maxlength="FIELD_LIMITS.variantName"
|
||||
@update:model-value="emit('update:name', String($event))" />
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'name')" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel :for="`variant_stock_${variant.client_id}`" required>
|
||||
Stok
|
||||
</FieldLabel>
|
||||
<NumberInput
|
||||
:id="`variant_stock_${variant.client_id}`"
|
||||
:model-value="variant.stock"
|
||||
@update:model-value="emit('update:stock', String($event))"
|
||||
/>
|
||||
<NumberInput :id="`variant_stock_${variant.client_id}`" :model-value="variant.stock"
|
||||
@update:model-value="emit('update:stock', String($event))" />
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'stock')" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel :for="`variant_retail_stock_${variant.client_id}`" required>
|
||||
Stok Ecer
|
||||
</FieldLabel>
|
||||
<NumberInput
|
||||
:id="`variant_retail_stock_${variant.client_id}`"
|
||||
<NumberInput :id="`variant_retail_stock_${variant.client_id}`"
|
||||
:model-value="variant.retail_stock"
|
||||
@update:model-value="emit('update:retail-stock', String($event))"
|
||||
/>
|
||||
@update:model-value="emit('update:retail-stock', String($event))" />
|
||||
<FieldError :errors="variantErrors(variant.client_id, 'retail_stock')" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
<div class="border-t pt-4 mt-2">
|
||||
<div v-if="showPrices" class="border-t pt-4 mt-2">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-3">
|
||||
<h4 class="text-sm font-semibold">Harga Varian</h4>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5"
|
||||
@click="handleCopy"
|
||||
>
|
||||
<Check v-if="isCopied" class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5" @click="handleCopy">
|
||||
<Check v-if="isCopied"
|
||||
class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Copy v-else class="size-3.5" />
|
||||
{{ isCopied ? 'Berhasil' : 'Salin Harga' }}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5"
|
||||
:disabled="!hasCopiedPrices"
|
||||
@click="handlePaste"
|
||||
>
|
||||
<Check v-if="isPasted" class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5" :disabled="!hasCopiedPrices"
|
||||
@click="handlePaste">
|
||||
<Check v-if="isPasted"
|
||||
class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Clipboard v-else class="size-3.5" />
|
||||
{{ isPasted ? 'Berhasil' : 'Tempel Harga' }}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5"
|
||||
@click="handleApply"
|
||||
>
|
||||
<Check v-if="isApplied" class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
class="h-8 px-2.5 text-xs flex items-center gap-1.5" @click="handleApply">
|
||||
<Check v-if="isApplied"
|
||||
class="size-3.5 text-green-600 animate-in fade-in zoom-in-50 duration-200" />
|
||||
<Check v-else class="size-3.5" />
|
||||
{{ isApplied ? 'Berhasil' : 'Terapkan ke Semua' }}
|
||||
</Button>
|
||||
@ -180,26 +156,18 @@ function updatePrice(type: string, value: string) {
|
||||
<FieldLabel class="text-xs" :for="`variant_price_${variant.client_id}_${type}`" required>
|
||||
{{ PRICE_TYPE_LABELS[type] }}
|
||||
</FieldLabel>
|
||||
<RupiahInput
|
||||
:id="`variant_price_${variant.client_id}_${type}`"
|
||||
<RupiahInput :id="`variant_price_${variant.client_id}_${type}`"
|
||||
:model-value="(variant.prices as Record<string, string>)?.[type] ?? '0'"
|
||||
@update:model-value="updatePrice(type, $event)"
|
||||
/>
|
||||
@update:model-value="updatePrice(type, $event)" />
|
||||
<FieldError :errors="variantErrors(variant.client_id, `prices.${type}`)" />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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')"
|
||||
@update:model-value="emit('update:media', $event)"
|
||||
/>
|
||||
<MediaDropzone :id="`variant_images_${variant.client_id}`" :model-value="variant.media"
|
||||
label="Foto Varian" :max-files="5" required :errors="variantErrors(variant.client_id, 'images')"
|
||||
@update:model-value="emit('update:media', $event)" />
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user