109745edb0
Make /jobs/:id the canonical application workspace while preserving list state and compatibility links. Replace popup and expandable-row navigation with accessible whole-row routing and richer job details.
159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
|
|
|
import {
|
|
Box,
|
|
Dialog,
|
|
DialogContent,
|
|
List,
|
|
ListItemButton,
|
|
ListItemText,
|
|
TextField,
|
|
Typography,
|
|
} from "@mui/material";
|
|
|
|
import SearchIcon from "@mui/icons-material/Search";
|
|
|
|
import { api } from "../api";
|
|
import { useI18n } from "../i18n/I18nProvider";
|
|
|
|
type CommandItem = {
|
|
id: string;
|
|
label: string;
|
|
hint?: string;
|
|
action: () => void;
|
|
};
|
|
|
|
type JobSearchItem = {
|
|
id: number;
|
|
jobTitle: string;
|
|
company: { name: string };
|
|
};
|
|
|
|
type CompanySearchItem = {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onNavigate: (to: string) => void;
|
|
onOpenAddJob: () => void;
|
|
}
|
|
|
|
export default function QuickCommandDialog({ open, onClose, onNavigate, onOpenAddJob }: Props) {
|
|
const { t } = useI18n();
|
|
const [query, setQuery] = useState("");
|
|
const [jobs, setJobs] = useState<JobSearchItem[]>([]);
|
|
const [companies, setCompanies] = useState<CompanySearchItem[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setQuery("");
|
|
setJobs([]);
|
|
setCompanies([]);
|
|
return;
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const q = query.trim();
|
|
if (!q) {
|
|
setJobs([]);
|
|
setCompanies([]);
|
|
return;
|
|
}
|
|
|
|
const timeout = window.setTimeout(() => {
|
|
api
|
|
.get<{ items: JobSearchItem[] }>("/jobapplications", { params: { q, page: 1, pageSize: 15 } })
|
|
.then((r) => setJobs(r.data?.items ?? []))
|
|
.catch(() => setJobs([]));
|
|
api
|
|
.get<CompanySearchItem[]>("/companies")
|
|
.then((r) => {
|
|
const matches = (r.data ?? []).filter((c) => c.name.toLowerCase().includes(q.toLowerCase())).slice(0, 8);
|
|
setCompanies(matches);
|
|
})
|
|
.catch(() => setCompanies([]));
|
|
}, 250);
|
|
|
|
return () => window.clearTimeout(timeout);
|
|
}, [open, query]);
|
|
|
|
const commands = useMemo<CommandItem[]>(() => {
|
|
const base: CommandItem[] = [
|
|
{ id: "go-dashboard", label: t("goToDashboard"), hint: t("analyticsOverview"), action: () => onNavigate("/dashboard") },
|
|
{ id: "go-jobs", label: t("goToJobs"), hint: t("mainApplicationsTable"), action: () => onNavigate("/jobs") },
|
|
{ id: "go-reminders", label: t("goToReminders"), hint: t("followUpQueue"), action: () => onNavigate("/reminders") },
|
|
{ id: "go-companies", label: t("goToCompanies"), hint: t("crmAndSourceTracking"), action: () => onNavigate("/companies") },
|
|
{ id: "go-settings", label: t("goToSettings"), hint: t("preferencesAndAdminTools"), action: () => onNavigate("/settings") },
|
|
{ id: "add-job", label: t("addNewJob"), hint: t("openAddJobModal"), action: onOpenAddJob },
|
|
];
|
|
|
|
const q = query.trim().toLowerCase();
|
|
if (!q) return base;
|
|
return base.filter((item) => item.label.toLowerCase().includes(q) || item.hint?.toLowerCase().includes(q));
|
|
}, [onNavigate, onOpenAddJob, query, t]);
|
|
|
|
const allItems: CommandItem[] = [
|
|
...commands,
|
|
...jobs.slice(0, 6).map((job) => ({
|
|
id: `job-${job.id}`,
|
|
label: `${job.company?.name ?? t("company")} - ${job.jobTitle}`,
|
|
hint: t("openJobListAndSearchResult"),
|
|
action: () => onNavigate(`/jobs/${job.id}`),
|
|
})),
|
|
...companies.slice(0, 6).map((company) => ({
|
|
id: `company-${company.id}`,
|
|
label: company.name,
|
|
hint: t("openCompanies"),
|
|
action: () => onNavigate(`/companies?edit=${company.id}`),
|
|
})),
|
|
];
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
|
|
<DialogContent sx={{ p: 1.5 }}>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, pt: 0.5 }}>
|
|
<SearchIcon fontSize="small" />
|
|
<TextField
|
|
autoFocus
|
|
variant="standard"
|
|
fullWidth
|
|
placeholder={t("searchPlaceholder")}
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
InputProps={{ disableUnderline: true }}
|
|
/>
|
|
<Typography variant="caption" sx={{ color: "text.secondary", whiteSpace: "nowrap" }}>
|
|
Ctrl/Cmd + K
|
|
</Typography>
|
|
</Box>
|
|
|
|
<List sx={{ mt: 1 }}>
|
|
{allItems.length === 0 ? (
|
|
<Box sx={{ px: 2, py: 3 }}>
|
|
<Typography sx={{ color: "text.secondary" }}>{t("noMatchingCommands")}</Typography>
|
|
</Box>
|
|
) : (
|
|
allItems.map((item) => (
|
|
<ListItemButton
|
|
key={item.id}
|
|
onClick={() => {
|
|
item.action();
|
|
onClose();
|
|
}}
|
|
sx={{ borderRadius: 2 }}
|
|
>
|
|
<ListItemText primary={item.label} secondary={item.hint} />
|
|
</ListItemButton>
|
|
))
|
|
)}
|
|
</List>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|