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

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