feat(interviews): add stage signal banners and extend move emit in InterviewCard
This commit is contained in:
parent
dd151f1afc
commit
fb3e5b7532
2 changed files with 101 additions and 3 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { PipelineJob } from '../stores/interviews'
|
import type { PipelineJob } from '../stores/interviews'
|
||||||
|
import type { StageSignal, PipelineStage } from '../stores/interviews'
|
||||||
|
import { useApiFetch } from '../composables/useApi'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
job: PipelineJob
|
job: PipelineJob
|
||||||
|
|
@ -8,10 +10,51 @@ const props = defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
move: [jobId: number]
|
move: [jobId: number, preSelectedStage?: PipelineStage]
|
||||||
prep: [jobId: number]
|
prep: [jobId: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Signal state
|
||||||
|
const sigExpanded = ref(false)
|
||||||
|
|
||||||
|
interface SignalMeta {
|
||||||
|
label: string
|
||||||
|
stage: PipelineStage
|
||||||
|
color: 'amber' | 'green' | 'red'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIGNAL_META: Record<StageSignal['stage_signal'], SignalMeta> = {
|
||||||
|
interview_scheduled: { label: 'Move to Phone Screen', stage: 'phone_screen', color: 'amber' },
|
||||||
|
positive_response: { label: 'Move to Phone Screen', stage: 'phone_screen', color: 'amber' },
|
||||||
|
offer_received: { label: 'Move to Offer', stage: 'offer', color: 'green' },
|
||||||
|
survey_received: { label: 'Move to Survey', stage: 'survey', color: 'amber' },
|
||||||
|
rejected: { label: 'Mark Rejected', stage: 'interview_rejected', color: 'red' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_BG: Record<'amber' | 'green' | 'red', string> = {
|
||||||
|
amber: 'rgba(245,158,11,0.08)',
|
||||||
|
green: 'rgba(39,174,96,0.08)',
|
||||||
|
red: 'rgba(192,57,43,0.08)',
|
||||||
|
}
|
||||||
|
const COLOR_BORDER: Record<'amber' | 'green' | 'red', string> = {
|
||||||
|
amber: 'rgba(245,158,11,0.4)',
|
||||||
|
green: 'rgba(39,174,96,0.4)',
|
||||||
|
red: 'rgba(192,57,43,0.4)',
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleSignals(): StageSignal[] {
|
||||||
|
const sigs = props.job.stage_signals ?? []
|
||||||
|
return sigExpanded.value ? sigs : sigs.slice(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissSignal(sig: StageSignal) {
|
||||||
|
// Optimistic removal
|
||||||
|
const arr = props.job.stage_signals
|
||||||
|
const idx = arr.findIndex(s => s.id === sig.id)
|
||||||
|
if (idx !== -1) arr.splice(idx, 1)
|
||||||
|
await useApiFetch(`/api/stage-signals/${sig.id}/dismiss`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
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'
|
||||||
|
|
@ -84,6 +127,40 @@ const columnColor = computed(() => {
|
||||||
<button class="card-action" @click.stop="emit('move', job.id)">Move to… ›</button>
|
<button class="card-action" @click.stop="emit('move', job.id)">Move to… ›</button>
|
||||||
<button class="card-action" @click.stop="emit('prep', job.id)">Prep →</button>
|
<button class="card-action" @click.stop="emit('prep', job.id)">Prep →</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
<!-- Signal banners -->
|
||||||
|
<template v-if="job.stage_signals?.length">
|
||||||
|
<div
|
||||||
|
v-for="sig in visibleSignals()"
|
||||||
|
:key="sig.id"
|
||||||
|
class="signal-banner"
|
||||||
|
:style="{
|
||||||
|
background: COLOR_BG[SIGNAL_META[sig.stage_signal].color],
|
||||||
|
borderTopColor: COLOR_BORDER[SIGNAL_META[sig.stage_signal].color],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="signal-label">
|
||||||
|
📧 Email suggests: <strong>{{ SIGNAL_META[sig.stage_signal].label }}</strong>
|
||||||
|
</span>
|
||||||
|
<span class="signal-subject">{{ sig.subject.slice(0, 60) }}{{ sig.subject.length > 60 ? '…' : '' }}</span>
|
||||||
|
<div class="signal-actions">
|
||||||
|
<button
|
||||||
|
class="btn-signal-move"
|
||||||
|
@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}`"
|
||||||
|
>→ {{ SIGNAL_META[sig.stage_signal].label }}</button>
|
||||||
|
<button
|
||||||
|
class="btn-signal-dismiss"
|
||||||
|
@click.stop="dismissSignal(sig)"
|
||||||
|
aria-label="Dismiss signal"
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="(job.stage_signals?.length ?? 0) > 1"
|
||||||
|
class="btn-sig-expand"
|
||||||
|
@click.stop="sigExpanded = !sigExpanded"
|
||||||
|
>{{ sigExpanded ? '− less' : `+${(job.stage_signals?.length ?? 1) - 1} more` }}</button>
|
||||||
|
</template>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -93,7 +170,6 @@ const columnColor = computed(() => {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border-left: 4px solid var(--card-accent, var(--color-border));
|
border-left: 4px solid var(--card-accent, var(--color-border));
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.07);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.07);
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: box-shadow 150ms;
|
transition: box-shadow 150ms;
|
||||||
|
|
@ -220,4 +296,25 @@ const columnColor = computed(() => {
|
||||||
.card-action:hover {
|
.card-action:hover {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signal-banner {
|
||||||
|
border-top: 1px solid transparent; /* color set inline */
|
||||||
|
padding: 8px 12px;
|
||||||
|
display: flex; flex-direction: column; gap: 4px;
|
||||||
|
}
|
||||||
|
.signal-label { font-size: 0.82em; }
|
||||||
|
.signal-subject { font-size: 0.78em; color: var(--color-text-muted); }
|
||||||
|
.signal-actions { display: flex; gap: 6px; align-items: center; }
|
||||||
|
.btn-signal-move {
|
||||||
|
background: var(--color-primary); color: #fff;
|
||||||
|
border: none; border-radius: 4px; padding: 2px 8px; font-size: 0.78em; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-signal-dismiss {
|
||||||
|
background: none; border: none; color: var(--color-text-muted); font-size: 0.85em; cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
.btn-sig-expand {
|
||||||
|
background: none; border: none; font-size: 0.75em; color: var(--color-info); cursor: pointer;
|
||||||
|
padding: 4px 12px; text-align: left;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ function select(stage: PipelineStage) {
|
||||||
|
|
||||||
function confirm() {
|
function confirm() {
|
||||||
if (!selectedStage.value) return
|
if (!selectedStage.value) return
|
||||||
|
if (!stages.value.includes(selectedStage.value)) return // guard: preSelectedStage was filtered out
|
||||||
const opts: { interview_date?: string; rejection_stage?: string } = {}
|
const opts: { interview_date?: string; rejection_stage?: string } = {}
|
||||||
if (interviewDate.value) opts.interview_date = new Date(interviewDate.value).toISOString()
|
if (interviewDate.value) opts.interview_date = new Date(interviewDate.value).toISOString()
|
||||||
if (rejectionStage.value) opts.rejection_stage = rejectionStage.value
|
if (rejectionStage.value) opts.rejection_stage = rejectionStage.value
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue