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:
parent
65054a115d
commit
c3c25679be
@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -84,6 +84,12 @@ export type CuttingListItem = {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
submitted_by?: {
|
||||
username: string;
|
||||
profile?: {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
rejection?: {
|
||||
reason: string;
|
||||
rejected_by?: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user