feat: add functionality to create new purchase variants and update related UI components for variant management
This commit is contained in:
parent
f20889e962
commit
8da733ec08
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\PurchaseDraftItemRequest;
|
||||
use App\Http\Requests\Admin\Manage\PurchaseNewVariantRequest;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Manage\PurchaseService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@ -22,6 +23,13 @@ public function store(PurchaseDraftItemRequest $request): JsonResponse
|
||||
return response()->json(['item' => $item]);
|
||||
}
|
||||
|
||||
public function storeNewVariant(PurchaseNewVariantRequest $request): JsonResponse
|
||||
{
|
||||
$price = $this->purchaseService->createVariantAndDraft($request->validated(), $request->user());
|
||||
|
||||
return response()->json(['price' => $price]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, RawMaterialPrice $rawMaterialPrice): JsonResponse
|
||||
{
|
||||
$this->purchaseService->removeDraftItem($request->user(), $rawMaterialPrice);
|
||||
|
||||
50
app/Http/Requests/Admin/Manage/PurchaseNewVariantRequest.php
Normal file
50
app/Http/Requests/Admin/Manage/PurchaseNewVariantRequest.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Requests\Concerns\ValidatesMediaUploads;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PurchaseNewVariantRequest extends FormRequest
|
||||
{
|
||||
use ValidatesMediaUploads;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can(Permission::PURCHASES_CREATE->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'raw_material_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('raw_materials', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'variant' => ['required', 'string', 'max:200'],
|
||||
'price' => ['required', 'integer', 'min:0'],
|
||||
'stock' => ['nullable', 'numeric', 'decimal:0,4', 'min:0'],
|
||||
...$this->photoRules(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'raw_material_id' => 'Bahan Baku',
|
||||
'variant' => 'Nama Varian',
|
||||
'price' => 'Harga',
|
||||
'stock' => 'Stok',
|
||||
...$this->photoUploadAttributes('Foto Varian'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,8 @@ public function materialUsageInput(): Attribute
|
||||
public function unitAbbreviation(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation(),
|
||||
get: fn () => $this->attributes['unit_abbreviation']
|
||||
?? $this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -40,8 +40,10 @@ public function quantityFormatted(): Attribute
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$formatted = rtrim(rtrim(number_format((float) $this->quantity, 4, ',', '.'), '0'), ',');
|
||||
$unitAbbreviation = $this->attributes['unit_abbreviation']
|
||||
?? $this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation();
|
||||
|
||||
return "{$formatted} {$this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation()}";
|
||||
return "{$formatted} {$unitAbbreviation}";
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -63,7 +65,8 @@ public function subtotalFormatted(): Attribute
|
||||
public function unitAbbreviation(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation(),
|
||||
get: fn () => $this->attributes['unit_abbreviation']
|
||||
?? $this->rawMaterialPrice?->rawMaterial?->unit?->abbreviation(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
|
||||
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
|
||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
||||
$this->appendCostPreview($cutting);
|
||||
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||
|
||||
return $cutting;
|
||||
});
|
||||
@ -84,6 +85,7 @@ public function getInProgressCuttings(User $user): Collection
|
||||
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
|
||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
||||
$this->appendCostPreview($cutting);
|
||||
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||
});
|
||||
}
|
||||
|
||||
@ -104,6 +106,7 @@ public function getCompletedCuttings(User $user): Collection
|
||||
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
|
||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
||||
$this->appendCostPreview($cutting);
|
||||
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||
});
|
||||
}
|
||||
|
||||
@ -188,11 +191,10 @@ public function findForEdit(Cutting $cutting): Cutting
|
||||
$price = $material->rawMaterialPrice;
|
||||
|
||||
if ($price) {
|
||||
$price->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($price, 'images'),
|
||||
);
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
}
|
||||
|
||||
$this->breakMaterialCircularReference($material);
|
||||
});
|
||||
|
||||
$cutting->results->each(function (CuttingResult $result): void {
|
||||
@ -217,7 +219,12 @@ public function draftMaterialsForUser(User $user): array
|
||||
'rawMaterialPrice.media',
|
||||
])
|
||||
->get()
|
||||
->map(fn (CuttingMaterial $item) => $this->presentDraftMaterial($item))
|
||||
->map(function (CuttingMaterial $item) {
|
||||
$result = $this->presentDraftMaterial($item);
|
||||
$this->breakMaterialCircularReference($item);
|
||||
|
||||
return $result;
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
@ -265,7 +272,10 @@ public function syncDraftMaterial(array $validated, User $user): array
|
||||
'rawMaterialPrice.media',
|
||||
]);
|
||||
|
||||
return $this->presentDraftMaterial($item);
|
||||
$result = $this->presentDraftMaterial($item);
|
||||
$this->breakMaterialCircularReference($item);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function syncDraftResult(array $validated, User $user): array
|
||||
@ -794,6 +804,25 @@ private function storeRejection(Cutting $cutting, ?string $reason, User $user):
|
||||
]);
|
||||
}
|
||||
|
||||
private function breakMaterialCircularReference(CuttingMaterial $material): void
|
||||
{
|
||||
$price = $material->rawMaterialPrice;
|
||||
|
||||
if ($price) {
|
||||
$rawMaterial = $price->rawMaterial;
|
||||
|
||||
if ($rawMaterial) {
|
||||
$unitAbbreviation = $rawMaterial->unit->abbreviation();
|
||||
$price->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
$material->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
}
|
||||
|
||||
$price->unsetRelation('rawMaterial');
|
||||
}
|
||||
|
||||
$material->unsetRelation('rawMaterialPrice');
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
if (in_array($sort, ['created_at', 'status'], true)) {
|
||||
|
||||
@ -71,6 +71,8 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
$purchase->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||
$purchase->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||
|
||||
$purchase->items->each(fn (PurchaseItem $item) => $this->breakItemCircularReference($item));
|
||||
|
||||
return $purchase;
|
||||
});
|
||||
}
|
||||
@ -136,11 +138,10 @@ public function findForEdit(Purchase $purchase): Purchase
|
||||
$price = $item->rawMaterialPrice;
|
||||
|
||||
if ($price) {
|
||||
$price->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($price, 'images'),
|
||||
);
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
}
|
||||
|
||||
$this->breakItemCircularReference($item);
|
||||
});
|
||||
|
||||
return $purchase;
|
||||
@ -154,7 +155,12 @@ public function draftItemsForUser(User $user): array
|
||||
'rawMaterialPrice.media',
|
||||
])
|
||||
->get()
|
||||
->map(fn (PurchaseItem $item) => $this->presentDraftItem($item))
|
||||
->map(function (PurchaseItem $item) {
|
||||
$result = $this->presentDraftItem($item);
|
||||
$this->breakItemCircularReference($item);
|
||||
|
||||
return $result;
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
@ -187,7 +193,48 @@ public function syncDraftItem(array $validated, User $user): array
|
||||
'rawMaterialPrice.media',
|
||||
]);
|
||||
|
||||
return $this->presentDraftItem($item);
|
||||
$result = $this->presentDraftItem($item);
|
||||
$this->breakItemCircularReference($item);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function createVariantAndDraft(array $validated, User $user): array
|
||||
{
|
||||
$rawMaterial = RawMaterial::query()->findOrFail($validated['raw_material_id']);
|
||||
|
||||
$price = RawMaterialPrice::create([
|
||||
'raw_material_id' => $rawMaterial->id,
|
||||
'variant' => $validated['variant'],
|
||||
'price' => (int) $validated['price'],
|
||||
'stock' => (float) ($validated['stock'] ?? 0),
|
||||
]);
|
||||
|
||||
if (! empty($validated['photos'])) {
|
||||
$this->mediaService->syncCollection(
|
||||
$price,
|
||||
'images',
|
||||
$validated['photos'],
|
||||
null,
|
||||
5,
|
||||
required: false,
|
||||
errorKey: 'photos',
|
||||
);
|
||||
}
|
||||
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
|
||||
return [
|
||||
'id' => $price->id,
|
||||
'variant' => $price->variant,
|
||||
'price' => $price->price,
|
||||
'price_formatted' => $price->price_formatted,
|
||||
'price_input' => $price->price_input,
|
||||
'stock' => $price->stock,
|
||||
'stock_formatted' => $price->stock_formatted,
|
||||
'stock_input' => $price->stock_input,
|
||||
'images' => $price->images,
|
||||
];
|
||||
}
|
||||
|
||||
public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice): void
|
||||
@ -741,6 +788,25 @@ private function payloadNew(OwnerVerificationRequest $verificationRequest): arra
|
||||
return is_array($payload) ? $payload : [];
|
||||
}
|
||||
|
||||
private function breakItemCircularReference(PurchaseItem $item): void
|
||||
{
|
||||
$price = $item->rawMaterialPrice;
|
||||
|
||||
if ($price) {
|
||||
$rawMaterial = $price->rawMaterial;
|
||||
|
||||
if ($rawMaterial) {
|
||||
$unitAbbreviation = $rawMaterial->unit->abbreviation();
|
||||
$price->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
$item->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
}
|
||||
|
||||
$price->unsetRelation('rawMaterial');
|
||||
}
|
||||
|
||||
$item->unsetRelation('rawMaterialPrice');
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
if (in_array($sort, ['created_at', 'total', 'discount', 'subtotal'], true)) {
|
||||
|
||||
@ -4,9 +4,11 @@
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingMaterial;
|
||||
use App\Models\CuttingResultPrice;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\User;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use Illuminate\Support\Collection;
|
||||
@ -35,6 +37,7 @@ public function getPendingVerificationCuttings(User $user): Collection
|
||||
->get()
|
||||
->each(function (Cutting $cutting): void {
|
||||
$this->appendCostPreview($cutting);
|
||||
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,6 +58,7 @@ public function getPendingApprovalCuttings(User $user): Collection
|
||||
->get()
|
||||
->each(function (Cutting $cutting): void {
|
||||
$this->appendCostPreview($cutting);
|
||||
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||
});
|
||||
}
|
||||
|
||||
@ -223,6 +227,25 @@ public function rejectVerification(
|
||||
}
|
||||
}
|
||||
|
||||
private function breakMaterialCircularReference(CuttingMaterial $material): void
|
||||
{
|
||||
$price = $material->rawMaterialPrice;
|
||||
|
||||
if ($price) {
|
||||
$rawMaterial = $price->rawMaterial;
|
||||
|
||||
if ($rawMaterial) {
|
||||
$unitAbbreviation = $rawMaterial->unit->abbreviation();
|
||||
$price->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
$material->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||
}
|
||||
|
||||
$price->unsetRelation('rawMaterial');
|
||||
}
|
||||
|
||||
$material->unsetRelation('rawMaterialPrice');
|
||||
}
|
||||
|
||||
private function applyProductStockOnVerify(Cutting $cutting): void
|
||||
{
|
||||
foreach ($cutting->results as $result) {
|
||||
|
||||
@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { DecimalInput } from '@/components/form/decimal-input';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||
import { parseRupiah } from '@/lib/rupiah';
|
||||
import { store_new_variant as storeNewVariantRoute } from '@/routes/admin/manage/purchases/draft_items';
|
||||
import { createMediaUploadState } from '@/types/media';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
import type { PurchaseCatalogItem } from '@/types/purchase';
|
||||
import type { RawMaterialPrice } from '@/types/raw-material';
|
||||
|
||||
const props = defineProps<{
|
||||
rawMaterial: PurchaseCatalogItem | null;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
'variant-added': [rawMaterialId: number, price: RawMaterialPrice];
|
||||
}>();
|
||||
|
||||
const variant = ref('');
|
||||
const price = ref('');
|
||||
const stock = ref('0');
|
||||
const media = ref<MediaUploadState>(createMediaUploadState());
|
||||
const loading = ref(false);
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
variant.value = '';
|
||||
price.value = '';
|
||||
stock.value = '0';
|
||||
media.value = createMediaUploadState();
|
||||
}
|
||||
});
|
||||
|
||||
function parseStockValue(value: string): number {
|
||||
const parsed = Number.parseFloat(value.replace(',', '.'));
|
||||
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function getXsrfToken(): string {
|
||||
const match = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='));
|
||||
|
||||
return match ? decodeURIComponent(match.split('=')[1] ?? '') : '';
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!props.rawMaterial) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('raw_material_id', String(props.rawMaterial.id));
|
||||
formData.append('variant', variant.value.trim());
|
||||
formData.append('price', String(Number.parseInt(parseRupiah(price.value), 10) || 0));
|
||||
formData.append('stock', String(parseStockValue(stock.value)));
|
||||
|
||||
media.value.newFiles.forEach((file) => {
|
||||
formData.append('photos[]', file);
|
||||
});
|
||||
|
||||
const response = await fetch(storeNewVariantRoute.url(), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-XSRF-TOKEN': getXsrfToken(),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
const isJson = contentType.includes('application/json');
|
||||
|
||||
if (isJson) {
|
||||
const error = (await response.json().catch(() => ({}))) as {
|
||||
message?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
const firstValidationError = error.errors
|
||||
? Object.values(error.errors).flat()[0]
|
||||
: undefined;
|
||||
|
||||
throw new Error(firstValidationError ?? error.message ?? 'Permintaan gagal diproses.');
|
||||
}
|
||||
|
||||
throw new Error(`Permintaan gagal diproses (HTTP ${response.status}).`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { price: RawMaterialPrice };
|
||||
|
||||
emit('variant-added', props.rawMaterial.id, data.price);
|
||||
open.value = false;
|
||||
toast.success('Varian baru berhasil ditambahkan.');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal menambahkan varian baru.');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tambah Varian Baru</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p v-if="rawMaterial" class="text-sm text-muted-foreground">
|
||||
Bahan baku: <span class="font-medium text-foreground">{{ rawMaterial.name }}</span>
|
||||
</p>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="submit">
|
||||
<Field>
|
||||
<FieldLabel for="new_variant_name" required>Nama Varian</FieldLabel>
|
||||
<Input id="new_variant_name" v-model="variant" type="text" placeholder="Contoh: Premium / 40s"
|
||||
:maxlength="FIELD_LIMITS.variantName" />
|
||||
</Field>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="new_variant_stock" required>Stok</FieldLabel>
|
||||
<DecimalInput id="new_variant_stock" v-model="stock" />
|
||||
<FieldDescription>Stok awal varian ini.</FieldDescription>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="new_variant_price" required>Harga</FieldLabel>
|
||||
<RupiahInput id="new_variant_price" v-model="price" />
|
||||
<FieldDescription>Harga per satuan bahan baku.</FieldDescription>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<MediaDropzone id="new_variant_images" v-model="media" label="Foto Varian" :max-files="5"
|
||||
:required="false" />
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="open = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" :disabled="loading || !variant || !price">
|
||||
{{ loading ? 'Menyimpan...' : 'Tambah Varian' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Minus, Plus, Search } from '@lucide/vue';
|
||||
import { Minus, Plus, Search, PlusCircle } from '@lucide/vue';
|
||||
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -26,6 +26,7 @@ const search = defineModel<string>('search', { required: true });
|
||||
const emit = defineEmits<{
|
||||
'add-to-cart': [rawMaterial: PurchaseCatalogItem, price: PurchaseCatalogPrice];
|
||||
'decrease-qty': [priceId: number];
|
||||
'add-variant': [rawMaterial: PurchaseCatalogItem];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -64,6 +65,19 @@ const emit = defineEmits<{
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<div class="px-3 pt-1 pb-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-full text-xs text-muted-foreground hover:text-primary"
|
||||
@click.stop="emit('add-variant', rawMaterial)"
|
||||
>
|
||||
<PlusCircle class="size-3.5" />
|
||||
Tambah Varian
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
|
||||
Belum ada varian
|
||||
</p>
|
||||
|
||||
@ -12,6 +12,8 @@ import { parseRupiah } from '@/lib/rupiah';
|
||||
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
||||
import type { MediaItem, MediaUploadState } from '@/types/media';
|
||||
import type { PurchaseCartItem, PurchaseCatalogItem, SelectOption } from '@/types/purchase';
|
||||
import type { RawMaterialPrice } from '@/types/raw-material';
|
||||
import PurchasePosAddVariantDialog from './PurchasePosAddVariantDialog.vue';
|
||||
import PurchasePosCartDetailDialog from './PurchasePosCartDetailDialog.vue';
|
||||
import PurchasePosCartSummaryItems from './PurchasePosCartSummaryItems.vue';
|
||||
import PurchasePosCatalogPanel from './PurchasePosCatalogPanel.vue';
|
||||
@ -38,6 +40,8 @@ const props = defineProps<{
|
||||
|
||||
const isCreateMode = computed(() => props.method === 'post');
|
||||
const cartDetailOpen = ref(false);
|
||||
const addVariantOpen = ref(false);
|
||||
const addVariantRawMaterial = ref<PurchaseCatalogItem | null>(null);
|
||||
const photoState = ref<MediaUploadState>(createMediaUploadState());
|
||||
|
||||
const form = useForm({
|
||||
@ -88,6 +92,19 @@ function populateForm() {
|
||||
watch(() => props.initialData, populateForm, { immediate: true });
|
||||
loadDraftItems(props.draftItems ?? []);
|
||||
|
||||
function openAddVariant(rawMaterial: PurchaseCatalogItem) {
|
||||
addVariantRawMaterial.value = rawMaterial;
|
||||
addVariantOpen.value = true;
|
||||
}
|
||||
|
||||
function handleVariantAdded(rawMaterialId: number, newPrice: RawMaterialPrice) {
|
||||
const rawMaterial = props.catalog.find((rm) => rm.id === rawMaterialId);
|
||||
|
||||
if (rawMaterial) {
|
||||
rawMaterial.prices.push(newPrice);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFormData(): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
@ -146,6 +163,7 @@ function submit() {
|
||||
:get-cart-item="getCartItem"
|
||||
@add-to-cart="addToCart"
|
||||
@decrease-qty="decreasePriceQty"
|
||||
@add-variant="openAddVariant"
|
||||
/>
|
||||
|
||||
<Card class="h-fit xl:sticky xl:top-4">
|
||||
@ -194,6 +212,12 @@ function submit() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<PurchasePosAddVariantDialog
|
||||
v-model:open="addVariantOpen"
|
||||
:raw-material="addVariantRawMaterial"
|
||||
@variant-added="handleVariantAdded"
|
||||
/>
|
||||
|
||||
<PurchasePosCartDetailDialog
|
||||
v-model:open="cartDetailOpen"
|
||||
:cart="cart"
|
||||
|
||||
@ -202,5 +202,6 @@ export function usePurchasePosCart(options: {
|
||||
adjustQuantity,
|
||||
syncCartItemQuantity,
|
||||
decreasePriceQty,
|
||||
upsertCartItem,
|
||||
};
|
||||
}
|
||||
|
||||
@ -227,6 +227,10 @@
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('draft_items.store');
|
||||
|
||||
Route::post('draft-items/new-variant', [PurchaseDraftItemController::class, 'storeNewVariant'])
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('draft_items.store_new_variant');
|
||||
|
||||
Route::delete('draft-items/{rawMaterialPrice}', [PurchaseDraftItemController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::PURCHASES_CREATE->value)
|
||||
->name('draft_items.destroy');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user