feat: enhance order creation and editing with print options and customer creation event handling

This commit is contained in:
Yoga Pangestu 2026-06-18 01:25:53 +07:00
parent 3a8cb727fa
commit 8c2be8945d
5 changed files with 114 additions and 24 deletions

View File

@ -57,7 +57,14 @@ public function store(OrderRequest $request): RedirectResponse
$this->flashCreated('Pesanan');
return redirect()->route('admin.manage.orders.index');
$params = [];
if ($request->boolean('print')) {
$params['print'] = 1;
$params['paper_size'] = $request->input('paper_size', '80mm');
}
return redirect()->route('admin.manage.orders.index', $params);
}
public function edit(Order $order): Response|RedirectResponse

View File

@ -3,9 +3,9 @@ import { useForm, usePage } from '@inertiajs/vue3';
import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import CustomerQuickCreateModal from '@/components/admin/manage/orders/CustomerQuickCreateModal.vue';
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
import CustomerQuickCreateModal from '@/components/admin/manage/orders/CustomerQuickCreateModal.vue';
import { NumberInput } from '@/components/form/number-input';
import { RupiahInput } from '@/components/form/rupiah-input';
import { Button } from '@/components/ui/button';
@ -24,6 +24,7 @@ import {
FieldSet,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -35,10 +36,10 @@ import {
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { useCan } from '@/composables/useCan';
import { OrderChannel } from '@/constants/order-channel';
import { apiFetch } from '@/lib/api';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { useCan } from '@/composables/useCan';
import { formErrors } from '@/lib/form';
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { Auth } from '@/types/auth';
@ -71,6 +72,10 @@ const props = defineProps<{
submitLabel: string;
}>();
const emit = defineEmits<{
'customer-created': [customer: { id: number; name: string }];
}>();
const isCreateMode = computed(() => props.method === 'post');
const { can } = useCan();
@ -84,9 +89,11 @@ const isMarketingUser = computed(() =>
const search = ref('');
const cart = ref<OrderCartItem[]>([]);
const customerFormOpen = ref(false);
const printAfterSave = ref(false);
const selectedPaperSize = ref<'58mm' | '80mm'>('80mm');
function onCustomerCreated(customer: { id: number; name: string }) {
props.customers.push({ value: customer.id, label: customer.name });
emit('customer-created', customer);
form.customer_id = String(customer.id);
}
@ -396,6 +403,11 @@ function buildFormData(): FormData {
formData.append('discount', parseRupiah(form.discount));
formData.append('notes', form.notes);
if (isCreateMode.value && printAfterSave.value) {
formData.append('print', '1');
formData.append('paper_size', selectedPaperSize.value);
}
if (props.method === 'put') {
cart.value.forEach((item, index) => {
formData.append(`items[${index}][product_variant_id]`, String(item.product_variant_id));
@ -733,6 +745,29 @@ function submit() {
<FieldError :errors="formErrors(form, 'notes')" />
</Field>
<Field v-if="isCreateMode && can('orders.create')">
<div class="flex items-center gap-3">
<Switch id="print-after-save" :model-value="printAfterSave"
@update:model-value="printAfterSave = $event" />
<Label for="print-after-save" class="cursor-pointer text-sm">
Cetak struk setelah simpan
</Label>
</div>
<div v-if="printAfterSave" class="mt-2 flex items-center gap-2 pl-1">
<span class="text-xs text-muted-foreground">Ukuran kertas:</span>
<div class="flex gap-1.5">
<button v-for="size in (['58mm', '80mm'] as const)" :key="size" type="button"
class="inline-flex h-7 items-center rounded-md border px-2.5 text-xs font-medium transition-colors cursor-pointer"
:class="selectedPaperSize === size
? 'border-primary bg-primary/5 text-primary'
: 'border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground'"
@click="selectedPaperSize = size">
{{ size }}
</button>
</div>
</div>
</Field>
<Button type="submit" class="w-full" :disabled="form.processing || cart.length === 0">
<Save class="size-4" />
{{ form.processing ? 'Menyimpan...' : submitLabel }}

View File

@ -1,12 +1,13 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { ArrowLeft } from '@lucide/vue';
import { ref } from 'vue';
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
import { Button } from '@/components/ui/button';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
defineProps<{
const props = defineProps<{
customers: SelectOption[];
marketings: SelectOption[];
catalog: OrderCatalogItem[];
@ -15,6 +16,12 @@ defineProps<{
paymentTypes: EnumOption[];
draftItems: OrderCartItem[];
}>();
const customers = ref([...props.customers]);
function onCustomerCreated(customer: { id: number; name: string }) {
customers.value.push({ value: customer.id, label: customer.name });
}
</script>
<template>
@ -47,6 +54,7 @@ defineProps<{
submit-url="/admin/manage/orders"
method="post"
submit-label="Simpan"
@customer-created="onCustomerCreated"
/>
</AdminLayout>
</template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { ArrowLeft } from '@lucide/vue';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
import { Button } from '@/components/ui/button';
import AdminLayout from '@/layouts/AdminLayout.vue';
@ -17,6 +17,8 @@ const props = defineProps<{
paymentTypes: EnumOption[];
}>();
const customers = ref([...props.customers]);
const initialData = computed(() => ({
customer_id: props.order.customer_id ? String(props.order.customer_id) : '',
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
@ -37,6 +39,10 @@ const initialData = computed(() => ({
images: item.product_variant?.images ?? [],
})),
}));
function onCustomerCreated(customer: { id: number; name: string }) {
customers.value.push({ value: customer.id, label: customer.name });
}
</script>
<template>
@ -72,6 +78,7 @@ const initialData = computed(() => ({
:submit-url="`/admin/manage/orders/${order.id}`"
method="put"
submit-label="Perbarui"
@customer-created="onCustomerCreated"
/>
</AdminLayout>
</template>

View File

@ -1,4 +1,8 @@
<script setup lang="ts">
import { Head, Link, usePage } from '@inertiajs/vue3';
import { Plus } from '@lucide/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import OrderGroupedTable from '@/components/admin/manage/orders/OrderGroupedTable.vue';
import ThermalPrinterConnectButton from '@/components/admin/manage/orders/ThermalPrinterConnectButton.vue';
import { Button } from '@/components/ui/button';
@ -8,11 +12,10 @@ import {
useDataTableQuery,
useDataTableQuerySync,
} from '@/composables/useDataTableQuery';
import { useThermalPrinter, getPaperSizeLabel } from '@/composables/useThermalPrinter';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { PaperSize } from '@/lib/thermal-printer/types';
import type { PaginatedOrders } from '@/types/order';
import { Head, Link } from '@inertiajs/vue3';
import { Plus } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
const props = defineProps<{
orders: PaginatedOrders;
@ -24,6 +27,8 @@ const props = defineProps<{
}>();
const { can } = useCan();
const page = usePage();
const { isConnected, printOrderReceipt, tryReconnect } = useThermalPrinter();
const search = ref(props.filters.search ?? '');
const { setSearch, resetFilters, syncFromServer } = useDataTableQuery({
@ -56,23 +61,57 @@ watch(
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="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')"
>
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center" v-if="can('orders.create')">
<ThermalPrinterConnectButton />
<Button as-child>
@ -86,14 +125,8 @@ watch(
<Card class="min-w-0">
<CardContent class="min-w-0">
<OrderGroupedTable
v-model:search="search"
:orders="orders.data"
:first-item="firstItem"
:pagination="tablePagination"
:pagination-links="orders.links"
@filters-reset="resetFilters"
/>
<OrderGroupedTable v-model:search="search" :orders="orders.data" :first-item="firstItem"
:pagination="tablePagination" :pagination-links="orders.links" @filters-reset="resetFilters" />
</CardContent>
</Card>
</AdminLayout>