diff --git a/__tests__/lib/sponsor/sponsorForConference.test.ts b/__tests__/lib/sponsor/sponsorForConference.test.ts index 674e4f6f..5f3862d0 100644 --- a/__tests__/lib/sponsor/sponsorForConference.test.ts +++ b/__tests__/lib/sponsor/sponsorForConference.test.ts @@ -4,6 +4,8 @@ import { SponsorForConferenceUpdateSchema, SponsorTagSchema, ImportAllHistoricSponsorsSchema, + BulkUpdateSponsorCRMSchema, + BulkDeleteSponsorCRMSchema, } from '@/server/schemas/sponsorForConference' describe('SponsorForConferenceInputSchema', () => { @@ -180,3 +182,53 @@ describe('ImportAllHistoricSponsorsSchema', () => { expect(result.success).toBe(false) }) }) + +describe('BulkUpdateSponsorCRMSchema', () => { + it('passes with valid ids and status', () => { + const result = BulkUpdateSponsorCRMSchema.safeParse({ + ids: ['id-1', 'id-2'], + status: 'negotiating', + }) + expect(result.success).toBe(true) + }) + + it('passes with add_tags', () => { + const result = BulkUpdateSponsorCRMSchema.safeParse({ + ids: ['id-1'], + add_tags: ['high-priority', 'returning-sponsor'], + }) + expect(result.success).toBe(true) + }) + + it('fails with empty ids array', () => { + const result = BulkUpdateSponsorCRMSchema.safeParse({ + ids: [], + status: 'closed-won', + }) + expect(result.success).toBe(false) + }) + + it('fails with invalid status', () => { + const result = BulkUpdateSponsorCRMSchema.safeParse({ + ids: ['id-1'], + status: 'invalid', + }) + expect(result.success).toBe(false) + }) +}) + +describe('BulkDeleteSponsorCRMSchema', () => { + it('passes with valid ids', () => { + const result = BulkDeleteSponsorCRMSchema.safeParse({ + ids: ['id-1', 'id-2'], + }) + expect(result.success).toBe(true) + }) + + it('fails with empty ids', () => { + const result = BulkDeleteSponsorCRMSchema.safeParse({ + ids: [], + }) + expect(result.success).toBe(false) + }) +}) diff --git a/package.json b/package.json index 944b380b..35fb10a4 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "html2canvas-pro": "^1.6.6", "jose": "^6.1.3", "jsonld": "^9.0.0", + "nanoid": "^5.1.6", "next": "^16.1.6", "next-auth": "5.0.0-beta.30", "next-sanity": "^12.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5edb089c..994c1c51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: jsonld: specifier: ^9.0.0 version: 9.0.0 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 next: specifier: ^16.1.6 version: 16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) diff --git a/src/app/(admin)/admin/api/sponsors/email/send/route.ts b/src/app/(admin)/admin/api/sponsors/email/send/route.ts new file mode 100644 index 00000000..a0c080f8 --- /dev/null +++ b/src/app/(admin)/admin/api/sponsors/email/send/route.ts @@ -0,0 +1,92 @@ +import { auth, NextAuthRequest } from '@/lib/auth' +import { + setupEmailRoute, + convertPortableTextToHTML, + renderEmailTemplate, + createEmailSuccessResponse, + createEmailErrorResponse, +} from '@/lib/email/route-helpers' +import { resend, retryWithBackoff } from '@/lib/email/config' +import { getSponsor } from '@/lib/sponsor/sanity' + +export const POST = auth(async (req: NextAuthRequest) => { + try { + const body = await req.json() + const { sponsorId } = body + + if (!sponsorId) { + return createEmailErrorResponse('sponsorId is required', 400) + } + + const { context, error } = await setupEmailRoute(req, body, { + sponsors: true, + sponsorContact: true, + }) + + if (error) return error + + const { conference, messagePortableText, subject } = context! + + // Fetch sponsor contacts + const { sponsor, error: sponsorError } = await getSponsor(sponsorId, true) + if (sponsorError || !sponsor) { + return createEmailErrorResponse( + sponsorError?.message || 'Sponsor not found', + 404, + ) + } + + const contacts = 'contact_persons' in sponsor ? sponsor.contact_persons : [] + const recipients = contacts + ?.filter((c) => c.email) + .map((c) => ({ email: c.email, name: c.name })) + + if (!recipients || recipients.length === 0) { + return createEmailErrorResponse( + 'Sponsor has no contact persons with email addresses', + 400, + ) + } + + const { htmlContent, error: htmlError } = + await convertPortableTextToHTML(messagePortableText) + if (htmlError) return htmlError + + const emailTemplate = renderEmailTemplate({ + conference, + subject, + htmlContent: htmlContent!, + }) + + if (!conference.sponsor_email) { + return createEmailErrorResponse( + 'Missing sponsor_email in conference configuration', + 500, + ) + } + + const result = await retryWithBackoff(async () => { + return await resend.emails.send({ + from: `${conference.organizer || 'Cloud Native Days'} <${conference.sponsor_email}>`, + to: recipients.map((r) => r.email), + subject, + react: emailTemplate, + }) + }) + + if (result.error) { + return createEmailErrorResponse(result.error.message, 500) + } + + return createEmailSuccessResponse({ + emailId: result.data?.id, + recipientCount: recipients.length, + }) + } catch (err) { + console.error('[SponsorIndividualEmail] Unexpected error:', err) + return createEmailErrorResponse( + err instanceof Error ? err.message : 'Failed to send email', + 500, + ) + } +}) diff --git a/src/app/(admin)/admin/sponsors/crm/SponsorCRMClient.tsx b/src/app/(admin)/admin/sponsors/crm/SponsorCRMClient.tsx index 3ea5d52f..ad398c09 100644 --- a/src/app/(admin)/admin/sponsors/crm/SponsorCRMClient.tsx +++ b/src/app/(admin)/admin/sponsors/crm/SponsorCRMClient.tsx @@ -9,48 +9,68 @@ import { useState, useEffect } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { SponsorCRMForm } from '@/components/admin/sponsor-crm/SponsorCRMForm' import { ImportHistoricSponsorsButton } from '@/components/admin/sponsor-crm/ImportHistoricSponsorsButton' +import { SponsorIndividualEmailModal } from '@/components/admin/SponsorIndividualEmailModal' import { BoardViewSwitcher, type BoardView, } from '@/components/admin/sponsor-crm/BoardViewSwitcher' import { SponsorBoardColumn } from '@/components/admin/sponsor-crm/SponsorBoardColumn' +import { SponsorBulkActions } from '@/components/admin/sponsor-crm/SponsorBulkActions' import { FilterDropdown, FilterOption } from '@/components/admin/FilterDropdown' -import { XMarkIcon } from '@heroicons/react/20/solid' +import { XMarkIcon, CheckIcon, PlusIcon } from '@heroicons/react/20/solid' +import { Conference } from '@/lib/conference/types' interface SponsorCRMClientProps { conferenceId: string + conference: Conference + domain: string } -export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { +export function SponsorCRMClient({ + conferenceId, + conference, + domain, +}: SponsorCRMClientProps) { const router = useRouter() const searchParams = useSearchParams() const [selectedSponsor, setSelectedSponsor] = useState(null) + const [emailSponsor, setEmailSponsor] = + useState(null) + const [selectedIds, setSelectedIds] = useState([]) const [isFormOpen, setIsFormOpen] = useState(false) + const [isEmailModalOpen, setIsEmailModalOpen] = useState(false) const [currentView, setCurrentView] = useState('pipeline') // Parse filters from URL const tiersFilter = searchParams.get('tiers')?.split(',').filter(Boolean) || [] const assignedToFilter = searchParams.get('assigned_to') || undefined - const tagsFilter = searchParams.get('tags')?.split(',').filter(Boolean) as - | SponsorTag[] - | undefined + const tagsFilter = (searchParams.get('tags')?.split(',').filter(Boolean) || + []) as SponsorTag[] // Fetch with filters const { data: sponsors = [], isLoading } = api.sponsor.crm.list.useQuery({ conferenceId, assigned_to: assignedToFilter, - tags: tagsFilter, + tags: tagsFilter.length > 0 ? tagsFilter : undefined, tiers: tiersFilter.length > 0 ? tiersFilter : undefined, }) - // Fetch tiers and organizers for filters + // Fetch tiers for filters const { data: tiers = [] } = api.sponsor.tiers.listByConference.useQuery({ conferenceId, }) - const { data: organizers = [] } = api.sponsor.crm.listOrganizers.useQuery() + + // Use organizers from conference data + const organizers = + conference.organizers?.map((o) => ({ + _id: o._id, + name: o.name, + email: o.email, + avatar: o.image, + })) || [] const utils = api.useUtils() @@ -77,18 +97,60 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { setIsFormOpen(true) } + const handleCreateNew = () => { + setSelectedSponsor(null) + setIsFormOpen(true) + } + const handleCloseForm = () => { setSelectedSponsor(null) setIsFormOpen(false) } - // CMD+O / CTRL+O keyboard shortcut to open new sponsor form + const handleOpenEmail = (sponsor: SponsorForConferenceExpanded) => { + setEmailSponsor(sponsor) + setIsEmailModalOpen(true) + } + + const handleCloseEmail = () => { + setEmailSponsor(null) + setIsEmailModalOpen(false) + } + + const handleToggleSelect = (id: string) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id], + ) + } + + const handleClearSelection = () => { + setSelectedIds([]) + } + + const handleSelectAllFiltered = () => { + const allFilteredIds = filteredSponsors.map((s) => s._id) + setSelectedIds(allFilteredIds) + } + + // Clear selection when filters change to avoid accidental bulk updates on hidden items + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- Clear selection when view/filters change + handleClearSelection() + }, [tiersFilter.length, assignedToFilter, tagsFilter.length, currentView]) + + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + // CMD+O / CTRL+O: Open form (edit selected or new) if ((e.metaKey || e.ctrlKey) && e.key === 'o') { e.preventDefault() handleOpenForm() } + // CMD+N / CTRL+N: Create new sponsor + if ((e.metaKey || e.ctrlKey) && e.key === 'n') { + e.preventDefault() + handleCreateNew() + } } window.addEventListener('keydown', handleKeyDown) @@ -149,9 +211,9 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { } const toggleTagFilter = (tag: SponsorTag) => { - const newTags = tagsFilter?.includes(tag) + const newTags = tagsFilter.includes(tag) ? tagsFilter.filter((t) => t !== tag) - : [...(tagsFilter || []), tag] + : [...tagsFilter, tag] updateFilters('tags', newTags.length > 0 ? newTags.join(',') : null) } @@ -173,7 +235,7 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { // Calculate active filter count const activeFilterCount = - tiersFilter.length + (assignedToFilter ? 1 : 0) + (tagsFilter?.length || 0) + tiersFilter.length + (assignedToFilter ? 1 : 0) + tagsFilter.length // Available tags const availableTags: SponsorTag[] = [ @@ -238,19 +300,50 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { const columns = getColumns() + if (!conferenceId) { + return ( +
+

