Localize kanban, auth, backup, and admin utilities

This commit is contained in:
cesnimda
2026-03-23 21:05:49 +01:00
parent 9661a321da
commit 2c03379504
11 changed files with 181 additions and 75 deletions
@@ -233,7 +233,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
await load();
toast(t("correspondenceLogEmail"), "success");
} catch (error) {
toast(getApiErrorMessage(error, "Failed to import email."), "error");
toast(getApiErrorMessage(error, t("addJobModalImportFailed")), "error");
}
};
@@ -241,9 +241,9 @@ export default function Correspondence({ jobId }: { jobId: number }) {
try {
const res = await api.get<{ url: string }>("/gmail/connect-url");
const popup = window.open(res.data.url, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes");
if (!popup) toast("Your browser blocked the Gmail popup.", "error");
if (!popup) toast(t("correspondenceBlockedPopup"), "error");
} catch (error) {
toast(getApiErrorMessage(error, "Failed to start Gmail connection."), "error");
toast(getApiErrorMessage(error, t("correspondenceStartGmailFailed")), "error");
}
};
@@ -254,18 +254,18 @@ export default function Correspondence({ jobId }: { jobId: number }) {
setGmailMessages([]);
toast(t("googleUnlinked"), "success");
} catch (error) {
toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error");
toast(getApiErrorMessage(error, t("correspondenceDisconnectFailed")), "error");
}
};
const deleteMessage = async (messageId: number) => {
if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: t("jobTableDeleteSelected"), destructive: true }))) return;
if (!(await confirmAction(t("correspondenceDeleteConfirm"), { title: t("correspondenceDeleteTitle"), confirmLabel: t("adminUsersDelete"), destructive: true }))) return;
try {
await api.delete(`/correspondence/${messageId}`);
await load();
toast("Message removed.", "success");
toast(t("correspondenceDeleted"), "success");
} catch (error) {
toast(getApiErrorMessage(error, "Failed to remove message."), "error");
toast(getApiErrorMessage(error, t("correspondenceDeleteFailed")), "error");
}
};
@@ -276,7 +276,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
await load();
toast(t("correspondenceImportEmail"), "success");
} catch (error: any) {
toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error");
toast(getApiErrorMessage(error, t("correspondenceImportGmailFailed")), "error");
} finally {
setImportingMessageId(null);
}
@@ -287,9 +287,9 @@ export default function Correspondence({ jobId }: { jobId: number }) {
setImportingThreadId(threadId);
const res = await api.post<{ imported: number; skipped: number; threadId?: string }>("/gmail/import-thread", { jobApplicationId: jobId, threadId, messageIds });
await load();
toast(`Imported ${res.data.imported} messages${res.data.skipped ? `, skipped ${res.data.skipped} duplicates` : ""}.`, "success");
toast(t("correspondenceImportThreadResult", { imported: res.data.imported, skippedText: res.data.skipped ? t("correspondenceImportThreadSkipped", { count: res.data.skipped }) : "" }), "success");
} catch (error: any) {
toast(getApiErrorMessage(error, "Failed to import Gmail thread."), "error");
toast(getApiErrorMessage(error, t("correspondenceImportThreadFailed")), "error");
} finally {
setImportingThreadId(null);
}
@@ -299,7 +299,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
<Box>
<Paper ref={scrollRef} sx={{ p: 1.5, maxHeight: 360, overflowY: "auto", background: theme.palette.mode === "dark" ? "rgba(15,23,42,0.45)" : "rgba(255,255,255,0.75)", backdropFilter: "blur(8px)" }}>
{messages.length === 0 ? (
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>No messages yet.</Typography>
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("correspondenceNoMessages")}</Typography>
) : (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{messages.map((m) => {
@@ -384,12 +384,12 @@ export default function Correspondence({ jobId }: { jobId: number }) {
<TextField label={t("correspondenceSearchGmail")} value={gmailQuery} onChange={(e) => setGmailQuery(e.target.value)} placeholder={t("correspondenceSearchGmailPlaceholder")} size="small" fullWidth />
<Button variant="outlined" onClick={() => void loadGmailMessages()} disabled={gmailMessagesLoading}>{t("correspondenceSearch")}</Button>
</Box>
{gmailStatus.lastSyncedAt ? <Chip label={`Last synced ${new Date(gmailStatus.lastSyncedAt).toLocaleString()}`} size="small" /> : null}
{gmailStatus.lastSyncedAt ? <Chip label={t("correspondenceLastSynced", { date: new Date(gmailStatus.lastSyncedAt).toLocaleString() })} size="small" /> : null}
<Paper variant="outlined" sx={{ maxHeight: 420, overflowY: "auto" }}>
{gmailMessagesLoading ? (
<Box sx={{ py: 5, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box>
) : groupedByThread.length === 0 ? (
<Typography sx={{ color: "text.secondary", p: 2 }}>No Gmail messages found.</Typography>
<Typography sx={{ color: "text.secondary", p: 2 }}>{t("correspondenceNoGmailMessages")}</Typography>
) : (
<List disablePadding>
{groupedByThread.map(({ threadId, items }, threadIndex) => (
@@ -398,11 +398,11 @@ export default function Correspondence({ jobId }: { jobId: number }) {
<Box sx={{ p: 1.5, backgroundColor: alpha(theme.palette.primary.main, 0.04) }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1 }}>
<Box>
<Typography sx={{ fontWeight: 800 }}>{items[0]?.subject || "(No subject)"}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{items.length} message{items.length === 1 ? "" : "s"} in thread</Typography>
<Typography sx={{ fontWeight: 800 }}>{items[0]?.subject || t("correspondenceNoSubject")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("correspondenceMessagesInThread", { count: items.length })}</Typography>
</Box>
<Button startIcon={<MailOutlineIcon />} variant="outlined" size="small" disabled={importingThreadId === threadId} onClick={() => void importGmailThread(threadId, items.map((x) => x.id))}>
{importingThreadId === threadId ? "Importing..." : "Import thread"}
{importingThreadId === threadId ? t("correspondenceImporting") : t("correspondenceImportThread")}
</Button>
</Box>
{items.map((message, index) => (
@@ -410,11 +410,11 @@ export default function Correspondence({ jobId }: { jobId: number }) {
{index > 0 ? <Divider sx={{ my: 1 }} /> : null}
<ListItemButton sx={{ alignItems: "flex-start", px: 0, py: 1 }}>
<ListItemText
primary={<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}><Typography sx={{ fontWeight: 700 }}>{message.subject || "(No subject)"}</Typography><Typography variant="caption" sx={{ color: "text.secondary" }}>{message.date ? new Date(message.date).toLocaleString() : ""}</Typography></Box>}
secondary={<Box sx={{ mt: 0.5 }}><Typography variant="body2" sx={{ color: "text.primary" }}>From: {message.from || t("correspondenceUnknown")}</Typography><Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>{message.snippet}</Typography></Box>}
primary={<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}><Typography sx={{ fontWeight: 700 }}>{message.subject || t("correspondenceNoSubject")}</Typography><Typography variant="caption" sx={{ color: "text.secondary" }}>{message.date ? new Date(message.date).toLocaleString() : ""}</Typography></Box>}
secondary={<Box sx={{ mt: 0.5 }}><Typography variant="body2" sx={{ color: "text.primary" }}>{t("correspondenceFromLabel", { value: message.from || t("correspondenceUnknown") })}</Typography><Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>{message.snippet}</Typography></Box>}
/>
<Button variant="contained" size="small" disabled={importingMessageId === message.id} onClick={() => void importGmailMessage(message.id)}>
{importingMessageId === message.id ? "Importing..." : "Import"}
{importingMessageId === message.id ? t("correspondenceImporting") : t("correspondenceImportEmail")}
</Button>
</ListItemButton>
</React.Fragment>