store/resources/js/pages/admin/manage/orders/Index.vue
Yoga Pangestu c2c5f12b5c
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add date filters and summary for order management
2026-08-05 00:30:41 +07:00

223 lines
6.5 KiB
Vue

<script setup lang="ts">
import { Head, usePage } from '@inertiajs/vue3';
import { computed, onMounted, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import CreateButton from '@/components/button/CreateButton.vue';
import ThermalPrinterConnectButton from '@/components/order/ThermalPrinterConnectButton.vue';
import { Card, CardContent } from '@/components/ui/card';
import { useCan } from '@/composables/useCan';
import {
useDataTableQuery,
useDataTableQuerySync,
} from '@/composables/useDataTableQuery';
import {
getPaperSizeLabel,
useThermalPrinter,
} from '@/composables/useThermalPrinter';
import { OrderChannel } from '@/constants/order-channel';
import { OrderPaymentType } from '@/constants/order-payment-type';
import { OrderStatus } from '@/constants/order-status';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { PaperSize } from '@/lib/thermal-printer/types';
import { index, create } from '@/routes/admin/manage/orders';
import type { DataTableFilterDef } from '@/types/data-table';
import type { PaginatedOrders } from '@/types/order';
import OrderGroupedTable from './table/OrderGroupedTable.vue';
const props = defineProps<{
orders: PaginatedOrders;
summary: {
total_orders: number;
total_amount: number;
total_amount_formatted: string;
total_subtotal: number;
total_subtotal_formatted: string;
total_discount: number;
total_discount_formatted: string;
};
filters: {
search: string;
sort?: string;
direction?: 'asc' | 'desc';
channel?: string;
status?: string;
payment_type?: string;
date_from?: string;
date_to?: string;
};
}>();
const { can } = useCan();
const page = usePage();
const { isConnected, printOrderReceipt, tryReconnect } = useThermalPrinter();
const search = ref(props.filters.search ?? '');
const { query, setSearch, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
url: index.url(),
initial: { ...props.filters },
filterKeys: ['channel', 'status', 'payment_type', 'date_from', 'date_to'],
});
useDataTableQuerySync(() => props.filters, syncFromServer);
const filterDefs = computed<DataTableFilterDef[]>(() => [
{
key: 'channel',
label: 'Channel',
type: 'select',
options: [
{ value: OrderChannel.STORE, label: 'Toko' },
{ value: OrderChannel.SHOPEE, label: 'Shopee' },
{ value: OrderChannel.TIKTOK, label: 'TikTok' },
],
},
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: OrderStatus.PENDING, label: 'Menunggu' },
{ value: OrderStatus.PROCESSING, label: 'Diproses' },
{ value: OrderStatus.COMPLETED, label: 'Selesai' },
{ value: OrderStatus.CANCELLED, label: 'Dibatalkan' },
],
},
{
key: 'payment_type',
label: 'Tipe Pembayaran',
type: 'select',
options: [
{ value: OrderPaymentType.CASH, label: 'Cash' },
{ value: OrderPaymentType.TRANSFER, label: 'Transfer' },
{ value: OrderPaymentType.QRIS, label: 'QRIS' },
{ value: OrderPaymentType.MARKETPLACE, label: 'Marketplace' },
],
},
{
key: 'date_from',
label: 'Dari Tanggal',
type: 'date',
},
{
key: 'date_to',
label: 'Sampai Tanggal',
type: 'date',
},
]);
const filterValues = computed(() => ({
channel: query.value.channel ?? '',
status: query.value.status ?? '',
payment_type: query.value.payment_type ?? '',
date_from: query.value.date_from ?? '',
date_to: query.value.date_to ?? '',
}));
const tablePagination = computed(() => ({
currentPage: props.orders.current_page,
perPage: props.orders.per_page,
lastPage: props.orders.last_page,
total: props.orders.total,
}));
const firstItem = computed(() =>
props.orders.data.length > 0
? (props.orders.current_page - 1) * props.orders.per_page + 1
: 0,
);
watch(search, (value) => {
setSearch(value);
});
watch(
() => props.filters.search,
(value) => {
search.value = value ?? '';
},
);
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('print') || props.orders.data.length === 0) {
return;
}
const paperSize = (urlParams.get('paper_size') || '80mm') as PaperSize;
const order = props.orders.data[0];
window.history.replaceState({}, '', window.location.pathname);
if (!isConnected.value) {
const restored = await tryReconnect();
if (!restored) {
toast.error('Printer belum terhubung. Struk tidak dapat dicetak.');
return;
}
}
try {
await printOrderReceipt({
order,
paperSize,
storeName: page.props.name as string,
storeAddress: (page.props.address as string) ?? '',
});
toast.success(
`Struk ${order.order_number} dikirim ke printer ${getPaperSizeLabel(paperSize)}.`,
);
} catch (error) {
const message =
error instanceof Error
? error.message
: 'Gagal mencetak struk pesanan.';
toast.error(message);
}
});
</script>
<template>
<Head title="Pesanan" />
<AdminLayout>
<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">Pesanan</h2>
</div>
<div
class="flex shrink-0 items-center gap-2 self-start sm:self-center"
v-if="can('orders.create')"
>
<ThermalPrinterConnectButton />
<CreateButton :href="create.url()" />
</div>
</div>
<Card class="min-w-0">
<CardContent class="min-w-0">
<OrderGroupedTable
v-model:search="search"
:orders="orders.data"
:summary="summary"
:first-item="firstItem"
:pagination="tablePagination"
:pagination-links="orders.links"
:filter-defs="filterDefs"
:filter-values="filterValues"
@filter-change="setFilter"
@filters-reset="resetFilters"
/>
</CardContent>
</Card>
</AdminLayout>
</template>