Initial project

This commit is contained in:
2025-05-08 16:12:33 +12:00
commit 5340430298
11 changed files with 942 additions and 0 deletions

View File

@@ -0,0 +1,588 @@
<!DOCTYPE html>
<html>
<head>
<title>Accounting System</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.section { margin-bottom: 30px; padding: 15px; border: 1px solid #eee; border-radius: 5px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.tabs { margin-bottom: 20px; }
.tab { padding: 10px 15px; cursor: pointer; background: #f1f1f1; display: inline-block; margin-right: 5px; }
.tab.active { background: #4CAF50; color: white; }
input, select { padding: 8px; margin: 5px 0; width: 200px; }
button { padding: 8px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; margin: 5px; }
button:hover { background: #45a049; }
button:disabled { background: #cccccc; }
.file-input { margin: 15px 0; }
.score-bar { height: 10px; background-color: #4CAF50; margin: 5px 0; }
.match-quality-filters { margin: 15px 0; }
.match-quality-filters label { margin-right: 15px; }
.report-actions { margin-top: 20px; }
.primary { background-color: #2196F3; }
.danger { background-color: #f44336; }
.bank-section { margin-bottom: 30px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<h1>Accounting System</h1>
<div class="tabs">
<div class="tab" :class="{active: activeTab === 'accounts'}" @click="activeTab = 'accounts'">Accounts</div>
<div class="tab" :class="{active: activeTab === 'trial-balance'}" @click="activeTab = 'trial-balance'">Trial Balance</div>
<div class="tab" :class="{active: activeTab === 'banking'}" @click="activeTab = 'banking'">Bank Reconciliation</div>
<div class="tab" :class="{active: activeTab === 'reports'}" @click="activeTab = 'reports'">Reports</div>
</div>
<!-- Accounts Tab -->
<div class="section" v-if="activeTab === 'accounts'">
<h2>Accounts</h2>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Type</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts">
<td>{{ account.code }}</td>
<td>{{ account.name }}</td>
<td>{{ account.type }}</td>
<td>{{ formatCurrency(account.balance) }}</td>
</tr>
</tbody>
</table>
<h3>Add New Account</h3>
<div class="form-group">
<label>Account Name</label>
<input v-model="newAccount.name" placeholder="Account Name">
</div>
<div class="form-group">
<label>Account Code</label>
<input v-model="newAccount.code" placeholder="Account Code">
</div>
<div class="form-group">
<label>Account Type</label>
<select v-model="newAccount.type">
<option value="Asset">Asset</option>
<option value="Liability">Liability</option>
<option value="Equity">Equity</option>
<option value="Revenue">Revenue</option>
<option value="Expense">Expense</option>
</select>
</div>
<button @click="addAccount">Add Account</button>
</div>
<!-- Trial Balance Tab -->
<div class="section" v-if="activeTab === 'trial-balance'">
<h2>Trial Balance</h2>
<table>
<thead>
<tr>
<th>Account</th>
<th>Debit</th>
<th>Credit</th>
</tr>
</thead>
<tbody>
<tr v-for="item in trialBalance">
<td>{{ item.account }}</td>
<td>{{ formatCurrency(item.debit) }}</td>
<td>{{ formatCurrency(item.credit) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Banking Tab -->
<div class="section" v-if="activeTab === 'banking'">
<h2>Bank Reconciliation</h2>
<div class="bank-section">
<h3>Bank Accounts</h3>
<table>
<thead>
<tr>
<th>Bank</th>
<th>Account Number</th>
<th>Name</th>
<th>Currency</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="account in bankAccounts">
<td>{{ account.bank_name }}</td>
<td>{{ account.account_number }}</td>
<td>{{ account.name }}</td>
<td>{{ account.currency }}</td>
<td>
<button @click="viewReconciliation(account.id)">Reconcile</button>
</td>
</tr>
</tbody>
</table>
<h3>Add New Bank Account</h3>
<div class="form-group">
<label>Account Name</label>
<input v-model="newBankAccount.name" placeholder="Account Name">
</div>
<div class="form-group">
<label>Bank Name</label>
<input v-model="newBankAccount.bank_name" placeholder="Bank Name">
</div>
<div class="form-group">
<label>Account Number</label>
<input v-model="newBankAccount.account_number" placeholder="Account Number">
</div>
<div class="form-group">
<label>Currency</label>
<input v-model="newBankAccount.currency" placeholder="Currency" value="USD">
</div>
<button @click="addBankAccount">Add Bank Account</button>
</div>
<div class="bank-section" v-if="selectedBankAccount">
<h3>Import Transactions</h3>
<div class="form-group">
<label>Select Bank Account</label>
<select v-model="selectedBankAccount">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<div class="form-group">
<label>CSV File</label>
<input type="file" @change="handleFileUpload" accept=".csv">
</div>
<button @click="importTransactions" :disabled="!selectedBankAccount || !fileData">Import</button>
<div v-if="importResult">
<p>Imported {{ importResult.imported.length }} transactions</p>
</div>
</div>
<div class="bank-section" v-if="selectedBankAccountForReconciliation">
<h3>Unreconciled Transactions</h3>
<div class="form-group">
<label>Select Bank Account</label>
<select v-model="selectedBankAccountForReconciliation" @change="fetchUnreconciledTransactions">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<table v-if="unreconciledTransactions.length > 0">
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th>Amount</th>
<th>Match With Journal Entry</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="txn in unreconciledTransactions">
<td>{{ txn.date }}</td>
<td>{{ txn.description }}</td>
<td>{{ formatCurrency(txn.amount) }}</td>
<td>
<select v-model="selectedJournalEntries[txn.id]">
<option value="">Select matching entry</option>
<option v-for="entry in potentialMatches(txn)" :value="entry.id">
{{ entry.date }} - {{ entry.description }} ({{ formatCurrency(entry.amount) }})
</option>
</select>
</td>
<td>
<button @click="reconcile(txn.id)" :disabled="!selectedJournalEntries[txn.id]">Reconcile</button>
</td>
</tr>
</tbody>
</table>
<p v-else>No unreconciled transactions found</p>
</div>
</div>
<!-- Reports Tab -->
<div class="section" v-if="activeTab === 'reports'">
<h2>Reconciliation Reports</h2>
<div v-if="!activeReport">
<h3>Create New Report</h3>
<div class="form-group">
<label>Bank Account</label>
<select v-model="newReport.bank_account_id">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<div class="form-group">
<label>Start Date</label>
<input type="date" v-model="newReport.start_date">
</div>
<div class="form-group">
<label>End Date</label>
<input type="date" v-model="newReport.end_date">
</div>
<button @click="createReconciliationReport"
:disabled="!newReport.bank_account_id || !newReport.start_date || !newReport.end_date">
Generate Report
</button>
<h3>Recent Reports</h3>
<table v-if="reconciliationReports.length > 0">
<thead>
<tr>
<th>Bank Account</th>
<th>Date Range</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="report in reconciliationReports">
<td>{{ getBankAccountName(report.bank_account_id) }}</td>
<td>{{ report.start_date }} to {{ report.end_date }}</td>
<td>{{ report.status }}</td>
<td>{{ report.created_at }}</td>
<td>
<button @click="loadReport(report.id)">View</button>
</td>
</tr>
</tbody>
</table>
<p v-else>No reconciliation reports found</p>
</div>
<div v-if="activeReport">
<h3>Reconciliation Report - {{ activeReport.start_date }} to {{ activeReport.end_date }}</h3>
<p>Bank Account: {{ getBankAccountName(activeReport.bank_account_id) }}</p>
<p>Status: {{ activeReport.status }}</p>
<div class="match-quality-filters">
<label>
<input type="checkbox" v-model="filterMatches" value="high"> High Confidence (>80%)
</label>
<label>
<input type="checkbox" v-model="filterMatches" value="medium"> Medium Confidence (50-80%)
</label>
</div>
<table>
<thead>
<tr>
<th>Bank Transaction</th>
<th>Match Score</th>
<th>Journal Entry</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="match in filteredMatches">
<td>
{{ match.transaction_date }}<br>
{{ match.transaction_description }}<br>
<strong>{{ formatCurrency(match.transaction_amount) }}</strong>
</td>
<td>
<div class="score-bar" :style="{width: match.match_score + '%'}"></div>
{{ match.match_score }}%
</td>
<td>
{{ match.journal_entry_date }}<br>
{{ match.journal_entry_description }}
</td>
<td>
<button @click="acceptMatch(match)">Accept</button>
<button @click="rejectMatch(match)" class="danger">Reject</button>
</td>
</tr>
</tbody>
</table>
<div class="report-actions">
<button @click="finalizeReconciliation" class="primary" v-if="activeReport.status === 'draft'">Finalize Reconciliation</button>
<button @click="cancelReconciliation">Back to Reports</button>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
activeTab: 'accounts',
accounts: [],
trialBalance: [],
newAccount: {
name: '',
code: '',
type: 'Asset'
},
bankAccounts: [],
newBankAccount: {
name: '',
bank_name: '',
account_number: '',
currency: 'USD'
},
selectedBankAccount: null,
selectedBankAccountForReconciliation: null,
fileData: null,
importResult: null,
unreconciledTransactions: [],
selectedJournalEntries: {},
journalEntries: [],
reconciliationReports: [],
newReport: {
bank_account_id: null,
start_date: '',
end_date: ''
},
activeReport: null,
filterMatches: ['high', 'medium']
}
},
computed: {
filteredMatches() {
if (!this.activeReport) return [];
return this.activeReport.matches.filter(match => {
if (this.filterMatches.includes('high') && match.match_score >= 80) return true;
if (this.filterMatches.includes('medium') && match.match_score >= 50 && match.match_score < 80) return true;
return false;
});
}
},
mounted() {
this.fetchAccounts();
this.fetchTrialBalance();
this.fetchBankAccounts();
this.fetchJournalEntries();
this.fetchReconciliationReports();
},
methods: {
formatCurrency(value) {
if (value === undefined || value === null) return '$0.00';
return '$' + parseFloat(value).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
},
fetchAccounts() {
axios.get('/api/accounts')
.then(response => {
this.accounts = response.data;
});
},
fetchTrialBalance() {
axios.get('/api/trial_balance')
.then(response => {
this.trialBalance = response.data;
});
},
addAccount() {
axios.post('/api/add_account', this.newAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchAccounts();
this.newAccount = { name: '', code: '', type: 'Asset' };
}
});
},
fetchBankAccounts() {
axios.get('/api/bank_accounts')
.then(response => {
this.bankAccounts = response.data;
});
},
addBankAccount() {
axios.post('/api/add_bank_account', this.newBankAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchBankAccounts();
this.newBankAccount = { name: '', bank_name: '', account_number: '', currency: 'USD' };
}
});
},
handleFileUpload(event) {
this.fileData = event.target.files[0];
},
importTransactions() {
if (!this.fileData || !this.selectedBankAccount) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const lines = content.split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const transactions = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = lines[i].split(',');
const txn = {};
for (let j = 0; j < headers.length; j++) {
txn[headers[j]] = values[j] ? values[j].trim() : '';
}
// Convert amount to number
if (txn.amount) {
txn.amount = parseFloat(txn.amount);
}
transactions.push(txn);
}
axios.post('/api/import_bank_transactions', {
bank_account_id: this.selectedBankAccount,
transactions: transactions
}).then(response => {
this.importResult = response.data;
this.fileData = null;
document.querySelector('input[type="file"]').value = '';
if (this.selectedBankAccount === this.selectedBankAccountForReconciliation) {
this.fetchUnreconciledTransactions();
}
});
};
reader.readAsText(this.fileData);
},
fetchUnreconciledTransactions() {
if (this.selectedBankAccountForReconciliation) {
axios.get(`/api/get_unreconciled_transactions?bank_account_id=${this.selectedBankAccountForReconciliation}`)
.then(response => {
this.unreconciledTransactions = response.data;
});
}
},
fetchJournalEntries() {
axios.get('/api/journal_entries')
.then(response => {
this.journalEntries = response.data.map(entry => {
const debitLines = entry.lines.filter(line => line.is_debit);
const creditLines = entry.lines.filter(line => !line.is_debit);
return {
id: entry.id,
date: entry.date,
description: entry.description,
amount: debitLines.reduce((sum, line) => sum + line.amount, 0),
lines: entry.lines
};
});
});
},
potentialMatches(txn) {
// Simple matching by amount and date
const txnDate = new Date(txn.date);
return this.journalEntries.filter(entry => {
const entryDate = new Date(entry.date);
return Math.abs(entry.amount - txn.amount) < 0.01 &&
txnDate.toDateString() === entryDate.toDateString();
});
},
reconcile(txnId) {
const journalEntryId = this.selectedJournalEntries[txnId];
if (!journalEntryId) return;
axios.post('/api/reconcile_transaction', {
transaction_id: txnId,
journal_entry_id: journalEntryId
}).then(response => {
if (response.data.status === 'success') {
this.fetchUnreconciledTransactions();
this.selectedJournalEntries[txnId] = null;
} else {
alert(response.data.message);
}
});
},
fetchReconciliationReports() {
axios.get('/api/reconciliation_reports')
.then(response => {
this.reconciliationReports = response.data;
});
},
createReconciliationReport() {
axios.post('/api/create_reconciliation_report', this.newReport)
.then(response => {
if (response.data.status === 'success') {
this.loadReport(response.data.report_id);
this.fetchReconciliationReports();
}
});
},
loadReport(reportId) {
axios.get(`/api/get_reconciliation_report?report_id=${reportId}`)
.then(response => {
if (response.data.status === 'success') {
this.activeReport = response.data.report;
this.activeTab = 'reports';
}
});
},
acceptMatch(match) {
axios.post('/api/reconcile_transaction', {
transaction_id: match.transaction_id,
journal_entry_id: match.journal_entry_id
}).then(response => {
if (response.data.status === 'success') {
this.loadReport(this.activeReport.id);
}
});
},
rejectMatch(match) {
axios.delete(`/api/reconciliation_match/${match.id}`)
.then(() => {
this.loadReport(this.activeReport.id);
});
},
finalizeReconciliation() {
axios.post('/api/finalize_reconciliation', {
report_id: this.activeReport.id
}).then(response => {
if (response.data.status === 'success') {
this.activeReport = null;
this.fetchReconciliationReports();
this.fetchUnreconciledTransactions();
}
});
},
cancelReconciliation() {
this.activeReport = null;
},
getBankAccountName(accountId) {
const account = this.bankAccounts.find(a => a.id === accountId);
return account ? `${account.bank_name} - ${account.account_number}` : '';
},
viewReconciliation(accountId) {
this.selectedBankAccountForReconciliation = accountId;
this.fetchUnreconciledTransactions();
this.activeTab = 'banking';
}
}
});
</script>
</body>
</html>