feature: nodemailer set
This commit is contained in:
@@ -36,6 +36,7 @@ export default function ApplicationsTable({ applicants }: { applicants: Applican
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [bulkStatus, setBulkStatus] = useState<string>('');
|
||||
const [rows, setRows] = useState<Applicant[]>(applicants);
|
||||
const [sendEmail, setSendEmail] = useState<boolean>(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
@@ -61,17 +62,40 @@ export default function ApplicationsTable({ applicants }: { applicants: Applican
|
||||
const targets = new Set(selected);
|
||||
startTransition(async () => {
|
||||
const updates = rows.filter((r) => targets.has(r.applicationId));
|
||||
let notifiedCount = 0;
|
||||
let errorCount = 0;
|
||||
for (const r of updates) {
|
||||
await fetch(`/api/applications/${r.applicationId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: bulkStatus, studentId: r.studentId ?? 0 }),
|
||||
});
|
||||
try {
|
||||
const res = await fetch(`/api/applications/${r.applicationId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: bulkStatus, studentId: r.studentId ?? 0, notify: sendEmail }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
errorCount += 1;
|
||||
} else if (sendEmail) {
|
||||
if (data?.notified) notifiedCount += 1;
|
||||
if (data?.emailError) errorCount += 1;
|
||||
}
|
||||
} catch (_e) {
|
||||
errorCount += 1;
|
||||
}
|
||||
}
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (targets.has(r.applicationId) ? { ...r, status: bulkStatus } : r)),
|
||||
);
|
||||
setSelected([]);
|
||||
|
||||
if (sendEmail) {
|
||||
if (errorCount === 0) {
|
||||
alert(`Updated ${updates.length} and emailed ${notifiedCount} student(s).`);
|
||||
} else {
|
||||
alert(`Updated ${updates.length}. Emails sent: ${notifiedCount}. Errors: ${errorCount}.`);
|
||||
}
|
||||
} else {
|
||||
alert(`Updated ${updates.length}.`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -103,6 +127,14 @@ export default function ApplicationsTable({ applicants }: { applicants: Applican
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sendEmail}
|
||||
onChange={(e) => setSendEmail(e.target.checked)}
|
||||
/>
|
||||
Send email notifications
|
||||
</label>
|
||||
<Button onClick={onBulkUpdate} disabled={!bulkStatus || selected.length === 0 || isPending}>
|
||||
Update {selected.length || ''}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { db, applications } from '@workspace/db';
|
||||
import { db, applications, students } from '@workspace/db';
|
||||
import { eq } from '@workspace/db/drizzle';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { sendEmail } from '@/lib/mailer';
|
||||
import { statusUpdateHtml, statusUpdateSubject, statusUpdateText } from '@/lib/mail-templates';
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ applicationId: string }> }) {
|
||||
const { applicationId: applicationIdParam } = await params;
|
||||
@@ -9,18 +11,63 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ ap
|
||||
return NextResponse.json({ error: 'Invalid applicationId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { status } = await req.json();
|
||||
const { status, studentId, notify } = await req.json();
|
||||
if (!status) {
|
||||
return NextResponse.json({ error: 'Missing status' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await db.update(applications)
|
||||
.set({ status })
|
||||
.where(eq(applications.id, applicationId));
|
||||
let result;
|
||||
try {
|
||||
result = await db
|
||||
.update(applications)
|
||||
.set({ status })
|
||||
.where(eq(applications.id, applicationId));
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown database error';
|
||||
return NextResponse.json({ error: 'Database update failed', detail: message }, { status: 500 });
|
||||
}
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
if (!result || result.rowCount === 0) {
|
||||
return NextResponse.json({ error: 'Application not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
const shouldNotify = notify === undefined ? true : Boolean(notify);
|
||||
let emailError: string | undefined;
|
||||
let notified = false;
|
||||
try {
|
||||
if (shouldNotify) {
|
||||
// studentId is provided by client components; if missing, try to infer
|
||||
let effectiveStudentId: number | null = Number.isFinite(Number(studentId)) && Number(studentId) > 0 ? Number(studentId) : null;
|
||||
if (!effectiveStudentId) {
|
||||
const app = await db.query.applications.findFirst({
|
||||
where: eq(applications.id, applicationId),
|
||||
columns: { studentId: true },
|
||||
});
|
||||
effectiveStudentId = app?.studentId ?? null;
|
||||
}
|
||||
|
||||
if (effectiveStudentId) {
|
||||
const student = await db.query.students.findFirst({
|
||||
where: eq(students.id, effectiveStudentId),
|
||||
columns: { email: true, firstName: true, lastName: true },
|
||||
});
|
||||
|
||||
if (student?.email) {
|
||||
const name = `${student.firstName ?? ''} ${student.lastName ?? ''}`.trim() || 'Student';
|
||||
await sendEmail({
|
||||
to: student.email,
|
||||
subject: statusUpdateSubject(status),
|
||||
text: statusUpdateText(name, status),
|
||||
html: statusUpdateHtml(name, status),
|
||||
});
|
||||
notified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
emailError = err instanceof Error ? err.message : 'Unknown email error';
|
||||
console.error('Failed to send status update email', { applicationId, status, error: emailError });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, emailError: emailError ?? null, notified });
|
||||
}
|
||||
@@ -1,15 +1,43 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getMailer } from '@/lib/mailer'
|
||||
import { db } from '@workspace/db'
|
||||
import { sql } from '@workspace/db/drizzle'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// You can add additional health checks here
|
||||
// For example, database connectivity, external service checks, etc.
|
||||
|
||||
const envSeen = {
|
||||
SMTP_URL: Boolean(process.env.SMTP_URL || process.env.MAIL_URL),
|
||||
SMTP_HOST: Boolean(process.env.SMTP_HOST || process.env.MAIL_HOST),
|
||||
SMTP_PORT: Boolean(process.env.SMTP_PORT || process.env.MAIL_PORT),
|
||||
SMTP_USER: Boolean(process.env.SMTP_USER || process.env.SMTP_USERNAME || process.env.MAIL_USER || process.env.MAIL_USERNAME),
|
||||
SMTP_PASS: Boolean(process.env.SMTP_PASS || process.env.SMTP_PASSWORD || process.env.MAIL_PASS || process.env.MAIL_PASSWORD),
|
||||
SMTP_FROM: Boolean(process.env.SMTP_FROM),
|
||||
}
|
||||
|
||||
let smtp: { ok: boolean; message?: string } = { ok: false }
|
||||
try {
|
||||
await getMailer().verify()
|
||||
smtp = { ok: true }
|
||||
} catch (e) {
|
||||
smtp = { ok: false, message: e instanceof Error ? e.message : 'unknown error' }
|
||||
}
|
||||
|
||||
let database: { ok: boolean; message?: string } = { ok: false }
|
||||
try {
|
||||
await db.execute(sql`select 1`)
|
||||
database = { ok: true }
|
||||
} catch (e) {
|
||||
database = { ok: false, message: e instanceof Error ? e.message : 'unknown error' }
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'admin-app'
|
||||
service: 'admin-app',
|
||||
smtp,
|
||||
database,
|
||||
envSeen
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user