Muhammed Sameer commited on
Commit
cf06972
·
1 Parent(s): 4b3a33f

new feature implemented

Browse files
src/components/Admin/AdminInterviewManagement.jsx CHANGED
@@ -1,28 +1,173 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
  import CandidateDrawer from '../CandidateDrawer';
5
- import ScheduleInterviewModal from '../ScheduleInterviewModal'; // <--- IMPORT NEW MODAL
6
 
7
  // --- ICONS ---
8
  const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
9
  const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- // --- Simple Message Modal (Kept Inline) ---
12
- const MessageModal = ({ isOpen, onClose, onSend }) => {
13
- const [message, setMessage] = useState('');
14
- const handleSend = () => { if (message.trim()) { onSend(message); onClose(); } else { alert('Message cannot be empty.'); } };
15
- if (!isOpen) return null;
16
  return (
17
- <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 60 }}>
18
- <div style={{ width: '100%', maxWidth: '500px', backgroundColor: '#1f2937', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '1rem', padding: '2rem', color: 'white' }}>
19
- <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1.5rem' }}>Compose Message</h2>
20
- <textarea value={message} onChange={e => setMessage(e.target.value)} placeholder="Type here..." rows="6" style={{ width: '100%', padding: '0.75rem', borderRadius: '0.5rem', border: '1px solid rgba(255,255,255,0.2)', backgroundColor: 'rgba(255,255,255,0.05)', color: 'white', outline: 'none' }}></textarea>
21
- <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
22
- <button onClick={onClose} style={{ background: 'transparent', color: 'white', border: 'none', cursor: 'pointer' }}>Cancel</button>
23
- <button onClick={handleSend} style={{ backgroundColor: '#EF4444', color: 'white', padding: '0.5rem 1.5rem', borderRadius: '0.5rem', border: 'none', cursor: 'pointer' }}>Send</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  );
28
  };
@@ -34,7 +179,7 @@ export default function AdminInterviewManagement() {
34
  const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] });
35
 
36
  // Modals State
37
- const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); // <--- Updated Name
38
  const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
39
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
40
 
@@ -72,22 +217,17 @@ export default function AdminInterviewManagement() {
72
  // Combine skills and technical_skills
73
  const profileSkills = Array.isArray(app.profiles?.skills) ? app.profiles.skills : [];
74
  const profileTechSkills = Array.isArray(app.profiles?.technical_skills) ? app.profiles.technical_skills : [];
75
- // Handle comma-separated strings if they were stored as text in legacy rows
76
  const parsedTechSkills = (typeof app.profiles?.technical_skills === 'string')
77
  ? app.profiles.technical_skills.split(',').map(s => s.trim())
78
  : profileTechSkills;
79
 
80
  const combinedSkills = [...new Set([...profileSkills, ...parsedTechSkills])];
81
-
82
- // Fallback to application skills if profiles skills are empty
83
  const finalSkills = combinedSkills.length > 0
84
  ? combinedSkills
85
  : (Array.isArray(app.skills) ? app.skills : (app.skills ? [app.skills] : []));
86
 
87
- // Format the object for UI (Handle '0' values correctly)
88
  const formattedApp = {
89
  ...app,
90
- // Mapped fields for FullProfileOverlay
91
  full_name: app.profiles?.full_name,
92
  email: app.profiles?.email,
93
  phone: app.profiles?.phone,
@@ -105,12 +245,10 @@ export default function AdminInterviewManagement() {
105
  resumeUrl: app.resume_url || app.profiles?.resume_url,
106
  userId: app.profiles?.id || app.user_id,
107
  jobId: app.job_id,
108
- // FIX: Handle 0 experience
109
  experience: (app.experience === '0' || app.experience === 0)
110
  ? 'Fresher'
111
  : (app.experience ? `${app.experience} years` : 'N/A'),
112
  skills: finalSkills,
113
- // Use interview details if available
114
  interviewId: interviewData?.id,
115
  date: interviewData ? interviewData.date : 'Not Scheduled',
116
  time: interviewData ? interviewData.time : '',
@@ -118,13 +256,10 @@ export default function AdminInterviewManagement() {
118
 
119
  // --- SORTING LOGIC ---
120
  if (interviewData) {
121
- // HAS interview -> Interviews Tab
122
  categorized.interviews.push(formattedApp);
123
  } else if (app.status === 'Accepted' || app.status === 'Approved') {
124
- // Approved but NO interview -> Accepted Tab
125
  categorized.accepted.push(formattedApp);
126
  } else if (app.status === 'Rejected') {
127
- // Rejected -> Rejected Tab
128
  categorized.rejected.push(formattedApp);
129
  }
130
  });
@@ -137,11 +272,10 @@ export default function AdminInterviewManagement() {
137
  }
138
  };
139
 
140
- // 2. Updated Schedule Handler (Receives Object from Modal)
141
  const handleScheduleConfirm = async (scheduleData) => {
142
  if (!selectedApplicant) return;
143
 
144
- // Destructure data from modal
145
  const { date, time, interviewType, mode, details, interviewerName, interviewerRole } = scheduleData;
146
 
147
  try {
@@ -154,15 +288,13 @@ export default function AdminInterviewManagement() {
154
  status: 'Scheduled',
155
  interview_type: interviewType,
156
  mode: mode,
157
- // Conditionally save link or location
158
  meeting_link: mode === 'Online' ? details : null,
159
  location: mode === 'Offline' ? details : null,
160
  interviewer_name: interviewerName,
161
  interviewer_role: interviewerRole,
162
- duration_mins: 45 // Default
163
  };
164
 
165
- // Database Operation: Update if exists, otherwise Insert
166
  let dbError;
167
  if (selectedApplicant.interviewId) {
168
  const { error } = await supabase
@@ -179,7 +311,6 @@ export default function AdminInterviewManagement() {
179
 
180
  if (dbError) throw dbError;
181
 
182
- // Email Notification
183
  if (selectedApplicant.email) {
184
  await supabase.functions.invoke('send-interview-email', {
185
  body: {
@@ -201,12 +332,10 @@ export default function AdminInterviewManagement() {
201
  }
202
  };
203
 
204
- const handleSendMessage = (message) => { alert(`Message sent to ${selectedApplicant?.name}: "${message}"`); };
205
  const openScheduleModal = (applicant) => { setSelectedApplicant(applicant); setIsScheduleModalOpen(true); };
206
  const openMessageModal = (applicant) => { setSelectedApplicant(applicant); setIsMessageModalOpen(true); };
207
  const openDrawer = (applicant) => { setDrawerCandidate(applicant); setIsDrawerOpen(true); };
208
 
209
- // Styles
210
  const primaryButtonStyle = { backgroundColor: '#EF4444', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' };
211
  const secondaryButtonStyle = { backgroundColor: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' };
212
 
@@ -270,7 +399,7 @@ export default function AdminInterviewManagement() {
270
  </motion.div>
271
  </AnimatePresence>
272
 
273
- {/* --- MOUNT NEW MODAL --- */}
274
  <AnimatePresence>
275
  {isScheduleModalOpen && (
276
  <ScheduleInterviewModal
@@ -282,8 +411,26 @@ export default function AdminInterviewManagement() {
282
  )}
283
  </AnimatePresence>
284
 
285
- <AnimatePresence>{isMessageModalOpen && <MessageModal isOpen={isMessageModalOpen} onClose={() => setIsMessageModalOpen(false)} onSend={handleSendMessage} />}</AnimatePresence>
286
- <AnimatePresence>{isDrawerOpen && <CandidateDrawer isOpen={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} candidate={drawerCandidate} />}</AnimatePresence>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  </div>
288
  );
289
  }
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
  import CandidateDrawer from '../CandidateDrawer';
5
+ import ScheduleInterviewModal from '../ScheduleInterviewModal';
6
 
7
  // --- ICONS ---
8
  const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
9
  const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>);
10
+ const CloseIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>;
11
+ const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>;
12
+
13
+ // --- NEW FULL CHAT UI MODAL ---
14
+ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
15
+ const [messages, setMessages] = useState([]);
16
+ const [text, setText] = useState('');
17
+ const [loading, setLoading] = useState(true);
18
+ const [adminId, setAdminId] = useState(null);
19
+ const messagesEndRef = useRef(null);
20
+
21
+ // Fetch chat history
22
+ useEffect(() => {
23
+ if (!isOpen || !applicant?.userId) return;
24
+
25
+ const fetchMessages = async () => {
26
+ setLoading(true);
27
+ try {
28
+ const { data: { user } } = await supabase.auth.getUser();
29
+ if (!user) return;
30
+ setAdminId(user.id);
31
+
32
+ // Fetch conversation strictly between this Admin and this Applicant
33
+ const { data, error } = await supabase
34
+ .from('messages')
35
+ .select('*')
36
+ .or(`and(sender_id.eq.${user.id},receiver_id.eq.${applicant.userId}),and(sender_id.eq.${applicant.userId},receiver_id.eq.${user.id})`)
37
+ .order('created_at', { ascending: true });
38
+
39
+ if (!error && data) {
40
+ setMessages(data);
41
+ }
42
+ } catch (err) {
43
+ console.error("Error fetching chat:", err);
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+ fetchMessages();
49
+ }, [isOpen, applicant]);
50
+
51
+ // Auto-scroll to bottom of chat
52
+ useEffect(() => {
53
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
54
+ }, [messages]);
55
+
56
+ const sendMsg = async () => {
57
+ if (!text.trim() || !adminId || !applicant?.userId) return;
58
+
59
+ const newMsg = {
60
+ id: Date.now(), // Temporary ID for optimistic UI update
61
+ sender_id: adminId,
62
+ receiver_id: applicant.userId,
63
+ content: text.trim(),
64
+ created_at: new Date().toISOString()
65
+ };
66
+
67
+ // Update UI instantly
68
+ setMessages(prev => [...prev, newMsg]);
69
+ setText('');
70
+
71
+ // Send to database
72
+ const { error } = await supabase.from('messages').insert([{
73
+ sender_id: adminId,
74
+ receiver_id: applicant.userId,
75
+ content: newMsg.content
76
+ }]);
77
+
78
+ if (error) console.error("Error sending message:", error);
79
+ };
80
+
81
+ if (!isOpen || !applicant) return null;
82
 
 
 
 
 
 
83
  return (
84
+ <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
85
+ <style>{`.chat-scroll::-webkit-scrollbar { width: 6px; } .chat-scroll::-webkit-scrollbar-track { background: transparent; } .chat-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }`}</style>
86
+
87
+ <motion.div
88
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
89
+ animate={{ opacity: 1, scale: 1, y: 0 }}
90
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
91
+ style={{
92
+ width: '100%', maxWidth: '600px', height: '75vh',
93
+ background: 'rgba(15, 23, 42, 0.95)', border: '1px solid rgba(255,255,255,0.1)',
94
+ borderRadius: '1.25rem', display: 'flex', flexDirection: 'column',
95
+ overflow: 'hidden', boxShadow: '0 25px 50px rgba(0,0,0,0.5)'
96
+ }}
97
+ >
98
+ {/* Chat Header */}
99
+ <div style={{ padding: '1.25rem', borderBottom: '1px solid rgba(255,255,255,0.08)', background: 'rgba(0,0,0,0.2)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
100
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
101
+ <img src={applicant.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(applicant.name)}&background=random`} alt={applicant.name} style={{ width: '40px', height: '40px', borderRadius: '50%', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.1)' }} />
102
+ <div>
103
+ <h3 style={{ margin: 0, color: 'white', fontSize: '1.1rem', fontWeight: 'bold' }}>{applicant.name}</h3>
104
+ <p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>{applicant.role}</p>
105
+ </div>
106
+ </div>
107
+ <button onClick={onClose} style={{ background: 'rgba(255,255,255,0.05)', border: 'none', color: '#94a3b8', cursor: 'pointer', padding: '0.5rem', borderRadius: '50%', display: 'flex' }}>
108
+ <CloseIcon />
109
+ </button>
110
  </div>
111
+
112
+ {/* Chat History */}
113
+ <div className="chat-scroll" style={{ flex: 1, padding: '1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
114
+ {loading ? (
115
+ <div style={{ textAlign: 'center', color: '#64748b', marginTop: '2rem' }}>Loading conversation...</div>
116
+ ) : messages.length === 0 ? (
117
+ <div style={{ textAlign: 'center', color: '#64748b', marginTop: '2rem', fontSize: '0.9rem' }}>
118
+ No previous messages. Start the conversation!
119
+ </div>
120
+ ) : (
121
+ messages.map(m => {
122
+ const isMe = m.sender_id === adminId;
123
+ return (
124
+ <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '75%' }}>
125
+ <div style={{
126
+ // Use Admin's Red Theme for outgoing bubbles
127
+ background: isMe ? 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' : 'rgba(255,255,255,0.08)',
128
+ color: 'white', padding: '0.85rem 1.25rem', borderRadius: '1.25rem',
129
+ borderBottomRightRadius: isMe ? 4 : '1.25rem', borderBottomLeftRadius: isMe ? '1.25rem' : 4,
130
+ fontSize: '0.95rem', lineHeight: '1.5',
131
+ boxShadow: isMe ? '0 4px 15px rgba(239, 68, 68, 0.3)' : 'none'
132
+ }}>
133
+ {m.content}
134
+ </div>
135
+ <div style={{ fontSize: '0.7rem', color: '#64748b', marginTop: 4, textAlign: isMe ? 'right' : 'left', padding: '0 0.5rem' }}>
136
+ {new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
137
+ </div>
138
+ </motion.div>
139
+ );
140
+ })
141
+ )}
142
+ <div ref={messagesEndRef} />
143
+ </div>
144
+
145
+ {/* Input Area */}
146
+ <div style={{ padding: '1.25rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
147
+ <div style={{ display: 'flex', gap: '0.75rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
148
+ <textarea
149
+ value={text}
150
+ onChange={e => setText(e.target.value)}
151
+ onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}}
152
+ placeholder="Type a message..."
153
+ style={{ flex: 1, padding: '0.75rem', border: 'none', background: 'transparent', color: 'white', outline: 'none', resize: 'none', height: '45px', fontFamily: 'inherit' }}
154
+ />
155
+ <button
156
+ onClick={sendMsg}
157
+ disabled={!text.trim()}
158
+ style={{
159
+ background: text.trim() ? '#EF4444' : 'rgba(255,255,255,0.05)',
160
+ color: text.trim() ? 'white' : '#64748b', border: 'none', borderRadius: '50%',
161
+ width: '45px', height: '45px', cursor: text.trim() ? 'pointer' : 'not-allowed',
162
+ display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
163
+ transition: 'all 0.2s ease'
164
+ }}
165
+ >
166
+ <SendIcon />
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </motion.div>
171
  </div>
172
  );
173
  };
 
179
  const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] });
180
 
181
  // Modals State
182
+ const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
183
  const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
184
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
185
 
 
217
  // Combine skills and technical_skills
218
  const profileSkills = Array.isArray(app.profiles?.skills) ? app.profiles.skills : [];
219
  const profileTechSkills = Array.isArray(app.profiles?.technical_skills) ? app.profiles.technical_skills : [];
 
220
  const parsedTechSkills = (typeof app.profiles?.technical_skills === 'string')
221
  ? app.profiles.technical_skills.split(',').map(s => s.trim())
222
  : profileTechSkills;
223
 
224
  const combinedSkills = [...new Set([...profileSkills, ...parsedTechSkills])];
 
 
225
  const finalSkills = combinedSkills.length > 0
226
  ? combinedSkills
227
  : (Array.isArray(app.skills) ? app.skills : (app.skills ? [app.skills] : []));
228
 
 
229
  const formattedApp = {
230
  ...app,
 
231
  full_name: app.profiles?.full_name,
232
  email: app.profiles?.email,
233
  phone: app.profiles?.phone,
 
245
  resumeUrl: app.resume_url || app.profiles?.resume_url,
246
  userId: app.profiles?.id || app.user_id,
247
  jobId: app.job_id,
 
248
  experience: (app.experience === '0' || app.experience === 0)
249
  ? 'Fresher'
250
  : (app.experience ? `${app.experience} years` : 'N/A'),
251
  skills: finalSkills,
 
252
  interviewId: interviewData?.id,
253
  date: interviewData ? interviewData.date : 'Not Scheduled',
254
  time: interviewData ? interviewData.time : '',
 
256
 
257
  // --- SORTING LOGIC ---
258
  if (interviewData) {
 
259
  categorized.interviews.push(formattedApp);
260
  } else if (app.status === 'Accepted' || app.status === 'Approved') {
 
261
  categorized.accepted.push(formattedApp);
262
  } else if (app.status === 'Rejected') {
 
263
  categorized.rejected.push(formattedApp);
264
  }
265
  });
 
272
  }
273
  };
274
 
275
+ // 2. Updated Schedule Handler
276
  const handleScheduleConfirm = async (scheduleData) => {
277
  if (!selectedApplicant) return;
278
 
 
279
  const { date, time, interviewType, mode, details, interviewerName, interviewerRole } = scheduleData;
280
 
281
  try {
 
288
  status: 'Scheduled',
289
  interview_type: interviewType,
290
  mode: mode,
 
291
  meeting_link: mode === 'Online' ? details : null,
292
  location: mode === 'Offline' ? details : null,
293
  interviewer_name: interviewerName,
294
  interviewer_role: interviewerRole,
295
+ duration_mins: 45
296
  };
297
 
 
298
  let dbError;
299
  if (selectedApplicant.interviewId) {
300
  const { error } = await supabase
 
311
 
312
  if (dbError) throw dbError;
313
 
 
314
  if (selectedApplicant.email) {
315
  await supabase.functions.invoke('send-interview-email', {
316
  body: {
 
332
  }
333
  };
334
 
 
335
  const openScheduleModal = (applicant) => { setSelectedApplicant(applicant); setIsScheduleModalOpen(true); };
336
  const openMessageModal = (applicant) => { setSelectedApplicant(applicant); setIsMessageModalOpen(true); };
337
  const openDrawer = (applicant) => { setDrawerCandidate(applicant); setIsDrawerOpen(true); };
338
 
 
339
  const primaryButtonStyle = { backgroundColor: '#EF4444', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' };
340
  const secondaryButtonStyle = { backgroundColor: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' };
341
 
 
399
  </motion.div>
400
  </AnimatePresence>
401
 
402
+ {/* --- MODALS --- */}
403
  <AnimatePresence>
404
  {isScheduleModalOpen && (
405
  <ScheduleInterviewModal
 
411
  )}
412
  </AnimatePresence>
413
 
414
+ {/* NEW FULL CHAT MODAL */}
415
+ <AnimatePresence>
416
+ {isMessageModalOpen && (
417
+ <AdminChatModal
418
+ isOpen={isMessageModalOpen}
419
+ onClose={() => setIsMessageModalOpen(false)}
420
+ applicant={selectedApplicant}
421
+ />
422
+ )}
423
+ </AnimatePresence>
424
+
425
+ <AnimatePresence>
426
+ {isDrawerOpen && (
427
+ <CandidateDrawer
428
+ isOpen={isDrawerOpen}
429
+ onClose={() => setIsDrawerOpen(false)}
430
+ candidate={drawerCandidate}
431
+ />
432
+ )}
433
+ </AnimatePresence>
434
  </div>
435
  );
436
  }
src/components/ApplicantLayout.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import {
@@ -6,12 +6,36 @@ import {
6
  CalendarIcon, AtsCheckerIcon
7
  } from './Icons';
8
 
 
 
 
 
 
 
 
 
9
  export default function ApplicantLayout({ children, activePage, onNavigate }) {
10
 
11
  // ✅ FIX: Initialize state directly from LocalStorage.
12
  // This removes the "flicker" because it grabs the name instantly before the page paints.
13
  const [userName, setUserName] = useState(() => localStorage.getItem('applicant_name') || '');
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  useEffect(() => {
16
  const fetchUserName = async () => {
17
  try {
@@ -40,6 +64,40 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
40
  fetchUserName();
41
  }, []);
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  const handleLogout = async () => {
44
  await supabase.auth.signOut();
45
  localStorage.removeItem('applicant_name'); // ✅ Clear cache so next user doesn't see your name
@@ -74,15 +132,86 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
74
  {userName ? `Hi, ${userName} 👋` : 'Welcome 👋'}
75
  </h1>
76
 
77
- <motion.button
78
- onClick={handleLogout}
79
- whileHover={{ scale: 1.03 }}
80
- whileTap={{ scale: 0.98 }}
81
- style={{ backgroundColor: '#FBBF24', color: '#1a202c', display: 'flex', alignItems: 'center', padding: '0.75rem', borderRadius: '0.5rem', fontWeight: 'bold', cursor: 'pointer', border: 'none' }}
82
- >
83
- <LogoutIcon />
84
- Logout
85
- </motion.button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </header>
87
 
88
  {/* Navigation Bar */}
 
1
+ import React, { useState, useEffect, useRef } from 'react'; // ✅ ADDED useRef
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../supabaseClient';
4
  import {
 
6
  CalendarIcon, AtsCheckerIcon
7
  } from './Icons';
8
 
9
+ // ✅ ADDED: Bell Icon SVG
10
+ const BellIcon = () => (
11
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
12
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
13
+ <path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
14
+ </svg>
15
+ );
16
+
17
  export default function ApplicantLayout({ children, activePage, onNavigate }) {
18
 
19
  // ✅ FIX: Initialize state directly from LocalStorage.
20
  // This removes the "flicker" because it grabs the name instantly before the page paints.
21
  const [userName, setUserName] = useState(() => localStorage.getItem('applicant_name') || '');
22
 
23
+ // ✅ ADDED: Notification States
24
+ const [notifications, setNotifications] = useState([]);
25
+ const [showNotifications, setShowNotifications] = useState(false);
26
+ const notifRef = useRef(null);
27
+
28
+ // ✅ ADDED: Close dropdown when clicking outside
29
+ useEffect(() => {
30
+ function handleClickOutside(event) {
31
+ if (notifRef.current && !notifRef.current.contains(event.target)) {
32
+ setShowNotifications(false);
33
+ }
34
+ }
35
+ document.addEventListener("mousedown", handleClickOutside);
36
+ return () => document.removeEventListener("mousedown", handleClickOutside);
37
+ }, [notifRef]);
38
+
39
  useEffect(() => {
40
  const fetchUserName = async () => {
41
  try {
 
64
  fetchUserName();
65
  }, []);
66
 
67
+ // ✅ ADDED: Fetch notifications exclusively for the profile page
68
+ useEffect(() => {
69
+ const fetchNotifications = async () => {
70
+ if (activePage === 'applicant-profile') {
71
+ try {
72
+ const { data: { user } } = await supabase.auth.getUser();
73
+ if (user) {
74
+ const { data: apps } = await supabase
75
+ .from('applications')
76
+ .select('id, status, jobs(title), updated_at')
77
+ .eq('user_id', user.id)
78
+ .in('status', ['Accepted', 'Rejected', 'Interviewing'])
79
+ .order('updated_at', { ascending: false })
80
+ .limit(5);
81
+
82
+ if (apps) {
83
+ const notifs = apps.map(app => ({
84
+ id: app.id,
85
+ title: `Application ${app.status}`,
86
+ text: `Your application for ${app.jobs?.title} is now ${app.status}.`,
87
+ time: app.updated_at,
88
+ color: app.status === 'Accepted' ? '#10b981' : app.status === 'Rejected' ? '#ef4444' : '#FBBF24'
89
+ }));
90
+ setNotifications(notifs);
91
+ }
92
+ }
93
+ } catch (error) {
94
+ console.error("Error fetching notifications:", error);
95
+ }
96
+ }
97
+ };
98
+ fetchNotifications();
99
+ }, [activePage]); // Runs when activePage changes
100
+
101
  const handleLogout = async () => {
102
  await supabase.auth.signOut();
103
  localStorage.removeItem('applicant_name'); // ✅ Clear cache so next user doesn't see your name
 
132
  {userName ? `Hi, ${userName} 👋` : 'Welcome 👋'}
133
  </h1>
134
 
135
+ {/* ✅ ADDED: Group wrapper to place Bell and Logout together */}
136
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
137
+
138
+ {/* ✅ ADDED: Conditionally render Bell ONLY on profile page */}
139
+ {activePage === 'applicant-profile' && (
140
+ <div style={{ position: 'relative' }} ref={notifRef}>
141
+ <motion.button
142
+ whileHover={{ scale: 1.05 }}
143
+ whileTap={{ scale: 0.95 }}
144
+ onClick={() => setShowNotifications(!showNotifications)}
145
+ style={{
146
+ background: 'rgba(255,255,255,0.05)',
147
+ border: '1px solid rgba(255,255,255,0.1)',
148
+ borderRadius: '50%',
149
+ width: '45px', height: '45px',
150
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
151
+ color: '#FCD34D', cursor: 'pointer', position: 'relative',
152
+ padding: 0
153
+ }}
154
+ >
155
+ <BellIcon />
156
+ {notifications.length > 0 && (
157
+ <span style={{
158
+ position: 'absolute', top: '0px', right: '0px',
159
+ width: '12px', height: '12px',
160
+ backgroundColor: '#FBBF24', // Yellow dot
161
+ borderRadius: '50%',
162
+ border: '2px solid #020617'
163
+ }}></span>
164
+ )}
165
+ </motion.button>
166
+
167
+ {/* Dropdown */}
168
+ <AnimatePresence>
169
+ {showNotifications && (
170
+ <motion.div
171
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
172
+ animate={{ opacity: 1, y: 0, scale: 1 }}
173
+ exit={{ opacity: 0, y: 10, scale: 0.95 }}
174
+ style={{
175
+ position: 'absolute', top: '55px', right: '0', width: '320px',
176
+ backgroundColor: '#1e293b', border: '1px solid rgba(255,255,255,0.1)',
177
+ borderRadius: '12px', boxShadow: '0 10px 25px rgba(0,0,0,0.5)', zIndex: 50, overflow: 'hidden'
178
+ }}
179
+ >
180
+ <div style={{ padding: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', fontWeight: 'bold', fontSize: '0.9rem' }}>
181
+ Notifications
182
+ </div>
183
+ <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
184
+ {notifications.length > 0 ? (
185
+ notifications.map(notif => (
186
+ <div key={notif.id} style={{ padding: '1rem', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', gap: '0.75rem', alignItems: 'start' }}>
187
+ <div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: notif.color, marginTop: '6px', flexShrink: 0 }}></div>
188
+ <div>
189
+ <p style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0 0 0.25rem 0', fontWeight: 'bold' }}>{notif.title}</p>
190
+ <p style={{ fontSize: '0.8rem', color: '#cbd5e1', margin: '0 0 0.25rem 0' }}>{notif.text}</p>
191
+ <p style={{ fontSize: '0.7rem', color: '#94a3b8', margin: 0 }}>{new Date(notif.time).toLocaleDateString()}</p>
192
+ </div>
193
+ </div>
194
+ ))
195
+ ) : (
196
+ <div style={{ padding: '2rem', textAlign: 'center', color: '#64748b', fontSize: '0.85rem' }}>No new notifications</div>
197
+ )}
198
+ </div>
199
+ </motion.div>
200
+ )}
201
+ </AnimatePresence>
202
+ </div>
203
+ )}
204
+
205
+ <motion.button
206
+ onClick={handleLogout}
207
+ whileHover={{ scale: 1.03 }}
208
+ whileTap={{ scale: 0.98 }}
209
+ style={{ backgroundColor: '#FBBF24', color: '#1a202c', display: 'flex', alignItems: 'center', padding: '0.75rem', borderRadius: '0.5rem', fontWeight: 'bold', cursor: 'pointer', border: 'none' }}
210
+ >
211
+ <LogoutIcon />
212
+ Logout
213
+ </motion.button>
214
+ </div>
215
  </header>
216
 
217
  {/* Navigation Bar */}
src/pages/ApplicantMessages.jsx CHANGED
@@ -1,16 +1,122 @@
1
- import React from 'react';
 
 
2
  import ApplicantLayout from '../components/ApplicantLayout';
3
  import PlaceholderContent from '../components/PlaceholderContent';
4
- import { ChatIcon } from '../components/Icons';
 
 
 
 
 
 
 
5
 
6
  export default function ApplicantMessages({ onNavigate }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  return (
8
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
9
- <PlaceholderContent
10
- title="No messages yet"
11
- message="You'll see notifications here when you receive updates."
12
- icon={<ChatIcon />}
13
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  </ApplicantLayout>
15
  );
16
  }
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { supabase } from '../supabaseClient';
4
  import ApplicantLayout from '../components/ApplicantLayout';
5
  import PlaceholderContent from '../components/PlaceholderContent';
6
+ import { ChatIcon, SearchIcon } from '../components/Icons';
7
+
8
+ const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>;
9
+ const Avatar = ({ name }) => <div style={{ width: 45, height: 45, borderRadius: '50%', background: 'linear-gradient(135deg, #374151, #1f2937)', border: '2px solid rgba(255,255,255,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', color: 'white', flexShrink: 0 }}>{name ? name[0].toUpperCase() : 'U'}</div>;
10
+
11
+ // Shared Styles to save lines
12
+ const glassStyle = { background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(16px)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', overflow: 'hidden' };
13
+ const inputStyle = { width: '100%', padding: '0.8rem', background: 'transparent', border: 'none', color: 'white', outline: 'none', fontSize: '0.95rem' };
14
 
15
  export default function ApplicantMessages({ onNavigate }) {
16
+ const [threads, setThreads] = useState([]);
17
+ const [selected, setSelected] = useState(null);
18
+ const [text, setText] = useState('');
19
+ const [search, setSearch] = useState('');
20
+
21
+ useEffect(() => {
22
+ supabase.auth.getUser().then(({ data: { user } }) => {
23
+ // Precise Mock Data (Replace with real DB fetch later)
24
+ setThreads([{
25
+ id: '1', name: 'HR Team - Iris', subj: 'Application Update: Software Engineer',
26
+ last: 'Please let us know your availability.', unread: true, time: new Date().toISOString(),
27
+ msgs: [
28
+ { id: 1, text: 'Hi, your profile looks impressive!', time: '2 days ago', isMe: false },
29
+ { id: 2, text: 'Thank you! Excited for the opportunity.', time: '1 day ago', isMe: true },
30
+ { id: 3, text: 'Please let us know your availability for an interview.', time: '1 hour ago', isMe: false }
31
+ ]
32
+ }]);
33
+ });
34
+ }, []);
35
+
36
+ const sendMsg = () => {
37
+ if (!text.trim() || !selected) return;
38
+ const upd = { ...selected, msgs: [...selected.msgs, { id: Date.now(), text, time: 'Just now', isMe: true }], last: text };
39
+ setSelected(upd);
40
+ setThreads(threads.map(t => t.id === upd.id ? upd : t));
41
+ setText('');
42
+ };
43
+
44
+ const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()) || t.subj.toLowerCase().includes(search.toLowerCase()));
45
+
46
  return (
47
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
48
+ <style>{`.hide-scroll::-webkit-scrollbar { width: 0px; }`}</style>
49
+
50
+ <div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
51
+
52
+ {/* LEFT PANE: THREAD LIST */}
53
+ <div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
54
+ <div style={{ padding: '1.5rem' }}>
55
+ <h2 style={{ fontSize: '1.4rem', fontWeight: 'bold', marginBottom: '1rem' }}>Inbox</h2>
56
+ <div style={{ position: 'relative', background: 'rgba(0,0,0,0.3)', borderRadius: '0.75rem', border: '1px solid rgba(255,255,255,0.05)' }}>
57
+ <div style={{ position: 'absolute', left: 14, top: 12, color: '#64748b' }}><SearchIcon /></div>
58
+ <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search..." style={{ ...inputStyle, paddingLeft: '2.5rem' }} />
59
+ </div>
60
+ </div>
61
+
62
+ <div className="hide-scroll" style={{ flex: 1, overflowY: 'auto', padding: '0 0.75rem 1rem' }}>
63
+ {filtered.map(t => {
64
+ const isAct = selected?.id === t.id;
65
+ return (
66
+ <motion.div
67
+ key={t.id} onClick={() => { setSelected(t); setThreads(threads.map(x => x.id === t.id ? { ...x, unread: false } : x)); }}
68
+ whileHover={{ scale: 0.98 }} style={{ padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem', cursor: 'pointer', background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent', border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent' }}
69
+ >
70
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: isAct ? '#FCD34D' : 'white', fontWeight: 'bold' }}>
71
+ {t.name} <span style={{ fontSize: '0.75rem', color: t.unread ? '#FCD34D' : '#64748b' }}>{new Date(t.time).toLocaleDateString([],{month:'short', day:'numeric'})}</span>
72
+ </div>
73
+ <div style={{ fontSize: '0.85rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
74
+ <div style={{ fontSize: '0.8rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
75
+ </motion.div>
76
+ );
77
+ })}
78
+ </div>
79
+ </div>
80
+
81
+ {/* RIGHT PANE: ACTIVE CHAT */}
82
+ <div style={{ ...glassStyle, flex: 1 }}>
83
+ {selected ? (
84
+ <>
85
+ {/* Chat Header */}
86
+ <div style={{ padding: '1.25rem 2rem', background: 'rgba(0,0,0,0.2)', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', gap: '1rem' }}>
87
+ <Avatar name={selected.name} />
88
+ <div><h3 style={{ margin: 0, color: 'white' }}>{selected.name}</h3><p style={{ margin: 0, fontSize: '0.85rem', color: '#10b981' }}>Active Now</p></div>
89
+ </div>
90
+
91
+ {/* Chat History */}
92
+ <div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
93
+ {selected.msgs.map(m => (
94
+ <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: m.isMe ? 'flex-end' : 'flex-start', maxWidth: '70%' }}>
95
+ <div style={{ background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)', color: m.isMe ? '#020617' : 'white', padding: '1rem 1.25rem', borderRadius: '1.25rem', borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4 }}>
96
+ {m.text}
97
+ </div>
98
+ <div style={{ fontSize: '0.7rem', color: '#64748b', marginTop: 4, textAlign: m.isMe ? 'right' : 'left' }}>{m.time}</div>
99
+ </motion.div>
100
+ ))}
101
+ </div>
102
+
103
+ {/* Input Area */}
104
+ <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
105
+ <div style={{ display: 'flex', gap: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
106
+ <textarea value={text} onChange={e => setText(e.target.value)} onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}} placeholder="Write a message..." style={{ ...inputStyle, resize: 'none', height: '45px' }} />
107
+ <button onClick={sendMsg} disabled={!text.trim()} style={{ background: text.trim() ? '#FBBF24' : 'rgba(255,255,255,0.05)', color: text.trim() ? '#020617' : '#64748b', border: 'none', borderRadius: '50%', width: 45, height: 45, cursor: text.trim() ? 'pointer' : 'not-allowed' }}>
108
+ <SendIcon />
109
+ </button>
110
+ </div>
111
+ </div>
112
+ </>
113
+ ) : (
114
+ <div style={{ display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center' }}>
115
+ <PlaceholderContent title="Your Messages" message="Select a conversation to start chatting." icon={<ChatIcon />} />
116
+ </div>
117
+ )}
118
+ </div>
119
+ </div>
120
  </ApplicantLayout>
121
  );
122
  }