272 lines
11 KiB
Python
272 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import date, datetime
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, flash, redirect, render_template, request, send_from_directory, url_for
|
|
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from sqlalchemy import func, select
|
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
|
|
from reporting.report_builder import build_validation_report
|
|
|
|
db = SQLAlchemy()
|
|
login_manager = LoginManager()
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
GENERATED_DIR = BASE_DIR / "generated"
|
|
GENERATED_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
class User(UserMixin, db.Model):
|
|
__tablename__ = "users"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
username = db.Column(db.String(100), unique=True, nullable=False)
|
|
password_hash = db.Column(db.String(255), nullable=False)
|
|
|
|
def set_password(self, password):
|
|
self.password_hash = generate_password_hash(password)
|
|
|
|
def check_password(self, password):
|
|
return check_password_hash(self.password_hash, password)
|
|
|
|
|
|
class Customer(db.Model):
|
|
__tablename__ = "customers"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
practice_name = db.Column(db.String(180), nullable=False)
|
|
operator_name = db.Column(db.String(180), nullable=False)
|
|
contact_person = db.Column(db.String(180))
|
|
hygiene_contact = db.Column(db.String(180))
|
|
street = db.Column(db.String(180))
|
|
postal_code = db.Column(db.String(20))
|
|
city = db.Column(db.String(120))
|
|
email = db.Column(db.String(180))
|
|
phone = db.Column(db.String(80))
|
|
|
|
@property
|
|
def address(self):
|
|
return ", ".join(x for x in [
|
|
self.street or "",
|
|
f"{self.postal_code or ''} {self.city or ''}".strip()
|
|
] if x)
|
|
|
|
|
|
class Device(db.Model):
|
|
__tablename__ = "devices"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False)
|
|
manufacturer = db.Column(db.String(120), nullable=False)
|
|
model = db.Column(db.String(120), nullable=False)
|
|
serial_number = db.Column(db.String(120), unique=True, nullable=False)
|
|
year_built = db.Column(db.String(20))
|
|
commissioning_date = db.Column(db.Date)
|
|
chamber_volume = db.Column(db.String(50))
|
|
steam_generation = db.Column(db.String(180))
|
|
water_supply = db.Column(db.String(180))
|
|
documentation = db.Column(db.String(180))
|
|
supplier = db.Column(db.String(180))
|
|
customer = db.relationship("Customer", backref="devices")
|
|
|
|
|
|
class Equipment(db.Model):
|
|
__tablename__ = "equipment"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
designation = db.Column(db.String(150), nullable=False)
|
|
manufacturer = db.Column(db.String(120))
|
|
model = db.Column(db.String(120))
|
|
serial_number = db.Column(db.String(120), unique=True, nullable=False)
|
|
measurement_range = db.Column(db.String(150))
|
|
calibration_date = db.Column(db.Date)
|
|
calibration_due = db.Column(db.Date)
|
|
equipment_type = db.Column(db.String(80))
|
|
|
|
|
|
class Validation(db.Model):
|
|
__tablename__ = "validations"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
report_number = db.Column(db.String(60), unique=True, nullable=False)
|
|
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False)
|
|
device_id = db.Column(db.Integer, db.ForeignKey("devices.id"), nullable=False)
|
|
validation_type = db.Column(db.String(60), nullable=False)
|
|
project = db.Column(db.String(120), nullable=False)
|
|
test_date = db.Column(db.Date, nullable=False)
|
|
test_location = db.Column(db.String(120), nullable=False)
|
|
examiner = db.Column(db.String(150), nullable=False)
|
|
participants = db.Column(db.String(255))
|
|
status = db.Column(db.String(30), default="Entwurf", nullable=False)
|
|
result = db.Column(db.String(30), default="Bestanden", nullable=False)
|
|
room_temperature = db.Column(db.String(20))
|
|
room_humidity = db.Column(db.String(20))
|
|
vacuum_leak_rate = db.Column(db.String(30))
|
|
next_validation = db.Column(db.Date)
|
|
recommendations = db.Column(db.Text)
|
|
notes = db.Column(db.Text)
|
|
customer = db.relationship("Customer")
|
|
device = db.relationship("Device")
|
|
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
try:
|
|
return db.session.get(User, int(user_id))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def parse_date(value):
|
|
if not value:
|
|
return None
|
|
try:
|
|
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def next_report_number():
|
|
count = db.session.scalar(select(func.count()).select_from(Validation)) or 0
|
|
return f"VAL-{datetime.now().year}-{count + 1:04d}"
|
|
|
|
|
|
def seed_demo_data():
|
|
if db.session.scalar(select(func.count()).select_from(Customer)):
|
|
return
|
|
|
|
customer = Customer(
|
|
practice_name="Urologische Praxis Muster",
|
|
operator_name="Dr. Max Mustermann",
|
|
contact_person="Dr. Max Mustermann",
|
|
hygiene_contact="Frau Beispiel",
|
|
street="Musterstraße 5",
|
|
postal_code="90459",
|
|
city="Nürnberg",
|
|
email="praxis@example.de",
|
|
phone="0911 123456",
|
|
)
|
|
db.session.add(customer)
|
|
db.session.flush()
|
|
|
|
device = Device(
|
|
customer_id=customer.id,
|
|
manufacturer="Euronda",
|
|
model="E10.7",
|
|
serial_number="DEMO-EXN250688",
|
|
year_built="2025",
|
|
commissioning_date=date(2025, 12, 5),
|
|
chamber_volume="23 Liter",
|
|
steam_generation="Eigendampferzeugung",
|
|
water_supply="Automatische Versorgung über Aquafilter",
|
|
documentation="Interne CF-Card, Ausgabe am PC möglich",
|
|
supplier="Medizintechnik Muster",
|
|
)
|
|
db.session.add(device)
|
|
|
|
equipment = [
|
|
Equipment(designation="Temperatur Datenlogger 1", manufacturer="Ebro", model="EBI 11 T235", serial_number="15102807", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
|
|
Equipment(designation="Temperatur Datenlogger 2", manufacturer="Ebro", model="EBI 11 T240", serial_number="15211066", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
|
|
Equipment(designation="Temperatur Datenlogger 3", manufacturer="Ebro", model="EBI 11 T240", serial_number="15102538", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
|
|
Equipment(designation="Temperatur Datenlogger 4", manufacturer="Ebro", model="EBI 11 T240", serial_number="15211067", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
|
|
Equipment(designation="Temperatur Datenlogger 5", manufacturer="Ebro", model="EBI 11 T240", serial_number="15125738", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
|
|
Equipment(designation="Drucklogger", manufacturer="Ebro", model="EBI 11 P111", serial_number="15092368", measurement_range="-100 mbar bis 4 bar abs.", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Druck"),
|
|
]
|
|
db.session.add_all(equipment)
|
|
db.session.flush()
|
|
|
|
validation = Validation(
|
|
report_number="VAL-DEMO-0001",
|
|
customer_id=customer.id,
|
|
device_id=device.id,
|
|
validation_type="Erstvalidierung",
|
|
project="Erst-Validierung",
|
|
test_date=date(2025, 12, 5),
|
|
test_location="Nürnberg",
|
|
examiner="Andreas Schubert",
|
|
participants="Frau Beispiel, Dr. Max Mustermann",
|
|
status="Entwurf",
|
|
result="Bestanden",
|
|
room_temperature="21 °C",
|
|
room_humidity="41 %",
|
|
vacuum_leak_rate="0,2 mbar/min",
|
|
next_validation=date(2027, 11, 1),
|
|
recommendations="Auflagen und Empfehlungen werden im Kapitel 4.2 dokumentiert.",
|
|
notes="V2-Vorschau: Winlog-Messwerte, Originalbilder, Zertifikate und Logo werden später dynamisch ersetzt.",
|
|
)
|
|
db.session.add(validation)
|
|
db.session.commit()
|
|
|
|
|
|
def create_app():
|
|
app = Flask(__name__)
|
|
app.config.update(
|
|
SECRET_KEY=os.getenv("SECRET_KEY", "validation-v2-local-secret"),
|
|
SQLALCHEMY_DATABASE_URI=os.getenv("DATABASE_URL", "sqlite:///validation-v2.db"),
|
|
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
|
)
|
|
db.init_app(app)
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = "login"
|
|
|
|
@app.route("/register", methods=["GET", "POST"])
|
|
def register():
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for("dashboard"))
|
|
if request.method == "POST":
|
|
username = (request.form.get("username") or "").strip()
|
|
password = request.form.get("password") or ""
|
|
repeat = request.form.get("password_repeat") or ""
|
|
if len(username) < 3 or len(password) < 6 or password != repeat:
|
|
flash("Bitte gültige Zugangsdaten eingeben.", "error")
|
|
elif db.session.scalar(select(User).where(func.lower(User.username) == username.lower())):
|
|
flash("Benutzername bereits vergeben.", "error")
|
|
else:
|
|
user = User(username=username)
|
|
user.set_password(password)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return redirect(url_for("login"))
|
|
return render_template("register.html")
|
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
|
def login():
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for("dashboard"))
|
|
if request.method == "POST":
|
|
username = (request.form.get("username") or "").strip()
|
|
user = db.session.scalar(select(User).where(func.lower(User.username) == username.lower()))
|
|
if user and user.check_password(request.form.get("password") or ""):
|
|
login_user(user)
|
|
return redirect(url_for("dashboard"))
|
|
flash("Anmeldung fehlgeschlagen.", "error")
|
|
return render_template("login.html")
|
|
|
|
@app.route("/logout", methods=["POST"])
|
|
@login_required
|
|
def logout():
|
|
logout_user()
|
|
return redirect(url_for("login"))
|
|
|
|
@app.route("/")
|
|
@login_required
|
|
def dashboard():
|
|
validations = list(db.session.scalars(select(Validation).order_by(Validation.id.desc())))
|
|
return render_template("dashboard.html", validations=validations)
|
|
|
|
@app.route("/validations/<int:item_id>/report")
|
|
@login_required
|
|
def report(item_id):
|
|
validation = db.session.get(Validation, item_id)
|
|
if not validation:
|
|
return "Nicht gefunden", 404
|
|
equipment = list(db.session.scalars(select(Equipment).order_by(Equipment.id)))
|
|
filename = build_validation_report(validation, equipment, GENERATED_DIR)
|
|
return send_from_directory(GENERATED_DIR, filename, as_attachment=True)
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
seed_demo_data()
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|