122 lines
No EOL
4.1 KiB
Vue
122 lines
No EOL
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useToastStore } from '@/stores/toast'
|
|
import * as api from '@/api'
|
|
import CostDisplay from '@/components/CostDisplay.vue'
|
|
import type { TodayResponse } from '@/types'
|
|
|
|
const toast = useToastStore()
|
|
const router = useRouter()
|
|
|
|
const today = ref<TodayResponse | null>(null)
|
|
const loading = ref(true)
|
|
|
|
const nudgeIds = computed(() => new Set(today.value?.nudges.map((n) => n.application_id) ?? []))
|
|
|
|
async function loadToday() {
|
|
try {
|
|
today.value = await api.getToday()
|
|
} catch {
|
|
toast.push('Failed to load today digest', 'error')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function goToApplication(id: string) {
|
|
router.push(`/applications/${id}`)
|
|
}
|
|
|
|
function copyNudge(suggestion: string) {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(suggestion).then(
|
|
() => toast.push('Follow-up draft copied to clipboard', 'success'),
|
|
() => toast.push('Copy failed', 'error')
|
|
)
|
|
} else {
|
|
toast.push('Clipboard not available', 'error')
|
|
}
|
|
}
|
|
|
|
onMounted(loadToday)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<h1 class="text-2xl font-bold">Today</h1>
|
|
|
|
<div v-if="loading" class="text-gray-500">Loading...</div>
|
|
|
|
<template v-if="!loading && today">
|
|
<!-- Pending approvals banner -->
|
|
<div v-if="today.pending_approvals > 0" class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
<span class="font-medium text-yellow-800">
|
|
{{ today.pending_approvals }} pending approval{{ today.pending_approvals > 1 ? 's' : '' }} waiting for you.
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Digest cards -->
|
|
<section>
|
|
<h2 class="font-semibold text-lg mb-3">Top Matches Today</h2>
|
|
<div v-if="today.digest.length === 0" class="text-gray-400 text-sm">No postings in your digest yet.</div>
|
|
<div v-else class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<div
|
|
v-for="item in today.digest"
|
|
:key="item.application_id"
|
|
class="bg-white rounded-lg border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow"
|
|
@click="goToApplication(item.application_id)"
|
|
>
|
|
<div class="font-medium">{{ item.title }}</div>
|
|
<div class="text-sm text-gray-600">{{ item.company }}</div>
|
|
<div class="mt-2 flex items-center gap-2">
|
|
<span class="text-xs bg-green-100 text-green-800 rounded px-2 py-0.5 font-medium">
|
|
Score: {{ item.score }}
|
|
</span>
|
|
<span
|
|
v-if="nudgeIds.has(item.application_id)"
|
|
class="inline-block w-2 h-2 rounded-full bg-orange-500"
|
|
title="Follow-up nudge pending"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Nudge cards -->
|
|
<section>
|
|
<h2 class="font-semibold text-lg mb-3">Follow-up Nudges</h2>
|
|
<div v-if="today.nudges.length === 0" class="text-gray-400 text-sm">No nudges. You are up to date.</div>
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="nudge in today.nudges"
|
|
:key="nudge.application_id"
|
|
class="bg-orange-50 border border-orange-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<span class="font-medium text-orange-900">
|
|
Sent {{ nudge.days_since_sent }} days ago
|
|
</span>
|
|
<button
|
|
class="text-sm text-indigo-600 hover:underline"
|
|
@click="goToApplication(nudge.application_id)"
|
|
>
|
|
Open application
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-gray-700 mt-2">{{ nudge.suggestion }}</p>
|
|
<button
|
|
class="mt-2 text-sm bg-indigo-600 text-white px-3 py-1 rounded hover:bg-indigo-700"
|
|
@click="copyNudge(nudge.suggestion)"
|
|
>
|
|
Copy follow-up draft
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Cost display -->
|
|
<CostDisplay />
|
|
</template>
|
|
</div>
|
|
</template> |