Files
delphi-database/templates/financial.html
2025-08-11 21:58:25 -05:00

1113 lines
58 KiB
HTML

{% extends "base.html" %}
{% block title %}Financial/Ledger - Delphi Database{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
<i class="fa-solid fa-calculator text-lg"></i>
</div>
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Financial/Ledger</h1>
</div>
<div class="flex items-center gap-3">
<button id="quickTimeBtn" class="flex items-center gap-2 px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200">
<i class="fa-regular fa-clock"></i>
<span>Quick Time</span>
<kbd class="hidden sm:inline-block ml-2 px-1.5 py-0.5 bg-success-700 rounded text-xs">Ctrl+T</kbd>
</button>
<button id="unbilledBtn" class="flex items-center gap-2 px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-hourglass-half"></i>
<span>Unbilled Items</span>
</button>
<button id="dashboardBtn" class="flex items-center gap-2 px-4 py-2 bg-info-600 text-white hover:bg-info-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-chart-line"></i>
<span>Dashboard</span>
</button>
<div class="relative">
<button type="button" class="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white hover:bg-gray-700 rounded-lg transition-colors duration-200" onclick="toggleReportsDropdown()">
<i class="fa-regular fa-file-lines"></i>
<span>Reports</span>
</button>
<div id="reportsDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 z-10">
<a href="#" class="block px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="timeReportBtn">Time Summary Report</a>
<a href="#" class="block px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="billingReportBtn">Billing Report</a>
<a href="#" class="block px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="financialReportBtn">Financial Statement</a>
</div>
</div>
</div>
</div>
<!-- Financial Dashboard Summary -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6" id="dashboardSummary">
<div class="bg-primary-600 text-white rounded-lg p-4 text-center">
<h3 class="text-xl font-bold" id="totalCharges">$0.00</h3>
<p class="text-sm">Total Charges</p>
</div>
<div class="bg-success-600 text-white rounded-lg p-4 text-center">
<h3 class="text-xl font-bold" id="totalOwing">$0.00</h3>
<p class="text-sm">Amount Owing</p>
</div>
<div class="bg-warning-600 text-white rounded-lg p-4 text-center">
<h3 class="text-xl font-bold" id="unbilledAmount">$0.00</h3>
<p class="text-sm">Unbilled</p>
</div>
<div class="bg-info-600 text-white rounded-lg p-4 text-center">
<h3 class="text-xl font-bold" id="totalHours">0.0</h3>
<p class="text-sm">Total Hours</p>
</div>
</div>
<!-- Recent Time Entries -->
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden mb-6">
<div class="flex justify-between items-center p-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold"><i class="fa-solid fa-clock-rotate-left mr-2"></i>Recent Time Entries</h2>
<div class="flex gap-2">
<select class="px-3 py-1 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-sm text-neutral-900 dark:text-neutral-100 transition-all duration-200" 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="px-3 py-1 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-sm text-neutral-900 dark:text-neutral-100 transition-all duration-200" id="employeeFilter">
<option value="">All Employees</option>
</select>
<button class="px-3 py-1 bg-gray-200 dark:bg-neutral-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-300 dark:hover:bg-neutral-600 transition-colors" id="refreshRecentBtn">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-neutral-100 dark:bg-neutral-700">
<tr>
<th class="px-4 py-2">Date</th>
<th class="px-4 py-2">File</th>
<th class="px-4 py-2">Client</th>
<th class="px-4 py-2">Employee</th>
<th class="px-4 py-2">Hours</th>
<th class="px-4 py-2">Rate</th>
<th class="px-4 py-2">Amount</th>
<th class="px-4 py-2">Description</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody id="recentEntriesTableBody">
<!-- Recent entries will be loaded here -->
</tbody>
</table>
</div>
</div>
<!-- Action Cards Row -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md">
<div class="p-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold"><i class="fa-solid fa-circle-plus mr-2"></i>Quick Actions</h2>
</div>
<div class="p-4 space-y-3">
<button class="w-full px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors" id="addTimeEntryBtn">
<i class="fa-regular fa-clock mr-2"></i>Add Time Entry
</button>
<button class="w-full px-4 py-2 bg-success-600 text-white rounded-lg hover:bg-success-700 transition-colors" id="recordPaymentBtn">
<i class="fa-solid fa-sack-dollar mr-2"></i>Record Payment
</button>
<button class="w-full px-4 py-2 bg-warning-600 text-white rounded-lg hover:bg-warning-700 transition-colors" id="addExpenseBtn">
<i class="fa-solid fa-receipt mr-2"></i>Add Expense
</button>
<button class="w-full px-4 py-2 bg-info-600 text-white rounded-lg hover:bg-info-700 transition-colors" id="viewLedgerBtn">
<i class="fa-regular fa-file-lines mr-2"></i>View File Ledger
</button>
</div>
</div>
</div>
<div class="lg:col-span-2">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md">
<div class="p-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold"><i class="fa-solid fa-chart-column mr-2"></i>Top Files by Balance</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-neutral-100 dark:bg-neutral-700">
<tr>
<th class="px-4 py-2">File</th>
<th class="px-4 py-2">Total Charges</th>
<th class="px-4 py-2">Amount Owing</th>
</tr>
</thead>
<tbody id="topFilesTableBody">
<!-- Top files will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Time Entry Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="quickTimeModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-lg w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Quick Time Entry</h2>
<button onclick="closeModal('quickTimeModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4">
<form id="quickTimeForm" class="space-y-4">
<div>
<label for="quickTimeFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="relative">
<input type="text" id="quickTimeFile" name="file_no" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<button type="button" id="selectFileBtn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-primary-500">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<p class="text-sm text-neutral-500 mt-1" id="selectedFileInfo">Enter file number or browse to select</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="quickTimeHours" class="block text-sm font-medium mb-1">Hours *</label>
<input type="number" id="quickTimeHours" name="hours" step="0.25" min="0" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div>
<label for="quickTimeDate" class="block text-sm font-medium mb-1">Date *</label>
<input type="date" id="quickTimeDate" name="entry_date" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
</div>
<div>
<label for="quickTimeDescription" class="block text-sm font-medium mb-1">Description *</label>
<textarea id="quickTimeDescription" name="description" rows="3" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Describe the work performed..."></textarea>
</div>
</form>
</div>
<div class="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('quickTimeModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Cancel</button>
<button id="saveQuickTimeBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors">
<i class="fa-regular fa-circle-check mr-2"></i>Save Time Entry
</button>
</div>
</div>
</div>
<!-- Time Entry Modal (Full) -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="timeEntryModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Time Entry</h2>
<button onclick="closeModal('timeEntryModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4">
<form id="timeEntryForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="timeEntryFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="relative">
<input type="text" id="timeEntryFile" name="file_no" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<button type="button" id="selectTimeFileBtn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-primary-500">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
</div>
<div>
<label for="timeEntryEmployee" class="block text-sm font-medium mb-1">Employee</label>
<select id="timeEntryEmployee" name="empl_num" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<option value="">Use file default</option>
</select>
</div>
<div class="col-span-2 sm:col-span-1">
<label for="timeEntryDate" class="block text-sm font-medium mb-1">Date *</label>
<input type="date" id="timeEntryDate" name="date" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div class="col-span-2 sm:col-span-1">
<label for="timeEntryHours" class="block text-sm font-medium mb-1">Hours *</label>
<input type="number" id="timeEntryHours" name="quantity" step="0.25" min="0" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div class="col-span-2 sm:col-span-1">
<label for="timeEntryRate" class="block text-sm font-medium mb-1">Rate</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-neutral-500 dark:text-neutral-400">$</span>
</span>
<input type="number" id="timeEntryRate" name="rate" step="0.01" min="0" readonly class="w-full pl-7 pr-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
</div>
<div class="col-span-2 sm:col-span-1">
<label for="timeEntryCode" class="block text-sm font-medium mb-1">Transaction Code</label>
<select id="timeEntryCode" name="t_code" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<option value="TIME">TIME - Time Entry</option>
</select>
</div>
<div class="col-span-2 sm:col-span-1">
<label for="timeEntryAmount" class="block text-sm font-medium mb-1">Amount</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-neutral-500 dark:text-neutral-400">$</span>
</span>
<input type="text" id="timeEntryAmount" name="amount" readonly class="w-full pl-7 pr-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
</div>
<div class="col-span-2">
<label for="timeEntryNote" class="block text-sm font-medium mb-1">Description *</label>
<textarea id="timeEntryNote" name="note" rows="3" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Describe the work performed in detail..."></textarea>
</div>
</div>
</form>
</div>
<div class="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('timeEntryModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Cancel</button>
<button id="saveTimeEntryBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors">
<i class="fa-regular fa-circle-check mr-2"></i>Save Entry
</button>
</div>
</div>
</div>
<!-- Payment Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="paymentModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Record Payment</h2>
<button onclick="closeModal('paymentModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4">
<form id="paymentForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="paymentFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="relative">
<input type="text" id="paymentFile" name="file_no" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<button type="button" id="selectPaymentFileBtn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-primary-500">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
</div>
<div>
<label for="paymentAmount" class="block text-sm font-medium mb-1">Payment Amount *</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-neutral-500 dark:text-neutral-400">$</span>
</span>
<input type="number" id="paymentAmount" name="amount" step="0.01" min="0" required class="w-full pl-7 pr-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
</div>
<div class="col-span-2 sm:col-span-1">
<label for="paymentDate" class="block text-sm font-medium mb-1">Payment Date *</label>
<input type="date" id="paymentDate" name="payment_date" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div class="col-span-2 sm:col-span-1">
<label for="paymentMethod" class="block text-sm font-medium mb-1">Payment Method</label>
<select id="paymentMethod" name="payment_method" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<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-span-2 sm:col-span-1">
<label for="paymentReference" class="block text-sm font-medium mb-1">Reference/Check #</label>
<input type="text" id="paymentReference" name="reference" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Check number or reference">
</div>
<div class="col-span-2 sm:col-span-1">
<div class="flex items-center mt-2">
<input type="checkbox" id="applyToTrust" name="apply_to_trust" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-neutral-300 rounded">
<label for="applyToTrust" class="ml-2 text-sm text-neutral-700 dark:text-neutral-300">
Apply to Trust Account
</label>
</div>
</div>
<div class="col-span-2">
<label for="paymentNotes" class="block text-sm font-medium mb-1">Notes</label>
<textarea id="paymentNotes" name="notes" rows="2" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Additional payment notes..."></textarea>
</div>
</div>
</form>
</div>
<div class="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('paymentModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Cancel</button>
<button id="savePaymentBtn" class="px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors">
<i class="fa-solid fa-sack-dollar mr-2"></i>Record Payment
</button>
</div>
</div>
</div>
<!-- Expense Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="expenseModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Record Expense</h2>
<button onclick="closeModal('expenseModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4">
<form id="expenseForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="expenseFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="relative">
<input type="text" id="expenseFile" name="file_no" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<button type="button" id="selectExpenseFileBtn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-primary-500">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
</div>
<div>
<label for="expenseAmount" class="block text-sm font-medium mb-1">Amount *</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-neutral-500 dark:text-neutral-400">$</span>
</span>
<input type="number" id="expenseAmount" name="amount" step="0.01" min="0" required class="w-full pl-7 pr-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
</div>
<div class="col-span-2 sm:col-span-1">
<label for="expenseDate" class="block text-sm font-medium mb-1">Date *</label>
<input type="date" id="expenseDate" name="expense_date" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div class="col-span-2 sm:col-span-1">
<label for="expenseCategory" class="block text-sm font-medium mb-1">Category</label>
<select id="expenseCategory" name="category" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<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-span-2 sm:col-span-1">
<label for="expenseEmployee" class="block text-sm font-medium mb-1">Employee</label>
<select id="expenseEmployee" name="employee" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<option value="">Use file default</option>
</select>
</div>
<div class="col-span-2 sm:col-span-1">
<div class="flex items-center mt-2">
<input type="checkbox" id="expenseReceipts" name="receipts" class="h-4 w-4 text-warning-600 focus:ring-warning-500 border-neutral-300 rounded">
<label for="expenseReceipts" class="ml-2 text-sm text-neutral-700 dark:text-neutral-300">
Receipts on File
</label>
</div>
</div>
<div class="col-span-2">
<label for="expenseDescription" class="block text-sm font-medium mb-1">Description *</label>
<textarea id="expenseDescription" name="description" rows="3" required class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Describe the expense..."></textarea>
</div>
</div>
</form>
</div>
<div class="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('expenseModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Cancel</button>
<button id="saveExpenseBtn" class="px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg transition-colors">
<i class="fa-solid fa-receipt mr-2"></i>Record Expense
</button>
</div>
</div>
</div>
<!-- Unbilled Items Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="unbilledModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Unbilled Items</h2>
<button onclick="closeModal('unbilledModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4 mb-4">
<div class="flex-1">
<div class="flex gap-2">
<input type="text" class="px-3 py-1 border border-neutral-300 dark:border-neutral-600 rounded-lg text-sm" id="unbilledFileFilter" placeholder="Filter by file...">
<select class="px-3 py-1 border border-neutral-300 dark:border-neutral-600 rounded-lg text-sm" id="unbilledEmployeeFilter">
<option value="">All employees</option>
</select>
<button class="px-3 py-1 bg-gray-200 dark:bg-neutral-700 rounded-lg hover:bg-gray-300 dark:hover:bg-neutral-600 transition-colors" id="refreshUnbilledBtn">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
</div>
<div class="text-right">
<div class="bg-warning-100 dark:bg-warning-900/30 p-2 rounded-lg">
<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="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('unbilledModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Close</button>
<button id="billSelectedBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors" disabled>
<i class="fa-regular fa-circle-check mr-2"></i>Mark Selected as Billed
</button>
</div>
</div>
</div>
<!-- Financial Dashboard Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="financialDashboardModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h2 class="text-lg font-semibold">Financial Dashboard</h2>
<button onclick="closeModal('financialDashboardModal')" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="px-6 py-4" id="financialDashboardContent">
<!-- Dashboard content will be loaded here -->
</div>
<div class="flex justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button onclick="closeModal('financialDashboardModal')" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors">Close</button>
</div>
</div>
</div>
<script>
// Financial management functionality
let dashboardData = null;
let recentEntries = [];
let unbilledData = null;
// Authorization and JSON headers are injected by window.http.wrappedFetch
// 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 window.http.wrappedFetch('/api/financial/financial-dashboard');
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-right">${formatCurrency(file.total_charges)}</td>
<td class="text-right text-green-600"><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 window.http.wrappedFetch(`/api/financial/time-entries/recent?${params}`);
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-neutral-500">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-right">${formatCurrency(entry.rate)}</td>
<td class="text-right text-green-600"><strong>${formatCurrency(entry.amount)}</strong></td>
<td class="small">${entry.description ? entry.description.substring(0, 50) + (entry.description.length > 50 ? '...' : '') : ''}</td>
<td>
<span class="inline-block px-2 py-0.5 text-xs rounded ${entry.billed ? 'bg-green-100 text-green-700 border border-green-400' : 'bg-yellow-100 text-yellow-700 border border-yellow-500'}">
${entry.billed ? 'Billed' : 'Unbilled'}
</span>
</td>
<td>
<button class="px-2 py-1 border border-blue-600 text-blue-600 rounded hover:bg-blue-100" onclick="editTimeEntry(${entry.id})">
<i class="fa-solid fa-pencil"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
async function loadEmployeeOptions() {
try {
const response = await window.http.wrappedFetch('/api/files/lookups/employees');
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() {
document.getElementById('quickTimeModal').classList.remove('hidden');
}
function showTimeEntryModal() {
document.getElementById('timeEntryModal').classList.remove('hidden');
}
function showPaymentModal() {
document.getElementById('paymentModal').classList.remove('hidden');
}
function showExpenseModal() {
document.getElementById('expenseModal').classList.remove('hidden');
}
async function saveQuickTime() {
const form = document.getElementById('quickTimeForm');
const formData = new FormData(form);
if (!form.checkValidity()) {
form.reportValidity();
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 window.http.wrappedFetch('/api/financial/time-entry/quick', {
method: 'POST',
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();
closeModal('quickTimeModal');
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.reportValidity();
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 window.http.wrappedFetch('/api/financial/payments/', {
method: 'POST',
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();
closeModal('paymentModal');
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.reportValidity();
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 window.http.wrappedFetch('/api/financial/expenses/', {
method: 'POST',
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();
closeModal('expenseModal');
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 window.http.wrappedFetch('/api/financial/unbilled-entries');
if (!response.ok) throw new Error('Failed to load unbilled entries');
unbilledData = await response.json();
displayUnbilledItems(unbilledData);
openModal('unbilledModal');
} 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-neutral-500 p-4">No unbilled entries found</div>';
return;
}
data.files.forEach(file => {
const fileCard = document.createElement('div');
fileCard.className = 'bg-white dark:bg-neutral-800 rounded-lg shadow-md mb-3 border border-neutral-200 dark:border-neutral-700';
fileCard.innerHTML = `
<div class="px-4 py-3 flex justify-between items-center border-b border-neutral-200 dark:border-neutral-700">
<div>
<strong>${file.file_no}</strong> - ${file.client_name}
<br><small class="text-neutral-500">${file.matter}</small>
</div>
<div class="text-right">
<div><strong>Total: ${formatCurrency(file.total_amount)}</strong></div>
<div><small>${file.total_hours.toFixed(2)} hours</small></div>
</div>
</div>
<div class="px-4 py-3">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th>
<input type="checkbox" class="h-4 w-4 text-primary-600 border-neutral-300 rounded 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="h-4 w-4 text-primary-600 border-neutral-300 rounded 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-right">${formatCurrency(entry.rate)}</td>
<td class="text-right text-green-600">${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 window.http.wrappedFetch('/api/financial/bill-entries', {
method: 'POST',
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);
openModal('financialDashboardModal');
}
function displayDashboardModal(data) {
const content = document.getElementById('financialDashboardContent');
content.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h6 class="font-semibold mb-2">Financial Summary</h6>
<ul class="space-y-1">
<li class="flex justify-between"><strong>Total Charges:</strong><span>${formatCurrency(data.summary.total_charges)}</span></li>
<li class="flex justify-between"><strong>Amount Owing:</strong><span>${formatCurrency(data.summary.total_owing)}</span></li>
<li class="flex justify-between"><strong>Trust Balance:</strong><span>${formatCurrency(data.summary.total_trust)}</span></li>
<li class="flex justify-between"><strong>Unbilled Amount:</strong><span>${formatCurrency(data.summary.unbilled_amount)}</span></li>
<li class="flex justify-between"><strong>Total Hours:</strong><span>${data.summary.total_hours.toFixed(1)}</span></li>
</ul>
<h6 class="font-semibold mt-4 mb-2">Recent Activity (${data.recent_activity.period_days} days)</h6>
<ul class="space-y-1">
<li class="flex justify-between"><strong>Entries:</strong><span>${data.recent_activity.entries_count}</span></li>
<li class="flex justify-between"><strong>Amount:</strong><span>${formatCurrency(data.recent_activity.total_amount)}</span></li>
</ul>
</div>
<div>
<h6 class="font-semibold mb-2">Employee Activity (30 days)</h6>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead class="bg-neutral-100 dark:bg-neutral-700">
<tr>
<th class="px-3 py-2">Employee</th>
<th class="px-3 py-2">Hours</th>
<th class="px-3 py-2">Amount</th>
<th class="px-3 py-2">Entries</th>
</tr>
</thead>
<tbody>
${data.employee_stats.map(stat => `
<tr>
<td class="px-3 py-2">${stat.employee}</td>
<td class="px-3 py-2 text-right">${stat.hours.toFixed(1)}</td>
<td class="px-3 py-2 text-right">${formatCurrency(stat.amount)}</td>
<td class="px-3 py-2 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 window.http.wrappedFetch(`/api/files/${fileNo}`);
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') {
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
alert(String(message));
}
}
// Update JavaScript if needed for new classes, e.g., for toggling dropdown
function toggleReportsDropdown() {
document.getElementById('reportsDropdown').classList.toggle('hidden');
}
// Add closeModal function
function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
</script>
{% endblock %}