409 lines
24 KiB
JavaScript
409 lines
24 KiB
JavaScript
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', '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"
|
||
>
|
||
{/* 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', '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('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 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>
|
||
);
|
||
}
|