sameer2026 commited on
Commit
b66d14f
·
1 Parent(s): 9d6cc86

Fix messaging UI alignment, unsend functionality, and applicant notification bugs

Browse files
src/components/Admin/AdminInterviewManagement.jsx CHANGED
@@ -2,13 +2,13 @@ import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
  import FullProfileOverlay from '../FullProfileOverlay';
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 }) => {
@@ -78,20 +78,56 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
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
  >
@@ -121,19 +157,26 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
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
  );
@@ -145,19 +188,19 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
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'
@@ -179,7 +222,7 @@ export default function AdminInterviewManagement() {
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
 
@@ -271,8 +314,8 @@ export default function AdminInterviewManagement() {
271
  };
272
 
273
  // 1. Fetch Data on Component Mount
274
- useEffect(() => {
275
- fetchData();
276
  }, []);
277
 
278
  // 2. Updated Schedule Handler
@@ -314,7 +357,7 @@ export default function AdminInterviewManagement() {
314
  location: mode === 'Offline' ? details : null,
315
  interviewer_name: interviewerName,
316
  interviewer_role: interviewerRole,
317
- duration_mins: 45
318
  };
319
 
320
  let dbError;
@@ -444,19 +487,19 @@ export default function AdminInterviewManagement() {
444
 
445
  <AnimatePresence>
446
  {isMessageModalOpen && (
447
- <AdminChatModal
448
- isOpen={isMessageModalOpen}
449
- onClose={() => setIsMessageModalOpen(false)}
450
- applicant={selectedApplicant}
451
  />
452
  )}
453
  </AnimatePresence>
454
-
455
  <AnimatePresence>
456
  {isDrawerOpen && (
457
- <FullProfileOverlay
458
- candidate={drawerCandidate}
459
- onClose={() => setIsDrawerOpen(false)}
460
  />
461
  )}
462
  </AnimatePresence>
 
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
  import FullProfileOverlay from '../FullProfileOverlay';
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 }) => {
 
78
  if (error) console.error("Error sending message:", error);
79
  };
80
 
81
+ const unsendMsg = async (msgId) => {
82
+ if (!window.confirm("Unsend this message?")) return;
83
+
84
+ // 1. Try to delete the message
85
+ const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select();
86
+
87
+ if (error) {
88
+ console.error("Unsend Error:", error);
89
+ alert("Failed to unsend message.");
90
+ return;
91
+ }
92
+
93
+ // 2. If RLS silently blocks deletion, fallback to update
94
+ if (!data || data.length === 0) {
95
+ const { error: updateError } = await supabase
96
+ .from('messages')
97
+ .update({ content: "🚫 This message was unsent" })
98
+ .eq('id', msgId);
99
+
100
+ if (updateError) {
101
+ alert("Database policies prevent unsending this message.");
102
+ return;
103
+ }
104
+
105
+ setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: "🚫 This message was unsent" } : m));
106
+ } else {
107
+ setMessages(prev => prev.filter(m => m.id !== msgId));
108
+ }
109
+ };
110
+
111
+ const canUnsend = (rawTime) => {
112
+ if (!rawTime) return false;
113
+ const msgTime = new Date(rawTime).getTime();
114
+ return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes
115
+ };
116
+
117
  if (!isOpen || !applicant) return null;
118
 
