Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 112 additions & 10 deletions frontend/src/domains/new_dashboard/NewDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { Toast } from "./components/shared/Toast";
import { jobStatuses } from "./constants";
import { useDashboardJobs } from "./hooks/useDashboardJobs";
import { useUserDashboardData } from "./hooks/useUserDashboardData";
import {
getJobFilterOptions,
FALLBACK_JOB_FAMILIES,
type JobFamilyOption,
} from "./infrastructure/dashboardJobsApi";
import type {
CareerChecklist,
Job,
Expand Down Expand Up @@ -153,24 +158,99 @@ export default function NewDashboardPage() {
const { user, refreshUser } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();

const [searchQuery, setSearchQuery] = useState("");
const [filterType, setFilterType] = useState<JobModelFilter>("Todos");
const [filterLevel, setFilterLevel] = useState("Todos");
const [continentFilter, setContinentFilter] =
useState<ContinentFilter>("Todos");
const [countryFilter, setCountryFilter] = useState<CountryFilter>("Todos");
const [matchSort, setMatchSort] = useState<MatchSort>("default");
const [filterLevel, setFilterLevel] = useState(
() => searchParams.get("level") || "Todos",
);
const [continentFilter, setContinentFilter] = useState<ContinentFilter>(
() => (searchParams.get("continent") as ContinentFilter) || "Todos",
);
const [countryFilter, setCountryFilter] = useState<CountryFilter>(
() => (searchParams.get("country") as CountryFilter) || "Todos",
);
const [matchSort, setMatchSort] = useState<MatchSort>(
() => (searchParams.get("matchSort") as MatchSort) || "default",
);
const [familyOptions, setFamilyOptions] = useState<JobFamilyOption[]>(
FALLBACK_JOB_FAMILIES,
);
const [selectedFamilies, setSelectedFamilies] = useState<string[]>(() => {
const param = searchParams.get("family");
return param ? param.split(",").filter(Boolean) : [];
});

const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
const [timelineVersion, setTimelineVersion] = useState(0);
const [searchParams, setSearchParams] = useSearchParams();
const [isAddJobOpen, setIsAddJobOpen] = useState(false);
const [toast, setToast] = useState("");
const [hasUserChangedJobFilters, setHasUserChangedJobFilters] =
useState(false);
const checklistSaveTimeout = useRef<number | null>(null);
const hasUserSelectedModelFilter = useRef(false);
const showToast = useCallback((message: string) => setToast(message), []);

useEffect(() => {
let active = true;
getJobFilterOptions()
.then((res) => {
if (active && res?.families) {
setFamilyOptions(res.families);
}
})
.catch(() => {
// Fallback already used
});
return () => {
active = false;
};
}, []);

useEffect(() => {
const nextParams = new URLSearchParams(searchParams);
if (selectedFamilies.length > 0) {
nextParams.set("family", selectedFamilies.join(","));
nextParams.set("familyMode", "primary");
} else {
nextParams.delete("family");
nextParams.delete("familyMode");
}
if (filterLevel !== "Todos") {
nextParams.set("level", filterLevel);
} else {
nextParams.delete("level");
}
if (continentFilter !== "Todos") {
nextParams.set("continent", continentFilter);
} else {
nextParams.delete("continent");
}
if (countryFilter !== "Todos") {
nextParams.set("country", countryFilter);
} else {
nextParams.delete("country");
}
if (matchSort !== "default") {
nextParams.set("matchSort", matchSort);
} else {
nextParams.delete("matchSort");
}

if (nextParams.toString() !== searchParams.toString()) {
setSearchParams(nextParams, { replace: true });
}
}, [
selectedFamilies,
filterLevel,
continentFilter,
countryFilter,
matchSort,
searchParams,
setSearchParams,
]);

const {
userProfile,
setUserProfile,
Expand All @@ -192,7 +272,6 @@ export default function NewDashboardPage() {
if (!jobId) return;
// eslint-disable-next-line react-hooks/set-state-in-effect -- sincroniza o jobId vindo da URL (navegação externa) com o estado local; execução única por navegação, não é loop de renderização.
setSelectedJobId(jobId);
setSelectedJobId(jobId);
setSearchParams(
(current) => {
const next = new URLSearchParams(current);
Expand All @@ -209,9 +288,17 @@ export default function NewDashboardPage() {
? null
: {
keywords: [],
filters: modelFilterToApiFilter(preferredModelFilter),
filters: {
...modelFilterToApiFilter(preferredModelFilter),
...(selectedFamilies.length > 0
? {
family: selectedFamilies.join(","),
familyMode: "primary",
}
: {}),
},
},
[isLoadingUserData, preferredModelFilter],
[isLoadingUserData, preferredModelFilter, selectedFamilies],
);
const {
trackedJobs,
Expand Down Expand Up @@ -330,6 +417,11 @@ export default function NewDashboardPage() {
setMatchSort(value);
}, []);

const handleSelectedFamiliesChange = useCallback((value: string[]) => {
setHasUserChangedJobFilters(true);
setSelectedFamilies(value);
}, []);

const handleCareerChecklistChange = useCallback(
(careerChecklist: CareerChecklist[]) => {
const nextPreferences = {
Expand Down Expand Up @@ -369,7 +461,6 @@ export default function NewDashboardPage() {
changeJobNotesLocally(jobId, notes);
};


const handleAddJob = async (newJob: NewJob) => {
try {
await addTrackedJob(newJob);
Expand Down Expand Up @@ -415,6 +506,12 @@ export default function NewDashboardPage() {
? { country: countryFilter, location: countryFilter }
: {}),
...(matchSort !== "default" ? { matchSort } : {}),
...(selectedFamilies.length > 0
? {
family: selectedFamilies.join(","),
familyMode: "primary",
}
: {}),
};

return {
Expand All @@ -437,6 +534,7 @@ export default function NewDashboardPage() {
filterLevel,
filterType,
matchSort,
selectedFamilies,
refreshRecommendations,
searchQuery,
]);
Expand Down Expand Up @@ -472,6 +570,7 @@ export default function NewDashboardPage() {
continentFilter,
countryFilter,
matchSort,
selectedFamilies,
section,
]);

Expand Down Expand Up @@ -512,6 +611,9 @@ export default function NewDashboardPage() {
setCountryFilter={handleCountryFilterChange}
matchSort={matchSort}
setMatchSort={handleMatchSortChange}
selectedFamilies={selectedFamilies}
setSelectedFamilies={handleSelectedFamiliesChange}
familyOptions={familyOptions}
searchPreferences={searchPreferences}
showPreferenceNotice={showPreferenceNotice}
isSearching={isRefreshingJobs}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ExternalLink } from "lucide-react";
import { useEffect, useState } from "react";
import { jobStatusClasses, jobStatuses } from "../../constants";
import { getDashboardSavedJobEvents } from "../../infrastructure/dashboardJobsApi";
import {
getDashboardSavedJobEvents,
getFamilyLabel,
} from "../../infrastructure/dashboardJobsApi";
import type { Job, JobStatus, JobTimelineEvent } from "../../types";
import { Modal } from "../shared/Modal";
import { ApplicationNotesSection } from "./ApplicationNotesSection";
Expand Down Expand Up @@ -47,6 +50,7 @@ const redundantPayloadKeys = new Set([
"modality",
"source",
"description",
"classification",
]);

function payloadValueToText(value: unknown): string {
Expand Down Expand Up @@ -147,6 +151,14 @@ export function JobDetailModal({
};
}, [isTracked, job.id, timelineVersion]);

const classification = (job.rawPayload?.classification ?? {}) as {
primaryFamily?: string;
relatedFamilies?: string[];
seniority?: string;
technologies?: string[];
reasons?: string[];
};

const payloadEntries = Object.entries(job.rawPayload ?? {}).filter(
([key, value]) =>
!redundantPayloadKeys.has(key) && payloadValueToText(value) !== "Não informado",
Expand Down Expand Up @@ -192,20 +204,50 @@ export function JobDetailModal({
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
<InfoTile label="Local" value={job.location} />
<InfoTile label="Modalidade" value={job.type} />
<InfoTile label="Nível" value={job.level} />
<InfoTile label="Nível" value={classification.seniority ? classification.seniority.charAt(0).toUpperCase() + classification.seniority.slice(1) : job.level} />
<InfoTile label="Fonte" value={job.source} />
<InfoTile label="Salário" value={job.salary} />
<InfoTile
label="Match"
value={`${job.matchScore}%`}
valueClassName="text-emerald-600 dark:text-emerald-400"
/>
{classification.primaryFamily ? (
<InfoTile
label="Área principal"
value={getFamilyLabel(classification.primaryFamily)}
valueClassName="text-primary"
/>
) : null}
</div>

{classification.relatedFamilies && classification.relatedFamilies.length > 0 ? (
<div className="space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">
Áreas relacionadas
</span>
<div className="flex flex-wrap gap-2">
{classification.relatedFamilies.map((fam) => (
<span
key={fam}
className="rounded border border-border bg-background px-2 py-1 text-xs font-semibold text-muted-foreground"
>
{getFamilyLabel(fam)}
</span>
))}
</div>
</div>
) : null}

<div className="space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">Tecnologias</span>
<span className="text-xs font-bold uppercase text-muted-foreground">
{classification.technologies && classification.technologies.length > 0 ? "Tecnologias e Competências" : "Tags"}
</span>
<div className="flex flex-wrap gap-2">
{job.tags.map((tag) => (
{(classification.technologies && classification.technologies.length > 0
? classification.technologies
: job.tags
).map((tag) => (
<span
key={tag}
className="rounded border border-border bg-background px-2 py-1 text-xs font-semibold"
Expand All @@ -216,6 +258,19 @@ export function JobDetailModal({
</div>
</div>

{classification.reasons && classification.reasons.length > 0 ? (
<div className="space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">
Justificativas de match
</span>
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
{classification.reasons.map((reason, idx) => (
<li key={idx}>{reason}</li>
))}
</ul>
</div>
) : null}

{hasDescription ? (
<div className="space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">
Expand Down Expand Up @@ -263,7 +318,7 @@ export function JobDetailModal({
</div>
) : null}

<label className="space-y-2 block">
<label className="block space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">Status</span>
<select
value={job.status}
Expand All @@ -279,7 +334,7 @@ export function JobDetailModal({
</label>

{isTracked ? <ApplicationNotesSection savedJobId={job.id} /> : <div className="space-y-2">
<label className="space-y-2 block">
<label className="block space-y-2">
<span className="text-xs font-bold uppercase text-muted-foreground">Notas</span>
<textarea
aria-label="Notas"
Expand Down
Loading
Loading