Init commit!
This commit is contained in:
commit
086d682592
34 changed files with 19249 additions and 0 deletions
285
app/pages/migrate.vue
Normal file
285
app/pages/migrate.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue