migrant-web/resources/js/Pages/Employer/SelectedCandidates.jsx

409 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useMemo } from 'react';
import { Head, Link, router } from '@inertiajs/react';
import EmployerLayout from '../../Layouts/EmployerLayout';
import { useTranslation } from '../../lib/LanguageContext';
import {
MessageSquare,
UserCheck,
Search,
Filter,
MoreHorizontal,
MapPin,
Briefcase,
Home,
Globe2,
Plane
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips } from '../../Components/Employer/FilterDrawer';
export default function SelectedCandidates({ selectedWorkers }) {
const { t } = useTranslation();
const [workers, setWorkers] = useState((selectedWorkers || []).filter(w => w.status !== 'Searching'));
const [searchTerm, setSearchTerm] = useState('');
// Drawer state
const [drawerOpen, setDrawerOpen] = useState(false);
// Filter States
const [filterLocation, setFilterLocation] = useState('');
const [filterJobType, setFilterJobType] = useState('All');
const [filterAccommodation, setFilterAccommodation] = useState('All');
const [filterNationality, setFilterNationality] = useState('All');
const [filterInCountry, setFilterInCountry] = useState('All');
const [filterVisaType, setFilterVisaType] = useState('All');
useEffect(() => {
setWorkers((selectedWorkers || []).filter(w => w.status !== 'Searching'));
}, [selectedWorkers]);
const changeStatus = (id, newStatus) => {
setWorkers(workers.map(w => w.id === id ? { ...w, status: newStatus } : w));
router.post(`/employer/candidates/${id}/status`, { status: newStatus }, {
preserveScroll: true
});
};
// Extract unique nationalities for filter dropdown
const nationalities = useMemo(() => {
const nats = (selectedWorkers || []).map(w => w.nationality).filter(Boolean);
return [...new Set(nats)];
}, [selectedWorkers]);
// Apply Client-Side Filters
const filteredWorkers = useMemo(() => {
let list = (selectedWorkers || []).filter(w => w.status !== 'Searching');
if (searchTerm.trim() !== '') {
const query = searchTerm.toLowerCase();
list = list.filter(w => w.name.toLowerCase().includes(query));
}
if (filterLocation.trim() !== '') {
const query = filterLocation.toLowerCase();
list = list.filter(w => w.preferred_location && w.preferred_location.toLowerCase().includes(query));
}
if (filterJobType !== 'All') {
list = list.filter(w => w.preferred_job_type && w.preferred_job_type.toLowerCase() === filterJobType.toLowerCase());
}
if (filterAccommodation !== 'All') {
list = list.filter(w => w.live_in_out && w.live_in_out.toLowerCase() === filterAccommodation.toLowerCase());
}
if (filterNationality !== 'All') {
list = list.filter(w => w.nationality && w.nationality.toLowerCase() === filterNationality.toLowerCase());
}
if (filterInCountry !== 'All') {
const wantInCountry = filterInCountry === 'In Country';
list = list.filter(w => {
const wVal = w.in_country;
return (wVal === true || wVal === '1' || wVal === 1) === wantInCountry;
});
}
if (filterInCountry === 'In Country' && filterVisaType !== 'All') {
list = list.filter(w => w.visa_status && w.visa_status.toLowerCase().includes(filterVisaType.toLowerCase()));
}
return list;
}, [selectedWorkers, searchTerm, filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]);
const resetFilters = () => {
setFilterLocation('');
setFilterJobType('All');
setFilterAccommodation('All');
setFilterNationality('All');
setFilterInCountry('All');
setFilterVisaType('All');
};
const activeFilterCount = useMemo(() => {
let count = 0;
if (filterLocation.trim() !== '') count++;
if (filterJobType !== 'All') count++;
if (filterAccommodation !== 'All') count++;
if (filterNationality !== 'All') count++;
if (filterInCountry !== 'All') count++;
if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++;
return count;
}, [filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]);
// Active filter tags for display
const activeFilterTags = useMemo(() => {
const tags = [];
if (filterLocation.trim()) tags.push({ key: 'location', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('') });
if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') });
if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') });
if (filterNationality !== 'All') tags.push({ key: 'nat', label: `🌍 ${filterNationality}`, clear: () => setFilterNationality('All') });
if (filterInCountry !== 'All') tags.push({ key: 'country', label: `✈️ ${filterInCountry}`, clear: () => { setFilterInCountry('All'); setFilterVisaType('All'); } });
if (filterInCountry === 'In Country' && filterVisaType !== 'All') tags.push({ key: 'visa', label: `🪪 ${filterVisaType}`, clear: () => setFilterVisaType('All') });
return tags;
}, [filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]);
return (
<EmployerLayout title={t('candidates_pipeline', 'Candidates')}>
<Head title={`${t('candidates_pipeline', 'Candidates')} - ${t('employer_portal', 'Employer Portal')}`} />
{/* ── Filter Drawer ── */}
<FilterDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onReset={resetFilters}
activeCount={activeFilterCount}
title="Filter Candidates"
>
{/* Preferred Location */}
<FilterSection label="Preferred Location">
<FilterInput
id="filter_preferred_location"
value={filterLocation}
onChange={e => setFilterLocation(e.target.value)}
placeholder="e.g. Dubai, Abu Dhabi…"
/>
</FilterSection>
{/* Job Type */}
<FilterSection label="Job Type">
<FilterChips
options={[
{ value: 'All', label: 'All' },
{ value: 'full-time', label: 'Full-time' },
{ value: 'part-time', label: 'Part-time' },
]}
selected={filterJobType}
onChange={setFilterJobType}
/>
</FilterSection>
{/* Accommodation */}
<FilterSection label="Accommodation Type">
<FilterChips
options={[
{ value: 'All', label: 'All' },
{ value: 'live_in', label: 'Live-in' },
{ value: 'live_out', label: 'Live-out' },
]}
selected={filterAccommodation}
onChange={setFilterAccommodation}
/>
</FilterSection>
{/* Nationality */}
<FilterSection label="Nationality">
<FilterSelect
id="filter_nationality"
value={filterNationality}
onChange={e => setFilterNationality(e.target.value)}
>
<option value="All">All Nationalities</option>
{nationalities.map(nat => (
<option key={nat} value={nat}>{nat}</option>
))}
</FilterSelect>
</FilterSection>
{/* Country Status */}
<FilterSection label="Country Status">
<FilterChips
options={[
{ value: 'All', label: 'All' },
{ value: 'In Country', label: 'In Country' },
{ value: 'Out of Country', label: 'Out of Country' },
]}
selected={filterInCountry}
onChange={val => {
setFilterInCountry(val);
if (val !== 'In Country') setFilterVisaType('All');
}}
/>
</FilterSection>
{/* Visa Type */}
<FilterSection label="Visa Type">
<div className={filterInCountry !== 'In Country' ? 'opacity-40 pointer-events-none' : ''}>
<FilterSelect
id="filter_visa_status"
value={filterVisaType}
onChange={e => setFilterVisaType(e.target.value)}
disabled={filterInCountry !== 'In Country'}
>
<option value="All">All Visa Types</option>
<option value="Residence Visa">Residence Visa</option>
<option value="Tourist Visa">Tourist Visa</option>
<option value="Employment Visa">Employment Visa</option>
<option value="Cancelled Visa">Cancelled Visa</option>
<option value="Own Visa">Own Visa</option>
</FilterSelect>
{filterInCountry !== 'In Country' && (
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">
Select "In Country" to enable this filter
</p>
)}
</div>
</FilterSection>
</FilterDrawer>
<div className="space-y-6 select-none">
{/* Header Section */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-black text-slate-900 tracking-tight">{t('candidates_pipeline', 'Candidates')}</h1>
<p className="text-xs font-medium text-slate-500 mt-1">{t('candidates_pipeline_desc', 'Manage your workforce recruitment pipeline')}</p>
</div>
</div>
{/* Table Section */}
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
{/* Toolbar */}
<div className="p-5 border-b border-slate-100 bg-slate-50/50 flex flex-col sm:flex-row sm:items-center gap-3">
{/* Search */}
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder={t('search_by_name', 'Search by name…')}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-100 focus:border-[#185FA5] transition-all outline-none"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex items-center gap-3 ml-auto">
{/* Results count */}
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest whitespace-nowrap">
{filteredWorkers.length} result{filteredWorkers.length !== 1 ? 's' : ''}
</span>
{/* Export actions */}
<div className="hidden sm:flex items-center bg-white border border-slate-200 rounded-xl p-1">
{['COPY', 'CSV', 'PDF', 'PRINT'].map(action => (
<button key={action} className="px-3 py-1.5 text-[10px] font-bold text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all">
{action}
</button>
))}
</div>
{/* Filter button */}
<button
onClick={() => setDrawerOpen(true)}
className={`relative flex items-center space-x-2 px-4 py-2.5 rounded-xl font-bold text-xs transition-all border ${
activeFilterCount > 0
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-md shadow-blue-200'
: 'bg-white text-slate-600 border-slate-200 hover:border-[#185FA5] hover:text-[#185FA5]'
}`}
>
<Filter className="w-4 h-4" />
<span>Filters</span>
{activeFilterCount > 0 && (
<span className="bg-white/25 text-white text-[10px] font-black px-1.5 py-0.5 rounded-full leading-none">
{activeFilterCount}
</span>
)}
</button>
</div>
</div>
{/* Active filter tags */}
{activeFilterTags.length > 0 && (
<div className="px-5 py-3 border-b border-slate-100 bg-blue-50/30 flex flex-wrap gap-2 items-center">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mr-1">Active:</span>
{activeFilterTags.map(tag => (
<button
key={tag.key}
onClick={tag.clear}
className="inline-flex items-center space-x-1.5 px-2.5 py-1 bg-[#185FA5]/10 text-[#185FA5] text-[11px] font-bold rounded-full border border-[#185FA5]/20 hover:bg-[#185FA5]/20 transition-all"
>
<span>{tag.label}</span>
<span className="text-[#185FA5]/60 font-black">×</span>
</button>
))}
<button onClick={resetFilters} className="text-[11px] font-bold text-rose-500 hover:text-rose-700 ml-1 transition-colors">
Clear all
</button>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-slate-100">
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('candidate_header', 'Candidate')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('selected_header', 'Selected')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('category_header', 'Category')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('salary_header', 'Salary')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('status_header', 'Status')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">{t('actions_header', 'Actions')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredWorkers.length > 0 ? (
filteredWorkers.map((worker) => (
<tr key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
<td className="px-6 py-5">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-100 to-blue-200 text-[#185FA5] flex items-center justify-center font-bold text-sm flex-shrink-0">
{worker.name.charAt(0)}
</div>
<div>
<div className="text-sm font-bold text-slate-900">{worker.name}</div>
<div className="text-[10px] text-slate-400 font-medium flex items-center space-x-1 mt-0.5">
<Globe2 className="w-3 h-3" />
<span>{worker.nationality || '—'}</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-5 text-[11px] font-bold text-slate-500">{worker.applied_at || '—'}</td>
<td className="px-6 py-5">
<span className="px-2 py-1 bg-slate-100 text-slate-600 text-[10px] font-bold rounded-lg">{worker.category}</span>
</td>
<td className="px-6 py-5">
<span className="text-sm font-bold text-slate-800">{worker.salary} <span className="text-[10px] text-slate-400">AED</span></span>
</td>
<td className="px-6 py-5">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-black tracking-tight bg-emerald-50 text-emerald-700 border border-emerald-100">
<span className="w-1.5 h-1.5 rounded-full mr-1.5 bg-emerald-500" />
{t('hired', 'Hired').toUpperCase()}
</span>
</td>
<td className="px-6 py-5 text-right">
<div className="flex items-center justify-end space-x-1">
<Link
href={`/employer/messages/start/${worker.worker_id}`}
className="p-2 bg-blue-50 text-[#185FA5] hover:bg-[#185FA5] hover:text-white rounded-lg transition-all"
>
<MessageSquare className="w-3.5 h-3.5" />
</Link>
<Link
href={`/employer/workers/${worker.worker_id}`}
className="p-2 bg-slate-50 text-slate-400 hover:text-slate-900 rounded-lg transition-all"
>
<MoreHorizontal className="w-3.5 h-3.5" />
</Link>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="6" className="py-20 text-center">
<div className="space-y-3">
<div className="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto">
<UserCheck className="w-8 h-8 text-slate-300" />
</div>
<div className="text-base font-bold text-slate-400">{t('no_candidates_found', 'No candidates found')}</div>
{activeFilterCount > 0 && (
<button onClick={resetFilters} className="text-sm font-bold text-[#185FA5] hover:underline">
Clear filters to see all candidates
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Footer */}
<div className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">
Showing <span className="text-slate-700">{filteredWorkers.length}</span> of <span className="text-slate-700">{(selectedWorkers || []).filter(w => w.status !== 'Searching').length}</span> candidates
</div>
<div className="flex items-center space-x-2">
<button className="px-3 py-1.5 bg-white border border-slate-200 text-slate-400 rounded-lg text-[10px] font-black cursor-not-allowed">PREV</button>
<button className="w-8 h-8 flex items-center justify-center bg-[#185FA5] text-white rounded-lg text-[10px] font-black">1</button>
<button className="px-3 py-1.5 bg-white border border-slate-200 text-slate-400 rounded-lg text-[10px] font-black cursor-not-allowed">NEXT</button>
</div>
</div>
</div>
</div>
</EmployerLayout>
);
}