1150 lines
50 KiB
HTML
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 %} |