diff --git a/apps/admin-dashboard/app/routes/_dashboard.students.remove.tsx b/apps/admin-dashboard/app/routes/_dashboard.students.remove.tsx new file mode 100644 index 000000000..e89eede82 --- /dev/null +++ b/apps/admin-dashboard/app/routes/_dashboard.students.remove.tsx @@ -0,0 +1,140 @@ +import { + type ActionFunctionArgs, + data, + Form, + type LoaderFunctionArgs, + redirect, + useActionData, +} from 'react-router'; +import z from 'zod'; + +import { job } from '@oyster/core/bull'; +import { db } from '@oyster/db'; +import { + Button, + ErrorMessage, + Field, + getErrors, + Modal, + Textarea, + validateForm, +} from '@oyster/ui'; +import { Callout } from '@oyster/ui/callout'; + +import { Route } from '@/shared/constants'; +import { + commitSession, + ensureUserAuthenticated, + toast, +} from '@/shared/session.server'; + +export async function loader({ request }: LoaderFunctionArgs) { + await ensureUserAuthenticated(request); + + return {}; +} + +const RemoveMembersFormData = z.object({ + memberIds: z + .string() + .min(1) + .transform((value) => value.split('\n').filter(Boolean)), +}); + +export async function action({ request }: ActionFunctionArgs) { + const session = await ensureUserAuthenticated(request); + + const form = await request.formData(); + + const result = await validateForm(form, RemoveMembersFormData); + + if (!result.ok) { + return data(result, { status: 400 }); + } + + const count = await removeMembers(result.data.memberIds); + + toast(session, { + message: `Removed ${count} members.`, + }); + + return redirect(Route['/students'], { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); +} + +async function removeMembers(ids: string[]): Promise { + const students = await db + .deleteFrom('students') + .where('id', 'in', ids) + .returning(['airtableId', 'email', 'firstName', 'slackId']) + .execute(); + + for (const student of students) { + job('student.removed', { + airtableId: student.airtableId as string, + email: student.email, + firstName: student.firstName, + sendViolationEmail: false, + slackId: student.slackId, + }); + } + + return students.length; +} + +export default function RemoveMembersPage() { + const { error, errors } = getErrors(useActionData()); + + return ( + + + Bulk Remove Members + + + + + This action is not reversible. All of their engagement records will be + deleted and they will be removed from Slack, Mailchimp and Airtable. + + + + Note: These members will immediately be removed from our database, but + it may take some time for them to be removed from Slack, Mailchimp and + Airtable. + + +
+ {error} + + +