Upload 17 files
Browse files- Dockerfile +25 -0
- __pycache__/email_agent.cpython-312.pyc +0 -0
- __pycache__/email_service.cpython-312.pyc +0 -0
- __pycache__/other_functions.cpython-312.pyc +0 -0
- __pycache__/shortlist_mail.cpython-312.pyc +0 -0
- ai_interview_agent.py +296 -0
- app.py +535 -0
- email_agent.py +198 -0
- email_service.py +51 -0
- other_functions.py +252 -0
- requirements.txt +5 -0
- templates/dashboard.html +377 -0
- templates/email_dashboard.html +123 -0
- templates/index.html +124 -0
- templates/job_details.html +575 -0
- templates/job_posting.html +353 -0
- templates/technical.html +52 -0
Dockerfile
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python runtime as a parent image
|
2 |
+
FROM python:3.9
|
3 |
+
|
4 |
+
# Set the working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy the requirements file
|
8 |
+
COPY requirements.txt .
|
9 |
+
|
10 |
+
# Install dependencies
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Copy the rest of the application code
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
# Expose the Flask default port
|
17 |
+
EXPOSE 5000
|
18 |
+
|
19 |
+
# Set environment variables
|
20 |
+
ENV FLASK_APP=app.py
|
21 |
+
ENV FLASK_RUN_HOST=0.0.0.0
|
22 |
+
ENV FLASK_ENV=production
|
23 |
+
|
24 |
+
# Command to run the application
|
25 |
+
CMD ["flask", "run"]
|
__pycache__/email_agent.cpython-312.pyc
ADDED
Binary file (8.23 kB). View file
|
|
__pycache__/email_service.cpython-312.pyc
ADDED
Binary file (2.31 kB). View file
|
|
__pycache__/other_functions.cpython-312.pyc
ADDED
Binary file (10.2 kB). View file
|
|
__pycache__/shortlist_mail.cpython-312.pyc
ADDED
Binary file (8.04 kB). View file
|
|
ai_interview_agent.py
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
from pymongo import MongoClient
|
4 |
+
import speech_recognition as sr
|
5 |
+
from datetime import datetime
|
6 |
+
import certifi
|
7 |
+
import cv2
|
8 |
+
import threading
|
9 |
+
import time
|
10 |
+
|
11 |
+
|
12 |
+
class AIInterviewer:
|
13 |
+
def __init__(self):
|
14 |
+
# IBM Cloud API setup
|
15 |
+
self.url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation?version=2023-05-29"
|
16 |
+
self.auth_url = "https://iam.cloud.ibm.com/identity/token"
|
17 |
+
self.apikey = "9FV7l0Jxqe7ceL09MeH_g9bYioIQuABXsr1j1VHbKOpr"
|
18 |
+
|
19 |
+
# MongoDB setup
|
20 |
+
MONGODB_URI = "mongodb+srv://roshauninfant:[email protected]/?retryWrites=true&w=majority&appName=slackbot"
|
21 |
+
self.client = MongoClient(MONGODB_URI, tlsCAFile=certifi.where())
|
22 |
+
self.db = self.client['Resume']
|
23 |
+
|
24 |
+
# Question categories
|
25 |
+
self.question_categories = [
|
26 |
+
"technical_expertise",
|
27 |
+
"problem_solving",
|
28 |
+
"project_experience",
|
29 |
+
"behavioral",
|
30 |
+
"role_specific"
|
31 |
+
]
|
32 |
+
|
33 |
+
def get_response(self, question_type, candidate_info, focus_areas, depth_level, special_instructions, category):
|
34 |
+
token = self.get_auth_token()
|
35 |
+
headers = {
|
36 |
+
"Accept": "application/json",
|
37 |
+
"Content-Type": "application/json",
|
38 |
+
"Authorization": f"Bearer {token}"
|
39 |
+
}
|
40 |
+
|
41 |
+
prompt = f"""<|start_of_role|>system<|end_of_role|>
|
42 |
+
You are an experienced HR interviewer conducting a professional interview. You specialize in asking insightful questions that reveal a candidate's true capabilities and potential.
|
43 |
+
|
44 |
+
Candidate Profile:
|
45 |
+
- Experience Level: {candidate_info.get('experience')}
|
46 |
+
- Key Skills: {', '.join(candidate_info.get('matching_skills', []))}
|
47 |
+
- Focus Areas: {', '.join(focus_areas)}
|
48 |
+
- Seniority Level: {depth_level}
|
49 |
+
|
50 |
+
Category: {category}
|
51 |
+
Special Instructions: {special_instructions}
|
52 |
+
|
53 |
+
Guidelines for question generation:
|
54 |
+
1. For Technical Expertise: Focus on practical application of skills
|
55 |
+
2. For Problem Solving: Present real-world scenarios
|
56 |
+
3. For Project Experience: Ask about specific achievements and challenges
|
57 |
+
4. For Behavioral: Focus on past experiences and decision-making
|
58 |
+
5. For Role Specific: Align with the target position requirements
|
59 |
+
|
60 |
+
Generate a single, well-structured interview question that:
|
61 |
+
- Is specific to the {category} category
|
62 |
+
- Matches the candidate's experience level
|
63 |
+
- Allows the candidate to showcase their expertise
|
64 |
+
- Encourages detailed responses
|
65 |
+
- Avoids yes/no answers
|
66 |
+
- Is different from standard interview questions
|
67 |
+
|
68 |
+
Question format: Start with a brief context (if needed) followed by the main question.
|
69 |
+
<|end_of_text|>
|
70 |
+
<|start_of_role|>assistant<|end_of_role|>"""
|
71 |
+
|
72 |
+
body = {
|
73 |
+
"input": prompt,
|
74 |
+
"parameters": {
|
75 |
+
"decoding_method": "sample",
|
76 |
+
"top_k": 50,
|
77 |
+
"temperature": 0.7,
|
78 |
+
"max_new_tokens": 200,
|
79 |
+
"repetition_penalty": 1.2
|
80 |
+
},
|
81 |
+
"model_id": "ibm/granite-3-8b-instruct",
|
82 |
+
"project_id": "4aa39c25-19d7-48c1-9cf6-e31b5c223a1f"
|
83 |
+
}
|
84 |
+
|
85 |
+
response = requests.post(self.url, headers=headers, json=body)
|
86 |
+
return response.json().get("results", [{}])[0].get("generated_text", "")
|
87 |
+
|
88 |
+
def get_auth_token(self):
|
89 |
+
auth_data = {
|
90 |
+
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
|
91 |
+
"apikey": self.apikey
|
92 |
+
}
|
93 |
+
auth_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
94 |
+
auth_response = requests.post(self.auth_url, data=auth_data, headers=auth_headers)
|
95 |
+
return auth_response.json().get("access_token")
|
96 |
+
|
97 |
+
|
98 |
+
def get_audio_input():
|
99 |
+
r = sr.Recognizer()
|
100 |
+
with sr.Microphone() as source:
|
101 |
+
st.write("🎤 Listening... Please speak your answer.")
|
102 |
+
try:
|
103 |
+
# Adjust for ambient noise
|
104 |
+
r.adjust_for_ambient_noise(source, duration=0.5)
|
105 |
+
audio = r.listen(source, timeout=30, phrase_time_limit=120)
|
106 |
+
text = r.recognize_google(audio)
|
107 |
+
return text, None
|
108 |
+
except sr.WaitTimeoutError:
|
109 |
+
return None, "No speech detected. Please try again."
|
110 |
+
except sr.UnknownValueError:
|
111 |
+
return None, "Could not understand audio. Please try again."
|
112 |
+
except sr.RequestError:
|
113 |
+
return None, "Could not process speech. Please try text input instead."
|
114 |
+
|
115 |
+
|
116 |
+
def video_feed():
|
117 |
+
cap = cv2.VideoCapture(0)
|
118 |
+
frame_placeholder = st.empty()
|
119 |
+
|
120 |
+
while cap.isOpened():
|
121 |
+
ret, frame = cap.read()
|
122 |
+
if not ret:
|
123 |
+
st.error("Failed to capture video")
|
124 |
+
break
|
125 |
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
126 |
+
frame_placeholder.image(frame, channels="RGB")
|
127 |
+
time.sleep(0.03) # Reduce CPU usage
|
128 |
+
|
129 |
+
cap.release()
|
130 |
+
|
131 |
+
|
132 |
+
def main():
|
133 |
+
st.set_page_config(page_title="Professional AI Interview", page_icon="👔")
|
134 |
+
|
135 |
+
# Styling
|
136 |
+
st.markdown("""
|
137 |
+
<style>
|
138 |
+
.main {
|
139 |
+
padding: 2rem;
|
140 |
+
}
|
141 |
+
.stButton button {
|
142 |
+
width: 100%;
|
143 |
+
border-radius: 5px;
|
144 |
+
height: 3em;
|
145 |
+
}
|
146 |
+
</style>
|
147 |
+
""", unsafe_allow_html=True)
|
148 |
+
|
149 |
+
# Initialize interviewer
|
150 |
+
interviewer = AIInterviewer()
|
151 |
+
|
152 |
+
# Get interview ID and details
|
153 |
+
query_params = st.query_params
|
154 |
+
interview_id = query_params.get("interview_id", None)
|
155 |
+
|
156 |
+
if not interview_id:
|
157 |
+
st.error("⚠️ Invalid interview link")
|
158 |
+
return
|
159 |
+
|
160 |
+
# Get interview and candidate details
|
161 |
+
interview = interviewer.db.interviews.find_one({"interview_id": interview_id})
|
162 |
+
if not interview:
|
163 |
+
st.error("⚠️ Interview not found")
|
164 |
+
return
|
165 |
+
|
166 |
+
candidate = interviewer.db.ai_processed_candidates.find_one({"_id": interview["candidate_id"]})
|
167 |
+
if not candidate:
|
168 |
+
st.error("⚠️ Candidate information not found")
|
169 |
+
return
|
170 |
+
|
171 |
+
# Professional welcome message
|
172 |
+
st.title("🤝 Professional AI Interview")
|
173 |
+
st.write(
|
174 |
+
f"Welcome, {candidate['candidate_info']['name']}! We're excited to learn more about your experience and expertise.")
|
175 |
+
|
176 |
+
# Video feed in sidebar
|
177 |
+
st.sidebar.title("📹 Video Feed")
|
178 |
+
st.sidebar.info("Please ensure your camera is on and you're well-positioned in the frame.")
|
179 |
+
if 'video_thread' not in st.session_state:
|
180 |
+
st.session_state.video_thread = threading.Thread(target=video_feed, daemon=True)
|
181 |
+
st.session_state.video_thread.start()
|
182 |
+
|
183 |
+
# Initialize interview state
|
184 |
+
if 'current_question' not in st.session_state:
|
185 |
+
st.session_state.current_question = 0
|
186 |
+
st.session_state.questions_total = 5
|
187 |
+
st.session_state.answers = []
|
188 |
+
st.session_state.verbal_questions = [1, 3] # 2nd and 4th questions
|
189 |
+
st.session_state.current_category = None
|
190 |
+
st.session_state.verbal_response = None
|
191 |
+
|
192 |
+
# Display progress
|
193 |
+
progress = st.progress(st.session_state.current_question / st.session_state.questions_total)
|
194 |
+
st.write(f"📝 Question {st.session_state.current_question + 1} of {st.session_state.questions_total}")
|
195 |
+
|
196 |
+
# Generate question based on category
|
197 |
+
if st.session_state.current_category is None and st.session_state.current_question < st.session_state.questions_total:
|
198 |
+
st.session_state.current_category = interviewer.question_categories[st.session_state.current_question]
|
199 |
+
|
200 |
+
current_q = interviewer.get_response(
|
201 |
+
interview.get('interview_focus', 'technical'),
|
202 |
+
{
|
203 |
+
'experience': candidate['candidate_info'].get('experience'),
|
204 |
+
'matching_skills': candidate['ai_evaluation'].get('matching_skills', []),
|
205 |
+
'interview_focus': interview.get('interview_focus')
|
206 |
+
},
|
207 |
+
focus_areas=interview.get('focus_areas', []),
|
208 |
+
depth_level=interview.get('depth_level', 'entry'),
|
209 |
+
special_instructions=interview.get('special_instructions', ''),
|
210 |
+
category=st.session_state.current_category
|
211 |
+
)
|
212 |
+
|
213 |
+
# Display question with styling
|
214 |
+
st.markdown(f"### Question {st.session_state.current_question + 1}:")
|
215 |
+
st.write(current_q)
|
216 |
+
|
217 |
+
# Handle answer input
|
218 |
+
if st.session_state.current_question in st.session_state.verbal_questions:
|
219 |
+
col1, col2 = st.columns(2)
|
220 |
+
|
221 |
+
with col1:
|
222 |
+
if st.session_state.verbal_response is None:
|
223 |
+
if st.button("🎤 Answer Verbally"):
|
224 |
+
answer, error = get_audio_input()
|
225 |
+
if error:
|
226 |
+
st.error(error)
|
227 |
+
else:
|
228 |
+
st.session_state.verbal_response = answer
|
229 |
+
st.rerun()
|
230 |
+
|
231 |
+
if st.session_state.verbal_response is not None:
|
232 |
+
st.write("Your answer (transcribed):", st.session_state.verbal_response)
|
233 |
+
if st.button("✅ Submit Answer"):
|
234 |
+
st.session_state.answers.append({
|
235 |
+
"question": current_q,
|
236 |
+
"answer": st.session_state.verbal_response,
|
237 |
+
"category": st.session_state.current_category
|
238 |
+
})
|
239 |
+
st.session_state.current_question += 1
|
240 |
+
st.session_state.current_category = None
|
241 |
+
st.session_state.verbal_response = None
|
242 |
+
st.rerun()
|
243 |
+
if st.button("🔄 Record Again"):
|
244 |
+
st.session_state.verbal_response = None
|
245 |
+
st.rerun()
|
246 |
+
|
247 |
+
with col2:
|
248 |
+
if st.button("⌨️ Switch to Text Input"):
|
249 |
+
st.session_state.verbal_questions.remove(st.session_state.current_question)
|
250 |
+
st.session_state.verbal_response = None
|
251 |
+
st.rerun()
|
252 |
+
else:
|
253 |
+
answer = st.text_area("Your answer:", height=150)
|
254 |
+
if st.button("Submit Answer"):
|
255 |
+
if answer.strip():
|
256 |
+
st.session_state.answers.append({
|
257 |
+
"question": current_q,
|
258 |
+
"answer": answer,
|
259 |
+
"category": st.session_state.current_category
|
260 |
+
})
|
261 |
+
st.session_state.current_question += 1
|
262 |
+
st.session_state.current_category = None
|
263 |
+
st.rerun()
|
264 |
+
else:
|
265 |
+
st.warning("Please provide an answer before continuing.")
|
266 |
+
|
267 |
+
# Interview completion
|
268 |
+
if st.session_state.current_question >= st.session_state.questions_total:
|
269 |
+
st.success("Interview completed! Thank you for your time.")
|
270 |
+
|
271 |
+
# Save interview results to MongoDB
|
272 |
+
interview_results = {
|
273 |
+
"interview_id": interview_id,
|
274 |
+
"candidate_id": candidate["_id"],
|
275 |
+
"completion_time": datetime.now(),
|
276 |
+
"answers": st.session_state.answers
|
277 |
+
}
|
278 |
+
interviewer.db.interview_results.insert_one(interview_results)
|
279 |
+
|
280 |
+
st.balloons()
|
281 |
+
|
282 |
+
# Display completion message
|
283 |
+
st.markdown("""
|
284 |
+
### Next Steps
|
285 |
+
- Our team will carefully review your responses
|
286 |
+
- You will receive feedback within 2-3 business days
|
287 |
+
- If you have any questions, please contact your hiring manager
|
288 |
+
|
289 |
+
Thank you for participating in our AI-assisted interview process!
|
290 |
+
""")
|
291 |
+
|
292 |
+
return
|
293 |
+
|
294 |
+
|
295 |
+
if __name__ == "__main__":
|
296 |
+
main()
|
app.py
ADDED
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify, send_file
|
2 |
+
from pymongo import MongoClient
|
3 |
+
from bson import ObjectId
|
4 |
+
from datetime import datetime
|
5 |
+
import gridfs
|
6 |
+
from io import BytesIO
|
7 |
+
import certifi
|
8 |
+
import uuid
|
9 |
+
|
10 |
+
from email_agent import send_emails
|
11 |
+
from datetime import datetime, timedelta
|
12 |
+
import uuid
|
13 |
+
from email_service import EmailService
|
14 |
+
from other_functions import download_resume,generatecoding
|
15 |
+
|
16 |
+
email_service = EmailService(
|
17 |
+
gmail_user="[email protected]",
|
18 |
+
gmail_app_password="kpvo hgsg jfjs qoky"
|
19 |
+
)
|
20 |
+
app = Flask(__name__)
|
21 |
+
|
22 |
+
# MongoDB connection with SSL settings
|
23 |
+
MONGODB_URI = "mongodb+srv://roshauninfant:[email protected]/?retryWrites=true&w=majority&appName=slackbot&tlsAllowInvalidCertificates=true"
|
24 |
+
client = MongoClient(MONGODB_URI, tlsCAFile=certifi.where())
|
25 |
+
db = client['Resume']
|
26 |
+
fs = gridfs.GridFS(db)
|
27 |
+
|
28 |
+
|
29 |
+
@app.route('/')
|
30 |
+
def index():
|
31 |
+
return render_template('index.html')
|
32 |
+
|
33 |
+
|
34 |
+
@app.route('/post-jobs')
|
35 |
+
def post_jobs():
|
36 |
+
return render_template('job_posting.html')
|
37 |
+
@app.route('/get_coding_question')
|
38 |
+
def get_coding_question():
|
39 |
+
return jsonify({"question": generatecoding()})
|
40 |
+
|
41 |
+
@app.route('/technical.html')
|
42 |
+
def technical_page():
|
43 |
+
return render_template('technical.html')
|
44 |
+
@app.route('/dashboard')
|
45 |
+
def dashboard():
|
46 |
+
try:
|
47 |
+
# Get unique job IDs from ai_processed_candidates
|
48 |
+
unique_jobs = db.ai_processed_candidates.distinct('job_id')
|
49 |
+
jobs_data = []
|
50 |
+
|
51 |
+
# Get candidate counts for each job
|
52 |
+
for job_id in unique_jobs:
|
53 |
+
total_candidates = db.ai_processed_candidates.count_documents({"job_id": job_id})
|
54 |
+
shortlisted = db.ai_processed_candidates.count_documents({
|
55 |
+
"job_id": job_id,
|
56 |
+
"status": "shortlisted"
|
57 |
+
})
|
58 |
+
|
59 |
+
jobs_data.append({
|
60 |
+
"job_id": job_id,
|
61 |
+
"title": "Software Engineer", # You can customize this
|
62 |
+
"department": "Engineering", # You can customize this
|
63 |
+
"total_candidates": total_candidates,
|
64 |
+
"shortlisted_count": shortlisted,
|
65 |
+
"status": "active"
|
66 |
+
})
|
67 |
+
|
68 |
+
print("Jobs data:", jobs_data)
|
69 |
+
return render_template('dashboard.html', job_postings=jobs_data)
|
70 |
+
except Exception as e:
|
71 |
+
print(f"Dashboard Error: {e}")
|
72 |
+
return f"Error loading dashboard: {str(e)}", 500
|
73 |
+
|
74 |
+
|
75 |
+
@app.route('/candidate/<candidate_id>')
|
76 |
+
def get_candidate_details(candidate_id):
|
77 |
+
try:
|
78 |
+
# Convert string ID to ObjectId
|
79 |
+
candidate_id = candidate_id
|
80 |
+
|
81 |
+
# Find the candidate with proper null checks
|
82 |
+
candidate = db.ai_processed_candidates.find_one({"_id": candidate_id})
|
83 |
+
if not candidate:
|
84 |
+
return jsonify({"error": "Candidate not found"}), 404
|
85 |
+
|
86 |
+
# Get all interviews for the candidate
|
87 |
+
interviews = list(db.interviews.find({"candidate_id": candidate_id}))
|
88 |
+
|
89 |
+
# Ensure all ObjectId fields are converted to strings
|
90 |
+
candidate['_id'] = str(candidate['_id'])
|
91 |
+
if 'resume_id' in candidate:
|
92 |
+
candidate['resume_id'] = str(candidate['resume_id'])
|
93 |
+
|
94 |
+
# Ensure candidate_info exists with default values
|
95 |
+
if 'candidate_info' not in candidate:
|
96 |
+
candidate['candidate_info'] = {
|
97 |
+
'name': 'Not Available',
|
98 |
+
'email': 'Not Available',
|
99 |
+
'phone': None,
|
100 |
+
'experience': None,
|
101 |
+
'current_company': None
|
102 |
+
}
|
103 |
+
|
104 |
+
# Ensure ai_evaluation exists with default values
|
105 |
+
if 'ai_evaluation' not in candidate:
|
106 |
+
candidate['ai_evaluation'] = {
|
107 |
+
'confidence_score': None,
|
108 |
+
'matching_skills': [],
|
109 |
+
'missing_skills': []
|
110 |
+
}
|
111 |
+
|
112 |
+
# Process interviews
|
113 |
+
processed_interviews = []
|
114 |
+
for interview in interviews:
|
115 |
+
interview['_id'] = str(interview['_id'])
|
116 |
+
interview['candidate_id'] = str(interview['candidate_id'])
|
117 |
+
|
118 |
+
# Format date if exists
|
119 |
+
if 'scheduled_date' in interview and interview['scheduled_date']:
|
120 |
+
interview['scheduled_date'] = interview['scheduled_date'].strftime('%Y-%m-%d %H:%M')
|
121 |
+
else:
|
122 |
+
interview['scheduled_date'] = 'Not scheduled'
|
123 |
+
|
124 |
+
processed_interviews.append(interview)
|
125 |
+
|
126 |
+
return jsonify({
|
127 |
+
"candidate": candidate,
|
128 |
+
"interviews": processed_interviews
|
129 |
+
})
|
130 |
+
except Exception as e:
|
131 |
+
print(f"Error in get_candidate_details: {str(e)}")
|
132 |
+
return jsonify({"error": str(e)}), 500
|
133 |
+
|
134 |
+
|
135 |
+
@app.route('/job/<job_id>')
|
136 |
+
def view_job(job_id):
|
137 |
+
try:
|
138 |
+
print(f"Viewing job: {job_id}")
|
139 |
+
|
140 |
+
# Get and segregate candidates
|
141 |
+
shortlisted_candidates = list(db.ai_processed_candidates.find({
|
142 |
+
"job_id": job_id,
|
143 |
+
"status": {"$in": ["shortlisted", "interview_scheduled", "ai_interview", "tech_interview"]}
|
144 |
+
}))
|
145 |
+
|
146 |
+
rejected_candidates = list(db.ai_processed_candidates.find({
|
147 |
+
"job_id": job_id,
|
148 |
+
"status": "rejected"
|
149 |
+
}))
|
150 |
+
|
151 |
+
print(f"Found {len(shortlisted_candidates)} shortlisted and {len(rejected_candidates)} rejected candidates")
|
152 |
+
|
153 |
+
# Process each candidate
|
154 |
+
for candidate in shortlisted_candidates + rejected_candidates:
|
155 |
+
# Convert ObjectId to string
|
156 |
+
candidate_id = candidate['_id']
|
157 |
+
candidate['_id'] = str(candidate['_id'])
|
158 |
+
candidate['resume_id'] = str(candidate['resume_id'])
|
159 |
+
|
160 |
+
# Check for interview completion if candidate is in ai_interview status
|
161 |
+
if candidate['status'] == 'ai_interview':
|
162 |
+
interview_result = db.interview_results.find_one({
|
163 |
+
"candidate_id": candidate_id
|
164 |
+
})
|
165 |
+
candidate['interview_completed'] = bool(interview_result)
|
166 |
+
else:
|
167 |
+
candidate['interview_completed'] = False
|
168 |
+
|
169 |
+
# Get interview details if any (keeping this for reference)
|
170 |
+
interview = db.interviews.find_one({
|
171 |
+
"candidate_id": candidate_id
|
172 |
+
})
|
173 |
+
|
174 |
+
if interview:
|
175 |
+
interview['_id'] = str(interview['_id'])
|
176 |
+
interview['candidate_id'] = str(interview['candidate_id'])
|
177 |
+
|
178 |
+
# Safely format scheduled_date
|
179 |
+
scheduled_date = interview.get('scheduled_date')
|
180 |
+
if isinstance(scheduled_date, datetime):
|
181 |
+
formatted_date = scheduled_date.strftime('%Y-%m-%d %H:%M')
|
182 |
+
else:
|
183 |
+
formatted_date = str(scheduled_date)
|
184 |
+
|
185 |
+
candidate['has_interview'] = True
|
186 |
+
candidate['interview_data'] = {
|
187 |
+
'type': interview.get('type', 'Not specified'),
|
188 |
+
'scheduled_date': formatted_date,
|
189 |
+
'meeting_link': interview.get('meeting_link', '#'),
|
190 |
+
'streamlit_url': interview.get('streamlit_url', '#')
|
191 |
+
}
|
192 |
+
else:
|
193 |
+
candidate['has_interview'] = False
|
194 |
+
|
195 |
+
job = {
|
196 |
+
"job_id": job_id,
|
197 |
+
"title": "Software Engineer",
|
198 |
+
"department": "Engineering",
|
199 |
+
"status": "active",
|
200 |
+
"total_candidates": len(shortlisted_candidates) + len(rejected_candidates),
|
201 |
+
"shortlisted_count": len(shortlisted_candidates)
|
202 |
+
}
|
203 |
+
|
204 |
+
print("Rendering template...")
|
205 |
+
return render_template('job_details.html',
|
206 |
+
job=job,
|
207 |
+
shortlisted_candidates=shortlisted_candidates,
|
208 |
+
rejected_candidates=rejected_candidates)
|
209 |
+
|
210 |
+
except Exception as e:
|
211 |
+
print(f"View Job Error: {str(e)}")
|
212 |
+
return f"Error viewing job: {str(e)}", 500
|
213 |
+
|
214 |
+
|
215 |
+
|
216 |
+
@app.route('/send_confirmation_emails/<job_id>', methods=['POST'])
|
217 |
+
def send_shortlist_emails(job_id):
|
218 |
+
try:
|
219 |
+
# Your Gmail credentials
|
220 |
+
gmail_user = "[email protected]"
|
221 |
+
gmail_app_password = "kpvo hgsg jfjs qoky"
|
222 |
+
|
223 |
+
result = send_emails(job_id, "shortlisted", gmail_user, gmail_app_password)
|
224 |
+
|
225 |
+
if result['success']:
|
226 |
+
message = f"Successfully sent {result['emails_sent']} emails. "
|
227 |
+
if result['skipped_candidates'] > 0:
|
228 |
+
message += f"Skipped {result['skipped_candidates']} candidates who already have interviews scheduled."
|
229 |
+
if result['failed_emails']:
|
230 |
+
message += f"\nFailed to send to: {', '.join(result['failed_emails'])}"
|
231 |
+
|
232 |
+
return jsonify({
|
233 |
+
"success": True,
|
234 |
+
"message": message,
|
235 |
+
"details": result
|
236 |
+
})
|
237 |
+
else:
|
238 |
+
return jsonify({
|
239 |
+
"success": False,
|
240 |
+
"message": result.get('error', 'Unknown error occurred'),
|
241 |
+
"details": result
|
242 |
+
}), 400
|
243 |
+
|
244 |
+
except Exception as e:
|
245 |
+
return jsonify({
|
246 |
+
"success": False,
|
247 |
+
"message": str(e)
|
248 |
+
}), 400
|
249 |
+
|
250 |
+
|
251 |
+
@app.route('/send_rejection_emails/<job_id>', methods=['POST'])
|
252 |
+
def send_reject_emails(job_id):
|
253 |
+
try:
|
254 |
+
# Your Gmail credentials
|
255 |
+
gmail_user = "[email protected]"
|
256 |
+
gmail_app_password = "kpvo hgsg jfjs qoky"
|
257 |
+
|
258 |
+
result = send_emails(job_id, "rejected", gmail_user, gmail_app_password)
|
259 |
+
|
260 |
+
if result['success']:
|
261 |
+
message = f"Successfully sent {result['emails_sent']} emails. "
|
262 |
+
if result['skipped_candidates'] > 0:
|
263 |
+
message += f"Skipped {result['skipped_candidates']} candidates who already have interviews scheduled."
|
264 |
+
if result['failed_emails']:
|
265 |
+
message += f"\nFailed to send to: {', '.join(result['failed_emails'])}"
|
266 |
+
|
267 |
+
return jsonify({
|
268 |
+
"success": True,
|
269 |
+
"message": message,
|
270 |
+
"details": result
|
271 |
+
})
|
272 |
+
else:
|
273 |
+
return jsonify({
|
274 |
+
"success": False,
|
275 |
+
"message": result.get('error', 'Unknown error occurred'),
|
276 |
+
"details": result
|
277 |
+
}), 400
|
278 |
+
|
279 |
+
except Exception as e:
|
280 |
+
return jsonify({
|
281 |
+
"success": False,
|
282 |
+
"message": str(e)
|
283 |
+
}), 400
|
284 |
+
|
285 |
+
|
286 |
+
@app.route('/schedule_hr_interview', methods=['POST'])
|
287 |
+
def schedule_hr_interview():
|
288 |
+
try:
|
289 |
+
print("Received HR interview scheduling request")
|
290 |
+
data = request.json
|
291 |
+
candidate_id = data['candidate_id']
|
292 |
+
|
293 |
+
print(f"Scheduling for candidate: {candidate_id}")
|
294 |
+
|
295 |
+
# Get candidate details using ObjectId
|
296 |
+
candidate = db.ai_processed_candidates.find_one({"_id": candidate_id})
|
297 |
+
if not candidate:
|
298 |
+
return jsonify({"success": False, "message": "Candidate not found"}), 404
|
299 |
+
|
300 |
+
# Generate meet link
|
301 |
+
meet_id = f"meet-{str(candidate['_id'])[-6:]}"
|
302 |
+
meet_link = f"https://meet.google.com/{meet_id}"
|
303 |
+
|
304 |
+
# Create interview document
|
305 |
+
interview = {
|
306 |
+
"candidate_id": candidate_id, # Store as ObjectId
|
307 |
+
"round_type": "HR Interview",
|
308 |
+
"scheduled_date": datetime.now(),
|
309 |
+
"meeting_link": meet_link,
|
310 |
+
"status": "scheduled",
|
311 |
+
"created_at": datetime.now()
|
312 |
+
}
|
313 |
+
|
314 |
+
print("Creating interview:", interview)
|
315 |
+
|
316 |
+
# Insert interview
|
317 |
+
result = db.interviews.insert_one(interview)
|
318 |
+
|
319 |
+
# Update candidate status
|
320 |
+
db.ai_processed_candidates.update_one(
|
321 |
+
{"_id": candidate_id},
|
322 |
+
{
|
323 |
+
"$set": {
|
324 |
+
"status": "interview_scheduled",
|
325 |
+
"last_updated": datetime.now()
|
326 |
+
}
|
327 |
+
}
|
328 |
+
)
|
329 |
+
|
330 |
+
print("Interview scheduled successfully")
|
331 |
+
|
332 |
+
return jsonify({
|
333 |
+
"success": True,
|
334 |
+
"message": "HR Interview scheduled successfully",
|
335 |
+
"meeting_link": meet_link,
|
336 |
+
"interview_id": str(result.inserted_id)
|
337 |
+
})
|
338 |
+
|
339 |
+
except Exception as e:
|
340 |
+
print(f"Error scheduling HR interview: {str(e)}")
|
341 |
+
return jsonify({"success": False, "message": str(e)}), 400
|
342 |
+
|
343 |
+
|
344 |
+
@app.route('/schedule_ai_interview', methods=['POST'])
|
345 |
+
def schedule_ai_interview():
|
346 |
+
try:
|
347 |
+
data = request.json
|
348 |
+
candidate_id = data['candidate_id']
|
349 |
+
|
350 |
+
# Get candidate details
|
351 |
+
candidate = db.ai_processed_candidates.find_one({"_id": candidate_id})
|
352 |
+
if not candidate:
|
353 |
+
return jsonify({"success": False, "message": "Candidate not found"}), 404
|
354 |
+
|
355 |
+
# Generate unique interview ID
|
356 |
+
interview_id = str(uuid.uuid4())
|
357 |
+
|
358 |
+
# Create interview configuration
|
359 |
+
interview_config = {
|
360 |
+
"candidate_id": candidate_id,
|
361 |
+
"interview_id": interview_id,
|
362 |
+
"type": "ai_interview",
|
363 |
+
"interview_focus": data.get('interview_focus', 'technical'),
|
364 |
+
"depth_level": data.get('depth_level', 'mid'),
|
365 |
+
"focus_areas": data.get('focus_areas', []),
|
366 |
+
"duration": int(data.get('duration', 30)),
|
367 |
+
"special_instructions": data.get('special_instructions', ''),
|
368 |
+
"status": "scheduled",
|
369 |
+
"streamlit_url": f"http://localhost:8501?interview_id={interview_id}", # For local testing
|
370 |
+
"created_at": datetime.now(),
|
371 |
+
"expires_at": datetime.now() + timedelta(days=2)
|
372 |
+
}
|
373 |
+
|
374 |
+
# Store in interviews collection
|
375 |
+
db.interviews.insert_one(interview_config)
|
376 |
+
|
377 |
+
# Update candidate status
|
378 |
+
db.ai_processed_candidates.update_one(
|
379 |
+
{"_id": candidate_id},
|
380 |
+
{
|
381 |
+
"$set": {
|
382 |
+
"status": "ai_interview",
|
383 |
+
"last_updated": datetime.now()
|
384 |
+
}
|
385 |
+
}
|
386 |
+
)
|
387 |
+
|
388 |
+
# Send AI interview email
|
389 |
+
candidate_email = candidate['candidate_info']['email']
|
390 |
+
interview_url = interview_config['streamlit_url']
|
391 |
+
duration = interview_config['duration']
|
392 |
+
|
393 |
+
email_sent = email_service.send_ai_interview_email(candidate_email, interview_url, duration)
|
394 |
+
|
395 |
+
if email_sent:
|
396 |
+
return jsonify({
|
397 |
+
"success": True,
|
398 |
+
"message": "AI Interview scheduled successfully, email sent",
|
399 |
+
"interview_url": interview_url
|
400 |
+
})
|
401 |
+
else:
|
402 |
+
return jsonify({"success": False, "message": "AI Interview scheduled, but email sending failed"})
|
403 |
+
|
404 |
+
except Exception as e:
|
405 |
+
print(f"Error scheduling AI interview: {str(e)}")
|
406 |
+
return jsonify({"success": False, "message": str(e)}), 400
|
407 |
+
|
408 |
+
|
409 |
+
@app.route('/schedule_interview', methods=['POST'])
|
410 |
+
def schedule_interview():
|
411 |
+
try:
|
412 |
+
data = request.json
|
413 |
+
candidate_id = data['candidate_id']
|
414 |
+
scheduled_date = datetime.strptime(data['scheduled_date'], '%Y-%m-%dT%H:%M')
|
415 |
+
|
416 |
+
# Create interview document
|
417 |
+
interview = {
|
418 |
+
"candidate_id": candidate_id,
|
419 |
+
"round_type": data['round_type'],
|
420 |
+
"scheduled_date": scheduled_date,
|
421 |
+
"meeting_link": data['meeting_link'],
|
422 |
+
"status": "scheduled",
|
423 |
+
"created_at": datetime.now()
|
424 |
+
}
|
425 |
+
|
426 |
+
# Insert interview
|
427 |
+
db.interviews.insert_one(interview)
|
428 |
+
|
429 |
+
return jsonify({"message": "Interview scheduled successfully"})
|
430 |
+
except Exception as e:
|
431 |
+
return jsonify({"error": str(e)}), 400
|
432 |
+
|
433 |
+
|
434 |
+
@app.route('/view_resume/<resume_id>')
|
435 |
+
def view_resume(resume_id):
|
436 |
+
try:
|
437 |
+
# First get the resume document
|
438 |
+
resume_doc = db.raw_resumes.find_one({"_id": ObjectId(resume_id)})
|
439 |
+
if not resume_doc:
|
440 |
+
return "Resume not found", 404
|
441 |
+
|
442 |
+
# Get the file from GridFS
|
443 |
+
file_data = fs.get(resume_doc['resume']['file_id'])
|
444 |
+
|
445 |
+
return send_file(
|
446 |
+
BytesIO(file_data.read()),
|
447 |
+
mimetype='application/pdf',
|
448 |
+
as_attachment=True,
|
449 |
+
download_name=resume_doc['resume']['filename']
|
450 |
+
)
|
451 |
+
except Exception as e:
|
452 |
+
print(f"Resume Error: {e}")
|
453 |
+
return str(e), 400
|
454 |
+
|
455 |
+
|
456 |
+
@app.route('/favicon.ico')
|
457 |
+
def favicon():
|
458 |
+
return '', 204
|
459 |
+
mail_db = client['Resume']
|
460 |
+
|
461 |
+
|
462 |
+
@app.route('/mail-inbox')
|
463 |
+
def email_dashboard():
|
464 |
+
try:
|
465 |
+
# Get all emails from MongoDB
|
466 |
+
print(mail_db,"HI")
|
467 |
+
job_emails = list(mail_db.email_inbox.find({"mail_type": "Job application"}))
|
468 |
+
general_emails = list(mail_db.email_inbox.find({"mail_type": {"$ne": "Job application"}}))
|
469 |
+
|
470 |
+
# Convert ObjectId to string for JSON serialization
|
471 |
+
for email in job_emails + general_emails:
|
472 |
+
email['_id'] = str(email['_id'])
|
473 |
+
if 'resume_pdf' in email:
|
474 |
+
email['resume_pdf'] = str(email['resume_pdf'])
|
475 |
+
|
476 |
+
return render_template('email_dashboard.html',
|
477 |
+
job_emails=job_emails,
|
478 |
+
general_emails=general_emails)
|
479 |
+
except Exception as e:
|
480 |
+
print(f"Error loading email dashboard: {str(e)}")
|
481 |
+
return str(e), 500
|
482 |
+
|
483 |
+
|
484 |
+
@app.route('/api/get-emails')
|
485 |
+
def get_emails():
|
486 |
+
try:
|
487 |
+
job_emails = list(mail_db.emails.find({"mail_type": "Job application"}))
|
488 |
+
general_emails = list(mail_db.emails.find({"mail_type": {"$ne": "Job application"}}))
|
489 |
+
|
490 |
+
# Convert ObjectId to string
|
491 |
+
for email in job_emails + general_emails:
|
492 |
+
email['_id'] = str(email['_id'])
|
493 |
+
if 'resume_pdf' in email:
|
494 |
+
email['resume_pdf'] = str(email['resume_pdf'])
|
495 |
+
|
496 |
+
return jsonify({
|
497 |
+
"job_emails": job_emails,
|
498 |
+
"general_emails": general_emails
|
499 |
+
})
|
500 |
+
except Exception as e:
|
501 |
+
return jsonify({"error": str(e)}), 500
|
502 |
+
|
503 |
+
|
504 |
+
@app.route('/test_db')
|
505 |
+
def test_db():
|
506 |
+
try:
|
507 |
+
candidates = list(db.ai_processed_candidates.find())
|
508 |
+
return jsonify({
|
509 |
+
"status": "success",
|
510 |
+
"candidate_count": len(candidates),
|
511 |
+
"first_candidate_id": str(candidates[0]['_id']) if candidates else None
|
512 |
+
})
|
513 |
+
except Exception as e:
|
514 |
+
return jsonify({"error": str(e)}), 500
|
515 |
+
|
516 |
+
|
517 |
+
@app.route('/download_resumes', methods=['GET'])
|
518 |
+
def trigger_resume_download():
|
519 |
+
try:
|
520 |
+
downloaded_files = download_resume() # This should return a list of candidate names
|
521 |
+
return jsonify({
|
522 |
+
"status": "success",
|
523 |
+
"message": "Resumes downloaded successfully",
|
524 |
+
"candidates": downloaded_files # Ensure frontend uses 'candidates'
|
525 |
+
})
|
526 |
+
except Exception as e:
|
527 |
+
return jsonify({
|
528 |
+
"status": "error",
|
529 |
+
"message": str(e)
|
530 |
+
}), 500
|
531 |
+
|
532 |
+
|
533 |
+
|
534 |
+
if __name__ == '__main__':
|
535 |
+
app.run(debug=True, port=5000)
|
email_agent.py
ADDED
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from typing import Dict, Any
|
3 |
+
from datetime import datetime
|
4 |
+
import smtplib
|
5 |
+
from email.mime.text import MIMEText
|
6 |
+
from email.mime.multipart import MIMEMultipart
|
7 |
+
from pymongo import MongoClient
|
8 |
+
import certifi
|
9 |
+
|
10 |
+
# MongoDB connection
|
11 |
+
MONGODB_URI = "mongodb+srv://roshauninfant:[email protected]/?retryWrites=true&w=majority&appName=slackbot"
|
12 |
+
client = MongoClient(MONGODB_URI, tlsCAFile=certifi.where())
|
13 |
+
db = client['Resume']
|
14 |
+
|
15 |
+
|
16 |
+
class EmailAgent:
|
17 |
+
def __init__(self, gmail_user, gmail_app_password):
|
18 |
+
self.url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation?version=2023-05-29"
|
19 |
+
self.gmail_user = gmail_user
|
20 |
+
self.gmail_app_password = gmail_app_password
|
21 |
+
self.auth_token = self.get_ibm_auth_token()
|
22 |
+
|
23 |
+
def get_ibm_auth_token(self):
|
24 |
+
"""Fetch a valid IBM IAM access token."""
|
25 |
+
auth_url = "https://iam.cloud.ibm.com/identity/token"
|
26 |
+
auth_data = {
|
27 |
+
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
|
28 |
+
"apikey": "9FV7l0Jxqe7ceL09MeH_g9bYioIQuABXsr1j1VHbKOpr"
|
29 |
+
}
|
30 |
+
auth_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
31 |
+
|
32 |
+
auth_response = requests.post(auth_url, data=auth_data, headers=auth_headers)
|
33 |
+
auth_token = auth_response.json().get("access_token")
|
34 |
+
|
35 |
+
if not auth_token:
|
36 |
+
raise Exception("Failed to retrieve access token: " + str(auth_response.text))
|
37 |
+
|
38 |
+
return auth_token
|
39 |
+
|
40 |
+
def generate_email(self, candidate_info: Dict[str, Any], task: str) -> str:
|
41 |
+
email_prompt = f"""<|start_of_role|>system<|end_of_role|>
|
42 |
+
You are an HR Email Assistant responsible for drafting professional emails.
|
43 |
+
Your task is to generate a {task} email for the candidate.
|
44 |
+
|
45 |
+
Generate a professional, well-structured email that clearly communicates the {task} message.
|
46 |
+
Consider the specific context and requirements of a {task} email in the hiring process.
|
47 |
+
Maintain formal business email standards while keeping an appropriate tone for the {task}.
|
48 |
+
|
49 |
+
Use these candidate details:
|
50 |
+
- Name: {candidate_info['candidate_info']['name']}
|
51 |
+
- Position: Software Engineer
|
52 |
+
- Company: ABC Company Inc
|
53 |
+
- Task: {task}
|
54 |
+
|
55 |
+
Requirements for the email:
|
56 |
+
1. Clear and direct communication about the {task}
|
57 |
+
2. Professional and appropriate tone for the specific {task}
|
58 |
+
3. Any necessary next steps or actions required from the candidate
|
59 |
+
4. Professional closing with name Roshaun - HR - ABC Company Inc
|
60 |
+
|
61 |
+
Return only the email content without any explanations or meta text.
|
62 |
+
<|end_of_text|>
|
63 |
+
<|start_of_role|>assistant<|end_of_role|>"""
|
64 |
+
|
65 |
+
body = {
|
66 |
+
"input": email_prompt,
|
67 |
+
"parameters": {
|
68 |
+
"decoding_method": "greedy",
|
69 |
+
"max_new_tokens": 900,
|
70 |
+
"min_new_tokens": 0,
|
71 |
+
"repetition_penalty": 1
|
72 |
+
},
|
73 |
+
"model_id": "ibm/granite-3-8b-instruct",
|
74 |
+
"project_id": "4aa39c25-19d7-48c1-9cf6-e31b5c223a1f"
|
75 |
+
}
|
76 |
+
|
77 |
+
headers = {
|
78 |
+
"Accept": "application/json",
|
79 |
+
"Content-Type": "application/json",
|
80 |
+
"Authorization": f"Bearer {self.auth_token}"
|
81 |
+
}
|
82 |
+
|
83 |
+
try:
|
84 |
+
response = requests.post(self.url, headers=headers, json=body)
|
85 |
+
|
86 |
+
if response.status_code != 200:
|
87 |
+
raise Exception(f"API Error: {response.text}")
|
88 |
+
|
89 |
+
email_content = response.json()['results'][0]['generated_text']
|
90 |
+
return email_content
|
91 |
+
|
92 |
+
except Exception as e:
|
93 |
+
print(f"Error generating email: {str(e)}")
|
94 |
+
return None
|
95 |
+
|
96 |
+
def send_email(self, to_email: str, subject: str, body: str) -> bool:
|
97 |
+
try:
|
98 |
+
msg = MIMEMultipart()
|
99 |
+
msg['From'] = self.gmail_user
|
100 |
+
msg['To'] = to_email
|
101 |
+
msg['Subject'] = subject
|
102 |
+
|
103 |
+
msg.attach(MIMEText(body, 'plain'))
|
104 |
+
|
105 |
+
server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
|
106 |
+
server.login(self.gmail_user, self.gmail_app_password)
|
107 |
+
server.send_message(msg)
|
108 |
+
server.quit()
|
109 |
+
|
110 |
+
return True
|
111 |
+
except Exception as e:
|
112 |
+
print(f"Error sending email: {str(e)}")
|
113 |
+
return False
|
114 |
+
|
115 |
+
|
116 |
+
def send_emails(job_id: str, task: str, gmail_user: str, gmail_app_password: str, candidate_list: list = None) -> Dict:
|
117 |
+
try:
|
118 |
+
# Get candidates based on parameter or status
|
119 |
+
if candidate_list:
|
120 |
+
# If candidate list is provided, get those specific candidates
|
121 |
+
candidates = list(db.ai_processed_candidates.find({
|
122 |
+
"job_id": job_id,
|
123 |
+
"_id": {"$in": candidate_list}
|
124 |
+
}))
|
125 |
+
else:
|
126 |
+
# If no candidate list, get all candidates with matching status
|
127 |
+
candidates = list(db.ai_processed_candidates.find({
|
128 |
+
"job_id": job_id,
|
129 |
+
"status": task
|
130 |
+
}))
|
131 |
+
|
132 |
+
print(f"Found {len(candidates)} candidates for {task}")
|
133 |
+
|
134 |
+
if not candidates:
|
135 |
+
return {
|
136 |
+
"success": False,
|
137 |
+
"error": f"No candidates found for {task} emails"
|
138 |
+
}
|
139 |
+
|
140 |
+
email_agent = EmailAgent(gmail_user, gmail_app_password)
|
141 |
+
emails_sent = 0
|
142 |
+
failed_emails = []
|
143 |
+
skipped_candidates = 0
|
144 |
+
|
145 |
+
for candidate in candidates:
|
146 |
+
# Skip if candidate already received this type of email
|
147 |
+
if db.email_communications.find_one({
|
148 |
+
"candidate_id": candidate['_id'],
|
149 |
+
"email_type": task,
|
150 |
+
"status": "sent"
|
151 |
+
}):
|
152 |
+
skipped_candidates += 1
|
153 |
+
continue
|
154 |
+
|
155 |
+
print(f"Processing candidate: {candidate['candidate_info']['email']}")
|
156 |
+
|
157 |
+
email_content = email_agent.generate_email(candidate, task)
|
158 |
+
if not email_content:
|
159 |
+
print(f"Email content generation failed for {candidate['candidate_info']['email']}")
|
160 |
+
continue
|
161 |
+
|
162 |
+
success = email_agent.send_email(
|
163 |
+
to_email=candidate['candidate_info']['email'],
|
164 |
+
subject=f"ABC Company Inc | {task.replace('_', ' ').title()} Update",
|
165 |
+
body=email_content
|
166 |
+
)
|
167 |
+
|
168 |
+
print(f"Email sent: {'Success' if success else 'Failed'} for {candidate['candidate_info']['email']}")
|
169 |
+
|
170 |
+
# Store email record in MongoDB
|
171 |
+
email_record = {
|
172 |
+
"candidate_id": candidate['_id'],
|
173 |
+
"job_id": job_id,
|
174 |
+
"email_type": task,
|
175 |
+
"email_content": email_content,
|
176 |
+
"sent_date": datetime.now(),
|
177 |
+
"status": "sent" if success else "failed"
|
178 |
+
}
|
179 |
+
db.email_communications.insert_one(email_record)
|
180 |
+
|
181 |
+
if success:
|
182 |
+
emails_sent += 1
|
183 |
+
else:
|
184 |
+
failed_emails.append(candidate['candidate_info']['email'])
|
185 |
+
|
186 |
+
return {
|
187 |
+
"success": True,
|
188 |
+
"emails_sent": emails_sent,
|
189 |
+
"failed_emails": failed_emails,
|
190 |
+
"skipped_candidates": skipped_candidates,
|
191 |
+
"total_candidates": len(candidates)
|
192 |
+
}
|
193 |
+
|
194 |
+
except Exception as e:
|
195 |
+
print(f"Error sending emails: {str(e)}")
|
196 |
+
return {"success": False, "error": str(e)}
|
197 |
+
|
198 |
+
|
email_service.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import smtplib
|
2 |
+
from email.mime.text import MIMEText
|
3 |
+
from email.mime.multipart import MIMEMultipart
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
|
7 |
+
class EmailService:
|
8 |
+
def __init__(self, gmail_user, gmail_app_password):
|
9 |
+
self.gmail_user = gmail_user
|
10 |
+
self.gmail_app_password = gmail_app_password
|
11 |
+
|
12 |
+
def send_ai_interview_email(self, candidate_email, interview_url, duration):
|
13 |
+
subject = "Your AI Interview Link"
|
14 |
+
|
15 |
+
body = f"""
|
16 |
+
Dear Candidate,
|
17 |
+
|
18 |
+
Thank you for your application. You have been selected for an AI-based interview assessment.
|
19 |
+
|
20 |
+
Please click the link below to start your interview:
|
21 |
+
{interview_url}
|
22 |
+
|
23 |
+
Important Information:
|
24 |
+
- The interview will take approximately {duration} minutes
|
25 |
+
- Please ensure a stable internet connection
|
26 |
+
- Have your camera and microphone ready if required
|
27 |
+
- The link will be valid for 48 hours
|
28 |
+
|
29 |
+
Best of luck!
|
30 |
+
|
31 |
+
Best regards,
|
32 |
+
HR Team
|
33 |
+
"""
|
34 |
+
|
35 |
+
try:
|
36 |
+
msg = MIMEMultipart()
|
37 |
+
msg['From'] = self.gmail_user
|
38 |
+
msg['To'] = candidate_email
|
39 |
+
msg['Subject'] = subject
|
40 |
+
|
41 |
+
msg.attach(MIMEText(body, 'plain'))
|
42 |
+
|
43 |
+
server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
|
44 |
+
server.login(self.gmail_user, self.gmail_app_password)
|
45 |
+
server.send_message(msg)
|
46 |
+
server.quit()
|
47 |
+
|
48 |
+
return True
|
49 |
+
except Exception as e:
|
50 |
+
print(f"Error sending email: {str(e)}")
|
51 |
+
return False
|
other_functions.py
ADDED
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import os
|
3 |
+
import gridfs
|
4 |
+
from pymongo import MongoClient
|
5 |
+
|
6 |
+
def download_resume():
|
7 |
+
# MongoDB connection
|
8 |
+
uri = "mongodb+srv://roshauninfant:[email protected]/?retryWrites=true&w=majority&appName=slackbot&tlsAllowInvalidCertificates=true"
|
9 |
+
client = MongoClient(uri)
|
10 |
+
db = client["Resume"]
|
11 |
+
fs = gridfs.GridFS(db)
|
12 |
+
|
13 |
+
collection = db["raw_resumes"]
|
14 |
+
|
15 |
+
# Create a directory for saving resumes
|
16 |
+
download_dir = "downloaded_resumes"
|
17 |
+
os.makedirs(download_dir, exist_ok=True)
|
18 |
+
|
19 |
+
downloaded_files = [] # Store filenames for UI
|
20 |
+
|
21 |
+
# Fetch and download files
|
22 |
+
for doc in collection.find():
|
23 |
+
file_id = doc["resume"]["file_id"]
|
24 |
+
filename = doc["resume"]["filename"]
|
25 |
+
|
26 |
+
# Retrieve the file from GridFS
|
27 |
+
file_data = fs.get(file_id)
|
28 |
+
|
29 |
+
# Save the file locally
|
30 |
+
file_path = os.path.join(download_dir, filename)
|
31 |
+
with open(file_path, "wb") as f:
|
32 |
+
f.write(file_data.read())
|
33 |
+
|
34 |
+
downloaded_files.append(filename)
|
35 |
+
|
36 |
+
print("All resumes downloaded successfully!")
|
37 |
+
return downloaded_files # Return filenames for UI
|
38 |
+
|
39 |
+
def screenresume():
|
40 |
+
print
|
41 |
+
# IBM Granite AI Authentication
|
42 |
+
def get_ibm_auth_token(api_key):
|
43 |
+
auth_url = "https://iam.cloud.ibm.com/identity/token"
|
44 |
+
auth_data = {
|
45 |
+
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
|
46 |
+
"apikey": api_key
|
47 |
+
}
|
48 |
+
auth_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
49 |
+
|
50 |
+
response = requests.post(auth_url, data=auth_data, headers=auth_headers)
|
51 |
+
return response.json().get("access_token")
|
52 |
+
|
53 |
+
# Extract text from Word documents
|
54 |
+
def extract_text_from_docx(file_path):
|
55 |
+
doc = Document(file_path)
|
56 |
+
return "\n".join([para.text for para in doc.paragraphs])
|
57 |
+
|
58 |
+
# Construct a powerful AI prompt
|
59 |
+
def generate_prompt(resume_text, job_id, _id):
|
60 |
+
return f"""
|
61 |
+
<|start_of_role|>system<|end_of_role|>You are a highly intelligent AI model specialized in resume screening for hiring purposes. You analyze candidate resumes against job descriptions, extracting key information and evaluating how well they match.
|
62 |
+
|
63 |
+
JOB Description: Software engineer/developer
|
64 |
+
|
65 |
+
Below is a candidate's resume. Extract structured information such as name, email, phone, experience, current company, and 5 key skills. Then, evaluate the candidate based on the job description.
|
66 |
+
|
67 |
+
**Confidence Score Guidelines:**
|
68 |
+
- **90 - 100 (Highly Recommended):** Strong alignment with job requirements, possesses most required skills and relevant experience, ideal for shortlisting.
|
69 |
+
- **75 - 89 (Recommended):** Good match with the job role, some missing or secondary skills but still a strong candidate.
|
70 |
+
- **50 - 74 (Considered):** Partial match, lacks some key skills but has relevant experience.
|
71 |
+
- **Below 50 (Not Recommended):** Does not meet core job requirements, significant skill gaps.
|
72 |
+
|
73 |
+
Resume:
|
74 |
+
{resume_text}
|
75 |
+
|
76 |
+
Example output format:
|
77 |
+
{{
|
78 |
+
"_id": "{_id}",
|
79 |
+
"job_id": "{job_id}",
|
80 |
+
"resume_id": "{_id}",
|
81 |
+
"candidate_info": {{
|
82 |
+
"name": "John Doe",
|
83 |
+
"email": "[email protected]",
|
84 |
+
"phone": "+1234567890",
|
85 |
+
"experience": "5 years",
|
86 |
+
"current_company": "Tech Corp"
|
87 |
+
}},
|
88 |
+
"ai_evaluation": {{
|
89 |
+
"confidence_score": 85,
|
90 |
+
"matching_skills": ["Python", "MongoDB", "AWS"], # Return only top 8 skills
|
91 |
+
"missing_skills": ["GraphQL"],
|
92 |
+
"shortlist_reasons": [
|
93 |
+
"Strong technical match",
|
94 |
+
"Relevant experience"
|
95 |
+
] # Return only top 4 reasons
|
96 |
+
}},
|
97 |
+
"status": "shortlisted", # 'shortlisted' if confidence_score >= 65 else 'rejected',no other words
|
98 |
+
"created_at": "{datetime.now().isoformat()}"
|
99 |
+
}}
|
100 |
+
<|end_of_role|>"""
|
101 |
+
|
102 |
+
|
103 |
+
# Call IBM Granite AI
|
104 |
+
def analyze_resume_with_ai(auth_token, prompt):
|
105 |
+
url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation?version=2023-05-29"
|
106 |
+
headers = {
|
107 |
+
"Accept": "application/json",
|
108 |
+
"Content-Type": "application/json",
|
109 |
+
"Authorization": f"Bearer {auth_token}"
|
110 |
+
}
|
111 |
+
body = {
|
112 |
+
"input": prompt,
|
113 |
+
"parameters": {"decoding_method": "greedy", "max_new_tokens": 900, "repetition_penalty": 1},
|
114 |
+
"model_id": "ibm/granite-3-8b-instruct",
|
115 |
+
"project_id": "4aa39c25-19d7-48c1-9cf6-e31b5c223a1f"
|
116 |
+
}
|
117 |
+
|
118 |
+
response = requests.post(url, headers=headers, json=body)
|
119 |
+
return response.json().get("results", [{}])[0].get("generated_text", "{}")
|
120 |
+
|
121 |
+
# MongoDB Connection (for later use if needed)
|
122 |
+
client = MongoClient("mongodb+srv://roshauninfant:[email protected]/?retryWrites=true&w=majority")
|
123 |
+
db = client["Resume"]
|
124 |
+
collection = db["raw_resumes"] # Assuming you have the collection with raw resumes
|
125 |
+
|
126 |
+
# Process resumes
|
127 |
+
resume_folder =download_resume()
|
128 |
+
resume_folder='downloaded_resumes'
|
129 |
+
api_key = "9FV7l0Jxqe7ceL09MeH_g9bYioIQuABXsr1j1VHbKOpr" # Replace with your actual API key
|
130 |
+
auth_token = get_ibm_auth_token(api_key)
|
131 |
+
|
132 |
+
if not auth_token:
|
133 |
+
raise Exception("Failed to retrieve IBM auth token")
|
134 |
+
|
135 |
+
job_id = "SDE001"
|
136 |
+
output = []
|
137 |
+
print('hi',os.listdir(resume_folder))
|
138 |
+
for filename in os.listdir(resume_folder):
|
139 |
+
if filename.endswith(".docx"):
|
140 |
+
file_path = os.path.join(resume_folder, filename)
|
141 |
+
|
142 |
+
# Extract text from resume
|
143 |
+
resume_text = extract_text_from_docx(file_path)
|
144 |
+
|
145 |
+
# Find existing ObjectId based on the resume filename or other criteria
|
146 |
+
existing_document = collection.find_one({"resume.filename": filename})
|
147 |
+
|
148 |
+
if not existing_document:
|
149 |
+
print(f"Error: No document found for {filename}")
|
150 |
+
continue
|
151 |
+
|
152 |
+
_id = str(existing_document['_id']) # Use the existing _id from MongoDB
|
153 |
+
|
154 |
+
# Generate AI prompt with existing _id
|
155 |
+
prompt = generate_prompt(resume_text, job_id, _id)
|
156 |
+
|
157 |
+
# Get AI analysis
|
158 |
+
ai_response = analyze_resume_with_ai(auth_token, prompt)
|
159 |
+
print(ai_response)
|
160 |
+
try:
|
161 |
+
ai_data = eval(ai_response) # Convert AI output to dict (use eval safely or json.loads if needed)
|
162 |
+
output.append(ai_data)
|
163 |
+
|
164 |
+
# Update the existing document in MongoDB with the AI evaluation results
|
165 |
+
# print(_id,filename)
|
166 |
+
# print(f"Processed and saved: {filename}")
|
167 |
+
except Exception as e:
|
168 |
+
print(f"Error processing {filename}: {e}")
|
169 |
+
|
170 |
+
collection = db["ai_processed_candidates"] # New collection for storing JSON data
|
171 |
+
|
172 |
+
# Delete existing data in the collection
|
173 |
+
collection.delete_many({})
|
174 |
+
|
175 |
+
|
176 |
+
# Insert JSON data into MongoDB
|
177 |
+
if isinstance(output, list):
|
178 |
+
collection.insert_many(output)
|
179 |
+
else:
|
180 |
+
collection.insert_one(output)
|
181 |
+
collection.update_many({"status": "considered"}, {"$set": {"status": "rejected"}})
|
182 |
+
|
183 |
+
|
184 |
+
print("output.json successfully saved to MongoDB!")
|
185 |
+
|
186 |
+
|
187 |
+
|
188 |
+
import requests
|
189 |
+
def generatecoding():
|
190 |
+
# IBM Granite AI Authentication
|
191 |
+
def get_ibm_auth_token(api_key):
|
192 |
+
auth_url = "https://iam.cloud.ibm.com/identity/token"
|
193 |
+
auth_data = {
|
194 |
+
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
|
195 |
+
"apikey": api_key
|
196 |
+
}
|
197 |
+
auth_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
198 |
+
|
199 |
+
response = requests.post(auth_url, data=auth_data, headers=auth_headers)
|
200 |
+
return response.json().get("access_token")
|
201 |
+
|
202 |
+
|
203 |
+
|
204 |
+
# Construct a powerful AI prompt
|
205 |
+
def generate_prompt(job_title ):
|
206 |
+
return f"""
|
207 |
+
<|start_of_role|>system<|end_of_role|>You are an advanced AI specializing in technical hiring and coding assessments. Your task is to generate two high-quality coding question for evaluating candidates applying for the role of {job_title}.
|
208 |
+
|
209 |
+
The problem should be designed to assess proficiency in {job_title} and should include:
|
210 |
+
|
211 |
+
1. **Problem Description**: A clear and concise problem statement that describes the coding task.
|
212 |
+
2. **Input Format**: A detailed explanation of the expected input.
|
213 |
+
3. **Output Format**: A description of the expected output.
|
214 |
+
4. **Constraints**: Reasonable constraints on input values to ensure efficiency.
|
215 |
+
5. **Test Cases**: At most 3 sample test cases with explanations.
|
216 |
+
|
217 |
+
Ensure that the problem is relevant to real-world scenarios particularly Leetcode Medium DSA problem for a {job_title} and effectively measures problem-solving skills.
|
218 |
+
|
219 |
+
<|end_of_role|>
|
220 |
+
"""
|
221 |
+
|
222 |
+
|
223 |
+
|
224 |
+
# Call IBM Granite AI
|
225 |
+
def analyze_resume_with_ai(auth_token, prompt):
|
226 |
+
url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation?version=2023-05-29"
|
227 |
+
headers = {
|
228 |
+
"Accept": "application/json",
|
229 |
+
"Content-Type": "application/json",
|
230 |
+
"Authorization": f"Bearer {auth_token}"
|
231 |
+
}
|
232 |
+
body = {
|
233 |
+
"input": prompt,
|
234 |
+
"parameters": {"decoding_method": "greedy", "max_new_tokens": 900, "repetition_penalty": 1},
|
235 |
+
"model_id": "ibm/granite-3-8b-instruct",
|
236 |
+
"project_id": "4aa39c25-19d7-48c1-9cf6-e31b5c223a1f"
|
237 |
+
}
|
238 |
+
|
239 |
+
response = requests.post(url, headers=headers, json=body)
|
240 |
+
return response.json().get("results", [{}])[0].get("generated_text", "{}")
|
241 |
+
|
242 |
+
|
243 |
+
api_key = "9FV7l0Jxqe7ceL09MeH_g9bYioIQuABXsr1j1VHbKOpr" # Replace with your actual API key
|
244 |
+
auth_token = get_ibm_auth_token(api_key)
|
245 |
+
|
246 |
+
|
247 |
+
prompt = generate_prompt("Software developer")
|
248 |
+
|
249 |
+
# Get AI analysis
|
250 |
+
ai_response = analyze_resume_with_ai(auth_token, prompt)
|
251 |
+
print(ai_response)
|
252 |
+
return ai_response
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask
|
2 |
+
pymongo
|
3 |
+
certifi
|
4 |
+
gridfs
|
5 |
+
requests
|
templates/dashboard.html
ADDED
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<title>HR Dashboard | Active Jobs</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet" />
|
8 |
+
</head>
|
9 |
+
<style>
|
10 |
+
.checkbox-checked {
|
11 |
+
background-color: #10B981;
|
12 |
+
border-color: #10B981;
|
13 |
+
transform: scale(1.1);
|
14 |
+
}
|
15 |
+
|
16 |
+
.checkbox-tick {
|
17 |
+
color: white;
|
18 |
+
display: none;
|
19 |
+
transform: scale(0);
|
20 |
+
transition: transform 0.2s ease-in-out;
|
21 |
+
}
|
22 |
+
|
23 |
+
.checkbox-checked .checkbox-tick {
|
24 |
+
display: block;
|
25 |
+
transform: scale(1);
|
26 |
+
}
|
27 |
+
|
28 |
+
.hover-trigger .hover-target {
|
29 |
+
display: none;
|
30 |
+
}
|
31 |
+
|
32 |
+
.hover-trigger:hover .hover-target {
|
33 |
+
display: flex;
|
34 |
+
}
|
35 |
+
</style>
|
36 |
+
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
|
37 |
+
<!-- Enhanced Header -->
|
38 |
+
<nav class="bg-white shadow-sm border-b border-gray-200">
|
39 |
+
<div class="max-w-7xl mx-auto px-6 py-4">
|
40 |
+
<div class="flex justify-between items-center">
|
41 |
+
<div class="flex items-center space-x-4">
|
42 |
+
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
43 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
44 |
+
</svg>
|
45 |
+
<div>
|
46 |
+
<h1 class="text-2xl font-bold text-gray-900">HR Dashboard</h1>
|
47 |
+
<p class="text-sm text-gray-500">Talent Acquisition Management</p>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
<div class="flex items-center space-x-4">
|
51 |
+
<span id="currentDateTime" class="text-sm text-gray-500"></span>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</nav>
|
56 |
+
|
57 |
+
<main class="max-w-7xl mx-auto px-6 py-8">
|
58 |
+
<!-- Dashboard Overview Cards -->
|
59 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
60 |
+
<div class="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
|
61 |
+
<div class="flex items-center">
|
62 |
+
<div class="bg-blue-50 rounded-full p-3">
|
63 |
+
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
64 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
65 |
+
</svg>
|
66 |
+
</div>
|
67 |
+
<div class="ml-4">
|
68 |
+
<h3 class="text-lg font-semibold text-gray-900">Active Jobs</h3>
|
69 |
+
<p class="text-2xl font-bold text-blue-600">{{ job_postings|length }}</p>
|
70 |
+
</div>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<div class="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
|
75 |
+
<div class="flex items-center">
|
76 |
+
<div class="bg-green-50 rounded-full p-3">
|
77 |
+
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
78 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
79 |
+
</svg>
|
80 |
+
</div>
|
81 |
+
<div class="ml-4">
|
82 |
+
<h3 class="text-lg font-semibold text-gray-900">Total Candidates</h3>
|
83 |
+
<p class="text-2xl font-bold text-green-600">{{ job_postings|sum(attribute='total_candidates') }}</p>
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
</div>
|
87 |
+
|
88 |
+
<div class="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
|
89 |
+
<div class="flex items-center">
|
90 |
+
<div class="bg-purple-50 rounded-full p-3">
|
91 |
+
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
92 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/>
|
93 |
+
</svg>
|
94 |
+
</div>
|
95 |
+
<div class="ml-4">
|
96 |
+
<h3 class="text-lg font-semibold text-gray-900">Shortlisted</h3>
|
97 |
+
<p class="text-2xl font-bold text-purple-600">{{ job_postings|sum(attribute='shortlisted_count') }}</p>
|
98 |
+
</div>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
|
103 |
+
<!-- Job Listings Table -->
|
104 |
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
105 |
+
<div class="px-6 py-4 border-b border-gray-200">
|
106 |
+
<div class="flex justify-between items-center">
|
107 |
+
<h2 class="text-xl font-semibold text-gray-900">Active Job Postings</h2>
|
108 |
+
<div class="flex space-x-2">
|
109 |
+
<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
|
110 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
111 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
112 |
+
</svg>
|
113 |
+
</button>
|
114 |
+
<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
|
115 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
116 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
117 |
+
</svg>
|
118 |
+
</button>
|
119 |
+
</div>
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
|
123 |
+
<div class="overflow-x-auto">
|
124 |
+
<table class="min-w-full divide-y divide-gray-200">
|
125 |
+
<thead class="bg-gray-50">
|
126 |
+
<tr>
|
127 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job ID</th>
|
128 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Position</th>
|
129 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Department</th>
|
130 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
131 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Applications</th>
|
132 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
133 |
+
</tr>
|
134 |
+
</thead>
|
135 |
+
<tbody class="bg-white divide-y divide-gray-200">
|
136 |
+
{% for job in job_postings %}
|
137 |
+
<tr class="hover:bg-gray-50 transition-colors duration-200">
|
138 |
+
<td class="px-6 py-4">
|
139 |
+
<div class="text-sm font-medium text-gray-900">{{ job.job_id }}</div>
|
140 |
+
<div class="text-xs text-gray-500">Created on {{ job.created_at|default('N/A') }}</div>
|
141 |
+
</td>
|
142 |
+
<td class="px-6 py-4">
|
143 |
+
<div class="text-sm text-gray-900 font-medium">{{ job.title }}</div>
|
144 |
+
</td>
|
145 |
+
<td class="px-6 py-4">
|
146 |
+
<div class="text-sm text-gray-900">{{ job.department }}</div>
|
147 |
+
</td>
|
148 |
+
<td class="px-6 py-4">
|
149 |
+
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full
|
150 |
+
{% if job.status == 'active' %}
|
151 |
+
bg-green-100 text-green-800
|
152 |
+
{% else %}
|
153 |
+
bg-red-100 text-red-800
|
154 |
+
{% endif %}">
|
155 |
+
{{ job.status|title }}
|
156 |
+
</span>
|
157 |
+
</td>
|
158 |
+
<td class="px-6 py-4">
|
159 |
+
<div class="flex flex-col space-y-1">
|
160 |
+
<div class="text-sm text-gray-900">
|
161 |
+
<span class="font-medium">{{ job.total_candidates }}</span> total applications
|
162 |
+
</div>
|
163 |
+
<div class="text-sm text-green-600">
|
164 |
+
<span class="font-medium">{{ job.shortlisted_count }}</span> shortlisted
|
165 |
+
</div>
|
166 |
+
<div class="w-full bg-gray-200 rounded-full h-1.5 mt-1">
|
167 |
+
|
168 |
+
</div>
|
169 |
+
|
170 |
+
</div>
|
171 |
+
</div>
|
172 |
+
</td>
|
173 |
+
<td class="px-6 py-4">
|
174 |
+
{% if job.status == 'active' %}
|
175 |
+
<div class="flex flex-col space-y-2">
|
176 |
+
<div class="flex space-x-2">
|
177 |
+
<a href="/job/{{ job.job_id }}"
|
178 |
+
class="inline-flex items-center px-3 py-1.5 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors duration-200">
|
179 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
180 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
181 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7"/>
|
182 |
+
</svg>
|
183 |
+
View Details
|
184 |
+
</a>
|
185 |
+
<button onclick="closeJob('{{ job.job_id }}')"
|
186 |
+
class="inline-flex items-center px-3 py-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors duration-200">
|
187 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
188 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
189 |
+
</svg>
|
190 |
+
Close Job
|
191 |
+
</button>
|
192 |
+
</div>
|
193 |
+
<button onclick="download_resume('{{ job.job_id }}')"
|
194 |
+
class="inline-flex items-center px-3 py-1.5 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition-colors duration-200">
|
195 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
196 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
197 |
+
</svg>
|
198 |
+
Screen Resumes
|
199 |
+
</button>
|
200 |
+
|
201 |
+
<!-- Status and Progress Section -->
|
202 |
+
<div class="mt-2 space-y-2">
|
203 |
+
<div id="download-status-{{ job.job_id }}"
|
204 |
+
class="text-sm font-medium"></div>
|
205 |
+
<div class="overflow-hidden">
|
206 |
+
<ul id="candidate-list-{{ job.job_id }}"
|
207 |
+
class="space-y-2 transition-all duration-200"></ul>
|
208 |
+
</div>
|
209 |
+
</div>
|
210 |
+
</div>
|
211 |
+
{% endif %}
|
212 |
+
</td>
|
213 |
+
</tr>
|
214 |
+
{% endfor %}
|
215 |
+
</tbody>
|
216 |
+
</table>
|
217 |
+
</div>
|
218 |
+
</div>
|
219 |
+
</main>
|
220 |
+
|
221 |
+
<script>
|
222 |
+
function closeJob(jobId) {
|
223 |
+
if (confirm("Are you sure you want to close this job posting? This will delete all associated resumes and candidate data.")) {
|
224 |
+
fetch("/close_job", {
|
225 |
+
method: "POST",
|
226 |
+
headers: { "Content-Type": "application/json" },
|
227 |
+
body: JSON.stringify({ job_id: jobId }),
|
228 |
+
})
|
229 |
+
.then(response => response.json())
|
230 |
+
.then(data => {
|
231 |
+
if (data.error) {
|
232 |
+
showNotification("Error: " + data.error, "error");
|
233 |
+
} else {
|
234 |
+
showNotification("Job closed successfully!", "success");
|
235 |
+
setTimeout(() => location.reload(), 1500);
|
236 |
+
}
|
237 |
+
})
|
238 |
+
.catch(error => {
|
239 |
+
showNotification("Error processing request.", "error");
|
240 |
+
console.error("Error:", error);
|
241 |
+
});
|
242 |
+
}
|
243 |
+
}
|
244 |
+
|
245 |
+
function download_resume(jobId) {
|
246 |
+
const statusDiv = document.getElementById("download-status-" + jobId);
|
247 |
+
const candidateList = document.getElementById("candidate-list-" + jobId);
|
248 |
+
|
249 |
+
statusDiv.innerHTML = `
|
250 |
+
<div class="flex items-center text-blue-600">
|
251 |
+
<svg class="animate-spin h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24">
|
252 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
253 |
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
254 |
+
</svg>
|
255 |
+
Downloading resumes...
|
256 |
+
</div>`;
|
257 |
+
candidateList.innerHTML = "";
|
258 |
+
|
259 |
+
fetch("/download_resumes", {
|
260 |
+
method: "GET",
|
261 |
+
})
|
262 |
+
.then(response => response.json())
|
263 |
+
.then(data => {
|
264 |
+
if (data.status === "error") {
|
265 |
+
statusDiv.innerHTML = `
|
266 |
+
<div class="flex items-center text-red-600">
|
267 |
+
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
268 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
269 |
+
</svg>
|
270 |
+
Error downloading resumes.
|
271 |
+
</div>`;
|
272 |
+
} else {
|
273 |
+
statusDiv.innerHTML = `
|
274 |
+
<div class="flex items-center text-green-600">
|
275 |
+
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
276 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
277 |
+
</svg>
|
278 |
+
Resumes downloaded successfully!
|
279 |
+
</div>`;
|
280 |
+
|
281 |
+
// Enhanced candidate list with animation
|
282 |
+
data.candidates.forEach((name, index) => {
|
283 |
+
const listItem = document.createElement("li");
|
284 |
+
listItem.className = "flex items-center justify-between p-2 bg-gray-50 rounded-lg opacity-0 transform translate-y-2";
|
285 |
+
listItem.style.transition = "all 0.3s ease";
|
286 |
+
listItem.style.transitionDelay = `${index * 0.1}s`;
|
287 |
+
|
288 |
+
const nameSpan = document.createElement("span");
|
289 |
+
nameSpan.className = "text-sm text-gray-700";
|
290 |
+
nameSpan.textContent = name;
|
291 |
+
|
292 |
+
const checkboxContainer = document.createElement("div");
|
293 |
+
checkboxContainer.className = "h-5 w-5 border rounded flex items-center justify-center transition-all duration-200";
|
294 |
+
checkboxContainer.id = `checkbox-${jobId}-${index}`;
|
295 |
+
|
296 |
+
const checkmark = document.createElement("span");
|
297 |
+
checkmark.innerHTML = "✓";
|
298 |
+
checkmark.className = "checkbox-tick transition-transform duration-200";
|
299 |
+
|
300 |
+
checkboxContainer.appendChild(checkmark);
|
301 |
+
listItem.appendChild(nameSpan);
|
302 |
+
listItem.appendChild(checkboxContainer);
|
303 |
+
candidateList.appendChild(listItem);
|
304 |
+
|
305 |
+
// Trigger animation after a brief delay
|
306 |
+
setTimeout(() => {
|
307 |
+
listItem.classList.remove("opacity-0", "translate-y-2");
|
308 |
+
}, 50);
|
309 |
+
});
|
310 |
+
|
311 |
+
animateCheckboxes(jobId, data.candidates.length);
|
312 |
+
}
|
313 |
+
})
|
314 |
+
.catch(error => {
|
315 |
+
statusDiv.innerHTML = `
|
316 |
+
<div class="flex items-center text-red-600">
|
317 |
+
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
318 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
319 |
+
</svg>
|
320 |
+
Error processing request.
|
321 |
+
</div>`;
|
322 |
+
console.error("Error:", error);
|
323 |
+
});
|
324 |
+
}
|
325 |
+
|
326 |
+
function animateCheckboxes(jobId, totalBoxes) {
|
327 |
+
let currentBox = 0;
|
328 |
+
|
329 |
+
function checkNext() {
|
330 |
+
if (currentBox < totalBoxes) {
|
331 |
+
const checkbox = document.getElementById(`checkbox-${jobId}-${currentBox}`);
|
332 |
+
if (checkbox) {
|
333 |
+
checkbox.classList.add("checkbox-checked");
|
334 |
+
currentBox++;
|
335 |
+
setTimeout(checkNext, 2500);
|
336 |
+
}
|
337 |
+
}
|
338 |
+
}
|
339 |
+
|
340 |
+
setTimeout(checkNext, 500);
|
341 |
+
}
|
342 |
+
|
343 |
+
function showNotification(message, type) {
|
344 |
+
const notification = document.createElement("div");
|
345 |
+
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-y-[-100%] ${
|
346 |
+
type === "success" ? "bg-green-500" : "bg-red-500"
|
347 |
+
} text-white`;
|
348 |
+
notification.textContent = message;
|
349 |
+
|
350 |
+
document.body.appendChild(notification);
|
351 |
+
setTimeout(() => notification.style.transform = "translateY(0)", 100);
|
352 |
+
setTimeout(() => {
|
353 |
+
notification.style.transform = "translateY(-100%)";
|
354 |
+
setTimeout(() => notification.remove(), 300);
|
355 |
+
}, 3000);
|
356 |
+
}
|
357 |
+
function updateDateTime() {
|
358 |
+
const now = new Date();
|
359 |
+
const options = {
|
360 |
+
year: 'numeric',
|
361 |
+
month: 'long',
|
362 |
+
day: 'numeric',
|
363 |
+
hour: '2-digit',
|
364 |
+
minute: '2-digit'
|
365 |
+
};
|
366 |
+
document.getElementById('currentDateTime').textContent =
|
367 |
+
'Last updated: ' + now.toLocaleDateString('en-US', options);
|
368 |
+
}
|
369 |
+
|
370 |
+
// Update initially and then every minute
|
371 |
+
updateDateTime();
|
372 |
+
setInterval(updateDateTime, 60000);
|
373 |
+
</script>
|
374 |
+
</body>
|
375 |
+
</html>
|
376 |
+
|
377 |
+
|
templates/email_dashboard.html
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Email Dashboard</title>
|
7 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
8 |
+
</head>
|
9 |
+
<body class="bg-gray-100 p-6">
|
10 |
+
<div class="max-w-7xl mx-auto">
|
11 |
+
<h1 class="text-3xl font-bold mb-8 text-gray-800">Email Dashboard</h1>
|
12 |
+
|
13 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
14 |
+
<!-- Job Applications Container -->
|
15 |
+
<div class="bg-white rounded-lg shadow-lg p-6">
|
16 |
+
<div class="flex justify-between items-center mb-4">
|
17 |
+
<h2 class="text-xl font-semibold text-gray-800">
|
18 |
+
Job Applications
|
19 |
+
<span class="ml-2 text-sm text-gray-500">({{ job_emails|length }})</span>
|
20 |
+
</h2>
|
21 |
+
<button onclick="automateReplies('jobs')"
|
22 |
+
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition duration-200">
|
23 |
+
Automate Replies
|
24 |
+
</button>
|
25 |
+
</div>
|
26 |
+
<div class="space-y-4">
|
27 |
+
{% for email in job_emails %}
|
28 |
+
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-200">
|
29 |
+
<div class="flex justify-between items-start">
|
30 |
+
<div>
|
31 |
+
<h3 class="font-semibold text-gray-800">{{ email.name }}</h3>
|
32 |
+
<p class="text-sm text-gray-600">{{ email.from }}</p>
|
33 |
+
</div>
|
34 |
+
<span class="text-sm text-gray-500">{{ email.mail_type }}</span>
|
35 |
+
</div>
|
36 |
+
<h4 class="font-medium text-gray-700 mt-2">{{ email.subject }}</h4>
|
37 |
+
<p class="text-gray-600 mt-2 text-sm whitespace-pre-line">{{ email.message }}</p>
|
38 |
+
{% if email.resume_pdf %}
|
39 |
+
<div class="mt-3 flex items-center">
|
40 |
+
<svg class="w-4 h-4 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
41 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
42 |
+
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13">
|
43 |
+
</path>
|
44 |
+
</svg>
|
45 |
+
<span class="text-sm text-gray-500">Resume Attached</span>
|
46 |
+
</div>
|
47 |
+
{% endif %}
|
48 |
+
</div>
|
49 |
+
{% endfor %}
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
|
53 |
+
<!-- General Queries Container -->
|
54 |
+
<div class="bg-white rounded-lg shadow-lg p-6">
|
55 |
+
<div class="flex justify-between items-center mb-4">
|
56 |
+
<h2 class="text-xl font-semibold text-gray-800">
|
57 |
+
General Queries
|
58 |
+
<span class="ml-2 text-sm text-gray-500">({{ general_emails|length }})</span>
|
59 |
+
</h2>
|
60 |
+
<button onclick="automateReplies('general')"
|
61 |
+
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition duration-200">
|
62 |
+
Automate Replies
|
63 |
+
</button>
|
64 |
+
</div>
|
65 |
+
<div class="space-y-4">
|
66 |
+
{% for email in general_emails %}
|
67 |
+
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition duration-200">
|
68 |
+
<div class="flex justify-between items-start">
|
69 |
+
<div>
|
70 |
+
<h3 class="font-semibold text-gray-800">{{ email.name }}</h3>
|
71 |
+
<p class="text-sm text-gray-600">{{ email.from }}</p>
|
72 |
+
</div>
|
73 |
+
<span class="text-sm text-gray-500">{{ email.mail_type }}</span>
|
74 |
+
</div>
|
75 |
+
<h4 class="font-medium text-gray-700 mt-2">{{ email.subject }}</h4>
|
76 |
+
<p class="text-gray-600 mt-2 text-sm whitespace-pre-line">{{ email.message }}</p>
|
77 |
+
</div>
|
78 |
+
{% endfor %}
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<!-- Status Alert -->
|
84 |
+
<div id="status-alert" class="fixed bottom-4 right-4 hidden">
|
85 |
+
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-lg">
|
86 |
+
<p class="font-bold">Success!</p>
|
87 |
+
<p id="alert-message"></p>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
|
92 |
+
<script>
|
93 |
+
function automateReplies(type) {
|
94 |
+
const alertDiv = document.getElementById('status-alert');
|
95 |
+
const alertMessage = document.getElementById('alert-message');
|
96 |
+
|
97 |
+
fetch('/api/automate-replies', {
|
98 |
+
method: 'POST',
|
99 |
+
headers: {
|
100 |
+
'Content-Type': 'application/json'
|
101 |
+
},
|
102 |
+
body: JSON.stringify({ type: type })
|
103 |
+
})
|
104 |
+
.then(response => response.json())
|
105 |
+
.then(data => {
|
106 |
+
alertMessage.textContent = data.message;
|
107 |
+
alertDiv.classList.remove('hidden');
|
108 |
+
setTimeout(() => {
|
109 |
+
alertDiv.classList.add('hidden');
|
110 |
+
}, 3000);
|
111 |
+
})
|
112 |
+
.catch(error => {
|
113 |
+
console.error('Error:', error);
|
114 |
+
alertMessage.textContent = 'Error processing request';
|
115 |
+
alertDiv.classList.remove('hidden');
|
116 |
+
setTimeout(() => {
|
117 |
+
alertDiv.classList.add('hidden');
|
118 |
+
}, 3000);
|
119 |
+
});
|
120 |
+
}
|
121 |
+
</script>
|
122 |
+
</body>
|
123 |
+
</html>
|
templates/index.html
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Enterprise HR Portal | Dashboard</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
|
8 |
+
</head>
|
9 |
+
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
|
10 |
+
<!-- Header -->
|
11 |
+
<header class="fixed top-0 w-full bg-white shadow-sm z-10">
|
12 |
+
<div class="container mx-auto px-6 py-4">
|
13 |
+
<div class="flex items-center justify-between">
|
14 |
+
<div class="flex items-center">
|
15 |
+
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
16 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
17 |
+
</svg>
|
18 |
+
<h1 class="ml-3 text-xl font-semibold text-gray-800">Enterprise HR Portal</h1>
|
19 |
+
</div>
|
20 |
+
<div class="text-sm text-gray-500">Welcome, Administrator</div>
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
</header>
|
24 |
+
|
25 |
+
<!-- Main Content -->
|
26 |
+
<main class="container mx-auto px-6 pt-24 pb-12">
|
27 |
+
<div class="max-w-5xl mx-auto">
|
28 |
+
<!-- Welcome Section -->
|
29 |
+
<div class="text-center mb-12">
|
30 |
+
<h2 class="text-3xl font-bold text-gray-900 mb-4">Welcome to HR Management Suite</h2>
|
31 |
+
<p class="text-gray-600 max-w-2xl mx-auto">Access your comprehensive HR management tools and streamline your recruitment process with our integrated solutions.</p>
|
32 |
+
</div>
|
33 |
+
|
34 |
+
<!-- Cards Grid -->
|
35 |
+
<div class="grid md:grid-cols-3 gap-6">
|
36 |
+
<!-- Hiring Card -->
|
37 |
+
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
38 |
+
<div class="p-6">
|
39 |
+
<div class="bg-blue-50 rounded-full w-12 h-12 flex items-center justify-center mb-4">
|
40 |
+
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
41 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
42 |
+
</svg>
|
43 |
+
</div>
|
44 |
+
<h3 class="text-xl font-semibold text-gray-900 mb-2">Talent Acquisition</h3>
|
45 |
+
<p class="text-gray-600 mb-4">Manage your recruitment pipeline and candidate evaluations efficiently.</p>
|
46 |
+
<a href="/dashboard" class="inline-flex items-center justify-center w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-300">
|
47 |
+
<span>Access Dashboard</span>
|
48 |
+
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
49 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
50 |
+
</svg>
|
51 |
+
</a>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
|
55 |
+
<!-- Mail Inbox Card -->
|
56 |
+
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
57 |
+
<div class="p-6">
|
58 |
+
<div class="bg-green-50 rounded-full w-12 h-12 flex items-center justify-center mb-4">
|
59 |
+
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
60 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
61 |
+
</svg>
|
62 |
+
</div>
|
63 |
+
<h3 class="text-xl font-semibold text-gray-900 mb-2">Communication Hub</h3>
|
64 |
+
<p class="text-gray-600 mb-4">Centralized inbox for all recruitment-related communications.</p>
|
65 |
+
<a href="/mail-inbox" class="inline-flex items-center justify-center w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors duration-300">
|
66 |
+
<span>Open Inbox</span>
|
67 |
+
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
68 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
69 |
+
</svg>
|
70 |
+
</a>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<!-- Post Jobs Card -->
|
75 |
+
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
76 |
+
<div class="p-6">
|
77 |
+
<div class="bg-purple-50 rounded-full w-12 h-12 flex items-center justify-center mb-4">
|
78 |
+
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
79 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
80 |
+
</svg>
|
81 |
+
</div>
|
82 |
+
<h3 class="text-xl font-semibold text-gray-900 mb-2">Job Management</h3>
|
83 |
+
<p class="text-gray-600 mb-4">Create and manage job postings across multiple platforms.</p>
|
84 |
+
<a href="/post-jobs" class="inline-flex items-center justify-center w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-300">
|
85 |
+
<span>Create Posting</span>
|
86 |
+
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
87 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
88 |
+
</svg>
|
89 |
+
</a>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
|
94 |
+
<!-- Quick Stats -->
|
95 |
+
<div class="mt-12 bg-white rounded-xl shadow-md p-6">
|
96 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Overview</h3>
|
97 |
+
<div class="grid grid-cols-3 gap-6">
|
98 |
+
<div class="text-center">
|
99 |
+
<div class="text-2xl font-bold text-blue-600">1</div>
|
100 |
+
<div class="text-sm text-gray-600">Active Jobs</div>
|
101 |
+
</div>
|
102 |
+
<div class="text-center">
|
103 |
+
<div class="text-2xl font-bold text-green-600">12</div>
|
104 |
+
<div class="text-sm text-gray-600">New Applications</div>
|
105 |
+
</div>
|
106 |
+
<div class="text-center">
|
107 |
+
<div class="text-2xl font-bold text-purple-600">2</div>
|
108 |
+
<div class="text-sm text-gray-600">Interviews Today</div>
|
109 |
+
</div>
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
</div>
|
113 |
+
</main>
|
114 |
+
|
115 |
+
<!-- Footer -->
|
116 |
+
<footer class="bg-white border-t border-gray-200">
|
117 |
+
<div class="container mx-auto px-6 py-4">
|
118 |
+
<div class="text-center text-sm text-gray-500">
|
119 |
+
© 2024 Enterprise HR Portal. All rights reserved.
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
</footer>
|
123 |
+
</body>
|
124 |
+
</html>
|
templates/job_details.html
ADDED
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Job Details - HR Dashboard</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
|
8 |
+
</head>
|
9 |
+
<body class="bg-gray-50">
|
10 |
+
<!-- Enhanced Navigation -->
|
11 |
+
<nav class="bg-white shadow-lg border-b border-gray-200">
|
12 |
+
<div class="max-w-7xl mx-auto px-4 py-4">
|
13 |
+
<div class="flex justify-between items-center">
|
14 |
+
<div class="flex items-center">
|
15 |
+
<h1 class="text-2xl font-bold text-gray-900">HR Dashboard</h1>
|
16 |
+
<span class="mx-2 text-gray-300">|</span>
|
17 |
+
<h2 class="text-gray-600">Job Details</h2>
|
18 |
+
</div>
|
19 |
+
<a href="/" class="flex items-center text-indigo-600 hover:text-indigo-900">
|
20 |
+
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
21 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
22 |
+
</svg>
|
23 |
+
Back to Jobs
|
24 |
+
</a>
|
25 |
+
</div>
|
26 |
+
</div>
|
27 |
+
</nav>
|
28 |
+
|
29 |
+
<main class="max-w-7xl mx-auto px-4 py-8 space-y-8">
|
30 |
+
<!-- Enhanced Job Details Header -->
|
31 |
+
<div class="bg-white shadow rounded-lg p-6 border border-gray-100">
|
32 |
+
<div class="grid grid-cols-2 gap-6">
|
33 |
+
<div>
|
34 |
+
<h2 class="text-3xl font-bold text-gray-900">{{ job.title }}</h2>
|
35 |
+
<div class="mt-4 space-y-2">
|
36 |
+
<p class="text-gray-600 flex items-center">
|
37 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
38 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
39 |
+
</svg>
|
40 |
+
Department: {{ job.department }}
|
41 |
+
</p>
|
42 |
+
<p class="text-gray-600 flex items-center">
|
43 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
44 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
|
45 |
+
</svg>
|
46 |
+
Job ID: {{ job.job_id }}
|
47 |
+
</p>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
<div class="text-right">
|
51 |
+
<span class="px-4 py-2 inline-flex text-base leading-5 font-semibold rounded-full
|
52 |
+
{% if job.status == 'active' %}
|
53 |
+
bg-green-100 text-green-800
|
54 |
+
{% else %}
|
55 |
+
bg-red-100 text-red-800
|
56 |
+
{% endif %}">
|
57 |
+
{{ job.status|default('Status Not Set')|title }}
|
58 |
+
</span>
|
59 |
+
</div>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
|
63 |
+
<!-- Enhanced Shortlisted Candidates Section -->
|
64 |
+
<div class="bg-white shadow rounded-lg p-6 border border-gray-100">
|
65 |
+
<div class="flex justify-between items-center mb-6">
|
66 |
+
<div>
|
67 |
+
<h3 class="text-xl font-semibold text-gray-900">Shortlisted Candidates</h3>
|
68 |
+
<p class="text-sm text-gray-500 mt-1">{{ shortlisted_candidates|length }} candidates in pipeline</p>
|
69 |
+
</div>
|
70 |
+
{% if shortlisted_candidates %}
|
71 |
+
<button onclick="handleConfirmationEmails('{{ job.job_id }}')"
|
72 |
+
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition duration-150 ease-in-out flex items-center">
|
73 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
74 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
75 |
+
</svg>
|
76 |
+
Send Confirmation Emails
|
77 |
+
</button>
|
78 |
+
{% endif %}
|
79 |
+
</div>
|
80 |
+
|
81 |
+
{% if shortlisted_candidates %}
|
82 |
+
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
83 |
+
<table class="min-w-full divide-y divide-gray-200">
|
84 |
+
<thead class="bg-gray-50">
|
85 |
+
<tr>
|
86 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Candidate</th>
|
87 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
88 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Interview Progress</th>
|
89 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
90 |
+
</tr>
|
91 |
+
</thead>
|
92 |
+
<tbody class="bg-white divide-y divide-gray-200">
|
93 |
+
{% for candidate in shortlisted_candidates %}
|
94 |
+
<tr class="hover:bg-gray-50">
|
95 |
+
<td class="px-6 py-4">
|
96 |
+
<div>
|
97 |
+
<div class="text-sm font-medium text-gray-900">{{ candidate.candidate_info.name }}</div>
|
98 |
+
<div class="text-sm text-gray-500">{{ candidate.candidate_info.email }}</div>
|
99 |
+
</div>
|
100 |
+
</td>
|
101 |
+
<td class="px-6 py-4">
|
102 |
+
{% if candidate.status %}
|
103 |
+
<span class="px-3 py-1 text-sm font-medium rounded-full
|
104 |
+
{% if candidate.status == 'shortlisted' %}
|
105 |
+
bg-yellow-100 text-yellow-800
|
106 |
+
{% elif candidate.status == 'interview_scheduled' %}
|
107 |
+
bg-blue-100 text-blue-800
|
108 |
+
{% elif candidate.status == 'ai_interview' %}
|
109 |
+
bg-purple-100 text-purple-800
|
110 |
+
{% elif candidate.status == 'tech_interview' %}
|
111 |
+
bg-indigo-100 text-indigo-800
|
112 |
+
{% else %}
|
113 |
+
bg-gray-100 text-gray-800
|
114 |
+
{% endif %}">
|
115 |
+
{{ candidate.status|default('Not Set')|replace('_', ' ')|title }}
|
116 |
+
</span>
|
117 |
+
{% else %}
|
118 |
+
<span class="px-3 py-1 text-sm font-medium rounded-full bg-gray-100 text-gray-800">
|
119 |
+
Status Not Set
|
120 |
+
</span>
|
121 |
+
{% endif %}
|
122 |
+
</td>
|
123 |
+
<td class="px-6 py-4">
|
124 |
+
{% if candidate.status == 'ai_interview' %}
|
125 |
+
{% if candidate.interview_completed %}
|
126 |
+
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-full">
|
127 |
+
Interview Completed
|
128 |
+
</span>
|
129 |
+
{% else %}
|
130 |
+
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded-full">
|
131 |
+
Interview Pending
|
132 |
+
</span>
|
133 |
+
{% endif %}
|
134 |
+
{% elif candidate.status == 'tech_interview' %}
|
135 |
+
<span class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
|
136 |
+
Technical Interview Scheduled
|
137 |
+
</span>
|
138 |
+
{% else %}
|
139 |
+
<span class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">
|
140 |
+
No Interview / Pending
|
141 |
+
</span>
|
142 |
+
{% endif %}
|
143 |
+
</td>
|
144 |
+
<td class="px-6 py-4 text-sm font-medium space-x-3">
|
145 |
+
<button onclick="viewCandidate('{{ candidate._id }}')"
|
146 |
+
class="text-indigo-600 hover:text-indigo-900">
|
147 |
+
View Details
|
148 |
+
</button>
|
149 |
+
{% if candidate.resume_id %}
|
150 |
+
<a href="/view_resume/{{ candidate.resume_id }}"
|
151 |
+
class="text-green-600 hover:text-green-900">
|
152 |
+
View Resume
|
153 |
+
</a>
|
154 |
+
{% endif %}
|
155 |
+
</td>
|
156 |
+
</tr>
|
157 |
+
{% endfor %}
|
158 |
+
</tbody>
|
159 |
+
</table>
|
160 |
+
</div>
|
161 |
+
{% else %}
|
162 |
+
<div class="text-center py-8">
|
163 |
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
164 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
165 |
+
</svg>
|
166 |
+
<h3 class="mt-2 text-sm font-medium text-gray-900">No shortlisted candidates</h3>
|
167 |
+
<p class="mt-1 text-sm text-gray-500">Start by reviewing applications to shortlist candidates.</p>
|
168 |
+
</div>
|
169 |
+
{% endif %}
|
170 |
+
</div>
|
171 |
+
|
172 |
+
<!-- Modal Content - Only show what's available -->
|
173 |
+
<div id="candidateModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
174 |
+
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-lg bg-white">
|
175 |
+
<div id="modalContent" class="mt-3"></div>
|
176 |
+
<div class="mt-6 flex justify-end">
|
177 |
+
<button onclick="closeModal()"
|
178 |
+
class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition duration-150 ease-in-out">
|
179 |
+
Close
|
180 |
+
</button>
|
181 |
+
</div>
|
182 |
+
</div>
|
183 |
+
</div>
|
184 |
+
<!-- Rejected Candidates Section -->
|
185 |
+
<div class="bg-white shadow rounded-lg p-6 border border-gray-100">
|
186 |
+
<div class="flex justify-between items-center mb-6">
|
187 |
+
<div>
|
188 |
+
<h3 class="text-xl font-semibold text-gray-900">Rejected Candidates</h3>
|
189 |
+
<p class="text-sm text-gray-500 mt-1">{{ rejected_candidates|length }} candidates rejected</p>
|
190 |
+
</div>
|
191 |
+
{% if rejected_candidates %}
|
192 |
+
<button onclick="sendRejectionEmails('{{ job.job_id }}')"
|
193 |
+
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition duration-150 ease-in-out flex items-center">
|
194 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
195 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
196 |
+
</svg>
|
197 |
+
Send Rejection Emails
|
198 |
+
</button>
|
199 |
+
{% endif %}
|
200 |
+
</div>
|
201 |
+
|
202 |
+
{% if rejected_candidates %}
|
203 |
+
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
204 |
+
<table class="min-w-full divide-y divide-gray-200">
|
205 |
+
<thead class="bg-gray-50">
|
206 |
+
<tr>
|
207 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Candidate</th>
|
208 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
209 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rejection Reason</th>
|
210 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
211 |
+
</tr>
|
212 |
+
</thead>
|
213 |
+
<tbody class="bg-white divide-y divide-gray-200">
|
214 |
+
{% for candidate in rejected_candidates %}
|
215 |
+
<tr class="hover:bg-gray-50">
|
216 |
+
<td class="px-6 py-4">
|
217 |
+
<div>
|
218 |
+
<div class="text-sm font-medium text-gray-900">{{ candidate.candidate_info.name|default('Name Not Available') }}</div>
|
219 |
+
<div class="text-sm text-gray-500">{{ candidate.candidate_info.email|default('Email Not Available') }}</div>
|
220 |
+
</div>
|
221 |
+
</td>
|
222 |
+
<td class="px-6 py-4">
|
223 |
+
<span class="px-3 py-1 text-sm font-medium rounded-full bg-red-100 text-red-800">
|
224 |
+
{{ candidate.status|default('Rejected')|replace('_', ' ')|title }}
|
225 |
+
</span>
|
226 |
+
</td>
|
227 |
+
<td class="px-6 py-4">
|
228 |
+
<span class="text-sm text-gray-500">
|
229 |
+
{{ candidate.rejection_reason|default('No reason specified') }}
|
230 |
+
</span>
|
231 |
+
</td>
|
232 |
+
<td class="px-6 py-4 text-sm font-medium space-x-3">
|
233 |
+
<button onclick="viewCandidate('{{ candidate._id }}')"
|
234 |
+
class="text-indigo-600 hover:text-indigo-900">
|
235 |
+
View Details
|
236 |
+
</button>
|
237 |
+
{% if candidate.resume_id %}
|
238 |
+
<a href="/view_resume/{{ candidate.resume_id }}"
|
239 |
+
class="text-green-600 hover:text-green-900">
|
240 |
+
View Resume
|
241 |
+
</a>
|
242 |
+
{% endif %}
|
243 |
+
</td>
|
244 |
+
</tr>
|
245 |
+
{% endfor %}
|
246 |
+
</tbody>
|
247 |
+
</table>
|
248 |
+
</div>
|
249 |
+
{% else %}
|
250 |
+
<div class="text-center py-8">
|
251 |
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
252 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
253 |
+
</svg>
|
254 |
+
<h3 class="mt-2 text-sm font-medium text-gray-900">No rejected candidates</h3>
|
255 |
+
<p class="mt-1 text-sm text-gray-500">All candidates are still under consideration.</p>
|
256 |
+
</div>
|
257 |
+
{% endif %}
|
258 |
+
</div>
|
259 |
+
</main>
|
260 |
+
|
261 |
+
<script>
|
262 |
+
function viewCandidate(candidateId) {
|
263 |
+
console.log("Viewing candidate:", candidateId);
|
264 |
+
|
265 |
+
fetch(`/candidate/${candidateId}`)
|
266 |
+
.then(response => {
|
267 |
+
if (!response.ok) {
|
268 |
+
throw new Error('Network response was not ok');
|
269 |
+
}
|
270 |
+
return response.json();
|
271 |
+
})
|
272 |
+
.then(data => {
|
273 |
+
console.log("Received data:", data);
|
274 |
+
const candidate = data.candidate;
|
275 |
+
const interviews = data.interviews || [];
|
276 |
+
|
277 |
+
// Determine interview status and type
|
278 |
+
const currentStatus = candidate.status;
|
279 |
+
const hasInterview = interviews.length > 0;
|
280 |
+
const latestInterview = hasInterview ? interviews[interviews.length - 1] : null;
|
281 |
+
|
282 |
+
let content = `
|
283 |
+
<div class="space-y-6">
|
284 |
+
<div class="flex justify-between items-start">
|
285 |
+
<div>
|
286 |
+
<h2 class="text-2xl font-bold">${candidate.candidate_info.name || 'Name Not Available'}</h2>
|
287 |
+
<p class="text-gray-600">${candidate.candidate_info.email || 'Email Not Available'}</p>
|
288 |
+
${candidate.candidate_info.phone ?
|
289 |
+
`<p class="text-gray-600">${candidate.candidate_info.phone}</p>` : ''}
|
290 |
+
${candidate.candidate_info.experience ?
|
291 |
+
`<p class="text-gray-600">Experience: ${candidate.candidate_info.experience}</p>` : ''}
|
292 |
+
${candidate.candidate_info.current_company ?
|
293 |
+
`<p class="text-gray-600">Current Company: ${candidate.candidate_info.current_company}</p>` : ''}
|
294 |
+
</div>
|
295 |
+
<div class="text-right">
|
296 |
+
${candidate.ai_evaluation?.confidence_score ?
|
297 |
+
`<div class="text-lg font-semibold">AI Score: ${candidate.ai_evaluation.confidence_score}%</div>` : ''}
|
298 |
+
<div class="text-sm text-gray-500">Status: ${(currentStatus || 'Not Set').replace('_', ' ').toUpperCase()}</div>
|
299 |
+
</div>
|
300 |
+
</div>`;
|
301 |
+
|
302 |
+
// Skills Assessment Section (if available)
|
303 |
+
if (candidate.ai_evaluation?.matching_skills || candidate.ai_evaluation?.missing_skills) {
|
304 |
+
content += `
|
305 |
+
<div class="bg-gray-50 p-4 rounded-lg">
|
306 |
+
<h3 class="font-semibold mb-2">Skills Assessment</h3>
|
307 |
+
<div class="space-y-2">
|
308 |
+
${candidate.ai_evaluation.matching_skills ? `
|
309 |
+
<div>
|
310 |
+
<span class="font-medium">Matching Skills:</span>
|
311 |
+
<div class="flex flex-wrap gap-1 mt-1">
|
312 |
+
${candidate.ai_evaluation.matching_skills.map(skill =>
|
313 |
+
`<span class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-sm">${skill}</span>`
|
314 |
+
).join('')}
|
315 |
+
</div>
|
316 |
+
</div>` : ''}
|
317 |
+
${candidate.ai_evaluation.missing_skills ? `
|
318 |
+
<div>
|
319 |
+
<span class="font-medium">Missing Skills:</span>
|
320 |
+
<div class="flex flex-wrap gap-1 mt-1">
|
321 |
+
${candidate.ai_evaluation.missing_skills.map(skill =>
|
322 |
+
`<span class="px-2 py-1 bg-red-100 text-red-800 rounded-full text-sm">${skill}</span>`
|
323 |
+
).join('')}
|
324 |
+
</div>
|
325 |
+
</div>` : ''}
|
326 |
+
</div>
|
327 |
+
</div>`;
|
328 |
+
}
|
329 |
+
|
330 |
+
// Interview Options Section
|
331 |
+
content += `
|
332 |
+
<div class="space-y-4">
|
333 |
+
<h3 class="font-semibold">Interview Options</h3>
|
334 |
+
<div class="grid grid-cols-3 gap-4">
|
335 |
+
<button
|
336 |
+
onclick="showAiInterviewModal('${candidate._id}')"
|
337 |
+
class="p-3 text-center rounded ${currentStatus === 'ai_interview' ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 text-white'}"
|
338 |
+
${currentStatus === 'ai_interview' ? 'disabled' : ''}>
|
339 |
+
<svg class="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
340 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
341 |
+
</svg>
|
342 |
+
AI Interview
|
343 |
+
</button>
|
344 |
+
<button
|
345 |
+
onclick="openCodingQuestionPage()"
|
346 |
+
class="p-3 text-center rounded bg-green-600 hover:bg-green-700 text-white">
|
347 |
+
<svg class="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
348 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
349 |
+
</svg>
|
350 |
+
Technical Interview
|
351 |
+
</button>
|
352 |
+
<button
|
353 |
+
onclick="scheduleHRInterview('${candidate._id}')"
|
354 |
+
class="p-3 text-center rounded ${currentStatus === 'interview_scheduled' ? 'bg-gray-300 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700 text-white'}"
|
355 |
+
${currentStatus === 'interview_scheduled' ? 'disabled' : ''}>
|
356 |
+
<svg class="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
357 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
358 |
+
</svg>
|
359 |
+
HR Interview
|
360 |
+
</button>
|
361 |
+
</div>
|
362 |
+
</div>`;
|
363 |
+
|
364 |
+
// Scheduled Interviews Section
|
365 |
+
if (hasInterview) {
|
366 |
+
content += `
|
367 |
+
<div class="bg-blue-50 p-4 rounded-lg">
|
368 |
+
<h3 class="font-semibold mb-2">Scheduled Interview</h3>
|
369 |
+
${interviews.map(interview => `
|
370 |
+
<div class="flex justify-between items-center p-2 bg-white rounded-lg mb-2">
|
371 |
+
<div>
|
372 |
+
<div class="font-medium">${interview.round_type || interview.type}</div>
|
373 |
+
<div class="text-sm text-gray-500">
|
374 |
+
Scheduled for: ${interview.scheduled_date || 'Date not set'}
|
375 |
+
</div>
|
376 |
+
</div>
|
377 |
+
${(() => {
|
378 |
+
// Only show join button for HR interviews
|
379 |
+
if (currentStatus === 'interview_scheduled' && interview.meeting_link) {
|
380 |
+
return `
|
381 |
+
<a href="${interview.meeting_link}"
|
382 |
+
target="_blank"
|
383 |
+
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition duration-150 ease-in-out">
|
384 |
+
Join Meeting
|
385 |
+
</a>
|
386 |
+
`;
|
387 |
+
}
|
388 |
+
// For AI interviews, show completion status
|
389 |
+
else if (currentStatus === 'ai_interview') {
|
390 |
+
return `
|
391 |
+
<div class="text-sm ${candidate.interview_completed ? 'text-green-600' : 'text-yellow-600'}">
|
392 |
+
${candidate.interview_completed ? 'Interview Completed' : 'Interview Pending'}
|
393 |
+
</div>
|
394 |
+
`;
|
395 |
+
}
|
396 |
+
// For technical interviews, just show status
|
397 |
+
else if (currentStatus === 'tech_interview') {
|
398 |
+
return `
|
399 |
+
<div class="text-sm text-blue-600">
|
400 |
+
Technical Interview Scheduled
|
401 |
+
</div>
|
402 |
+
`;
|
403 |
+
}
|
404 |
+
return '';
|
405 |
+
})()}
|
406 |
+
</div>
|
407 |
+
`).join('')}
|
408 |
+
</div>`;
|
409 |
+
}
|
410 |
+
|
411 |
+
content += `</div>`;
|
412 |
+
document.getElementById('modalContent').innerHTML = content;
|
413 |
+
document.getElementById('candidateModal').classList.remove('hidden');
|
414 |
+
})
|
415 |
+
.catch(error => {
|
416 |
+
console.error('Error:', error);
|
417 |
+
alert('Error loading candidate details. Please try again.');
|
418 |
+
});
|
419 |
+
}
|
420 |
+
|
421 |
+
function closeModal() {
|
422 |
+
document.getElementById('candidateModal').classList.add('hidden');
|
423 |
+
}
|
424 |
+
|
425 |
+
// Enhanced email handling functions
|
426 |
+
function handleConfirmationEmails(jobId) {
|
427 |
+
if(!jobId) {
|
428 |
+
console.error('Job ID is required');
|
429 |
+
alert('Error: Job ID is missing');
|
430 |
+
return;
|
431 |
+
}
|
432 |
+
|
433 |
+
if(confirm('Send confirmation emails to all shortlisted candidates?')) {
|
434 |
+
fetch(`/send_confirmation_emails/${jobId}`, {
|
435 |
+
method: 'POST',
|
436 |
+
headers: {
|
437 |
+
'Content-Type': 'application/json',
|
438 |
+
}
|
439 |
+
})
|
440 |
+
.then(response => {
|
441 |
+
if (!response.ok) {
|
442 |
+
throw new Error('Network response was not ok');
|
443 |
+
}
|
444 |
+
return response.json();
|
445 |
+
})
|
446 |
+
.then(data => {
|
447 |
+
if (data.success) {
|
448 |
+
alert('Success: ' + data.message);
|
449 |
+
location.reload();
|
450 |
+
} else {
|
451 |
+
alert('Error: ' + (data.message || 'Failed to send emails'));
|
452 |
+
}
|
453 |
+
})
|
454 |
+
.catch(error => {
|
455 |
+
console.error('Error:', error);
|
456 |
+
alert('Error sending emails. Please try again.');
|
457 |
+
});
|
458 |
+
}
|
459 |
+
}
|
460 |
+
function sendRejectionEmails(jobId) {
|
461 |
+
if(!jobId) {
|
462 |
+
console.error('Job ID is required');
|
463 |
+
alert('Error: Job ID is missing');
|
464 |
+
return;
|
465 |
+
}
|
466 |
+
|
467 |
+
if(confirm('Send rejection emails to all not-shortlisted candidates?')) {
|
468 |
+
fetch(`/send_rejection_emails/${jobId}`, {
|
469 |
+
method: 'POST',
|
470 |
+
headers: {
|
471 |
+
'Content-Type': 'application/json',
|
472 |
+
}
|
473 |
+
})
|
474 |
+
.then(response => {
|
475 |
+
if (!response.ok) {
|
476 |
+
throw new Error('Network response was not ok');
|
477 |
+
}
|
478 |
+
return response.json();
|
479 |
+
})
|
480 |
+
.then(data => {
|
481 |
+
if (data.success) {
|
482 |
+
alert('Success: ' + data.message);
|
483 |
+
location.reload();
|
484 |
+
} else {
|
485 |
+
alert('Error: ' + (data.message || 'Failed to send emails'));
|
486 |
+
}
|
487 |
+
})
|
488 |
+
.catch(error => {
|
489 |
+
console.error('Error:', error);
|
490 |
+
alert('Error sending emails. Please try again.');
|
491 |
+
});
|
492 |
+
}
|
493 |
+
}
|
494 |
+
// Enhanced interview scheduling functions
|
495 |
+
function scheduleHRInterview(candidateId) {
|
496 |
+
if(!candidateId) {
|
497 |
+
console.error('Candidate ID is required');
|
498 |
+
alert('Error: Candidate ID is missing');
|
499 |
+
return;
|
500 |
+
}
|
501 |
+
|
502 |
+
if(confirm('Schedule HR interview for this candidate?')) {
|
503 |
+
fetch('/schedule_hr_interview', {
|
504 |
+
method: 'POST',
|
505 |
+
headers: {
|
506 |
+
'Content-Type': 'application/json',
|
507 |
+
},
|
508 |
+
body: JSON.stringify({
|
509 |
+
candidate_id: candidateId
|
510 |
+
})
|
511 |
+
})
|
512 |
+
.then(response => {
|
513 |
+
if (!response.ok) {
|
514 |
+
throw new Error('Network response was not ok');
|
515 |
+
}
|
516 |
+
return response.json();
|
517 |
+
})
|
518 |
+
.then(data => {
|
519 |
+
if (data.success) {
|
520 |
+
alert('HR Interview scheduled successfully!\nMeeting Link: ' + data.meeting_link);
|
521 |
+
viewCandidate(candidateId); // Refresh the modal
|
522 |
+
} else {
|
523 |
+
alert('Error: ' + (data.message || 'Failed to schedule interview'));
|
524 |
+
}
|
525 |
+
})
|
526 |
+
.catch(error => {
|
527 |
+
console.error('Error:', error);
|
528 |
+
alert('Error scheduling interview. Please try again.');
|
529 |
+
});
|
530 |
+
}
|
531 |
+
}
|
532 |
+
|
533 |
+
function scheduleInterview(candidateId, type) {
|
534 |
+
if(!candidateId || !type) {
|
535 |
+
console.error('Candidate ID and interview type are required');
|
536 |
+
alert('Error: Missing required information');
|
537 |
+
return;
|
538 |
+
}
|
539 |
+
|
540 |
+
fetch('/schedule_interview', {
|
541 |
+
method: 'POST',
|
542 |
+
headers: {
|
543 |
+
'Content-Type': 'application/json',
|
544 |
+
},
|
545 |
+
body: JSON.stringify({
|
546 |
+
candidate_id: candidateId,
|
547 |
+
interview_type: type
|
548 |
+
})
|
549 |
+
})
|
550 |
+
.then(response => {
|
551 |
+
if (!response.ok) {
|
552 |
+
throw new Error('Network response was not ok');
|
553 |
+
}
|
554 |
+
return response.json();
|
555 |
+
})
|
556 |
+
.then(data => {
|
557 |
+
if (data.success) {
|
558 |
+
alert(`${type.charAt(0).toUpperCase() + type.slice(1)} interview scheduled successfully!`);
|
559 |
+
viewCandidate(candidateId); // Refresh the modal
|
560 |
+
} else {
|
561 |
+
alert('Error: ' + (data.message || 'Failed to schedule interview'));
|
562 |
+
}
|
563 |
+
})
|
564 |
+
.catch(error => {
|
565 |
+
console.error('Error:', error);
|
566 |
+
alert('Error scheduling interview. Please try again.');
|
567 |
+
});
|
568 |
+
}
|
569 |
+
function openCodingQuestionPage() {
|
570 |
+
window.location.href = "/technical.html"; // Ensures the file loads from root directory
|
571 |
+
}
|
572 |
+
|
573 |
+
</script>
|
574 |
+
</body>
|
575 |
+
</html>
|
templates/job_posting.html
ADDED
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<title>HR Dashboard | Jobs Management</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet" />
|
8 |
+
</head>
|
9 |
+
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
|
10 |
+
<!-- Header -->
|
11 |
+
<nav class="bg-white shadow-sm border-b border-gray-200">
|
12 |
+
<div class="max-w-7xl mx-auto px-6 py-4">
|
13 |
+
<div class="flex justify-between items-center">
|
14 |
+
<div class="flex items-center space-x-4">
|
15 |
+
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
16 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
17 |
+
</svg>
|
18 |
+
<div>
|
19 |
+
<h1 class="text-2xl font-bold text-gray-900">HR Dashboard</h1>
|
20 |
+
<p class="text-sm text-gray-500">Jobs Management</p>
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
<div class="flex items-center space-x-4">
|
24 |
+
<span id="currentDateTime" class="text-sm text-gray-500"></span>
|
25 |
+
</div>
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
</nav>
|
29 |
+
|
30 |
+
<main class="max-w-7xl mx-auto px-6 py-8 space-y-8">
|
31 |
+
<!-- Job Posting Form -->
|
32 |
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
33 |
+
<div class="px-6 py-4 border-b border-gray-200">
|
34 |
+
<div class="flex items-center justify-between">
|
35 |
+
<div class="flex items-center space-x-4">
|
36 |
+
<div class="bg-blue-50 rounded-full p-3">
|
37 |
+
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
38 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
39 |
+
</svg>
|
40 |
+
</div>
|
41 |
+
<h2 class="text-xl font-semibold text-gray-900">Create New Job Posting</h2>
|
42 |
+
</div>
|
43 |
+
<button id="toggleForm" class="text-blue-600 hover:text-blue-800">
|
44 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
45 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
46 |
+
</svg>
|
47 |
+
</button>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
|
51 |
+
<form id="jobPostingForm" class="p-6 space-y-6 hidden">
|
52 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
53 |
+
<div>
|
54 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Job Title</label>
|
55 |
+
<input type="text" name="jobTitle" required
|
56 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<div>
|
60 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Department</label>
|
61 |
+
<input type="text" name="department" required
|
62 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
63 |
+
</div>
|
64 |
+
|
65 |
+
<div>
|
66 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Location</label>
|
67 |
+
<input type="text" name="location" required
|
68 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
69 |
+
</div>
|
70 |
+
|
71 |
+
<div>
|
72 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Employment Type</label>
|
73 |
+
<select name="employmentType" required
|
74 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
75 |
+
<option value="full-time">Full Time</option>
|
76 |
+
<option value="part-time">Part Time</option>
|
77 |
+
<option value="contract">Contract</option>
|
78 |
+
<option value="internship">Internship</option>
|
79 |
+
</select>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<div>
|
84 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Job Description</label>
|
85 |
+
<textarea name="jobDescription" rows="4" required
|
86 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
87 |
+
</div>
|
88 |
+
|
89 |
+
<div>
|
90 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Roles and Responsibilities</label>
|
91 |
+
<textarea name="responsibilities" rows="4" required
|
92 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
93 |
+
</div>
|
94 |
+
|
95 |
+
<div>
|
96 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Required Qualifications</label>
|
97 |
+
<textarea name="qualifications" rows="4" required
|
98 |
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
99 |
+
</div>
|
100 |
+
|
101 |
+
<div class="border-t border-gray-200 pt-6">
|
102 |
+
<label class="block text-sm font-medium text-gray-700 mb-4">Select Platform for Posting</label>
|
103 |
+
<div class="flex space-x-4">
|
104 |
+
<label class="flex items-center space-x-2">
|
105 |
+
<input type="radio" name="platform" value="linkedin" checked
|
106 |
+
class="h-4 w-4 text-blue-600 focus:ring-blue-500">
|
107 |
+
<span class="text-sm text-gray-700">LinkedIn</span>
|
108 |
+
</label>
|
109 |
+
<label class="flex items-center space-x-2">
|
110 |
+
<input type="radio" name="platform" value="naukri"
|
111 |
+
class="h-4 w-4 text-blue-600 focus:ring-blue-500">
|
112 |
+
<span class="text-sm text-gray-700">Naukri</span>
|
113 |
+
</label>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
|
117 |
+
<div class="flex justify-end space-x-4">
|
118 |
+
<button type="reset"
|
119 |
+
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
|
120 |
+
Reset
|
121 |
+
</button>
|
122 |
+
<button type="submit"
|
123 |
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
124 |
+
Create Job Posting
|
125 |
+
</button>
|
126 |
+
</div>
|
127 |
+
</form>
|
128 |
+
</div>
|
129 |
+
|
130 |
+
<!-- Job Listings Table -->
|
131 |
+
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
132 |
+
<div class="px-6 py-4 border-b border-gray-200">
|
133 |
+
<div class="flex justify-between items-center">
|
134 |
+
<h2 class="text-xl font-semibold text-gray-900">Job Postings History</h2>
|
135 |
+
<div class="flex space-x-2">
|
136 |
+
<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
|
137 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
138 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
139 |
+
</svg>
|
140 |
+
</button>
|
141 |
+
<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
|
142 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
143 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
144 |
+
</svg>
|
145 |
+
</button>
|
146 |
+
</div>
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<div class="overflow-x-auto">
|
151 |
+
<table class="min-w-full divide-y divide-gray-200">
|
152 |
+
<thead class="bg-gray-50">
|
153 |
+
<tr>
|
154 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Job ID</th>
|
155 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Position</th>
|
156 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Department</th>
|
157 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
158 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Applications</th>
|
159 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
160 |
+
</tr>
|
161 |
+
</thead>
|
162 |
+
<tbody class="bg-white divide-y divide-gray-200">
|
163 |
+
<!-- Sample Current Job -->
|
164 |
+
<tr class="hover:bg-gray-50">
|
165 |
+
<td class="px-6 py-4">
|
166 |
+
<div class="text-sm font-medium text-gray-900">SDE001</div>
|
167 |
+
<div class="text-xs text-gray-500">Created on Feb 20, 2024</div>
|
168 |
+
</td>
|
169 |
+
<td class="px-6 py-4">
|
170 |
+
<div class="text-sm text-gray-900">Software Engineer</div>
|
171 |
+
</td>
|
172 |
+
<td class="px-6 py-4">
|
173 |
+
<div class="text-sm text-gray-900">Engineering</div>
|
174 |
+
</td>
|
175 |
+
<td class="px-6 py-4">
|
176 |
+
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
177 |
+
Active
|
178 |
+
</span>
|
179 |
+
</td>
|
180 |
+
<td class="px-6 py-4">
|
181 |
+
<div class="flex flex-col space-y-1">
|
182 |
+
<div class="text-sm text-gray-900">12 total applications</div>
|
183 |
+
<div class="text-sm text-green-600">4 shortlisted</div>
|
184 |
+
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
185 |
+
<div class="bg-green-600 h-1.5 rounded-full" style="width: 33%"></div>
|
186 |
+
</div>
|
187 |
+
</div>
|
188 |
+
</td>
|
189 |
+
<td class="px-6 py-4">
|
190 |
+
<div class="flex space-x-2">
|
191 |
+
<button class="text-blue-600 hover:text-blue-800">View</button>
|
192 |
+
<button class="text-red-600 hover:text-red-800">Close</button>
|
193 |
+
</div>
|
194 |
+
</td>
|
195 |
+
</tr>
|
196 |
+
|
197 |
+
<!-- Sample Past Job -->
|
198 |
+
<tr class="hover:bg-gray-50">
|
199 |
+
<td class="px-6 py-4">
|
200 |
+
<div class="text-sm font-medium text-gray-900">UI001</div>
|
201 |
+
<div class="text-xs text-gray-500">Created on Jan 15, 2024</div>
|
202 |
+
</td>
|
203 |
+
<td class="px-6 py-4">
|
204 |
+
<div class="text-sm text-gray-900">UI/UX Designer</div>
|
205 |
+
</td>
|
206 |
+
<td class="px-6 py-4">
|
207 |
+
<div class="text-sm text-gray-900">Design</div>
|
208 |
+
</td>
|
209 |
+
<td class="px-6 py-4">
|
210 |
+
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
|
211 |
+
Closed
|
212 |
+
</span>
|
213 |
+
</td>
|
214 |
+
<td class="px-6 py-4">
|
215 |
+
<div class="flex flex-col space-y-1">
|
216 |
+
<div class="text-sm text-gray-900">18 total applications</div>
|
217 |
+
<div class="text-sm text-green-600">6 shortlisted</div>
|
218 |
+
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
219 |
+
<div class="bg-green-600 h-1.5 rounded-full" style="width: 33%"></div>
|
220 |
+
</div>
|
221 |
+
</div>
|
222 |
+
</td>
|
223 |
+
<td class="px-6 py-4">
|
224 |
+
<button class="text-blue-600 hover:text-blue-800">View</button>
|
225 |
+
</td>
|
226 |
+
</tr>
|
227 |
+
<!-- Sample Past Job -->
|
228 |
+
<tr class="hover:bg-gray-50">
|
229 |
+
<td class="px-6 py-4">
|
230 |
+
<div class="text-sm font-medium text-gray-900">PM001</div>
|
231 |
+
<div class="text-xs text-gray-500">Created on Jan 5, 2024</div>
|
232 |
+
</td>
|
233 |
+
<td class="px-6 py-4">
|
234 |
+
<div class="text-sm text-gray-900">Product Manager</div>
|
235 |
+
</td>
|
236 |
+
<td class="px-6 py-4">
|
237 |
+
<div class="text-sm text-gray-900">Product</div>
|
238 |
+
</td>
|
239 |
+
<td class="px-6 py-4">
|
240 |
+
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
|
241 |
+
Closed
|
242 |
+
</span>
|
243 |
+
</td>
|
244 |
+
<td class="px-6 py-4">
|
245 |
+
<div class="flex flex-col space-y-1">
|
246 |
+
<div class="text-sm text-gray-900">15 total applications</div>
|
247 |
+
<div class="text-sm text-green-600">5 shortlisted</div>
|
248 |
+
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
249 |
+
<div class="bg-green-600 h-1.5 rounded-full" style="width: 33%"></div>
|
250 |
+
</div>
|
251 |
+
</div>
|
252 |
+
</td>
|
253 |
+
<td class="px-6 py-4">
|
254 |
+
<button class="text-blue-600 hover:text-blue-800">View</button>
|
255 |
+
</td>
|
256 |
+
</tr>
|
257 |
+
</tbody>
|
258 |
+
</table>
|
259 |
+
</div>
|
260 |
+
|
261 |
+
<!-- Pagination -->
|
262 |
+
<div class="px-6 py-4 border-t border-gray-200">
|
263 |
+
<div class="flex items-center justify-between">
|
264 |
+
<div class="text-sm text-gray-700">
|
265 |
+
Showing <span class="font-medium">1</span> to <span class="font-medium">3</span> of <span class="font-medium">3</span> results
|
266 |
+
</div>
|
267 |
+
<div class="flex space-x-2">
|
268 |
+
<button class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50">
|
269 |
+
Previous
|
270 |
+
</button>
|
271 |
+
<button class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50">
|
272 |
+
Next
|
273 |
+
</button>
|
274 |
+
</div>
|
275 |
+
</div>
|
276 |
+
</div>
|
277 |
+
</div>
|
278 |
+
</main>
|
279 |
+
|
280 |
+
<script>
|
281 |
+
// Toggle Job Posting Form
|
282 |
+
document.getElementById('toggleForm').addEventListener('click', function() {
|
283 |
+
const form = document.getElementById('jobPostingForm');
|
284 |
+
const icon = this.querySelector('svg path');
|
285 |
+
if (form.classList.contains('hidden')) {
|
286 |
+
form.classList.remove('hidden');
|
287 |
+
icon.setAttribute('d', 'M5 15l7-7 7 7');
|
288 |
+
} else {
|
289 |
+
form.classList.add('hidden');
|
290 |
+
icon.setAttribute('d', 'M19 9l-7 7-7-7');
|
291 |
+
}
|
292 |
+
});
|
293 |
+
|
294 |
+
// Form Submission
|
295 |
+
document.getElementById('jobPostingForm').addEventListener('submit', function(e) {
|
296 |
+
e.preventDefault();
|
297 |
+
|
298 |
+
const formData = new FormData(e.target);
|
299 |
+
const data = Object.fromEntries(formData.entries());
|
300 |
+
|
301 |
+
// Show success notification
|
302 |
+
const notification = document.createElement('div');
|
303 |
+
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300';
|
304 |
+
notification.textContent = `Job posting created successfully for ${data.platform}!`;
|
305 |
+
|
306 |
+
document.body.appendChild(notification);
|
307 |
+
setTimeout(() => notification.remove(), 3000);
|
308 |
+
|
309 |
+
// Reset form and hide it
|
310 |
+
e.target.reset();
|
311 |
+
e.target.classList.add('hidden');
|
312 |
+
});
|
313 |
+
|
314 |
+
// Update DateTime
|
315 |
+
function updateDateTime() {
|
316 |
+
const now = new Date();
|
317 |
+
const options = {
|
318 |
+
year: 'numeric',
|
319 |
+
month: 'long',
|
320 |
+
day: 'numeric',
|
321 |
+
hour: '2-digit',
|
322 |
+
minute: '2-digit'
|
323 |
+
};
|
324 |
+
document.getElementById('currentDateTime').textContent =
|
325 |
+
'Last updated: ' + now.toLocaleDateString('en-US', options);
|
326 |
+
}
|
327 |
+
|
328 |
+
// Initialize and update datetime
|
329 |
+
updateDateTime();
|
330 |
+
setInterval(updateDateTime, 60000);
|
331 |
+
|
332 |
+
// Close Job Function
|
333 |
+
function closeJob(jobId) {
|
334 |
+
if (confirm("Are you sure you want to close this job posting?")) {
|
335 |
+
// Add your close job logic here
|
336 |
+
showNotification("Job closed successfully!", "success");
|
337 |
+
}
|
338 |
+
}
|
339 |
+
|
340 |
+
// Show Notification Function
|
341 |
+
function showNotification(message, type) {
|
342 |
+
const notification = document.createElement('div');
|
343 |
+
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 ${
|
344 |
+
type === "success" ? "bg-green-500" : "bg-red-500"
|
345 |
+
} text-white`;
|
346 |
+
notification.textContent = message;
|
347 |
+
|
348 |
+
document.body.appendChild(notification);
|
349 |
+
setTimeout(() => notification.remove(), 3000);
|
350 |
+
}
|
351 |
+
</script>
|
352 |
+
</body>
|
353 |
+
</html>
|
templates/technical.html
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Technical Interview - Coding Challenge</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
|
8 |
+
</head>
|
9 |
+
<body class="bg-gray-100 flex justify-center items-center min-h-screen">
|
10 |
+
|
11 |
+
<div class="bg-white p-6 rounded-lg shadow-lg max-w-2xl w-full">
|
12 |
+
<h2 class="text-2xl font-bold mb-4">Technical Interview - Coding Challenge</h2>
|
13 |
+
<p id="codingProblem" class="text-gray-700 whitespace-pre-line">Loading...</p>
|
14 |
+
|
15 |
+
<div class="mt-4 flex justify-end">
|
16 |
+
<button onclick="postCode()" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
|
17 |
+
Post Code
|
18 |
+
</button>
|
19 |
+
<button onclick="closePage()" class="ml-2 px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600">
|
20 |
+
Close
|
21 |
+
</button>
|
22 |
+
</div>
|
23 |
+
</div>
|
24 |
+
|
25 |
+
<script>
|
26 |
+
// Fetch coding question from Flask API
|
27 |
+
function fetchCodingQuestion() {
|
28 |
+
fetch("/get_coding_question")
|
29 |
+
.then(response => response.json())
|
30 |
+
.then(data => {
|
31 |
+
document.getElementById("codingProblem").innerText = data.question;
|
32 |
+
})
|
33 |
+
.catch(error => {
|
34 |
+
console.error("Error fetching coding question:", error);
|
35 |
+
document.getElementById("codingProblem").innerText = "Error loading question.";
|
36 |
+
});
|
37 |
+
}
|
38 |
+
|
39 |
+
function postCode() {
|
40 |
+
alert("Posted Successfully!");
|
41 |
+
}
|
42 |
+
|
43 |
+
function closePage() {
|
44 |
+
window.history.back(); // Go back to the previous page
|
45 |
+
}
|
46 |
+
|
47 |
+
// Call function on page load
|
48 |
+
fetchCodingQuestion();
|
49 |
+
</script>
|
50 |
+
|
51 |
+
</body>
|
52 |
+
</html>
|