formies/frontend/script.js
Mohamad.Elsena fe5184e18c demo001
2025-05-05 17:01:20 +02:00

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
}
});