document.addEventListener("DOMContentLoaded", () => { // --- Configuration --- const API_BASE_URL = "http://localhost:8080/api"; // Assuming backend serves API under /api // --- State --- let authToken = sessionStorage.getItem("authToken"); // Use sessionStorage for non-persistent login // --- DOM Elements --- const loginSection = document.getElementById("login-section"); const adminSection = document.getElementById("admin-section"); const loginForm = document.getElementById("login-form"); const usernameInput = document.getElementById("username"); const passwordInput = document.getElementById("password"); const logoutButton = document.getElementById("logout-button"); const statusArea = document.getElementById("status-area"); const loggedInUserSpan = document.getElementById("logged-in-user"); // Added this if needed const createForm = document.getElementById("create-form"); const formNameInput = document.getElementById("form-name"); const loadFormsButton = document.getElementById("load-forms-button"); const formsList = document.getElementById("forms-list"); const submissionsSection = document.getElementById("submissions-section"); const submissionsList = document.getElementById("submissions-list"); const submissionsFormNameSpan = document.getElementById( "submissions-form-name" ); const publicFormIdInput = document.getElementById("public-form-id-input"); const loadPublicFormButton = document.getElementById( "load-public-form-button" ); const publicFormArea = document.getElementById("public-form-area"); const publicFormTitle = document.getElementById("public-form-title"); const publicForm = document.getElementById("public-form"); // --- Helper Functions --- function showStatus(message, isError = false) { statusArea.textContent = message; statusArea.className = "status"; // Reset classes if (message) { statusArea.classList.add(isError ? "error" : "success"); } } function toggleSections() { console.log("toggleSections called. Current authToken:", authToken); // Log 3 if (authToken) { console.log("AuthToken found, showing admin section."); // Log 4 loginSection.classList.add("hidden"); adminSection.classList.remove("hidden"); // Optionally display username if you fetch it after login // loggedInUserSpan.textContent = 'Admin'; // Placeholder } else { console.log("AuthToken not found, showing login section."); // Log 5 loginSection.classList.remove("hidden"); adminSection.classList.add("hidden"); submissionsSection.classList.add("hidden"); // Hide submissions when logged out } // Always hide public form initially on state change publicFormArea.classList.add("hidden"); publicForm.innerHTML = ''; // Reset form content } async function makeApiRequest( endpoint, method = "GET", body = null, requiresAuth = false ) { const url = `${API_BASE_URL}${endpoint}`; const headers = { "Content-Type": "application/json", Accept: "application/json", }; if (requiresAuth) { if (!authToken) { throw new Error("Authentication required, but no token found."); } headers["Authorization"] = `Bearer ${authToken}`; } const options = { method, headers, }; if (body) { options.body = JSON.stringify(body); } try { const response = await fetch(url, options); if (!response.ok) { let errorData; try { errorData = await response.json(); // Try to parse error JSON } catch (e) { // If response is not JSON errorData = { message: `HTTP Error: ${response.status} ${response.statusText}`, }; } // Check for backend's validation error structure if (errorData && errorData.validation_errors) { throw { validationErrors: errorData.validation_errors }; } // Throw a more generic error message or the one from backend if available throw new Error( errorData.message || `Request failed with status ${response.status}` ); } // Handle responses with no content (e.g., logout) if ( response.status === 204 || response.headers.get("content-length") === "0" ) { return null; // Or return an empty object/success indicator } return await response.json(); // Parse successful JSON response } catch (error) { console.error(`API Request Error (${method} ${endpoint}):`, error); // Re-throw validation errors specifically if they exist if (error.validationErrors) { throw error; } // Re-throw other errors throw new Error(error.message || "Network error or failed to fetch"); } } // --- Event Handlers --- loginForm.addEventListener("submit", async (e) => { e.preventDefault(); showStatus(""); // Clear previous status const username = usernameInput.value.trim(); const password = passwordInput.value.trim(); if (!username || !password) { showStatus("Username and password are required.", true); return; } try { const data = await makeApiRequest("/login", "POST", { username, password, }); if (data && data.token) { console.log("Login successful, received token:", data.token); // Log 1 authToken = data.token; sessionStorage.setItem("authToken", authToken); // Store token console.log("Calling toggleSections after login..."); // Log 2 toggleSections(); showStatus("Login successful!"); usernameInput.value = ""; // Clear fields passwordInput.value = ""; } else { throw new Error("Login failed: No token received."); } } catch (error) { showStatus(`Login failed: ${error.message}`, true); authToken = null; sessionStorage.removeItem("authToken"); toggleSections(); } }); logoutButton.addEventListener("click", async () => { showStatus(""); if (!authToken) return; try { await makeApiRequest("/logout", "POST", null, true); showStatus("Logout successful!"); } catch (error) { showStatus(`Logout failed: ${error.message}`, true); // Decide if you still want to clear local state even if server fails // Forcing logout locally might be better UX in case of server error } finally { // Always clear local state on logout attempt authToken = null; sessionStorage.removeItem("authToken"); toggleSections(); } }); if (createForm) { createForm.addEventListener("submit", async (e) => { e.preventDefault(); showStatus(""); const formName = formNameInput.value.trim(); if (!formName) { showStatus("Please enter a form name", true); return; } try { // Refactor to use makeApiRequest const data = await makeApiRequest( "/forms", // Endpoint relative to API_BASE_URL "POST", // TODO: Need a way to define form fields in the UI. // Sending minimal structure for now. { name: formName, fields: [] }, true // Requires authentication ); if (!data || !data.id) { throw new Error( "Failed to create form or received invalid response." ); } showStatus( `Form '${data.name}' created successfully! (ID: ${data.id})`, "success" ); formNameInput.value = ""; // Automatically refresh the forms list after creation if (loadFormsButton) { loadFormsButton.click(); } } catch (error) { showStatus(`Error creating form: ${error.message}`, true); } }); } // Ensure createFormFromUrl exists before adding listener const createFormFromUrlEl = document.getElementById("create-form-from-url"); if (createFormFromUrlEl) { // Check if the element exists const formNameUrlInput = document.getElementById("form-name-url"); const formUrlInput = document.getElementById("form-url"); createFormFromUrlEl.addEventListener("submit", async (e) => { e.preventDefault(); showStatus(""); const name = formNameUrlInput.value.trim(); const url = formUrlInput.value.trim(); if (!name || !url) { showStatus("Form name and URL are required.", true); return; } try { const newForm = await makeApiRequest( "/forms/from-url", "POST", { name, url }, true ); showStatus( `Form '${newForm.name}' created successfully with ID: ${newForm.id}` ); formNameUrlInput.value = ""; // Clear form formUrlInput.value = ""; loadFormsButton.click(); // Refresh the forms list } catch (error) { showStatus(`Failed to create form from URL: ${error.message}`, true); } }); } if (loadFormsButton) { loadFormsButton.addEventListener("click", async () => { showStatus(""); submissionsSection.classList.add("hidden"); // Hide submissions when reloading forms formsList.innerHTML = "
Error: Form definition is invalid.
"; console.error("Invalid form fields definition:", formDefinition.fields); return; } formDefinition.fields.forEach((field) => { const div = document.createElement("div"); const label = document.createElement("label"); label.htmlFor = `field-${field.name}`; label.textContent = field.label || field.name; // Use label, fallback to name div.appendChild(label); let input; // Basic type handling - could be expanded switch (field.type) { case "textarea": // Allow explicit textarea type case "string": // Use textarea for string if maxLength suggests it might be long if (field.maxLength && field.maxLength > 100) { input = document.createElement("textarea"); input.rows = 4; // Default rows } else { input = document.createElement("input"); input.type = "text"; } if (field.minLength) input.minLength = field.minLength; if (field.maxLength) input.maxLength = field.maxLength; break; case "email": input = document.createElement("input"); input.type = "email"; break; case "url": input = document.createElement("input"); input.type = "url"; break; case "number": input = document.createElement("input"); input.type = "number"; if (field.min !== undefined) input.min = field.min; if (field.max !== undefined) input.max = field.max; input.step = field.step || "any"; // Allow decimals by default break; case "boolean": input = document.createElement("input"); input.type = "checkbox"; // Checkbox label handling is slightly different label.insertBefore(input, label.firstChild); // Put checkbox before text input.style.width = "auto"; // Override default width input.style.marginRight = "10px"; break; // Add cases for 'select', 'radio', 'date' etc. if needed default: input = document.createElement("input"); input.type = "text"; console.warn( `Unsupported field type "${field.type}" for field "${field.name}". Rendering as text.` ); } if (input.type !== "checkbox") { // Checkbox is already appended inside label div.appendChild(input); } input.id = `field-${field.name}`; input.name = field.name; // Crucial for form data collection if (field.required) input.required = true; if (field.placeholder) input.placeholder = field.placeholder; if (field.pattern) input.pattern = field.pattern; // Add regex pattern validation publicForm.appendChild(div); }); const submitButton = document.createElement("button"); submitButton.type = "submit"; submitButton.textContent = "Submit Form"; publicForm.appendChild(submitButton); } publicForm.addEventListener("submit", async (e) => { e.preventDefault(); showStatus(""); const formId = e.target.dataset.formId; if (!formId) { showStatus("Error: Form ID is missing.", true); return; } const formData = new FormData(e.target); const submissionData = {}; // Convert FormData to a plain object, handling checkboxes correctly for (const [key, value] of formData.entries()) { const inputElement = e.target.elements[key]; // Handle Checkboxes (boolean) if (inputElement && inputElement.type === "checkbox") { // A checkbox value is only present in FormData if it's checked. // We need to ensure we always send a boolean. // Check if the element exists in the form (it might be unchecked) submissionData[key] = inputElement.checked; } // Handle Number inputs (convert from string) else if (inputElement && inputElement.type === "number") { // Only convert if the value is not empty, otherwise send null or handle as needed if (value !== "") { submissionData[key] = parseFloat(value); // Or parseInt if only integers allowed if (isNaN(submissionData[key])) { // Handle potential parsing errors if input validation fails console.warn(`Could not parse number for field ${key}: ${value}`); submissionData[key] = null; // Or keep as string, or show error } } else { submissionData[key] = null; // Or undefined, depending on backend expectation for empty numbers } } // Handle potential multiple values for the same name (e.g., multi-select), though not rendered here else if (submissionData.hasOwnProperty(key)) { if (!Array.isArray(submissionData[key])) { submissionData[key] = [submissionData[key]]; } submissionData[key].push(value); } // Default: treat as string else { submissionData[key] = value; } } // Ensure boolean fields that were *unchecked* are explicitly set to false // FormData only includes checked checkboxes. Find all checkbox inputs in the form. const checkboxes = e.target.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach((cb) => { if (!submissionData.hasOwnProperty(cb.name)) { submissionData[cb.name] = false; // Set unchecked boxes to false } }); console.log("Submitting data:", submissionData); // Debugging try { // Public submission endpoint doesn't require auth const result = await makeApiRequest( `/forms/${formId}/submissions`, "POST", submissionData, false ); showStatus( `Submission successful! Submission ID: ${result.submission_id}` ); e.target.reset(); // Clear the form // Optionally hide the form after successful submission // publicFormArea.classList.add('hidden'); } catch (error) { let errorMsg = `Submission failed: ${error.message}`; // Handle validation errors specifically if (error.validationErrors) { errorMsg = "Submission failed due to validation errors:\n"; for (const [field, message] of Object.entries(error.validationErrors)) { errorMsg += `- ${field}: ${message}\n`; } // Highlight invalid fields? (More complex UI update) } showStatus(errorMsg, true); } }); // --- Initial Setup --- toggleSections(); // Set initial view based on stored token if (authToken) { loadFormsButton.click(); // Auto-load forms if logged in } });