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