feat(signals): expandable body + reclassify chips in InterviewCard
This commit is contained in:
parent
3b2df5e89e
commit
2796d0d911
1 changed files with 115 additions and 15 deletions
|
|
@ -55,6 +55,42 @@ async function dismissSignal(sig: StageSignal) {
|
||||||
await useApiFetch(`/api/stage-signals/${sig.id}/dismiss`, { method: 'POST' })
|
await useApiFetch(`/api/stage-signals/${sig.id}/dismiss`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bodyExpanded = ref(false)
|
||||||
|
|
||||||
|
// Re-classify chips — neutral triggers two-call dismiss path
|
||||||
|
const RECLASSIFY_CHIPS = [
|
||||||
|
{ label: '🟡 Interview', value: 'interview_scheduled' as const },
|
||||||
|
{ label: '✅ Positive', value: 'positive_response' as const },
|
||||||
|
{ label: '🟢 Offer', value: 'offer_received' as const },
|
||||||
|
{ label: '📋 Survey', value: 'survey_received' as const },
|
||||||
|
{ label: '✖ Rejected', value: 'rejected' as const },
|
||||||
|
{ label: '— Neutral', value: 'neutral' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
async function reclassifySignal(sig: StageSignal, newLabel: string) {
|
||||||
|
if (newLabel === 'neutral') {
|
||||||
|
// Optimistic removal — neutral signals are dismissed
|
||||||
|
const arr = props.job.stage_signals
|
||||||
|
const idx = arr.findIndex(s => s.id === sig.id)
|
||||||
|
if (idx !== -1) arr.splice(idx, 1)
|
||||||
|
// Two-call path: persist corrected label then dismiss (Avocet training hook)
|
||||||
|
await useApiFetch(`/api/stage-signals/${sig.id}/reclassify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ stage_signal: 'neutral' }),
|
||||||
|
})
|
||||||
|
await useApiFetch(`/api/stage-signals/${sig.id}/dismiss`, { method: 'POST' })
|
||||||
|
} else {
|
||||||
|
// Optimistic local re-label — Vue 3 proxy tracks the mutation
|
||||||
|
sig.stage_signal = newLabel as StageSignal['stage_signal']
|
||||||
|
await useApiFetch(`/api/stage-signals/${sig.id}/reclassify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ stage_signal: newLabel }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scoreClass = computed(() => {
|
const scoreClass = computed(() => {
|
||||||
const s = (props.job.match_score ?? 0) * 100
|
const s = (props.job.match_score ?? 0) * 100
|
||||||
if (s >= 85) return 'score--high'
|
if (s >= 85) return 'score--high'
|
||||||
|
|
@ -138,16 +174,20 @@ const columnColor = computed(() => {
|
||||||
borderTopColor: COLOR_BORDER[SIGNAL_META[sig.stage_signal].color],
|
borderTopColor: COLOR_BORDER[SIGNAL_META[sig.stage_signal].color],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<div class="signal-header">
|
||||||
<span class="signal-label">
|
<span class="signal-label">
|
||||||
📧 Email suggests: <strong>{{ SIGNAL_META[sig.stage_signal].label }}</strong>
|
📧 <strong>{{ SIGNAL_META[sig.stage_signal].label.replace('Move to ', '') }}</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="signal-subject">{{ sig.subject.slice(0, 60) }}{{ sig.subject.length > 60 ? '…' : '' }}</span>
|
<span class="signal-subject">{{ sig.subject.slice(0, 60) }}{{ sig.subject.length > 60 ? '…' : '' }}</span>
|
||||||
<div class="signal-actions">
|
<div class="signal-header-actions">
|
||||||
|
<button class="btn-signal-read" @click.stop="bodyExpanded = !bodyExpanded">
|
||||||
|
{{ bodyExpanded ? '▾ Hide' : '▸ Read' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-signal-move"
|
class="btn-signal-move"
|
||||||
@click.stop="emit('move', props.job.id, SIGNAL_META[sig.stage_signal].stage)"
|
@click.stop="emit('move', props.job.id, SIGNAL_META[sig.stage_signal].stage)"
|
||||||
:aria-label="`${SIGNAL_META[sig.stage_signal].label} for ${props.job.title}`"
|
:aria-label="`${SIGNAL_META[sig.stage_signal].label} for ${props.job.title}`"
|
||||||
>→ {{ SIGNAL_META[sig.stage_signal].label }}</button>
|
>→ Move</button>
|
||||||
<button
|
<button
|
||||||
class="btn-signal-dismiss"
|
class="btn-signal-dismiss"
|
||||||
@click.stop="dismissSignal(sig)"
|
@click.stop="dismissSignal(sig)"
|
||||||
|
|
@ -155,6 +195,23 @@ const columnColor = computed(() => {
|
||||||
>✕</button>
|
>✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Expanded body + reclassify chips -->
|
||||||
|
<div v-if="bodyExpanded" class="signal-body-expanded">
|
||||||
|
<div v-if="sig.from_addr" class="signal-from">From: {{ sig.from_addr }}</div>
|
||||||
|
<div v-if="sig.body" class="signal-body-text">{{ sig.body }}</div>
|
||||||
|
<div v-else class="signal-body-empty">No email body available.</div>
|
||||||
|
<div class="signal-reclassify">
|
||||||
|
<span class="signal-reclassify-label">Re-classify:</span>
|
||||||
|
<button
|
||||||
|
v-for="chip in RECLASSIFY_CHIPS"
|
||||||
|
:key="chip.value"
|
||||||
|
class="btn-chip"
|
||||||
|
:class="{ 'btn-chip-active': sig.stage_signal === chip.value }"
|
||||||
|
@click.stop="reclassifySignal(sig, chip.value)"
|
||||||
|
>{{ chip.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="(job.stage_signals?.length ?? 0) > 1"
|
v-if="(job.stage_signals?.length ?? 0) > 1"
|
||||||
class="btn-sig-expand"
|
class="btn-sig-expand"
|
||||||
|
|
@ -313,6 +370,49 @@ const columnColor = computed(() => {
|
||||||
background: none; border: none; color: var(--color-text-muted); font-size: 0.85em; cursor: pointer;
|
background: none; border: none; color: var(--color-text-muted); font-size: 0.85em; cursor: pointer;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
.btn-signal-read {
|
||||||
|
background: none; border: none; color: var(--color-text-muted); font-size: 0.82em;
|
||||||
|
cursor: pointer; padding: 2px 6px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.signal-header {
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.signal-header-actions {
|
||||||
|
margin-left: auto; display: flex; gap: 6px; align-items: center;
|
||||||
|
}
|
||||||
|
.signal-body-expanded {
|
||||||
|
margin-top: 8px; font-size: 0.8em; border-top: 1px dashed var(--color-border);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.signal-from {
|
||||||
|
color: var(--color-text-muted); margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.signal-body-text {
|
||||||
|
white-space: pre-wrap; color: var(--color-text); line-height: 1.5;
|
||||||
|
max-height: 200px; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.signal-body-empty {
|
||||||
|
color: var(--color-text-muted); font-style: italic;
|
||||||
|
}
|
||||||
|
.signal-reclassify {
|
||||||
|
display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 8px;
|
||||||
|
}
|
||||||
|
.signal-reclassify-label {
|
||||||
|
font-size: 0.75em; color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.btn-chip {
|
||||||
|
background: var(--color-surface); color: var(--color-text-muted);
|
||||||
|
border: 1px solid var(--color-border); border-radius: 4px;
|
||||||
|
padding: 2px 7px; font-size: 0.75em; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-chip:hover {
|
||||||
|
background: var(--color-hover);
|
||||||
|
}
|
||||||
|
.btn-chip-active {
|
||||||
|
background: var(--color-primary-muted, #e8f0ff);
|
||||||
|
color: var(--color-primary); border-color: var(--color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.btn-sig-expand {
|
.btn-sig-expand {
|
||||||
background: none; border: none; font-size: 0.75em; color: var(--color-info); cursor: pointer;
|
background: none; border: none; font-size: 0.75em; color: var(--color-info); cursor: pointer;
|
||||||
padding: 4px 12px; text-align: left;
|
padding: 4px 12px; text-align: left;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue