feat: add admin inquiries page with mark-as-read action

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-12 11:49:53 +02:00
parent 5bcc5ed6c8
commit 85de1cf92e
3 changed files with 165 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
.page {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.table {
width: 100%;
border-collapse: collapse;
background: white;
}
.table th,
.table td {
text-align: left;
padding: var(--space-sm);
border-bottom: 1px solid var(--color-cream-alt);
vertical-align: top;
}
.error {
color: #a33;
}

View File

@@ -0,0 +1,51 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const orderMock = vi.fn()
const selectMock = vi.fn()
const eqUpdateMock = vi.fn()
const updateMock = vi.fn()
const fromMock = vi.fn()
vi.mock('../../lib/supabase', () => ({
supabase: { from: (...args: unknown[]) => fromMock(...args) },
}))
const INQUIRY = {
id: 'inquiry-1',
contact_name: 'Nadia',
contact_email: 'nadia@example.com',
contact_notes: 'Is this in stock?',
status: 'new',
products: { name: 'Silk Edge Control' },
}
import AdminInquiries from './AdminInquiries'
describe('AdminInquiries', () => {
beforeEach(() => {
orderMock.mockReset().mockResolvedValue({ data: [INQUIRY], error: null })
selectMock.mockReset().mockReturnValue({ order: orderMock })
eqUpdateMock.mockReset().mockResolvedValue({ error: null })
updateMock.mockReset().mockReturnValue({ eq: eqUpdateMock })
fromMock.mockReset().mockReturnValue({ select: selectMock, update: updateMock })
})
it('lists inquiries with the related product name', async () => {
render(<AdminInquiries />)
expect(await screen.findByText('Nadia')).toBeInTheDocument()
expect(screen.getByText('Silk Edge Control')).toBeInTheDocument()
expect(fromMock).toHaveBeenCalledWith('inquiries')
})
it('marks an inquiry as read', async () => {
render(<AdminInquiries />)
await screen.findByText('Nadia')
await userEvent.click(screen.getByRole('button', { name: /mark as read/i }))
await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'read' }))
expect(eqUpdateMock).toHaveBeenCalledWith('id', 'inquiry-1')
})
})

View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from 'react'
import { supabase } from '../../lib/supabase'
import styles from './AdminInquiries.module.css'
interface AdminInquiry {
id: string
contact_name: string
contact_email: string
contact_notes: string
status: 'new' | 'read'
products: { name: string } | null
}
function AdminInquiries() {
const [inquiries, setInquiries] = useState<AdminInquiry[]>([])
const [error, setError] = useState('')
async function loadInquiries() {
const { data, error: fetchError } = await supabase
.from('inquiries')
.select('id, contact_name, contact_email, contact_notes, status, products(name)')
.order('created_at', { ascending: false })
if (fetchError) {
setError(fetchError.message)
return
}
setInquiries((data ?? []) as unknown as AdminInquiry[])
}
useEffect(() => {
async function load() {
const { data, error: fetchError } = await supabase
.from('inquiries')
.select('id, contact_name, contact_email, contact_notes, status, products(name)')
.order('created_at', { ascending: false })
if (fetchError) {
setError(fetchError.message)
return
}
setInquiries((data ?? []) as unknown as AdminInquiry[])
}
void load()
}, [])
async function markRead(id: string) {
const { error: updateError } = await supabase.from('inquiries').update({ status: 'read' }).eq('id', id)
if (updateError) {
setError(updateError.message)
return
}
await loadInquiries()
}
return (
<div className={styles.page}>
<h1>Inquiries</h1>
{error && <p className={styles.error}>{error}</p>}
<table className={styles.table}>
<thead>
<tr>
<th>Product</th>
<th>Contact</th>
<th>Notes</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{inquiries.map((inquiry) => (
<tr key={inquiry.id}>
<td>{inquiry.products?.name}</td>
<td>
<div>{inquiry.contact_name}</div>
<div>{inquiry.contact_email}</div>
</td>
<td>{inquiry.contact_notes}</td>
<td>{inquiry.status}</td>
<td>
{inquiry.status === 'new' && (
<button type="button" onClick={() => markRead(inquiry.id)}>Mark as read</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default AdminInquiries