diff --git a/frontend/e2e/admin-dashboard.spec.ts b/frontend/e2e/admin-dashboard.spec.ts index ed8abd0..8272ab6 100644 --- a/frontend/e2e/admin-dashboard.spec.ts +++ b/frontend/e2e/admin-dashboard.spec.ts @@ -47,13 +47,30 @@ test.describe('Admin dashboard', () => { await expect(page.getByText('GHI789').first()).toBeVisible() }) - test('click expand button shows letter content', async ({ page }) => { + test('show message button opens modal with full letter text', async ({ + page, + }) => { + await page.goto('/admin') + + await page + .locator('.admin__row', { hasText: 'ABC123' }) + .getByRole('button', { name: 'Visa meddelande' }) + .click() + + const dialog = page.getByRole('dialog', { name: 'Brevtext' }) + await expect(dialog).toBeVisible() + await expect(dialog).toContainText('fin bil') + await dialog.getByRole('button', { name: 'Stäng' }).click() + await expect(dialog).not.toBeVisible() + }) + + test('click expand button shows tracking section', async ({ page }) => { await page.goto('/admin') const expandBtns = page.locator('.admin__expand-btn') await expandBtns.first().click() - await expect(page.getByText('Brevtext')).toBeVisible() + await expect(page.getByText('Spårnings-ID').first()).toBeVisible() }) test('click expand button again collapses it', async ({ page }) => { @@ -61,10 +78,10 @@ test.describe('Admin dashboard', () => { const expandBtns = page.locator('.admin__expand-btn') await expandBtns.first().click() - await expect(page.getByText('Brevtext')).toBeVisible() + await expect(page.locator('.admin__tracking-input').first()).toBeVisible() await expandBtns.first().click() - await expect(page.getByText('Brevtext')).not.toBeVisible() + await expect(page.locator('.admin__tracking-input').first()).not.toBeVisible() }) test('status dropdown changes update order status', async ({ page }) => { diff --git a/frontend/src/__tests__/AdminDashboard.spec.ts b/frontend/src/__tests__/AdminDashboard.spec.ts index 8cffd4a..901d4c2 100644 --- a/frontend/src/__tests__/AdminDashboard.spec.ts +++ b/frontend/src/__tests__/AdminDashboard.spec.ts @@ -55,6 +55,16 @@ const mockOrders = [ amountPaid: null, createdAt: '2026-05-14T13:00:00Z', }, + { + id: 'c3eebc99-9c0b-4ef8-bb6d-6bb9bd380a13', + email: 'pending@example.com', + plate: 'PND111', + letterText: 'Väntar på betalning.', + status: 'pending_payment', + trackingId: null, + amountPaid: null, + createdAt: '2026-05-15T14:00:00Z', + }, ] describe('AdminDashboard', () => { @@ -91,8 +101,10 @@ describe('AdminDashboard', () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Datum') + expect(wrapper.text()).toContain('Beställnings-ID') expect(wrapper.text()).toContain('E-post') expect(wrapper.text()).toContain('Regnr') + expect(wrapper.text()).toContain('Meddelande') expect(wrapper.text()).toContain('Status') }) @@ -121,19 +133,30 @@ describe('AdminDashboard', () => { expect(wrapper.text()).toContain('Kunde inte hämta beställningar') }) - it('expands row on button click to show letter content', async () => { + it('opens message modal with full letter text', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) - const rows = wrapper.findAll('.admin__row') - expect(rows.length).toBe(2) - - const expandBtns = wrapper.findAll('.admin__expand-btn') - await expandBtns[0].trigger('click') + const messageBtns = wrapper.findAll('.admin__message-btn') + await messageBtns[0].trigger('click') await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).toContain('Hej fin bil!') - expect(wrapper.text()).toContain('Brevtext') + expect(wrapper.find('.admin-modal').exists()).toBe(true) + expect(wrapper.find('.admin-modal__body').text()).toBe('Hej fin bil!') + expect(wrapper.text()).toContain('ABC123') + expect(wrapper.text()).toContain('c1eebc99') + }) + + it('closes message modal on close button click', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + await wrapper.findAll('.admin__message-btn')[0].trigger('click') + await new Promise((r) => setTimeout(r, 50)) + await wrapper.find('.admin-modal__close').trigger('click') + await new Promise((r) => setTimeout(r, 50)) + + expect(wrapper.find('.admin-modal').exists()).toBe(false) }) it('collapses row on second button click', async () => { @@ -143,11 +166,11 @@ describe('AdminDashboard', () => { const expandBtns = wrapper.findAll('.admin__expand-btn') await expandBtns[0].trigger('click') await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).toContain('Hej fin bil!') + expect(wrapper.find('.admin__expanded-row').exists()).toBe(true) await expandBtns[0].trigger('click') await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).not.toContain('Hej fin bil!') + expect(wrapper.find('.admin__expanded-row').exists()).toBe(false) }) it('only expands one row at a time', async () => { @@ -157,12 +180,12 @@ describe('AdminDashboard', () => { const expandBtns = wrapper.findAll('.admin__expand-btn') await expandBtns[0].trigger('click') await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).toContain('Hej fin bil!') + expect(wrapper.findAll('.admin__expanded-row')).toHaveLength(1) await expandBtns[1].trigger('click') await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).not.toContain('Hej fin bil!') - expect(wrapper.text()).toContain('Vill köpa din bil.') + expect(wrapper.findAll('.admin__expanded-row')).toHaveLength(1) + expect(wrapper.find('.admin__tracking-row').exists()).toBe(true) }) it('renders status dropdowns', async () => { @@ -170,7 +193,7 @@ describe('AdminDashboard', () => { await new Promise((r) => setTimeout(r, 50)) const selects = wrapper.findAll('.admin__status-select') - expect(selects.length).toBe(2) + expect(selects.length).toBe(3) }) it('fires status update API on dropdown change', async () => { @@ -309,11 +332,67 @@ describe('AdminDashboard', () => { expect(wrapper.text()).toContain('Att göra') }) + it('shows visa meddelande button in each row', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + expect(wrapper.findAll('.admin__message-btn')).toHaveLength(3) + expect(wrapper.text()).toContain('Visa meddelande') + }) + + it('filters orders when Väntar tab is clicked', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const stats = wrapper.findAll('.admin__stat') + const waitingTab = stats.find((stat) => stat.text().includes('Väntar')) + expect(waitingTab).toBeDefined() + await waitingTab!.trigger('click') + await new Promise((r) => setTimeout(r, 50)) + + expect(wrapper.text()).toContain('pending@example.com') + expect(wrapper.text()).not.toContain('test@bilhalsning.se') + expect(wrapper.text()).not.toContain('user@example.com') + }) + + it('filters orders by partial order id search', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + await wrapper.find('#admin-order-search').setValue('c2eebc99') + await new Promise((r) => setTimeout(r, 50)) + + expect(wrapper.text()).toContain('user@example.com') + expect(wrapper.text()).not.toContain('test@bilhalsning.se') + expect(wrapper.text()).not.toContain('pending@example.com') + }) + + it('filters orders by registration number search', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + await wrapper.find('#admin-order-search').setValue('abc123') + await new Promise((r) => setTimeout(r, 50)) + + expect(wrapper.text()).toContain('test@bilhalsning.se') + expect(wrapper.text()).not.toContain('user@example.com') + expect(wrapper.text()).not.toContain('pending@example.com') + }) + + it('shows shortened order id with full id in title', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const orderIdCell = wrapper.find('.admin__order-id') + expect(orderIdCell.text()).toBe('c1eebc99') + expect(orderIdCell.attributes('title')).toBe(mockOrders[0].id) + }) + it('highlights processing rows', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const rows = wrapper.findAll('.admin__row') - expect(rows[1].classes()).toContain('admin__row--todo') + const processingRow = rows.find((row) => row.text().includes('XYZ789')) + expect(processingRow?.classes()).toContain('admin__row--todo') }) }) diff --git a/frontend/src/pages/AdminPage.vue b/frontend/src/pages/AdminPage.vue index f07f5a9..2021671 100644 --- a/frontend/src/pages/AdminPage.vue +++ b/frontend/src/pages/AdminPage.vue @@ -1,5 +1,5 @@ @@ -138,24 +191,64 @@ onMounted(async () => { - + {{ stats.total }} Totalt - - + + {{ stats.todo }} Att göra - - + + {{ stats.paid }} Betalda - - + + {{ stats.pending }} Väntar - + + + Sök beställnings-ID eller regnr + + + + + Inga beställningar matchar filtret. + + { {{ statusError }} - + Datum + Beställnings-ID E-post Regnr + Meddelande Status - + {{ formatDate(order.createdAt) }} + + {{ shortOrderId(order.id) }} + {{ order.email }} {{ order.plate }} + + + Visa meddelande + + { v-if="expandedOrderId === order.id" class="admin__expanded-row" > - + - - Brevtext - - {{ order.letterText }} - - - Spårnings-ID @@ -313,6 +413,52 @@ onMounted(async () => { + + + + + + Brevtext + + + + + + + + + + {{ messageModalOrder.plate }} · + {{ shortOrderId(messageModalOrder.id) }} + + + {{ messageModalOrder.letterText }} + + + @@ -343,13 +489,67 @@ onMounted(async () => { padding: var(--space-md) var(--space-lg); text-align: center; box-shadow: var(--shadow-sm); + cursor: pointer; + font: inherit; + width: 100%; + transition: + border-color var(--transition-fast), + background var(--transition-fast); } -.admin__stat--todo { +.admin__stat:hover { + border-color: var(--color-primary); +} + +.admin__stat--active { background: var(--color-primary-soft); border-color: var(--color-primary); } +.admin__toolbar { + margin-bottom: var(--space-lg); +} + +.admin__search-label { + display: block; + font-size: 0.75rem; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--space-xs); +} + +.admin__search-input { + width: 100%; + max-width: 24rem; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.875rem; + color: var(--color-ink); + outline: none; + transition: border-color var(--transition-fast); +} + +.admin__search-input:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px var(--color-primary-ring); +} + +.admin__filter-empty { + margin-bottom: var(--space-md); +} + +.admin__order-id { + font-family: ui-monospace, monospace; + font-size: 0.8125rem; +} + +.admin__message-btn { + white-space: nowrap; +} + .admin__stat-value { display: block; font-size: 1.5rem; @@ -559,6 +759,82 @@ onMounted(async () => { font-size: 0.8125rem; } +.admin-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: var(--space-md); +} + +.admin-modal { + background: var(--color-surface); + border-radius: var(--radius-xl); + width: 100%; + max-width: 32rem; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-card); + border: 1px solid var(--color-border); +} + +.admin-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-lg) var(--space-lg) 0; +} + +.admin-modal__title { + margin: 0; + font-size: 1.125rem; + color: var(--color-ink); +} + +.admin-modal__close { + background: none; + border: none; + color: var(--color-muted); + cursor: pointer; + padding: 0.25rem; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + transition: + color var(--transition-fast), + background var(--transition-fast); +} + +.admin-modal__close:hover { + color: var(--color-ink); + background: var(--color-border-light); +} + +.admin-modal__meta { + margin: var(--space-sm) var(--space-lg) 0; + font-size: 0.8125rem; + color: var(--color-muted); + font-family: ui-monospace, monospace; +} + +.admin-modal__body { + margin: var(--space-md) var(--space-lg) var(--space-lg); + padding: var(--space-md); + background: var(--color-border-light); + border-radius: var(--radius-md); + font-size: 0.875rem; + color: var(--color-ink); + line-height: 1.6; + white-space: pre-wrap; + overflow-y: auto; + max-height: 60vh; +} + @media (max-width: 768px) { .admin__stats { grid-template-columns: repeat(2, 1fr);
+ Inga beställningar matchar filtret. +
{ {{ statusError }}
+ {{ messageModalOrder.plate }} · + {{ shortOrderId(messageModalOrder.id) }} +