diff --git a/src/app/dashboard/businesses/_components/businesses-data-table.tsx b/src/app/dashboard/businesses/_components/businesses-data-table.tsx index b160f29..9b8718a 100644 --- a/src/app/dashboard/businesses/_components/businesses-data-table.tsx +++ b/src/app/dashboard/businesses/_components/businesses-data-table.tsx @@ -43,17 +43,6 @@ interface BusinessesDataTableProps { businesses: Business[]; } -const formatAddress = (business: Business) => { - const parts = [ - business.addressLine1, - business.addressLine2, - business.city, - business.state, - business.postalCode, - ].filter(Boolean); - return parts.join(", ") || "—"; -}; - export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) { const router = useRouter(); const [businessToDelete, setBusinessToDelete] = useState( @@ -103,7 +92,7 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {

{business.name}

- {business.email ?? "—"} + {business.nickname ?? "—"}

@@ -111,11 +100,11 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) { }, }, { - accessorKey: "nickname", + accessorKey: "email", header: ({ column }) => ( - + ), - cell: ({ row }) => row.original.nickname ?? "—", + cell: ({ row }) => row.original.email ?? "—", meta: { headerClassName: "hidden sm:table-cell", cellClassName: "hidden sm:table-cell", @@ -132,26 +121,6 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) { cellClassName: "hidden md:table-cell", }, }, - { - id: "address", - header: "Address", - cell: ({ row }) => formatAddress(row.original), - meta: { - headerClassName: "hidden lg:table-cell", - cellClassName: "hidden lg:table-cell", - }, - }, - { - accessorKey: "taxId", - header: ({ column }) => ( - - ), - cell: ({ row }) => row.original.taxId ?? "—", - meta: { - headerClassName: "hidden xl:table-cell", - cellClassName: "hidden xl:table-cell", - }, - }, { accessorKey: "website", header: ({ column }) => ( diff --git a/src/components/forms/business-form.tsx b/src/components/forms/business-form.tsx index 5a47f75..c1784ac 100644 --- a/src/components/forms/business-form.tsx +++ b/src/components/forms/business-form.tsx @@ -123,29 +123,18 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { // Update email configuration mutation const updateEmailConfig = api.businesses.updateEmailConfig.useMutation({ - onSuccess: () => { - toast.success("Email configuration updated successfully"); - }, onError: (error) => { toast.error(`Failed to update email configuration: ${error.message}`); }, }); const createBusiness = api.businesses.create.useMutation({ - onSuccess: () => { - toast.success("Business created successfully"); - router.push("/dashboard/businesses"); - }, onError: (error) => { toast.error(error.message || "Failed to create business"); }, }); const updateBusiness = api.businesses.update.useMutation({ - onSuccess: () => { - toast.success("Business updated successfully"); - router.push("/dashboard/businesses"); - }, onError: (error) => { toast.error(error.message || "Failed to update business"); }, @@ -203,69 +192,87 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { newErrors.name = VALIDATION_MESSAGES.required; } // Nickname validation (optional, max 255 chars) - if (formData.nickname && formData.nickname.length > 255) { + if (formData.nickname && formData.nickname.trim().length > 255) { newErrors.nickname = "Nickname must be 255 characters or less"; } // Email validation - if (formData.email && !isValidEmail(formData.email)) { + if (formData.email.trim() && !isValidEmail(formData.email.trim())) { newErrors.email = VALIDATION_MESSAGES.email; } // Phone validation (basic check for US format) - if (formData.phone) { + if (formData.phone.trim()) { const phoneDigits = formData.phone.replace(/\D/g, ""); if (phoneDigits.length > 0 && phoneDigits.length < 10) { newErrors.phone = VALIDATION_MESSAGES.phone; } } - // Address validation if any address field is filled - const hasAddressData = - formData.addressLine1 || - formData.city || - formData.state || - formData.postalCode; + // Address validation if any address field is filled (excluding country as it has a default) + const hasAddressData = !!( + formData.addressLine1.trim() || + formData.city.trim() || + formData.state.trim() || + formData.postalCode.trim() + ); - if (hasAddressData) { - if (!formData.addressLine1) + // Also check if country was explicitly changed from default + const hasNonDefaultCountry = + formData.country.trim() && formData.country.trim() !== "United States"; + const hasAnyAddressInput = hasAddressData || hasNonDefaultCountry; + + // Only validate address if user has actually entered address data + if (hasAnyAddressInput) { + if (!formData.addressLine1.trim()) newErrors.addressLine1 = VALIDATION_MESSAGES.required; - if (!formData.city) newErrors.city = VALIDATION_MESSAGES.required; - if (!formData.country) newErrors.country = VALIDATION_MESSAGES.required; + if (!formData.city.trim()) newErrors.city = VALIDATION_MESSAGES.required; + if (!formData.country.trim()) + newErrors.country = VALIDATION_MESSAGES.required; - if (formData.country === "United States") { - if (!formData.state) newErrors.state = VALIDATION_MESSAGES.required; - if (!formData.postalCode) + // Only require US-specific fields if country is United States AND we have actual address data + if (formData.country.trim() === "United States" && hasAddressData) { + if (!formData.state.trim()) + newErrors.state = VALIDATION_MESSAGES.required; + if (!formData.postalCode.trim()) newErrors.postalCode = VALIDATION_MESSAGES.required; } } // Email configuration validation // API Key validation - if (formData.resendApiKey && !formData.resendApiKey.startsWith("re_")) { + if ( + formData.resendApiKey.trim() && + !formData.resendApiKey.trim().startsWith("re_") + ) { newErrors.resendApiKey = "Resend API key should start with 're_'"; } // Domain validation - if (formData.resendDomain) { + if (formData.resendDomain.trim()) { const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.([a-zA-Z]{2,})+$/; - if (!domainRegex.test(formData.resendDomain)) { + if (!domainRegex.test(formData.resendDomain.trim())) { newErrors.resendDomain = "Please enter a valid domain (e.g., yourdomain.com)"; } } // If API key is provided, domain must also be provided - if (formData.resendApiKey && !formData.resendDomain) { + if (formData.resendApiKey.trim() && !formData.resendDomain.trim()) { newErrors.resendDomain = "Domain is required when API key is provided"; } - // If domain is provided, API key must also be provided + // If domain is provided, API key must also be provided (unless there's already one on the server) + // In edit mode, if domain comes from server and API key field is empty, don't require new API key + const userEnteredDomain = + formData.resendDomain.trim() !== (emailConfig?.resendDomain ?? ""); + if ( - formData.resendDomain && - !formData.resendApiKey && - !emailConfig?.hasApiKey + formData.resendDomain.trim() && + !formData.resendApiKey.trim() && + !emailConfig?.hasApiKey && + (mode === "create" || userEnteredDomain) ) { newErrors.resendApiKey = "API key is required when domain is provided"; } @@ -289,7 +296,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { const dataToSubmit = { ...formData, name: formData.name.trim(), - nickname: formData.nickname.trim(), + nickname: formData.nickname.trim() || undefined, website: formData.website ? formatWebsiteUrl(formData.website) : "", }; @@ -313,20 +320,24 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { const newBusiness = await createBusiness.mutateAsync(businessData); - // Update email configuration separately if any email fields are provided - if ( - newBusiness && - (formData.resendApiKey || - formData.resendDomain || - formData.emailFromName) - ) { + // Update email configuration separately if any email fields have values + const newApiKey = formData.resendApiKey.trim(); + const newDomain = formData.resendDomain.trim(); + const newFromName = formData.emailFromName.trim(); + + const hasEmailData = newApiKey || newDomain || newFromName; + + if (newBusiness && hasEmailData) { await updateEmailConfig.mutateAsync({ id: newBusiness.id, - resendApiKey: formData.resendApiKey || undefined, - resendDomain: formData.resendDomain || undefined, - emailFromName: formData.emailFromName || undefined, + resendApiKey: newApiKey || undefined, + resendDomain: newDomain || undefined, + emailFromName: newFromName || undefined, }); } + + toast.success("Business created successfully"); + router.push("/dashboard/businesses"); } else { // Update business data (excluding email config fields) const businessData = { @@ -350,19 +361,31 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { ...businessData, }); - // Update email configuration separately if any email fields are provided - if ( - formData.resendApiKey || - formData.resendDomain || - formData.emailFromName - ) { + // Only update email configuration if there are actual changes or new values + const currentApiKey = emailConfig?.hasApiKey ? "EXISTING" : ""; + const currentDomain = emailConfig?.resendDomain ?? ""; + const currentFromName = emailConfig?.emailFromName ?? ""; + + const newApiKey = formData.resendApiKey.trim(); + const newDomain = formData.resendDomain.trim(); + const newFromName = formData.emailFromName.trim(); + + const hasEmailChanges = + (newApiKey && newApiKey !== currentApiKey) || + newDomain !== currentDomain || + newFromName !== currentFromName; + + if (hasEmailChanges) { await updateEmailConfig.mutateAsync({ id: businessId!, - resendApiKey: formData.resendApiKey || undefined, - resendDomain: formData.resendDomain || undefined, - emailFromName: formData.emailFromName || undefined, + resendApiKey: newApiKey || undefined, + resendDomain: newDomain || undefined, + emailFromName: newFromName || undefined, }); } + + toast.success("Business updated successfully"); + router.push("/dashboard/businesses"); } } finally { setIsSubmitting(false); @@ -667,7 +690,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { {/* Current Status */} {mode === "edit" && ( -
+
Current Status: @@ -688,7 +711,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { )}
{emailConfig?.resendDomain && ( - + Domain: {emailConfig.resendDomain} )} @@ -742,7 +765,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) { } placeholder={ mode === "edit" && emailConfig?.hasApiKey - ? "Enter new API key to update" + ? "••••••••••••••••••••••••••••••••" : "re_..." } className={ diff --git a/src/server/api/routers/clients.ts b/src/server/api/routers/clients.ts index d0c71fc..a42597c 100644 --- a/src/server/api/routers/clients.ts +++ b/src/server/api/routers/clients.ts @@ -108,19 +108,41 @@ export const clientsRouter = createTRPCRouter({ .input(createClientSchema) .mutation(async ({ ctx, input }) => { try { - // Clean up empty strings to null, but preserve required fields - const cleanInput = Object.fromEntries( - Object.entries(input).map(([key, value]) => [ - key, - value === "" ? null : value, - ]), - ); - const [client] = await ctx.db .insert(clients) .values({ - name: input.name, // Ensure name is included - ...cleanInput, + name: input.name.trim(), + email: + input.email && input.email.trim() !== "" + ? input.email.trim() + : null, + phone: + input.phone && input.phone.trim() !== "" + ? input.phone.trim() + : null, + addressLine1: + input.addressLine1 && input.addressLine1.trim() !== "" + ? input.addressLine1.trim() + : null, + addressLine2: + input.addressLine2 && input.addressLine2.trim() !== "" + ? input.addressLine2.trim() + : null, + city: + input.city && input.city.trim() !== "" ? input.city.trim() : null, + state: + input.state && input.state.trim() !== "" + ? input.state.trim() + : null, + postalCode: + input.postalCode && input.postalCode.trim() !== "" + ? input.postalCode.trim() + : null, + country: + input.country && input.country.trim() !== "" + ? input.country.trim() + : null, + defaultHourlyRate: input.defaultHourlyRate ?? null, createdById: ctx.session.user.id, }) .returning(); @@ -168,18 +190,59 @@ export const clientsRouter = createTRPCRouter({ }); } - // Clean up empty strings to null - const cleanData = Object.fromEntries( - Object.entries(data).map(([key, value]) => [ - key, - value === "" ? null : value, - ]), - ); - const [updatedClient] = await ctx.db .update(clients) .set({ - ...cleanData, + name: data.name ? data.name.trim() : undefined, + email: + data.email !== undefined + ? data.email && data.email.trim() !== "" + ? data.email.trim() + : null + : undefined, + phone: + data.phone !== undefined + ? data.phone && data.phone.trim() !== "" + ? data.phone.trim() + : null + : undefined, + addressLine1: + data.addressLine1 !== undefined + ? data.addressLine1 && data.addressLine1.trim() !== "" + ? data.addressLine1.trim() + : null + : undefined, + addressLine2: + data.addressLine2 !== undefined + ? data.addressLine2 && data.addressLine2.trim() !== "" + ? data.addressLine2.trim() + : null + : undefined, + city: + data.city !== undefined + ? data.city && data.city.trim() !== "" + ? data.city.trim() + : null + : undefined, + state: + data.state !== undefined + ? data.state && data.state.trim() !== "" + ? data.state.trim() + : null + : undefined, + postalCode: + data.postalCode !== undefined + ? data.postalCode && data.postalCode.trim() !== "" + ? data.postalCode.trim() + : null + : undefined, + country: + data.country !== undefined + ? data.country && data.country.trim() !== "" + ? data.country.trim() + : null + : undefined, + defaultHourlyRate: data.defaultHourlyRate ?? undefined, updatedAt: new Date(), }) .where(eq(clients.id, id))