diff --git a/src/app/admin/customers/actions.ts b/src/app/admin/customers/actions.ts index cc85389..c8a5604 100644 --- a/src/app/admin/customers/actions.ts +++ b/src/app/admin/customers/actions.ts @@ -31,7 +31,19 @@ export async function sendBulkEmail(formData: FormData) { } : null; - const recipients = await prisma.mailingListEntry.findMany({ where: { subscribed: true } }); + // Get selected customer IDs from form + const selectedIds = formData.getAll('selectedCustomers') as string[]; + + let recipients; + if (selectedIds.length > 0) { + // Send to selected customers only + recipients = await prisma.mailingListEntry.findMany({ + where: { id: { in: selectedIds } }, + }); + } else { + // Fall back to all subscribed if no selection made + recipients = await prisma.mailingListEntry.findMany({ where: { subscribed: true } }); + } let sent = 0; let failed = 0; diff --git a/src/app/admin/customers/page.tsx b/src/app/admin/customers/page.tsx index ea3cc4d..89a7a3c 100644 --- a/src/app/admin/customers/page.tsx +++ b/src/app/admin/customers/page.tsx @@ -1,5 +1,6 @@ import { prisma } from '@/lib/prisma'; import AdminNav from '@/components/AdminNav'; +import CustomerSelector from '@/components/CustomerSelector'; import { updateSubscribed, sendBulkEmail } from './actions'; const SOURCE_LABELS: Record = { @@ -75,6 +76,8 @@ export default async function CustomersPage({

Send an email

+ +
diff --git a/src/components/CustomerSelector.tsx b/src/components/CustomerSelector.tsx new file mode 100644 index 0000000..e67d34c --- /dev/null +++ b/src/components/CustomerSelector.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { useState, useMemo } from 'react'; + +interface MailingListEntry { + id: string; + name: string | null; + email: string; + source: string; + subscribed: boolean; +} + +interface Props { + entries: MailingListEntry[]; + sourceLabels: Record; +} + +export default function CustomerSelector({ entries, sourceLabels }: Props) { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const filtered = useMemo(() => { + if (!searchTerm) return entries.filter((e) => e.subscribed); + return entries.filter( + (e) => + e.subscribed && + (e.email.toLowerCase().includes(searchTerm.toLowerCase()) || + (e.name && e.name.toLowerCase().includes(searchTerm.toLowerCase()))) + ); + }, [searchTerm, entries]); + + const toggleAll = () => { + if (selectedIds.size === filtered.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(filtered.map((e) => e.id))); + } + }; + + const toggleCustomer = (id: string) => { + const newSelected = new Set(selectedIds); + if (newSelected.has(id)) { + newSelected.delete(id); + } else { + newSelected.add(id); + } + setSelectedIds(newSelected); + }; + + return ( +
+

Select customers to email

+ +
+ setSearchTerm(e.target.value)} + className="w-full border border-line bg-paper px-3 py-2 text-sm" + /> +

+ Showing {filtered.length} subscribed customer{filtered.length !== 1 ? 's' : ''} + {searchTerm && ` matching "${searchTerm}"`} +

+
+ + {filtered.length === 0 ? ( +

No customers found.

+ ) : ( +
+
+ 0 && selectedIds.size === filtered.length} + onChange={toggleAll} + className="h-4 w-4 accent-clay" + /> + +
+ +
+ {filtered.map((customer) => ( +
+ toggleCustomer(customer.id)} + className="h-4 w-4 accent-clay" + /> + +
+ ))} +
+
+ )} + + {/* Hidden inputs for selected customer IDs */} + {Array.from(selectedIds).map((id) => ( + + ))} + + {selectedIds.size > 0 && ( +
+

+ Will send to {selectedIds.size} customer{selectedIds.size !== 1 ? 's' : ''} +

+
+ )} +
+ ); +}