enhanced questionnarier and request detail screen
This commit is contained in:
parent
8ef092f723
commit
0ff5412d04
@ -39,6 +39,18 @@ export const API = {
|
||||
getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth
|
||||
submitPublicResponse: (data: any) => axios.post('http://localhost:5000/api/questionnaire/public/submit', data),
|
||||
|
||||
// Assessment & Interviews
|
||||
getAiSummary: (appId: string) => client.get(`/assessment/ai-summary/${appId}`),
|
||||
scheduleInterview: (data: any) => client.post('/assessment/interviews', data),
|
||||
updateInterview: (id: string, data: any) => client.put(`/assessment/interviews/${id}`, data),
|
||||
submitEvaluation: (id: string, data: any) => client.post(`/assessment/interviews/${id}/evaluation`, data),
|
||||
|
||||
// Collaboration & Participants
|
||||
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
|
||||
addWorknote: (data: any) => client.post('/collaboration/worknotes', data),
|
||||
addParticipant: (data: any) => client.post('/collaboration/participants', data),
|
||||
removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`),
|
||||
|
||||
// User management routes
|
||||
getUsers: () => client.get('/admin/users'),
|
||||
createUser: (data: any) => client.post('/admin/users', data),
|
||||
|
||||
@ -37,7 +37,7 @@ import { Progress } from '../ui/progress';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import {
|
||||
Table,
|
||||
@ -152,6 +152,7 @@ export function ApplicationDetails() {
|
||||
loaDate: getStageDate('LOA'),
|
||||
eorCompleteDate: getStageDate('EOR Complete'),
|
||||
inaugurationDate: getStageDate('Inauguration'),
|
||||
participants: data.participants || [],
|
||||
};
|
||||
setApplication(mappedApp);
|
||||
} catch (error) {
|
||||
@ -175,6 +176,7 @@ export function ApplicationDetails() {
|
||||
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
|
||||
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
|
||||
const [showDocumentsModal, setShowDocumentsModal] = useState(false);
|
||||
const [showAssignModal, setShowAssignModal] = useState(false);
|
||||
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
||||
const [interviewMode, setInterviewMode] = useState('virtual');
|
||||
const [approvalRemark, setApprovalRemark] = useState('');
|
||||
@ -184,6 +186,25 @@ export function ApplicationDetails() {
|
||||
'architectural-work': true,
|
||||
'statutory-documents': true
|
||||
});
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<string>('');
|
||||
const [participantType, setParticipantType] = useState<string>('contributor');
|
||||
const [interviewDate, setInterviewDate] = useState('');
|
||||
const [interviewType, setInterviewType] = useState('level1');
|
||||
const [meetingLink, setMeetingLink] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await onboardingService.getUsers();
|
||||
setUsers(response || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users', error);
|
||||
}
|
||||
};
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading application details...</div>;
|
||||
@ -575,6 +596,50 @@ export function ApplicationDetails() {
|
||||
setWorkNote('');
|
||||
};
|
||||
|
||||
const handleAddParticipant = async () => {
|
||||
if (!selectedUser) {
|
||||
alert('Please select a user');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await onboardingService.addParticipant({
|
||||
requestId: applicationId,
|
||||
requestType: 'application',
|
||||
userId: selectedUser,
|
||||
participantType
|
||||
});
|
||||
alert('User assigned successfully!');
|
||||
// Refresh application data
|
||||
const data = await onboardingService.getApplicationById(applicationId);
|
||||
setApplication({ ...application, participants: data.participants || [] });
|
||||
setSelectedUser('');
|
||||
setShowAssignModal(false);
|
||||
} catch (error) {
|
||||
alert('Failed to assign user');
|
||||
}
|
||||
};
|
||||
|
||||
const handleScheduleInterview = async () => {
|
||||
if (!interviewDate) {
|
||||
alert('Please select date and time');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await onboardingService.scheduleInterview({
|
||||
applicationId,
|
||||
level: interviewType,
|
||||
scheduledAt: interviewDate,
|
||||
type: interviewMode,
|
||||
location: interviewMode === 'virtual' ? meetingLink : location,
|
||||
participants: [] // Optional: add multi-participant support if needed
|
||||
});
|
||||
alert('Interview scheduled successfully!');
|
||||
setShowScheduleModal(false);
|
||||
} catch (error) {
|
||||
alert('Failed to schedule interview');
|
||||
}
|
||||
};
|
||||
|
||||
// If Work Notes page is open, show that instead
|
||||
if (showWorkNotesPage) {
|
||||
return (
|
||||
@ -584,6 +649,7 @@ export function ApplicationDetails() {
|
||||
registrationNumber={application.registrationNumber}
|
||||
onBack={() => setShowWorkNotesPage(false)}
|
||||
initialNotes={mockWorkNotes}
|
||||
participants={application.participants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1232,10 +1298,55 @@ export function ApplicationDetails() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="outline" className="w-full">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Assign User
|
||||
</Button>
|
||||
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Assign User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign User to Application</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a user and their role for this application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Select User</Label>
|
||||
<Select value={selectedUser} onValueChange={setSelectedUser}>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="Search users..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{users.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>
|
||||
{u.fullName} ({u.email})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Assignment Role</Label>
|
||||
<Select value={participantType} onValueChange={setParticipantType}>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="owner">Owner</SelectItem>
|
||||
<SelectItem value="contributor">Contributor</SelectItem>
|
||||
<SelectItem value="reviewer">Reviewer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button className="w-full bg-amber-600 hover:bg-amber-700" onClick={handleAddParticipant}>
|
||||
Confirm Assignment
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@ -1411,7 +1522,7 @@ export function ApplicationDetails() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Interview Type</Label>
|
||||
<Select>
|
||||
<Select value={interviewType} onValueChange={setInterviewType}>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="Select interview type" />
|
||||
</SelectTrigger>
|
||||
@ -1436,22 +1547,33 @@ export function ApplicationDetails() {
|
||||
</div>
|
||||
<div>
|
||||
<Label>Date & Time</Label>
|
||||
<Input type="datetime-local" className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Participants</Label>
|
||||
<Input placeholder="Enter participant emails (comma-separated)" className="mt-2" />
|
||||
<Input
|
||||
type="datetime-local"
|
||||
className="mt-2"
|
||||
value={interviewDate}
|
||||
onChange={(e) => setInterviewDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{interviewMode === 'virtual' && (
|
||||
<div>
|
||||
<Label>Meeting Link</Label>
|
||||
<Input placeholder="https://meet.google.com/..." className="mt-2" />
|
||||
<Input
|
||||
placeholder="https://meet.google.com/..."
|
||||
className="mt-2"
|
||||
value={meetingLink}
|
||||
onChange={(e) => setMeetingLink(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{interviewMode === 'physical' && (
|
||||
<div>
|
||||
<Label>Location</Label>
|
||||
<Input placeholder="Enter interview location address" className="mt-2" />
|
||||
<Input
|
||||
placeholder="Enter interview location address"
|
||||
className="mt-2"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
@ -1464,10 +1586,7 @@ export function ApplicationDetails() {
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 bg-amber-600 hover:bg-amber-700"
|
||||
onClick={() => {
|
||||
alert('Interview scheduled! Calendar invites will be sent.');
|
||||
setShowScheduleModal(false);
|
||||
}}
|
||||
onClick={handleScheduleInterview}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
|
||||
@ -4,15 +4,15 @@ import { Input } from '../ui/input';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||
import { Badge } from '../ui/badge';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Paperclip,
|
||||
import {
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Paperclip,
|
||||
Smile,
|
||||
Image as ImageIcon,
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
import { WorkNote } from '../../lib/mock-data';
|
||||
import { WorkNote, Participant } from '../../lib/mock-data';
|
||||
|
||||
interface WorkNotesPageProps {
|
||||
applicationId: string;
|
||||
@ -20,20 +20,23 @@ interface WorkNotesPageProps {
|
||||
registrationNumber: string;
|
||||
onBack: () => void;
|
||||
initialNotes?: WorkNote[];
|
||||
participants?: Participant[];
|
||||
}
|
||||
|
||||
interface Participant {
|
||||
// This interface defines the structure for participants displayed in the UI
|
||||
interface ParticipantUI {
|
||||
name: string;
|
||||
initials: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function WorkNotesPage({
|
||||
applicationId,
|
||||
export function WorkNotesPage({
|
||||
applicationId,
|
||||
applicationName,
|
||||
registrationNumber,
|
||||
onBack,
|
||||
initialNotes = []
|
||||
initialNotes = [],
|
||||
participants: externalParticipants = []
|
||||
}: WorkNotesPageProps) {
|
||||
const [notes, setNotes] = useState<WorkNote[]>(initialNotes);
|
||||
const [message, setMessage] = useState('');
|
||||
@ -43,13 +46,40 @@ export function WorkNotesPage({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Mock participants for @mentions
|
||||
const participants: Participant[] = [
|
||||
{ name: 'Sarah Chen', initials: 'SC', color: 'bg-green-600' },
|
||||
{ name: 'Lisa Wong', initials: 'LW', color: 'bg-blue-600' },
|
||||
{ name: 'Mark Johnson', initials: 'MJ', color: 'bg-purple-600' },
|
||||
{ name: 'Anjali Sharma', initials: 'AS', color: 'bg-amber-600' },
|
||||
];
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.substring(0, 2);
|
||||
};
|
||||
|
||||
const getAvatarColor = (name: string) => {
|
||||
const colors = [
|
||||
'bg-green-600',
|
||||
'bg-blue-600',
|
||||
'bg-purple-600',
|
||||
'bg-amber-600',
|
||||
'bg-pink-600',
|
||||
'bg-indigo-600',
|
||||
'bg-teal-600',
|
||||
];
|
||||
const index = name.length % colors.length;
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
// Map backend participants to the UI sub-format
|
||||
const participantsList: ParticipantUI[] = externalParticipants.map(p => ({
|
||||
name: p.user?.name || 'Unknown User',
|
||||
initials: getInitials(p.user?.name || 'U'),
|
||||
color: getAvatarColor(p.user?.name || 'U')
|
||||
}));
|
||||
|
||||
// Fallback to current user if no participants
|
||||
if (participantsList.length === 0) {
|
||||
participantsList.push({ name: 'System User', initials: 'SU', color: 'bg-slate-600' });
|
||||
}
|
||||
|
||||
// Scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
@ -61,14 +91,14 @@ export function WorkNotesPage({
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const cursorPos = e.target.selectionStart || 0;
|
||||
|
||||
|
||||
setMessage(value);
|
||||
setCursorPosition(cursorPos);
|
||||
|
||||
// Check if user is typing @ mention
|
||||
const textBeforeCursor = value.substring(0, cursorPos);
|
||||
const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
|
||||
|
||||
|
||||
if (lastAtSymbol !== -1 && lastAtSymbol === textBeforeCursor.length - 1) {
|
||||
setShowMentionSuggestions(true);
|
||||
setMentionQuery('');
|
||||
@ -89,12 +119,12 @@ export function WorkNotesPage({
|
||||
const textBeforeCursor = message.substring(0, cursorPosition);
|
||||
const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
|
||||
const textAfterCursor = message.substring(cursorPosition);
|
||||
|
||||
const newMessage =
|
||||
message.substring(0, lastAtSymbol) +
|
||||
`@${name} ` +
|
||||
|
||||
const newMessage =
|
||||
message.substring(0, lastAtSymbol) +
|
||||
`@${name} ` +
|
||||
textAfterCursor;
|
||||
|
||||
|
||||
setMessage(newMessage);
|
||||
setShowMentionSuggestions(false);
|
||||
inputRef.current?.focus();
|
||||
@ -139,7 +169,7 @@ export function WorkNotesPage({
|
||||
|
||||
const renderMessageWithMentions = (text: string) => {
|
||||
const parts = text.split(/(@\w+\s*\w*)/g);
|
||||
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (part.startsWith('@')) {
|
||||
return (
|
||||
@ -152,30 +182,8 @@ export function WorkNotesPage({
|
||||
});
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.substring(0, 2);
|
||||
};
|
||||
|
||||
const getAvatarColor = (name: string) => {
|
||||
const colors = [
|
||||
'bg-green-600',
|
||||
'bg-blue-600',
|
||||
'bg-purple-600',
|
||||
'bg-amber-600',
|
||||
'bg-pink-600',
|
||||
'bg-indigo-600',
|
||||
'bg-teal-600',
|
||||
];
|
||||
const index = name.length % colors.length;
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
const filteredParticipants = participants.filter(p =>
|
||||
const filteredParticipants = participantsList.filter(p =>
|
||||
p.name.toLowerCase().includes(mentionQuery.toLowerCase())
|
||||
);
|
||||
|
||||
@ -185,19 +193,19 @@ export function WorkNotesPage({
|
||||
<div className="bg-white border-b border-slate-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="hover:bg-slate-100"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
|
||||
<div className="w-12 h-12 bg-purple-600 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h1 className="text-slate-900">Work Notes</h1>
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
@ -210,8 +218,8 @@ export function WorkNotesPage({
|
||||
|
||||
{/* Participant Avatars */}
|
||||
<div className="flex items-center -space-x-2">
|
||||
{participants.slice(0, 3).map((participant, index) => (
|
||||
<Avatar
|
||||
{participantsList.slice(0, 3).map((participant, index) => (
|
||||
<Avatar
|
||||
key={index}
|
||||
className="w-8 h-8 border-2 border-white"
|
||||
>
|
||||
@ -220,9 +228,9 @@ export function WorkNotesPage({
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
{participants.length > 3 && (
|
||||
{participantsList.length > 3 && (
|
||||
<div className="w-8 h-8 rounded-full bg-slate-200 border-2 border-white flex items-center justify-center">
|
||||
<span className="text-slate-600 text-xs">+{participants.length - 3}</span>
|
||||
<span className="text-slate-600 text-xs">+{participantsList.length - 3}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -265,7 +273,7 @@ export function WorkNotesPage({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="bg-white rounded-lg border border-slate-200 px-4 py-3 shadow-sm">
|
||||
<p className="text-slate-700 leading-relaxed whitespace-pre-wrap">
|
||||
{renderMessageWithMentions(note.message)}
|
||||
|
||||
@ -6,7 +6,7 @@ interface Question {
|
||||
id: string;
|
||||
sectionName: string;
|
||||
questionText: string;
|
||||
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq';
|
||||
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq' | 'radio' | 'textarea' | 'email';
|
||||
options?: any;
|
||||
questionOptions?: any[]; // From backend inclusion
|
||||
weight: number;
|
||||
@ -158,9 +158,9 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
||||
{q.questionText} {q.isMandatory && !readOnly && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
|
||||
{q.inputType === 'text' && (
|
||||
{(q.inputType === 'text' || q.inputType === 'email') && (
|
||||
<input
|
||||
type="text"
|
||||
type={q.inputType === 'email' ? 'email' : 'text'}
|
||||
className="w-full border p-2 rounded disabled:bg-gray-100"
|
||||
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
||||
value={responses[q.id] || ''}
|
||||
@ -168,6 +168,15 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{q.inputType === 'textarea' && (
|
||||
<textarea
|
||||
className="w-full border p-2 rounded disabled:bg-gray-100 min-h-[100px]"
|
||||
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
||||
value={responses[q.id] || ''}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{q.inputType === 'file' && (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
@ -202,7 +211,7 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{(q.inputType === 'yesno' || q.inputType === 'select' || q.inputType === 'mcq') && (
|
||||
{(q.inputType === 'yesno' || q.inputType === 'select' || q.inputType === 'mcq' || q.inputType === 'radio') && (
|
||||
<div className="space-y-2">
|
||||
{/* Use backend options if available, or fallbacks for legacy yesno */}
|
||||
{(q.questionOptions && q.questionOptions.length > 0 ? q.questionOptions : (
|
||||
|
||||
@ -103,6 +103,19 @@ export interface Application {
|
||||
architectureCompletionDate?: string;
|
||||
inaugurationDate?: string;
|
||||
questionnaireResponses?: any[]; // added for response view
|
||||
participants?: Participant[];
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
id: string;
|
||||
userId: string;
|
||||
participantType: string;
|
||||
joinedMethod: string;
|
||||
user?: {
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
@ -36,5 +36,32 @@ export const onboardingService = {
|
||||
console.error('Get application by id error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getUsers: async () => {
|
||||
try {
|
||||
const response = await API.getUsers();
|
||||
return response.data?.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
addParticipant: async (data: any) => {
|
||||
try {
|
||||
const response = await API.addParticipant(data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Add participant error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
scheduleInterview: async (data: any) => {
|
||||
try {
|
||||
const response = await API.scheduleInterview(data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Schedule interview error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user