refactor: enhance routing and state management in CoopBuilder, add migration checks on startup, and update Tailwind configuration for improved component styling
This commit is contained in:
parent
848386e3dd
commit
4cea1f71fe
55 changed files with 4053 additions and 1486 deletions
211
components/MilestoneRunwayOverlay.vue
Normal file
211
components/MilestoneRunwayOverlay.vue
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue