Init commit!

This commit is contained in:
Jennie Robinson Faber 2025-08-22 18:36:16 +01:00
commit 086d682592
34 changed files with 19249 additions and 0 deletions

285
app/pages/migrate.vue Normal file
View file

@ -0,0 +1,285 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Data Migration</h1>
<p class="text-gray-600 mt-2">Migrate your data from localStorage to MongoDB</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<!-- Status -->
<div v-if="status" class="mb-6 p-4 rounded" :class="statusClass">
{{ status }}
</div>
<!-- Migration Section -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">1. Auto-Migration from localStorage</h3>
<button
@click="migrateFromLocalStorage"
:disabled="loading"
class="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded">
{{ loading ? 'Migrating...' : 'Migrate from localStorage' }}
</button>
<p class="text-sm text-gray-600 mt-2">
This will automatically detect and migrate any existing localStorage data.
</p>
</div>
<!-- Manual Import Section -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">2. Manual Data Import</h3>
<!-- Current Balance -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
Current Balance (CAD $)
</label>
<input
v-model.number="manualData.balance"
type="number"
step="0.01"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0.00" />
</div>
<!-- Transactions JSON -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
Transactions JSON
</label>
<textarea
v-model="manualData.transactionsJson"
rows="10"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Paste your transactions JSON array here"></textarea>
<p class="text-sm text-gray-600 mt-1">
Paste the JSON array of your transactions (the data you provided earlier)
</p>
</div>
<button
@click="migrateManualData"
:disabled="loading"
class="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded">
{{ loading ? 'Importing...' : 'Import Manual Data' }}
</button>
</div>
<!-- Current Data Display -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">3. Current Data Status</h3>
<button
@click="loadCurrentData"
:disabled="loading"
class="bg-gray-500 hover:bg-gray-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded mb-4">
{{ loading ? 'Loading...' : 'Refresh Data Status' }}
</button>
<div class="bg-gray-50 p-4 rounded">
<p><strong>Current Balance:</strong> {{ formatCurrency(currentData.balance) }}</p>
<p><strong>Transactions:</strong> {{ currentData.transactions.length }} items</p>
<div v-if="currentData.transactions.length > 0" class="mt-2">
<p class="text-sm font-medium">Sample transactions:</p>
<ul class="text-sm text-gray-600 mt-1">
<li v-for="transaction in currentData.transactions.slice(0, 3)" :key="transaction.id">
{{ transaction.description }}: {{ formatCurrency(transaction.amount) }}
</li>
</ul>
</div>
</div>
</div>
<!-- Navigation -->
<div class="pt-4 border-t">
<NuxtLink
to="/"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Back to Dashboard
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
// Page metadata
useSeoMeta({
title: 'Data Migration - Faber Finances',
description: 'Migrate your financial data'
});
// State
const loading = ref(false);
const status = ref('');
const statusClass = ref('');
const manualData = ref({
balance: 0,
transactionsJson: ''
});
const currentData = ref({
balance: 0,
transactions: []
});
// Load current data status
const loadCurrentData = async () => {
loading.value = true;
try {
// Load balance
const balanceResponse = await fetch('/api/balances');
if (balanceResponse.ok) {
const balanceData = await balanceResponse.json();
currentData.value.balance = balanceData.currentBalance || 0;
}
// Load transactions
const transactionsResponse = await fetch('/api/transactions');
if (transactionsResponse.ok) {
const transactionsData = await transactionsResponse.json();
currentData.value.transactions = transactionsData;
}
setStatus('Data loaded successfully', 'success');
} catch (error) {
console.error('Failed to load current data:', error);
setStatus('Failed to load current data: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Migrate from localStorage
const migrateFromLocalStorage = async () => {
loading.value = true;
try {
if (typeof window === 'undefined') {
throw new Error('Window not available');
}
const savedBalance = localStorage.getItem('cashflow_balance');
const savedTransactions = localStorage.getItem('cashflow_transactions');
if (!savedBalance && !savedTransactions) {
setStatus('No localStorage data found to migrate', 'warning');
return;
}
const migrationData = {
balance: savedBalance ? parseFloat(savedBalance) : 0,
transactions: savedTransactions ? JSON.parse(savedTransactions) : []
};
const response = await fetch('/api/migrate/localStorage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(migrationData)
});
if (!response.ok) {
throw new Error(`Migration failed: ${response.statusText}`);
}
const result = await response.json();
setStatus(
`Migration successful! Migrated ${result.transactionsCount} transactions and balance of ${formatCurrency(result.balance)}`,
'success'
);
// Clear localStorage after successful migration
localStorage.removeItem('cashflow_balance');
localStorage.removeItem('cashflow_transactions');
localStorage.removeItem('account_balances');
// Refresh current data
await loadCurrentData();
} catch (error) {
console.error('Migration error:', error);
setStatus('Migration failed: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Import manual data
const migrateManualData = async () => {
loading.value = true;
try {
let transactions = [];
if (manualData.value.transactionsJson.trim()) {
try {
transactions = JSON.parse(manualData.value.transactionsJson);
if (!Array.isArray(transactions)) {
throw new Error('Transactions must be an array');
}
} catch (error) {
throw new Error('Invalid JSON format: ' + error.message);
}
}
const migrationData = {
balance: manualData.value.balance || 0,
transactions: transactions
};
const response = await fetch('/api/migrate/localStorage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(migrationData)
});
if (!response.ok) {
throw new Error(`Import failed: ${response.statusText}`);
}
const result = await response.json();
setStatus(
`Import successful! Imported ${result.transactionsCount} transactions and balance of ${formatCurrency(result.balance)}`,
'success'
);
// Clear form
manualData.value = { balance: 0, transactionsJson: '' };
// Refresh current data
await loadCurrentData();
} catch (error) {
console.error('Import error:', error);
setStatus('Import failed: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Utility functions
const setStatus = (message, type) => {
status.value = message;
statusClass.value = type === 'success' ? 'bg-green-100 text-green-800' :
type === 'warning' ? 'bg-yellow-100 text-yellow-800' :
type === 'error' ? 'bg-red-100 text-red-800' :
'bg-blue-100 text-blue-800';
// Clear status after 5 seconds
setTimeout(() => {
status.value = '';
}, 5000);
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: 'CAD'
}).format(amount || 0);
};
// Load current data on mount
onMounted(() => {
loadCurrentData();
});
</script>