citation-app-page
Science Score: 44.0%
This score indicates how likely this project is to be science-related based on various indicators:
-
✓CITATION.cff file
Found CITATION.cff file -
✓codemeta.json file
Found codemeta.json file -
✓.zenodo.json file
Found .zenodo.json file -
○DOI references
-
○Academic publication links
-
○Academic email domains
-
○Institutional organization owner
-
○JOSS paper metadata
-
○Scientific vocabulary similarity
Low similarity (0.6%) to scientific vocabulary
Last synced: 6 months ago
·
JSON representation
·
Repository
Basic Info
- Host: GitHub
- Owner: bober12345678901
- Language: HTML
- Default Branch: main
- Size: 609 KB
Statistics
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
- Releases: 1
Created 9 months ago
· Last pushed 7 months ago
Metadata Files
Readme
Citation
README.md
citation-app-page
simple citation/arrest/warrant fill out site
Owner
- Name: bober12345678901
- Login: bober12345678901
- Kind: organization
- Repositories: 1
- Profile: https://github.com/bober12345678901
Citation (citation.html)
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" href="favicon.png" type="image/png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Citation Form</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="styles.css">
<script src="theme.js"></script>
<style>
/* Custom styles for this page, overriding or extending theme/tailwind */
/* Autocomplete suggestions dropdown styles */
.autocomplete-suggestions {
border: 1px solid var(--border-color);
background: var(--box-bg-color);
max-height: 150px;
overflow-y: auto;
position: absolute;
z-index: 100;
width: calc(100% - 22px); /* Adjust based on input padding */
left: 11px; /* Align with input field */
border-radius: 0.375rem; /* rounded-md */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.autocomplete-suggestions div {
padding: 8px 10px;
cursor: pointer;
color: var(--text-color);
}
.autocomplete-suggestions div:hover {
background-color: var(--button-secondary-hover-bg);
color: var(--button-text-color);
}
/* Style for dynamically added penal code groups */
.penal-code-group {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 1.5rem;
margin-bottom: 1rem;
align-items: end;
}
.penal-code-group .input-wrapper {
position: relative;
grid-column: span 1;
}
.penal-code-group .amount-due-wrapper {
grid-column: span 1;
}
.penal-code-group .button-remove {
grid-column: span 1;
justify-self: end;
width: auto;
min-width: 80px;
}
@media (max-width: 767px) {
.penal-code-group {
grid-template-columns: 1fr;
}
.penal-code-group .input-wrapper,
.penal-code-group .amount-due-wrapper {
grid-column: span 1;
}
.penal-code-group .button-remove {
grid-column: span 1;
width: 100%;
justify-self: stretch;
}
}
.text-right {
text-align: right;
}
/* Styles for radio button container */
.radio-container {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 1rem;
}
.radio-container input[type="radio"] {
width: 18px;
height: 18px;
accent-color: var(--accent-color);
}
.radio-container label {
margin-bottom: 0; /* Override block mb-2 */
}
/* Styles for the penal code list modal */
.penal-code-list-modal-content {
max-height: 400px; /* Limit height for scrollability */
overflow-y: auto;
padding: 1rem;
background-color: var(--input-bg-color); /* Match theme input background */
border-radius: 0.375rem;
border: 1px solid var(--border-color);
}
.penal-code-list-modal-content div {
padding: 0.5rem 0;
border-bottom: 1px dashed var(--border-color);
color: var(--text-color); /* Set text color to theme's main text color */
}
.penal-code-list-modal-content div:last-child {
border-bottom: none;
}
.penal-code-list-modal-content strong {
color: var(--accent-color);
}
</style>
</head>
<body class="min-h-screen theme-app-container flex items-center justify-center p-4">
<div class="container mx-auto max-w-4xl w-full theme-lookup-box p-8 rounded-xl shadow-lg transition-colors duration-300 border border-solid">
<h2 class="theme-title text-3xl font-bold text-center mb-6">Citation Form</h2>
<form id="citationForm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 mb-6">
<div>
<label for="officerBadge" class="theme-label block mb-2 font-medium">Officer Badge Number:</label>
<input type="text" id="officerBadge" name="officerBadge" required class="theme-input w-full p-2 border rounded-md">
</div>
<div>
<label for="officerName" class="theme-label block mb-2 font-medium">Officer Username(s):</label>
<input type="text" id="officerName" name="officerName" required class="theme-input w-full p-2 border rounded-md" placeholder="Full Roblox Username">
</div>
<div>
<label for="officerRank" class="theme-label block mb-2 font-medium">Officer Rank & Signature:</label>
<input type="text" id="officerRank" name="officerRank" required class="theme-input w-full p-2 border rounded-md" placeholder="Rank + Your RP name">
</div>
<div>
<label for="violatorName" class="theme-label block mb-2 font-medium">Violator's USER ID:</label>
<input type="text" id="violatorName" name="violatorName" required class="theme-input w-full p-2 border rounded-md">
</div>
<div>
<label for="officerDepartmentDisplay" class="theme-label block mb-2 font-medium">Department:</label>
<input type="text" id="officerDepartmentDisplay" name="officerDepartmentDisplay" class="theme-input w-full p-2 border rounded-md" readonly>
</div>
<div>
<label for="violationType" class="theme-label block mb-2 font-medium">Type of Violation:</label>
<input type="text" id="violationType" name="violationType" required class="theme-input w-full p-2 border rounded-md" placeholder="Auto-filled from Penal Code(s)" readonly>
</div>
</div>
<div class="mb-6 p-4 rounded-lg theme-section-bg"> <!-- Removed border theme-border-color -->
<h3 class="theme-title font-bold text-lg mb-4">Penal Code(s) & Amount Due:</h3>
<div id="penalCodeInputsContainer">
<div class="penal-code-group" data-group-id="0">
<div class="input-wrapper">
<label for="penalCode_0" class="theme-label block mb-2 font-medium">Penal Code:</label>
<input type="text" id="penalCode_0" name="penalCode_0" class="theme-input w-full p-2 border rounded-md penal-code-input" autocomplete="off">
<div id="penalCodeSuggestions_0" class="autocomplete-suggestions"></div>
</div>
<div class="amount-due-wrapper">
<label for="amountDue_0" class="theme-label block mb-2 font-medium">Amount Due:</label>
<input type="number" id="amountDue_0" name="amountDue_0" step="0.01" min="0" value="0.00" class="theme-input w-full p-2 border rounded-md amount-due-input" readonly>
</div>
</div>
</div>
<div class="flex flex-wrap gap-4 mt-4 justify-center">
<button type="button" id="addPenalCodeBtn" class="theme-button-secondary py-2 px-6 rounded-md font-semibold">Add Another Penal Code</button>
<button type="button" id="viewAllPenalCodesBtn" class="theme-button-secondary py-2 px-6 rounded-md font-semibold">View All Penal Codes</button>
</div>
</div>
<div class="mb-6 text-right">
<label for="totalAmountDueSum" class="theme-title block mb-2 font-bold text-xl">Total Amount Due (All Codes):</label>
<input type="text" id="totalAmountDueSum" name="totalAmountDueSum" class="theme-input p-2 border rounded-md text-right font-bold text-xl" value="$0.00" readonly>
</div>
<div class="mb-6">
<label for="violationDetails" class="theme-label block mb-2 font-medium">Additional Notes:</label>
<textarea id="violationDetails" name="violationDetails" placeholder="Provide details of the violation" class="theme-input w-full p-2 border rounded-md min-h-[80px]"></textarea>
</div>
<div class="mb-6 p-4 rounded-lg theme-section-bg"> <!-- Removed border theme-border-color -->
<label class="theme-title block mb-2 font-bold text-lg">Signed?</label>
<div class="radio-container">
<input type="radio" id="signedYes" name="signedStatus" value="YES" class="theme-input" checked> <!-- Default to YES -->
<label for="signedYes" class="theme-label">YES</label>
<input type="radio" id="signedNo" name="signedStatus" value="NO" class="theme-input">
<label for="signedNo" class="theme-label">NO</label>
</div>
<div id="notSignedReasonContainer" class="mt-4 hidden">
<label for="notSignedReason" class="theme-label block mb-2 font-medium">Signature Override</label>
<textarea id="notSignedReason" name="notSignedReason" placeholder="Ex. User Didn't Sign" class="theme-input w-full p-2 border rounded-md min-h-[60px]"></textarea>
</div>
</div>
<div class="flex flex-wrap justify-center gap-4">
<button type="submit" class="theme-button-primary py-2 px-6 rounded-md font-semibold">Submit Citation</button>
<button type="reset" class="theme-button-secondary py-2 px-6 rounded-md font-semibold" id="clearFormBtn">Clear Form</button>
<button type="button" onclick="window.location.href='selection.html'" class="theme-button-secondary py-2 px-6 rounded-md font-semibold">Back to Selection</button>
</div>
</form>
</div>
<!-- Penal Code List Modal -->
<div id="penalCodeListModalOverlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div id="penalCodeListModal" class="theme-lookup-box p-6 rounded-lg shadow-xl max-w-lg w-full mx-4 sm:mx-auto">
<h3 id="penalCodeListModalTitle" class="theme-title text-xl font-bold mb-4">Available Penal Codes</h3>
<div id="penalCodeListModalContent" class="penal-code-list-modal-content mb-6">
<!-- Penal codes will be loaded here -->
</div>
<div class="flex justify-end">
<button id="penalCodeListModalCloseBtn" class="theme-button-primary py-2 px-4 rounded-md font-semibold">Close</button>
</div>
</div>
</div>
<!-- Custom Modal for Alerts (from theme.js) -->
<div id="customModalOverlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div id="customModal" class="theme-lookup-box p-6 rounded-lg shadow-xl max-w-sm w-full mx-4 sm:mx-auto">
<h3 id="customModalTitle" class="theme-title text-xl font-bold mb-4"></h3>
<p id="customModalMessage" class="theme-label mb-6"></p>
<div id="customModalButtons" class="flex justify-end space-x-3">
<button id="customModalCancelBtn" class="theme-button-secondary py-2 px-4 rounded-md font-semibold hidden">Cancel</button>
<button id="customModalOkBtn" class="theme-button-primary py-2 px-4 rounded-md font-semibold">OK</button>
</div>
</div>
</div>
<script>
let penalCodesData = [];
let allFilteredPenalCodesForCitationForm = [];
let groupCounter = 0;
// --- UTILITY FUNCTIONS (from theme.js, but included here for clarity if theme.js is missing or overridden) ---
// These are fallback definitions. In your actual setup, theme.js should provide these.
if (typeof window.showCustomAlert === 'undefined') {
window.showCustomAlert = (message, type = 'info', title = null) => {
return new Promise(resolve => {
const modalOverlay = document.getElementById('customModalOverlay');
const modalTitle = document.getElementById('customModalTitle');
const modalMessage = document.getElementById('customModalMessage');
const modalOkBtn = document.getElementById('customModalOkBtn');
const modalCancelBtn = document.getElementById('customModalCancelBtn');
modalTitle.textContent = title || (type === 'success' ? 'Success!' : type === 'error' ? 'Error!' : type === 'warning' ? 'Warning!' : 'Information');
modalMessage.textContent = message;
modalTitle.classList.remove('success', 'error', 'warning', 'info');
modalTitle.classList.add(type.replace(/\s+/g, '-'));
modalCancelBtn.classList.add('hidden');
modalOkBtn.classList.remove('hidden');
modalOkBtn.textContent = 'OK';
modalOverlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
modalOkBtn.focus();
modalOkBtn.onclick = () => {
modalOverlay.classList.add('hidden');
document.body.style.overflow = '';
modalOkBtn.onclick = null;
resolve();
};
modalCancelBtn.onclick = null;
});
};
}
if (typeof window.showCustomConfirm === 'undefined') {
window.showCustomConfirm = (message, title = "Confirm Action") => {
return new Promise((resolve) => {
const modalOverlay = document.getElementById('customModalOverlay');
const modalTitle = document.getElementById('customModalTitle');
const modalMessage = document.getElementById('customModalMessage');
const modalOkBtn = document.getElementById('customModalOkBtn');
const modalCancelBtn = document.getElementById('customModalCancelBtn');
modalTitle.textContent = title;
modalMessage.textContent = message;
modalTitle.classList.remove('success', 'error', 'warning', 'info');
modalTitle.classList.add('info');
modalCancelBtn.classList.remove('hidden');
modalOkBtn.classList.remove('hidden');
modalOkBtn.textContent = 'Yes'; // Set to Yes
modalCancelBtn.textContent = 'No'; // Set to No
modalOverlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
modalOkBtn.focus();
const okHandler = () => {
modalOverlay.classList.add('hidden');
document.body.style.overflow = '';
modalOkBtn.removeEventListener('click', okHandler);
modalCancelBtn.removeEventListener('click', cancelHandler);
resolve(true);
};
const cancelHandler = () => {
modalOverlay.classList.add('hidden');
document.body.style.overflow = '';
modalOkBtn.removeEventListener('click', okHandler);
modalCancelBtn.removeEventListener('click', cancelHandler);
resolve(false);
};
modalOkBtn.addEventListener('click', okHandler);
modalCancelBtn.addEventListener('click', cancelHandler);
});
};
}
// Function to handle redirection to login page and clear session storage
function redirectToLogin(message = "Your session has expired. Please log in again.") {
console.warn("Redirecting to login:", message);
window.showCustomAlert(message, "Session Expired").then(() => {
sessionStorage.clear();
window.location.href = 'index.html';
});
}
// --- JWT Decoding (Client-side, for expiration check) ---
function decodeJwt(token) {
if (!token) return null;
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
return JSON.parse(jsonPayload);
} catch (e) {
console.error("Error decoding JWT:", e);
return null;
}
}
// --- Token Refresh Mechanism (Calls your Worker's refresh endpoint) ---
async function refreshAuthToken() {
console.log("Attempting to refresh token...");
try {
const response = await fetch('https://citation-app-worker.pnbober.workers.dev/api/auth/refresh-token', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
sessionStorage.setItem('authToken', data.accessToken);
console.log("Token refreshed successfully.");
return true;
} else {
console.error("Refresh token failed:", response.status, await response.text());
return false;
}
} catch (error) {
console.error("Network error during token refresh:", error);
return false;
}
}
// --- Authenticated Fetch Wrapper ---
async function fetchWithAuth(url, options = {}) {
let token = sessionStorage.getItem('authToken');
const decodedToken = decodeJwt(token);
const fiveMinutesFromNow = Math.floor(Date.now() / 1000) + (5 * 60);
if (!token || !decodedToken || decodedToken.exp < fiveMinutesFromNow) {
console.log("Token is missing, expired, or near expiration. Attempting refresh.");
const refreshed = await refreshAuthToken();
if (refreshed) {
token = sessionStorage.getItem('authToken');
} else {
redirectToLogin("Your session has expired. Please log in again.");
return null;
}
}
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
try {
const response = await fetch(url, options);
if (response.status === 401 || response.status === 403) {
console.warn("Received 401/403 from API. Attempting to refresh token once.");
const refreshed = await refreshAuthToken();
if (refreshed) {
console.log("Token refreshed, retrying original request.");
options.headers['Authorization'] = `Bearer ${sessionStorage.getItem('authToken')}`;
const retryResponse = await fetch(url, options);
if (retryResponse.status === 401 || retryResponse.status === 403) {
redirectToLogin("Your session has expired. Please log in again.");
return null;
}
return retryResponse;
} else {
redirectToLogin("Your session has expired. Please log in again.");
return null;
}
}
return response;
} catch (error) {
console.error("Network error during authenticated fetch:", error);
window.showCustomAlert('A network error occurred. Please check your internet connection.', 'Network Error');
return null;
}
}
document.addEventListener('DOMContentLoaded', async function() {
const token = sessionStorage.getItem('authToken');
if (!token) {
redirectToLogin("You must be logged in to access this page.");
return;
}
const penalCodeInputsContainer = document.getElementById('penalCodeInputsContainer');
const addPenalCodeBtn = document.getElementById('addPenalCodeBtn');
const totalAmountDueSumInput = document.getElementById('totalAmountDueSum');
const citationForm = document.getElementById('citationForm');
const clearFormBtn = document.getElementById('clearFormBtn');
const mainViolationTypeInput = document.getElementById('violationType');
const officerBadgeInput = document.getElementById('officerBadge');
const officerNameInput = document.getElementById('officerName');
const officerDepartmentDisplayInput = document.getElementById('officerDepartmentDisplay');
const signedYesRadio = document.getElementById('signedYes');
const signedNoRadio = document.getElementById('signedNo');
const notSignedReasonContainer = document.getElementById('notSignedReasonContainer');
const notSignedReasonInput = document.getElementById('notSignedReason');
const viewAllPenalCodesBtn = document.getElementById('viewAllPenalCodesBtn');
const penalCodeListModalOverlay = document.getElementById('penalCodeListModalOverlay');
const penalCodeListModalContent = document.getElementById('penalCodeListModalContent');
const penalCodeListModalCloseBtn = document.getElementById('penalCodeListModalCloseBtn');
const loggedInUserString = sessionStorage.getItem('loggedInUser');
let loggedInUser = null;
let userDepartment = 'N/A';
if (loggedInUserString) {
loggedInUser = JSON.parse(loggedInUserString);
if (loggedInUser.badgeNumber) {
officerBadgeInput.value = loggedInUser.badgeNumber;
}
if (loggedInUser.username) {
officerNameInput.value = loggedInUser.username;
}
if (loggedInUser.department) {
userDepartment = loggedInUser.department;
officerDepartmentDisplayInput.value = userDepartment;
} else {
officerDepartmentDisplayInput.value = 'N/A';
}
if (loggedInUser.role === 'admin') {
officerDepartmentDisplayInput.removeAttribute('readonly');
}
console.log("Logged-in Officer Department:", userDepartment);
}
// --- Fetch Penal Codes on Load ---
try {
const response = await fetchWithAuth('https://citation-app-worker.pnbober.workers.dev/penal-codes');
if (response && response.ok) {
let allPenalCodes = await response.json();
console.log("All penal codes loaded:", allPenalCodes);
penalCodesData = allPenalCodes.filter(pc => {
if (pc.description && pc.description.toLowerCase() === 'reckless driving') {
return true;
}
if (pc.type === 'Traffic Violation' || pc.type === 'Citation') {
return true;
}
if ((pc.type === 'Misdemeanor' || pc.type === 'Felony') && (!pc.jail_time_seconds || pc.jail_time_seconds === 0)) {
return true;
}
return false;
}).sort((a, b) => {
const codeA = a.code;
const codeB = b.code;
const numA = parseFloat(codeA);
const numB = parseFloat(codeB);
if (!isNaN(numA) && !isNaN(numB)) {
return numA - numB;
} else if (!isNaN(numA)) {
return -1;
} else if (!isNaN(numB)) {
return 1;
} else {
return codeA.localeCompare(b.code);
}
});
allFilteredPenalCodesForCitationForm = penalCodesData;
console.log("Filtered and sorted penal codes for Citation Form:", penalCodesData);
} else if (response) {
const errorData = await response.json();
console.error('Failed to load penal codes:', errorData.message);
window.showCustomAlert('Warning: Could not load penal codes for auto-fill. ' + (errorData.message || response.statusText), 'Warning');
}
} catch (error) {
console.error('Network error loading penal codes:', error);
window.showCustomAlert('Network error: Could not load penal codes for auto-fill.', 'Error');
}
// --- Function to Add a New Penal Code Input Group ---
function addInputGroup(initialCode = '', initialAmount = '0.00') {
groupCounter++;
const newGroupId = groupCounter;
const newGroup = document.createElement('div');
newGroup.classList.add('penal-code-group', 'grid', 'gap-x-6', 'gap-y-4', 'mb-4', 'items-end');
newGroup.style.gridTemplateColumns = '1fr 1fr auto';
newGroup.dataset.groupId = newGroupId;
newGroup.innerHTML = `
<div class="input-wrapper">
<label for="penalCode_${newGroupId}" class="theme-label block mb-2 font-medium">Penal Code:</label>
<input type="text" id="penalCode_${newGroupId}" name="penalCode_${newGroupId}" class="theme-input w-full p-2 border rounded-md penal-code-input" autocomplete="off">
<div id="penalCodeSuggestions_${newGroupId}" class="autocomplete-suggestions"></div>
</div>
<div class="amount-due-wrapper">
<label for="amountDue_${newGroupId}" class="theme-label block mb-2 font-medium">Amount Due:</label>
<input type="number" id="amountDue_${newGroupId}" name="amountDue_${newGroupId}" step="0.01" min="0" value="0.00" class="theme-input w-full p-2 border rounded-md amount-due-input" readonly>
</div>
<button type="button" class="theme-button-danger py-2 px-6 rounded-md font-semibold button-remove">Remove</button>
`;
penalCodeInputsContainer.appendChild(newGroup);
setupAutocompleteForPenalCodeInput(newGroupId);
newGroup.querySelector('.button-remove').addEventListener('click', function() {
newGroup.remove();
calculateTotalAmountDue();
updateMainViolationType();
});
calculateTotalAmountDue();
updateMainViolationType();
}
// --- Setup Autocomplete for a specific input group ---
function setupAutocompleteForPenalCodeInput(groupId) {
const penalCodeInput = document.getElementById(`penalCode_${groupId}`);
const amountDueInput = document.getElementById(`amountDue_${groupId}`);
const suggestionsDiv = document.getElementById(`penalCodeSuggestions_${groupId}`);
penalCodeInput.addEventListener('input', function() {
const query = this.value.toLowerCase();
suggestionsDiv.innerHTML = '';
suggestionsDiv.style.display = 'none';
if (query.length > 0) {
const filteredCodes = penalCodesData.filter(pc =>
pc.code.toLowerCase().includes(query) || (pc.description && pc.description.toLowerCase().includes(query))
);
if (filteredCodes.length > 0) {
filteredCodes.forEach(pc => {
const suggestionItem = document.createElement('div');
suggestionItem.textContent = `${pc.code} - ${pc.description || 'No description'} (${pc.type || 'N/A'})`;
suggestionItem.dataset.code = pc.code;
suggestionItem.dataset.amount = pc.amount_due;
suggestionItem.dataset.type = pc.type || '';
suggestionItem.dataset.description = pc.description || '';
suggestionItem.dataset.jailTime = pc.jail_time_seconds || 0;
suggestionsDiv.appendChild(suggestionItem);
suggestionItem.addEventListener('click', function() {
penalCodeInput.value = this.dataset.code;
amountDueInput.value = parseFloat(this.dataset.amount).toFixed(2);
updateMainViolationType();
suggestionsDiv.innerHTML = '';
suggestionsDiv.style.display = 'none';
calculateTotalAmountDue();
});
});
suggestionsDiv.style.display = 'block';
}
}
const matchedCode = penalCodesData.find(pc =>
pc.code.toLowerCase() === query
);
if (!matchedCode) {
amountDueInput.value = '0.00';
} else {
amountDueInput.value = parseFloat(matchedCode.amount_due).toFixed(2);
}
calculateTotalAmountDue();
updateMainViolationType();
});
penalCodeInput.addEventListener('blur', () => {
setTimeout(() => {
suggestionsDiv.style.display = 'none';
}, 100);
const exactMatch = penalCodesData.find(pc =>
pc.code.toLowerCase() === penalCodeInput.value.toLowerCase()
);
if (exactMatch) {
amountDueInput.value = parseFloat(exactMatch.amount_due).toFixed(2);
} else {
amountDueInput.value = '0.00';
}
calculateTotalAmountDue();
updateMainViolationType();
});
penalCodeInput.addEventListener('focus', () => {
if (suggestionsDiv.innerHTML !== '' && penalCodeInput.value.length > 0) {
suggestionsDiv.style.display = 'block';
}
});
}
// --- Calculate Total Amount Due ---
function calculateTotalAmountDue() {
let total = 0;
document.querySelectorAll('.amount-due-input').forEach(input => {
const value = parseFloat(input.value);
if (!isNaN(value)) {
total += value;
}
});
if (totalAmountDueSumInput) totalAmountDueSumInput.value = `$${total.toFixed(2)}`;
}
// --- Update Main Violation Type based on all selected Penal Codes ---
function updateMainViolationType() {
const selectedDescriptions = [];
document.querySelectorAll('.penal-code-input').forEach(input => {
const code = input.value.trim();
if (code) {
const matchedCode = penalCodesData.find(pc => pc.code.toLowerCase() === code.toLowerCase());
if (matchedCode && matchedCode.description) {
selectedDescriptions.push(matchedCode.description);
}
}
});
if (mainViolationTypeInput) {
mainViolationTypeInput.value = Array.from(new Set(selectedDescriptions)).join(', ');
if (mainViolationTypeInput.value === '') {
mainViolationTypeInput.placeholder = "Auto-filled from Penal Code(s)";
} else {
mainViolationTypeInput.placeholder = "";
}
}
}
// --- NEW: Signed Status Logic ---
function toggleSignedReason() {
if (signedNoRadio.checked) {
notSignedReasonContainer.classList.remove('hidden');
notSignedReasonInput.setAttribute('required', 'required');
} else {
notSignedReasonContainer.classList.add('hidden');
notSignedReasonInput.removeAttribute('required');
notSignedReasonInput.value = '';
}
}
signedYesRadio.addEventListener('change', toggleSignedReason);
signedNoRadio.addEventListener('change', toggleSignedReason);
// Initial call to set correct state on page load
signedYesRadio.checked = true; // Set default to YES
toggleSignedReason();
// --- View All Penal Codes Modal Logic ---
if (viewAllPenalCodesBtn) {
viewAllPenalCodesBtn.addEventListener('click', () => {
penalCodeListModalContent.innerHTML = '';
if (allFilteredPenalCodesForCitationForm.length === 0) {
penalCodeListModalContent.innerHTML = '<p class="theme-label">No penal codes available or loaded.</p>';
} else {
allFilteredPenalCodesForCitationForm.forEach(pc => {
const itemDiv = document.createElement('div');
itemDiv.classList.add('theme-label-small');
// Apply theme-label class to the div to ensure text color
itemDiv.classList.add('theme-label');
itemDiv.innerHTML = `<strong>${pc.code}</strong>: ${pc.description || 'N/A'} (Amount: $${parseFloat(pc.amount_due || 0).toFixed(2)})`;
penalCodeListModalContent.appendChild(itemDiv);
});
}
penalCodeListModalOverlay.classList.remove('hidden');
});
}
if (penalCodeListModalCloseBtn) {
penalCodeListModalCloseBtn.addEventListener('click', () => {
penalCodeListModalOverlay.classList.add('hidden');
});
}
// --- Initial Setup ---
setupAutocompleteForPenalCodeInput(0);
addPenalCodeBtn.addEventListener('click', () => addInputGroup());
calculateTotalAmountDue();
updateMainViolationType();
// --- Form Clear Logic ---
clearFormBtn.addEventListener('click', function() {
window.showCustomConfirm("Are you sure you want to clear the form?", "Confirm Clear").then(confirmed => {
if (confirmed) {
document.querySelectorAll('.penal-code-group[data-group-id]:not([data-group-id="0"])').forEach(group => group.remove());
document.getElementById('penalCode_0').value = '';
document.getElementById('amountDue_0').value = '0.00';
document.getElementById('officerBadge').value = loggedInUser.badgeNumber || '';
document.getElementById('officerName').value = loggedInUser.username || '';
document.getElementById('officerRank').value = '';
document.getElementById('violatorName').value = '';
document.getElementById('violationDetails').value = '';
mainViolationTypeInput.value = '';
document.getElementById('officerDepartmentDisplay').value = userDepartment;
if (loggedInUser.role !== 'admin') {
officerDepartmentDisplayInput.setAttribute('readonly', 'readonly');
}
calculateTotalAmountDue();
updateMainViolationType();
signedYesRadio.checked = true; // Reset to YES
toggleSignedReason();
}
});
});
// --- Form Submission ---
citationForm.addEventListener('submit', async function(event) {
event.preventDefault();
const form = event.target;
const citationData = {};
citationData.officerBadge = document.getElementById('officerBadge').value;
citationData.officerName = document.getElementById('officerName').value;
citationData.officerRank = document.getElementById('officerRank').value;
citationData.violatorName = document.getElementById('violatorName').value;
citationData.violationDetails = document.getElementById('violationDetails').value;
citationData.violationType = mainViolationTypeInput.value;
citationData.department = officerDepartmentDisplayInput.value;
citationData.signedStatus = document.querySelector('input[name="signedStatus"]:checked').value;
citationData.notSignedReason = signedNoRadio.checked ? notSignedReasonInput.value : '';
const appliedPenalCodes = [];
let overallTotalAmount = 0;
document.querySelectorAll('.penal-code-group').forEach(group => {
const penalCodeInput = group.querySelector('.penal-code-input');
const amountDueInput = group.querySelector('.amount-due-input');
const code = penalCodeInput.value.trim();
const amount = parseFloat(amountDueInput.value);
if (code && !isNaN(amount)) {
const matchedCode = penalCodesData.find(pc => pc.code.toLowerCase() === code.toLowerCase());
if (matchedCode) {
appliedPenalCodes.push({
code: code,
description: matchedCode.description || 'N/A',
amount: amount,
type: matchedCode.type || 'N/A',
jail_time_seconds: matchedCode.jail_time_seconds || 0
});
overallTotalAmount += amount;
}
}
});
citationData.appliedPenalCodes = appliedPenalCodes;
citationData.totalAmountDue = overallTotalAmount;
const response = await fetchWithAuth('https://citation-app-worker.pnbober.workers.dev/submit-citation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(citationData),
});
if (response && response.ok) {
const result = await response.json();
window.showCustomAlert(result.message, 'Success').then(() => {
citationForm.reset();
document.getElementById('officerBadge').value = loggedInUser.badgeNumber || '';
document.getElementById('officerName').value = loggedInUser.username || '';
document.getElementById('officerRank').value = '';
document.getElementById('violatorName').value = '';
document.getElementById('violationDetails').value = '';
mainViolationTypeInput.value = '';
document.getElementById('officerDepartmentDisplay').value = userDepartment;
if (loggedInUser.role !== 'admin') {
officerDepartmentDisplayInput.setAttribute('readonly', 'readonly');
}
document.querySelectorAll('.penal-code-group[data-group-id]:not([data-group-id="0"])').forEach(group => group.remove());
document.getElementById('penalCode_0').value = '';
document.getElementById('amountDue_0').value = '0.00';
calculateTotalAmountDue();
updateMainViolationType();
signedYesRadio.checked = true; // Reset to YES
toggleSignedReason();
});
} else if (response) {
const errorData = await response.json();
window.showCustomAlert('Submission failed: ' + (errorData.message || response.statusText), 'Error');
}
});
});
</script></body>
</html>
GitHub Events
Total
- Issues event: 35
- Issue comment event: 12
- Public event: 1
- Push event: 181
- Create event: 1
Last Year
- Issues event: 35
- Issue comment event: 12
- Public event: 1
- Push event: 181
- Create event: 1