No conference selected

+
+ ) + } + return (
{/* Form Modal */} - { - utils.sponsor.crm.list.invalidate() - }} - existingSponsorsInCRM={sponsors.map((s) => s.sponsor._id)} - /> + {isFormOpen && ( + { + utils.sponsor.crm.list.invalidate() + }} + existingSponsorsInCRM={sponsors.map((s) => s.sponsor._id)} + /> + )} + + {/* Email Modal */} + {emailSponsor && conference && ( + + )} {/* Filters and View Switcher */}
@@ -309,7 +402,7 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { {/* Tags Filter */} @@ -317,7 +410,7 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { toggleTagFilter(tag)} - checked={tagsFilter?.includes(tag) || false} + checked={tagsFilter.includes(tag)} keepOpen > {tag @@ -330,18 +423,34 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) { {/* Clear Filters */} {activeFilterCount > 0 && ( - +
+ + +
)}
+ { @@ -355,6 +464,15 @@ export function SponsorCRMClient({ conferenceId }: SponsorCRMClientProps) {
+ {/* Bulk Actions Bar */} + { + utils.sponsor.crm.list.invalidate() + }} + /> + {/* Board Columns */}
0} onSponsorClick={handleOpenForm} onSponsorDelete={handleDelete} + onSponsorEmail={handleOpenEmail} + onSponsorToggleSelect={handleToggleSelect} onAddClick={() => handleOpenForm()} emptyMessage="No sponsors" /> diff --git a/src/app/(admin)/admin/sponsors/crm/page.tsx b/src/app/(admin)/admin/sponsors/crm/page.tsx index 72b9d9ff..7f1584c1 100644 --- a/src/app/(admin)/admin/sponsors/crm/page.tsx +++ b/src/app/(admin)/admin/sponsors/crm/page.tsx @@ -2,10 +2,18 @@ import { getConferenceForCurrentDomain } from '@/lib/conference/sanity' import { ErrorDisplay, AdminPageHeader } from '@/components/admin' import { SponsorCRMClient } from './SponsorCRMClient' import { BuildingOffice2Icon } from '@heroicons/react/24/outline' +import { headers } from 'next/headers' export default async function AdminSponsorsCRM() { + const headerList = await headers() + const domain = headerList.get('host') || 'localhost:3000' + const { conference, error: conferenceError } = - await getConferenceForCurrentDomain({}) + await getConferenceForCurrentDomain({ + sponsors: true, + sponsorContact: true, + sponsorTiers: true, + }) if (conferenceError || !conference) { return ( @@ -27,7 +35,11 @@ export default async function AdminSponsorsCRM() { backLink={{ href: '/admin/sponsors', label: 'Back to Dashboard' }} /> - +
) } diff --git a/src/app/(admin)/admin/tickets/discount/page.tsx b/src/app/(admin)/admin/tickets/discount/page.tsx index f98edded..e1edbb7e 100644 --- a/src/app/(admin)/admin/tickets/discount/page.tsx +++ b/src/app/(admin)/admin/tickets/discount/page.tsx @@ -13,8 +13,8 @@ import Link from 'next/link' interface SponsorWithTierInfo { id: string name: string - website: string - logo: string + website?: string + logo?: string tier: { title: string tagline: string diff --git a/src/components/admin/DiscountCodeManager.tsx b/src/components/admin/DiscountCodeManager.tsx index 758770be..6b897b62 100644 --- a/src/components/admin/DiscountCodeManager.tsx +++ b/src/components/admin/DiscountCodeManager.tsx @@ -25,8 +25,8 @@ import type { EventDiscountWithUsage } from '@/lib/discounts/types' interface SponsorWithTierInfo { id: string name: string - website: string - logo: string + website?: string + logo?: string tier: { title: string tagline: string diff --git a/src/components/admin/SponsorAddModal.tsx b/src/components/admin/SponsorAddModal.tsx index 98711449..c80a9cf0 100644 --- a/src/components/admin/SponsorAddModal.tsx +++ b/src/components/admin/SponsorAddModal.tsx @@ -427,15 +427,11 @@ export default function SponsorAddModal({ if (!formData.tierId) return false if (isCreatingNew) { - return ( - formData.name.trim() && formData.website.trim() && formData.logo.trim() - ) + return !!formData.name.trim() } if (editingSponsor || selectedExistingSponsor) { - return ( - formData.name.trim() && formData.website.trim() && formData.logo.trim() - ) + return !!formData.name.trim() } return false diff --git a/src/components/admin/SponsorContactTable.tsx b/src/components/admin/SponsorContactTable.tsx index d5717338..b35039f0 100644 --- a/src/components/admin/SponsorContactTable.tsx +++ b/src/components/admin/SponsorContactTable.tsx @@ -1,21 +1,26 @@ 'use client' +import { useState, useEffect, Fragment } from 'react' import { SponsorWithContactInfo, ContactPerson } from '@/lib/sponsor/types' import { EnvelopeIcon, BuildingOffice2Icon, ClipboardIcon, PencilIcon, - CheckIcon as CheckIconOutline, XMarkIcon, - PlusIcon, } from '@heroicons/react/24/outline' import { CheckIcon } from '@heroicons/react/24/solid' -import { useState, useEffect } from 'react' -import { ContactRoleSelect } from '@/components/common/ContactRoleSelect' import { api } from '@/lib/trpc/client' import { useNotification } from './NotificationProvider' import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' +import { SponsorContactEditor } from './sponsor/SponsorContactEditor' +import { + Dialog, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from '@headlessui/react' interface SponsorContactTableProps { sponsors: SponsorWithContactInfo[] @@ -63,235 +68,33 @@ interface ContactRow { isFirstContactForSponsor: boolean } -interface EditingContact { - name: string - email: string - phone: string - role: string - billing?: { - email: string - reference: string - comments: string - } -} - export function SponsorContactTable({ sponsors: initialSponsors, }: SponsorContactTableProps) { const [sponsors, setSponsors] = useState(initialSponsors) - const [editingRowId, setEditingRowId] = useState(null) - const [editingContact, setEditingContact] = useState({ - name: '', - email: '', - phone: '', - role: '', - billing: { - email: '', - reference: '', - comments: '', - }, - }) - const [savingRowId, setSavingRowId] = useState(null) - const { showNotification } = useNotification() + const [editingSponsor, setEditingSponsor] = + useState(null) const utils = api.useUtils() useEffect(() => { setSponsors(initialSponsors) }, [initialSponsors]) - const resetEditingState = () => { - setEditingRowId(null) - setEditingContact({ - name: '', - email: '', - phone: '', - role: '', - billing: { - email: '', - reference: '', - comments: '', - }, - }) - } - - const updateSponsorMutation = api.sponsor.update.useMutation({ - onSuccess: async (updatedSponsor) => { - setSponsors((prevSponsors) => - prevSponsors.map((sponsor) => - sponsor._id === updatedSponsor._id ? updatedSponsor : sponsor, - ), - ) - - await utils.sponsor.list.invalidate() - - showNotification({ - type: 'success', - title: 'Contact updated', - message: 'Sponsor contact information has been successfully updated.', - }) - setSavingRowId(null) - resetEditingState() - }, - onError: (error: { message: string }) => { - showNotification({ - type: 'error', - title: 'Update failed', - message: - error.message || - 'Failed to update contact information. Please try again.', - }) - setSavingRowId(null) - }, - }) - - const handleStartEdit = (row: ContactRow) => { - const rowId = `${row.sponsor._id}-${row.contact._key}` - setEditingRowId(rowId) - setEditingContact({ - name: row.contact.name || '', - email: row.contact.email || '', - phone: row.contact.phone || '', - role: row.contact.role || '', - billing: { - email: row.sponsor.billing?.email || '', - reference: row.sponsor.billing?.reference || '', - comments: row.sponsor.billing?.comments || '', - }, - }) - } - - const handleCancelEdit = () => { - resetEditingState() + const handleStartEdit = (sponsor: SponsorWithContactInfo) => { + setEditingSponsor(sponsor) } - const handleAddContact = (sponsorId: string) => { - const newRowId = `${sponsorId}-new-contact-${Date.now()}` - setEditingRowId(newRowId) - setEditingContact({ - name: '', - email: '', - phone: '', - role: '', - billing: { - email: '', - reference: '', - comments: '', - }, - }) + const handleCloseEdit = () => { + setEditingSponsor(null) } - const handleSaveEdit = async (row: ContactRow) => { - const rowId = `${row.sponsor._id}-${row.contact._key}` - - if ( - editingContact.role === 'Billing Reference' && - !editingContact.billing?.email - ) { - showNotification({ - type: 'warning', - title: 'Billing email required', - message: - 'Please provide a billing email for Billing Reference contacts.', - }) - return - } - - setSavingRowId(rowId) - - try { - let updatedContactPersons: ContactPerson[] - - if ( - row.contact._key === 'no-contact' || - row.contact._key === 'new-contact-temp' - ) { - const newContact: ContactPerson = { - // eslint-disable-next-line react-hooks/purity -- Unique key generation for new contact - _key: `contact-${Date.now()}`, - name: editingContact.name, - email: editingContact.email, - phone: editingContact.phone || undefined, - role: editingContact.role || undefined, - } - - if (row.contact._key === 'no-contact') { - updatedContactPersons = [newContact] - } else { - updatedContactPersons = [ - ...(row.sponsor.contact_persons || []), - newContact, - ] - } - } else { - updatedContactPersons = - row.sponsor.contact_persons?.map((contact) => - contact._key === row.contact._key - ? { - ...contact, - name: editingContact.name, - email: editingContact.email, - phone: editingContact.phone || undefined, - role: editingContact.role || undefined, - } - : contact, - ) || [] - } - - const isBillingReference = editingContact.role === 'Billing Reference' - let billingData = undefined - - if (isBillingReference && editingContact.billing?.email?.trim()) { - billingData = { - email: editingContact.billing.email.trim(), - reference: editingContact.billing.reference?.trim() || undefined, - comments: editingContact.billing.comments?.trim() || undefined, - } - } else if ( - !isBillingReference && - row.sponsor.billing && - row.sponsor.billing.email - ) { - billingData = { - email: row.sponsor.billing.email, - reference: row.sponsor.billing.reference || undefined, - comments: row.sponsor.billing.comments || undefined, - } - } - - const updateData: { - name: string - website: string - logo: string - org_number?: string - contact_persons: ContactPerson[] - billing?: { - email: string - reference?: string - comments?: string - } - } = { - name: row.sponsor.name, - website: row.sponsor.website, - logo: row.sponsor.logo, - contact_persons: updatedContactPersons, - } - - if (row.sponsor.org_number) { - updateData.org_number = row.sponsor.org_number - } - - if (billingData && billingData.email && billingData.email.trim()) { - updateData.billing = billingData - } - - await updateSponsorMutation.mutateAsync({ - id: row.sponsor._id, - data: updateData, - }) - } catch { - // Silently fail - error will be shown by mutation's onError handler - } + const handleUpdateSuccess = (updatedSponsor: SponsorWithContactInfo) => { + setSponsors((prev) => + prev.map((s) => (s._id === updatedSponsor._id ? updatedSponsor : s)), + ) + handleCloseEdit() + utils.sponsor.list.invalidate() } const contactRows: ContactRow[] = [] @@ -305,23 +108,6 @@ export function SponsorContactTable({ isFirstContactForSponsor: index === 0, }) }) - - if ( - editingRowId && - editingRowId.startsWith(`${sponsor._id}-new-contact-`) - ) { - contactRows.push({ - sponsor, - contact: { - _key: 'new-contact-temp', - name: '', - email: '', - phone: '', - role: '', - }, - isFirstContactForSponsor: false, - }) - } } else { contactRows.push({ sponsor, @@ -351,43 +137,101 @@ export function SponsorContactTable({ return (
+ {/* Editor Modal */} + + + +
+ + +
+
+ + +
+ + Manage Contacts: {editingSponsor?.name} + + +
+ + {editingSponsor && ( +
+ +
+ )} +
+
+
+
+
+
+
@@ -401,15 +245,6 @@ export function SponsorContactTable({ {contactRows.map((row, index) => { - const rowId = - row.contact._key === 'new-contact-temp' && - editingRowId?.startsWith(`${row.sponsor._id}-new-contact-`) - ? editingRowId - : `${row.sponsor._id}-${row.contact._key}` - const isEditing = editingRowId === rowId - const isSaving = - savingRowId === rowId || updateSponsorMutation.isPending - return (
Sponsor Contact Name Contact Email Phone Role Billing Info
-
+
{row.sponsor.name}
{row.sponsor.org_number && ( @@ -428,24 +263,7 @@ export function SponsorContactTable({
- {isEditing ? ( - - setEditingContact({ - ...editingContact, - name: e.target.value, - }) - } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" - placeholder="Contact name" - /> - ) : row.contact._key === 'new-contact-temp' ? ( -
- Add new contact -
- ) : row.contact.name ? ( + {row.contact.name ? (
{row.contact.name}
@@ -456,24 +274,7 @@ export function SponsorContactTable({ )}
- {isEditing ? ( - - setEditingContact({ - ...editingContact, - email: e.target.value, - }) - } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" - placeholder="Email address" - /> - ) : row.contact._key === 'new-contact-temp' ? ( -
- - -
- ) : row.contact.email ? ( + {row.contact.email ? (
- {isEditing ? ( - - setEditingContact({ - ...editingContact, - phone: e.target.value, - }) - } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" - placeholder="Phone number" - /> - ) : row.contact._key === 'new-contact-temp' ? ( -
- - -
- ) : row.contact.phone ? ( + {row.contact.phone ? (
- {isEditing ? ( - - setEditingContact({ - ...editingContact, - role: value, - }) - } - className="w-full" - placeholder="Select role..." - /> - ) : row.contact._key === 'new-contact-temp' ? ( -
- - -
- ) : row.contact.role ? ( + {row.contact.role ? (
{row.contact.role}
@@ -552,61 +320,7 @@ export function SponsorContactTable({
{row.isFirstContactForSponsor && (
- {isEditing && - editingContact.role === 'Billing Reference' ? ( - <> -
- Edit Billing Information -
-
- - setEditingContact({ - ...editingContact, - billing: { - ...editingContact.billing!, - email: e.target.value, - }, - }) - } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" - placeholder="Billing email" - /> - - setEditingContact({ - ...editingContact, - billing: { - ...editingContact.billing!, - reference: e.target.value, - }, - }) - } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" - placeholder="Billing reference" - /> -