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