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');
|
$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
|
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 { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
import CustomerQuickCreateModal from '@/components/admin/manage/orders/CustomerQuickCreateModal.vue';
|
||||||
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
|
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
|
||||||
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.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 { NumberInput } from '@/components/form/number-input';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -24,6 +24,7 @@ import {
|
|||||||
FieldSet,
|
FieldSet,
|
||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -35,10 +36,10 @@ import {
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useCan } from '@/composables/useCan';
|
||||||
import { OrderChannel } from '@/constants/order-channel';
|
import { OrderChannel } from '@/constants/order-channel';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import { useCan } from '@/composables/useCan';
|
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||||
import type { Auth } from '@/types/auth';
|
import type { Auth } from '@/types/auth';
|
||||||
@ -71,6 +72,10 @@ const props = defineProps<{
|
|||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'customer-created': [customer: { id: number; name: string }];
|
||||||
|
}>();
|
||||||
|
|
||||||
const isCreateMode = computed(() => props.method === 'post');
|
const isCreateMode = computed(() => props.method === 'post');
|
||||||
|
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
@ -84,9 +89,11 @@ const isMarketingUser = computed(() =>
|
|||||||
const search = ref('');
|
const search = ref('');
|
||||||
const cart = ref<OrderCartItem[]>([]);
|
const cart = ref<OrderCartItem[]>([]);
|
||||||
const customerFormOpen = ref(false);
|
const customerFormOpen = ref(false);
|
||||||
|
const printAfterSave = ref(false);
|
||||||
|
const selectedPaperSize = ref<'58mm' | '80mm'>('80mm');
|
||||||
|
|
||||||
function onCustomerCreated(customer: { id: number; name: string }) {
|
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);
|
form.customer_id = String(customer.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,6 +403,11 @@ function buildFormData(): FormData {
|
|||||||
formData.append('discount', parseRupiah(form.discount));
|
formData.append('discount', parseRupiah(form.discount));
|
||||||
formData.append('notes', form.notes);
|
formData.append('notes', form.notes);
|
||||||
|
|
||||||
|
if (isCreateMode.value && printAfterSave.value) {
|
||||||
|
formData.append('print', '1');
|
||||||
|
formData.append('paper_size', selectedPaperSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
cart.value.forEach((item, index) => {
|
cart.value.forEach((item, index) => {
|
||||||
formData.append(`items[${index}][product_variant_id]`, String(item.product_variant_id));
|
formData.append(`items[${index}][product_variant_id]`, String(item.product_variant_id));
|
||||||
@ -733,6 +745,29 @@ function submit() {
|
|||||||
<FieldError :errors="formErrors(form, 'notes')" />
|
<FieldError :errors="formErrors(form, 'notes')" />
|
||||||
</Field>
|
</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">
|
<Button type="submit" class="w-full" :disabled="form.processing || cart.length === 0">
|
||||||
<Save class="size-4" />
|
<Save class="size-4" />
|
||||||
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { ArrowLeft } from '@lucide/vue';
|
import { ArrowLeft } from '@lucide/vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
|
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
|
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
customers: SelectOption[];
|
customers: SelectOption[];
|
||||||
marketings: SelectOption[];
|
marketings: SelectOption[];
|
||||||
catalog: OrderCatalogItem[];
|
catalog: OrderCatalogItem[];
|
||||||
@ -15,6 +16,12 @@ defineProps<{
|
|||||||
paymentTypes: EnumOption[];
|
paymentTypes: EnumOption[];
|
||||||
draftItems: OrderCartItem[];
|
draftItems: OrderCartItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const customers = ref([...props.customers]);
|
||||||
|
|
||||||
|
function onCustomerCreated(customer: { id: number; name: string }) {
|
||||||
|
customers.value.push({ value: customer.id, label: customer.name });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -47,6 +54,7 @@ defineProps<{
|
|||||||
submit-url="/admin/manage/orders"
|
submit-url="/admin/manage/orders"
|
||||||
method="post"
|
method="post"
|
||||||
submit-label="Simpan"
|
submit-label="Simpan"
|
||||||
|
@customer-created="onCustomerCreated"
|
||||||
/>
|
/>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { ArrowLeft } from '@lucide/vue';
|
import { ArrowLeft } from '@lucide/vue';
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
|
import OrderPosForm from '@/components/admin/manage/orders/OrderPosForm.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
@ -17,6 +17,8 @@ const props = defineProps<{
|
|||||||
paymentTypes: EnumOption[];
|
paymentTypes: EnumOption[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const customers = ref([...props.customers]);
|
||||||
|
|
||||||
const initialData = computed(() => ({
|
const initialData = computed(() => ({
|
||||||
customer_id: props.order.customer_id ? String(props.order.customer_id) : '',
|
customer_id: props.order.customer_id ? String(props.order.customer_id) : '',
|
||||||
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
|
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
|
||||||
@ -37,6 +39,10 @@ const initialData = computed(() => ({
|
|||||||
images: item.product_variant?.images ?? [],
|
images: item.product_variant?.images ?? [],
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onCustomerCreated(customer: { id: number; name: string }) {
|
||||||
|
customers.value.push({ value: customer.id, label: customer.name });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -72,6 +78,7 @@ const initialData = computed(() => ({
|
|||||||
:submit-url="`/admin/manage/orders/${order.id}`"
|
:submit-url="`/admin/manage/orders/${order.id}`"
|
||||||
method="put"
|
method="put"
|
||||||
submit-label="Perbarui"
|
submit-label="Perbarui"
|
||||||
|
@customer-created="onCustomerCreated"
|
||||||
/>
|
/>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<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 OrderGroupedTable from '@/components/admin/manage/orders/OrderGroupedTable.vue';
|
||||||
import ThermalPrinterConnectButton from '@/components/admin/manage/orders/ThermalPrinterConnectButton.vue';
|
import ThermalPrinterConnectButton from '@/components/admin/manage/orders/ThermalPrinterConnectButton.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -8,11 +12,10 @@ import {
|
|||||||
useDataTableQuery,
|
useDataTableQuery,
|
||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
|
import { useThermalPrinter, getPaperSizeLabel } from '@/composables/useThermalPrinter';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import type { PaperSize } from '@/lib/thermal-printer/types';
|
||||||
import type { PaginatedOrders } from '@/types/order';
|
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<{
|
const props = defineProps<{
|
||||||
orders: PaginatedOrders;
|
orders: PaginatedOrders;
|
||||||
@ -24,6 +27,8 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
|
const page = usePage();
|
||||||
|
const { isConnected, printOrderReceipt, tryReconnect } = useThermalPrinter();
|
||||||
const search = ref(props.filters.search ?? '');
|
const search = ref(props.filters.search ?? '');
|
||||||
|
|
||||||
const { setSearch, resetFilters, syncFromServer } = useDataTableQuery({
|
const { setSearch, resetFilters, syncFromServer } = useDataTableQuery({
|
||||||
@ -56,23 +61,57 @@ watch(
|
|||||||
search.value = 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Pesanan" />
|
<Head title="Pesanan" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Pesanan</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Pesanan</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center" v-if="can('orders.create')">
|
||||||
class="flex shrink-0 items-center gap-2 self-start sm:self-center"
|
|
||||||
v-if="can('orders.create')"
|
|
||||||
>
|
|
||||||
<ThermalPrinterConnectButton />
|
<ThermalPrinterConnectButton />
|
||||||
|
|
||||||
<Button as-child>
|
<Button as-child>
|
||||||
@ -86,14 +125,8 @@ watch(
|
|||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<OrderGroupedTable
|
<OrderGroupedTable v-model:search="search" :orders="orders.data" :first-item="firstItem"
|
||||||
v-model:search="search"
|
:pagination="tablePagination" :pagination-links="orders.links" @filters-reset="resetFilters" />
|
||||||
:orders="orders.data"
|
|
||||||
:first-item="firstItem"
|
|
||||||
:pagination="tablePagination"
|
|
||||||
:pagination-links="orders.links"
|
|
||||||
@filters-reset="resetFilters"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user