211 lines
No EOL
6.6 KiB
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-gray-50 rounded-lg text-sm">
|
|
<div>
|
|
<span class="text-gray-600">Min mode runway:</span>
|
|
<div class="font-bold text-lg">{{ minRunwayMonths }} months</div>
|
|
<div class="text-xs text-gray-500">Until {{ formatDate(minRunwayEndDate) }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600">Target mode runway:</span>
|
|
<div class="font-bold text-lg">{{ targetRunwayMonths }} months</div>
|
|
<div class="text-xs text-gray-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-gray-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-gray-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-gray-500">{{ formatDate(milestone.date) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-xs text-gray-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-gray-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> |