|
| 1 | +<script lang="ts"> |
| 2 | + // Add a person IN org context — spec step 6's "magic". The operator only |
| 3 | + // resolves the person (candidates are ALWAYS gated: pick a match or |
| 4 | + // explicitly create); the affiliation edge + its paired observation come |
| 5 | + // from person.affiliate with this card's org pre-bound. No affiliation UI |
| 6 | + // exists because none is needed — and the same person can gain more orgs |
| 7 | + // later (N-affiliation assumption, never 1:1). |
| 8 | +
|
| 9 | + import { fetchPersonCandidates, applyPerson, affiliatePerson } from './lib/org-client'; |
| 10 | + import type { PersonCandidate, PersonNormRecord } from './lib/types'; |
| 11 | +
|
| 12 | + let { |
| 13 | + org_slug, |
| 14 | + orgName, |
| 15 | + client, |
| 16 | + onadded, |
| 17 | + }: { |
| 18 | + org_slug: string; |
| 19 | + orgName: string; |
| 20 | + client: string; |
| 21 | + onadded: () => void; |
| 22 | + } = $props(); |
| 23 | +
|
| 24 | + let open = $state(false); |
| 25 | + let name = $state(''); |
| 26 | + let linkedin = $state(''); |
| 27 | + let role = $state(''); |
| 28 | + let phase = $state<'form' | 'gate' | 'writing'>('form'); |
| 29 | + let candidates = $state<PersonCandidate[]>([]); |
| 30 | + let error = $state<string | null>(null); |
| 31 | +
|
| 32 | + function record(): PersonNormRecord { |
| 33 | + return { |
| 34 | + name: name.trim(), |
| 35 | + linkedin_url: linkedin.trim() || null, |
| 36 | + role: role.trim() || null, |
| 37 | + }; |
| 38 | + } |
| 39 | +
|
| 40 | + async function findCandidates(e: SubmitEvent) { |
| 41 | + e.preventDefault(); |
| 42 | + if (!name.trim()) return; |
| 43 | + error = null; |
| 44 | + try { |
| 45 | + candidates = await fetchPersonCandidates(record(), client); |
| 46 | + phase = 'gate'; // always gate — even zero candidates gets an explicit "create" |
| 47 | + } catch (err) { |
| 48 | + error = err instanceof Error ? err.message : String(err); |
| 49 | + } |
| 50 | + } |
| 51 | +
|
| 52 | + async function resolve(action: 'match' | 'create', person_uuid?: string) { |
| 53 | + phase = 'writing'; |
| 54 | + error = null; |
| 55 | + try { |
| 56 | + const applied = await applyPerson({ |
| 57 | + action, |
| 58 | + person_uuid, |
| 59 | + record: record(), |
| 60 | + client, |
| 61 | + source: 'org-workbench', |
| 62 | + }); |
| 63 | + await affiliatePerson({ |
| 64 | + person_uuid: applied.person_uuid, |
| 65 | + org_slug, |
| 66 | + role: role.trim() || null, |
| 67 | + client, |
| 68 | + }); |
| 69 | + name = ''; |
| 70 | + linkedin = ''; |
| 71 | + role = ''; |
| 72 | + candidates = []; |
| 73 | + phase = 'form'; |
| 74 | + open = false; |
| 75 | + onadded(); |
| 76 | + window.dispatchEvent( |
| 77 | + new CustomEvent('augment-it:entity-updated', { detail: { org_slug } }), |
| 78 | + ); |
| 79 | + } catch (err) { |
| 80 | + error = err instanceof Error ? err.message : String(err); |
| 81 | + phase = 'gate'; |
| 82 | + } |
| 83 | + } |
| 84 | +</script> |
| 85 | + |
| 86 | +<div class="ow-addperson"> |
| 87 | + {#if !open} |
| 88 | + <button type="button" class="ow-addperson-open" onclick={() => (open = true)}> |
| 89 | + + Add a person to {orgName} |
| 90 | + </button> |
| 91 | + {:else} |
| 92 | + <form class="ow-addperson-form" onsubmit={findCandidates}> |
| 93 | + <input class="ow-add-url" type="text" placeholder="Full name (required)" bind:value={name} required disabled={phase !== 'form'} /> |
| 94 | + <input class="ow-add-url" type="url" placeholder="LinkedIn URL (optional)" bind:value={linkedin} disabled={phase !== 'form'} /> |
| 95 | + <input class="ow-add-kind" type="text" placeholder="Role at {orgName} (optional)" bind:value={role} disabled={phase !== 'form'} /> |
| 96 | + {#if phase === 'form'} |
| 97 | + <span class="ow-addperson-actions"> |
| 98 | + <button type="submit" class="ow-add-go" disabled={!name.trim()}>Find matches</button> |
| 99 | + <button type="button" class="ow-add-go" onclick={() => { open = false; error = null; }}>Cancel</button> |
| 100 | + </span> |
| 101 | + {/if} |
| 102 | + </form> |
| 103 | + |
| 104 | + {#if phase === 'gate'} |
| 105 | + <div class="ow-gate"> |
| 106 | + {#if candidates.length > 0} |
| 107 | + <p class="ow-gate-note">Existing persons that might be “{name}” — pick one or create new:</p> |
| 108 | + <ul class="ow-gate-list"> |
| 109 | + {#each candidates as c (c.person_uuid)} |
| 110 | + <li> |
| 111 | + <button type="button" class="ow-gate-pick" onclick={() => resolve('match', c.person_uuid)}> |
| 112 | + <strong>{c.name ?? c.person_uuid}</strong> |
| 113 | + {#if c.headline}<span class="ow-gate-headline">{c.headline}</span>{/if} |
| 114 | + <span class="ow-gate-score">{c.score} · {c.match_reason.join(', ')}</span> |
| 115 | + </button> |
| 116 | + </li> |
| 117 | + {/each} |
| 118 | + </ul> |
| 119 | + {:else} |
| 120 | + <p class="ow-gate-note">No existing person matches “{name}”.</p> |
| 121 | + {/if} |
| 122 | + <span class="ow-addperson-actions"> |
| 123 | + <button type="button" class="ow-add-go" onclick={() => resolve('create')}> |
| 124 | + Create new person + affiliate with {orgName} |
| 125 | + </button> |
| 126 | + <button type="button" class="ow-add-go" onclick={() => (phase = 'form')}>Back</button> |
| 127 | + </span> |
| 128 | + </div> |
| 129 | + {:else if phase === 'writing'} |
| 130 | + <p class="ow-gate-note">writing person + affiliation…</p> |
| 131 | + {/if} |
| 132 | + {#if error}<div class="ow-error">{error}</div>{/if} |
| 133 | + {/if} |
| 134 | +</div> |
0 commit comments