119
  return (
120
  <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
121
  <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>
122
+
123
+ <motion.div
124
  initial={{ opacity: 0, scale: 0.95, y: 20 }}
125
  animate={{ opacity: 1, scale: 1, y: 0 }}
126
  exit={{ opacity: 0, scale: 0.95, y: 20 }}
127
+ style={{
128
+ width: '100%', maxWidth: '600px', height: '75vh',
129
+ background: 'rgba(15, 23, 42, 0.95)', border: '1px solid rgba(255,255,255,0.1)',
130
+ borderRadius: '1.25rem', display: 'flex', flexDirection: 'column',
131
  overflow: 'hidden', boxShadow: '0 25px 50px rgba(0,0,0,0.5)'
132
  }}
133
  >
 
157
  messages.map(m => {
158
  const isMe = m.sender_id === adminId;
159
  return (
160
+ <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '75%', position: 'relative' }}>
161
+ <div style={{
162
  // Use Admin's Red Theme for outgoing bubbles
163
+ background: isMe ? 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' : 'rgba(255,255,255,0.08)',
164
+ color: 'white', padding: '0.85rem 1.25rem', borderRadius: '1.25rem',
165
  borderBottomRightRadius: isMe ? 4 : '1.25rem', borderBottomLeftRadius: isMe ? '1.25rem' : 4,
166
  fontSize: '0.95rem', lineHeight: '1.5',
167
  boxShadow: isMe ? '0 4px 15px rgba(239, 68, 68, 0.3)' : 'none'
168
  }}>
169
  {m.content}
170
  </div>
171
+ <div style={{ display: 'flex', justifyContent: isMe ? 'flex-end' : 'flex-start', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
172
+ <div style={{ fontSize: '0.7rem', color: '#64748b', padding: '0 0.5rem' }}>
173
+ {new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
174
+ </div>
175
+ {isMe && canUnsend(m.created_at) && (
176
+ <button onClick={() => unsendMsg(m.id)} title="Unsend message (5 min window)" style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '0 2px', display: 'flex', alignItems: 'center' }}>
177
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
178
+ </button>
179
+ )}
180
  </div>
181
  </motion.div>
182
  );
 
188
  {/* Input Area */}
189
  <div style={{ padding: '1.25rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
190
  <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)' }}>
191
+ <textarea
192
+ value={text}
193
+ onChange={e => setText(e.target.value)}
194
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }}
195
+ placeholder="Type a message..."
196
+ style={{ flex: 1, padding: '0.75rem', border: 'none', background: 'transparent', color: 'white', outline: 'none', resize: 'none', height: '45px', fontFamily: 'inherit' }}
197
  />
198
+ <button
199
+ onClick={sendMsg}
200
+ disabled={!text.trim()}
201
+ style={{
202
+ background: text.trim() ? '#EF4444' : 'rgba(255,255,255,0.05)',
203
+ color: text.trim() ? 'white' : '#64748b', border: 'none', borderRadius: '50%',
204
  width: '45px', height: '45px', cursor: text.trim() ? 'pointer' : 'not-allowed',
205
  display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
206
  transition: 'all 0.2s ease'
 
222
  const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] });
223
 
224
  // Modals State
225
+ const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
226
  const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
227
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
228
 
 
314
  };
315
 
316
  // 1. Fetch Data on Component Mount
317
+ useEffect(() => {
318
+ fetchData();
319
  }, []);
320
 
321
  // 2. Updated Schedule Handler
 
357
  location: mode === 'Offline' ? details : null,
358
  interviewer_name: interviewerName,
359
  interviewer_role: interviewerRole,
360
+ duration_mins: 45
361
  };
362
 
363
  let dbError;
 
487
 
488
  <AnimatePresence>
489
  {isMessageModalOpen && (
490
+ <AdminChatModal
491
+ isOpen={isMessageModalOpen}
492
+ onClose={() => setIsMessageModalOpen(false)}
493
+ applicant={selectedApplicant}
494
  />
495
  )}
496
  </AnimatePresence>
497
+
498
  <AnimatePresence>
499
  {isDrawerOpen && (
500
+ <FullProfileOverlay
501
+ candidate={drawerCandidate}
502
+ onClose={() => setIsDrawerOpen(false)}
503
  />
504
  )}
505
  </AnimatePresence>
src/components/ApplicantLayout.jsx CHANGED
@@ -171,6 +171,13 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
171
  async (payload) => {
172
  console.log("=== MESSAGES PAYLOAD ===", payload);
173
 
 
 
 
 
 
 
 
174
  // Fetch the sender's name from user_roles/companies
175
  let senderName = 'Admin / HR';
176
  const { data: roleData } = await supabase
 
171
  async (payload) => {
172
  console.log("=== MESSAGES PAYLOAD ===", payload);
173
 
174
+ // Ignore applicant's own manual replies to the system thread
175
+ if (payload.new.sender_id === user.id) {
176
+ if (!payload.new.content || !payload.new.content.startsWith("Hello, Thank you for applying")) {
177
+ return;
178
+ }
179
+ }
180
+
181
  // Fetch the sender's name from user_roles/companies
182
  let senderName = 'Admin / HR';
183
  const { data: roleData } = await supabase
src/components/JobListings.jsx CHANGED
@@ -48,15 +48,15 @@ export default function JobListings({ searchQuery, setSearchQuery, isSearching,
48
  // ✅ Helper: Send welcome message to applicant
49
  const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
50
  try {
51
- const { data: { user: adminUser } } = await supabase.auth.getUser();
52
- if (!adminUser) return;
53
-
54
  const message = `Hello, Thank you for applying for the **${jobTitle}** position. We have received your application and our team is currently reviewing your profile. If your qualifications match our requirements, we will contact you shortly regarding the next steps in the selection process. We appreciate your interest in this opportunity.`;
55
 
 
 
56
  const { error } = await supabase.from('messages').insert([{
57
- sender_id: adminUser.id,
58
  receiver_id: userId,
59
- content: message
 
60
  }]);
61
 
62
  if (error) console.error('Error sending confirmation message:', error);
 
48
  // ✅ Helper: Send welcome message to applicant
49
  const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
50
  try {
 
 
 
51
  const message = `Hello, Thank you for applying for the **${jobTitle}** position. We have received your application and our team is currently reviewing your profile. If your qualifications match our requirements, we will contact you shortly regarding the next steps in the selection process. We appreciate your interest in this opportunity.`;
52
 
53
+ // Insert message using applicant's ID for both sender and receiver
54
+ // (RLS prevents applicants from inserting messages on behalf of an Admin)
55
  const { error } = await supabase.from('messages').insert([{
56
+ sender_id: userId,
57
  receiver_id: userId,
58
+ content: message,
59
+ is_read: false
60
  }]);
61
 
62
  if (error) console.error('Error sending confirmation message:', error);
src/pages/ApplicantMessages.jsx CHANGED
@@ -3,22 +3,22 @@ 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
  // --- Inline UI Components for Chat ---
9
  const SendIcon = () => (
10
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
11
- <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
12
  </svg>
13
  );
14
 
15
  const Avatar = ({ name }) => (
16
- <div style={{
17
- width: 45, height: 45, borderRadius: '50%',
18
- background: 'linear-gradient(135deg, #374151, #1f2937)',
19
- border: '2px solid rgba(255,255,255,0.1)',
20
- display: 'flex', alignItems: 'center', justifyContent: 'center',
21
- fontWeight: 'bold', color: 'white', flexShrink: 0
22
  }}>
23
  {name ? name[0].toUpperCase() : 'U'}
24
  </div>
@@ -55,17 +55,30 @@ export default function ApplicantMessages({ onNavigate }) {
55
 
56
  const threadMap = {};
57
  messages.forEach(m => {
58
- const isMe = m.sender_id === uid;
59
- const otherId = isMe ? m.receiver_id : m.sender_id;
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  if (!threadMap[otherId]) {
62
- threadMap[otherId] = {
63
- id: otherId,
64
- name: 'Admin / HR',
65
- subj: 'Application Update',
66
- last: '',
67
- unread: false,
68
- time: m.created_at,
69
  msgs: [],
70
  companyName: '',
71
  companyLogo: '',
@@ -73,11 +86,12 @@ export default function ApplicantMessages({ onNavigate }) {
73
  };
74
  }
75
 
76
- threadMap[otherId].msgs.push({
77
- id: m.id,
78
- text: m.content,
79
- time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
80
- isMe
 
81
  });
82
  threadMap[otherId].last = m.content;
83
  threadMap[otherId].time = m.created_at;
@@ -91,7 +105,7 @@ export default function ApplicantMessages({ onNavigate }) {
91
  if (otherUserIds.length > 0) {
92
  const { data: rolesData } = await supabase
93
  .from('user_roles')
94
- .select('user_id, name, company_id')
95
  .in('user_id', otherUserIds);
96
 
97
  console.log("Roles Data:", rolesData);
@@ -106,7 +120,7 @@ export default function ApplicantMessages({ onNavigate }) {
106
  // ✅ Fetch Company Details including logo from storage
107
  const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
108
  console.log("Company IDs to fetch:", companyIds);
109
-
110
  if (companyIds.length > 0) {
111
  const { data: companiesData } = await supabase
112
  .from('companies')
@@ -134,7 +148,7 @@ export default function ApplicantMessages({ onNavigate }) {
134
 
135
  console.log("Final Thread List:", threadList);
136
  setThreads(threadList);
137
-
138
  // Refresh currently open chat window if data changed
139
  if (selected) {
140
  const updated = threadList.find(t => t.id === selected.id);
@@ -158,11 +172,11 @@ export default function ApplicantMessages({ onNavigate }) {
158
 
159
  // Create a channel to listen for any new messages where THIS user is the receiver
160
  channel = supabase.channel('applicant_inbox')
161
- .on('postgres_changes', {
162
- event: 'INSERT',
163
- schema: 'public',
164
  table: 'messages',
165
- filter: `receiver_id=eq.${user.id}`
166
  }, () => {
167
  console.log("New message received from Admin!");
168
  fetchMsgs(user.id);
@@ -194,11 +208,11 @@ export default function ApplicantMessages({ onNavigate }) {
194
  setText('');
195
 
196
  // Send to Supabase
197
- const { error } = await supabase.from('messages').insert([{
198
- sender_id: userId,
199
- receiver_id: selected.id,
200
- content: messageText,
201
- is_read: false
202
  }]);
203
 
204
  if (error) {
@@ -208,14 +222,48 @@ export default function ApplicantMessages({ onNavigate }) {
208
  }
209
  };
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
212
 
213
  return (
214
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
215
  <style>{`.hide-scroll::-webkit-scrollbar { width: 0px; }`}</style>
216
-
217
  <div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
218
-
219
  {/* --- CONVERSATIONS LIST --- */}
220
  <div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
221
  <div style={{ padding: '1.5rem' }}>
@@ -235,36 +283,36 @@ export default function ApplicantMessages({ onNavigate }) {
235
  filtered.map(t => {
236
  const isAct = selected?.id === t.id;
237
  return (
238
- <motion.div
239
  key={t.id} onClick={() => markAsRead(t)}
240
- whileHover={{ scale: 0.98 }}
241
- style={{
242
- padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
243
  cursor: 'pointer', position: 'relative',
244
- background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
245
  border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
246
  display: 'flex', gap: '1rem'
247
  }}
248
  >
249
  {t.unread && !isAct && <div style={{ position: 'absolute', top: '1.5rem', right: '1.25rem', width: '10px', height: '10px', backgroundColor: '#FBBF24', borderRadius: '50%' }}></div>}
250
-
251
  {/* Company Logo */}
252
  {t.companyLogo ? (
253
- <img
254
- src={t.companyLogo}
255
- alt={t.companyName}
256
  style={{ width: 48, height: 48, borderRadius: '0.5rem', objectFit: 'cover', flexShrink: 0, border: '1px solid rgba(255,255,255,0.2)' }}
257
  />
258
  ) : (
259
  <Avatar name={t.companyName || t.name} />
260
  )}
261
-
262
  {/* Message Info */}
263
  <div style={{ flex: 1, minWidth: 0 }}>
264
  <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
265
  <span style={{ fontWeight: 'bold', color: isAct ? '#FCD34D' : 'white', fontSize: '0.95rem' }}>{t.companyName || t.name}</span>
266
  <span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.65rem', fontWeight: '600', flexShrink: 0 }}>HR</span>
267
- <span style={{ fontSize: '0.7rem', color: t.unread ? '#FCD34D' : '#64748b', marginLeft: 'auto', flexShrink: 0 }}>{new Date(t.time).toLocaleDateString([],{month:'short', day:'numeric'})}</span>
268
  </div>
269
  <div style={{ fontSize: '0.8rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
270
  <div style={{ fontSize: '0.75rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
@@ -282,9 +330,9 @@ export default function ApplicantMessages({ onNavigate }) {
282
  <>
283
  <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' }}>
284
  {selected.companyLogo ? (
285
- <img
286
- src={selected.companyLogo}
287
- alt={selected.companyName}
288
  style={{ width: 45, height: 45, borderRadius: '0.5rem', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.2)', flexShrink: 0 }}
289
  />
290
  ) : (
@@ -301,16 +349,23 @@ export default function ApplicantMessages({ onNavigate }) {
301
 
302
  <div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
303
  {selected.msgs.map(m => (
304
- <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%' }}>
305
- <div style={{
306
- background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)',
307
- color: m.isMe ? '#020617' : 'white',
308
- padding: '1rem 1.25rem', borderRadius: '1.25rem',
309
- borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4
310
  }}>
311
  {m.text}
312
  </div>
313
- <div style={{ fontSize: '0.7rem', color: '#64748b', marginTop: 4, textAlign: m.isMe ? 'right' : 'left' }}>{m.time}</div>
 
 
 
 
 
 
 
314
  </motion.div>
315
  ))}
316
  <div ref={scrollRef} />
@@ -318,12 +373,12 @@ export default function ApplicantMessages({ onNavigate }) {
318
 
319
  <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
320
  <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)' }}>
321
- <textarea
322
- value={text}
323
- onChange={e => setText(e.target.value)}
324
- onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}}
325
- placeholder="Type your reply..."
326
- style={{ ...inputStyle, resize: 'none', height: '45px', fontFamily: 'inherit' }}
327
  />
328
  <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', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
329
  <SendIcon />
 
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
  // --- Inline UI Components for Chat ---
9
  const SendIcon = () => (
10
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
11
+ <line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" />
12
  </svg>
13
  );
14
 
15
  const Avatar = ({ name }) => (
16
+ <div style={{
17
+ width: 45, height: 45, borderRadius: '50%',
18
+ background: 'linear-gradient(135deg, #374151, #1f2937)',
19
+ border: '2px solid rgba(255,255,255,0.1)',
20
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
21
+ fontWeight: 'bold', color: 'white', flexShrink: 0
22
  }}>
23
  {name ? name[0].toUpperCase() : 'U'}
24
  </div>
 
55
 
56
  const threadMap = {};
57
  messages.forEach(m => {
58
+ let isMe = m.sender_id === uid;
59
+ let otherId = isMe ? m.receiver_id : m.sender_id;
60
+
61
+ // --- FIX FOR AUTOMATED MESSAGES ---
62
+ // If the applicant sent it to themselves, it's a thread used for system messages.
63
+ if (m.sender_id === m.receiver_id && m.sender_id === uid) {
64
+ otherId = uid; // Keep a valid UUID so replies work
65
+
66
+ // Only force the automated welcome message to the left side
67
+ if (m.content && m.content.startsWith("Hello, Thank you for applying")) {
68
+ isMe = false;
69
+ } else {
70
+ isMe = true; // Applicant's manual replies stay on the right
71
+ }
72
+ }
73
 
74
  if (!threadMap[otherId]) {
75
+ threadMap[otherId] = {
76
+ id: otherId,
77
+ name: 'Admin / HR',
78
+ subj: 'Application Update',
79
+ last: '',
80
+ unread: false,
81
+ time: m.created_at,
82
  msgs: [],
83
  companyName: '',
84
  companyLogo: '',
 
86
  };
87
  }
88
 
89
+ threadMap[otherId].msgs.push({
90
+ id: m.id,
91
+ text: m.content,
92
+ time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
93
+ rawTime: m.created_at,
94
+ isMe
95
  });
96
  threadMap[otherId].last = m.content;
97
  threadMap[otherId].time = m.created_at;
 
105
  if (otherUserIds.length > 0) {
106
  const { data: rolesData } = await supabase
107
  .from('user_roles')
108
+ .select('user_id, name, company_id')
109
  .in('user_id', otherUserIds);
110
 
111
  console.log("Roles Data:", rolesData);
 
120
  // ✅ Fetch Company Details including logo from storage
121
  const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
122
  console.log("Company IDs to fetch:", companyIds);
123
+
124
  if (companyIds.length > 0) {
125
  const { data: companiesData } = await supabase
126
  .from('companies')
 
148
 
149
  console.log("Final Thread List:", threadList);
150
  setThreads(threadList);
151
+
152
  // Refresh currently open chat window if data changed
153
  if (selected) {
154
  const updated = threadList.find(t => t.id === selected.id);
 
172
 
173
  // Create a channel to listen for any new messages where THIS user is the receiver
174
  channel = supabase.channel('applicant_inbox')
175
+ .on('postgres_changes', {
176
+ event: 'INSERT',
177
+ schema: 'public',
178
  table: 'messages',
179
+ filter: `receiver_id=eq.${user.id}`
180
  }, () => {
181
  console.log("New message received from Admin!");
182
  fetchMsgs(user.id);
 
208
  setText('');
209
 
210
  // Send to Supabase
211
+ const { error } = await supabase.from('messages').insert([{
212
+ sender_id: userId,
213
+ receiver_id: selected.id,
214
+ content: messageText,
215
+ is_read: false
216
  }]);
217
 
218
  if (error) {
 
222
  }
223
  };
224
 
225
+ const unsendMsg = async (msgId) => {
226
+ if (!window.confirm("Unsend this message?")) return;
227
+
228
+ // 1. Try to delete the message
229
+ const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select();
230
+
231
+ if (error) {
232
+ console.error("Unsend Error:", error);
233
+ alert("Failed to unsend message.");
234
+ return;
235
+ }
236
+
237
+ // 2. If RLS silently blocks deletion (data is empty), fallback to updating the content
238
+ if (!data || data.length === 0) {
239
+ const { error: updateError } = await supabase
240
+ .from('messages')
241
+ .update({ content: "🚫 This message was unsent" })
242
+ .eq('id', msgId);
243
+
244
+ if (updateError) {
245
+ alert("Database policies prevent unsending this message.");
246
+ return;
247
+ }
248
+ }
249
+
250
+ fetchMsgs(userId);
251
+ };
252
+
253
+ const canUnsend = (rawTime) => {
254
+ if (!rawTime) return false;
255
+ const msgTime = new Date(rawTime).getTime();
256
+ return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes
257
+ };
258
+
259
  const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
260
 
261
  return (
262
  <ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
263
  <style>{`.hide-scroll::-webkit-scrollbar { width: 0px; }`}</style>
264
+
265
  <div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
266
+
267
  {/* --- CONVERSATIONS LIST --- */}
268
  <div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
269
  <div style={{ padding: '1.5rem' }}>
 
283
  filtered.map(t => {
284
  const isAct = selected?.id === t.id;
285
  return (
286
+ <motion.div
287
  key={t.id} onClick={() => markAsRead(t)}
288
+ whileHover={{ scale: 0.98 }}
289
+ style={{
290
+ padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
291
  cursor: 'pointer', position: 'relative',
292
+ background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
293
  border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
294
  display: 'flex', gap: '1rem'
295
  }}
296
  >
297
  {t.unread && !isAct && <div style={{ position: 'absolute', top: '1.5rem', right: '1.25rem', width: '10px', height: '10px', backgroundColor: '#FBBF24', borderRadius: '50%' }}></div>}
298
+
299
  {/* Company Logo */}
300
  {t.companyLogo ? (
301
+ <img
302
+ src={t.companyLogo}
303
+ alt={t.companyName}
304
  style={{ width: 48, height: 48, borderRadius: '0.5rem', objectFit: 'cover', flexShrink: 0, border: '1px solid rgba(255,255,255,0.2)' }}
305
  />
306
  ) : (
307
  <Avatar name={t.companyName || t.name} />
308
  )}
309
+
310
  {/* Message Info */}
311
  <div style={{ flex: 1, minWidth: 0 }}>
312
  <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
313
  <span style={{ fontWeight: 'bold', color: isAct ? '#FCD34D' : 'white', fontSize: '0.95rem' }}>{t.companyName || t.name}</span>
314
  <span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.65rem', fontWeight: '600', flexShrink: 0 }}>HR</span>
315
+ <span style={{ fontSize: '0.7rem', color: t.unread ? '#FCD34D' : '#64748b', marginLeft: 'auto', flexShrink: 0 }}>{new Date(t.time).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
316
  </div>
317
  <div style={{ fontSize: '0.8rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
318
  <div style={{ fontSize: '0.75rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
 
330
  <>
331
  <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' }}>
332
  {selected.companyLogo ? (
333
+ <img
334
+ src={selected.companyLogo}
335
+ alt={selected.companyName}
336
  style={{ width: 45, height: 45, borderRadius: '0.5rem', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.2)', flexShrink: 0 }}
337
  />
338
  ) : (
 
349
 
350
  <div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
351
  {selected.msgs.map(m => (
352
+ <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%', position: 'relative' }}>
353
+ <div style={{
354
+ background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)',
355
+ color: m.isMe ? '#020617' : 'white',
356
+ padding: '1rem 1.25rem', borderRadius: '1.25rem',
357
+ borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4
358
  }}>
359
  {m.text}
360
  </div>
361
+ <div style={{ display: 'flex', justifyContent: m.isMe ? 'flex-end' : 'flex-start', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
362
+ <div style={{ fontSize: '0.7rem', color: '#64748b' }}>{m.time}</div>
363
+ {m.isMe && canUnsend(m.rawTime) && (
364
+ <button onClick={() => unsendMsg(m.id)} title="Unsend message (5 min window)" style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '0 2px', display: 'flex', alignItems: 'center' }}>
365
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
366
+ </button>
367
+ )}
368
+ </div>
369
  </motion.div>
370
  ))}
371
  <div ref={scrollRef} />
 
373
 
374
  <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
375
  <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)' }}>
376
+ <textarea
377
+ value={text}
378
+ onChange={e => setText(e.target.value)}
379
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }}
380
+ placeholder="Type your reply..."
381
+ style={{ ...inputStyle, resize: 'none', height: '45px', fontFamily: 'inherit' }}
382
  />
383
  <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', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
384
  <SendIcon />