Skip to content

Commit

Permalink
feat: skillsFilter on applicationManagement
Browse files Browse the repository at this point in the history
  • Loading branch information
Luisfp0 committed Nov 1, 2024
1 parent 8a5f69b commit 9bcd170
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { Eye, ChevronLeft, Filter, ChevronDown } from 'lucide-react'
import SkillsFilter from 'app/components/SkillsFilter'

const statusColors = {
pending: 'bg-yellow-100 text-yellow-800',
Expand Down Expand Up @@ -31,6 +32,7 @@ export default function ApplicationsManagement({
roleId,
applications: initialApplications,
userId,
allSkills,
}) {
const router = useRouter()
const [applications, setApplications] = useState(initialApplications)
Expand All @@ -39,6 +41,16 @@ export default function ApplicationsManagement({
englishLevel: '',
yearsOfExperience: '',
})
const [selectedSkills, setSelectedSkills] = useState([])

const filterApplicationsBySkills = (applications) => {
if (selectedSkills.length === 0) return applications

return applications.filter((application) => {
const applicantSkills = application.Subscribers.skillsId || []
return selectedSkills.some((skillId) => applicantSkills.includes(skillId))
})
}

const calculateExperience = (startDate: string) => {
const start = new Date(startDate)
Expand All @@ -65,12 +77,22 @@ export default function ApplicationsManagement({
})
}

// Adicione o filtro de skills
if (selectedSkills.length > 0) {
filtered = filtered.filter((application) => {
const applicantSkills = application.Subscribers.skillsId || []
return selectedSkills.some((skillId) =>
applicantSkills.includes(skillId)
)
})
}

setApplications(filtered)
}

useEffect(() => {
applyFilters()
}, [filters])
}, [filters, selectedSkills])

