feat: add payment page and wire compose submit to payment flow
- api/payment.ts: payOrder(orderId) calls POST /api/payment/{id}/pay
- api/orders.ts: add amountPaid (number|null) to Order type
- PaymentRedirect.vue: route /betalning/:orderId, shows plate from
query?plate, amount label (49 kr), green Betalt button, mock note:
"Detta är en mock-betalning. I framtiden skickas du till Stripe."
On click: calls payOrder, on success navigates to /orders, on
failure shows error. Button disables and shows "Bearbetar..." while
paying.
- ComposePage.vue: after createOrder success, captures returned order
object and navigates to /betalning/{orderId}?plate=... instead of
the old direct-to-orders route
- Router: add /betalning/:orderId route (name: payment, component:
PaymentRedirect, meta: { requiresAuth: true })
This commit is contained in:
parent
d27bde2fbe
commit
c3c1513ac1
5 changed files with 160 additions and 2 deletions
|
|
@ -5,6 +5,7 @@ export interface Order {
|
||||||
plate: string
|
plate: string
|
||||||
status: string
|
status: string
|
||||||
trackingId: string | null
|
trackingId: string | null
|
||||||
|
amountPaid: number | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
8
frontend/src/api/payment.ts
Normal file
8
frontend/src/api/payment.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { request } from './client'
|
||||||
|
import type { Order } from './orders'
|
||||||
|
|
||||||
|
export function payOrder(orderId: string): Promise<Order> {
|
||||||
|
return request<Order>(`/payment/${orderId}/pay`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,12 @@ async function handleSubmit() {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createOrder(plate.value, letterText.value)
|
const order = await createOrder(plate.value, letterText.value)
|
||||||
await router.push({ name: 'orders' })
|
await router.push({
|
||||||
|
name: 'payment',
|
||||||
|
params: { orderId: order.id },
|
||||||
|
query: { plate: plate.value },
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.'
|
errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.'
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
138
frontend/src/pages/PaymentRedirect.vue
Normal file
138
frontend/src/pages/PaymentRedirect.vue
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { payOrder } from '@/api/payment'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const orderId = route.params.orderId as string
|
||||||
|
const paying = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function handlePay() {
|
||||||
|
paying.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await payOrder(orderId)
|
||||||
|
await router.push({ name: 'orders' })
|
||||||
|
} catch {
|
||||||
|
error.value = 'Kunde inte genomföra betalningen. Försök igen.'
|
||||||
|
} finally {
|
||||||
|
paying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="payment">
|
||||||
|
<h1 class="payment__title">Betalning</h1>
|
||||||
|
<p class="payment__subtitle">
|
||||||
|
Registreringsnummer: <strong>{{ route.query.plate || '—' }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="payment__card">
|
||||||
|
<div class="payment__amount-row">
|
||||||
|
<span class="payment__label">Att betala</span>
|
||||||
|
<span class="payment__amount">49 kr</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error" class="payment__error">{{ error }}</p>
|
||||||
|
|
||||||
|
<button class="payment__button" :disabled="paying" @click="handlePay">
|
||||||
|
{{ paying ? 'Bearbetar...' : 'Betalt' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p class="payment__note">
|
||||||
|
Detta är en mock-betalning. I framtiden skickas du till Stripe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment {
|
||||||
|
max-width: 28rem;
|
||||||
|
margin: 3rem auto 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__title {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__subtitle {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: #718096;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__amount-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__amount {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__error {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #fff5f5;
|
||||||
|
border: 1px solid #fed7d7;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: #c53030;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #48bb78;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__button:hover:not(:disabled) {
|
||||||
|
background: #38a169;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment__note {
|
||||||
|
margin: 0.75rem 0 0 0;
|
||||||
|
color: #a0aec0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -7,6 +7,7 @@ import RegisterPage from '@/pages/RegisterPage.vue'
|
||||||
import LoginPage from '@/pages/LoginPage.vue'
|
import LoginPage from '@/pages/LoginPage.vue'
|
||||||
import OrdersPage from '@/pages/OrdersPage.vue'
|
import OrdersPage from '@/pages/OrdersPage.vue'
|
||||||
import AdminPage from '@/pages/AdminPage.vue'
|
import AdminPage from '@/pages/AdminPage.vue'
|
||||||
|
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { getActivePinia } from 'pinia'
|
import { getActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
|
@ -36,6 +37,12 @@ const router = createRouter({
|
||||||
component: AdminPage,
|
component: AdminPage,
|
||||||
meta: { requiresAuth: true, requiresAdmin: true },
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/betalning/:orderId',
|
||||||
|
name: 'payment',
|
||||||
|
component: PaymentRedirect,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/registrera',
|
path: '/registrera',
|
||||||
name: 'register',
|
name: 'register',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue