Dashboard upgrades, workflows added and assitant emailer

This commit is contained in:
cesnimda
2026-03-21 13:25:13 +01:00
parent 8cc4b0dfce
commit 51a539068f
9 changed files with 1358 additions and 1421 deletions
@@ -0,0 +1,156 @@
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";
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 [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: "Go to dashboard", hint: "Analytics overview", action: () => onNavigate("/dashboard") },
{ id: "go-jobs", label: "Go to jobs", hint: "Main applications table", action: () => onNavigate("/jobs") },
{ id: "go-reminders", label: "Go to reminders", hint: "Follow-up queue", action: () => onNavigate("/reminders") },
{ id: "go-companies", label: "Go to companies", hint: "CRM and source tracking", action: () => onNavigate("/companies") },
{ id: "go-settings", label: "Go to settings", hint: "Preferences and admin tools", action: () => onNavigate("/settings") },
{ id: "add-job", label: "Add new job", hint: "Open the add-job modal", 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]);
const allItems: CommandItem[] = [
...commands,
...jobs.slice(0, 6).map((job) => ({
id: `job-${job.id}`,
label: `${job.company?.name ?? "Company"} - ${job.jobTitle}`,
hint: "Open job list and search result",
action: () => onNavigate(`/jobs`),
})),
...companies.slice(0, 6).map((company) => ({
id: `company-${company.id}`,
label: company.name,
hint: "Open companies",
action: () => onNavigate(`/companies`),
})),
];
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="Search jobs, companies, or actions"
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" }}>No matching commands or records.</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>
);
}