Files
delphi-database/templates/financial.html
2025-08-08 15:55:15 -05:00

1150 lines
50 KiB
HTML

{% extends "base.html" %}
{% block title %}Financial/Ledger - Delphi Database{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-calculator"></i> Financial/Ledger</h2>
<div>
<button class="btn btn-success" id="quickTimeBtn">
<i class="bi bi-clock"></i> Quick Time (Ctrl+T)
</button>
<button class="btn btn-warning" id="unbilledBtn">
<i class="bi bi-hourglass-split"></i> Unbilled Items
</button>
<button class="btn btn-info" id="dashboardBtn">
<i class="bi bi-graph-up"></i> Dashboard
</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-file-earmark-text"></i> Reports
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" id="timeReportBtn">Time Summary Report</a></li>
<li><a class="dropdown-item" href="#" id="billingReportBtn">Billing Report</a></li>
<li><a class="dropdown-item" href="#" id="financialReportBtn">Financial Statement</a></li>
</ul>
</div>
</div>
</div>
<!-- Financial Dashboard Summary -->
<div class="row mb-4" id="dashboardSummary">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h3 id="totalCharges">$0.00</h3>
<small>Total Charges</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h3 id="totalOwing">$0.00</h3>
<small>Amount Owing</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<h3 id="unbilledAmount">$0.00</h3>
<small>Unbilled</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h3 id="totalHours">0.0</h3>
<small>Total Hours</small>
</div>
</div>
</div>
</div>
<!-- Recent Time Entries -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Time Entries</h5>
<div>
<select class="form-select form-select-sm d-inline-block w-auto" id="recentDaysFilter">
<option value="7">Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
</select>
<select class="form-select form-select-sm d-inline-block w-auto ms-2" id="employeeFilter">
<option value="">All Employees</option>
</select>
<button class="btn btn-sm btn-outline-secondary ms-2" id="refreshRecentBtn">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Date</th>
<th>File</th>
<th>Client</th>
<th>Employee</th>
<th>Hours</th>
<th>Rate</th>
<th>Amount</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="recentEntriesTableBody">
<!-- Recent entries will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Action Cards Row -->
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6><i class="bi bi-plus-circle"></i> Quick Actions</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-primary" id="addTimeEntryBtn">
<i class="bi bi-clock"></i> Add Time Entry
</button>
<button class="btn btn-success" id="recordPaymentBtn">
<i class="bi bi-cash-coin"></i> Record Payment
</button>
<button class="btn btn-warning" id="addExpenseBtn">
<i class="bi bi-receipt"></i> Add Expense
</button>
<button class="btn btn-info" id="viewLedgerBtn">
<i class="bi bi-journal-text"></i> View File Ledger
</button>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h6><i class="bi bi-bar-chart"></i> Top Files by Balance</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>File</th>
<th>Total Charges</th>
<th>Amount Owing</th>
</tr>
</thead>
<tbody id="topFilesTableBody">
<!-- Top files will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Time Entry Modal -->
<div class="modal fade" id="quickTimeModal" tabindex="-1" aria-labelledby="quickTimeModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="quickTimeModalLabel">Quick Time Entry</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="quickTimeForm">
<div class="mb-3">
<label for="quickTimeFile" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="quickTimeFile" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectFileBtn">
<i class="bi bi-search"></i> Browse
</button>
</div>
<div class="form-text" id="selectedFileInfo">Enter file number or browse to select</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="quickTimeHours" class="form-label">Hours *</label>
<input type="number" class="form-control" id="quickTimeHours" name="hours" step="0.25" min="0" required>
</div>
<div class="col-md-6">
<label for="quickTimeDate" class="form-label">Date *</label>
<input type="date" class="form-control" id="quickTimeDate" name="entry_date" required>
</div>
</div>
<div class="mt-3">
<label for="quickTimeDescription" class="form-label">Description *</label>
<textarea class="form-control" id="quickTimeDescription" name="description" rows="3" required placeholder="Describe the work performed..."></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveQuickTimeBtn">
<i class="bi bi-check-circle"></i> Save Time Entry
</button>
</div>
</div>
</div>
</div>
<!-- Time Entry Modal (Full) -->
<div class="modal fade" id="timeEntryModal" tabindex="-1" aria-labelledby="timeEntryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="timeEntryModalLabel">Time Entry</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="timeEntryForm">
<div class="row g-3">
<div class="col-md-6">
<label for="timeEntryFile" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="timeEntryFile" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectTimeFileBtn">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label for="timeEntryEmployee" class="form-label">Employee</label>
<select class="form-select" id="timeEntryEmployee" name="empl_num">
<option value="">Use file default</option>
</select>
</div>
<div class="col-md-4">
<label for="timeEntryDate" class="form-label">Date *</label>
<input type="date" class="form-control" id="timeEntryDate" name="date" required>
</div>
<div class="col-md-4">
<label for="timeEntryHours" class="form-label">Hours *</label>
<input type="number" class="form-control" id="timeEntryHours" name="quantity" step="0.25" min="0" required>
</div>
<div class="col-md-4">
<label for="timeEntryRate" class="form-label">Rate</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="timeEntryRate" name="rate" step="0.01" min="0" readonly>
</div>
</div>
<div class="col-md-6">
<label for="timeEntryCode" class="form-label">Transaction Code</label>
<select class="form-select" id="timeEntryCode" name="t_code">
<option value="TIME">TIME - Time Entry</option>
</select>
</div>
<div class="col-md-6">
<label for="timeEntryAmount" class="form-label">Amount</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="timeEntryAmount" readonly>
</div>
</div>
<div class="col-12">
<label for="timeEntryNote" class="form-label">Description *</label>
<textarea class="form-control" id="timeEntryNote" name="note" rows="3" required placeholder="Describe the work performed in detail..."></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveTimeEntryBtn">
<i class="bi bi-check-circle"></i> Save Entry
</button>
</div>
</div>
</div>
</div>
<!-- Payment Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1" aria-labelledby="paymentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paymentModalLabel">Record Payment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="paymentForm">
<div class="row g-3">
<div class="col-md-6">
<label for="paymentFile" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="paymentFile" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectPaymentFileBtn">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label for="paymentAmount" class="form-label">Payment Amount *</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="paymentAmount" name="amount" step="0.01" min="0" required>
</div>
</div>
<div class="col-md-6">
<label for="paymentDate" class="form-label">Payment Date *</label>
<input type="date" class="form-control" id="paymentDate" name="payment_date" required>
</div>
<div class="col-md-6">
<label for="paymentMethod" class="form-label">Payment Method</label>
<select class="form-select" id="paymentMethod" name="payment_method">
<option value="CHECK">Check</option>
<option value="CASH">Cash</option>
<option value="CREDIT">Credit Card</option>
<option value="WIRE">Wire Transfer</option>
<option value="ACH">ACH/Bank Transfer</option>
</select>
</div>
<div class="col-md-6">
<label for="paymentReference" class="form-label">Reference/Check #</label>
<input type="text" class="form-control" id="paymentReference" name="reference" placeholder="Check number or reference">
</div>
<div class="col-md-6">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="applyToTrust" name="apply_to_trust">
<label class="form-check-label" for="applyToTrust">
Apply to Trust Account
</label>
</div>
</div>
<div class="col-12">
<label for="paymentNotes" class="form-label">Notes</label>
<textarea class="form-control" id="paymentNotes" name="notes" rows="2" placeholder="Additional payment notes..."></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" id="savePaymentBtn">
<i class="bi bi-cash-coin"></i> Record Payment
</button>
</div>
</div>
</div>
</div>
<!-- Expense Modal -->
<div class="modal fade" id="expenseModal" tabindex="-1" aria-labelledby="expenseModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="expenseModalLabel">Record Expense</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="expenseForm">
<div class="row g-3">
<div class="col-md-6">
<label for="expenseFile" class="form-label">File Number *</label>
<div class="input-group">
<input type="text" class="form-control" id="expenseFile" name="file_no" required>
<button class="btn btn-outline-secondary" type="button" id="selectExpenseFileBtn">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label for="expenseAmount" class="form-label">Amount *</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="expenseAmount" name="amount" step="0.01" min="0" required>
</div>
</div>
<div class="col-md-6">
<label for="expenseDate" class="form-label">Date *</label>
<input type="date" class="form-control" id="expenseDate" name="expense_date" required>
</div>
<div class="col-md-6">
<label for="expenseCategory" class="form-label">Category</label>
<select class="form-select" id="expenseCategory" name="category">
<option value="MISC">Miscellaneous</option>
<option value="COPY">Copies/Printing</option>
<option value="POSTAGE">Postage</option>
<option value="PHONE">Phone/Communication</option>
<option value="TRAVEL">Travel</option>
<option value="FILING">Filing Fees</option>
<option value="SERVICE">Service of Process</option>
<option value="EXPERT">Expert Witness</option>
</select>
</div>
<div class="col-md-6">
<label for="expenseEmployee" class="form-label">Employee</label>
<select class="form-select" id="expenseEmployee" name="employee">
<option value="">Use file default</option>
</select>
</div>
<div class="col-md-6">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="expenseReceipts" name="receipts">
<label class="form-check-label" for="expenseReceipts">
Receipts on File
</label>
</div>
</div>
<div class="col-12">
<label for="expenseDescription" class="form-label">Description *</label>
<textarea class="form-control" id="expenseDescription" name="description" rows="3" required placeholder="Describe the expense..."></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" id="saveExpenseBtn">
<i class="bi bi-receipt"></i> Record Expense
</button>
</div>
</div>
</div>
</div>
<!-- Unbilled Items Modal -->
<div class="modal fade" id="unbilledModal" tabindex="-1" aria-labelledby="unbilledModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unbilledModalLabel">Unbilled Items</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-8">
<div class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" id="unbilledFileFilter" placeholder="Filter by file...">
</div>
<div class="col-md-4">
<select class="form-select form-select-sm" id="unbilledEmployeeFilter">
<option value="">All employees</option>
</select>
</div>
<div class="col-md-4">
<button class="btn btn-sm btn-outline-secondary" id="refreshUnbilledBtn">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div class="bg-warning p-2 rounded">
<strong>Total Unbilled: <span id="totalUnbilledAmount">$0.00</span></strong>
</div>
</div>
</div>
<div id="unbilledItemsContainer">
<!-- Unbilled items will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="billSelectedBtn" disabled>
<i class="bi bi-check-circle"></i> Mark Selected as Billed
</button>
</div>
</div>
</div>
</div>
<!-- Financial Dashboard Modal -->
<div class="modal fade" id="financialDashboardModal" tabindex="-1" aria-labelledby="financialDashboardModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="financialDashboardModalLabel">Financial Dashboard</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="financialDashboardContent">
<!-- Dashboard content will be loaded here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
// Financial management functionality
let dashboardData = null;
let recentEntries = [];
let unbilledData = null;
// Helper function for authenticated API calls
function getAuthHeaders() {
const token = localStorage.getItem('auth_token');
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Check authentication first
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
initializeFinancialPage();
setupEventListeners();
loadDashboardData();
loadRecentTimeEntries();
loadEmployeeOptions();
// Set default dates
const today = new Date().toISOString().split('T')[0];
document.getElementById('quickTimeDate').value = today;
document.getElementById('timeEntryDate').value = today;
document.getElementById('paymentDate').value = today;
document.getElementById('expenseDate').value = today;
});
function initializeFinancialPage() {
// Initialize any data tables or components
console.log('Financial page initialized');
}
function setupEventListeners() {
// Quick actions
document.getElementById('quickTimeBtn').addEventListener('click', showQuickTimeModal);
document.getElementById('addTimeEntryBtn').addEventListener('click', showTimeEntryModal);
document.getElementById('recordPaymentBtn').addEventListener('click', showPaymentModal);
document.getElementById('addExpenseBtn').addEventListener('click', showExpenseModal);
document.getElementById('unbilledBtn').addEventListener('click', showUnbilledModal);
document.getElementById('dashboardBtn').addEventListener('click', showDashboardModal);
// Modal save buttons
document.getElementById('saveQuickTimeBtn').addEventListener('click', saveQuickTime);
document.getElementById('saveTimeEntryBtn').addEventListener('click', saveTimeEntry);
document.getElementById('savePaymentBtn').addEventListener('click', savePayment);
document.getElementById('saveExpenseBtn').addEventListener('click', saveExpense);
document.getElementById('billSelectedBtn').addEventListener('click', billSelectedEntries);
// Filters
document.getElementById('recentDaysFilter').addEventListener('change', loadRecentTimeEntries);
document.getElementById('employeeFilter').addEventListener('change', loadRecentTimeEntries);
document.getElementById('refreshRecentBtn').addEventListener('click', loadRecentTimeEntries);
// File selection buttons
document.getElementById('selectFileBtn').addEventListener('click', () => showFileSelector('quickTimeFile'));
document.getElementById('selectTimeFileBtn').addEventListener('click', () => showFileSelector('timeEntryFile'));
document.getElementById('selectPaymentFileBtn').addEventListener('click', () => showFileSelector('paymentFile'));
document.getElementById('selectExpenseFileBtn').addEventListener('click', () => showFileSelector('expenseFile'));
// Calculate amounts when values change
document.getElementById('timeEntryHours').addEventListener('input', calculateTimeAmount);
document.getElementById('timeEntryRate').addEventListener('input', calculateTimeAmount);
document.getElementById('quickTimeHours').addEventListener('blur', validateQuickTimeFile);
}
async function loadDashboardData() {
try {
const response = await fetch('/api/financial/financial-dashboard', {
headers: getAuthHeaders()
});
if (!response.ok) throw new Error('Failed to load dashboard data');
dashboardData = await response.json();
updateDashboardSummary(dashboardData);
} catch (error) {
console.error('Error loading dashboard data:', error);
showAlert('Error loading financial dashboard: ' + error.message, 'danger');
}
}
function updateDashboardSummary(data) {
document.getElementById('totalCharges').textContent = formatCurrency(data.summary.total_charges);
document.getElementById('totalOwing').textContent = formatCurrency(data.summary.total_owing);
document.getElementById('unbilledAmount').textContent = formatCurrency(data.summary.unbilled_amount);
document.getElementById('totalHours').textContent = data.summary.total_hours.toFixed(1);
// Update top files table
const tbody = document.getElementById('topFilesTableBody');
tbody.innerHTML = '';
data.top_files.forEach(file => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${file.file_no}</strong></td>
<td class="text-end">${formatCurrency(file.total_charges)}</td>
<td class="text-end text-success"><strong>${formatCurrency(file.amount_owing)}</strong></td>
`;
tbody.appendChild(row);
});
}
async function loadRecentTimeEntries() {
const days = document.getElementById('recentDaysFilter').value;
const employee = document.getElementById('employeeFilter').value;
try {
const params = new URLSearchParams({ days });
if (employee) params.append('employee', employee);
const response = await fetch(`/api/financial/time-entries/recent?${params}`, {
headers: getAuthHeaders()
});
if (!response.ok) throw new Error('Failed to load recent entries');
const data = await response.json();
recentEntries = data.entries;
displayRecentTimeEntries(data.entries);
} catch (error) {
console.error('Error loading recent entries:', error);
showAlert('Error loading recent time entries: ' + error.message, 'danger');
}
}
function displayRecentTimeEntries(entries) {
const tbody = document.getElementById('recentEntriesTableBody');
tbody.innerHTML = '';
if (entries.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">No recent time entries found</td></tr>';
return;
}
entries.forEach(entry => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${formatDate(entry.date)}</td>
<td><strong>${entry.file_no}</strong></td>
<td>${entry.client_name}</td>
<td>${entry.employee}</td>
<td class="text-center">${entry.hours}</td>
<td class="text-end">${formatCurrency(entry.rate)}</td>
<td class="text-end text-success"><strong>${formatCurrency(entry.amount)}</strong></td>
<td class="small">${entry.description ? entry.description.substring(0, 50) + (entry.description.length > 50 ? '...' : '') : ''}</td>
<td>
<span class="badge bg-${entry.billed ? 'success' : 'warning'}">
${entry.billed ? 'Billed' : 'Unbilled'}
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="editTimeEntry(${entry.id})">
<i class="bi bi-pencil"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
async function loadEmployeeOptions() {
try {
const response = await fetch('/api/files/lookups/employees', {
headers: getAuthHeaders()
});
if (response.ok) {
const employees = await response.json();
populateEmployeeSelects(employees);
}
} catch (error) {
console.error('Error loading employees:', error);
}
}
function populateEmployeeSelects(employees) {
const selects = ['employeeFilter', 'timeEntryEmployee', 'expenseEmployee', 'unbilledEmployeeFilter'];
selects.forEach(selectId => {
const select = document.getElementById(selectId);
if (!select) return;
// Keep first option
const firstOption = select.children[0];
select.innerHTML = '';
if (firstOption) select.appendChild(firstOption);
employees.forEach(emp => {
const option = document.createElement('option');
option.value = emp.code;
option.textContent = `${emp.code} - ${emp.name}`;
select.appendChild(option);
});
});
}
function showQuickTimeModal() {
new bootstrap.Modal(document.getElementById('quickTimeModal')).show();
}
function showTimeEntryModal() {
new bootstrap.Modal(document.getElementById('timeEntryModal')).show();
}
function showPaymentModal() {
new bootstrap.Modal(document.getElementById('paymentModal')).show();
}
function showExpenseModal() {
new bootstrap.Modal(document.getElementById('expenseModal')).show();
}
async function saveQuickTime() {
const form = document.getElementById('quickTimeForm');
const formData = new FormData(form);
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
const data = {
file_no: formData.get('file_no'),
hours: parseFloat(formData.get('hours')),
description: formData.get('description'),
entry_date: formData.get('entry_date') || null
};
try {
const response = await fetch('/api/financial/time-entry/quick', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save time entry');
}
const result = await response.json();
bootstrap.Modal.getInstance(document.getElementById('quickTimeModal')).hide();
showAlert(result.message, 'success');
// Refresh data
loadDashboardData();
loadRecentTimeEntries();
// Clear form
form.reset();
document.getElementById('quickTimeDate').value = new Date().toISOString().split('T')[0];
} catch (error) {
console.error('Error saving quick time:', error);
showAlert('Error saving time entry: ' + error.message, 'danger');
}
}
async function savePayment() {
const form = document.getElementById('paymentForm');
const formData = new FormData(form);
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
const data = {
file_no: formData.get('file_no'),
amount: parseFloat(formData.get('amount')),
payment_date: formData.get('payment_date') || null,
payment_method: formData.get('payment_method'),
reference: formData.get('reference') || null,
notes: formData.get('notes') || null,
apply_to_trust: formData.get('apply_to_trust') === 'on'
};
try {
const response = await fetch('/api/financial/payments/', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to record payment');
}
const result = await response.json();
bootstrap.Modal.getInstance(document.getElementById('paymentModal')).hide();
showAlert(result.message, 'success');
// Refresh data
loadDashboardData();
// Clear form
form.reset();
document.getElementById('paymentDate').value = new Date().toISOString().split('T')[0];
} catch (error) {
console.error('Error recording payment:', error);
showAlert('Error recording payment: ' + error.message, 'danger');
}
}
async function saveExpense() {
const form = document.getElementById('expenseForm');
const formData = new FormData(form);
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
const data = {
file_no: formData.get('file_no'),
amount: parseFloat(formData.get('amount')),
description: formData.get('description'),
expense_date: formData.get('expense_date') || null,
category: formData.get('category'),
employee: formData.get('employee') || null,
receipts: formData.get('receipts') === 'on'
};
try {
const response = await fetch('/api/financial/expenses/', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to record expense');
}
const result = await response.json();
bootstrap.Modal.getInstance(document.getElementById('expenseModal')).hide();
showAlert(result.message, 'success');
// Refresh data
loadDashboardData();
// Clear form
form.reset();
document.getElementById('expenseDate').value = new Date().toISOString().split('T')[0];
} catch (error) {
console.error('Error recording expense:', error);
showAlert('Error recording expense: ' + error.message, 'danger');
}
}
async function showUnbilledModal() {
// Load unbilled data
try {
const response = await fetch('/api/financial/unbilled-entries', {
headers: getAuthHeaders()
});
if (!response.ok) throw new Error('Failed to load unbilled entries');
unbilledData = await response.json();
displayUnbilledItems(unbilledData);
new bootstrap.Modal(document.getElementById('unbilledModal')).show();
} catch (error) {
console.error('Error loading unbilled entries:', error);
showAlert('Error loading unbilled entries: ' + error.message, 'danger');
}
}
function displayUnbilledItems(data) {
document.getElementById('totalUnbilledAmount').textContent = formatCurrency(data.total_unbilled_amount);
const container = document.getElementById('unbilledItemsContainer');
container.innerHTML = '';
if (data.files.length === 0) {
container.innerHTML = '<div class="text-center text-muted p-4">No unbilled entries found</div>';
return;
}
data.files.forEach(file => {
const fileCard = document.createElement('div');
fileCard.className = 'card mb-3';
fileCard.innerHTML = `
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>${file.file_no}</strong> - ${file.client_name}
<br><small class="text-muted">${file.matter}</small>
</div>
<div class="text-end">
<div><strong>Total: ${formatCurrency(file.total_amount)}</strong></div>
<div><small>${file.total_hours.toFixed(2)} hours</small></div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>
<input type="checkbox" class="form-check-input file-select-all"
data-file="${file.file_no}">
</th>
<th>Date</th>
<th>Type</th>
<th>Employee</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${file.entries.map(entry => `
<tr>
<td>
<input type="checkbox" class="form-check-input entry-checkbox"
data-entry-id="${entry.id}" data-file="${file.file_no}">
</td>
<td>${formatDate(entry.date)}</td>
<td>${entry.type}</td>
<td>${entry.employee}</td>
<td class="text-center">${entry.quantity}</td>
<td class="text-end">${formatCurrency(entry.rate)}</td>
<td class="text-end text-success">${formatCurrency(entry.amount)}</td>
<td class="small">${entry.description || ''}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
container.appendChild(fileCard);
});
// Add event listeners for checkboxes
setupUnbilledCheckboxes();
}
function setupUnbilledCheckboxes() {
// File select all checkboxes
document.querySelectorAll('.file-select-all').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const fileNo = this.dataset.file;
const checked = this.checked;
document.querySelectorAll(`.entry-checkbox[data-file="${fileNo}"]`).forEach(entryCheckbox => {
entryCheckbox.checked = checked;
});
updateBillButton();
});
});
// Individual entry checkboxes
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBillButton);
});
}
function updateBillButton() {
const selectedEntries = document.querySelectorAll('.entry-checkbox:checked');
const billButton = document.getElementById('billSelectedBtn');
billButton.disabled = selectedEntries.length === 0;
billButton.textContent = selectedEntries.length > 0
? `Mark ${selectedEntries.length} Entries as Billed`
: 'Mark Selected as Billed';
}
async function billSelectedEntries() {
const selectedCheckboxes = document.querySelectorAll('.entry-checkbox:checked');
const entryIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.dataset.entryId));
if (entryIds.length === 0) {
showAlert('Please select entries to bill', 'warning');
return;
}
try {
const response = await fetch('/api/financial/bill-entries', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ entry_ids: entryIds })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to bill entries');
}
const result = await response.json();
showAlert(result.message, 'success');
// Refresh unbilled data
showUnbilledModal();
loadDashboardData();
loadRecentTimeEntries();
} catch (error) {
console.error('Error billing entries:', error);
showAlert('Error billing entries: ' + error.message, 'danger');
}
}
async function showDashboardModal() {
if (!dashboardData) {
await loadDashboardData();
}
displayDashboardModal(dashboardData);
new bootstrap.Modal(document.getElementById('financialDashboardModal')).show();
}
function displayDashboardModal(data) {
const content = document.getElementById('financialDashboardContent');
content.innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Financial Summary</h6>
<ul class="list-unstyled">
<li><strong>Total Charges:</strong> ${formatCurrency(data.summary.total_charges)}</li>
<li><strong>Amount Owing:</strong> ${formatCurrency(data.summary.total_owing)}</li>
<li><strong>Trust Balance:</strong> ${formatCurrency(data.summary.total_trust)}</li>
<li><strong>Unbilled Amount:</strong> ${formatCurrency(data.summary.unbilled_amount)}</li>
<li><strong>Total Hours:</strong> ${data.summary.total_hours.toFixed(1)}</li>
</ul>
<h6>Recent Activity (${data.recent_activity.period_days} days)</h6>
<ul class="list-unstyled">
<li><strong>Entries:</strong> ${data.recent_activity.entries_count}</li>
<li><strong>Amount:</strong> ${formatCurrency(data.recent_activity.total_amount)}</li>
</ul>
</div>
<div class="col-md-6">
<h6>Employee Activity (30 days)</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Employee</th>
<th>Hours</th>
<th>Amount</th>
<th>Entries</th>
</tr>
</thead>
<tbody>
${data.employee_stats.map(stat => `
<tr>
<td>${stat.employee}</td>
<td class="text-end">${stat.hours.toFixed(1)}</td>
<td class="text-end">${formatCurrency(stat.amount)}</td>
<td class="text-center">${stat.entries}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
}
function calculateTimeAmount() {
const hours = parseFloat(document.getElementById('timeEntryHours').value) || 0;
const rate = parseFloat(document.getElementById('timeEntryRate').value) || 0;
const amount = hours * rate;
document.getElementById('timeEntryAmount').value = formatCurrency(amount).replace('$', '');
}
function showFileSelector(targetInputId) {
// This would open a file selection modal
// For now, just focus on the input
document.getElementById(targetInputId).focus();
}
async function validateQuickTimeFile() {
const fileNo = document.getElementById('quickTimeFile').value;
if (!fileNo) return;
try {
const response = await fetch(`/api/files/${fileNo}`, {
headers: getAuthHeaders()
});
if (response.ok) {
const file = await response.json();
document.getElementById('selectedFileInfo').textContent =
`File: ${file.file_no} - Rate: ${formatCurrency(file.rate_per_hour)}/hr`;
} else {
document.getElementById('selectedFileInfo').textContent = 'File not found';
}
} catch (error) {
document.getElementById('selectedFileInfo').textContent = 'Error validating file';
}
}
// Utility functions
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString();
}
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount || 0);
}
function showAlert(message, type = 'info') {
// Create and show Bootstrap alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}