First Commit

This commit is contained in:
cesnimda
2026-03-21 11:55:27 +01:00
commit 2e8a29b4d0
1757 changed files with 166084 additions and 0 deletions
@@ -0,0 +1,166 @@
import React, { useMemo, useState } from "react";
import {
Button,
Divider,
IconButton,
ListItemText,
Menu,
MenuItem,
TextField,
Tooltip,
} from "@mui/material";
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
export type SavedViewParams = {
q?: string;
status?: string;
companyId?: number;
location?: string;
needsFollowUp?: boolean;
};
type SavedView = {
id: string;
name: string;
params: SavedViewParams;
createdAt: string;
};
const KEY = "jt_saved_views_v1";
function loadViews(): SavedView[] {
try {
const raw = window.localStorage.getItem(KEY);
if (!raw) return [];
const v = JSON.parse(raw);
if (!Array.isArray(v)) return [];
return v as SavedView[];
} catch {
return [];
}
}
function saveViews(views: SavedView[]) {
window.localStorage.setItem(KEY, JSON.stringify(views));
}
export default function SavedViewsMenu({
current,
onApply,
}: {
current: SavedViewParams;
onApply: (p: SavedViewParams) => void;
}) {
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
const [name, setName] = useState("");
const [views, setViews] = useState<SavedView[]>(() => loadViews());
const hasAny = views.length > 0;
const canSave = useMemo(() => name.trim().length > 0, [name]);
const open = Boolean(anchor);
const add = () => {
if (!canSave) return;
const next: SavedView = {
id: `v_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
name: name.trim(),
params: { ...current },
createdAt: new Date().toISOString(),
};
const updated = [next, ...views].slice(0, 50);
setViews(updated);
saveViews(updated);
setName("");
};
const remove = (id: string) => {
const updated = views.filter((v) => v.id !== id);
setViews(updated);
saveViews(updated);
};
return (
<>
<Tooltip title="Saved views">
<IconButton size="small" onClick={(e) => setAnchor(e.currentTarget)}>
<BookmarkBorderIcon fontSize="small" />
</IconButton>
</Tooltip>
<Menu
anchorEl={anchor}
open={open}
onClose={() => setAnchor(null)}
PaperProps={{ sx: { minWidth: 320, p: 0.5 } }}
>
<MenuItem disableRipple sx={{ cursor: "default", alignItems: "flex-start" }}>
<ListItemText
primary="Saved views"
secondary="Save the current filters as a 1-click view."
/>
</MenuItem>
<MenuItem disableRipple sx={{ cursor: "default" }}>
<TextField
size="small"
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
</MenuItem>
<MenuItem disableRipple sx={{ cursor: "default", justifyContent: "flex-end" }}>
<Button variant="contained" size="small" onClick={add} disabled={!canSave}>
Save current
</Button>
</MenuItem>
<Divider />
{!hasAny && (
<MenuItem disabled>No saved views yet.</MenuItem>
)}
{views.map((v) => (
<MenuItem
key={v.id}
onClick={() => {
onApply(v.params);
setAnchor(null);
}}
sx={{ gap: 1 }}
>
<ListItemText
primary={v.name}
secondary={
[
v.params.status ? `Status: ${v.params.status}` : null,
v.params.location ? `Loc: ${v.params.location}` : null,
v.params.needsFollowUp ? "Needs follow-up" : null,
]
.filter(Boolean)
.join(" · ") || "No filters"
}
/>
<IconButton
size="small"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
remove(v.id);
}}
>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</MenuItem>
))}
</Menu>
</>
);
}