39 lines
4.7 KiB
TypeScript
39 lines
4.7 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { Alert, Box, Button, Chip, MenuItem, Paper, Stack, TextField, Typography } from "@mui/material";
|
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
|
import { api, getApiErrorMessage } from "../../api";
|
|
import { useConfirm } from "../../confirm";
|
|
import { useI18n } from "../../i18n/I18nProvider";
|
|
|
|
type Evidence = { id: number; category: string; title: string; statement: string; tags: string[]; sourceReference: string | null };
|
|
|
|
export default function CareerEvidenceBank() {
|
|
const { t } = useI18n(); const { confirm } = useConfirm();
|
|
const [items, setItems] = useState<Evidence[]>([]); const [title, setTitle] = useState(""); const [statement, setStatement] = useState("");
|
|
const [category, setCategory] = useState("achievement"); const [tags, setTags] = useState(""); const [source, setSource] = useState("");
|
|
const [error, setError] = useState<string | null>(null); const [busy, setBusy] = useState(false);
|
|
const load = () => api.get<Evidence[]>("/career/evidence").then(r => { setItems(r.data); setError(null); }).catch(e => setError(getApiErrorMessage(e, t("careerEvidenceLoadFailed"))));
|
|
useEffect(() => { void load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
const add = async () => { setBusy(true); try { const r = await api.post<Evidence>("/career/evidence", { category, title, statement, tags: tags.split(","), sourceType: "manual", sourceReference: source, isVerified: true }); setItems(v => [r.data, ...v]); setTitle(""); setStatement(""); setTags(""); setSource(""); setError(null); } catch (e) { setError(getApiErrorMessage(e, t("careerEvidenceSaveFailed"))); } finally { setBusy(false); } };
|
|
const remove = async (item: Evidence) => { if (!(await confirm({ title: t("careerEvidenceDeleteTitle"), message: t("careerEvidenceDeleteMessage", { title: item.title }), confirmLabel: t("deleteAction"), destructive: true }))) return; try { await api.delete(`/career/evidence/${item.id}`); setItems(v => v.filter(x => x.id !== item.id)); } catch (e) { setError(getApiErrorMessage(e, t("careerEvidenceDeleteFailed"))); } };
|
|
const categories = ["achievement", "metric", "project", "technology", "responsibility", "example"];
|
|
const categoryLabel = (value: string) => t(`careerEvidenceCategory_${value}` as "careerEvidenceCategory_achievement");
|
|
return <Stack spacing={2}>
|
|
<Box><Typography variant="h5" fontWeight={800}>{t("careerEvidenceTitle")}</Typography><Typography color="text.secondary">{t("careerEvidenceSubtitle")}</Typography></Box>
|
|
{error && <Alert severity="error" action={<Button color="inherit" onClick={() => void load()}>{t("retry")}</Button>}>{error}</Alert>}
|
|
<Paper variant="outlined" sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 3 }}><Stack spacing={1.5}>
|
|
<Typography variant="subtitle1" fontWeight={700}>{t("careerEvidenceAdd")}</Typography>
|
|
<TextField select size="small" label={t("careerEvidenceCategory")} value={category} onChange={e => setCategory(e.target.value)}>{categories.map(v => <MenuItem key={v} value={v}>{categoryLabel(v)}</MenuItem>)}</TextField>
|
|
<TextField label={t("careerEvidenceItemTitle")} value={title} onChange={e => setTitle(e.target.value)} />
|
|
<TextField label={t("careerEvidenceStatement")} value={statement} onChange={e => setStatement(e.target.value)} multiline minRows={3} helperText={t("careerEvidenceTruthHelp")} />
|
|
<TextField label={t("careerEvidenceTags")} value={tags} onChange={e => setTags(e.target.value)} helperText={t("careerEvidenceTagsHelp")} />
|
|
<TextField label={t("careerEvidenceSource")} value={source} onChange={e => setSource(e.target.value)} helperText={t("careerEvidenceSourceHelp")} />
|
|
<Button variant="contained" disabled={busy || !title.trim() || !statement.trim()} onClick={() => void add()} sx={{ alignSelf: "flex-start" }}>{t("careerEvidenceSave")}</Button>
|
|
</Stack></Paper>
|
|
{items.length === 0 ? <Alert severity="info">{t("careerEvidenceEmpty")}</Alert> : items.map(item => <Paper key={item.id} variant="outlined" sx={{ p: 2, borderRadius: 3 }}><Stack spacing={1}>
|
|
<Stack direction="row" justifyContent="space-between" gap={1}><Box><Typography fontWeight={700}>{item.title}</Typography><Typography variant="caption" color="text.secondary">{categoryLabel(item.category)} · {item.sourceReference || t("careerEvidenceManualSource")}</Typography></Box><Button color="error" size="small" startIcon={<DeleteOutlineIcon />} onClick={() => void remove(item)}>{t("deleteAction")}</Button></Stack>
|
|
<Typography variant="body2">{item.statement}</Typography><Stack direction="row" flexWrap="wrap" gap={0.5}>{item.tags.map(tag => <Chip key={tag} size="small" label={tag} />)}</Stack>
|
|
</Stack></Paper>)}
|
|
</Stack>;
|
|
}
|