app/components/MilestoneRunwayOverlay.vue

211 lines
No EOL
6.6 KiB
Vue

<template>
<div class="space-y-4">
<!-- Runway summary -->
<div class="grid grid-cols-2 gap-4 p-3 bg-neutral-50 rounded-lg text-sm">
<div>
<span class="text-neutral-600">Min mode runway:</span>
<div class="font-bold text-lg">{{ minRunwayMonths }} months</div>
<div class="text-xs text-neutral-500">Until {{ formatDate(minRunwayEndDate) }}</div>
</div>
<div>
<span class="text-neutral-600">Target mode runway:</span>
<div class="font-bold text-lg">{{ targetRunwayMonths }} months</div>
<div class="text-xs text-neutral-500">Until {{ formatDate(targetRunwayEndDate) }}</div>
</div>
</div>
<!-- Milestones -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<h4 class="text-sm font-medium">Milestones</h4>
<UButton
size="xs"
variant="ghost"
@click="addMilestone"
>
<UIcon name="i-heroicons-plus" class="w-3 h-3 mr-1" />
Add
</UButton>
</div>
<div v-if="milestones.length === 0" class="text-xs text-neutral-500 italic p-2">
No milestones set. Add key dates to track runway coverage.
</div>
<div v-for="milestone in milestonesWithStatus" :key="milestone.id"
class="flex items-center justify-between p-2 border border-neutral-200 rounded text-sm">
<div class="flex items-center gap-2">
<UIcon
:name="milestone.status === 'safe' ? 'i-heroicons-check-circle' :
milestone.status === 'warning' ? 'i-heroicons-exclamation-triangle' :
'i-heroicons-x-circle'"
:class="milestone.status === 'safe' ? 'text-green-500' :
milestone.status === 'warning' ? 'text-yellow-500' :
'text-red-500'"
class="w-4 h-4"
/>
<div>
<div class="font-medium">{{ milestone.label }}</div>
<div class="text-xs text-neutral-500">{{ formatDate(milestone.date) }}</div>
</div>
</div>
<div class="text-right">
<div class="text-xs text-neutral-600">
{{ milestone.monthsFromNow > 0 ? `+${milestone.monthsFromNow}` : milestone.monthsFromNow }}mo
</div>
<UButton
size="xs"
variant="ghost"
color="red"
@click="removeMilestone(milestone.id)"
>
<UIcon name="i-heroicons-trash" class="w-3 h-3" />
</UButton>
</div>
</div>
</div>
<!-- Add milestone form -->
<div v-if="showAddForm" class="p-3 border border-neutral-200 rounded-lg space-y-2">
<UInput
v-model="newMilestone.label"
placeholder="Milestone name (e.g., 'Prototype release')"
size="sm"
/>
<UInput
v-model="newMilestone.date"
type="date"
size="sm"
/>
<div class="flex gap-2">
<UButton size="xs" @click="saveMilestone">Save</UButton>
<UButton size="xs" variant="ghost" @click="cancelAdd">Cancel</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Milestone {
id: string
label: string
date: string // YYYY-MM-DD
}
const membersStore = useMembersStore()
const policiesStore = usePoliciesStore()
const cashStore = useCashStore()
const { getDualModeRunway, formatRunway, getRunwayStatus } = useRunway()
// Runway calculations using the integrated composable
const runwayData = computed(() => {
const cash = cashStore.currentCash || 50000 // Mock fallback
const savings = cashStore.currentSavings || 15000 // Mock fallback
return getDualModeRunway(cash, savings)
})
const minRunwayMonths = computed(() => Math.floor(runwayData.value.minimum))
const targetRunwayMonths = computed(() => Math.floor(runwayData.value.target))
const minRunwayEndDate = computed(() => {
const date = new Date()
date.setMonth(date.getMonth() + minRunwayMonths.value)
return date
})
const targetRunwayEndDate = computed(() => {
const date = new Date()
date.setMonth(date.getMonth() + targetRunwayMonths.value)
return date
})
// Milestones management - store in localStorage
const milestones = ref<Milestone[]>([])
// Initialize milestones from localStorage
onMounted(() => {
const stored = localStorage.getItem('urgent-tools-milestones')
if (stored) {
try {
milestones.value = JSON.parse(stored)
} catch (e) {
console.warn('Failed to parse stored milestones')
milestones.value = []
}
} else {
// Default milestones for new users
milestones.value = [
{ id: '1', label: 'Prototype release', date: '2025-12-15' },
{ id: '2', label: 'Series A funding', date: '2026-03-01' }
]
saveMilestones()
}
})
// Save milestones to localStorage
function saveMilestones() {
localStorage.setItem('urgent-tools-milestones', JSON.stringify(milestones.value))
}
const showAddForm = ref(false)
const newMilestone = ref({ label: '', date: '' })
const milestonesWithStatus = computed(() => {
const currentMode = policiesStore.operatingMode || 'minimum'
const runwayMonths = currentMode === 'target' ? targetRunwayMonths.value : minRunwayMonths.value
const runwayEndDate = currentMode === 'target' ? targetRunwayEndDate.value : minRunwayEndDate.value
return milestones.value.map(milestone => {
const milestoneDate = new Date(milestone.date)
const now = new Date()
const monthsFromNow = Math.round((milestoneDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30))
let status: 'safe' | 'warning' | 'danger'
if (milestoneDate <= runwayEndDate) {
status = 'safe'
} else if (monthsFromNow <= runwayMonths + 2) {
status = 'warning'
} else {
status = 'danger'
}
return {
...milestone,
monthsFromNow,
status
}
}).sort((a, b) => a.monthsFromNow - b.monthsFromNow)
})
function formatDate(date: Date | string) {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
}
function addMilestone() {
showAddForm.value = true
}
function saveMilestone() {
if (newMilestone.value.label && newMilestone.value.date) {
milestones.value.push({
id: Date.now().toString(),
...newMilestone.value
})
saveMilestones() // Persist to localStorage
newMilestone.value = { label: '', date: '' }
showAddForm.value = false
}
}
function cancelAdd() {
newMilestone.value = { label: '', date: '' }
showAddForm.value = false
}
function removeMilestone(id: string) {
milestones.value = milestones.value.filter(m => m.id !== id)
saveMilestones() // Persist to localStorage
}
</script>