feat: enhance order creation and editing with print options and customer creation event handling
This commit is contained in:
parent
3a8cb727fa
commit
8c2be8945d
@ -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
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user