feat: update CuttingVerificationSection and ProductGroupedTable components for improved functionality and UI consistency
This commit is contained in:
parent
45f28e69ef
commit
7d39f85da7
@ -4,6 +4,7 @@ import { Check, ChevronDown, Copy, RotateCcw, Scissors, X } from '@lucide/vue';
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -41,15 +42,14 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
import {
|
import {
|
||||||
PRICE_TYPES,
|
PRICE_TYPES,
|
||||||
PRICE_TYPE_LABELS,
|
PRICE_TYPE_LABELS,
|
||||||
} from '@/types/product';
|
} from '@/types/product';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cuttings: CuttingListItem[];
|
cuttings: CuttingListItem[];
|
||||||
@ -172,6 +172,7 @@ function toggleUseSamePrice(checked: boolean) {
|
|||||||
|
|
||||||
function setResultPrice(resultIndex: number, type: string, value: string) {
|
function setResultPrice(resultIndex: number, type: string, value: string) {
|
||||||
const result = verifyForm.results[resultIndex];
|
const result = verifyForm.results[resultIndex];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -184,6 +185,7 @@ function setResultPrice(resultIndex: number, type: string, value: string) {
|
|||||||
|
|
||||||
function applyPriceToAllVariants(sourceIndex: number) {
|
function applyPriceToAllVariants(sourceIndex: number) {
|
||||||
const source = verifyForm.results[sourceIndex];
|
const source = verifyForm.results[sourceIndex];
|
||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -209,6 +211,7 @@ function openAction(cutting: CuttingListItem, action: CuttingStatusAction) {
|
|||||||
|
|
||||||
if (action.status === 'verified') {
|
if (action.status === 'verified') {
|
||||||
verifyDialogOpen.value = true;
|
verifyDialogOpen.value = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,6 +219,7 @@ function openAction(cutting: CuttingListItem, action: CuttingStatusAction) {
|
|||||||
rejectForm.reset();
|
rejectForm.reset();
|
||||||
rejectForm.clearErrors();
|
rejectForm.clearErrors();
|
||||||
rejectDialogOpen.value = true;
|
rejectDialogOpen.value = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,21 +291,21 @@ function submitVerify() {
|
|||||||
result_prices: buildResultPricesPayload(),
|
result_prices: buildResultPricesPayload(),
|
||||||
}))
|
}))
|
||||||
.post(`/admin/manage/cuttings/${activeCutting.value.id}/status`, {
|
.post(`/admin/manage/cuttings/${activeCutting.value.id}/status`, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
verifyDialogOpen.value = false;
|
verifyDialogOpen.value = false;
|
||||||
activeCutting.value = null;
|
activeCutting.value = null;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const message = Object.values(errors)[0];
|
const message = Object.values(errors)[0];
|
||||||
|
|
||||||
toast.error(
|
toast.error(
|
||||||
typeof message === 'string'
|
typeof message === 'string'
|
||||||
? message
|
? message
|
||||||
: 'Gagal memverifikasi cutting.',
|
: 'Gagal memverifikasi cutting.',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitReject() {
|
function submitReject() {
|
||||||
@ -344,15 +348,13 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Scissors class="size-4 shrink-0 text-green-600 dark:text-green-400" />
|
<Scissors class="size-4 shrink-0 text-green-600 dark:text-green-400" />
|
||||||
<h3 class="font-semibold leading-tight">
|
<h3 class="font-semibold leading-tight">
|
||||||
Cutting Menunggu Verifikasi
|
Verifikasi Cutting
|
||||||
</h3>
|
</h3>
|
||||||
<Badge v-if="totalCompleted > 0" variant="outline" class="border-green-600/30 text-green-600 dark:text-green-400">
|
<Badge v-if="totalCompleted > 0" variant="outline"
|
||||||
|
class="border-green-600/30 text-green-600 dark:text-green-400">
|
||||||
{{ totalCompleted }}
|
{{ totalCompleted }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
Daftar cutting yang telah selesai dikerjakan. Verifikasi untuk menambahkan stok ke produk.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChevronDown class="size-4 shrink-0 text-muted-foreground transition-transform duration-200"
|
<ChevronDown class="size-4 shrink-0 text-muted-foreground transition-transform duration-200"
|
||||||
@ -362,7 +364,8 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent class="pt-0 pb-6">
|
<CardContent class="pt-0 pb-6">
|
||||||
<div v-if="cuttings.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div v-if="cuttings.length" 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">
|
<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="border-b bg-muted/20 px-4 py-3 flex items-center justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h4 class="font-medium text-sm truncate">
|
<h4 class="font-medium text-sm truncate">
|
||||||
@ -375,11 +378,8 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-1">
|
<div class="flex shrink-0 flex-wrap items-center justify-end gap-1">
|
||||||
<Tooltip v-if="can('cuttings.verify')">
|
<Tooltip v-if="can('cuttings.verify')">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button size="sm" variant="ghost"
|
||||||
size="sm"
|
@click="openAction(cutting, { status: 'verified', label: 'Verifikasi', destructive: false, permission: 'cuttings.verify', icon_only: false })">
|
||||||
variant="ghost"
|
|
||||||
@click="openAction(cutting, { status: 'verified', label: 'Verifikasi', destructive: false, permission: 'cuttings.verify', icon_only: false })"
|
|
||||||
>
|
|
||||||
<Check class="size-3.5" />
|
<Check class="size-3.5" />
|
||||||
<span class="sr-only">Verifikasi</span>
|
<span class="sr-only">Verifikasi</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -389,12 +389,9 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
|
|
||||||
<Tooltip v-if="can('cuttings.reject')">
|
<Tooltip v-if="can('cuttings.reject')">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button size="sm" variant="ghost"
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-destructive hover:text-destructive"
|
class="text-destructive hover:text-destructive"
|
||||||
@click="openAction(cutting, { status: 'rejected', label: 'Tolak', destructive: true, permission: 'cuttings.reject', icon_only: false })"
|
@click="openAction(cutting, { status: 'rejected', label: 'Tolak', destructive: true, permission: 'cuttings.reject', icon_only: false })">
|
||||||
>
|
|
||||||
<X class="size-3.5" />
|
<X class="size-3.5" />
|
||||||
<span class="sr-only">Tolak</span>
|
<span class="sr-only">Tolak</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -404,12 +401,9 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
|
|
||||||
<Tooltip v-if="can('cuttings.reject')">
|
<Tooltip v-if="can('cuttings.reject')">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button size="sm" variant="ghost"
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-destructive hover:text-destructive"
|
class="text-destructive hover:text-destructive"
|
||||||
@click="openAction(cutting, { status: 'in_progress', label: 'Kembalikan ke Proses', destructive: true, permission: 'cuttings.reject', icon_only: false })"
|
@click="openAction(cutting, { status: 'in_progress', label: 'Kembalikan ke Proses', destructive: true, permission: 'cuttings.reject', icon_only: false })">
|
||||||
>
|
|
||||||
<RotateCcw class="size-3.5" />
|
<RotateCcw class="size-3.5" />
|
||||||
<span class="sr-only">Kembalikan ke Proses</span>
|
<span class="sr-only">Kembalikan ke Proses</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -428,7 +422,8 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<span class="font-medium text-foreground">Bahan Baku:</span>
|
<span class="font-medium text-foreground">Bahan Baku:</span>
|
||||||
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="mat in cutting.materials" :key="mat.id">
|
<li v-for="mat in cutting.materials" :key="mat.id">
|
||||||
{{ mat.raw_material_price?.raw_material?.name }} ({{ mat.raw_material_price?.variant }}) - {{ mat.material_usage_formatted }}
|
{{ mat.raw_material_price?.raw_material?.name }} ({{
|
||||||
|
mat.raw_material_price?.variant }}) - {{ mat.material_usage_formatted }}
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!cutting.materials.length">Belum ada bahan baku</li>
|
<li v-if="!cutting.materials.length">Belum ada bahan baku</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -438,7 +433,8 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<span class="font-medium text-foreground">Hasil Produk:</span>
|
<span class="font-medium text-foreground">Hasil Produk:</span>
|
||||||
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="res in cutting.results" :key="res.id">
|
<li v-for="res in cutting.results" :key="res.id">
|
||||||
{{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) - {{ res.cutting_result }} pcs
|
{{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) -
|
||||||
|
{{ res.cutting_result }} pcs
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!cutting.results.length">Belum ada hasil produk</li>
|
<li v-if="!cutting.results.length">Belum ada hasil produk</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -446,10 +442,12 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
|
|
||||||
<div v-if="cutting.estimated_cost_per_unit_formatted" class="text-[11px] pt-2 border-t">
|
<div v-if="cutting.estimated_cost_per_unit_formatted" class="text-[11px] pt-2 border-t">
|
||||||
<span class="text-muted-foreground">Harga modal: </span>
|
<span class="text-muted-foreground">Harga modal: </span>
|
||||||
<span class="font-semibold tabular-nums">{{ cutting.estimated_cost_per_unit_formatted }} / pcs</span>
|
<span class="font-semibold tabular-nums">{{
|
||||||
|
cutting.estimated_cost_per_unit_formatted }} / pcs</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-[11px] text-muted-foreground pt-2 border-t flex items-center justify-between">
|
<div
|
||||||
|
class="text-[11px] text-muted-foreground pt-2 border-t flex items-center justify-between">
|
||||||
<span>Pembuat:</span>
|
<span>Pembuat:</span>
|
||||||
<span class="font-medium text-foreground">
|
<span class="font-medium text-foreground">
|
||||||
{{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}
|
{{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}
|
||||||
@ -473,22 +471,12 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<!-- Status Confirm Dialog (Return to Process) -->
|
<!-- Status Confirm Dialog (Return to Process) -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-model:open="statusConfirmOpen" :title="pendingAction
|
||||||
v-model:open="statusConfirmOpen"
|
? `${pendingAction.label} cutting?`
|
||||||
:title="
|
: 'Ubah status cutting?'
|
||||||
pendingAction
|
" :description="pendingAction ? statusConfirmDescription(pendingAction) : ''
|
||||||
? `${pendingAction.label} cutting?`
|
" :confirm-label="pendingAction?.label ?? 'Konfirmasi'" cancel-label="Batal"
|
||||||
: 'Ubah status cutting?'
|
:destructive="pendingAction?.destructive ?? false" :loading="statusProcessing" @confirm="transitionStatus" />
|
||||||
"
|
|
||||||
:description="
|
|
||||||
pendingAction ? statusConfirmDescription(pendingAction) : ''
|
|
||||||
"
|
|
||||||
:confirm-label="pendingAction?.label ?? 'Konfirmasi'"
|
|
||||||
cancel-label="Batal"
|
|
||||||
:destructive="pendingAction?.destructive ?? false"
|
|
||||||
:loading="statusProcessing"
|
|
||||||
@confirm="transitionStatus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Reject Dialog -->
|
<!-- Reject Dialog -->
|
||||||
<Dialog v-model:open="rejectDialogOpen">
|
<Dialog v-model:open="rejectDialogOpen">
|
||||||
@ -501,42 +489,24 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="cutting-reject-reason" required
|
<FieldLabel for="cutting-reject-reason" required>Alasan Penolakan</FieldLabel>
|
||||||
>Alasan Penolakan</FieldLabel
|
<Textarea id="cutting-reject-reason" v-model="rejectForm.reason"
|
||||||
>
|
placeholder="Contoh: Jumlah barang yang diterima tidak sesuai" rows="3" autofocus
|
||||||
<Textarea
|
:maxlength="FIELD_LIMITS.reason" />
|
||||||
id="cutting-reject-reason"
|
<FieldError :errors="rejectForm.errors.reason
|
||||||
v-model="rejectForm.reason"
|
? [rejectForm.errors.reason]
|
||||||
placeholder="Contoh: Jumlah barang yang diterima tidak sesuai"
|
: []
|
||||||
rows="3"
|
" />
|
||||||
autofocus
|
|
||||||
:maxlength="FIELD_LIMITS.reason"
|
|
||||||
/>
|
|
||||||
<FieldError
|
|
||||||
:errors="
|
|
||||||
rejectForm.errors.reason
|
|
||||||
? [rejectForm.errors.reason]
|
|
||||||
: []
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
<DialogFooter class="mt-6">
|
<DialogFooter class="mt-6">
|
||||||
<Button
|
<Button type="button" variant="outline" :disabled="rejectForm.processing"
|
||||||
type="button"
|
@click="rejectDialogOpen = false">
|
||||||
variant="outline"
|
|
||||||
:disabled="rejectForm.processing"
|
|
||||||
@click="rejectDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" variant="destructive" :disabled="rejectForm.processing">
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
:disabled="rejectForm.processing"
|
|
||||||
>
|
|
||||||
{{ rejectForm.processing ? 'Menyimpan...' : 'Tolak' }}
|
{{ rejectForm.processing ? 'Menyimpan...' : 'Tolak' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -561,17 +531,11 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
<Label for="all-matches-verif" class="text-sm font-medium">
|
<Label for="all-matches-verif" class="text-sm font-medium">
|
||||||
Semua sesuai dengan data cutting
|
Semua sesuai dengan data cutting
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch id="all-matches-verif" :model-value="allMatches" @update:model-value="setAllMatches" />
|
||||||
id="all-matches-verif"
|
|
||||||
:model-value="allMatches"
|
|
||||||
@update:model-value="setAllMatches"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="activeCutting?.estimated_cost_per_unit_formatted"
|
||||||
v-if="activeCutting?.estimated_cost_per_unit_formatted"
|
class="rounded-lg border bg-muted/40 p-3 text-sm space-y-1">
|
||||||
class="rounded-lg border bg-muted/40 p-3 text-sm space-y-1"
|
|
||||||
>
|
|
||||||
<p class="font-medium">Harga Modal Batch</p>
|
<p class="font-medium">Harga Modal Batch</p>
|
||||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||||
<span>Bahan baku</span>
|
<span>Bahan baku</span>
|
||||||
@ -600,47 +564,39 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- Shared Price Toggle -->
|
<!-- Shared Price Toggle -->
|
||||||
<div v-if="verifyForm.results.length > 1" class="flex items-center justify-between rounded-lg border p-3">
|
<div v-if="verifyForm.results.length > 1"
|
||||||
|
class="flex items-center justify-between rounded-lg border p-3">
|
||||||
<Label for="use-same-price" class="text-sm font-medium">
|
<Label for="use-same-price" class="text-sm font-medium">
|
||||||
Gunakan harga yang sama untuk semua varian
|
Gunakan harga yang sama untuk semua varian
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch id="use-same-price" :model-value="useSamePrice"
|
||||||
id="use-same-price"
|
@update:model-value="toggleUseSamePrice" />
|
||||||
:model-value="useSamePrice"
|
|
||||||
@update:model-value="toggleUseSamePrice"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Shared Price Inputs -->
|
<!-- Shared Price Inputs -->
|
||||||
<div v-if="useSamePrice && verifyForm.results.length > 1" class="rounded-lg border p-3 space-y-3">
|
<div v-if="useSamePrice && verifyForm.results.length > 1"
|
||||||
|
class="rounded-lg border p-3 space-y-3">
|
||||||
<p class="text-sm font-medium">Harga Jual</p>
|
<p class="text-sm font-medium">Harga Jual</p>
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
<Field v-for="type in PRICE_TYPES" :key="`shared-${type}`">
|
<Field v-for="type in PRICE_TYPES" :key="`shared-${type}`">
|
||||||
<FieldLabel class="text-xs" :for="`shared-price-${type}`">
|
<FieldLabel class="text-xs" :for="`shared-price-${type}`">
|
||||||
{{ PRICE_TYPE_LABELS[type] }}
|
{{ PRICE_TYPE_LABELS[type] }}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<RupiahInput
|
<RupiahInput :id="`shared-price-${type}`" :model-value="sharedPrices[type]"
|
||||||
:id="`shared-price-${type}`"
|
@update:model-value="setSharedPrice(type, $event)" />
|
||||||
:model-value="sharedPrices[type]"
|
|
||||||
@update:model-value="setSharedPrice(type, $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Per Variant Section -->
|
<!-- Per Variant Section -->
|
||||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="p-3 border rounded-lg space-y-3">
|
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id"
|
||||||
|
class="p-3 border rounded-lg space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="font-medium text-sm">
|
<div class="font-medium text-sm">
|
||||||
{{ result.name }}
|
{{ result.name }}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-if="!useSamePrice && verifyForm.results.length > 1 && index > 0" type="button"
|
||||||
v-if="!useSamePrice && verifyForm.results.length > 1 && index > 0"
|
variant="outline" size="sm" @click="applyPriceToAllVariants(index)">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="applyPriceToAllVariants(index)"
|
|
||||||
>
|
|
||||||
<Copy class="size-3.5" />
|
<Copy class="size-3.5" />
|
||||||
Terapkan ke Semua
|
Terapkan ke Semua
|
||||||
</Button>
|
</Button>
|
||||||
@ -666,79 +622,53 @@ watch(rejectDialogOpen, (isOpen) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2 items-end text-xs">
|
<div class="grid grid-cols-2 gap-2 items-end text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label :for="`verif-good-${index}`" class="text-muted-foreground block mb-0.5">Stok Bagus (diterima):</label>
|
<label :for="`verif-good-${index}`" class="text-muted-foreground block mb-0.5">Stok
|
||||||
<Input
|
Bagus (diterima):</label>
|
||||||
:id="`verif-good-${index}`"
|
<Input :id="`verif-good-${index}`" type="number"
|
||||||
type="number"
|
v-model.number="result.warehouse_stock" min="0" :max="result.cutting_result"
|
||||||
v-model.number="result.warehouse_stock"
|
class="h-8 w-full px-2 text-xs" :disabled="allMatches"
|
||||||
min="0"
|
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock" />
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-8 w-full px-2 text-xs"
|
|
||||||
:disabled="allMatches"
|
|
||||||
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label :for="`verif-reject-${index}`" class="text-muted-foreground block mb-0.5">Stok Reject (diterima):</label>
|
<label :for="`verif-reject-${index}`"
|
||||||
<Input
|
class="text-muted-foreground block mb-0.5">Stok Reject (diterima):</label>
|
||||||
:id="`verif-reject-${index}`"
|
<Input :id="`verif-reject-${index}`" type="number"
|
||||||
type="number"
|
v-model.number="result.cutting_reject" min="0" :max="result.cutting_result"
|
||||||
v-model.number="result.cutting_reject"
|
class="h-8 w-full px-2 text-xs" :disabled="allMatches"
|
||||||
min="0"
|
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject" />
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-8 w-full px-2 text-xs"
|
|
||||||
:disabled="allMatches"
|
|
||||||
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!useSamePrice || verifyForm.results.length === 1" class="grid gap-2 sm:grid-cols-2">
|
<div v-if="!useSamePrice || verifyForm.results.length === 1"
|
||||||
|
class="grid gap-2 sm:grid-cols-2">
|
||||||
<Field v-for="type in PRICE_TYPES" :key="`${result.product_variant_id}-${type}`">
|
<Field v-for="type in PRICE_TYPES" :key="`${result.product_variant_id}-${type}`">
|
||||||
<FieldLabel class="text-xs" :for="`verif-price-${index}-${type}`">
|
<FieldLabel class="text-xs" :for="`verif-price-${index}-${type}`">
|
||||||
{{ PRICE_TYPE_LABELS[type] }}
|
{{ PRICE_TYPE_LABELS[type] }}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<RupiahInput
|
<RupiahInput :id="`verif-price-${index}-${type}`" :model-value="result.prices[type]"
|
||||||
:id="`verif-price-${index}-${type}`"
|
@update:model-value="setResultPrice(index, type, $event)" />
|
||||||
:model-value="result.prices[type]"
|
|
||||||
@update:model-value="setResultPrice(index, type, $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldError
|
<FieldError :errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []" />
|
||||||
:errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="verification-note">Catatan Verifikasi</FieldLabel>
|
<FieldLabel for="verification-note">Catatan Verifikasi</FieldLabel>
|
||||||
<Textarea
|
<Textarea id="verification-note" v-model="verifyForm.verification_note"
|
||||||
id="verification-note"
|
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko" rows="3" />
|
||||||
v-model="verifyForm.verification_note"
|
|
||||||
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []"
|
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="mt-6">
|
<DialogFooter class="mt-6">
|
||||||
<Button
|
<Button type="button" variant="outline" :disabled="verifyForm.processing"
|
||||||
type="button"
|
@click="verifyDialogOpen = false">
|
||||||
variant="outline"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
@click="verifyDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" :disabled="verifyForm.processing">
|
||||||
type="submit"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
>
|
|
||||||
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi' }}
|
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -157,7 +157,7 @@ function approvalStatusVariant(status: string): 'default' | 'secondary' | 'destr
|
|||||||
<TableHead>Foto</TableHead>
|
<TableHead>Foto</TableHead>
|
||||||
<TableHead>Stok Bagus</TableHead>
|
<TableHead>Stok Bagus</TableHead>
|
||||||
<TableHead>Stok Reject</TableHead>
|
<TableHead>Stok Reject</TableHead>
|
||||||
<TableHead>Harga Cutting Terakhir</TableHead>
|
<TableHead>Harga</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type {
|
import type {
|
||||||
CategoryOption,
|
CategoryOption,
|
||||||
@ -22,7 +23,6 @@ import type {
|
|||||||
ProductOutOfStockGroup,
|
ProductOutOfStockGroup,
|
||||||
ProductStockWarningGroup,
|
ProductStockWarningGroup,
|
||||||
} from '@/types/product';
|
} from '@/types/product';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
products: PaginatedProducts;
|
products: PaginatedProducts;
|
||||||
@ -120,21 +120,16 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Produk" />
|
<Head title="Produk" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Produk</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Produk</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button v-if="can('products.create')" as-child class="shrink-0 self-start sm:self-center">
|
||||||
v-if="can('products.create')"
|
|
||||||
as-child
|
|
||||||
class="shrink-0 self-start sm:self-center"
|
|
||||||
>
|
|
||||||
<Link href="/admin/master/products/create">
|
<Link href="/admin/master/products/create">
|
||||||
<Plus class="size-4" />
|
<Plus class="size-4" />
|
||||||
Tambah
|
Tambah
|
||||||
@ -142,46 +137,22 @@ watch(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CuttingVerificationSection
|
<CuttingVerificationSection v-if="can('cuttings.verify')" :cuttings="completedCuttings" />
|
||||||
v-if="can('cuttings.verify')"
|
|
||||||
:cuttings="completedCuttings"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MasterOutOfStockCatalogSection
|
<MasterOutOfStockCatalogSection severity="warning" title="Stok Menipis" empty-title="Tidak ada peringatan stok"
|
||||||
severity="warning"
|
empty-description="Semua varian masih berada di atas batas stok minimum." :groups="stockWarningGroups" />
|
||||||
title="Stok Menipis"
|
|
||||||
description="Hanya produk aktif · stok masih ada, tapi di bawah batas minimum · Klik kartu untuk melihat data."
|
|
||||||
empty-title="Tidak ada peringatan stok"
|
|
||||||
empty-description="Semua varian masih berada di atas batas stok minimum."
|
|
||||||
:groups="stockWarningGroups"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MasterOutOfStockCatalogSection
|
<MasterOutOfStockCatalogSection title="Stok Habis" :groups="outOfStockGroups" />
|
||||||
title="Produk Stok Habis"
|
|
||||||
description="Hanya produk aktif · Klik kartu untuk melihat data."
|
|
||||||
:groups="outOfStockGroups"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<ProductGroupedTable
|
<ProductGroupedTable v-model:search="search" :products="products.data" :first-item="firstItem"
|
||||||
v-model:search="search"
|
:pagination="tablePagination" :pagination-links="products.links" :filter-defs="filterDefs"
|
||||||
:products="products.data"
|
:filter-values="filterValues" @filter-change="setFilter" @filters-reset="resetFilters"
|
||||||
:first-item="firstItem"
|
@reject="openRejectModal" />
|
||||||
:pagination="tablePagination"
|
|
||||||
:pagination-links="products.links"
|
|
||||||
:filter-defs="filterDefs"
|
|
||||||
:filter-values="filterValues"
|
|
||||||
@filter-change="setFilter"
|
|
||||||
@filters-reset="resetFilters"
|
|
||||||
@reject="openRejectModal"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<RejectProductModal
|
<RejectProductModal v-model:open="rejectModalOpen" :product="rejectingProduct" />
|
||||||
v-model:open="rejectModalOpen"
|
|
||||||
:product="rejectingProduct"
|
|
||||||
/>
|
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user