feat(ui): separate career and connected accounts

This commit is contained in:
cesnimda
2026-07-15 10:37:02 +02:00
parent dab39bd570
commit d7d7e70d08
7 changed files with 221 additions and 28 deletions
+53
View File
@@ -0,0 +1,53 @@
# Jobbjakt Architecture Evolution
## Guiding constraints
- Preserve the existing single-master-profile model.
- Keep job applications and their workflow at the centre of the domain.
- Make AI providers replaceable and optional.
- Add SaaS foundations without prematurely building a billing platform or team product.
- Keep feature slices small while reducing the current controller concentration.
## Target boundaries
```text
Web UI
-> Application APIs
-> Application workflow domain
-> Career profile and document domain
-> Discovery provider adapters
-> Connected-account adapters
-> AI provider boundary
-> Entitlements and usage policy
```
### Career profile and documents
`ApplicationUser.ProfileCvStructureJson` remains the canonical master profile during the transition. A later migration may normalize high-change sections (projects and portfolio items) into user-owned tables, but generated CVs and application-specific drafts must remain immutable copies with a profile-version reference.
### Discovery
Create an `IJobDiscoveryProvider` contract with normalized query and result types. Providers own authentication, source-specific filters, terms-aware attribution, and mapping. The UI and import flow must not know provider payload shapes. Cache public results briefly, do not persist an external job until the user imports it.
### AI
Extract AI prompts and model transport from controllers behind task-focused interfaces: CV extraction, CV tailoring, draft generation, interview preparation, and summarization. Each request records provider, model, input profile version, job context, and user review state. Local and online providers implement the same boundary.
### Entitlements
Introduce account-level plan, entitlement, and usage-ledger records before payment integration. Product code checks named capabilities (for example `ai.tailor_cv`, `cv.premium_theme`, `storage.attachments`) rather than payment-provider state. This supports trials, manual access, subscriptions, and future organisation plans.
## Incremental engineering work
1. Add targeted feature services beside the existing controllers for every touched feature; do not attempt a big-bang controller rewrite.
2. Replace local-only preferences that affect behaviour with user-owned persisted settings.
3. Replace the in-memory CV queue with a database-backed work item before horizontal hosting.
4. Use PostgreSQL for hosted multi-tenant deployment; retain SQLite for self-hosted/single-user deployments.
5. Add provider health, job processing, AI usage, and entitlement audit telemetry before paid hosting.
## Explicit non-goals for the first SaaS release
- Teams/organisations and shared candidate data.
- Auto-apply automation or scraping-proxy infrastructure.
- A browser-extension marketplace release.
- A billing-provider integration before commercial policy is decided.
+81
View File
@@ -0,0 +1,81 @@
# Jobbjakt Product Evolution Plan
> Confirmed 2026-07-15. This document is the product decision record for the career-management evolution.
## Product position
Jobbjakt is a job-tracking and career-management product. The application workflow remains the core loop:
```text
Discover opportunity -> Track application -> Prepare application -> Follow up -> Learn and progress
```
Career profile, CV generation, cover letters, portfolio, interview preparation, and discovery support this loop. They must not displace it or turn Jobbjakt into a CV-builder-first product.
## Confirmed decisions
| Decision | Direction |
| --- | --- |
| Primary discovery market | Nordics and Europe; global later |
| Discovery sources | Permitted provider APIs and existing URL capture; no scraping or browser extension in the first release |
| Career workspace | Dedicated top-level navigation item |
| Add-job flow | Wizard; create the application after job review, then keep preparation steps optional and resumable |
| Portfolio | Lightweight links and project items connected to the master career profile |
| AI | Retain local and online capability behind a provider boundary and explicit user controls |
| SaaS priority | Hosted subscriptions and feature entitlements before organisations/teams |
| Brand | Keep the Jobbjakt name |
## Target information architecture
| Area | Purpose | Initial destinations |
| --- | --- | --- |
| Dashboard | Daily direction | priorities, follow-ups, recent activity |
| Applications | Core tracker | list, board, companies, application workspace |
| Career Workspace | Reusable career source of truth | master profile, CV builder, portfolio, interview preparation |
| Discover Jobs | Find then act | provider search, saved searches later, import into tracker |
| Settings | Product preferences | general, follow-ups, notifications, connected accounts, data |
| Account | Identity and security | personal details, password, providers, sessions, 2FA |
## Delivery roadmap
### Phase 1 — Workflow clarity
1. Create the Career Workspace and move CV-facing functionality out of Account Profile.
2. Move Gmail, Outlook, and IMAP management to Settings > Connected Accounts.
3. Reorganise Settings: remove normal-user export/import controls, hide admin-only status controls, and improve follow-up explanations.
4. Simplify application workspace navigation into task-oriented sections.
### Phase 2 — Application preparation
1. Replace the dense Add Job modal with a six-step wizard.
2. URL capture and extraction lead the flow; manual details remain an easy fallback.
3. Save the application after review; CV, cover letter, portfolio, and documents are explicitly optional.
4. Link job-specific CV copies and draft material to the application without mutating the master profile.
### Phase 3 — Career workspace completion
1. Strengthen the structured master career profile: experience, education, skills, projects, certifications, languages, achievements, and portfolio links.
2. Turn the existing template system into a guided three-step CV builder: theme, preview/customise, save defaults.
3. Keep five professional templates and persisted visual preferences; add drag-and-drop ordering after the underlying configuration is durable.
### Phase 4 — Nordic/European discovery
1. Introduce a provider-adapter contract and a normalized result model.
2. Implement approved public/provider APIs beginning with Nordic sources; add European sources only when their terms permit product use.
3. Provide search, filters, source attribution, map/distance enrichment where data permits, and one-click import to the wizard.
4. Add fuzzy role matching locally through a role/skill synonym catalog before any external AI ranking.
### Phase 5 — Hosted SaaS foundations
1. Add account-level entitlement and usage records independent of a billing provider.
2. Gate premium themes, AI allowances, storage, and discovery quotas through policy checks.
3. Integrate a billing provider only after pricing, tax, trial, cancellation, and support policy decisions are recorded.
4. Move restart-sensitive CV processing to durable storage before multi-instance hosting.
## Success measures
- A new user can create a tracked application from a URL in under two minutes.
- A user can find their master profile, create a CV, and manage email connections without visiting Account Profile.
- Optional preparation never blocks capture of an opportunity.
- Every AI-generated output visibly states its source profile/job context and requires user review before it is saved or sent.
- Discovery always preserves source attribution and sends users to the original job advert to apply.
+10
View File
@@ -16,6 +16,7 @@ import ShieldIcon from "@mui/icons-material/Shield";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import MailOutlineIcon from "@mui/icons-material/MailOutline"; import MailOutlineIcon from "@mui/icons-material/MailOutline";
import MemoryIcon from "@mui/icons-material/Memory"; import MemoryIcon from "@mui/icons-material/Memory";
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter, RouterProvider } from "react-router-dom"; import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter, RouterProvider } from "react-router-dom";
@@ -47,6 +48,8 @@ const SettingsView = lazy(() => import("./components/SettingsView"));
const RemindersView = lazy(() => import("./components/RemindersView")); const RemindersView = lazy(() => import("./components/RemindersView"));
const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog")); const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog"));
const ProfilePage = lazy(() => import("./views/ProfilePage")); const ProfilePage = lazy(() => import("./views/ProfilePage"));
const CareerWorkspacePage = lazy(() => import("./views/CareerWorkspacePage"));
const ConnectedAccountsPage = lazy(() => import("./views/ConnectedAccountsPage"));
const AdminAuditPage = lazy(() => import("./views/AdminAuditPage")); const AdminAuditPage = lazy(() => import("./views/AdminAuditPage"));
const AdminUsersPage = lazy(() => import("./views/AdminUsersPage")); const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
const AdminSystemPage = lazy(() => import("./views/AdminSystemPage")); const AdminSystemPage = lazy(() => import("./views/AdminSystemPage"));
@@ -78,6 +81,8 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
if (path.startsWith("/trash")) return [t("home"), t("trash")]; if (path.startsWith("/trash")) return [t("home"), t("trash")];
if (path.startsWith("/settings")) return [t("home"), t("settings")]; if (path.startsWith("/settings")) return [t("home"), t("settings")];
if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")]; if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")];
if (path.startsWith("/career")) return [t("home"), "Career Workspace"];
if (path.startsWith("/settings/connected-accounts")) return [t("home"), t("settings"), "Connected accounts"];
if (path.startsWith("/admin/audit")) return [t("home"), t("admin"), t("auditLog")]; if (path.startsWith("/admin/audit")) return [t("home"), t("admin"), t("auditLog")];
if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")]; if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")];
if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")]; if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")];
@@ -95,6 +100,8 @@ function titleFor(path: string, t: (k: any) => string): string {
if (path.startsWith("/trash")) return t("trash"); if (path.startsWith("/trash")) return t("trash");
if (path.startsWith("/settings")) return t("settings"); if (path.startsWith("/settings")) return t("settings");
if (path.startsWith("/profile")) return t("profile"); if (path.startsWith("/profile")) return t("profile");
if (path.startsWith("/career")) return "Career Workspace";
if (path.startsWith("/settings/connected-accounts")) return "Connected accounts";
if (path.startsWith("/admin/audit")) return t("auditLog"); if (path.startsWith("/admin/audit")) return t("auditLog");
if (path.startsWith("/admin/users")) return t("users"); if (path.startsWith("/admin/users")) return t("users");
if (path.startsWith("/admin/system")) return t("systemStatus"); if (path.startsWith("/admin/system")) return t("systemStatus");
@@ -234,6 +241,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
{ to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") }, { to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") },
{ to: "/correspondence", label: "Correspondence", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") }, { to: "/correspondence", label: "Correspondence", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") },
{ to: "/correspondence/review", label: "Gmail review", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") }, { to: "/correspondence/review", label: "Gmail review", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") },
{ to: "/career", label: "Career Workspace", icon: <DescriptionOutlinedIcon fontSize="small" />, section: t("manage") },
{ to: "/trash", label: t("trash"), icon: <DeleteOutlineIcon fontSize="small" />, section: t("manage") }, { to: "/trash", label: t("trash"), icon: <DeleteOutlineIcon fontSize="small" />, section: t("manage") },
]; ];
@@ -314,11 +322,13 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
<Route path="/correspondence" element={<CorrespondenceInboxPage />} /> <Route path="/correspondence" element={<CorrespondenceInboxPage />} />
<Route path="/correspondence/review" element={<GmailReviewPage />} /> <Route path="/correspondence/review" element={<GmailReviewPage />} />
<Route path="/profile" element={<ProfilePage />} /> <Route path="/profile" element={<ProfilePage />} />
<Route path="/career" element={<CareerWorkspacePage />} />
<Route path="/admin/audit" element={<AdminAuditPage />} /> <Route path="/admin/audit" element={<AdminAuditPage />} />
<Route path="/admin/users" element={<AdminUsersPage />} /> <Route path="/admin/users" element={<AdminUsersPage />} />
<Route path="/admin/system" element={<AdminSystemPage />} /> <Route path="/admin/system" element={<AdminSystemPage />} />
<Route path="/trash" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="trash" />} /> <Route path="/trash" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="trash" />} />
<Route path="/settings" element={<SettingsView pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} />} /> <Route path="/settings" element={<SettingsView pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} />} />
<Route path="/settings/connected-accounts" element={<ConnectedAccountsPage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</Suspense> </Suspense>
@@ -198,6 +198,9 @@ export default function SettingsView({
</SectionCard> </SectionCard>
<QuickCaptureCard /> <QuickCaptureCard />
<SectionCard title="Connected accounts" subtitle="Manage inbox connections separately from your account and security settings.">
<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>Manage connected accounts</Button>
</SectionCard>
</Box> </Box>
</TabPanel> </TabPanel>
@@ -226,7 +229,6 @@ export default function SettingsView({
</Typography> </Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}> <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button> <Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
<Button variant="text" onClick={() => navigate("/admin/system")}>{t("settingsCheckSystemStatus")}</Button>
</Box> </Box>
</SectionCard> </SectionCard>
</TabPanel> </TabPanel>
@@ -0,0 +1,22 @@
import React from "react";
import { Alert, Box, Paper, Typography } from "@mui/material";
import ProfilePage from "./ProfilePage";
export default function CareerWorkspacePage() {
return (
<Box sx={{ display: "grid", gap: 2 }}>
<Paper sx={{ p: 2.5, borderRadius: 4, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
<Typography variant="h5" sx={{ fontWeight: 900, mb: 0.5 }}>Career Workspace</Typography>
<Typography sx={{ color: "text.secondary" }}>
Maintain the master career profile that powers your CVs, tailored application material, and future portfolio outputs.
</Typography>
</Paper>
<Alert severity="info" sx={{ borderRadius: 3 }}>
Your master profile is the source of truth. Job-specific CV drafts remain separate and never overwrite it.
</Alert>
<ProfilePage careerOnly />
</Box>
);
}
@@ -0,0 +1,23 @@
import React from "react";
import { Box, Button, Paper, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import EmailProviderConnections from "../components/EmailProviderConnections";
export default function ConnectedAccountsPage() {
const navigate = useNavigate();
return (
<Box sx={{ display: "grid", gap: 2 }}>
<Paper sx={{ p: 2.5, borderRadius: 4, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
<Typography variant="h5" sx={{ fontWeight: 900, mb: 0.5 }}>Connected accounts</Typography>
<Typography sx={{ color: "text.secondary", maxWidth: 720 }}>
Connect the inboxes Jobbjakt may use to identify recruiter correspondence. Connecting an inbox never sends a message on your behalf.
</Typography>
<Button sx={{ mt: 1 }} onClick={() => navigate("/settings")}>Back to settings</Button>
</Paper>
<EmailProviderConnections />
</Box>
);
}
+29 -27
View File
@@ -13,7 +13,6 @@ import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
import AuthStatusCard from "../components/AuthStatusCard"; import AuthStatusCard from "../components/AuthStatusCard";
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard"; import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
import SessionsSettingsCard from "../components/SessionsSettingsCard"; import SessionsSettingsCard from "../components/SessionsSettingsCard";
import EmailProviderConnections from "../components/EmailProviderConnections";
import CropImageDialog from "../components/CropImageDialog"; import CropImageDialog from "../components/CropImageDialog";
import { useToast } from "../toast"; import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider"; import { useI18n } from "../i18n/I18nProvider";
@@ -227,7 +226,7 @@ function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata })
); );
} }
export default function ProfilePage() { export default function ProfilePage({ careerOnly = false }: { careerOnly?: boolean }) {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useI18n(); const { t } = useI18n();
const cvInputRef = useRef<HTMLInputElement | null>(null); const cvInputRef = useRef<HTMLInputElement | null>(null);
@@ -553,7 +552,7 @@ export default function ProfilePage() {
</Box> </Box>
<Box> <Box>
<Typography variant="h5" sx={{ fontWeight: 900 }}> <Typography variant="h5" sx={{ fontWeight: 900 }}>
{t("profileTitle")} {careerOnly ? "Master career profile" : t("profileTitle")}
</Typography> </Typography>
<Typography sx={{ color: "text.secondary" }}>{me?.userName || me?.displayName || fullName || me?.email || "-"}</Typography> <Typography sx={{ color: "text.secondary" }}>{me?.userName || me?.displayName || fullName || me?.email || "-"}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{headline || t("profileHeadlinePlaceholder")}</Typography> <Typography variant="body2" sx={{ color: "text.secondary" }}>{headline || t("profileHeadlinePlaceholder")}</Typography>
@@ -566,37 +565,38 @@ export default function ProfilePage() {
</Box> </Box>
</Box> </Box>
<AuthStatusCard /> {!careerOnly ? <>
<GoogleAuthCard /> <AuthStatusCard />
<MicrosoftAuthCard /> <GoogleAuthCard />
<Box sx={{ mt: 2 }}> <MicrosoftAuthCard />
<EmailProviderConnections /> </> : null}
</Box>
<Box sx={{ mt: 3, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}> <Box sx={{ mt: 3, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
<Box sx={{ gridColumn: "1 / -1" }}> {!careerOnly ? <Box sx={{ gridColumn: "1 / -1" }}>
<Typography variant="h6">{t("profileAccountSection")}</Typography> <Typography variant="h6">{t("profileAccountSection")}</Typography>
{!isLocal ? ( {!isLocal ? (
<Alert severity="info" sx={{ mt: 1 }}> <Alert severity="info" sx={{ mt: 1 }}>
{t("profileReadOnlyInfo")} {t("profileReadOnlyInfo")}
</Alert> </Alert>
) : null} ) : null}
</Box> </Box> : null}
<TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth /> {!careerOnly ? <>
<TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth /> <TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth />
<TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth /> <TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth />
<TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth /> <TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth />
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth /> <TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth />
<TextField <TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth />
<TextField
label={t("profileHeadline")} label={t("profileHeadline")}
value={headline} value={headline}
onChange={(e) => setHeadline(e.target.value)} onChange={(e) => setHeadline(e.target.value)}
helperText={t("profileHeadlineHelp")} helperText={t("profileHeadlineHelp")}
fullWidth fullWidth
/> />
</> : null}
<Box sx={{ gridColumn: "1 / -1", p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}> <Box sx={{ gridColumn: "1 / -1", p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", display: careerOnly ? "block" : "none" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}> <Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
<Box> <Box>
<Typography variant="h6">{t("profileMasterCv")}</Typography> <Typography variant="h6">{t("profileMasterCv")}</Typography>
@@ -1318,16 +1318,17 @@ export default function ProfilePage() {
</Button> </Button>
</Box> </Box>
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}> {!careerOnly ? <Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
<Divider sx={{ mb: 2 }} /> <Divider sx={{ mb: 2 }} />
<Typography variant="h6">{t("profileChangePassword")}</Typography> <Typography variant="h6">{t("profileChangePassword")}</Typography>
{!isLocal ? <Typography sx={{ color: "text.secondary" }}>{t("profilePasswordLocalOnly")}</Typography> : null} {!isLocal ? <Typography sx={{ color: "text.secondary" }}>{t("profilePasswordLocalOnly")}</Typography> : null}
</Box> </Box> : null}
<TextField label={t("profileCurrentPassword")} type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth /> {!careerOnly ? <>
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={!isLocal} fullWidth /> <TextField label={t("profileCurrentPassword")} type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth />
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={!isLocal} fullWidth />
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}> <Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
<Button <Button
variant="outlined" variant="outlined"
disabled={!isLocal || loading} disabled={!isLocal || loading}
@@ -1348,11 +1349,12 @@ export default function ProfilePage() {
> >
{t("profileUpdatePassword")} {t("profileUpdatePassword")}
</Button> </Button>
</Box> </Box>
</> : null}
</Box> </Box>
{isLocal ? <TwoFactorSettingsCard /> : null} {!careerOnly && isLocal ? <TwoFactorSettingsCard /> : null}
{isLocal ? <SessionsSettingsCard /> : null} {!careerOnly && isLocal ? <SessionsSettingsCard /> : null}
</Paper> </Paper>
); );
} }