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

537 lines
30 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, FilterCheckboxList } from '../../components/Employer/FilterDrawer';
export default function SelectedCandidates({
selectedWorkers,
allNationalities,
skills = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'],
languages = ['English', 'Arabic', 'Hindi', 'Tagalog']
}) {
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 [filterProfession, setFilterProfession] = useState('All Professions');
const [filterLocation, setFilterLocation] = useState('All');
const [filterJobType, setFilterJobType] = useState('All');
const [filterAccommodation, setFilterAccommodation] = useState('All');
const [filterNationalities, setFilterNationalities] = useState([]);
const [filterGender, setFilterGender] = useState('All');
const [filterInCountry, setFilterInCountry] = useState('All');
const [filterVisaType, setFilterVisaType] = useState('All');
const [filterSkills, setFilterSkills] = useState([]);
const [filterLanguages, setFilterLanguages] = useState([]);
const [maxSalary, setMaxSalary] = useState(5000);
const toggleMultiSelect = (item, setSelectedList) => {
setSelectedList(prev =>
prev.includes(item) ? prev.filter(x => x !== item) : [...prev, item]
);
};
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(() => {
if (allNationalities && allNationalities.length > 0) {
return allNationalities;
}
const nats = (selectedWorkers || []).map(w => w.nationality).filter(Boolean);
return [...new Set(nats)];
}, [selectedWorkers, allNationalities]);
// 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 (filterProfession !== 'All Professions') {
list = list.filter(w => w.main_profession && w.main_profession.toLowerCase() === filterProfession.toLowerCase());
}
if (filterLocation !== 'All' && filterLocation.trim() !== '') {
const locKey = filterLocation.toLowerCase();
const locationMapping = {
'dubai': ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'],
'abu dhabi': ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'],
'oman': ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq']
};
const allowedKeywords = locationMapping[locKey] || [locKey];
list = list.filter(w => {
if (!w.preferred_location) return false;
const wLoc = w.preferred_location.toLowerCase();
return allowedKeywords.some(keyword => wLoc.includes(keyword));
});
}
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 (filterNationalities.length > 0) {
list = list.filter(w => w.nationality && filterNationalities.map(n => n.toLowerCase()).includes(w.nationality.toLowerCase()));
}
if (filterGender !== 'All') {
list = list.filter(w => w.gender && w.gender.toLowerCase() === filterGender.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()));
}
if (filterSkills.length > 0) {
list = list.filter(w => w.skills && w.skills.some(s => filterSkills.includes(s.toLowerCase())));
}
if (filterLanguages.length > 0) {
list = list.filter(w => w.languages && w.languages.some(l => filterLanguages.includes(l)));
}
if (maxSalary < 5000) {
list = list.filter(w => w.salary && Number(w.salary) <= maxSalary);
}
return list;
}, [selectedWorkers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
const resetFilters = () => {
setFilterProfession('All Professions');
setFilterLocation('All');
setFilterJobType('All');
setFilterAccommodation('All');
setFilterNationalities([]);
setFilterGender('All');
setFilterInCountry('All');
setFilterVisaType('All');
setFilterSkills([]);
setFilterLanguages([]);
setMaxSalary(5000);
};
const activeFilterCount = useMemo(() => {
let count = 0;
if (filterProfession !== 'All Professions') count++;
if (filterLocation !== 'All' && filterLocation.trim() !== '') count++;
if (filterJobType !== 'All') count++;
if (filterAccommodation !== 'All') count++;
if (filterNationalities.length > 0) count++;
if (filterGender !== 'All') count++;
if (filterInCountry !== 'All') count++;
if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++;
if (filterSkills.length > 0) count++;
if (filterLanguages.length > 0) count++;
if (maxSalary < 5000) count++;
return count;
}, [filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
// Active filter tags for display
const activeFilterTags = useMemo(() => {
const tags = [];
if (filterProfession !== 'All Professions') tags.push({ key: 'profession', label: `🧑‍🔧 ${filterProfession}`, clear: () => setFilterProfession('All Professions') });
if (filterLocation !== 'All' && filterLocation.trim()) tags.push({ key: 'location', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('All') });
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 (filterNationalities.length > 0) tags.push({ key: 'nat', label: `🌍 ${filterNationalities.length} Nat`, clear: () => setFilterNationalities([]) });
if (filterGender !== 'All') tags.push({ key: 'gender', label: `🚻 ${filterGender}`, clear: () => setFilterGender('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') });
if (filterSkills.length > 0) tags.push({ key: 'skills', label: `🛠 ${filterSkills.length} skill${filterSkills.length > 1 ? 's' : ''}`, clear: () => setFilterSkills([]) });
if (filterLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${filterLanguages.length} lang`, clear: () => setFilterLanguages([]) });
if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 ≤${maxSalary} AED`, clear: () => setMaxSalary(5000) });
return tags;
}, [filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
return (
<EmployerLayout title={t('candidates_pipeline', 'Hired Workers')}>
<Head title={`${t('candidates_pipeline', 'Hired Workers')} - ${t('employer_portal', 'Employer Portal')}`} />
{/* ── Filter Drawer ── */}
<FilterDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onReset={resetFilters}
activeCount={activeFilterCount}
title="Filter Hired Workers"
>
{/* Profession */}
<FilterSection label="Profession">
<FilterSelect
id="filter_main_profession"
value={filterProfession}
onChange={e => setFilterProfession(e.target.value)}
>
<option value="All Professions">All Professions</option>
<option value="Housemaid">Housemaid</option>
<option value="Nanny">Nanny</option>
<option value="Cook">Cook</option>
<option value="Driver">Driver</option>
<option value="Caregiver">Caregiver</option>
</FilterSelect>
</FilterSection>
{/* Preferred Location */}
<FilterSection label="Preferred Location">
<FilterSelect
id="filter_preferred_location"
value={filterLocation}
onChange={e => setFilterLocation(e.target.value)}
>
<option value="All">All Locations</option>
<option value="Dubai">Dubai</option>
<option value="Abu Dhabi">Abu Dhabi</option>
<option value="Oman">Oman</option>
</FilterSelect>
</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 */}
{nationalities.length > 0 && (
<FilterSection label="Nationality">
<FilterCheckboxList
items={nationalities}
selected={filterNationalities}
onToggle={nat => toggleMultiSelect(nat, setFilterNationalities)}
/>
</FilterSection>
)}
{/* Gender */}
<FilterSection label="Gender">
<FilterChips
options={[
{ value: 'All', label: 'All' },
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
]}
selected={filterGender}
onChange={setFilterGender}
/>
</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>
</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>
{/* Skills */}
{skills && skills.length > 0 && (
<FilterSection label="Skills">
<FilterCheckboxList
items={skills}
selected={filterSkills}
onToggle={skill => toggleMultiSelect(skill, setFilterSkills)}
/>
</FilterSection>
)}
{/* Languages */}
{languages && languages.length > 0 && (
<FilterSection label="Languages">
<FilterCheckboxList
items={languages}
selected={filterLanguages}
onToggle={lang => toggleMultiSelect(lang, setFilterLanguages)}
/>
</FilterSection>
)}
{/* Salary Range */}
<FilterSection label={maxSalary === 5000 ? "Max Salary — Any" : `Max Salary — ${maxSalary} AED`}>
<input
type="range"
min="500"
max="5000"
step="100"
value={maxSalary}
onChange={e => setMaxSalary(Number(e.target.value))}
className="w-full accent-[#185FA5]"
/>
<div className="flex justify-between text-[10px] font-bold text-slate-400 mt-1">
<span>500 AED</span>
<span>5,000 AED</span>
</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', 'Hired Workers')}</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('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>
{worker.main_profession && (
<>
<span className="text-slate-300"></span>
<span>{worker.main_profession}</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="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 hired workers
</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> hired workers
</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>
);
}