feat: enhance RawMaterialController and RawMaterialService to support owner verification permissions; add RawMaterialVariantEditModal for editing price variants and implement JSON response for raw material details
This commit is contained in:
parent
5b9c294bf2
commit
8c3847ade3
@ -274,7 +274,6 @@ public function permissions(): array
|
||||
Permission::RAW_MATERIALS_VIEW,
|
||||
Permission::RAW_MATERIALS_CREATE,
|
||||
Permission::RAW_MATERIALS_UPDATE,
|
||||
Permission::RAW_MATERIALS_DELETE,
|
||||
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
||||
|
||||
Permission::OWNER_VERIFICATIONS_VIEW,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
@ -10,6 +11,8 @@
|
||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Services\Master\RawMaterialService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -52,7 +55,11 @@ public function store(RawMaterialRequest $request): RedirectResponse
|
||||
{
|
||||
$this->rawMaterialService->create($request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
||||
$this->flashCreated('Bahan baku');
|
||||
} else {
|
||||
$this->flashSuccess('Bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.master.raw_materials.index');
|
||||
}
|
||||
@ -69,7 +76,11 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
|
||||
{
|
||||
$this->rawMaterialService->update($rawMaterial, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
||||
$this->flashUpdated('Bahan baku');
|
||||
} else {
|
||||
$this->flashSuccess('Perubahan bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.master.raw_materials.index');
|
||||
}
|
||||
@ -78,7 +89,11 @@ public function destroy(Request $request, RawMaterial $rawMaterial): RedirectRes
|
||||
{
|
||||
$this->rawMaterialService->delete($rawMaterial, $request->user());
|
||||
|
||||
$this->flashSuccess('Penghapusan bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
||||
$this->flashDeleted('Bahan baku');
|
||||
} else {
|
||||
$this->flashSuccess('Penghapusan bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.master.raw_materials.index');
|
||||
}
|
||||
@ -87,8 +102,27 @@ public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMater
|
||||
{
|
||||
$this->rawMaterialService->toggleStatus($rawMaterial, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
||||
$this->flashStatusUpdated('bahan baku');
|
||||
} else {
|
||||
$this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function show(RawMaterial $rawMaterial): JsonResponse
|
||||
{
|
||||
$rawMaterial->load([
|
||||
'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
|
||||
]);
|
||||
|
||||
$rawMaterial->prices->each(function ($price) use ($rawMaterial): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
|
||||
$price->unsetRelation('rawMaterial');
|
||||
});
|
||||
|
||||
return response()->json($rawMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,13 +208,49 @@ function () use ($validated, $rawMaterial, $user, $isOwner): void {
|
||||
}
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
$user,
|
||||
'Ubah Bahan Baku',
|
||||
"Pengajuan ubah bahan baku '{$rawMaterial->name}' menunggu verifikasi owner.",
|
||||
route('admin.master.raw_materials.index', ['search' => $rawMaterial->name]),
|
||||
$rawMaterial->name,
|
||||
);
|
||||
$changedVariants = [];
|
||||
foreach ($validated['prices'] as $priceData) {
|
||||
if (! empty($priceData['id'])) {
|
||||
$originalPrice = $rawMaterial->prices->firstWhere('id', $priceData['id']);
|
||||
if ($originalPrice) {
|
||||
$isChanged = false;
|
||||
if ($originalPrice->variant !== $priceData['variant']) {
|
||||
$isChanged = true;
|
||||
}
|
||||
if ($originalPrice->price !== (int) $priceData['price']) {
|
||||
$isChanged = true;
|
||||
}
|
||||
if (rtrim(rtrim(number_format((float) $originalPrice->stock, 4, '.', ''), '0'), '.') !== rtrim(rtrim(number_format((float) $priceData['stock'], 4, '.', ''), '0'), '.')) {
|
||||
$isChanged = true;
|
||||
}
|
||||
|
||||
if ($isChanged) {
|
||||
$changedVariants[] = $priceData['variant'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$changedVariants[] = $priceData['variant'];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($changedVariants)) {
|
||||
$variantsStr = implode(', ', $changedVariants);
|
||||
$this->notifyForPendingRequest(
|
||||
$user,
|
||||
'Ubah Varian Bahan Baku',
|
||||
"Pengajuan ubah varian '{$variantsStr}' pada bahan baku '{$rawMaterial->name}' menunggu verifikasi owner.",
|
||||
route('admin.master.raw_materials.index', ['search' => $rawMaterial->name]),
|
||||
$rawMaterial->name,
|
||||
);
|
||||
} else {
|
||||
$this->notifyForPendingRequest(
|
||||
$user,
|
||||
'Ubah Bahan Baku',
|
||||
"Pengajuan ubah bahan baku '{$rawMaterial->name}' menunggu verifikasi owner.",
|
||||
route('admin.master.raw_materials.index', ['search' => $rawMaterial->name]),
|
||||
$rawMaterial->name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { computed } 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 { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Field, FieldError, FieldLabel, FieldGroup, FieldSet } from '@/components/ui/field';
|
||||
import { useFormDialog } from '@/composables/useFormDialog';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { formErrors } from '@/lib/form';
|
||||
import { parseRupiah } from '@/lib/rupiah';
|
||||
import { update } from '@/routes/admin/master/raw_materials';
|
||||
import { createMediaUploadState, appendMediaToFormData } from '@/types/media';
|
||||
import type { RawMaterialListItem, RawMaterialPrice } from '@/types/raw-material';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
price: RawMaterialPrice | null;
|
||||
material: RawMaterialListItem | null;
|
||||
}>();
|
||||
|
||||
const editForm = useForm({
|
||||
name: '',
|
||||
unit: '',
|
||||
prices: [] as any[],
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
editForm.reset();
|
||||
editForm.clearErrors();
|
||||
}
|
||||
|
||||
function populateForm(price: RawMaterialPrice | null) {
|
||||
resetForm();
|
||||
|
||||
if (!price || !props.material) {
|
||||
return;
|
||||
}
|
||||
|
||||
editForm.name = props.material.name;
|
||||
editForm.unit = props.material.unit;
|
||||
editForm.prices = (props.material.prices ?? []).map((p) => ({
|
||||
client_id: `price-${crypto.randomUUID()}`,
|
||||
id: p.id,
|
||||
variant: p.variant,
|
||||
price: p.price_input,
|
||||
stock: p.stock_input,
|
||||
media: createMediaUploadState(p.images ?? []),
|
||||
}));
|
||||
}
|
||||
|
||||
useFormDialog({
|
||||
open,
|
||||
source: () => props.price,
|
||||
populate: populateForm,
|
||||
reset: resetForm,
|
||||
});
|
||||
|
||||
const editingPriceIndex = computed(() => {
|
||||
if (!props.price || !editForm.prices.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return editForm.prices.findIndex((p) => p.id === props.price!.id);
|
||||
});
|
||||
|
||||
const editingPriceFormItem = computed(() => {
|
||||
const idx = editingPriceIndex.value;
|
||||
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return editForm.prices[idx];
|
||||
});
|
||||
|
||||
function parseStockValue(value: string): number {
|
||||
const parsed = Number.parseFloat(value.replace(',', '.'));
|
||||
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
const isUploading = computed(() => {
|
||||
return editForm.prices.some((p) => p.media?.pendingUploads > 0);
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (!props.material) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_method', 'PUT');
|
||||
formData.append('name', editForm.name.trim());
|
||||
formData.append('unit', editForm.unit);
|
||||
|
||||
editForm.prices.forEach((price, index) => {
|
||||
formData.append(`prices[${index}][id]`, String(price.id));
|
||||
formData.append(`prices[${index}][variant]`, price.variant.trim());
|
||||
formData.append(`prices[${index}][price]`, String(Number.parseInt(parseRupiah(price.price), 10) || 0));
|
||||
formData.append(`prices[${index}][stock]`, String(parseStockValue(price.stock)));
|
||||
appendMediaToFormData(formData, `prices[${index}]`, price.media);
|
||||
});
|
||||
|
||||
editForm.transform(() => formData).post(update.url(props.material.id), {
|
||||
forceFormData: true,
|
||||
onSuccess: () => {
|
||||
open.value = false;
|
||||
},
|
||||
onError: (errors) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ubah Varian Bahan Baku</DialogTitle>
|
||||
<DialogDescription>
|
||||
Mengubah properti varian untuk bahan baku <strong>{{ material?.name }}</strong>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form v-if="editingPriceFormItem" @submit.prevent="submit">
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="variant-name" required>Nama Varian</FieldLabel>
|
||||
<Input id="variant-name" v-model="editingPriceFormItem.variant" type="text"
|
||||
placeholder="Masukkan nama varian" />
|
||||
<FieldError :errors="formErrors(editForm, `prices.${editingPriceIndex}.variant`)" />
|
||||
</Field>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="variant-stock" required>Stok</FieldLabel>
|
||||
<DecimalInput id="variant-stock" v-model="editingPriceFormItem.stock" />
|
||||
<FieldError :errors="formErrors(editForm, `prices.${editingPriceIndex}.stock`)" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="variant-price" required>Harga</FieldLabel>
|
||||
<RupiahInput id="variant-price" v-model="editingPriceFormItem.price" />
|
||||
<FieldError :errors="formErrors(editForm, `prices.${editingPriceIndex}.price`)" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<MediaDropzone :id="`price_images_${editingPriceFormItem.client_id}`"
|
||||
v-model="editingPriceFormItem.media" label="Foto Varian" :max-files="5" required
|
||||
:errors="formErrors(editForm, `prices.${editingPriceIndex}.images`)" />
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
|
||||
<DialogFooter class="mt-6">
|
||||
<Button type="button" variant="outline" :disabled="editForm.processing" @click="open = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" :disabled="editForm.processing || isUploading">
|
||||
<Save class="size-4" />
|
||||
{{ isUploading ? 'Mengunggah...' : editForm.processing ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { RowEditAction } from '@/components/button';
|
||||
import { DataTableEmpty } from '@/components/data-table';
|
||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||
@ -21,7 +22,8 @@ import type {
|
||||
DataTablePagination,
|
||||
DataTablePaginationLink,
|
||||
} from '@/types/data-table';
|
||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||
import type { RawMaterialListItem, RawMaterialPrice } from '@/types/raw-material';
|
||||
import RawMaterialVariantEditModal from '../form/RawMaterialVariantEditModal.vue';
|
||||
import DataTableActions from './data-table-actions.vue';
|
||||
import RawMaterialStatusToggle from './raw-material-status-toggle.vue';
|
||||
|
||||
@ -68,6 +70,17 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
verificationModalOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Modal Edit Varian State ─────────────────────────────────────────────────
|
||||
const isEditing = ref(false);
|
||||
const editingMaterial = ref<RawMaterialListItem | null>(null);
|
||||
const editingPrice = ref<RawMaterialPrice | null>(null);
|
||||
|
||||
function openEditModal(price: RawMaterialPrice, material: RawMaterialListItem) {
|
||||
editingMaterial.value = material;
|
||||
editingPrice.value = price;
|
||||
isEditing.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -77,92 +90,100 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
|
||||
<div class="relative min-h-[100px]">
|
||||
<!-- Subtle loading bar — tidak ganggu layout -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute top-0 left-0 right-0 h-0.5 z-10 overflow-hidden bg-primary/10"
|
||||
>
|
||||
<div v-if="loading" class="absolute top-0 left-0 right-0 h-0.5 z-10 overflow-hidden bg-primary/10">
|
||||
<div class="h-full w-1/3 animate-[loading-bar_1.2s_ease-in-out_infinite] bg-primary rounded-full" />
|
||||
</div>
|
||||
|
||||
<div v-if="materials.length" class="space-y-4">
|
||||
<div v-for="(material, index) in materials" :key="material.id" class="overflow-hidden rounded-md border">
|
||||
<div
|
||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span class="text-muted-foreground w-8 shrink-0 pt-0.5 text-center text-sm tabular-nums">
|
||||
{{ rowNumber(index) }}
|
||||
</span>
|
||||
<div class="min-w-0 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="font-medium leading-tight">
|
||||
{{ material.name }}
|
||||
</h3>
|
||||
<Badge variant="secondary">
|
||||
{{ material.unit_label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p v-if="material.has_pending_request" class="text-sm text-amber-600">
|
||||
{{ material.pending_request_submitted_by_name }} mengajukan {{ material.pending_request_action_label?.toLowerCase() }} bahan baku ini —
|
||||
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(material.pending_request_id)">lihat</button>
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||
<span>
|
||||
Total stok <strong class="text-primary">
|
||||
{{ material.total_stock_formatted }}
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
Total harga <strong class="text-primary">
|
||||
{{ material.total_inventory_value_formatted }}
|
||||
</strong>
|
||||
</span>
|
||||
<div v-for="(material, index) in materials" :key="material.id"
|
||||
class="overflow-hidden rounded-md border bg-background">
|
||||
<div
|
||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span class="text-muted-foreground w-8 shrink-0 pt-0.5 text-center text-sm tabular-nums">
|
||||
{{ rowNumber(index) }}
|
||||
</span>
|
||||
<div class="min-w-0 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="font-semibold leading-tight text-foreground text-base">
|
||||
{{ material.name }}
|
||||
</h3>
|
||||
<Badge variant="secondary">
|
||||
{{ material.unit_label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p v-if="material.has_pending_request" class="text-sm text-amber-600">
|
||||
{{ material.pending_request_submitted_by_name }} mengajukan {{
|
||||
material.pending_request_action_label?.toLowerCase() }} bahan baku ini —
|
||||
<button type="button" class="underline-offset-2 hover:underline font-semibold"
|
||||
@click="openVerificationDetail(material.pending_request_id)">lihat</button>
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||
<span>
|
||||
Total stok <strong class="text-primary font-semibold">
|
||||
{{ material.total_stock_formatted }}
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
Total harga <strong class="text-primary font-semibold">
|
||||
{{ material.total_inventory_value_formatted }}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
|
||||
<RawMaterialStatusToggle :material="material" />
|
||||
<DataTableActions :material="material" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
|
||||
<RawMaterialStatusToggle :material="material" />
|
||||
<DataTableActions :material="material" />
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-10">#</TableHead>
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Foto</TableHead>
|
||||
<TableHead>Stok</TableHead>
|
||||
<TableHead>Harga per Satuan</TableHead>
|
||||
<TableHead class="w-24 text-center">Aksi</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!material.prices.length" :key="`${material.id}-empty`">
|
||||
<TableCell colspan="6" class="text-muted-foreground text-center">
|
||||
Belum ada varian
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="(price, priceIndex) in material.prices" :key="price.id">
|
||||
<TableCell class="text-muted-foreground tabular-nums text-center">
|
||||
{{ priceIndex + 1 }}
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">
|
||||
{{ price.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1"
|
||||
:title="`${material.name} - ${price.variant}`"
|
||||
:all-urls="allVariantImageUrls(material)"
|
||||
:all-titles="allVariantImageTitles(material)" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.stock_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.price_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<RowEditAction :disabled="material.has_pending_request" tooltip="Ubah Varian"
|
||||
@click="openEditModal(price, material)" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-10">#</TableHead>
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Foto</TableHead>
|
||||
<TableHead>Stok</TableHead>
|
||||
<TableHead>Harga per Satuan</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!material.prices.length" :key="`${material.id}-empty`">
|
||||
<TableCell colspan="5" class="text-muted-foreground">
|
||||
Belum ada varian
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="(price, priceIndex) in material.prices" :key="price.id">
|
||||
<TableCell class="text-muted-foreground tabular-nums text-center">
|
||||
{{ priceIndex + 1 }}
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">
|
||||
{{ price.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" :title="`${material.name} - ${price.variant}`" :all-urls="allVariantImageUrls(material)" :all-titles="allVariantImageTitles(material)" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.stock_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.price_formatted }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTableEmpty v-else />
|
||||
</div>
|
||||
@ -170,5 +191,12 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||
|
||||
<VerificationDetailModal v-model:open="verificationModalOpen" :request-id="selectedRequestId" />
|
||||
|
||||
<!-- Dialog Modal Edit Varian -->
|
||||
<RawMaterialVariantEditModal
|
||||
v-model:open="isEditing"
|
||||
:price="editingPrice"
|
||||
:material="editingMaterial"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -176,6 +176,7 @@
|
||||
])
|
||||
->name('destroy');
|
||||
|
||||
Route::get('{rawMaterial}', [RawMaterialController::class, 'show'])->name('show');
|
||||
Route::get('/', [RawMaterialController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
|
||||
@ -738,8 +738,8 @@ function createRawMaterialVerifierUser(): User
|
||||
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10.5]);
|
||||
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 5.25]);
|
||||
|
||||
expect($rawMaterial->fresh()->total_stock_formatted)->toContain('15,75');
|
||||
expect($rawMaterial->fresh()->total_stock_formatted)->toContain('m');
|
||||
expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('15,75');
|
||||
expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('m');
|
||||
});
|
||||
});
|
||||
|
||||
@ -907,3 +907,56 @@ function createRawMaterialVerifierUser(): User
|
||||
expect($price->fresh()->variant)->toBe($originalVariant);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Show JSON ────────────────────────────────────────────
|
||||
|
||||
describe('Raw Material Show JSON', function () {
|
||||
test('authenticated user with permission can get raw material details in JSON', function () {
|
||||
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
||||
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(route('admin.master.raw_materials.show', $rawMaterial))
|
||||
->assertOk();
|
||||
|
||||
$response->assertJsonStructure([
|
||||
'id',
|
||||
'name',
|
||||
'unit',
|
||||
'prices' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'variant',
|
||||
'price',
|
||||
'price_formatted',
|
||||
'price_input',
|
||||
'stock_formatted',
|
||||
'stock_input',
|
||||
'images',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $response->json();
|
||||
expect($data['id'])->toBe($rawMaterial->id);
|
||||
expect($data['prices'])->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('guest cannot get raw material details in JSON', function () {
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
|
||||
$this->get(route('admin.master.raw_materials.show', $rawMaterial))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without view permission cannot get raw material details in JSON', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.master.raw_materials.show', $rawMaterial))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user