576 lines
19 KiB
JavaScript
576 lines
19 KiB
JavaScript
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 = '<button type="submit">Submit Form</button>'; // 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 = "<li>Loading...</li>"; // Indicate loading
|
|
|
|
try {
|
|
const forms = await makeApiRequest("/forms", "GET", null, true);
|
|
formsList.innerHTML = ""; // Clear list
|
|
|
|
if (forms && forms.length > 0) {
|
|
forms.forEach((form) => {
|
|
const li = document.createElement("li");
|
|
li.textContent = `${form.name} (ID: ${form.id})`;
|
|
|
|
const viewSubmissionsButton = document.createElement("button");
|
|
viewSubmissionsButton.textContent = "View Submissions";
|
|
viewSubmissionsButton.onclick = () =>
|
|
loadSubmissions(form.id, form.name);
|
|
|
|
li.appendChild(viewSubmissionsButton);
|
|
formsList.appendChild(li);
|
|
});
|
|
} else {
|
|
formsList.innerHTML = "<li>No forms found.</li>";
|
|
}
|
|
} catch (error) {
|
|
showStatus(`Failed to load forms: ${error.message}`, true);
|
|
formsList.innerHTML = "<li>Error loading forms.</li>";
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadSubmissions(formId, formName) {
|
|
showStatus("");
|
|
submissionsList.innerHTML = "<li>Loading submissions...</li>";
|
|
submissionsFormNameSpan.textContent = `${formName} (ID: ${formId})`;
|
|
submissionsSection.classList.remove("hidden");
|
|
|
|
try {
|
|
const submissions = await makeApiRequest(
|
|
`/forms/${formId}/submissions`,
|
|
"GET",
|
|
null,
|
|
true
|
|
);
|
|
submissionsList.innerHTML = ""; // Clear list
|
|
|
|
if (submissions && submissions.length > 0) {
|
|
submissions.forEach((sub) => {
|
|
const li = document.createElement("li");
|
|
// Display submission data safely - avoid rendering raw HTML
|
|
const pre = document.createElement("pre");
|
|
pre.textContent = JSON.stringify(sub.data, null, 2); // Pretty print JSON
|
|
li.appendChild(pre);
|
|
// Optionally display submission ID and timestamp if available
|
|
// const info = document.createElement('small');
|
|
// info.textContent = `ID: ${sub.id}, Submitted: ${sub.created_at || 'N/A'}`;
|
|
// li.appendChild(info);
|
|
|
|
submissionsList.appendChild(li);
|
|
});
|
|
} else {
|
|
submissionsList.innerHTML =
|
|
"<li>No submissions found for this form.</li>";
|
|
}
|
|
} catch (error) {
|
|
showStatus(
|
|
`Failed to load submissions for form ${formId}: ${error.message}`,
|
|
true
|
|
);
|
|
submissionsList.innerHTML = "<li>Error loading submissions.</li>";
|
|
submissionsSection.classList.add("hidden"); // Hide section on error
|
|
}
|
|
}
|
|
|
|
// --- Public Form Handling ---
|
|
|
|
if (loadPublicFormButton) {
|
|
loadPublicFormButton.addEventListener("click", async () => {
|
|
const formId = publicFormIdInput.value.trim();
|
|
if (!formId) {
|
|
showStatus("Please enter a Form ID.", true);
|
|
return;
|
|
}
|
|
showStatus("");
|
|
publicFormArea.classList.add("hidden");
|
|
publicForm.innerHTML = "Loading form..."; // Clear previous form
|
|
|
|
// NOTE: Fetching form definition is NOT directly possible with the current backend
|
|
// The backend only provides GET /forms (all, protected) and GET /forms/{id}/submissions (protected)
|
|
// It DOES NOT provide a public GET /forms/{id} endpoint to fetch the definition.
|
|
//
|
|
// **WORKAROUND:** We will *assume* the user knows the structure or we have it cached/predefined.
|
|
// For this example, we'll fetch *all* forms (if logged in) and find it, OR fail if not logged in.
|
|
// A *better* backend design would include a public GET /forms/{id} endpoint.
|
|
|
|
try {
|
|
// Attempt to get the form definition (requires login for this workaround)
|
|
if (!authToken) {
|
|
showStatus(
|
|
"Loading public forms requires login in this demo version.",
|
|
true
|
|
);
|
|
publicForm.innerHTML = ""; // Clear loading message
|
|
return;
|
|
}
|
|
const forms = await makeApiRequest("/forms", "GET", null, true);
|
|
const formDefinition = forms.find((f) => f.id === formId);
|
|
|
|
if (!formDefinition) {
|
|
throw new Error(`Form with ID ${formId} not found or access denied.`);
|
|
}
|
|
|
|
renderPublicForm(formDefinition);
|
|
publicFormArea.classList.remove("hidden");
|
|
} catch (error) {
|
|
showStatus(`Failed to load form ${formId}: ${error.message}`, true);
|
|
publicForm.innerHTML = ""; // Clear loading message
|
|
publicFormArea.classList.add("hidden");
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderPublicForm(formDefinition) {
|
|
publicFormTitle.textContent = formDefinition.name;
|
|
publicForm.innerHTML = ""; // Clear previous fields
|
|
publicForm.dataset.formId = formDefinition.id; // Store form ID for submission
|
|
|
|
if (!formDefinition.fields || !Array.isArray(formDefinition.fields)) {
|
|
publicForm.innerHTML = "<p>Error: Form definition is invalid.</p>";
|
|
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
|
|
}
|
|
});
|