Improve orders page with details and deferred payment.

Users who leave the payment step can return later and still see what
they ordered. Unpaid orders get a clear path back to Swish checkout.

- Add letterText to frontend Order type
- Show beställnings-ID, message, and formatted date on each order card
- Add "Betala nu" link to payment route for pending_payment orders
- Extend OrdersPage unit tests and order-history e2e for pay-later flow
This commit is contained in:
Joakim Mörling 2026-05-21 14:49:50 +02:00
parent e2bccb4029
commit dfb3e0dedc
4 changed files with 109 additions and 0 deletions

View file

@ -54,6 +54,26 @@ test.describe('Order history', () => {
await expect(page.getByText('Levererat').first()).toBeVisible() await expect(page.getByText('Levererat').first()).toBeVisible()
}) })
test('shows pay button for unpaid order and opens payment page', async ({
page,
}) => {
await page.goto('/logga-in')
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
await page.getByLabel('Lösenord').fill('test1234')
await page.getByRole('button', { name: 'Logga in' }).click()
await page.waitForURL('/')
await page.goto('/orders')
const unpaidCard = page.locator('.orders__card', { hasText: 'DEF456' })
await expect(unpaidCard.getByRole('link', { name: 'Betala nu' })).toBeVisible()
await unpaidCard.getByRole('link', { name: 'Betala nu' }).click()
await expect(page).toHaveURL(/\/betalning\/c2eebc99/)
await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible()
await expect(page.getByText('DEF456')).toBeVisible()
})
test('shows tracking links for orders with tracking ID', async ({ page }) => { test('shows tracking links for orders with tracking ID', async ({ page }) => {
await page.goto('/logga-in') await page.goto('/logga-in')
await page.getByLabel('E-postadress').fill('test@bilhalsning.se') await page.getByLabel('E-postadress').fill('test@bilhalsning.se')

View file

@ -17,6 +17,11 @@ function createTestRouter() {
history: createMemoryHistory(), history: createMemoryHistory(),
routes: [ routes: [
{ path: '/orders', name: 'orders', component: OrdersPage }, { path: '/orders', name: 'orders', component: OrdersPage },
{
path: '/betalning/:orderId',
name: 'payment',
component: { template: '<div>Payment</div>' },
},
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } }, { path: '/', name: 'home', component: { template: '<div>Home</div>' } },
], ],
}) })
@ -38,6 +43,7 @@ const mockOrders = [
{ {
id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
plate: 'ABC123', plate: 'ABC123',
letterText: 'Hej fin bil!',
status: 'sent', status: 'sent',
trackingId: 'PN123456789', trackingId: 'PN123456789',
createdAt: '2026-05-11T12:00:00Z', createdAt: '2026-05-11T12:00:00Z',
@ -45,6 +51,7 @@ const mockOrders = [
{ {
id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
plate: 'DEF456', plate: 'DEF456',
letterText: 'Vill köpa din bil.',
status: 'pending_payment', status: 'pending_payment',
trackingId: null, trackingId: null,
createdAt: '2026-05-14T13:00:00Z', createdAt: '2026-05-14T13:00:00Z',
@ -112,6 +119,7 @@ describe('OrdersPage', () => {
{ {
id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
plate: 'DEF456', plate: 'DEF456',
letterText: 'Test',
status: 'pending_payment', status: 'pending_payment',
trackingId: null, trackingId: null,
createdAt: '2026-05-14T13:00:00Z', createdAt: '2026-05-14T13:00:00Z',
@ -126,6 +134,16 @@ describe('OrdersPage', () => {
expect(link.exists()).toBe(false) expect(link.exists()).toBe(false)
}) })
it('renders order id and message', async () => {
const { wrapper } = mountPage()
await new Promise((r) => setTimeout(r, 50))
expect(wrapper.text()).toContain('Beställnings-ID')
expect(wrapper.text()).toContain('c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11')
expect(wrapper.text()).toContain('Meddelande')
expect(wrapper.text()).toContain('Hej fin bil!')
expect(wrapper.text()).toContain('Vill köpa din bil.')
})
it('renders formatted date', async () => { it('renders formatted date', async () => {
const { wrapper } = mountPage() const { wrapper } = mountPage()
await new Promise((r) => setTimeout(r, 50)) await new Promise((r) => setTimeout(r, 50))
@ -156,11 +174,35 @@ describe('OrdersPage', () => {
expect(badges[1].classes()).toContain('badge--muted') expect(badges[1].classes()).toContain('badge--muted')
}) })
it('shows pay button only for pending payment orders', async () => {
const { wrapper } = mountPage()
await new Promise((r) => setTimeout(r, 50))
const payLinks = wrapper.findAll('.orders__pay-btn')
expect(payLinks).toHaveLength(1)
expect(payLinks[0].text()).toBe('Betala nu')
const href = payLinks[0].attributes('href')
expect(href).toContain('c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12')
expect(href).toContain('plate=DEF456')
})
it('does not show pay button for paid or sent orders', async () => {
const { wrapper } = mountPage()
await new Promise((r) => setTimeout(r, 50))
const sentCard = wrapper
.findAll('.orders__card')
.find((card) => card.text().includes('ABC123'))
expect(sentCard?.find('.orders__pay-btn').exists()).toBe(false)
})
it('renders processing status correctly', async () => { it('renders processing status correctly', async () => {
const ordersWithProcessing = [ const ordersWithProcessing = [
{ {
id: 'c4eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', id: 'c4eebc99-9c0b-4ef8-bb6d-6bb9bd380a14',
plate: 'XYZ123', plate: 'XYZ123',
letterText: 'Processing message',
status: 'processing', status: 'processing',
trackingId: null, trackingId: null,
createdAt: '2026-05-15T10:00:00Z', createdAt: '2026-05-15T10:00:00Z',

View file

@ -3,6 +3,7 @@ import { request } from './client'
export interface Order { export interface Order {
id: string id: string
plate: string plate: string
letterText: string
status: string status: string
trackingId: string | null trackingId: string | null
amountPaid: number | null amountPaid: number | null

View file

@ -86,6 +86,14 @@ onMounted(async () => {
</div> </div>
<div class="orders__card-meta"> <div class="orders__card-meta">
<span class="orders__meta-label">Beställnings-ID</span>
<span class="orders__meta-value orders__order-id">{{ order.id }}</span>
<span class="orders__meta-label">Meddelande</span>
<span class="orders__meta-value orders__message">{{
order.letterText
}}</span>
<span class="orders__meta-label">Datum</span> <span class="orders__meta-label">Datum</span>
<span class="orders__meta-value">{{ <span class="orders__meta-value">{{
formatDate(order.createdAt) formatDate(order.createdAt)
@ -103,6 +111,22 @@ onMounted(async () => {
</a> </a>
</template> </template>
</div> </div>
<div
v-if="order.status === 'pending_payment'"
class="orders__card-actions"
>
<RouterLink
:to="{
name: 'payment',
params: { orderId: order.id },
query: { plate: order.plate },
}"
class="btn btn--primary orders__pay-btn"
>
Betala nu
</RouterLink>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -177,6 +201,17 @@ onMounted(async () => {
color: var(--color-ink); color: var(--color-ink);
} }
.orders__order-id {
font-family: ui-monospace, monospace;
font-size: 0.8125rem;
word-break: break-all;
}
.orders__message {
white-space: pre-wrap;
line-height: 1.5;
}
.orders__tracking { .orders__tracking {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-primary); color: var(--color-primary);
@ -188,6 +223,17 @@ onMounted(async () => {
text-decoration: underline; text-decoration: underline;
} }
.orders__card-actions {
padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--color-border);
background: var(--color-border-light);
}
.orders__pay-btn {
width: 100%;
justify-content: center;
}
.orders__empty { .orders__empty {
padding: var(--space-2xl) 0; padding: var(--space-2xl) 0;
text-align: center; text-align: center;