feat: update StockVerifyRequest and related components to replace 'sampel' and 'hasil_cutting_diluar_sampel' with 'good' and 'reject' for improved clarity in cutting results management

This commit is contained in:
Yoga Pangestu 2026-06-29 08:34:40 +07:00
parent 65054a115d
commit c3c25679be
8 changed files with 207 additions and 72 deletions

View File

@ -26,6 +26,7 @@ public function index(Request $request): Response
return Inertia::render('admin/manage/stocks/Index', [
'pendingCuttings' => $this->stockService->getPendingVerificationCuttings($user),
'pendingApprovalCuttings' => $this->stockService->getPendingApprovalCuttings($user),
]);
}

View File

@ -26,8 +26,8 @@ public function rules(): array
'verification_note' => ['nullable', 'string', 'max:500'],
'results' => ['nullable', 'array'],
'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'],
'results.*.sampel' => ['required_with:results', 'integer', 'min:0'],
'results.*.hasil_cutting_diluar_sampel' => ['required_with:results', 'integer', 'min:0'],
'results.*.good' => ['required_with:results', 'integer', 'min:0'],
'results.*.reject' => ['required_with:results', 'integer', 'min:0'],
'result_prices' => ['nullable', 'array'],
'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
@ -50,8 +50,8 @@ public function attributes(): array
'result_prices.*.prices.*.price' => 'harga jual',
'results' => 'hasil cutting',
'results.*.product_variant_id' => 'varian produk',
'results.*.sampel' => 'sampel',
'results.*.hasil_cutting_diluar_sampel' => 'hasil cutting diluar sampel',
'results.*.good' => 'bagus',
'results.*.reject' => 'reject',
];
}
@ -71,8 +71,8 @@ public function withValidator(Validator $validator): void
$cuttingResults = $cutting->results->keyBy('product_variant_id');
foreach ($this->input('results', []) as $index => $item) {
$variantId = $item['product_variant_id'] ?? 0;
$sampel = (int) ($item['sampel'] ?? 0);
$hasilCuttingDiluarSampel = (int) ($item['hasil_cutting_diluar_sampel'] ?? 0);
$good = (int) ($item['good'] ?? 0);
$reject = (int) ($item['reject'] ?? 0);
$originalResult = $cuttingResults->get($variantId);
if ($originalResult === null) {
@ -81,8 +81,8 @@ public function withValidator(Validator $validator): void
continue;
}
if (($sampel + $hasilCuttingDiluarSampel) !== (int) $originalResult->cutting_result) {
$validator->errors()->add("results.{$index}.sampel", "Total jumlah (sampel + hasil cutting diluar sampel) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs).");
if (($good + $reject) !== (int) $originalResult->cutting_result) {
$validator->errors()->add("results.{$index}.good", "Total jumlah (bagus + reject) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs).");
}
}
}

View File

@ -84,8 +84,8 @@ public function submitVerification(
$cutting->results()
->where('product_variant_id', $item['product_variant_id'])
->update([
'sampel' => $item['sampel'],
'hasil_cutting_diluar_sampel' => $item['hasil_cutting_diluar_sampel'],
'sampel' => $item['good'],
'hasil_cutting_diluar_sampel' => $item['reject'],
]);
}
$cutting->load('results');

View File

