feat: enhance stok opname management with new submit action and grouped table view
This commit is contained in:
parent
2c58c3da5e
commit
950c3e2deb
@ -44,6 +44,8 @@ public function permissions(): array
|
|||||||
Permission::ATTENDANCES_CREATE,
|
Permission::ATTENDANCES_CREATE,
|
||||||
Permission::ATTENDANCES_DELETE,
|
Permission::ATTENDANCES_DELETE,
|
||||||
|
|
||||||
|
Permission::STOK_OPNAMES_SUBMIT,
|
||||||
|
|
||||||
Permission::ROLES_VIEW,
|
Permission::ROLES_VIEW,
|
||||||
Permission::ROLES_CREATE,
|
Permission::ROLES_CREATE,
|
||||||
Permission::ROLES_UPDATE,
|
Permission::ROLES_UPDATE,
|
||||||
@ -137,7 +139,6 @@ public function permissions(): array
|
|||||||
Permission::STOCKS_VIEW,
|
Permission::STOCKS_VIEW,
|
||||||
|
|
||||||
Permission::STOK_OPNAMES_VIEW,
|
Permission::STOK_OPNAMES_VIEW,
|
||||||
Permission::STOK_OPNAMES_VERIFY,
|
|
||||||
|
|
||||||
Permission::CUTTINGS_VERIFY,
|
Permission::CUTTINGS_VERIFY,
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ public function index(Request $request): Response
|
|||||||
$tableQuery = $this->parseDataTableQuery($request);
|
$tableQuery = $this->parseDataTableQuery($request);
|
||||||
|
|
||||||
return Inertia::render('admin/manage/stok-opnames/Index', [
|
return Inertia::render('admin/manage/stok-opnames/Index', [
|
||||||
'stokOpnames' => $this->stokOpnameService->paginateForIndex($tableQuery),
|
'stokOpnames' => $this->stokOpnameService->paginateForIndex($tableQuery, $request->user()),
|
||||||
'filters' => $this->dataTableFilters($tableQuery),
|
'filters' => $this->dataTableFilters($tableQuery),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Manage;
|
namespace App\Services\Manage;
|
||||||
|
|
||||||
|
use App\Enums\Permission;
|
||||||
use App\Enums\StokOpnameStatus;
|
use App\Enums\StokOpnameStatus;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
@ -20,13 +21,16 @@ public function __construct(
|
|||||||
private readonly PushNotificationService $pushNotificationService,
|
private readonly PushNotificationService $pushNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = StokOpname::query()
|
$query = StokOpname::query()
|
||||||
->with(['createdBy.profile', 'verifiedBy.profile'])
|
->with(['createdBy.profile', 'verifiedBy.profile', 'items.productVariant.product'])
|
||||||
->withCount(['items', 'items as items_with_difference_count' => function (Builder $query): void {
|
->withCount(['items', 'items as items_with_difference_count' => function (Builder $query): void {
|
||||||
$query->where('difference', '!=', 0);
|
$query->where('difference', '!=', 0);
|
||||||
}])
|
}])
|
||||||
|
->when(! $user->can(Permission::STOK_OPNAMES_SUBMIT), function (Builder $query): void {
|
||||||
|
$query->where('status', '!=', StokOpnameStatus::DRAFT);
|
||||||
|
})
|
||||||
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
||||||
$search = $tableQuery['search'];
|
$search = $tableQuery['search'];
|
||||||
$query->where(function (Builder $query) use ($search): void {
|
$query->where(function (Builder $query) use ($search): void {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const { open, processing, destroy } = useDestroy({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex">
|
<span class="inline-flex">
|
||||||
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8 delete-btn"
|
||||||
:class="buttonClass" :disabled="disabled" @click="!disabled && (open = true)">
|
:class="buttonClass" :disabled="disabled" @click="!disabled && (open = true)">
|
||||||
<Trash2 class="size-4" />
|
<Trash2 class="size-4" />
|
||||||
<span class="sr-only">{{ tooltip || 'Hapus' }}</span>
|
<span class="sr-only">{{ tooltip || 'Hapus' }}</span>
|
||||||
|
|||||||
@ -40,7 +40,7 @@ function handleClick(event: MouseEvent) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
as-child
|
as-child
|
||||||
class="size-8"
|
class="size-8 edit-btn"
|
||||||
:class="cn(buttonClass, disabled && 'opacity-50')"
|
:class="cn(buttonClass, disabled && 'opacity-50')"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
@ -58,7 +58,7 @@ function handleClick(event: MouseEvent) {
|
|||||||
v-else
|
v-else
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="size-8"
|
class="size-8 edit-btn"
|
||||||
:class="buttonClass"
|
:class="buttonClass"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
|
|||||||
27
resources/js/components/button/RowSubmitAction.vue
Normal file
27
resources/js/components/button/RowSubmitAction.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Send } from '@lucide/vue';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tooltip?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-8 text-blue-600 hover:text-blue-700" :disabled="disabled"
|
||||||
|
@click="$emit('click')">
|
||||||
|
<Send class="size-4" />
|
||||||
|
<span class="sr-only">{{ tooltip || 'Ajukan Verifikasi' }}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ tooltip || 'Ajukan Verifikasi' }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import {
|
import {
|
||||||
@ -9,12 +8,11 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
|
||||||
import type { PaginatedStokOpnames, StokOpnameListItem } from '@/types/stok-opname';
|
import type { PaginatedStokOpnames, StokOpnameListItem } from '@/types/stok-opname';
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { createColumns } from './table/columns';
|
import { index, create, edit, submit, verify, reject } from '@/routes/admin/manage/stok-opnames';
|
||||||
import { index, create, edit, submit, verify, reject, destroy } from '@/routes/admin/manage/stok-opnames';
|
import StokOpnameGroupedTable from './table/StokOpnameGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
stokOpnames: PaginatedStokOpnames;
|
stokOpnames: PaginatedStokOpnames;
|
||||||
@ -42,22 +40,12 @@ const { query, setSearch, setSort, resetFilters, syncFromServer } =
|
|||||||
|
|
||||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||||
|
|
||||||
const currentSort = computed<DataTableSort | null>(() => {
|
const pagination = () => ({
|
||||||
if (!query.value.sort || !query.value.direction) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
column: query.value.sort,
|
|
||||||
direction: query.value.direction,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const pagination = computed(() => ({
|
|
||||||
currentPage: props.stokOpnames.current_page,
|
currentPage: props.stokOpnames.current_page,
|
||||||
perPage: props.stokOpnames.per_page,
|
perPage: props.stokOpnames.per_page,
|
||||||
lastPage: props.stokOpnames.last_page,
|
lastPage: props.stokOpnames.last_page,
|
||||||
total: props.stokOpnames.total,
|
total: props.stokOpnames.total,
|
||||||
}));
|
});
|
||||||
|
|
||||||
function openEdit(item: StokOpnameListItem) {
|
function openEdit(item: StokOpnameListItem) {
|
||||||
router.get(edit.url(item.id));
|
router.get(edit.url(item.id));
|
||||||
@ -99,10 +87,6 @@ function confirmReject() {
|
|||||||
rejectDialogOpen.value = false;
|
rejectDialogOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = computed(() =>
|
|
||||||
createColumns(openEdit, openSubmitDialog, openVerifyDialog, openRejectDialog)
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(search, (value) => {
|
watch(search, (value) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
});
|
});
|
||||||
@ -132,14 +116,16 @@ watch(
|
|||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<DataTable
|
<StokOpnameGroupedTable
|
||||||
v-model:search="search"
|
v-model:search="search"
|
||||||
:columns="columns"
|
:stok-opnames="stokOpnames.data"
|
||||||
:data="stokOpnames.data"
|
:first-item="stokOpnames.from"
|
||||||
:pagination="pagination"
|
:pagination="pagination()"
|
||||||
:pagination-links="stokOpnames.links"
|
:pagination-links="stokOpnames.links"
|
||||||
:sort="currentSort"
|
@edit="openEdit"
|
||||||
@sort-change="setSort"
|
@submit="openSubmitDialog"
|
||||||
|
@verify="openVerifyDialog"
|
||||||
|
@reject="openRejectDialog"
|
||||||
@filters-reset="resetFilters"
|
@filters-reset="resetFilters"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -0,0 +1,164 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { DataTableEmpty } from '@/components/data-table';
|
||||||
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { usePaginationSummary } from '@/composables/usePaginationSummary';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { groupedTableRowNumber } from '@/lib/grouped-table';
|
||||||
|
import {
|
||||||
|
stokOpnameDifference,
|
||||||
|
stokOpnameDifferenceClass,
|
||||||
|
stokOpnameDifferenceText,
|
||||||
|
} from '@/lib/stok-opname-display';
|
||||||
|
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
||||||
|
import type {
|
||||||
|
DataTablePagination,
|
||||||
|
DataTablePaginationLink,
|
||||||
|
} from '@/types/data-table';
|
||||||
|
import type { StokOpnameListItem } from '@/types/stok-opname';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stokOpnames: StokOpnameListItem[];
|
||||||
|
firstItem?: number;
|
||||||
|
pagination?: DataTablePagination;
|
||||||
|
paginationLinks?: DataTablePaginationLink[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const search = defineModel<string>('search', { default: '' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
edit: [item: StokOpnameListItem];
|
||||||
|
submit: [item: StokOpnameListItem];
|
||||||
|
verify: [item: StokOpnameListItem];
|
||||||
|
reject: [item: StokOpnameListItem];
|
||||||
|
'filters-reset': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { can } = useCan();
|
||||||
|
|
||||||
|
const showingCount = computed(() => props.stokOpnames.length);
|
||||||
|
const paginationSummary = usePaginationSummary(() => props.pagination, showingCount, 'stok opname');
|
||||||
|
|
||||||
|
function rowNumber(index: number): number {
|
||||||
|
return groupedTableRowNumber(props.firstItem, index);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<DataTableToolbar v-model:search="search" search-placeholder="Cari stok opname..."
|
||||||
|
@filters-reset="emit('filters-reset')" />
|
||||||
|
|
||||||
|
<div v-if="stokOpnames.length" class="space-y-4">
|
||||||
|
<div v-for="(stokOpname, index) in stokOpnames" :key="stokOpname.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-1">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<h3 class="font-medium leading-tight">
|
||||||
|
{{ stokOpname.opname_date_formatted }}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
:variant="stokOpnameStatusBadgeVariant(stokOpname.status)"
|
||||||
|
:class="cn(stokOpname.status === 'verified' && 'bg-green-600 text-white')"
|
||||||
|
>
|
||||||
|
{{ stokOpname.status_label }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||||
|
<span>Oleh <strong class="text-foreground">{{ stokOpname.created_by_name }}</strong></span>
|
||||||
|
<span>{{ stokOpname.created_at_formatted }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="stokOpname.notes" class="text-sm text-muted-foreground">
|
||||||
|
{{ stokOpname.notes }}
|
||||||
|
</p>
|
||||||
|
<p v-if="stokOpname.verification_notes" class="text-sm text-amber-600">
|
||||||
|
Catatan verifikasi: {{ stokOpname.verification_notes }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
|
||||||
|
<DataTableActions
|
||||||
|
:stok-opname="stokOpname"
|
||||||
|
@edit="emit('edit', stokOpname)"
|
||||||
|
@submit="emit('submit', stokOpname)"
|
||||||
|
@verify="emit('verify', stokOpname)"
|
||||||
|
@reject="emit('reject', stokOpname)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead class="w-12 text-center">No.</TableHead>
|
||||||
|
<TableHead>Produk</TableHead>
|
||||||
|
<TableHead>Varian</TableHead>
|
||||||
|
<TableHead class="text-right">Stok Sistem</TableHead>
|
||||||
|
<TableHead class="text-right">Stok Fisik</TableHead>
|
||||||
|
<TableHead class="text-right">Selisih</TableHead>
|
||||||
|
<TableHead>Catatan</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow v-if="!stokOpname.items?.length" :key="`${stokOpname.id}-empty`">
|
||||||
|
<TableCell :colspan="7" class="text-muted-foreground text-center">
|
||||||
|
Belum ada item
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow v-for="(item, rIdx) in stokOpname.items" :key="item.id">
|
||||||
|
<TableCell class="text-center text-muted-foreground tabular-nums">
|
||||||
|
{{ rIdx + 1 }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="font-medium">
|
||||||
|
{{ item.product_name }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{{ item.variant_name }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ item.system_stock }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ item.physical_stock }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
<span :class="stokOpnameDifferenceClass(stokOpnameDifference(item.physical_stock, item.system_stock))">
|
||||||
|
{{ stokOpnameDifferenceText(stokOpnameDifference(item.physical_stock, item.system_stock)) }}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-muted-foreground">
|
||||||
|
{{ item.notes || '-' }}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTableEmpty v-else />
|
||||||
|
|
||||||
|
<GroupedTableFooter
|
||||||
|
:summary="paginationSummary"
|
||||||
|
:pagination="pagination"
|
||||||
|
:pagination-links="paginationLinks"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import type { ColumnDef } from '@tanstack/vue-table';
|
|
||||||
import { h } from 'vue';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
|
||||||
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
|
||||||
import type { StokOpnameListItem } from '@/types/stok-opname';
|
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export function createColumns(
|
|
||||||
onEdit: (item: StokOpnameListItem) => void,
|
|
||||||
onSubmit: (item: StokOpnameListItem) => void,
|
|
||||||
onVerify: (item: StokOpnameListItem) => void,
|
|
||||||
onReject: (item: StokOpnameListItem) => void,
|
|
||||||
): ColumnDef<StokOpnameListItem>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessorKey: 'opname_date_formatted',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Tanggal', column: 'opname_date' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'status_label',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Status', column: 'status' }),
|
|
||||||
cell: ({ row }) => h(Badge, {
|
|
||||||
variant: stokOpnameStatusBadgeVariant(row.original.status),
|
|
||||||
class: cn(row.original.status === 'verified' && 'bg-green-600 text-white'),
|
|
||||||
}, () => row.original.status_label),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'items_count',
|
|
||||||
enableSorting: false,
|
|
||||||
header: () => 'Jumlah Item',
|
|
||||||
cell: ({ row }) => `${row.original.items_count} item`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'items_with_difference_count',
|
|
||||||
enableSorting: false,
|
|
||||||
header: () => 'Selisih',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const count = row.original.items_with_difference_count;
|
|
||||||
if (count === 0) return '-';
|
|
||||||
return h(Badge, { variant: 'destructive' }, () => `${count} selisih`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'created_by_name',
|
|
||||||
enableSorting: false,
|
|
||||||
header: () => 'Dibuat Oleh',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'created_at_formatted',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Waktu', column: 'created_at' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
cell: ({ row }) => h(DataTableActions, {
|
|
||||||
stokOpname: row.original,
|
|
||||||
onEdit: () => onEdit(row.original),
|
|
||||||
onSubmit: () => onSubmit(row.original),
|
|
||||||
onVerify: () => onVerify(row.original),
|
|
||||||
onReject: () => onReject(row.original),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction, RowSubmitAction, RowApproveAction, RowRejectAction } from '@/components/button';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { StokOpnameListItem } from '@/types/stok-opname';
|
import type { StokOpnameListItem } from '@/types/stok-opname';
|
||||||
import { Send, CheckCircle, XCircle } from '@lucide/vue';
|
|
||||||
import { destroy } from '@/routes/admin/manage/stok-opnames';
|
import { destroy } from '@/routes/admin/manage/stok-opnames';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -22,38 +20,23 @@ const { can } = useCan();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<Button
|
<RowSubmitAction
|
||||||
v-if="can('stok_opnames.submit') && (stokOpname.status === 'draft' || stokOpname.status === 'rejected')"
|
v-if="can('stok_opnames.submit') && (stokOpname.status === 'draft' || stokOpname.status === 'rejected')"
|
||||||
variant="ghost"
|
tooltip="Ajukan Verifikasi"
|
||||||
size="icon"
|
|
||||||
class="size-8 text-blue-600"
|
|
||||||
title="Ajukan Verifikasi"
|
|
||||||
@click="emit('submit')"
|
@click="emit('submit')"
|
||||||
>
|
/>
|
||||||
<Send class="size-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<RowApproveAction
|
||||||
v-if="can('stok_opnames.verify') && stokOpname.status === 'pending'"
|
v-if="can('stok_opnames.verify') && stokOpname.status === 'pending'"
|
||||||
variant="ghost"
|
tooltip="Verifikasi"
|
||||||
size="icon"
|
|
||||||
class="size-8 text-green-600"
|
|
||||||
title="Verifikasi"
|
|
||||||
@click="emit('verify')"
|
@click="emit('verify')"
|
||||||
>
|
/>
|
||||||
<CheckCircle class="size-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<RowRejectAction
|
||||||
v-if="can('stok_opnames.verify') && stokOpname.status === 'pending'"
|
v-if="can('stok_opnames.verify') && stokOpname.status === 'pending'"
|
||||||
variant="ghost"
|
tooltip="Tolak"
|
||||||
size="icon"
|
|
||||||
class="size-8 text-red-600"
|
|
||||||
title="Tolak"
|
|
||||||
@click="emit('reject')"
|
@click="emit('reject')"
|
||||||
>
|
/>
|
||||||
<XCircle class="size-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<RowEditAction
|
<RowEditAction
|
||||||
v-if="can('stok_opnames.update') && (stokOpname.status === 'draft' || stokOpname.status === 'rejected')"
|
v-if="can('stok_opnames.update') && (stokOpname.status === 'draft' || stokOpname.status === 'rejected')"
|
||||||
|
|||||||
@ -94,6 +94,7 @@ watch(
|
|||||||
<CreateButton
|
<CreateButton
|
||||||
v-if="can('categories.create')"
|
v-if="can('categories.create')"
|
||||||
@click="openCreateModal"
|
@click="openCreateModal"
|
||||||
|
id="create-category-btn"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type StokOpnameListItem = {
|
|||||||
created_at_formatted: string;
|
created_at_formatted: string;
|
||||||
items_count: number;
|
items_count: number;
|
||||||
items_with_difference_count: number;
|
items_with_difference_count: number;
|
||||||
|
items: StokOpnameItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StokOpnameItem = {
|
export type StokOpnameItem = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user