feat/inquiry #3

Merged
arjunsthampi merged 2 commits from feat/inquiry into dev 2026-03-25 09:16:55 +00:00
6 changed files with 232 additions and 18 deletions
Showing only changes of commit 380cb4d999 - Show all commits
@@ -1,10 +1,13 @@
import prisma from "../prisma/client.js"; import prisma from "../prisma/client.js";
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
// CREATE ACADEMICS & RESEARCH // CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => { export const createAcademicsResearch = async (req, res) => {
try { try {
const {fullName, number, emailId, subject, courseName, message} = req.body; const { fullName, number, emailId, subject, courseName, message } =
req.body;
if (!fullName || !number) { if (!fullName || !number) {
return res.status(400).json({ return res.status(400).json({
@@ -24,6 +27,32 @@ export const createAcademicsResearch = async (req, res) => {
}, },
}); });
try {
const emailList = await getEmailsByType("ACADEMICS");
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: "New Academics & Research Inquiry",
html: `
<h2>New Academics & Research Inquiry</h2>
<p><b>Name:</b> ${fullName}</p>
<p><b>Phone:</b> ${number}</p>
<p><b>Email:</b> ${emailId || "-"}</p>
<p><b>Course:</b> ${courseName || "-"}</p>
<p><b>Subject:</b> ${subject || "-"}</p>
<p><b>Message:</b></p>
<p>${message || "-"}</p>
`,
});
}
} catch (err) {
console.error("Academics email failed:", err);
}
res.status(200).json({ res.status(200).json({
success: true, success: true,
status: 200, status: 200,
+2
View File
@@ -18,6 +18,7 @@ import EmailPage from "./pages/email";
import CareerPage from "./pages/Career"; import CareerPage from "./pages/Career";
import CandidatePage from "./pages/candidates"; import CandidatePage from "./pages/candidates";
import InquiryPage from "./pages/inquiry"; import InquiryPage from "./pages/inquiry";
import AcademicsPage from "./pages/Academics";
export default function App() { export default function App() {
return ( return (
@@ -40,6 +41,7 @@ export default function App() {
<Route path="/career" element={<CareerPage />} /> <Route path="/career" element={<CareerPage />} />
<Route path="/candidate" element={<CandidatePage />} /> <Route path="/candidate" element={<CandidatePage />} />
<Route path="/inquiry" element={<InquiryPage />} /> <Route path="/inquiry" element={<InquiryPage />} />
<Route path="/academics" element={<AcademicsPage />} />
</Route> </Route>
</Route> </Route>
+11
View File
@@ -0,0 +1,11 @@
import apiClient from "@/api/client";
export const getAcademicsApi = async () => {
const res = await apiClient.get("/academics/getAll");
return res.data;
};
export const deleteAcademicsApi = async (id: number) => {
const res = await apiClient.delete(`/academics/${id}`);
return res.data;
};
@@ -31,6 +31,10 @@ export default function Sidebar() {
name: "Inquiry", name: "Inquiry",
path: "/inquiry", path: "/inquiry",
}, },
{
name: "Academics & Research",
path: "/academics",
},
{ {
name: "Email", name: "Email",
path: "/email", path: "/email",
+172
View File
@@ -0,0 +1,172 @@
import { useState, useEffect, useCallback } from "react";
import { getAcademicsApi, deleteAcademicsApi } from "@/api/academics";
import { exportToExcel } from "@/utils/exportToExcel";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Loader2, Trash, RefreshCw, Download } from "lucide-react";
export default function AcademicsPage() {
const [records, setRecords] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState("");
const fetchAll = useCallback(async () => {
setLoading(true);
try {
const res = await getAcademicsApi();
setRecords(res?.data || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const filteredRecords = records.filter((item) => {
return (
item.fullName?.toLowerCase().includes(searchText.toLowerCase()) ||
item.number?.includes(searchText) ||
item.emailId?.toLowerCase().includes(searchText.toLowerCase()) ||
item.subject?.toLowerCase().includes(searchText.toLowerCase()) ||
item.courseName?.toLowerCase().includes(searchText.toLowerCase())
);
});
async function handleDelete(id: number) {
if (!confirm("Delete record?")) return;
await deleteAcademicsApi(id);
fetchAll();
}
const handleExport = () => {
const exportData = filteredRecords.map((item) => ({
ID: item.id,
Name: item.fullName,
Phone: item.number,
Email: item.emailId,
Course: item.courseName,
Subject: item.subject,
Message: item.message,
Date: new Date(item.createdAt).toLocaleDateString(),
}));
exportToExcel(exportData, "academics");
};
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold">Academics & Research</h1>
<div className="flex flex-wrap gap-2">
<Input
placeholder="Search name / phone / email / subject..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[260px]"
/>
<Button variant="outline" onClick={fetchAll} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button variant="outline" onClick={handleExport}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Academics Records</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table className="min-w-[1000px]">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Email</TableHead>
<TableHead>Course</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Message</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={9} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredRecords.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center">
No records found
</TableCell>
</TableRow>
) : (
filteredRecords.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell>{item.fullName}</TableCell>
<TableCell>{item.number}</TableCell>
<TableCell>{item.emailId}</TableCell>
<TableCell>{item.courseName}</TableCell>
<TableCell>{item.subject}</TableCell>
<TableCell className="max-w-[250px] whitespace-normal">
{item.message}
</TableCell>
<TableCell>
{new Date(item.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(item.id)}>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}
+5 -9
View File
@@ -181,16 +181,14 @@ export default function EmailPage() {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => openEdit(item)} onClick={() => openEdit(item)}>
>
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="destructive" variant="destructive"
onClick={() => handleDelete(item.id)} onClick={() => handleDelete(item.id)}>
>
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>
</TableCell> </TableCell>
@@ -227,11 +225,10 @@ export default function EmailPage() {
name="type" name="type"
value={form.type} value={form.type}
onChange={handleChange} onChange={handleChange}
className="border rounded px-2 py-2 w-full" className="border rounded px-2 py-2 w-full">
>
<option value="APPOINTMENT">APPOINTMENT</option> <option value="APPOINTMENT">APPOINTMENT</option>
<option value="CANDIDATE">CANDIDATE</option> <option value="CANDIDATE">CANDIDATE</option>
<option value="INQUIRY">INQUIRY</option> <option value="ACADEMICS">ACADEMICS</option>
</select> </select>
<select <select
@@ -243,8 +240,7 @@ export default function EmailPage() {
isActive: e.target.value === "true", isActive: e.target.value === "true",
}) })
} }
className="border rounded px-2 py-2 w-full" className="border rounded px-2 py-2 w-full">
>
<option value="true">Active</option> <option value="true">Active</option>
<option value="false">Inactive</option> <option value="false">Inactive</option>
</select> </select>