initial commit
This commit is contained in:
commit
94883878f7
22 changed files with 3749 additions and 0 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
**/node_modules
|
||||
**/dist
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.vscode
|
||||
__pycache__
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
|||
#build web app
|
||||
FROM node:lts-alpine AS build-stage
|
||||
WORKDIR /app
|
||||
COPY /frontend ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
#setup flask app
|
||||
FROM python:3.10-alpine AS production-stage
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./backend /app/backend
|
||||
#RUN mkdir /app/static
|
||||
RUN pip install --no-cache-dir -r backend/requirements.txt
|
||||
|
||||
COPY --from=build-stage /app/dist /app/frontend/dist/
|
||||
EXPOSE 8080
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--chdir", "backend", "app:app"]
|
13
backend/Pipfile
Normal file
13
backend/Pipfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
flask = "*"
|
||||
uwsgi = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
107
backend/Pipfile.lock
generated
Normal file
107
backend/Pipfile.lock
generated
Normal file
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "392f1c95a652ebb54d90d436d9b51a50d66a6ed0ce9dd16b444589df509a5f62"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.8"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"uwsgi": {
|
||||
"hashes": [
|
||||
"sha256:faa85e053c0b1be4d5585b0858d3a511d2cd10201802e8676060fd0a109e5869"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.19.1"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
102
backend/app.py
Normal file
102
backend/app.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
import os
|
||||
import logging
|
||||
from flask import Flask, send_file, request, jsonify
|
||||
from requests import HTTPError
|
||||
from zammad_py import ZammadAPI
|
||||
from zammad_py.api import Object as ZamObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ZAMMAD_TOKEN = os.getenv("ZAMMAD_TOKEN")
|
||||
ZAMMAD_URL = os.getenv("ZAMMAD_URL")
|
||||
|
||||
if not ZAMMAD_TOKEN or not ZAMMAD_URL:
|
||||
logger.error('Missing conf env var(s)')
|
||||
|
||||
app = Flask(__name__,
|
||||
static_folder=os.path.join("..","frontend","dist"))
|
||||
zammad = ZammadAPI(url=ZAMMAD_URL, http_token=ZAMMAD_TOKEN)
|
||||
|
||||
@app.route("/")
|
||||
def main():
|
||||
index_path = os.path.join(app.static_folder, "index.html")
|
||||
return send_file(index_path)
|
||||
|
||||
@app.route('/api/hello')
|
||||
def start():
|
||||
return "Hallo, Welt!"
|
||||
|
||||
@app.route('/api/buildings')
|
||||
def api_buildings():
|
||||
#return ["Theater Dortmund", "Schauspiel Dortmund", "Probebühnen Alte Straße", "Lager Alte Straße",
|
||||
# "Probebühne KJT","Schreinerei","Schlosserei", "Lager Niedersachsenweg" ]
|
||||
zob = ZamObject(zammad)
|
||||
for z in zob.all():
|
||||
if "gebaeude" == z['name']:
|
||||
return z['data_option']['options']
|
||||
|
||||
return jsonify([])
|
||||
|
||||
|
||||
@app.route("/api/groups")
|
||||
def api_groups():
|
||||
#return [{"text":'Hausmeister', "value":"5"}, {"text":"Haustechnik", "value":"4"}, {"text":"Leitung-HBT", "value":"2"}, {"text":"IT", "value":"4"}]
|
||||
arr = []
|
||||
for x in zammad.group.all():
|
||||
if "Users" != x['name']:
|
||||
arr.append(x['name'])
|
||||
|
||||
return jsonify(arr)
|
||||
|
||||
@app.route("/api/submit", methods=['POST'])
|
||||
def api_submit():
|
||||
data = request.json
|
||||
print(data)
|
||||
|
||||
if data['firstname'] != "" and data['lastname'] != "" and data['email'] != "":
|
||||
try:
|
||||
zammad.user.create({"firstname":data['firstname'],"lastname":data['lastname'],"email":data['email'],"roles":["Customer"]})
|
||||
except HTTPError as e:
|
||||
print("User creation failed: {}".format(e))
|
||||
if "Invalid email" in str(e):
|
||||
return jsonify(error='Invalid Email'), 500
|
||||
|
||||
|
||||
try:
|
||||
zammad.on_behalf_of = data['email']
|
||||
title = (data['description'][:25] + '..') if len(data['description']) > 25 else data['description']
|
||||
zammad.ticket.create({"title":"Web-Form: {}".format(title),"customer":data['email'],
|
||||
"group":data['group'],
|
||||
"gebaeude":data['building'],
|
||||
"raumnummer":data['room'],
|
||||
"article": {
|
||||
"subject": "",
|
||||
"body": data['description'],
|
||||
"type": "note",
|
||||
"internal": "false"
|
||||
}})
|
||||
except HTTPError as e:
|
||||
print("Ticket creation failed: {}".format(e))
|
||||
return jsonify(error=str(e)), 500
|
||||
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
# Everything not declared before (not a Flask route / API endpoint)...
|
||||
@app.route("/<path:path>")
|
||||
def route_frontend(path):
|
||||
# ...could be a static file needed by the front end that
|
||||
# doesn't use the `static` path (like in `<script src="bundle.js">`)
|
||||
file_path = os.path.join(app.static_folder, path)
|
||||
if os.path.isfile(file_path):
|
||||
return send_file(file_path)
|
||||
# ...or should be handled by the SPA's "router" in front end
|
||||
else:
|
||||
index_path = os.path.join(app.static_folder, "index.html")
|
||||
return send_file(index_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Only for debugging while developing
|
||||
app.run(host="0.0.0.0", debug=True, port=8080)
|
||||
|
4
backend/requirements.txt
Normal file
4
backend/requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
Flask==2.2.2
|
||||
Werkzeug==2.2.2
|
||||
gunicorn==23.0.0
|
||||
zammad_py==3.0.0
|
36
backend/uwsgi.ini
Normal file
36
backend/uwsgi.ini
Normal file
|
@ -0,0 +1,36 @@
|
|||
# [uwsgi]
|
||||
# # the module itself, by referring to the wsgi.py file minus the extension, and the callable within the file, app:
|
||||
# module = wsgi:app
|
||||
|
||||
# # Enable hot reload!
|
||||
# py-autoreload = 1
|
||||
|
||||
# # Nginx to handle actual client connections, which will then pass requests to uWSGI.
|
||||
# socket = :8080
|
||||
|
||||
# master = true
|
||||
# processes = 4
|
||||
# threads = 2
|
||||
|
||||
# # giving the Nginx group ownership of the uWSGI process later on,
|
||||
# # so we need to make sure the group owner of the socket can read information from it and write to it.
|
||||
# chmod-socket = 660
|
||||
|
||||
# # clean up the socket when the process stops by adding the vacuum option:
|
||||
# vacuum = true
|
||||
# die-on-term = true
|
||||
|
||||
[uwsgi]
|
||||
# module = uwsgi:app
|
||||
|
||||
module = main
|
||||
callable = app
|
||||
|
||||
# py-autoreload = 1
|
||||
# master = true
|
||||
# processes = 4
|
||||
# threads = 2
|
||||
|
||||
# vacuum = true
|
||||
|
||||
# wsgi-file=/app/uwsgi.py
|
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: local_single
|
||||
restart: always
|
||||
environment:
|
||||
- APP_NAME=VueApp
|
||||
ports:
|
||||
- "8080:8080"
|
22
frontend/.gitignore
vendored
Normal file
22
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
24
frontend/README.md
Normal file
24
frontend/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# hello
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
frontend/babel.config.js
Normal file
5
frontend/babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>Zammad Form</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
3016
frontend/package-lock.json
generated
Normal file
3016
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "hello",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"bootstrap": "^5.3.3",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-vue-devtools": "^7.6.8"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
16
frontend/src/App.vue
Normal file
16
frontend/src/App.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script setup>
|
||||
import Contact from './components/Contact.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<Contact />
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
86
frontend/src/assets/base.css
Normal file
86
frontend/src/assets/base.css
Normal file
|
@ -0,0 +1,86 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
8
frontend/src/assets/main.css
Normal file
8
frontend/src/assets/main.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
203
frontend/src/components/Contact.vue
Normal file
203
frontend/src/components/Contact.vue
Normal file
|
@ -0,0 +1,203 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
import axios from 'axios'
|
||||
|
||||
const formdata = reactive({
|
||||
firstname: '', lastname: '', email: '', phone: '', description: '',
|
||||
building: '', group: '', room: ''
|
||||
})
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
email: { required },
|
||||
firstname: { required },
|
||||
lastname: { required },
|
||||
description: { required },
|
||||
building: { required },
|
||||
group: { required },
|
||||
room: { required }
|
||||
}
|
||||
})
|
||||
const sending = ref(false);
|
||||
const error = ref(false);
|
||||
const success = ref(false);
|
||||
|
||||
const v$ = useVuelidate(rules, formdata);
|
||||
const buildings = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
axios.get('/api/buildings').then((response) => {
|
||||
buildings.value = response.data;
|
||||
});
|
||||
axios.get('/api/groups').then((response) => {
|
||||
groups.value = response.data;
|
||||
})
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
const result = await v$.value.$validate()
|
||||
if (result) {
|
||||
sending.value = true;
|
||||
console.log(formdata);
|
||||
axios.post('/api/submit', formdata, { headers: { 'Content-Type': 'application/json' } })
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
success.value = true;
|
||||
error.value = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
error.value = true;
|
||||
success.value = false;
|
||||
})
|
||||
.finally(() => { sending.value = false });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
|
||||
<h1>Ticket erstellen</h1>
|
||||
|
||||
<div v-if="success" class="alert alert-success" role="alert">
|
||||
Das Formular wurde versendet. Es wurde ein neues Ticket angelegt.
|
||||
</div>
|
||||
<div v-if="error" class="alert alert-danger" role="alert">
|
||||
Das Formular konnte nicht abgeschickt werden. Bitte wenden Sie sich per E-Mail an
|
||||
<a href="mailto:it-theater@theaterdo.de">it-theater@theaterdo.de</a>.
|
||||
</div>
|
||||
|
||||
|
||||
<form v-if="!success" @submit.prevent="submit" class="needs-validation" novalidate>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div data-mdb-input-init class="form-outline">
|
||||
<label class="form-label" for="firstname">Vorname *</label>
|
||||
<input type="text" id="firstname" class="form-control"
|
||||
:class="{ 'is-invalid': v$.firstname.$error }" v-model="formdata.firstname" required />
|
||||
<span class="invalid-feedback" v-if="v$.firstname.$error"> {{ v$.firstname.$errors[0].$message
|
||||
}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div data-mdb-input-init class="form-outline">
|
||||
<label class="form-label" for="lastname">Nachname *</label>
|
||||
<input type="text" id="lastname" class="form-control"
|
||||
:class="{ 'is-invalid': v$.lastname.$error }" v-model="formdata.lastname" required />
|
||||
<span class="invalid-feedback" v-if="v$.lastname.$error"> {{ v$.lastname.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<label for="emailaddress" class="form-label">Email Adresse *</label>
|
||||
<input type="email" class="form-control" id="emailaddress" :class="{ 'is-invalid': v$.email.$error }"
|
||||
v-model="formdata.email" required />
|
||||
<span class="invalid-feedback" v-if="v$.email.$error"> {{ v$.email.$errors[0].$message }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="phone" class="form-label">Telefon:</label>
|
||||
<input type="text" class="form-control" id="phone" v-model="formdata.phone">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mb-4" />
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label for="building" class="form-label">Gebäude *</label>
|
||||
<select id="building" class="form-select" :class="{ 'is-invalid': v$.building.$error }"
|
||||
v-model="formdata.building" required>
|
||||
<option v-for="option in buildings" :value="option.name">{{ option.name }}</option>
|
||||
</select>
|
||||
<span class="invalid-feedback" v-if="v$.building.$error"> {{ v$.building.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="room" class="form-label">Raum *</label>
|
||||
<input type="text" class="form-control" id="room" :class="{ 'is-invalid': v$.room.$error }"
|
||||
v-model="formdata.room" required />
|
||||
<span class="invalid-feedback" v-if="v$.room.$error"> {{ v$.room.$errors[0].$message }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Was ist los? *</label>
|
||||
<textarea class="form-control" id="description" rows="4" :class="{ 'is-invalid': v$.description.$error }"
|
||||
v-model="formdata.description" required>
|
||||
</textarea>
|
||||
<span class="invalid-feedback" v-if="v$.description.$error"> {{ v$.description.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="group" class="form-label">An wen soll das Ticket gehen? *</label>
|
||||
<select id="group" class="form-select" v-model="formdata.group" :class="{ 'is-invalid': v$.group.$error }"
|
||||
required>
|
||||
<option v-for="option in groups" :value="option">{{ option }}</option>
|
||||
</select>
|
||||
<span class="invalid-feedback" v-if="v$.group.$error"> {{ v$.group.$errors[0].$message }} </span>
|
||||
|
||||
<a class="icon-link" href="#" data-bs-toggle="modal" data-bs-target="#zustaendigkeitenmodal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info" viewBox="0 0 16 16">
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
||||
</svg>
|
||||
Zuständigkeiten anzeigen
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<button :disabled="sending" type="submit" class="btn btn-primary">Absenden</button>
|
||||
</form>
|
||||
|
||||
<div class="modal fade" tabindex="-1" id="zustaendigkeitenmodal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Zuständigkeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Haustechnik</h6>
|
||||
<ul>
|
||||
<li>Aufzugsstörungen, stecken bleiben, außer Betrieb..</li>
|
||||
<li>Lüftung + Heizung...zu warm, zu kalt..</li>
|
||||
<li>Stromausfälle...</li>
|
||||
<li>Beleuchtung...Leuchtmittel defekt...Schalter funktioniert nicht...</li>
|
||||
<li>Rauchmelderabschaltungen...mit Vorankündigung</li>
|
||||
<li>kleinere Elektroinstallationen...</li>
|
||||
<li>BGV A3 Geräteprüfung bei Neunanschaffung von z.b. Wasserkochern oder sonst...</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<h6>Hausmeister</h6>
|
||||
<ul>
|
||||
<li>Abflussverstopfungen...</li>
|
||||
<li>defekte Sanitäranlagen...Wasserhähne...Toiletten...Spülungen...</li>
|
||||
<li>Außenbereiche...defekte Stellen...Stolperfallen...</li>
|
||||
<li>Türen...klemmt...abgebrochene Schlüssel...Türklinken/Beschläge</li>
|
||||
<li>Druckerpapierlieferungen...Umzugskartons...</li>
|
||||
<li>Wasserschäden...</li>
|
||||
<li>Kleinreparraturen...Fenstergriffe...Wandausbesserungen...</li>
|
||||
<li>defekte Sitze in den Zuschauerräumen...</li>
|
||||
<li>kleinere hausinterne Transporte...</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootstrap"
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
27
frontend/vite.config.js
Normal file
27
frontend/vite.config.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080/',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
Loading…
Reference in a new issue