-
Notifications
You must be signed in to change notification settings - Fork 269
Implement weekly email schedule job #2022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 'use client' | ||
|
|
||
| import { FormEvent, useCallback, useEffect, useState } from 'react' | ||
| import { useSearchParams } from 'next/navigation' | ||
| import { Button } from '@latitude-data/web-ui/atoms/Button' | ||
| import { Input } from '@latitude-data/web-ui/atoms/Input' | ||
| import { TextArea } from '@latitude-data/web-ui/atoms/TextArea' | ||
| import { Text } from '@latitude-data/web-ui/atoms/Text' | ||
| import { useToast } from '@latitude-data/web-ui/atoms/Toast' | ||
| import { enqueueWeeklyEmailAction } from '$/actions/admin/weeklyEmail/enqueueWeeklyEmail' | ||
| import useLatitudeAction from '$/hooks/useLatitudeAction' | ||
|
|
||
| export function WeeklyEmailForm() { | ||
| const searchParams = useSearchParams() | ||
| const { toast } = useToast() | ||
| const [workspaceId, setWorkspaceId] = useState('') | ||
| const [emails, setEmails] = useState('') | ||
|
|
||
| // Pre-fill workspaceId from query params if present | ||
| useEffect(() => { | ||
| 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 ( | ||
| <form onSubmit={handleSubmit} className='flex flex-col gap-y-4 max-w-2xl'> | ||
| <div className='flex flex-col gap-y-2'> | ||
| <Text.H5 weight='medium'>Workspace ID</Text.H5> | ||
| <Input | ||
| type='number' | ||
| placeholder='Enter workspace ID' | ||
| value={workspaceId} | ||
| onChange={(e) => setWorkspaceId(e.target.value)} | ||
| required | ||
| /> | ||
| <Text.H6 color='foregroundMuted'> | ||
| The ID of the workspace to send the weekly email for | ||
| </Text.H6> | ||
| </div> | ||
|
|
||
| <div className='flex flex-col gap-y-2'> | ||
| <Text.H5 weight='medium'>Email Addresses (Optional)</Text.H5> | ||
| <TextArea | ||
| placeholder='email1@example.com, email2@example.com' | ||
| value={emails} | ||
| onChange={(e) => setEmails(e.target.value)} | ||
| rows={4} | ||
| /> | ||
| <Text.H6 color='foregroundMuted'> | ||
| Comma-separated list of email addresses. If left empty, the email will | ||
| be sent to all workspace members who have opted in to receive weekly | ||
| emails. | ||
| </Text.H6> | ||
| </div> | ||
|
|
||
| <div className='flex gap-2'> | ||
| <Button type='submit' fancy disabled={isPending}> | ||
| {isPending ? 'Enqueueing...' : 'Enqueue Weekly Email'} | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| 'use client' | ||
|
|
||
| import { Text } from '@latitude-data/web-ui/atoms/Text' | ||
| import { WeeklyEmailForm } from './_components/WeeklyEmailForm' | ||
|
|
||
| export default function AdminWeeklyEmail() { | ||
| return ( | ||
| <div className='container flex flex-col gap-y-8'> | ||
| <section className='flex flex-col gap-y-4'> | ||
| <Text.H1>Weekly Email</Text.H1> | ||
| <Text.H4 color='foregroundMuted'> | ||
| Manually trigger weekly email reports for specific workspaces. You can | ||
| optionally provide a comma-separated list of email addresses to send | ||
| the report to instead of the workspace members. | ||
| </Text.H4> | ||
| <WeeklyEmailForm /> | ||
| </section> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ import { useCurrentProject } from '$/app/providers/ProjectProvider' | |
| import { DurationCell } from './DurationCell' | ||
| import { ScoreCell } from './ScoreCell' | ||
| import { cn } from '@latitude-data/web-ui/utils' | ||
| import { formatCount } from '$/lib/formatCount' | ||
| import { formatCount } from '@latitude-data/constants/formatCount' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. weird that constants exposes a function
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But is a really nice method and I want to share it between |
||
| import { useSearchParams } from 'next/navigation' | ||
| import { LinkableTablePaginationFooter } from '$/components/TablePaginationFooter' | ||
| import { DocumentRoutes, ROUTES } from '$/services/routes' | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove ai comments when possible