return (
<div className="container mx-auto px-4 py-8">
Expand All @@ -96,12 +118,10 @@ export default function ApplicationsManagement({
</div>

<div className="p-4">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col gap-1.5">
<label
htmlFor="englishLevel"
className="text-sm font-medium text-gray-700"
>
{/* Grid de filtros */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
Nível de Inglês
</label>
<div className="relative">
Expand All @@ -127,11 +147,8 @@ export default function ApplicationsManagement({
</div>
</div>

<div className="flex flex-col gap-1.5">
<label
htmlFor="experience"
className="text-sm font-medium text-gray-700"
>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
Experiência Mínima
</label>
<div className="relative">
Expand All @@ -157,57 +174,90 @@ export default function ApplicationsManagement({
</div>
</div>

<div className="flex items-end lg:col-span-2">
{filters.englishLevel || filters.yearsOfExperience ? (
<div className="flex w-full flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap gap-2">
{filters.englishLevel && (
<span className="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600">
{englishLevelTranslations[filters.englishLevel]}
<button
onClick={() =>
setFilters((prev) => ({
...prev,
englishLevel: '',
}))
}
className="ml-1 rounded-full p-0.5 hover:bg-indigo-100"
>
×
</button>
</span>
)}
{filters.yearsOfExperience && (
<span className="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600">
{filters.yearsOfExperience}+ anos
<button
onClick={() =>
setFilters((prev) => ({
...prev,
yearsOfExperience: '',
}))
}
className="ml-1 rounded-full p-0.5 hover:bg-indigo-100"
>
×
</button>
</span>
)}
</div>
<SkillsFilter
allSkills={allSkills}
applications={initialApplications}
selectedSkills={selectedSkills}
onFilterChange={(newSkills) => {
setSelectedSkills(newSkills)
}}
onSelectedSkillRemove={(skillId) => {
setSelectedSkills((prev) => prev.filter((id) => id !== skillId))
}}
/>
</div>

{/* Tags dos filtros selecionados */}
{(filters.englishLevel ||
filters.yearsOfExperience ||
selectedSkills.length > 0) && (
<div className="mt-4 flex flex-wrap items-center gap-2">
{filters.englishLevel && (
<span className="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600">
{englishLevelTranslations[filters.englishLevel]}
<button
onClick={() =>
setFilters({ englishLevel: '', yearsOfExperience: '' })
setFilters((prev) => ({
...prev,
englishLevel: '',
}))
}
className="text-sm font-medium text-gray-500 hover:text-gray-700"
className="ml-1 rounded-full p-0.5 hover:bg-indigo-100"
>
Limpar todos
×
</button>
</div>
) : (
<></>
</span>
)}
{filters.yearsOfExperience && (
<span className="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600">
{filters.yearsOfExperience}+ anos
<button
onClick={() =>
setFilters((prev) => ({
...prev,
yearsOfExperience: '',
}))
}
className="ml-1 rounded-full p-0.5 hover:bg-indigo-100"
>
×
</button>
</span>
)}
{selectedSkills.map((skillId) => {
const skill = allSkills.find((s) => s.id.toString() === skillId)
if (!skill) return null

return (
<span
key={skillId}
className="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600"
>
{skill.emoji} {skill.name}
<button
onClick={() =>
setSelectedSkills((prev) =>
prev.filter((id) => id !== skillId)
)
}
className="ml-1 rounded-full p-0.5 hover:bg-indigo-100"
>
×
</button>
</span>
)
})}
<button
onClick={() => {
setFilters({ englishLevel: '', yearsOfExperience: '' })
setSelectedSkills([])
}}
className="text-sm font-medium text-gray-500 hover:text-gray-700"
>
Limpar todos
</button>
</div>
</div>
)}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getSupabaseClient } from 'db'
import { notFound } from 'next/navigation'
import ApplicationsManagement from './ApplicationManagement'

interface PageProps {
params: {
id: string
Expand Down Expand Up @@ -40,6 +39,22 @@ async function getApplications(roleId: string) {
return applications
}

async function getAllSkills() {
const supabase = getSupabaseClient()

const { data: skills, error } = await supabase
.from('Skills')
.select('*')
.order('name')

if (error) {
console.error('Error fetching skills:', error)
return null
}

return skills
}

async function verifyRoleOwnership(roleId: string, userId: string) {
const supabase = getSupabaseClient()

Expand Down Expand Up @@ -70,11 +85,14 @@ export default async function ApplicationsPage({ params }: PageProps) {
notFound()
}

const allSkills = await getAllSkills()

return (
<ApplicationsManagement
roleId={roleId}
userId={userId}
applications={applications}
allSkills={allSkills}
/>
)
}
106 changes: 106 additions & 0 deletions apps/web/app/components/SkillsFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'
import { ChevronDown } from 'lucide-react'

const SkillsFilter = ({
allSkills,
applications,
selectedSkills = [],
onFilterChange,
onSelectedSkillRemove,
}) => {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)

// Filtrar apenas as skills que existem entre os candidatos
const availableSkills = useMemo(() => {
// Coletar todas as skills de todos os candidatos
const candidateSkillIds = new Set()
applications.forEach((application) => {
const skills = application.Subscribers?.skillsId || []
skills.forEach((skillId) => candidateSkillIds.add(skillId))
})

// Filtrar o array de todas as skills para incluir apenas as que existem nos candidatos
return allSkills
.filter((skill) => candidateSkillIds.has(skill.id.toString()))
.sort((a, b) => a.name.localeCompare(b.name))
}, [applications, allSkills])

useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false)
}
}

document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

const handleSkillSelect = (skillId) => {
const isSelected = selectedSkills.includes(skillId)
let newSelectedSkills

if (isSelected) {
newSelectedSkills = selectedSkills.filter((id) => id !== skillId)
} else {
newSelectedSkills = [...selectedSkills, skillId]
}

onFilterChange(newSelectedSkills)
}

const getDisplayText = () => {
if (selectedSkills.length === 0) {
return 'Selecionar skills'
}
return `${selectedSkills.length} skill${
selectedSkills.length === 1 ? '' : 's'
} selecionada${selectedSkills.length === 1 ? '' : 's'}`
}

return (
<div ref={dropdownRef} className="relative">
<label className="mb-1.5 block text-sm font-medium text-gray-700">
Skills
</label>
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="relative h-10 w-full appearance-none rounded-lg border border-gray-300 bg-white px-3 py-2 text-left text-sm text-gray-900 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<span className="block truncate">{getDisplayText()}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDown className="h-4 w-4 text-gray-900" />
</span>
</button>

{isOpen && availableSkills.length > 0 && (
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5">
<div className="divide-y divide-gray-100">
{availableSkills.map((skill) => (
<div
key={skill.id}
onClick={() => handleSkillSelect(skill.id.toString())}
className={`flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-50 ${
selectedSkills.includes(skill.id.toString())
? 'bg-indigo-50'
: ''
}`}
>
<span className="mr-2">{skill.emoji}</span>
<span>{skill.name}</span>
{selectedSkills.includes(skill.id.toString()) && (
<span className="ml-auto text-indigo-600"></span>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}

export default SkillsFilter

0 comments on commit 9bcd170

Please sign in to comment.