@ -1,12 +1,18 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import StockPendingSection from './table/StockPendingSection.vue';
import { useCan } from '@/composables/useCan';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { CuttingListItem } from '@/types/cutting';
import StockPendingApprovalSection from './table/StockPendingApprovalSection.vue';
import StockPendingSection from './table/StockPendingSection.vue';
defineProps<{
pendingCuttings: CuttingListItem[];
pendingApprovalCuttings: CuttingListItem[];
}>();
const { can } = useCan();
const isOwner = can('owner_verifications.verify');
</script>
<template>
@ -21,6 +27,7 @@ defineProps<{
</div>
</div>
<StockPendingSection :cuttings="pendingCuttings" />
<StockPendingApprovalSection v-if="isOwner" :cuttings="pendingApprovalCuttings" />
<StockPendingSection v-else :cuttings="pendingCuttings" />
</AdminLayout>
</template>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { CuttingListItem } from '@/types/cutting';
defineProps<{
cuttings: CuttingListItem[];
}>();
</script>
<template>
<div v-if="cuttings.length" class="space-y-4">
<div v-for="cutting in cuttings" :key="cutting.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="min-w-0 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium leading-tight">
Cutting #{{ cutting.id }}
</h3>
<Badge variant="secondary">Menunggu Persetujuan</Badge>
</div>
<div class="text-muted-foreground text-sm">
<p>{{ cutting.created_at_formatted }}</p>
<p v-if="cutting.submitted_by">
Diajukan oleh {{ cutting.submitted_by.profile?.full_name ?? cutting.submitted_by.username }}
</p>
</div>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>Total Hasil Cutting <strong class="text-primary">{{ cutting.total_result_pieces ??
0 }} pcs</strong></span>
</div>
</div>
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
<OwnerVerificationRowActions type="cutting" :id="cutting.id" />
</div>
</div>
<div class="p-4">
<div class="space-y-2">
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Verifikasi</h4>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead>
<TableHead>Bagus</TableHead>
<TableHead>Reject</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada hasil verifikasi
</TableCell>
</TableRow>
<TableRow v-for="result in cutting.results" :key="result.id">
<TableCell class="font-medium">
{{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.sampel }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.hasil_cutting_diluar_sampel }} pcs
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</div>
</div>
</div>
<div v-else class="space-y-4">
<div class="rounded-md border px-6 py-10 text-center">
<p class="text-muted-foreground text-sm">
Tidak ada verifikasi yang menunggu persetujuan.
</p>
</div>
</div>
</template>

View File

@ -1,5 +1,12 @@
<script setup lang="ts">
import { Card } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { CuttingListItem } from '@/types/cutting';
import StockDataTableActions from './data-table-actions.vue';
@ -10,53 +17,71 @@ defineProps<{
<template>
<div v-if="cuttings.length" class="space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card v-for="cutting in cuttings" :key="cutting.id"
class="flex flex-col justify-between overflow-hidden border bg-card/50 py-0 gap-0">
<div class="border-b bg-muted/20 px-4 py-3 flex items-center justify-between gap-2">
<div class="min-w-0">
<h4 class="font-medium text-sm truncate">
<div v-for="cutting in cuttings" :key="cutting.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="min-w-0 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium leading-tight">
Cutting #{{ cutting.id }}
</h4>
<span class="text-[10px] text-muted-foreground block truncate">
{{ cutting.created_at_formatted }}
</span>
</h3>
</div>
<div class="flex shrink-0 items-center">
<StockDataTableActions :cutting="cutting" />
<div class="text-muted-foreground text-sm">
<p>{{ cutting.created_at_formatted }}</p>
</div>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>Total Hasil Cutting <strong class="text-primary">{{ cutting.total_result_pieces ??
0 }} pcs</strong></span>
</div>
</div>
<div class="p-4 space-y-3 flex-1 text-xs">
<div class="space-y-1">
<span class="font-medium text-foreground">Hasil Produk:</span>
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
<li v-for="res in cutting.results" :key="res.id">
{{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) -
{{ res.cutting_result }} pcs
<span class="text-[10px]">
({{ res.sampel }} sampel, {{ res.hasil_cutting_diluar_sampel }} diluar sampel)
</span>
</li>
<li v-if="!cutting.results.length">Belum ada hasil produk</li>
</ul>
</div>
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
<StockDataTableActions :cutting="cutting" />
</div>
</div>
<div v-if="cutting.results.length" class="pt-2 border-t">
<div class="flex items-center justify-between text-[11px]">
<span class="text-muted-foreground">Total Hasil Cutting:</span>
<span class="font-semibold tabular-nums">{{ cutting.total_result_pieces ?? 0 }} pcs</span>
</div>
<div class="p-4">
<div class="space-y-2">
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead>
<TableHead>Sampel</TableHead>
<TableHead>Diluar Sampel</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada hasil produk
</TableCell>
</TableRow>
<TableRow v-for="result in cutting.results" :key="result.id">
<TableCell class="font-medium">
{{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.sampel }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.hasil_cutting_diluar_sampel }} pcs
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</Card>
</div>
</div>
</div>
<div v-else class="space-y-4">
<h3 class="text-lg font-semibold">
Menunggu Verifikasi
</h3>
<div class="rounded-md border px-6 py-10 text-center">
<p class="text-muted-foreground text-sm">
Tidak ada cutting yang menunggu verifikasi.

View File

@ -53,10 +53,10 @@ const verifyForm = useForm({
product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result,
sampel: res.sampel,
hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_sampel: res.sampel,
original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_outside_sample: res.hasil_cutting_diluar_sampel,
good: res.cutting_result,
reject: 0,
})),
variant_prices: [] as VariantPriceRow[],
shared_prices: buildEmptyPrices(),
@ -70,10 +70,10 @@ watch(open, (isOpen) => {
product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result,
sampel: res.sampel,
hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_sampel: res.sampel,
original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_outside_sample: res.hasil_cutting_diluar_sampel,
good: res.cutting_result,
reject: 0,
}));
verifyForm.variant_prices = props.cutting.results.map(res => ({
product_variant_id: res.product_variant?.id || 0,
@ -92,8 +92,8 @@ watch(allMatches, (matches) => {
verifyForm.results = verifyForm.results.map((result) => ({
...result,
sampel: result.original_sampel,
hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
good: result.cutting_result,
reject: 0,
}));
});
@ -103,8 +103,8 @@ function setAllMatches(value: boolean) {
if (value) {
verifyForm.results = verifyForm.results.map((result) => ({
...result,
sampel: result.original_sampel,
hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
good: result.cutting_result,
reject: 0,
}));
}
}
@ -137,10 +137,10 @@ function submitVerify() {
verifyForm
.transform((data) => ({
verification_note: data.verification_note,
results: data.results.map(({ product_variant_id, sampel, hasil_cutting_diluar_sampel }) => ({
results: data.results.map(({ product_variant_id, good, reject }) => ({
product_variant_id,
sampel,
hasil_cutting_diluar_sampel,
good,
reject,
})),
result_prices: buildResultPricesPayload(),
}))
@ -187,25 +187,25 @@ function submitVerify() {
<p class="text-xs text-muted-foreground">
{{ result.cutting_result }} pcs
<span class="text-[10px]">({{ result.original_sampel }} sampel, {{
result.original_hasil_cutting_diluar_sampel }} diluar sampel)</span>
result.original_outside_sample }} outside sample)</span>
</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<div class="text-center">
<label :for="`stock-sampel-${index}`"
class="text-[10px] text-muted-foreground block">Sampel</label>
<Input :id="`stock-sampel-${index}`" type="number"
v-model.number="result.sampel" min="0" :max="result.cutting_result"
<label :for="`stock-good-${index}`"
class="text-[10px] text-muted-foreground block">Bagus</label>
<Input :id="`stock-good-${index}`" type="number"
v-model.number="result.good" min="0" :max="result.cutting_result"
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
@input="result.hasil_cutting_diluar_sampel = result.cutting_result - result.sampel" />
@input="result.reject = result.cutting_result - result.good" />
</div>
<div class="text-center">
<label :for="`stock-diluar-sampel-${index}`"
class="text-[10px] text-muted-foreground block">Diluar Sampel</label>
<Input :id="`stock-diluar-sampel-${index}`" type="number"
v-model.number="result.hasil_cutting_diluar_sampel" min="0" :max="result.cutting_result"
<label :for="`stock-reject-${index}`"
class="text-[10px] text-muted-foreground block">Reject</label>
<Input :id="`stock-reject-${index}`" type="number"
v-model.number="result.reject" min="0" :max="result.cutting_result"
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
@input="result.sampel = result.cutting_result - result.hasil_cutting_diluar_sampel" />
@input="result.good = result.cutting_result - result.reject" />
</div>
</div>
</div>

View File

@ -84,6 +84,12 @@ export type CuttingListItem = {
full_name: string;
};
};
submitted_by?: {
username: string;
profile?: {
full_name: string;
};
};
rejection?: {
reason: string;
rejected_by?: {