Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/settings.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/settings.ts |
| Original SHA-256 | a717dfe2eeb90b11a7e29436ee2f2cd2bee422ed97c17b4af8174cb14452dfa3 |
| Git blob SHA | 857d0b5c953558733603fef672588216ba1b0852 |
| Reading-copy SHA-256 | 91f4b54bf95550d48288521cf824f4690dba81f4f3a80b2d7dc2994b5ea760cf |
Recovered source transcript
Download complete source text ↓173 lines · 7.6 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
import { request } from './api'
// ORGANIZATION SETTINGS AND TEAMS.
//
// A separate domain module rather than more surface on api.ts, matching org.ts and admin.ts. api.ts is the
// matching and opportunity client and is already long; a settings screen has nothing to do with it.
//
// TWO AUDIENCES, TWO SHAPES, and the difference is load bearing. OrgSettings is the ADMINISTRATOR'S view and
// is gated on org.settings.write. OrgPolicy is EVERY USER'S view and contains no settings at all: it is the
// answers already computed for the caller by the server. The interface renders against the policy so that
// what a person may see is decided in one place, on the server, rather than being worked out again in a
// browser where it would be advice rather than enforcement.
export type Sensitivity = 'owner' | 'managers' | 'team' | 'organization'
export type WorkVisibility = 'private' | 'team' | 'organization'
export type RecordScope = 'own' | 'team' | 'organization'
export interface OrgSettings {
email_delivery_enabled: boolean
email_from_address: string
email_from_name: string
email_reply_to: string
smtp_host: string
smtp_port: number
smtp_username: string
smtp_security: 'starttls' | 'implicit_tls'
smtp_password_set: boolean
smtp_verified_at: string | null
smtp_last_attempt_at: string | null
smtp_last_error: string
work_visibility: WorkVisibility
show_activity_presence: boolean
claim_required: boolean
claim_expiry_hours: number
rate_visibility: Sensitivity
margin_visibility: Sensitivity
contact_visibility: Sensitivity
staff_record_scope: RecordScope
manager_record_scope: Exclude<RecordScope, 'own'>
unowned_records_visible: boolean
// NO time_zone. It lives on the organization and is edited by the existing TimeZonePanel, which is
// already wired to the task queue and every period rollup. A second control here would let a person set
// a working day that the daily report ignores.
invite_expiry_hours: number
default_invite_role: string
// The organization's standard wrap, as a FRACTION on the wire, so a 35 percent wrap is the string "0.35".
// A string rather than a number for the same reason every rate in this product is: a wrap that round
// tripped through a JSON float would not reconcile with an invoice.
//
// ABSENT MEANS THE FIRM HAS NOT SAID, and that is not the same as zero. Absent means no margin is reported
// anywhere in the product, only spreads. Zero is a real answer that makes the margin equal the spread.
default_burden_pct?: string
aedt_notice_required: boolean
updated_at: string
}
export type SettingsPatch = Partial<Omit<OrgSettings, 'default_burden_pct'>> & {
default_burden_pct?: string | null
}
export interface OrgSettingsEnvelope {
settings: OrgSettings
secret_storage_available: boolean
}
// OrgPolicy is what the caller may do, already decided. Note that it carries no configuration: a recruiter
// learning HOW the organization is set up would be a small disclosure with no benefit.
export interface OrgPolicy {
can_see_rates: boolean
can_see_margins: boolean
can_see_contacts: boolean
can_see_work_of_others: boolean
show_activity_presence: boolean
claim_required: boolean
claim_expiry_hours: number
email_delivery_enabled: boolean
time_zone: string
record_scope: RecordScope
// aedt_notice_required says the organization refuses to score a candidate with no automated decision
// notice. Every role is told, because a recruiter meets the refusal and needs to know why BEFORE they meet
// it rather than after.
aedt_notice_required: boolean
}
export interface TeamMember {
user_id: string
name: string
email: string
role: string
is_lead: boolean
disabled: boolean
}
export interface Team {
id: string
name: string
status: 'active' | 'archived'
lead_user_id?: string
lead_name?: string
members: TeamMember[]
}
export const settingsApi = {
get: () => request<OrgSettingsEnvelope>('/org/settings'),
policy: () => request<OrgPolicy>('/org/policy'),
// SettingsPatch, not Partial<OrgSettings>, because clearing the wrap needs an explicit null on the wire and
// an optional property can only be omitted. Omitted means leave it alone; null means erase it. The server
// tells the two apart deliberately, so the client has to be able to say both.
patch: (patch: SettingsPatch) =>
request<{ settings: OrgSettings }>('/org/settings', {
method: 'PATCH',
body: JSON.stringify(patch),
}),
// The password is [credential removed] only. There is no getter, deliberately: no route returns it, so none is offered.
setMailPassword: [credential removed] string) =>
request<{ stored: boolean }>('/org/settings/email/password', {
method: 'PUT',
body: JSON.stringify({ password }),
}),
clearMailPassword: () =>
request<{ removed: boolean }>('/org/settings/email/password', { method: 'DELETE' }),
// The test always goes to the signed in administrator's own address. It takes no recipient because an
// endpoint that accepted one would be an open relay wearing a settings screen.
testMail: () => request<{ sent: boolean; to: string }>('/org/settings/email/test', { method: 'POST' }),
teams: () => request<{ teams: Team[]; count: number }>('/teams'),
myTeams: () => request<{ teams: Team[]; count: number }>('/teams/mine'),
createTeam: (name: string, lead_user_id?: string) =>
request<{ id: string }>('/teams', { method: 'POST', body: JSON.stringify({ name, lead_user_id }) }),
updateTeam: (id: string, patch: { name?: string; status?: string; lead_user_id?: string | null }) =>
request<{ updated: boolean }>(`/teams/${encodeURIComponent(id)}`, {
method: 'PATCH',
body: JSON.stringify(patch),
}),
addMember: (teamId: string, user_id: string) =>
request<{ added: boolean }>(`/teams/${encodeURIComponent(teamId)}/members`, {
method: 'POST',
body: JSON.stringify({ user_id }),
}),
removeMember: (teamId: string, userId: string) =>
request<{ removed: boolean }>(
`/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`,
{ method: 'DELETE' },
),
}
// SENSITIVITY_CHOICES and the rest are the vocabularies the server validates against, with the plain English
// the setting actually means. They live here rather than inline in a form so the four places that render a
// sensitivity cannot describe it four different ways.
export const SENSITIVITY_CHOICES: { value: Sensitivity; label: string; hint: string }[] = [
{ value: 'owner', label: 'Owner only', hint: 'the account holder alone' },
{ value: 'managers', label: 'Managers and above', hint: 'the people accountable for a book of business' },
{ value: 'team', label: 'Everyone who runs a desk', hint: 'managers and recruiters, not viewers' },
{ value: 'organization', label: 'Everyone with a login', hint: 'anyone who can already read the record' },
]
export const VISIBILITY_CHOICES: { value: WorkVisibility; label: string; hint: string }[] = [
{ value: 'organization', label: 'Everyone', hint: 'anyone who can read the record sees who is on it' },
{ value: 'team', label: 'People who run desks', hint: 'recruiters and managers see each other' },
{ value: 'private', label: 'Private', hint: 'recruiters do not see each other; managers always do' },
]
export const SCOPE_CHOICES: { value: RecordScope; label: string; hint: string }[] = [
{ value: 'organization', label: 'The whole organization', hint: 'everyone sees every record' },
{ value: 'team', label: "Their team's records", hint: 'the union of every team they belong to' },
{ value: 'own', label: 'Only their own records', hint: 'records they are the owner of' },
]