diff --git a/src/pages/admin/AdminInquiries.module.css b/src/pages/admin/AdminInquiries.module.css new file mode 100644 index 0000000..d050b1d --- /dev/null +++ b/src/pages/admin/AdminInquiries.module.css @@ -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; +} diff --git a/src/pages/admin/AdminInquiries.test.tsx b/src/pages/admin/AdminInquiries.test.tsx new file mode 100644 index 0000000..547eade --- /dev/null +++ b/src/pages/admin/AdminInquiries.test.tsx @@ -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() + 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() + 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') + }) +}) diff --git a/src/pages/admin/AdminInquiries.tsx b/src/pages/admin/AdminInquiries.tsx new file mode 100644 index 0000000..8d8b229 --- /dev/null +++ b/src/pages/admin/AdminInquiries.tsx @@ -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([]) + 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 ( +
+

Inquiries

+ {error &&

{error}

} + + + + + + + + + + + + {inquiries.map((inquiry) => ( + + + + + + + + ))} + +
ProductContactNotesStatus
{inquiry.products?.name} +
{inquiry.contact_name}
+
{inquiry.contact_email}
+
{inquiry.contact_notes}{inquiry.status} + {inquiry.status === 'new' && ( + + )} +
+
+ ) +} + +export default AdminInquiries