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:
Joakim Mörling 2026-05-15 20:30:15 +02:00
parent d27bde2fbe
commit c3c1513ac1
5 changed files with 160 additions and 2 deletions

View file

@ -5,6 +5,7 @@ export interface Order {
plate: string
status: string
trackingId: string | null
amountPaid: number | null
createdAt: string
}

View 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',
})
}

View file

@ -34,8 +34,12 @@ async function handleSubmit() {
errorMessage.value = ''
try {
await createOrder(plate.value, letterText.value)
await router.push({ name: 'orders' })
const order = await createOrder(plate.value, letterText.value)
await router.push({
name: 'payment',
params: { orderId: order.id },
query: { plate: plate.value },
})
} catch {
errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.'
} finally {

View 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>

View file

@ -7,6 +7,7 @@ import RegisterPage from '@/pages/RegisterPage.vue'
import LoginPage from '@/pages/LoginPage.vue'
import OrdersPage from '@/pages/OrdersPage.vue'
import AdminPage from '@/pages/AdminPage.vue'
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
import { useAuthStore } from '@/stores/authStore'
import { getActivePinia } from 'pinia'
@ -36,6 +37,12 @@ const router = createRouter({
component: AdminPage,
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: '/betalning/:orderId',
name: 'payment',
component: PaymentRedirect,
meta: { requiresAuth: true },
},
{
path: '/registrera',
name: 'register',