750 lines
21 KiB
JavaScript
750 lines
21 KiB
JavaScript
|
|
const express = require("express");
|
||
|
|
const cors = require("cors");
|
||
|
|
const nodemailer = require("nodemailer");
|
||
|
|
const multer = require("multer");
|
||
|
|
const fs = require("fs");
|
||
|
|
const path = require("path");
|
||
|
|
|
||
|
|
const app = express();
|
||
|
|
const PORT = 8000;
|
||
|
|
|
||
|
|
const DATA_FILE = path.join(__dirname, "projects.json");
|
||
|
|
const JOBS_DATA_FILE = path.join(__dirname, "jobs.json");
|
||
|
|
const GALLERY_DATA_FILE = path.join(__dirname, "gallery.json");
|
||
|
|
|
||
|
|
app.use(cors());
|
||
|
|
app.use(express.json());
|
||
|
|
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
|
||
|
|
app.use("/applications", express.static(path.join(__dirname, "applications")));
|
||
|
|
app.use("/gallery-media", express.static(path.join(__dirname, "gallery-media")));
|
||
|
|
|
||
|
|
const loadData = (filePath) => {
|
||
|
|
if (!fs.existsSync(filePath)) return [];
|
||
|
|
const data = fs.readFileSync(filePath, "utf-8");
|
||
|
|
try {
|
||
|
|
return JSON.parse(data);
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const saveData = (filePath, data) => {
|
||
|
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||
|
|
};
|
||
|
|
|
||
|
|
// ====================== GALLERY API ====================== //
|
||
|
|
|
||
|
|
const galleryStorage = multer.diskStorage({
|
||
|
|
destination: (req, file, cb) => {
|
||
|
|
const dir = "./gallery-media";
|
||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||
|
|
cb(null, dir);
|
||
|
|
},
|
||
|
|
filename: (req, file, cb) => {
|
||
|
|
const uniqueName = Date.now() + "-" + file.originalname;
|
||
|
|
cb(null, uniqueName);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const uploadGallery = multer({
|
||
|
|
storage: galleryStorage,
|
||
|
|
fileFilter: (req, file, cb) => {
|
||
|
|
const filetypes = /jpeg|jpg|png|gif|mp4|mov|avi/;
|
||
|
|
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
|
||
|
|
const mimetype = filetypes.test(file.mimetype);
|
||
|
|
|
||
|
|
if (extname && mimetype) {
|
||
|
|
return cb(null, true);
|
||
|
|
} else {
|
||
|
|
cb(new Error("Only images (JPEG, JPG, PNG, GIF) and videos (MP4, MOV, AVI) are allowed"));
|
||
|
|
}
|
||
|
|
},
|
||
|
|
limits: { fileSize: 500 * 1024 * 1024 } // 500MB limit
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get all gallery items with filtering by category
|
||
|
|
app.get("/api/gallery", (req, res) => {
|
||
|
|
const items = loadData(GALLERY_DATA_FILE);
|
||
|
|
|
||
|
|
if (req.query.category) {
|
||
|
|
const filtered = items.filter(item => item.category === req.query.category);
|
||
|
|
return res.json(filtered);
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json(items);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add new gallery item
|
||
|
|
app.post("/api/gallery", uploadGallery.single("media"), (req, res) => {
|
||
|
|
const { category, caption, date } = req.body;
|
||
|
|
const mediaFile = req.file;
|
||
|
|
|
||
|
|
if (!category || !mediaFile) {
|
||
|
|
return res.status(400).json({ error: "Category and media file are required" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const mediaType = mediaFile.mimetype.startsWith("video") ? "video" : "image";
|
||
|
|
|
||
|
|
const newItem = {
|
||
|
|
id: Date.now(),
|
||
|
|
category,
|
||
|
|
caption: caption || "",
|
||
|
|
date: date || "",
|
||
|
|
type: mediaType,
|
||
|
|
url: `/gallery-media/${mediaFile.filename}`,
|
||
|
|
createdAt: new Date().toISOString()
|
||
|
|
};
|
||
|
|
|
||
|
|
const items = loadData(GALLERY_DATA_FILE);
|
||
|
|
items.push(newItem);
|
||
|
|
saveData(GALLERY_DATA_FILE, items);
|
||
|
|
|
||
|
|
res.status(201).json({ message: "Gallery item added successfully", item: newItem });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Update gallery item
|
||
|
|
app.put("/api/gallery/:id", uploadGallery.single("media"), (req, res) => {
|
||
|
|
const itemId = parseInt(req.params.id);
|
||
|
|
const { category, caption, date } = req.body;
|
||
|
|
const mediaFile = req.file;
|
||
|
|
|
||
|
|
let items = loadData(GALLERY_DATA_FILE);
|
||
|
|
const itemIndex = items.findIndex(item => item.id === itemId);
|
||
|
|
|
||
|
|
if (itemIndex === -1) {
|
||
|
|
return res.status(404).json({ error: "Gallery item not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const existingItem = items[itemIndex];
|
||
|
|
let mediaType = existingItem.type;
|
||
|
|
let mediaUrl = existingItem.url;
|
||
|
|
|
||
|
|
if (mediaFile) {
|
||
|
|
// Delete old media file
|
||
|
|
const oldFilename = existingItem.url.split("/").pop();
|
||
|
|
const oldPath = path.join(__dirname, "gallery-media", oldFilename);
|
||
|
|
if (fs.existsSync(oldPath)) {
|
||
|
|
fs.unlinkSync(oldPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
mediaType = mediaFile.mimetype.startsWith("video") ? "video" : "image";
|
||
|
|
mediaUrl = `/gallery-media/${mediaFile.filename}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
items[itemIndex] = {
|
||
|
|
...existingItem,
|
||
|
|
category: category || existingItem.category,
|
||
|
|
caption: caption || existingItem.caption,
|
||
|
|
date: date || existingItem.date,
|
||
|
|
type: mediaType,
|
||
|
|
url: mediaUrl,
|
||
|
|
updatedAt: new Date().toISOString()
|
||
|
|
};
|
||
|
|
|
||
|
|
saveData(GALLERY_DATA_FILE, items);
|
||
|
|
res.json({ message: "Gallery item updated successfully", item: items[itemIndex] });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Delete gallery item
|
||
|
|
app.delete("/api/gallery/:id", (req, res) => {
|
||
|
|
const itemId = parseInt(req.params.id);
|
||
|
|
const items = loadData(GALLERY_DATA_FILE);
|
||
|
|
const itemIndex = items.findIndex(item => item.id === itemId);
|
||
|
|
|
||
|
|
if (itemIndex === -1) {
|
||
|
|
return res.status(404).json({ error: "Gallery item not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete associated media file
|
||
|
|
const filename = items[itemIndex].url.split("/").pop();
|
||
|
|
const filePath = path.join(__dirname, "gallery-media", filename);
|
||
|
|
if (fs.existsSync(filePath)) {
|
||
|
|
fs.unlinkSync(filePath);
|
||
|
|
}
|
||
|
|
|
||
|
|
items.splice(itemIndex, 1);
|
||
|
|
saveData(GALLERY_DATA_FILE, items);
|
||
|
|
|
||
|
|
res.json({ message: "Gallery item deleted successfully" });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========= CONTACT FORM ========= //
|
||
|
|
app.post("/contact", async (req, res) => {
|
||
|
|
const { name, email, contact, message } = req.body;
|
||
|
|
|
||
|
|
// Validation
|
||
|
|
if (!name || !email || !contact || !message) {
|
||
|
|
return res.status(400).json({ success: false, error: "All fields are required" });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Additional validation
|
||
|
|
if (name.length < 2) {
|
||
|
|
return res.status(400).json({ success: false, error: "Name must be at least 2 characters" });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||
|
|
return res.status(400).json({ success: false, error: "Invalid email format" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const digits = contact.replace(/\D/g, "");
|
||
|
|
if (digits.length < 7) {
|
||
|
|
return res.status(400).json({ success: false, error: "Contact number must have at least 7 digits" });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (message.length < 10) {
|
||
|
|
return res.status(400).json({ success: false, error: "Message must be at least 10 characters" });
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Use environment variables for email credentials
|
||
|
|
const emailUser = process.env.EMAIL_USER || "laxmibamnale2002@gmail.com";
|
||
|
|
const emailPass = process.env.EMAIL_PASS || "smqcwjwdsuiywrse";
|
||
|
|
|
||
|
|
if (!emailPass) {
|
||
|
|
console.error("Email password not configured");
|
||
|
|
return res.status(500).json({ success: false, message: "Server configuration error" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const transporter = nodemailer.createTransport({
|
||
|
|
service: "gmail",
|
||
|
|
auth: {
|
||
|
|
user: emailUser,
|
||
|
|
pass: emailPass,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verify connection configuration
|
||
|
|
await transporter.verify();
|
||
|
|
|
||
|
|
const mailToOwner = {
|
||
|
|
from: emailUser, // Use your email as from address to avoid authentication issues
|
||
|
|
replyTo: email, // Set reply-to to customer's email
|
||
|
|
to: emailUser,
|
||
|
|
subject: `New Contact Form Submission from ${name}`,
|
||
|
|
html: `
|
||
|
|
<h3>New Contact Form Submission</h3>
|
||
|
|
<p><strong>Name:</strong> ${name}</p>
|
||
|
|
<p><strong>Email:</strong> ${email}</p>
|
||
|
|
<p><strong>Contact:</strong> ${contact}</p>
|
||
|
|
<p><strong>Message:</strong></p>
|
||
|
|
<p>${message.replace(/\n/g, "<br>")}</p>
|
||
|
|
<p><em>Received on: ${new Date().toLocaleString()}</em></p>
|
||
|
|
`,
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(mailToOwner);
|
||
|
|
|
||
|
|
const autoReply = {
|
||
|
|
from: emailUser,
|
||
|
|
to: email,
|
||
|
|
subject: "Thank you for contacting Laxmi Civil Engineering Services!",
|
||
|
|
html: `
|
||
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
|
|
<h2 style="color: #0056b3;">Thank You for Contacting Us!</h2>
|
||
|
|
<p>Dear ${name},</p>
|
||
|
|
<p>Thank you for reaching out to Laxmi Civil Engineering Services Pvt. Ltd. We have received your message and will get back to you within 24-48 hours.</p>
|
||
|
|
<p>For urgent inquiries, please call us at <strong>0231-2521554</strong> or <strong>0231-2683900</strong>.</p>
|
||
|
|
<p><strong>Summary of your message:</strong></p>
|
||
|
|
<blockquote style="background: #f9f9f9; padding: 10px; border-left: 4px solid #ccc;">
|
||
|
|
${message.replace(/\n/g, "<br>")}
|
||
|
|
</blockquote>
|
||
|
|
<p>Best regards,<br/>Team Laxmi Civil Engineering Services</p>
|
||
|
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||
|
|
<p style="font-size: 12px; color: #777;">
|
||
|
|
Laxmi Civil Engineering Services Pvt. Ltd.<br>
|
||
|
|
1148, E. Sykes Extension, Kolhapur 416 001, Maharashtra, India.<br>
|
||
|
|
Phone: 0231-2521554, 2683900 | Email: laxmibamnale2002@gmail.com
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
`,
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(autoReply);
|
||
|
|
|
||
|
|
res.status(200).json({ success: true, message: "Message sent successfully" });
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Error sending email:", err);
|
||
|
|
|
||
|
|
if (err.code === "EAUTH") {
|
||
|
|
res.status(500).json({ success: false, message: "Email authentication failed" });
|
||
|
|
} else if (err.code === "EENVELOPE") {
|
||
|
|
res.status(400).json({ success: false, message: "Invalid email address" });
|
||
|
|
} else {
|
||
|
|
res.status(500).json({ success: false, message: "Server error while sending email" });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
// ========= PROJECT UPLOAD ========= //
|
||
|
|
const storage = multer.diskStorage({
|
||
|
|
destination: (req, file, cb) => {
|
||
|
|
const dir = "./uploads";
|
||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||
|
|
cb(null, dir);
|
||
|
|
},
|
||
|
|
filename: (req, file, cb) => {
|
||
|
|
const uniqueName = Date.now() + "-" + file.originalname;
|
||
|
|
cb(null, uniqueName);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const upload = multer({ storage });
|
||
|
|
|
||
|
|
const loadProjects = () => {
|
||
|
|
if (!fs.existsSync(DATA_FILE)) return [];
|
||
|
|
const data = fs.readFileSync(DATA_FILE, "utf-8");
|
||
|
|
try {
|
||
|
|
return JSON.parse(data);
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const saveProjects = (projects) => {
|
||
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(projects, null, 2));
|
||
|
|
};
|
||
|
|
|
||
|
|
app.post("/api/projects", upload.single("image"), (req, res) => {
|
||
|
|
const { sector } = req.body;
|
||
|
|
const image = req.file ? `/uploads/${req.file.filename}` : "";
|
||
|
|
|
||
|
|
if (!sector || !image) {
|
||
|
|
return res.status(400).json({ error: "Sector and image are required" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const newProject = {
|
||
|
|
id: Date.now(),
|
||
|
|
sector,
|
||
|
|
image,
|
||
|
|
};
|
||
|
|
|
||
|
|
const projects = loadProjects();
|
||
|
|
projects.push(newProject);
|
||
|
|
saveProjects(projects);
|
||
|
|
|
||
|
|
res.status(201).json({ message: "Project added successfully", project: newProject });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get("/api/projects", (req, res) => {
|
||
|
|
const projects = loadProjects();
|
||
|
|
res.json(projects);
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post("/api/projects/update/:id", upload.single("image"), (req, res) => {
|
||
|
|
const projectId = parseInt(req.params.id);
|
||
|
|
const { sector } = req.body;
|
||
|
|
|
||
|
|
let projects = loadProjects();
|
||
|
|
const projectIndex = projects.findIndex((p) => p.id === projectId);
|
||
|
|
|
||
|
|
if (projectIndex === -1) {
|
||
|
|
return res.status(404).json({ error: "Project not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const existingProject = projects[projectIndex];
|
||
|
|
|
||
|
|
projects[projectIndex] = {
|
||
|
|
...existingProject,
|
||
|
|
sector,
|
||
|
|
image: req.file ? `/uploads/${req.file.filename}` : existingProject.image,
|
||
|
|
};
|
||
|
|
|
||
|
|
saveProjects(projects);
|
||
|
|
|
||
|
|
res.json({ message: "Project updated successfully", project: projects[projectIndex] });
|
||
|
|
});
|
||
|
|
|
||
|
|
app.delete("/api/projects/:id", (req, res) => {
|
||
|
|
const projectId = parseInt(req.params.id);
|
||
|
|
|
||
|
|
let projects = loadProjects();
|
||
|
|
const updatedProjects = projects.filter((project) => project.id !== projectId);
|
||
|
|
|
||
|
|
if (projects.length === updatedProjects.length) {
|
||
|
|
return res.status(404).json({ error: "Project not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
saveProjects(updatedProjects);
|
||
|
|
res.json({ message: "Project deleted successfully" });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========= JOB APPLICATION UPLOAD ========= //
|
||
|
|
const applicationStorage = multer.diskStorage({
|
||
|
|
destination: (req, file, cb) => {
|
||
|
|
const dir = "./applications";
|
||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||
|
|
cb(null, dir);
|
||
|
|
},
|
||
|
|
filename: (req, file, cb) => {
|
||
|
|
const uniqueName = Date.now() + "-" + file.originalname;
|
||
|
|
cb(null, uniqueName);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const uploadApplication = multer({ storage: applicationStorage });
|
||
|
|
|
||
|
|
app.post("/send-application", uploadApplication.single("resume"), async (req, res) => {
|
||
|
|
const {
|
||
|
|
fullName,
|
||
|
|
email,
|
||
|
|
phone,
|
||
|
|
address,
|
||
|
|
education,
|
||
|
|
skill,
|
||
|
|
interest,
|
||
|
|
totalExperience,
|
||
|
|
expectedSalary,
|
||
|
|
currentCompany,
|
||
|
|
currentDesignation,
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
if (!fullName || !email || !phone || !education || !skill || !totalExperience || !req.file) {
|
||
|
|
return res.status(400).json({ success: false, message: "Missing required fields" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const resumePath = path.join(__dirname, "applications", req.file.filename);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const transporter = nodemailer.createTransport({
|
||
|
|
service: "gmail",
|
||
|
|
auth: {
|
||
|
|
user: "laxmibamnale2002@gmail.com",
|
||
|
|
pass: "smqcwjwdsuiywrse",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const mailOptions = {
|
||
|
|
from: email,
|
||
|
|
to: "laxmibamnale2002@gmail.com",
|
||
|
|
subject: `New Job Application from ${fullName}`,
|
||
|
|
text: `Full Name: ${fullName}
|
||
|
|
Email: ${email}
|
||
|
|
Phone: ${phone}
|
||
|
|
Address: ${address}
|
||
|
|
Education: ${education}
|
||
|
|
Skill: ${skill}
|
||
|
|
Interest: ${interest}
|
||
|
|
Experience: ${totalExperience}
|
||
|
|
Expected Salary: ${expectedSalary}
|
||
|
|
Current Company: ${currentCompany}
|
||
|
|
Current Designation: ${currentDesignation}`,
|
||
|
|
attachments: [
|
||
|
|
{
|
||
|
|
filename: req.file.originalname,
|
||
|
|
path: resumePath,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(mailOptions);
|
||
|
|
|
||
|
|
const autoReply = {
|
||
|
|
from: "laxmibamnale2002@gmail.com",
|
||
|
|
to: email,
|
||
|
|
subject: "Application Received - Laxmi Civil Engineering Services",
|
||
|
|
text: `Dear ${fullName},
|
||
|
|
|
||
|
|
Thank you for applying to Laxmi Civil Engineering Services Pvt. Ltd.
|
||
|
|
|
||
|
|
We have received your application and resume. Our HR team will review your profile and get back to you shortly if shortlisted.
|
||
|
|
|
||
|
|
Best regards,
|
||
|
|
Laxmi Civil Engineering Services Pvt. Ltd.`,
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(autoReply);
|
||
|
|
|
||
|
|
res.status(200).json({ success: true, message: "Application submitted successfully" });
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Error submitting application:", err);
|
||
|
|
res.status(500).json({ success: false, message: "Server error" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========= ENHANCED JOB POSTINGS API ========= //
|
||
|
|
|
||
|
|
const loadJobs = () => {
|
||
|
|
if (!fs.existsSync(JOBS_DATA_FILE)) return [];
|
||
|
|
const data = fs.readFileSync(JOBS_DATA_FILE, "utf-8");
|
||
|
|
try {
|
||
|
|
return JSON.parse(data);
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const saveJobs = (jobs) => {
|
||
|
|
fs.writeFileSync(JOBS_DATA_FILE, JSON.stringify(jobs, null, 2));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Get all jobs with automatic status updates
|
||
|
|
app.get("/api/jobs", (req, res) => {
|
||
|
|
let jobs = loadJobs();
|
||
|
|
const currentDate = new Date();
|
||
|
|
|
||
|
|
// Update job statuses based on closing date
|
||
|
|
jobs = jobs.map(job => {
|
||
|
|
if (job.closingDate && new Date(job.closingDate) < currentDate) {
|
||
|
|
return { ...job, isActive: false };
|
||
|
|
}
|
||
|
|
return job;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Save updated statuses if any changed
|
||
|
|
saveJobs(jobs);
|
||
|
|
|
||
|
|
res.json(jobs);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create new job posting with enhanced fields
|
||
|
|
app.post("/api/jobs", (req, res) => {
|
||
|
|
const {
|
||
|
|
positionName,
|
||
|
|
qualification,
|
||
|
|
experience,
|
||
|
|
location,
|
||
|
|
skills,
|
||
|
|
salary,
|
||
|
|
numberOfOpenings,
|
||
|
|
jobDescription,
|
||
|
|
postingDate,
|
||
|
|
closingDate,
|
||
|
|
isActive = true
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
if (
|
||
|
|
!positionName ||
|
||
|
|
!qualification ||
|
||
|
|
!experience ||
|
||
|
|
!location ||
|
||
|
|
!Array.isArray(skills) ||
|
||
|
|
skills.length === 0 ||
|
||
|
|
!salary ||
|
||
|
|
!numberOfOpenings ||
|
||
|
|
!jobDescription ||
|
||
|
|
!postingDate
|
||
|
|
) {
|
||
|
|
return res.status(400).json({ error: "Missing required fields" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const jobs = loadJobs();
|
||
|
|
|
||
|
|
const newJob = {
|
||
|
|
id: Date.now(),
|
||
|
|
positionName,
|
||
|
|
qualification,
|
||
|
|
experience,
|
||
|
|
location,
|
||
|
|
skills,
|
||
|
|
salary,
|
||
|
|
numberOfOpenings,
|
||
|
|
jobDescription,
|
||
|
|
postingDate,
|
||
|
|
closingDate: closingDate || null,
|
||
|
|
isActive: closingDate ? new Date(closingDate) >= new Date() : isActive
|
||
|
|
};
|
||
|
|
|
||
|
|
jobs.push(newJob);
|
||
|
|
saveJobs(jobs);
|
||
|
|
|
||
|
|
res.status(201).json({ message: "Job created", job: newJob });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Update existing job posting with enhanced fields
|
||
|
|
app.put("/api/jobs/:id", (req, res) => {
|
||
|
|
const jobId = parseInt(req.params.id);
|
||
|
|
const {
|
||
|
|
positionName,
|
||
|
|
qualification,
|
||
|
|
experience,
|
||
|
|
location,
|
||
|
|
skills,
|
||
|
|
salary,
|
||
|
|
numberOfOpenings,
|
||
|
|
jobDescription,
|
||
|
|
postingDate,
|
||
|
|
closingDate,
|
||
|
|
isActive
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
const jobs = loadJobs();
|
||
|
|
const index = jobs.findIndex((job) => job.id === jobId);
|
||
|
|
|
||
|
|
if (index === -1) {
|
||
|
|
return res.status(404).json({ error: "Job not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
!positionName ||
|
||
|
|
!qualification ||
|
||
|
|
!experience ||
|
||
|
|
!location ||
|
||
|
|
!Array.isArray(skills) ||
|
||
|
|
skills.length === 0 ||
|
||
|
|
!salary ||
|
||
|
|
!numberOfOpenings ||
|
||
|
|
!jobDescription ||
|
||
|
|
!postingDate
|
||
|
|
) {
|
||
|
|
return res.status(400).json({ error: "Missing required fields" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const currentDate = new Date();
|
||
|
|
const closingDateObj = closingDate ? new Date(closingDate) : null;
|
||
|
|
const actualIsActive = closingDateObj ? closingDateObj >= currentDate : isActive;
|
||
|
|
|
||
|
|
jobs[index] = {
|
||
|
|
id: jobId,
|
||
|
|
positionName,
|
||
|
|
qualification,
|
||
|
|
experience,
|
||
|
|
location,
|
||
|
|
skills,
|
||
|
|
salary,
|
||
|
|
numberOfOpenings,
|
||
|
|
jobDescription,
|
||
|
|
postingDate,
|
||
|
|
closingDate: closingDate || null,
|
||
|
|
isActive: actualIsActive
|
||
|
|
};
|
||
|
|
|
||
|
|
saveJobs(jobs);
|
||
|
|
|
||
|
|
res.json({ message: "Job updated", job: jobs[index] });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Toggle job active status
|
||
|
|
app.patch("/api/jobs/:id/toggle-active", (req, res) => {
|
||
|
|
const jobId = parseInt(req.params.id);
|
||
|
|
const jobs = loadJobs();
|
||
|
|
const index = jobs.findIndex((job) => job.id === jobId);
|
||
|
|
|
||
|
|
if (index === -1) {
|
||
|
|
return res.status(404).json({ error: "Job not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't allow toggling if there's a closing date in the past
|
||
|
|
const currentDate = new Date();
|
||
|
|
if (jobs[index].closingDate && new Date(jobs[index].closingDate) < currentDate) {
|
||
|
|
return res.status(400).json({ error: "Cannot activate job with expired closing date" });
|
||
|
|
}
|
||
|
|
|
||
|
|
jobs[index].isActive = !jobs[index].isActive;
|
||
|
|
saveJobs(jobs);
|
||
|
|
|
||
|
|
res.json({ message: "Job status updated", job: jobs[index] });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Delete job posting
|
||
|
|
app.delete("/api/jobs/:id", (req, res) => {
|
||
|
|
const jobId = parseInt(req.params.id);
|
||
|
|
let jobs = loadJobs();
|
||
|
|
const initialLength = jobs.length;
|
||
|
|
jobs = jobs.filter((job) => job.id !== jobId);
|
||
|
|
|
||
|
|
if (jobs.length === initialLength) {
|
||
|
|
return res.status(404).json({ error: "Job not found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
saveJobs(jobs);
|
||
|
|
|
||
|
|
res.json({ message: "Job deleted" });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========= CAREER CONTACT FORM ========= //
|
||
|
|
const careerStorage = multer.diskStorage({
|
||
|
|
destination: (req, file, cb) => {
|
||
|
|
const dir = "./career-applications";
|
||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||
|
|
cb(null, dir);
|
||
|
|
},
|
||
|
|
filename: (req, file, cb) => {
|
||
|
|
const uniqueName = Date.now() + "-" + file.originalname;
|
||
|
|
cb(null, uniqueName);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const uploadCareer = multer({
|
||
|
|
storage: careerStorage,
|
||
|
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||
|
|
fileFilter: (req, file, cb) => {
|
||
|
|
const allowedExt = ['.pdf', '.doc', '.docx'];
|
||
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
||
|
|
if (allowedExt.includes(ext)) {
|
||
|
|
cb(null, true);
|
||
|
|
} else {
|
||
|
|
cb(new Error('Only PDF/DOC/DOCX files are allowed'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/careers/contact', uploadCareer.single('resume'), async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { fullName, email, phone } = req.body;
|
||
|
|
const resumeFile = req.file;
|
||
|
|
|
||
|
|
if (!fullName || !email || !resumeFile) {
|
||
|
|
return res.status(400).json({ success: false, error: 'Missing required fields or resume.' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const transporter = nodemailer.createTransport({
|
||
|
|
service: "gmail",
|
||
|
|
auth: {
|
||
|
|
user: "laxmibamnale2002@gmail.com",
|
||
|
|
pass: "smqcwjwdsuiywrse",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const mailOptions = {
|
||
|
|
from: '"Career Contact Form" <laxmibamnale2002@gmail.com>',
|
||
|
|
to: "laxmibamnale2002@gmail.com",
|
||
|
|
subject: `New Career Contact from ${fullName}`,
|
||
|
|
text: `
|
||
|
|
You have received a new career contact form submission.
|
||
|
|
|
||
|
|
Full Name: ${fullName}
|
||
|
|
Email: ${email}
|
||
|
|
Phone: ${phone || 'Not provided'}
|
||
|
|
|
||
|
|
Resume is attached.
|
||
|
|
`,
|
||
|
|
attachments: [
|
||
|
|
{
|
||
|
|
filename: resumeFile.originalname,
|
||
|
|
path: resumeFile.path,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(mailOptions);
|
||
|
|
|
||
|
|
const autoReply = {
|
||
|
|
from: "<laxmibamnale2002@gmail.com>",
|
||
|
|
to: email,
|
||
|
|
subject: "Thank you for contacting Laxmi Civil Engineering Services",
|
||
|
|
text: `Dear ${fullName},
|
||
|
|
|
||
|
|
Thank you for reaching out to us via the Career Contact form. We have received your details and resume.
|
||
|
|
|
||
|
|
Our HR team will review and get back to you soon.
|
||
|
|
|
||
|
|
Best regards,
|
||
|
|
Laxmi Civil Engineering Services Pvt. Ltd.`,
|
||
|
|
};
|
||
|
|
|
||
|
|
await transporter.sendMail(autoReply);
|
||
|
|
|
||
|
|
fs.unlink(resumeFile.path, (err) => {
|
||
|
|
if (err) console.error('Error deleting uploaded resume:', err);
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({ success: true, message: 'Career contact form submitted successfully.' });
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error in /api/careers/contact:', error);
|
||
|
|
res.status(500).json({ success: false, message: 'Server error' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========= START SERVER ========= //
|
||
|
|
app.listen(PORT, "0.0.0.0", () => {
|
||
|
|
console.log(`Server running on http://0.0.0.0:${PORT}`);
|
||
|
|
});
|