Skip to content

Commit 417fae8

Browse files
authored
feat: complete order action (#457)
* feat: complete order action * chore: update
1 parent 9e00ac8 commit 417fae8

12 files changed

Lines changed: 340 additions & 96 deletions

File tree

apps/essence/server/api/storefront/gateway.post.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { GatewayAddOrderItemRequest, GatewayDecrementOrderItemQuantityRequest, GatewayGetDeliveryByCourierStatusRequest, GatewayGetOrderRequest, GatewayGetSelfPickupStatusRequest, GatewayGetTimeSlotsRequest, GatewayIncrementOrderItemQuantityRequest, GatewayRequest, GatewayUpdateOrderRequest } from '@nextorders/food-schema'
1+
import type { GatewayAddOrderItemRequest, GatewayCompleteOrderRequest, GatewayDecrementOrderItemQuantityRequest, GatewayGetDeliveryByCourierStatusRequest, GatewayGetOrderRequest, GatewayGetSelfPickupStatusRequest, GatewayGetTimeSlotsRequest, GatewayIncrementOrderItemQuantityRequest, GatewayRequest, GatewayUpdateOrderRequest } from '@nextorders/food-schema'
22
import { handleGetChannels, handleGetDeliveryByCourierStatus, handleGetSelfPickupStatus, handleGetTimeSlots } from '~/server/services/channel'
33
import { handleGetMenu } from '~/server/services/menu'
44
import { handleGetOptions } from '~/server/services/options'
5-
import { handleAddOrderItem, handleChangeOrderItemQuantity, handleCreateOrder, handleGetOrder, handleUpdateOrder } from '~/server/services/order'
5+
import { handleAddOrderItem, handleChangeOrderItemQuantity, handleCompleteOrder, handleCreateOrder, handleGetOrder, handleUpdateOrder } from '~/server/services/order'
66

77
export default defineEventHandler(async (event) => {
88
try {
@@ -21,6 +21,8 @@ export default defineEventHandler(async (event) => {
2121
return handleCreateOrder()
2222
case 'updateOrder':
2323
return handleUpdateOrder(request.body as GatewayUpdateOrderRequest['body'])
24+
case 'completeOrder':
25+
return handleCompleteOrder(request.body as GatewayCompleteOrderRequest['body'])
2426
case 'addOrderItem':
2527
return handleAddOrderItem(request.body as GatewayAddOrderItemRequest['body'])
2628
case 'incrementOrderItemQuantity':

apps/essence/server/services/order.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GatewayAddOrderItemRequest, GatewayAddOrderItemResponse, GatewayCreateOrderResponse, GatewayDecrementOrderItemQuantityRequest, GatewayDecrementOrderItemQuantityResponse, GatewayGetOrderRequest, GatewayGetOrderResponse, GatewayIncrementOrderItemQuantityRequest, GatewayIncrementOrderItemQuantityResponse, GatewayUpdateOrderRequest, GatewayUpdateOrderResponse, Order, OrderItem } from '@nextorders/food-schema'
1+
import type { GatewayAddOrderItemRequest, GatewayAddOrderItemResponse, GatewayCompleteOrderRequest, GatewayCompleteOrderResponse, GatewayCreateOrderResponse, GatewayDecrementOrderItemQuantityRequest, GatewayDecrementOrderItemQuantityResponse, GatewayGetOrderRequest, GatewayGetOrderResponse, GatewayIncrementOrderItemQuantityRequest, GatewayIncrementOrderItemQuantityResponse, GatewayUpdateOrderRequest, GatewayUpdateOrderResponse, Order, OrderItem } from '@nextorders/food-schema'
22
import { createId } from '@paralleldrive/cuid2'
33
import { handleGetMenu } from './menu'
44

@@ -122,6 +122,38 @@ export function handleUpdateOrder(data: GatewayUpdateOrderRequest['body']): Gate
122122
}
123123
}
124124

125+
export function handleCompleteOrder(data: GatewayCompleteOrderRequest['body']): GatewayCompleteOrderResponse {
126+
if (!data.id) {
127+
throw new Error('Order id is required')
128+
}
129+
130+
const order = findOrder(data.id)
131+
if (!order) {
132+
throw new Error('Order not found')
133+
}
134+
135+
// Guard: if order is not in status "draft" - can't complete it
136+
if (order.status !== 'draft') {
137+
throw new Error('Order is not in draft status')
138+
}
139+
140+
const completedOrder = updateOrder(data.id, {
141+
...data,
142+
status: 'created',
143+
})
144+
if (!completedOrder) {
145+
throw new Error('Order not found')
146+
}
147+
148+
logger.success(`Order completed: ${completedOrder.id}`, completedOrder)
149+
150+
return {
151+
ok: true,
152+
type: 'completeOrder',
153+
result: completedOrder,
154+
}
155+
}
156+
125157
export function handleAddOrderItem({ orderId, variantId }: GatewayAddOrderItemRequest['body']): GatewayAddOrderItemResponse {
126158
const menu = handleGetMenu().result
127159

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<ClientOnly>
3+
<div
4+
v-if="isShown"
5+
v-confetti="{ particleCount: 160, duration: 3000, stageHeight: height, stageWidth: width, force: 0.6 }"
6+
class="mx-auto block overflow-hidden overscroll-none"
7+
/>
8+
</ClientOnly>
9+
</template>
10+
11+
<script setup lang="ts">
12+
import { vConfetti } from '@neoconfetti/vue'
13+
14+
const { isShown } = useConfetti()
15+
const { width, height } = useWindowSize()
16+
</script>

apps/web-app/app/components/product/Badge.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="py-1.5 px-2.5 w-fit text-base/5 md:text-sm/4 font-semibold uppercase text-neutral-900 bg-neutral-100 rounded-sm -skew-y-4 motion-preset-slide-up">
2+
<div class="py-1.5 px-2.5 w-fit text-sm/4 font-semibold uppercase text-neutral-900 bg-neutral-100 rounded-sm -skew-y-4 motion-preset-slide-up">
33
{{ optionsStore.getLocaleValue(badge.title) }}
44
</div>
55
</template>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
function _useConfetti() {
2+
const isShown = ref(false)
3+
let hideTimeout: ReturnType<typeof setTimeout> | undefined
4+
5+
function pop() {
6+
isShown.value = true
7+
if (hideTimeout) {
8+
clearTimeout(hideTimeout)
9+
}
10+
hideTimeout = setTimeout(() => {
11+
isShown.value = false
12+
}, 3000)
13+
}
14+
15+
return { isShown, pop }
16+
}
17+
18+
export const useConfetti = createSharedComposable(_useConfetti)

apps/web-app/app/pages/checkout.vue

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
:loading="orderStore.isLoading"
8383
:disabled="!orderStore.isReadyToCheckout || orderStore.isLoading"
8484
:label="$dict('web-app.checkout.create-order')"
85-
@click="updateCheckout()"
85+
@click="createOrder()"
8686
/>
8787
</UTooltip>
8888

@@ -126,12 +126,29 @@ const channelStore = useChannelStore()
126126
const orderStore = useOrderStore()
127127
const optionsStore = useOptionsStore()
128128
129-
async function updateCheckout() {
129+
async function createOrder() {
130130
orderStore.isLoading = true
131131
132-
// await orderStore.update()
133-
134-
// await navigateTo(`/finish?id=${finishedCheckout?.result?.id}`)
132+
try {
133+
const completedOrder = await orderStore.complete({
134+
phone: orderStore.phone,
135+
name: orderStore.name,
136+
paymentMethodId: orderStore.paymentMethodId,
137+
readyBy: orderStore.readyBy,
138+
readyType: orderStore.readyType,
139+
address: orderStore.address,
140+
note: orderStore.note,
141+
})
142+
143+
if (!completedOrder?.id) {
144+
await navigateTo('/')
145+
return
146+
}
147+
148+
await navigateTo(`/finish?id=${completedOrder.id}`)
149+
} finally {
150+
orderStore.isLoading = false
151+
}
135152
}
136153
137154
useHead({

apps/web-app/app/pages/finish.vue

Lines changed: 92 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,123 @@
11
<template>
2-
<h1 class="pt-8 mb-4 md:mb-8 text-3xl md:text-4xl font-medium text-center">
2+
<Confetti />
3+
4+
<h1 class="pt-8 mb-4 md:mb-8 text-3xl md:text-4xl font-medium text-center motion-preset-pop">
35
{{ $dict('web-app.finish.title') }}
46
</h1>
57

6-
<div class="p-3 md:p-6 bg-elevated/50 rounded-xl flex flex-col gap-6 motion-preset-slide-left-sm">
7-
<h2 class="text-center font-medium text-xl">
8-
{{ $dict('web-app.finish.success-message') }} {{ $dict('web-app.finish.expect-call') }}
9-
</h2>
10-
11-
<div>
12-
<h3 class="mb-2 text-lg font-medium text-muted">
13-
{{ order?.deliveryMethod === 'selfPickup' ? $dict('web-app.cart.pickup') : $dict('web-app.cart.delivery') }}
14-
</h3>
15-
16-
<p>{{ $dict('web-app.checkout.your-name') }}: <span class="font-medium">{{ order?.name }}</span></p>
17-
<p class="mb-2">
18-
{{ $dict('web-app.checkout.your-phone') }}: <span class="font-medium">{{ order?.phone }}</span>
19-
</p>
20-
21-
<p v-if="order?.readyBy">
22-
{{ $dict('web-app.checkout.time-title') }}: <span class="font-medium">{{ order.readyType === 'asap' ? $dict('web-app.checkout.as-soon-as-possible') : new Date(order.readyBy).toLocaleString(undefined, {
23-
year: 'numeric',
24-
month: 'numeric',
25-
day: 'numeric',
26-
hour: '2-digit',
27-
minute: '2-digit',
28-
hour12: false,
29-
}) }}</span>
30-
</p>
8+
<template v-if="!order?.id">
9+
<div class="flex flex-col items-center justify-center min-h-64">
10+
<UIcon name="i-lucide-shopping-basket" class="mx-auto my-auto size-24 text-dimmed/25 self-center motion-preset-pulse-sm" />
11+
</div>
12+
</template>
13+
14+
<ClientOnly v-else>
15+
<div class="p-3 md:p-6 max-w-lg mx-auto bg-elevated/50 rounded-xl flex flex-col gap-6 motion-preset-slide-left-sm">
16+
<h2 class="text-center font-medium text-xl">
17+
{{ $dict('web-app.finish.success-message') }} {{ $dict('web-app.finish.expect-call') }}
18+
</h2>
19+
3120
<div>
32-
{{ $dict('web-app.checkout.address.title') }}:
33-
<p v-if="warehouse?.address" class="inline font-medium">
34-
{{ optionsStore.getLocaleValue(warehouse.address.street) }}
21+
<h3 class="mb-2 text-lg font-medium text-muted">
22+
{{ order?.deliveryMethod === 'selfPickup' ? $dict('web-app.cart.pickup') : $dict('web-app.cart.delivery') }}
23+
</h3>
24+
25+
<p>{{ $dict('web-app.checkout.your-name') }}: <span class="font-medium">{{ order?.name }}</span></p>
26+
<p class="mb-2">
27+
{{ $dict('web-app.checkout.your-phone') }}: <span class="font-medium">{{ order?.phone }}</span>
3528
</p>
36-
<p v-if="deliveryAddress?.street" class="inline font-medium">
37-
<span>{{ deliveryAddress?.street }} {{ deliveryAddress?.flat }}</span>
38-
<span v-if="deliveryAddress?.intercom" class="lowercase">, {{ $dict('web-app.checkout.address.intercom') }} {{ deliveryAddress?.intercom }}</span>
39-
<span v-if="deliveryAddress?.entrance" class="lowercase">, {{ $dict('web-app.checkout.address.entrance') }} {{ deliveryAddress?.entrance }}</span>
40-
<span v-if="deliveryAddress?.floor" class="lowercase">, {{ $dict('web-app.checkout.address.floor') }} {{ deliveryAddress?.floor }}</span>
41-
<span v-if="deliveryAddress?.addressNote">. {{ deliveryAddress?.addressNote }}</span>
29+
30+
<p v-if="order?.readyBy">
31+
{{ $dict('web-app.checkout.time-title') }}: <span class="font-medium">{{ order.readyType === 'asap' ? $dict('web-app.checkout.as-soon-as-possible') : order.readyBy }}</span>
4232
</p>
43-
</div>
33+
<div>
34+
{{ $dict('web-app.checkout.address.title') }}:
35+
<p v-if="warehouse?.address" class="inline font-medium">
36+
{{ optionsStore.getLocaleValue(warehouse.address.street) }}
37+
</p>
38+
<p v-if="deliveryAddress?.street" class="inline font-medium">
39+
<span>{{ deliveryAddress?.street }} {{ deliveryAddress?.flat }}</span>
40+
<span v-if="deliveryAddress?.intercom" class="lowercase">, {{ $dict('web-app.checkout.address.intercom') }} {{ deliveryAddress?.intercom }}</span>
41+
<span v-if="deliveryAddress?.entrance" class="lowercase">, {{ $dict('web-app.checkout.address.entrance') }} {{ deliveryAddress?.entrance }}</span>
42+
<span v-if="deliveryAddress?.floor" class="lowercase">, {{ $dict('web-app.checkout.address.floor') }} {{ deliveryAddress?.floor }}</span>
43+
<span v-if="deliveryAddress?.addressNote">. {{ deliveryAddress?.addressNote }}</span>
44+
</p>
45+
</div>
4446

45-
<p>{{ $dict('web-app.checkout.payment-title') }}: <span class="font-medium">{{ optionsStore.getLocaleValue(paymentMethod) }}</span></p>
46-
<p v-if="order?.changeFrom">
47-
{{ $dict('web-app.checkout.change-label') }}: <span class="font-medium">{{ order.changeFrom }} {{ optionsStore.currencySign }}</span>
48-
</p>
49-
<p v-if="order?.note">
50-
{{ $dict('web-app.checkout.order-note') }}: <span class="font-medium">{{ order.note }}</span>
51-
</p>
52-
</div>
47+
<p>{{ $dict('web-app.checkout.payment-title') }}: <span class="font-medium">{{ optionsStore.getLocaleValue(paymentMethodTitle) }}</span></p>
48+
<p v-if="order?.changeFrom">
49+
{{ $dict('web-app.checkout.change-label') }}: <span class="font-medium">{{ order.changeFrom }} {{ optionsStore.currencySign }}</span>
50+
</p>
51+
<p v-if="order?.note">
52+
{{ $dict('web-app.checkout.order-note') }}: <span class="font-medium">{{ order.note }}</span>
53+
</p>
54+
</div>
5355

54-
<div class="flex flex-col gap-3">
55-
<h3 class="text-lg font-medium text-muted">
56-
{{ $dict('web-app.finish.ordered-title') }}
57-
</h3>
58-
59-
<CheckoutLine
60-
v-for="line in order?.items"
61-
:key="line.id"
62-
:line-id="line.id"
63-
:line="line"
64-
:can-be-changed="false"
65-
/>
66-
</div>
56+
<div class="flex flex-col gap-3">
57+
<h3 class="text-lg font-medium text-muted">
58+
{{ $dict('web-app.finish.ordered-title') }}
59+
</h3>
60+
61+
<CheckoutLine
62+
v-for="line in order?.items"
63+
:key="line.id"
64+
:line-id="line.id"
65+
:line="line"
66+
:can-be-changed="false"
67+
/>
68+
</div>
6769

68-
<div>
69-
<h3 class="mb-2 text-lg font-medium text-muted">
70-
{{ $dict('web-app.checkout.total-title') }}
71-
</h3>
72-
<div class="flex flex-row justify-between">
73-
<div>{{ $dict('web-app.checkout.cost.products') }}</div>
74-
<div class="tracking-tight text-lg">
75-
{{ order?.totalPrice }} <span class="text-sm">{{ optionsStore.currencySign }}</span>
70+
<div>
71+
<h3 class="mb-2 text-lg font-medium text-muted">
72+
{{ $dict('web-app.checkout.total-title') }}
73+
</h3>
74+
<div class="flex flex-row justify-between">
75+
<div>{{ $dict('web-app.checkout.cost.products') }}</div>
76+
<div class="tracking-tight text-lg">
77+
{{ order?.totalPrice }} <span class="text-sm">{{ optionsStore.currencySign }}</span>
78+
</div>
7679
</div>
7780
</div>
78-
</div>
7981

80-
<UButton
81-
to="/"
82-
variant="solid"
83-
color="secondary"
84-
size="xl"
85-
block
86-
:label="$dict('common.to-home')"
87-
/>
88-
</div>
82+
<UButton
83+
to="/"
84+
variant="solid"
85+
color="secondary"
86+
size="xl"
87+
block
88+
:label="$dict('common.to-home')"
89+
/>
90+
</div>
91+
</ClientOnly>
8992
</template>
9093

9194
<script setup lang="ts">
95+
import type { OrderDeliveryAddress, Warehouse } from '@nextorders/food-schema'
9296
import { useChannelStore } from '@nextorders/core/app/stores/channel'
9397
import { useOptionsStore } from '@nextorders/core/app/stores/options'
9498
9599
definePageMeta({
96100
layout: 'finish',
97101
})
98102
99-
const route = useRoute()
100-
101103
const channelStore = useChannelStore()
102104
const orderStore = useOrderStore()
103105
const optionsStore = useOptionsStore()
104106
107+
const route = useRoute()
108+
105109
const order = await orderStore.get(route.query.id?.toString() as string)
106-
if (!order) {
107-
await navigateTo('/')
108-
}
109110
110-
const deliveryAddress = computed(() => order?.address.type === 'deliveryAddress' ? order.address : null)
111-
const warehouseId = computed(() => order?.address.type === 'warehouseAddress' && order.address.id)
112-
const warehouse = computed(() => channelStore.selfPickup?.warehouses?.find((w) => w.id === warehouseId.value))
111+
const deliveryAddress = computed<OrderDeliveryAddress | undefined>(() => order?.address.type === 'deliveryAddress' ? order.address : undefined)
112+
const warehouseId = computed<string | undefined>(() => order?.address.type === 'warehouseAddress' ? order.address.id : undefined)
113+
const warehouse = computed<Warehouse | undefined>(() => channelStore.selfPickup?.warehouses?.find((w) => w.id === warehouseId.value))
113114
114-
const availablePaymentMethods = computed(() => orderStore.deliveryMethod === 'deliveryByCourier' ? channelStore.deliveryByCourier?.paymentMethods : channelStore.selfPickup?.paymentMethods)
115-
const paymentMethod = computed(() => availablePaymentMethods.value?.find((p) => p.id === order?.paymentMethodId)?.title)
115+
const availablePaymentMethods = computed(() => order?.deliveryMethod === 'deliveryByCourier' ? channelStore.deliveryByCourier?.paymentMethods : channelStore.selfPickup?.paymentMethods)
116+
const paymentMethodTitle = computed(() => availablePaymentMethods.value?.find((p) => p.id === order?.paymentMethodId)?.title)
117+
118+
const { pop } = useConfetti()
119+
120+
onMounted(() => {
121+
pop()
122+
})
116123
</script>

0 commit comments

Comments
 (0)