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()
})
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 }) => {
await page.goto('/logga-in')
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')

View file

@ -17,6 +17,11 @@ function createTestRouter() {
history: createMemoryHistory(),
routes: [
{ path: '/orders', name: 'orders', component: OrdersPage },
{
path: '/betalning/:orderId',
name: 'payment',
component: { template: '<div>Payment</div>' },
},
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
],
})
@ -38,6 +43,7 @@ const mockOrders = [
{
id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
plate: 'ABC123',
letterText: 'Hej fin bil!',
status: 'sent',
trackingId: 'PN123456789',
createdAt: '2026-05-11T12:00:00Z',
@ -45,6 +51,7 @@ const mockOrders = [
{
id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
plate: 'DEF456',
letterText: 'Vill köpa din bil.',
status: 'pending_payment',
trackingId: null,
createdAt: '2026-05-14T13:00:00Z',
@ -112,6 +119,7 @@ describe('OrdersPage', () => {
{
id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
plate: 'DEF456',
letterText: 'Test',
status: 'pending_payment',
trackingId: null,
createdAt: '2026-05-14T13:00:00Z',
@ -126,6 +134,16 @@ describe('OrdersPage', () => {
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 () => {
const { wrapper } = mountPage()
await new Promise((r) => setTimeout(r, 50))
@ -156,11 +174,35 @@ describe('OrdersPage', () => {
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 () => {
const ordersWithProcessing = [
{
id: 'c4eebc99-9c0b-4ef8-bb6d-6bb9bd380a14',
plate: 'XYZ123',
letterText: 'Processing message',
status: 'processing',
trackingId: null,
createdAt: '2026-05-15T10:00:00Z',

View file

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

View file

@ -86,6 +86,14 @@ onMounted(async () => {
</div>
<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-value">{{
formatDate(order.createdAt)
@ -103,6 +111,22 @@ onMounted(async () => {
</a>
</template>
</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>
@ -177,6 +201,17 @@ onMounted(async () => {
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 {
font-size: 0.875rem;
color: var(--color-primary);
@ -188,6 +223,17 @@ onMounted(async () => {
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 {
padding: var(--space-2xl) 0;
text-align: center;