Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/OrgSettingsPanels.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/OrgSettingsPanels.tsx |
| Original SHA-256 | 926d56758e63b75c314c17d484968b60370fefc05f0ea945251b7a21687c2337 |
| Git blob SHA | 937d1fef41747f882f623cd256ab75df5a0828c3 |
| Reading-copy SHA-256 | 9cbb97403254434149ba4614be0f01297eb744a88bfa543b130b6367b295d585 |
Recovered source transcript
Download complete source text ↓603 lines · 23.6 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
import { useState } from 'react'
import { AlertTriangle, CheckCircle2, Mail, Send, Trash2, Users2 } from 'lucide-react'
import { Link } from 'react-router-dom'
import { Panel, Badge } from '../components/ui'
import { Empty, Labeled, Note, Select, SettingRow, Toggle, btnGhost, btnPrimary, fieldCls } from '../components/form'
import {
useOrgSettings,
useSaveSettings,
useSetMailPassword,
useClearMailPassword,
useTestMail,
} from '../lib/settingsQueries'
import {
SENSITIVITY_CHOICES,
VISIBILITY_CHOICES,
SCOPE_CHOICES,
type OrgSettings,
} from '../lib/settings'
// THE ORGANIZATION SETTINGS PANELS.
//
// Owner only. Every panel is mounted behind a capability check in Settings.tsx, because the server refuses
// these for anyone else and offering a control that fails is worse than not offering it.
//
// THE SETTINGS SAVE ON CHANGE, not behind a Save button. There is no draft to lose and no ambiguity about
// whether what is on screen is what is in effect. Mail is the one exception, because its fields are typed
// rather than chosen and a half typed host would be saved on every keystroke.
// Choices carry a hint the settings vocabulary defines, which the shared Select does not render, so the
// selected hint is shown beneath the row instead. That keeps the explanation attached to the ANSWER rather
// than hidden inside a list somebody has to open to read.
function hintFor<T extends string>(choices: { value: T; hint: string }[], value: T): string {
return choices.find((c) => c.value === value)?.hint ?? ''
}
export function WhoSeesWhatPanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const s = q.data?.settings
if (!s) return null
const set = (patch: Partial<OrgSettings>) => save.mutate(patch)
return (
<Panel title="Who sees what" hint="pay data, contact details, and each other's work">
<p className="pb-1 text-[11px] leading-relaxed text-faint">
These narrow what people see on records they can already open. They never grant more than a
person's role allows, and the server decides, so nothing here can be worked around in a browser.
</p>
{/* The two rows govern DIFFERENT things and the labels now say which. Rates cover the bill rate, the pay
rate and their difference, because anybody holding both can subtract them. The margin setting covers
the wrap and everything computed from it, which is the firm's own cost structure and appears nowhere
else. Before the wrap existed these were the same disclosure wearing two names. */}
<SettingRow
label="Bill and pay rates, and the spread"
hint={hintFor(SENSITIVITY_CHOICES, s.rate_visibility)}
>
<Select value={s.rate_visibility} choices={SENSITIVITY_CHOICES} onChange={(v) => set({ rate_visibility: v })} />
</SettingRow>
<SettingRow
label="Your wrap rate, cost and margin"
hint={hintFor(SENSITIVITY_CHOICES, s.margin_visibility)}
>
<Select
value={s.margin_visibility}
choices={SENSITIVITY_CHOICES}
onChange={(v) => set({ margin_visibility: v })}
/>
</SettingRow>
<SettingRow label="Candidate contact details" hint={hintFor(SENSITIVITY_CHOICES, s.contact_visibility)}>
<Select
value={s.contact_visibility}
choices={SENSITIVITY_CHOICES}
onChange={(v) => set({ contact_visibility: v })}
/>
</SettingRow>
<SettingRow label="Seeing who is working a record" hint={hintFor(VISIBILITY_CHOICES, s.work_visibility)}>
<Select value={s.work_visibility} choices={VISIBILITY_CHOICES} onChange={(v) => set({ work_visibility: v })} />
</SettingRow>
<SettingRow
label="Show recent visits"
hint="a note that a colleague opened this record recently, which is how two people avoid calling the same candidate on the same day"
>
<Toggle
label="Show recent visits"
value={s.show_activity_presence}
onChange={(v) => set({ show_activity_presence: v })}
/>
</SettingRow>
<SettingRow
label="Ask people to claim before working"
hint="a warning, never a block, because a desk has to be able to move on a deadline day"
>
<Toggle
label="Ask people to claim before working"
value={s.claim_required}
onChange={(v) => set({ claim_required: v })}
/>
</SettingRow>
<p className="pt-2 text-[11px] leading-relaxed text-faint">
Managers and the owner see their team's activity at every setting. A manager who cannot see a
collision is the one person who could have fixed it.
</p>
</Panel>
)
}
export function RecordScopePanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const s = q.data?.settings
if (!s) return null
const narrowed = s.staff_record_scope !== 'organization'
return (
<Panel title="Which records people can see" hint="the whole bench, their team's, or their own">
<p className="pb-1 text-[11px] leading-relaxed text-faint">
This decides which records exist for a person at all, in every list, search and report. It is
stronger than the settings above, which only hide fields on records somebody can already open.
</p>
<SettingRow label="Recruiters, members and viewers" hint={hintFor(SCOPE_CHOICES, s.staff_record_scope)}>
<Select
value={s.staff_record_scope}
choices={SCOPE_CHOICES}
onChange={(v) => save.mutate({ staff_record_scope: v })}
/>
</SettingRow>
<SettingRow label="Managers" hint={hintFor(SCOPE_CHOICES, s.manager_record_scope)}>
{/* A manager cannot be narrowed to their own records. They could not run a desk, and the server
rejects it, so the option is absent rather than offered and refused. */}
<Select
value={s.manager_record_scope}
choices={SCOPE_CHOICES.filter((c) => c.value !== 'own')}
onChange={(v) => save.mutate({ manager_record_scope: v as 'team' | 'organization' })}
/>
</SettingRow>
<SettingRow
label="Records nobody owns"
hint="an unowned candidate nobody can see is a candidate nobody will ever work"
>
<Toggle
label="Records nobody owns"
value={s.unowned_records_visible}
onChange={(v) => save.mutate({ unowned_records_visible: v })}
/>
</SettingRow>
{narrowed && (
<div className="mt-2">
<Note tone="warn">
Scope is narrowed, so people will see fewer records than the bench holds and searches will say so
rather than looking empty. Put people on teams under{' '}
<Link to="/admin" className="underline">
accounts and access
</Link>
, or team scope resolves to each person alone.
</Note>
</div>
)}
<p className="pt-2 text-[11px] leading-relaxed text-faint">
The owner and administrators always see everything. Somebody has to, and it is the people accountable
for the account.
</p>
</Panel>
)
}
// MailPanel has a real sequence to it: configure, store a password, prove it works, then switch delivery on.
// The server enforces that order, so the panel shows where you are in it rather than presenting four
// controls and letting somebody discover the order by failing.
export function MailPanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const setPw = useSetMailPassword()
const clearPw = useClearMailPassword()
const test = useTestMail()
const [pw, setPwValue] = useState('')
const [draft, setDraft] = useState<Partial<OrgSettings>>({})
const s = q.data?.settings
if (!s) return null
const canStoreSecret = q.data?.secret_storage_available ?? false
const v = <K extends keyof OrgSettings>(k: K): OrgSettings[K] =>
k in draft ? (draft[k] as OrgSettings[K]) : s[k]
const verified = !!s.smtp_verified_at
const dirty = Object.keys(draft).length > 0
return (
<Panel title="Sending email" hint="invitations and candidate messages leave from your own address">
<p className="pb-3 text-[11px] leading-relaxed text-faint">
Messages go through your own mail server, so they arrive from your address and pass your own SPF and
DKIM rather than a shared sender nobody recognises. Until this is set up, inviting a teammate returns
a link for you to send by hand.
</p>
{!canStoreSecret && (
<div className="mb-3">
<Note tone="warn">
This server cannot store a password yet, so nothing here will save. The organization secret key is
not configured.
</Note>
</div>
)}
<div className="grid gap-2.5 sm:grid-cols-2">
<Labeled label="Mail server">
<input
value={String(v('smtp_host') ?? '')}
maxLength={255}
placeholder="smtp.yourcompany.com"
onChange={(e) => setDraft({ ...draft, smtp_host: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Port">
<input
type="number"
value={Number(v('smtp_port') ?? 587)}
onChange={(e) => setDraft({ ...draft, smtp_port: Number(e.target.value) })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Sign in name">
<input
value={String(v('smtp_username') ?? '')}
maxLength={255}
onChange={(e) => setDraft({ ...draft, smtp_username: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Security">
<select
value={String(v('smtp_security'))}
onChange={(e) => setDraft({ ...draft, smtp_security: e.target.value as OrgSettings['smtp_security'] })}
className={fieldCls}
>
<option value="starttls" className="bg-navy-900">
STARTTLS, usually port 587
</option>
<option value="implicit_tls" className="bg-navy-900">
TLS from the start, usually port 465
</option>
</select>
</Labeled>
<Labeled label="Send from">
<input
value={String(v('email_from_address') ?? '')}
maxLength={255}
placeholder="talent@yourcompany.com"
onChange={(e) => setDraft({ ...draft, email_from_address: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Sender name">
<input
value={String(v('email_from_name') ?? '')}
maxLength={120}
placeholder="Acme Recruiting"
onChange={(e) => setDraft({ ...draft, email_from_name: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Replies go to" hint="leave this empty and replies go to the sending address">
<input
value={String(v('email_reply_to') ?? '')}
maxLength={255}
onChange={(e) => setDraft({ ...draft, email_reply_to: e.target.value })}
className={fieldCls}
/>
</Labeled>
</div>
{dirty && (
<div className="mt-3 space-y-2">
{/* Changing the connection clears the proof that it worked and switches delivery off. Saying so
BEFORE the click is the difference between a deliberate step and a surprise. */}
{verified && (
<Note tone="warn">Saving these switches sending off until you test the connection again.</Note>
)}
<div className="flex gap-2">
<button
type="button"
disabled={save.isPending || !canStoreSecret}
onClick={() => save.mutate(draft, { onSuccess: () => setDraft({}) })}
className={btnPrimary}
>
{save.isPending ? 'Saving' : 'Save mail settings'}
</button>
<button type="button" onClick={() => setDraft({})} className={btnGhost}>
Discard
</button>
</div>
</div>
)}
{save.isError && (
<div className="mt-2">
<Note tone="risk">{(save.error as Error).message}</Note>
</div>
)}
<div className="mt-4 border-t border-line/40 pt-3">
<Labeled label="Mail password" hint="stored encrypted and never shown again, to anybody, including you">
<div className="flex gap-2">
<input
type="password"
value={pw}
maxLength={255}
disabled={!canStoreSecret}
onChange={(e) => setPwValue(e.target.value)}
placeholder={s.smtp_password_set ? 'a password is [credential removed], type to replace it' : 'not set'}
className={fieldCls}
/>
<button
type="button"
disabled={!pw || setPw.isPending || !canStoreSecret}
onClick={() => setPw.mutate(pw, { onSuccess: () => setPwValue('') })}
className={btnGhost}
>
Save
</button>
{s.smtp_password_set && (
<button type="button" onClick={() => clearPw.mutate()} className={btnGhost}>
<Trash2 size={13} /> Remove
</button>
)}
</div>
</Labeled>
</div>
<div className="mt-4 flex flex-wrap items-center gap-3 border-t border-line/40 pt-3">
<button
type="button"
onClick={() => test.mutate()}
disabled={test.isPending || !s.smtp_host || !s.smtp_password_set}
className={btnGhost}
>
<Send size={13} /> {test.isPending ? 'Sending' : 'Send a test to yourself'}
</button>
{verified ? (
<Badge color="var(--color-ok)" subtle>
<CheckCircle2 size={11} /> Verified
</Badge>
) : (
<Badge color="var(--color-warn)" subtle>
<AlertTriangle size={11} /> Not verified
</Badge>
)}
<div className="ml-auto flex items-center gap-2">
<span className="text-sm font-semibold text-ink">Send invitations and messages</span>
<Toggle
label="Send invitations and messages"
value={s.email_delivery_enabled}
disabled={!verified}
onChange={(val) => save.mutate({ email_delivery_enabled: val })}
/>
</div>
</div>
<div className="mt-2 space-y-2">
{test.isSuccess && (
<Note tone="ok">
<Mail size={11} className="mr-1 inline" />
Sent to {test.data?.to}. If it does not arrive within a few minutes, check the spam folder before
changing anything here.
</Note>
)}
{test.isError && <Note tone="risk">{(test.error as Error).message}</Note>}
{s.smtp_last_error && !test.isSuccess && !test.isError && (
<Note tone="risk">The last attempt failed: {s.smtp_last_error}</Note>
)}
{!verified && s.smtp_host && (
<p className="text-[11px] leading-relaxed text-faint">
Sending stays off until a test message actually leaves. That way a typo surfaces here rather than
as invitations quietly going nowhere.
</p>
)}
</div>
</Panel>
)
}
// THE WRAP RATE, which is the setting that decides whether this product can report a margin at all.
//
// Until a firm enters one, every money figure in the product is a SPREAD, bill minus pay, and the screens say
// so. That is not a limitation to apologise for, it is the honest position: without knowing what employing
// somebody costs, the difference between two rates is all that can be computed, and calling it profit would
// be a guess dressed as a number.
export function WrapRatePanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const [draft, setDraft] = useState<string | null>(null)
const s = q.data?.settings
if (!s) return null
const stored = s.default_burden_pct ?? ''
const shown = draft ?? stored
const dirty = draft !== null && draft !== stored
// Entered as a fraction and echoed as a percentage, because a firm says "our wrap is thirty five percent"
// and stores 0.35. Showing both removes the one ambiguity that would otherwise be entered wrong.
const asPercent = (() => {
const n = Number(shown)
if (!shown || Number.isNaN(n)) return ''
return `${(n * 100).toFixed(2).replace(/\.?0+$/, '')} percent`
})()
return (
<Panel title="Your wrap rate" hint="what employing somebody costs on top of their pay">
<p className="pb-2 text-[11px] leading-relaxed text-faint">
Payroll taxes, workers compensation, unemployment insurance, health cover, paid leave, retirement,
general and administrative, and overhead. Enter it as a fraction, so a 35 percent wrap is 0.35.
</p>
<div className="flex flex-wrap items-end gap-2">
<Labeled label="Standard wrap" hint={asPercent}>
<input
value={shown}
maxLength={12}
inputMode="decimal"
placeholder="0.35"
onChange={(e) => setDraft(e.target.value)}
className={`${fieldCls} max-w-[10rem]`}
/>
</Labeled>
{dirty && (
<button
type="button"
disabled={save.isPending}
onClick={() =>
save.mutate(
// An emptied box CLEARS the wrap, sent as an explicit null. Omitting the field would mean
// leave it alone, and an owner who deleted the number would watch nothing happen.
{ default_burden_pct: shown.trim() === '' ? null : shown.trim() },
{ onSuccess: () => setDraft(null) },
)
}
className={btnPrimary}
>
{save.isPending ? 'Saving' : 'Save the wrap'}
</button>
)}
{dirty && (
<button type="button" className={btnGhost} onClick={() => setDraft(null)}>
Discard
</button>
)}
</div>
{save.isError && (
<div className="mt-2">
<Note tone="risk">{(save.error as Error).message}</Note>
</div>
)}
<div className="mt-3">
{stored === '' ? (
<Note tone="warn">
No wrap is set, so the product reports SPREADS and no margins. A spread is bill minus pay before
any employment cost, and on typical numbers it is close to three times the money actually kept.
Nothing here is guessed on your behalf.
</Note>
) : (
<Note tone="muted">
Margins are reported using this wrap unless a placement carries its own. It is one figure for the
whole firm, which is right for most lines; a client contract or a labour category with a different
wrap is set on the record itself.
</Note>
)}
</div>
</Panel>
)
}
// THE AUTOMATED DECISION RULE, which is the one setting on this screen with a legal deadline attached.
export function AedtNoticePanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const s = q.data?.settings
if (!s) return null
return (
<Panel title="Automated decision notices" hint="telling candidates before a tool assesses them">
<p className="pb-2 text-[11px] leading-relaxed text-faint">
This product scores people with an automated employment decision tool. New York City requires the
person to be told at least ten business days beforehand and to be told what is assessed. The EU treats
hiring as high risk from August 2026. Illinois and Colorado have their own rules. The common shape is
the same everywhere: tell them first, and be able to prove you did.
</p>
<SettingRow
label="Refuse to score anybody with no notice on file"
hint="a recruiter is told exactly what is missing and can record it in a click on the candidate record"
>
<Toggle
label="Refuse to score anybody with no notice on file"
value={s.aedt_notice_required}
onChange={(v) => save.mutate({ aedt_notice_required: v })}
/>
</SettingRow>
{!s.aedt_notice_required && (
<div className="mt-2">
<Note tone="warn">
This is off, so people can be scored with no notice recorded. It ships off on purpose: switching it
on before your notices are in place would refuse your whole bench. Recording a notice works either
way, on every candidate record.
</Note>
</div>
)}
<p className="pt-2 text-[11px] leading-relaxed text-faint">
missionMATCH never sends the notice for you. Delivering it is yours; what this holds is the evidence
that you did, per candidate, with the date and what they were told.
</p>
</Panel>
)
}
export function InviteDefaultsPanel() {
const q = useOrgSettings()
const save = useSaveSettings()
const s = q.data?.settings
if (!s) return null
const HOURS = [
{ value: '24', label: '1 day' },
{ value: '72', label: '3 days' },
{ value: '168', label: '7 days' },
{ value: '336', label: '14 days' },
]
const CLAIM_HOURS = [
{ value: '8', label: '8 hours' },
{ value: '24', label: '1 day' },
{ value: '72', label: '3 days' },
{ value: '168', label: '7 days' },
]
const ROLES = [
{ value: 'viewer', label: 'Viewer' },
{ value: 'member', label: 'Member' },
{ value: 'recruiter', label: 'Recruiter' },
{ value: 'manager', label: 'Manager' },
{ value: 'admin', label: 'Administrator' },
]
return (
<Panel title="Defaults" hint="the small settings that only matter on the day they are wrong">
<SettingRow label="Invitation link lifetime" hint="an unused invitation stops working after this">
<Select
value={String(s.invite_expiry_hours)}
choices={HOURS}
onChange={(v) => save.mutate({ invite_expiry_hours: Number(v) })}
/>
</SettingRow>
<SettingRow
label="Suggested role for a new person"
hint="a starting point only, because nobody can invite somebody at or above their own level"
>
<Select
value={s.default_invite_role}
choices={ROLES}
onChange={(v) => save.mutate({ default_invite_role: v })}
/>
</SettingRow>
<SettingRow
label="How long a claim lasts"
hint="a claim nobody releases would hold a candidate forever, so it lapses on its own"
>
<Select
value={String(s.claim_expiry_hours)}
choices={CLAIM_HOURS}
onChange={(v) => save.mutate({ claim_expiry_hours: Number(v) })}
/>
</SettingRow>
<p className="flex items-center gap-1.5 pt-2 text-[11px] text-faint">
<Users2 size={12} />
Teams are managed under accounts and access, because who is on a desk is what decides team scope.
</p>
</Panel>
)
}
// SettingsLoadFailure is what an owner sees when the settings cannot be read at all. It is separate from the
// panels because a failed read must not render four panels of defaults that look like real settings.
export function SettingsLoadFailure() {
const q = useOrgSettings()
if (!q.isError) return null
return (
<Panel title="Organization settings">
<Empty
title="The organization settings could not be read"
reason={(q.error as Error)?.message || 'The server did not answer. Nothing has been changed.'}
action={
<button type="button" onClick={() => void q.refetch()} className={btnGhost}>
Try again
</button>
}
/>
</Panel>
)
}