From 6333616bff6cc98ae5b110b760ba2b84a9f3611b Mon Sep 17 00:00:00 2001 From: andresgutgon Date: Tue, 9 Dec 2025 17:28:59 +0100 Subject: [PATCH] Implement weekly email schedule job We want to send an email with relevant data for the organizations using Latitude. We'll do the processing of this data on monday 1:00 A.M and it will be sent to be ready on users' inbox on monday morning --- .../admin/weeklyEmail/enqueueWeeklyEmail.ts | 38 ++ .../_components/BackofficeTabs/index.tsx | 5 + .../_components/WorkspaceDashboard/index.tsx | 9 + .../_components/WeeklyEmailForm/index.tsx | 109 ++++ .../app/(admin)/backoffice/weekly/page.tsx | 20 + .../Sidebar/ProjectSection/index.tsx | 2 +- .../_components/charts/DailyOverview.tsx | 2 +- .../_components/charts/VersionOverview.tsx | 2 +- .../_components/ExperimentsTable/index.tsx | 2 +- apps/web/src/app/api/spans/limited/route.ts | 8 +- .../AnnotationExplanation/index.tsx | 4 +- .../LatteChat/_components/LatteUsageInfo.tsx | 21 +- .../StatusIndicator/index.tsx | 2 +- .../src/components/RunPanelStats/index.tsx | 2 +- .../components/evaluations/human/Rating.tsx | 2 +- .../src/components/evaluations/llm/Custom.tsx | 2 +- .../src/components/evaluations/llm/Rating.tsx | 2 +- .../src/components/evaluations/llm/index.tsx | 2 +- .../evaluations/rule/LengthCount.tsx | 2 +- .../Header/Rewards/Content/RewardItem.tsx | 2 +- apps/web/src/services/routes.ts | 6 + apps/workers/src/workers/schedule.ts | 7 + .../worker-definitions/maintenanceWorker.ts | 2 + packages/constants/package.json | 1 + .../constants/src}/formatCount.ts | 14 +- packages/core/package.json | 3 +- .../activeWorkspaces/index.test.ts | 334 +++++++++++ .../weeklyEmail/activeWorkspaces/index.ts | 43 ++ .../weeklyEmail/annotations/index.test.ts | 55 +- .../weeklyEmail/annotations/index.ts | 9 +- .../weeklyEmail/issues/index.test.ts | 4 +- .../data-access/weeklyEmail/issues/index.ts | 86 ++- .../data-access/weeklyEmail/logs/index.tsx | 3 +- .../handlers/notifyClientOfExportReady.ts | 2 +- .../events/handlers/sendInvitationToUser.ts | 2 +- .../sendIssueEscalatingHandler.test.ts | 183 +++--- .../handlers/sendIssueEscalatingHandler.ts | 77 +-- .../events/handlers/sendMagicLinkHandler.ts | 2 +- .../events/handlers/sendReferralInvitation.ts | 2 +- .../handlers/sendSuggestionNotification.ts | 2 +- .../datasets/notifyClientOfDatasetUpdate.ts | 2 +- .../core/src/jobs/job-definitions/index.ts | 1 + .../scheduleWorkspaceCleanupJobs.ts | 2 +- .../jobs/job-definitions/weeklyEmail/index.ts | 2 + .../scheduleWeeklyEmailJobs.test.ts | 145 +++++ .../weeklyEmail/scheduleWeeklyEmailJobs.ts | 28 + .../weeklyEmail/sendWeeklyEmailJob.test.ts | 314 ++++++++++ .../weeklyEmail/sendWeeklyEmailJob.ts | 102 ++++ .../src/{mailers/mailers => mailer}/Mailer.ts | 52 +- .../mailers => mailer}/adapters/index.ts | 0 .../mailers => mailer}/adapters/mailgun.ts | 0 .../mailers => mailer}/adapters/mailpit.ts | 0 .../mailers => mailer}/adapters/smtp.ts | 0 .../core/src/mailer/buildBatchRecipients.ts | 39 ++ .../mailers/datasets/DatasetUpdateMailer.ts | 6 +- .../DocumentTriggerMailer.ts | 2 +- .../mailers/exports/ExportReadyMailer.ts | 4 +- .../mailers/invitations/InvitationMailer.ts | 4 +- .../mailers/invitations/ReferralMailer.ts | 4 +- .../mailers/issues/IssueEscalatingMailer.ts | 30 +- .../mailers/magicLinks/MagicLinkMailer.ts | 2 +- .../mailers/suggestions/SuggestionMailer.ts | 2 +- .../mailers/weeklyEmail/WeeklyEmailMailer.ts | 43 ++ packages/core/src/mailers/index.ts | 1 - .../core/src/mailers/mailers/mailers/index.ts | 6 - .../handlers/email/sendResponse.ts | 2 +- .../core/src/services/workspaces/create.ts | 9 +- .../core/src/tests/factories/workspaces.ts | 2 + packages/emails/package.json | 6 + .../components/NotificationsFooter/index.tsx | 6 +- .../emails/src/components/tailwind.email.ts | 7 + packages/emails/src/routes.ts | 38 ++ .../templates/issues/IssueEscalatingMail.tsx | 2 +- .../templates/weeklyEmail/WeeklyEmailMail.tsx | 467 +++++++++++++++ .../src/templates/weeklyEmail/previewData.ts | 550 ++++++++++++++++++ .../emails/src/templates/weeklyEmail/types.ts | 73 +++ packages/emails/src/types.ts | 2 +- 77 files changed, 2738 insertions(+), 292 deletions(-) create mode 100644 apps/web/src/actions/admin/weeklyEmail/enqueueWeeklyEmail.ts create mode 100644 apps/web/src/app/(admin)/backoffice/weekly/_components/WeeklyEmailForm/index.tsx create mode 100644 apps/web/src/app/(admin)/backoffice/weekly/page.tsx rename {apps/web/src/lib => packages/constants/src}/formatCount.ts (60%) create mode 100644 packages/core/src/data-access/weeklyEmail/activeWorkspaces/index.test.ts create mode 100644 packages/core/src/data-access/weeklyEmail/activeWorkspaces/index.ts create mode 100644 packages/core/src/jobs/job-definitions/weeklyEmail/index.ts create mode 100644 packages/core/src/jobs/job-definitions/weeklyEmail/scheduleWeeklyEmailJobs.test.ts create mode 100644 packages/core/src/jobs/job-definitions/weeklyEmail/scheduleWeeklyEmailJobs.ts create mode 100644 packages/core/src/jobs/job-definitions/weeklyEmail/sendWeeklyEmailJob.test.ts create mode 100644 packages/core/src/jobs/job-definitions/weeklyEmail/sendWeeklyEmailJob.ts rename packages/core/src/{mailers/mailers => mailer}/Mailer.ts (68%) rename packages/core/src/{mailers/mailers => mailer}/adapters/index.ts (100%) rename packages/core/src/{mailers/mailers => mailer}/adapters/mailgun.ts (100%) rename packages/core/src/{mailers/mailers => mailer}/adapters/mailpit.ts (100%) rename packages/core/src/{mailers/mailers => mailer}/adapters/smtp.ts (100%) create mode 100644 packages/core/src/mailer/buildBatchRecipients.ts rename packages/core/src/{mailers/mailers => mailer}/mailers/datasets/DatasetUpdateMailer.ts (81%) rename packages/core/src/{mailers/mailers => mailer}/mailers/documentEmailTrigger/DocumentTriggerMailer.ts (94%) rename packages/core/src/{mailers/mailers => mailer}/mailers/exports/ExportReadyMailer.ts (86%) rename packages/core/src/{mailers/mailers => mailer}/mailers/invitations/InvitationMailer.ts (90%) rename packages/core/src/{mailers/mailers => mailer}/mailers/invitations/ReferralMailer.ts (87%) rename packages/core/src/{mailers/mailers => mailer}/mailers/issues/IssueEscalatingMailer.ts (68%) rename packages/core/src/{mailers/mailers => mailer}/mailers/magicLinks/MagicLinkMailer.ts (95%) rename packages/core/src/{mailers/mailers => mailer}/mailers/suggestions/SuggestionMailer.ts (96%) create mode 100644 packages/core/src/mailer/mailers/weeklyEmail/WeeklyEmailMailer.ts delete mode 100644 packages/core/src/mailers/index.ts delete mode 100644 packages/core/src/mailers/mailers/mailers/index.ts create mode 100644 packages/emails/src/templates/weeklyEmail/WeeklyEmailMail.tsx create mode 100644 packages/emails/src/templates/weeklyEmail/previewData.ts create mode 100644 packages/emails/src/templates/weeklyEmail/types.ts diff --git a/apps/web/src/actions/admin/weeklyEmail/enqueueWeeklyEmail.ts b/apps/web/src/actions/admin/weeklyEmail/enqueueWeeklyEmail.ts new file mode 100644 index 0000000000..2b402cdb13 --- /dev/null +++ b/apps/web/src/actions/admin/weeklyEmail/enqueueWeeklyEmail.ts @@ -0,0 +1,38 @@ +'use server' + +import { withAdmin } from '../../procedures' +import { z } from 'zod' +import { queues } from '@latitude-data/core/queues' + +export const enqueueWeeklyEmailAction = withAdmin + .inputSchema( + z.object({ + workspaceId: z.number(), + emails: z.string().optional(), + }), + ) + .action(async ({ parsedInput }) => { + const { workspaceId, emails } = parsedInput + + // Parse comma-separated emails if provided + const emailList = emails + ? emails + .split(',') + .map((e) => e.trim()) + .filter((e) => e.length > 0) + : undefined + + const { maintenanceQueue } = await queues() + await maintenanceQueue.add( + 'sendWeeklyEmailJob', + { + workspaceId, + emails: emailList, + }, + { + jobId: `weekly-email-manual-${workspaceId}-${Date.now()}`, + }, + ) + + return { success: true } + }) diff --git a/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs/index.tsx b/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs/index.tsx index c25d967f02..1156a25c35 100644 --- a/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs/index.tsx +++ b/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs/index.tsx @@ -61,6 +61,11 @@ export function BackofficeTabs({ children }: { children: ReactNode }) { value: BackofficeRoutes.integrations, route: ROUTES.backoffice.integrations.root, }, + { + label: 'Weekly', + value: BackofficeRoutes.weekly, + route: ROUTES.backoffice.weekly.root, + }, ]} selected={selected} /> diff --git a/apps/web/src/app/(admin)/backoffice/search/workspace/[id]/_components/WorkspaceDashboard/index.tsx b/apps/web/src/app/(admin)/backoffice/search/workspace/[id]/_components/WorkspaceDashboard/index.tsx index 28cac57d81..0d49f8a3ce 100644 --- a/apps/web/src/app/(admin)/backoffice/search/workspace/[id]/_components/WorkspaceDashboard/index.tsx +++ b/apps/web/src/app/(admin)/backoffice/search/workspace/[id]/_components/WorkspaceDashboard/index.tsx @@ -92,6 +92,15 @@ export function WorkspaceDashboard({ workspace }: Props) { issuesUnlocked={workspace.issuesUnlocked} />
+ + + { + const workspaceIdParam = searchParams.get('workspaceId') + if (workspaceIdParam) { + setWorkspaceId(workspaceIdParam) + } + }, [searchParams]) + + const { execute: enqueueEmail, isPending } = useLatitudeAction( + enqueueWeeklyEmailAction, + { + onSuccess: () => { + toast({ + title: 'Success', + description: 'Weekly email job has been enqueued successfully', + }) + // Clear form + setWorkspaceId('') + setEmails('') + }, + onError: (error) => { + toast({ + title: 'Error', + description: error.message, + variant: 'destructive', + }) + }, + }, + ) + + const handleSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault() + + const parsedWorkspaceId = parseInt(workspaceId) + if (isNaN(parsedWorkspaceId)) { + toast({ + title: 'Invalid Workspace ID', + description: 'Please enter a valid numeric workspace ID', + variant: 'destructive', + }) + return + } + + enqueueEmail({ + workspaceId: parsedWorkspaceId, + emails: emails.trim() || undefined, + }) + }, + [workspaceId, emails, enqueueEmail, toast], + ) + + return ( +
+
+ Workspace ID + setWorkspaceId(e.target.value)} + required + /> + + The ID of the workspace to send the weekly email for + +
+ +
+ Email Addresses (Optional) +