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
|
||||
status: string
|
||||
trackingId: string | null
|
||||
amountPaid: number | null
|
||||
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 = ''
|
||||
|
||||
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 {
|
||||
|
|
|
|||
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 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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue