Răsfoiți Sursa

feat: initialize intranet form builder system with flat-file JSON storage, documentation, and sample data structures

AI 2 zile în urmă
părinte
comite
68f557ceed

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+# Intranet Forms System
+
+A simple, fast, and secure form builder and response collection system built entirely on a flat-file backend using native PHP.
+
+## Features
+
+* **No Database Required:** Stores all forms and responses securely in JSON files.
+* **Drag-and-Drop Builder:** Quickly assemble forms using Text inputs, Text areas, Single-Choice (radio), Multiple-Choice (checkbox), and Dropdown elements powered by Sortable.js. 
+* **Seamless Answering:** Forms cache user credentials (Name/Email) automatically per local session, enabling rapid workflows on shared terminals or multiple submissions.
+* **Admin Viewer:** Share the randomly generated Admin secret link to view responses aggregated in a responsive table.
+* **Email Notifications:** Triggers notification emails using native `mail()` handlers across creations and form resolutions.
+* **Intranet Style System:** A strictly responsive, dark-mode styling conforming to universal intranet `docs/style_system.md` standards.
+
+## Installation Instructions
+
+1. **Clone or copy the directory** to your target web host. Any shared hosting serving PHP 7.4+ is supported out of the box.
+2. **Ensure File Permissions:** Ensure the web server application has write permissions over the `data/forms/` and `data/answers/` directories.
+3. Access `index.php` from your browser to begin creating forms.
+
+> **Security Note:** The `data` folder contains a restrictive `.htaccess` file. Do not remove it, as it locks direct browser access to raw JSON files storing your submission metrics.
+
+## Documentation
+
+For AI agents and developers scaling the infrastructure, refer to the `/docs` folder:
+- **`docs/ai_spec.md`**: Agent-first architecture specifications.
+- **`docs/implementation_plan.md`**: The original architecture implementation plan context.
+- **`docs/style_system.md`**: Canonical branding and style rules.

+ 29 - 13
assets/js/create.js

@@ -24,12 +24,13 @@ function addQuestion(type) {
     
     let typeLabel = '';
     const needsOptions = ['single_choice', 'multiple_choice', 'dropdown'].includes(type);
+    const supportsFreeText = ['single_choice', 'multiple_choice'].includes(type);
     
     switch(type) {
-        case 'text': typeLabel = 'Text Input'; break;
-        case 'textarea': typeLabel = 'Text Area'; break;
-        case 'single_choice': typeLabel = 'Single Choice'; break;
-        case 'multiple_choice': typeLabel = 'Multiple Choice'; break;
+        case 'text': typeLabel = 'Textfeld'; break;
+        case 'textarea': typeLabel = 'Textbereich'; break;
+        case 'single_choice': typeLabel = 'Einzelauswahl'; break;
+        case 'multiple_choice': typeLabel = 'Mehrfachauswahl'; break;
         case 'dropdown': typeLabel = 'Dropdown'; break;
     }
     
@@ -44,18 +45,26 @@ function addQuestion(type) {
             <button type="button" class="modal-close" style="position:relative; top:0; right:0;" onclick="removeQuestion(this)">&times;</button>
         </div>
         <div class="form-group" style="margin-bottom:0;">
-            <label>Question Label</label>
-            <input type="text" class="question-label" required placeholder="Enter your question here">
+            <label>Fragetext</label>
+            <input type="text" class="question-label" required placeholder="Geben Sie hier Ihre Frage ein">
         </div>
     `;
 
     if (needsOptions) {
         html += `
         <div class="form-group mt-2 mb-0">
-            <label>Options (one per line)</label>
+            <label>Optionen (eine pro Zeile)</label>
             <textarea class="question-options" rows="3" required placeholder="Option 1\nOption 2\nOption 3"></textarea>
         </div>
         `;
+        if (supportsFreeText) {
+            html += `
+            <div class="form-group mt-1 mb-0" style="flex-direction:row; align-items:center;">
+                <input type="checkbox" class="question-freetext" id="freetext_${id}" style="width:auto; margin-right:8px;">
+                <label for="freetext_${id}" style="margin:0; font-weight:normal;">Freitext erlauben ("Sonstiges")</label>
+            </div>
+            `;
+        }
     }
 
     div.innerHTML = html;
@@ -66,7 +75,7 @@ function removeQuestion(btn) {
     btn.closest('.builder-item').remove();
     const canvas = document.getElementById('builder-canvas');
     if (canvas.children.length === 0) {
-        canvas.innerHTML = '<div class="alert alert-info" id="empty-state">No questions added yet. Use the buttons above to add one.</div>';
+        canvas.innerHTML = '<div class="alert alert-info" id="empty-state">Noch keine Fragen hinzugefügt. Verwenden Sie die Schaltflächen oben.</div>';
     }
 }
 
@@ -76,7 +85,7 @@ function prepareSubmission(e) {
     
     if (items.length === 0) {
         e.preventDefault();
-        alert("Please add at least one question.");
+        alert("Bitte fügen Sie mindestens eine Frage hinzu.");
         return;
     }
 
@@ -86,6 +95,7 @@ function prepareSubmission(e) {
     items.forEach((item) => {
         let labelInput = item.querySelector('.question-label');
         let optionsInput = item.querySelector('.question-options');
+        let freetextInput = item.querySelector('.question-freetext');
         
         let label = labelInput ? labelInput.value.trim() : '';
         if (!label) {
@@ -94,29 +104,35 @@ function prepareSubmission(e) {
 
         let options = [];
         if (optionsInput) {
-            options = optionsInput.value.split('\\n').map(o => o.trim()).filter(o => o);
+            options = optionsInput.value.split('\n').map(o => o.trim()).filter(o => o);
             if (options.length === 0) {
                 hasEmptyOptions = true;
             }
         }
+        
+        let allowFreeText = false;
+        if (freetextInput && freetextInput.checked) {
+            allowFreeText = true;
+        }
 
         questions.push({
             id: item.dataset.id,
             type: item.dataset.type,
             label: label,
-            options: options
+            options: options,
+            allow_free_text: allowFreeText
         });
     });
 
     if (hasEmptyLabel) {
         e.preventDefault();
-        alert("Please fill out all question labels.");
+        alert("Bitte füllen Sie alle Fragetexte aus.");
         return;
     }
 
     if (hasEmptyOptions) {
         e.preventDefault();
-        alert("Please provide at least one option for your choice/dropdown questions.");
+        alert("Bitte geben Sie mindestens eine Option für Ihre Auswahlfragen an.");
         return;
     }
     

+ 13 - 0
data/answers/form_69d586eeaf089_ans_69d587787e2b2.json

@@ -0,0 +1,13 @@
+{
+    "answer_id": "ans_69d587787e2b2",
+    "form_id": "form_69d586eeaf089",
+    "respondent_name": "Josef",
+    "respondent_email": "jsoef@mailpit.medowar.de",
+    "submitted_at": "2026-04-07T22:38:48+00:00",
+    "answers": {
+        "q_1775601347939_1": "asd",
+        "q_1775601354064_2": "Ich komme alleine\r\nIch komme mit Begleitung",
+        "q_1775601369614_3": "Wache 1\r\nWache 2",
+        "q_1775601378187_4": "Fleisch\r\nVegetarisch"
+    }
+}

+ 40 - 0
data/forms/form_69d586eeaf089.json

@@ -0,0 +1,40 @@
+{
+    "id": "form_69d586eeaf089",
+    "title": "Abschiedsparty 2\/30",
+    "description": "When you submit this form, it will not automatically collect your details like name and email address unless you provide it yourself.",
+    "admin_email": "admin@mailpit.medowar.de",
+    "admin_token": "2e80c75668c91cd50ba882340af7a7d5",
+    "created_at": "2026-04-07T22:36:30+00:00",
+    "questions": [
+        {
+            "id": "q_1775601347939_1",
+            "type": "text",
+            "label": "Teilnahme",
+            "options": []
+        },
+        {
+            "id": "q_1775601354064_2",
+            "type": "single_choice",
+            "label": "Teilnahme",
+            "options": [
+                "Ich komme alleine\nIch komme mit Begleitung"
+            ]
+        },
+        {
+            "id": "q_1775601369614_3",
+            "type": "single_choice",
+            "label": "Wache",
+            "options": [
+                "Wache 1\nWache 2"
+            ]
+        },
+        {
+            "id": "q_1775601378187_4",
+            "type": "single_choice",
+            "label": "Essen",
+            "options": [
+                "Fleisch\nVegetarisch"
+            ]
+        }
+    ]
+}

+ 64 - 0
docs/ai_spec.md

@@ -0,0 +1,64 @@
+---
+description: Intelligent Context and Directives for AI Agents modifying the Forms System
+---
+
+# Forms System - AI Agent Context
+
+## Intended Architecture Overview
+This application serves as a strictly localized web form engine. 
+Its primary constraint is **No Database**. All states, identities, responses, and settings must be mapped dynamically to JSON files. 
+
+## Language Requirement Contract
+The target group operates in German context. **All front-end user interfaces, prompts, notifications, and labels must be written in formal German (Sie).**
+
+## Data Interface Contract
+All data payloads process through `/data`. Any newly introduced state requires modifying this abstraction carefully without abandoning `.htaccess` safeguards.
+
+### Form Definitions
+Path Map: `/data/forms/{form_id}.json`
+Structure Schema:
+```json
+{
+  "id": "form_{uniqid}",
+  "title": "String",
+  "description": "String",
+  "admin_email": "String",
+  "admin_token": "String",
+  "created_at": "ISO-8601",
+  "questions": [
+    {
+      "id": "q_{timestamp}_{index}",
+      "type": "text | textarea | single_choice | multiple_choice | dropdown",
+      "label": "String",
+      "options": ["Array of Strings if applicable"],
+      "allow_free_text": "Boolean (true/false) - for choice configurations"
+    }
+  ]
+}
+```
+
+### Response Answers
+Path Map: `/data/answers/{form_id}_{answer_id}.json`
+Structure Schema:
+```json
+{
+  "answer_id": "ans_{uniqid}",
+  "form_id": "form_{uniqid}",
+  "respondent_name": "String",
+  "respondent_email": "String",
+  "submitted_at": "ISO-8601",
+  "answers": {
+    "q_{timestamp}_{index}": "String or Array of Strings"
+  }
+}
+```
+
+## Styling Heuristics
+You must refer and conform locally to `docs/style_system.md` for any interface additions.
+- Do not drop in Tailwind or external CSS.
+- New DOM elements must invoke predefined token components, primarily using the `panel`, `btn`, `alert`, and strictly adopting the internal responsive breakpoint mechanisms. 
+
+## Modification Rules
+1. **Double Submission:** Allowed intentionally. The local application cache mitigates keystrokes for repeated `respondent_name` entries but does not gatekeep. Modifying `assets/js/answer.js` requires respecting this state requirement.
+2. **SortableJS:** Included to fulfill the complex choice mechanisms inside the form builder. Maintain the `Sortable.js` DOM synchronization inside `assets/js/create.js`.
+3. **No Dynamic Classes:** The web hosting rules dictate straightforward legacy rendering support via raw HTML endpoints (`php`). No templating engines (Blade/Twig) are attached.

+ 86 - 0
docs/implementation_plan.md

@@ -0,0 +1,86 @@
+# Forms Response Collection System Implementation Plan
+
+## Goal Description
+Create a simple, Microsoft Forms-like response collection system using flat files (no database) with HTML, PHP, JS, and CSS. The system will support creating forms via drag-and-drop, storing data on a shared hosting environment, capturing answers with user memory, sending native PHP emails, and viewing responses through a secure admin link.
+
+## Proposed Architecture
+
+Since there is no database, we will use JSON files to store forms and responses. 
+
+### Data Structure (`/data` directory)
+- `/data/forms/<form_id>.json` - Stores form structure (questions, types, creation details).
+- `/data/answers/<form_id>_<answer_id>.json` - Stores individual submitted responses.
+
+### Key Workflows
+
+**1. Form Creation (`create` mode)**
+- Drag-and-drop UI to assemble questions (text, multiple choice, etc.).
+- Admin can optionally provide an email address to receive the admin link.
+- Save creates a new `<form_id>` and a unique `<admin_token>`.
+- Redirects to a success page showing the public link to share and the secret admin link.
+
+**2. Form Answering (`answer` mode)**
+- User accesses the public link (`answer.php?id=<form_id>`).
+- Prompts for Name and Email (optional). These are saved in `localStorage` / cookies to remember the user on future visits.
+- Displays questions for answering.
+- Prevents double submissions (checked via localStorage & matching data).
+- Upon submit, sends a confirmation email (if email was provided) with the Q&A summary using PHP's native `mail()`.
+- Post-submit view shows a read-only list of submitted answers with options to "Add another answer" (with different identity) or "Edit response".
+
+**3. Admin View (`admin` mode)**
+- Accessible via `admin.php?id=<form_id>&token=<admin_token>`.
+- Displays a table/list of all submitted responses for that specific form.
+
+### Appearance 
+- The UI will faithfully implement the intranet dark theme specified in `docs/style_system.md` (e.g., `#2f3541` deep surface area, `#cac300` accents).
+
+## Proposed Changes
+
+### Assets
+#### [NEW] assets/css/style.css
+Implementation of the portable intranet style system spec (variables, spacing, grids, dark theme, typography).
+
+#### [NEW] assets/js/create.js
+Drag-and-drop logic for adding questions. Generating form payload for submission.
+
+#### [NEW] assets/js/answer.js
+Client-side logic for caching Name/Email, validating submissions, and enforcing single-submit-per-person logic.
+
+### Pages & Logic
+#### [NEW] index.php
+Landing page with a split choice: "Create a Form".
+
+#### [NEW] create.php
+The drag and drop editor view and the PHP handler for saving the form JSON. 
+
+#### [NEW] answer.php
+The page that loads a specific form's JSON and renders it for the end user to fill out.
+
+#### [NEW] submit.php
+PHP endpoint that receives a form submission, saves the answer JSON, and dispatches native PHP notification emails.
+
+#### [NEW] admin.php
+The private dashboard for viewing form submissions. Loaded via a secret token.
+
+#### [NEW] data/.htaccess
+A security lock to prevent direct public access to raw JSON files.
+
+## Open Questions
+
+> [!WARNING]  
+> Before proceeding, please confirm the following details:
+> 1. **Drag-and-Drop Complexity:** Are simple text inputs and text areas enough for questions, or do you require multiple-choice capabilities (radio/checkbox/dropdowns) in the form builder for version 1?
+> 2. **Double Submission:** Since there are no user accounts, is remembering the user via cookies/LocalStorage sufficient to prevent double submissions?
+> 3. **Validation:** Should we use external JS libraries for the drag-and-drop (like SortableJS) via CDN, or should I write a lightweight native HTML5 Drag and Drop implementation?
+
+## Verification Plan
+
+### Automated/Manual Testing
+- Create a new form with two custom questions and enter an admin email.
+- Verify that a JSON structure correctly saves to disk.
+- Navigate to the answering link in an incognito window.
+- Fill out the Name, Email, and answers. Submit and ensure local data remembers the submission.
+- Verify rendering of the completed answers.
+- Hit the "Reload" button and verify double submission logic flags the active session.
+- Validate that the CSS applied matches the `docs/style_system.md` values properties exactly using browser dev tools.
+- (Local email debugging will log to file or use standard `syslog` if native `mail()` is tricky to verify externally).

+ 8 - 8
index.php

@@ -2,11 +2,11 @@
 // index.php - Landing page
 ?>
 <!DOCTYPE html>
-<html lang="en">
+<html lang="de">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Intranet Forms</title>
+    <title>Intranet Formulare</title>
     <link rel="stylesheet" href="assets/css/style.css">
 </head>
 <body>
@@ -14,21 +14,21 @@
         <div class="container header-inner">
             <div class="brand">
                 <div class="brand-info">
-                    <span class="brand-title">Intranet Forms</span>
-                    <span class="brand-subtitle">Fast & Simple Data Collection</span>
+                    <span class="brand-title">Intranet Formulare</span>
+                    <span class="brand-subtitle">Schnelle & Einfache Datenerfassung</span>
                 </div>
             </div>
             <nav class="site-nav">
-                <a href="create.php" class="btn btn-secondary">Create Form</a>
+                <a href="create.php" class="btn btn-secondary">Formular erstellen</a>
             </nav>
         </div>
     </header>
 
     <main class="container">
         <div class="panel text-center" style="max-width: 600px; margin: 40px auto;">
-            <h1 class="card-title">Welcome to Intranet Forms</h1>
-            <p style="margin-bottom: 2rem;">Quickly create simple text forms and gather responses from your team.</p>
-            <a href="create.php" class="btn">Create a New Form</a>
+            <h1 class="card-title">Willkommen bei Intranet Formulare</h1>
+            <p style="margin-bottom: 2rem;">Erstellen Sie schnell einfache Formulare und sammeln Sie Antworten von Ihrem Team.</p>
+            <a href="create.php" class="btn">Neues Formular erstellen</a>
         </div>
     </main>
 </body>