Initial commit: LeoCRM - Flask CRM with auth, companies, contacts
This commit is contained in:
@@ -0,0 +1,256 @@
|
|||||||
|
"""LeoCRM - Minimal CRM with auth, companies, contacts."""
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, flash, session, g
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.urandom(24).hex()
|
||||||
|
DATABASE = os.path.join(os.path.dirname(__file__), 'leocrm.db')
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if 'db' not in g:
|
||||||
|
g.db = sqlite3.connect(DATABASE)
|
||||||
|
g.db.row_factory = sqlite3.Row
|
||||||
|
g.db.execute("PRAGMA foreign_keys = ON")
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_db(exception):
|
||||||
|
db = g.pop('db', None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
db = sqlite3.connect(DATABASE)
|
||||||
|
db.executescript('''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
address TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
email TEXT,
|
||||||
|
website TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
first_name TEXT NOT NULL,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
email TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
position TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
''')
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
flash('Bitte melde dich an.', 'warning')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if 'user_id' in session:
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username'].strip()
|
||||||
|
password = request.form['password']
|
||||||
|
if not username or not password:
|
||||||
|
flash('Benutzername und Passwort sind erforderlich.', 'danger')
|
||||||
|
return render_template('register.html')
|
||||||
|
db = get_db()
|
||||||
|
existing = db.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
if existing:
|
||||||
|
flash('Benutzername bereits vergeben.', 'danger')
|
||||||
|
return render_template('register.html')
|
||||||
|
db.execute('INSERT INTO users (username, password_hash) VALUES (?, ?)',
|
||||||
|
(username, generate_password_hash(password)))
|
||||||
|
db.commit()
|
||||||
|
flash('Registrierung erfolgreich! Bitte melde dich an.', 'success')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username'].strip()
|
||||||
|
password = request.form['password']
|
||||||
|
db = get_db()
|
||||||
|
user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
if user and check_password_hash(user['password_hash'], password):
|
||||||
|
session['user_id'] = user['id']
|
||||||
|
session['username'] = user['username']
|
||||||
|
flash(f'Willkommen, {username}!', 'success')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
flash('Ungültige Anmeldedaten.', 'danger')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
flash('Du wurdest abgemeldet.', 'info')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/dashboard')
|
||||||
|
@login_required
|
||||||
|
def dashboard():
|
||||||
|
db = get_db()
|
||||||
|
companies = db.execute(
|
||||||
|
'SELECT * FROM companies WHERE user_id = ? ORDER BY name',
|
||||||
|
(session['user_id'],)
|
||||||
|
).fetchall()
|
||||||
|
return render_template('dashboard.html', companies=companies)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/create', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def company_create():
|
||||||
|
if request.method == 'POST':
|
||||||
|
db = get_db()
|
||||||
|
db.execute('INSERT INTO companies (name, address, phone, email, website, notes, user_id) VALUES (?,?,?,?,?,?,?)',
|
||||||
|
(request.form['name'], request.form.get('address',''), request.form.get('phone',''),
|
||||||
|
request.form.get('email',''), request.form.get('website',''), request.form.get('notes',''),
|
||||||
|
session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
flash('Firma erstellt.', 'success')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
return render_template('company_form.html', company=None)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/<int:id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def company_edit(id):
|
||||||
|
db = get_db()
|
||||||
|
company = db.execute('SELECT * FROM companies WHERE id = ? AND user_id = ?',
|
||||||
|
(id, session['user_id'])).fetchone()
|
||||||
|
if not company:
|
||||||
|
flash('Firma nicht gefunden.', 'danger')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
if request.method == 'POST':
|
||||||
|
db.execute('UPDATE companies SET name=?, address=?, phone=?, email=?, website=?, notes=? WHERE id=?',
|
||||||
|
(request.form['name'], request.form.get('address',''), request.form.get('phone',''),
|
||||||
|
request.form.get('email',''), request.form.get('website',''), request.form.get('notes',''), id))
|
||||||
|
db.commit()
|
||||||
|
flash('Firma aktualisiert.', 'success')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
return render_template('company_form.html', company=company)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/<int:id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def company_delete(id):
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM companies WHERE id = ? AND user_id = ?', (id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
flash('Firma gelöscht.', 'info')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/<int:company_id>/contacts')
|
||||||
|
@login_required
|
||||||
|
def contact_list(company_id):
|
||||||
|
db = get_db()
|
||||||
|
company = db.execute('SELECT * FROM companies WHERE id = ? AND user_id = ?',
|
||||||
|
(company_id, session['user_id'])).fetchone()
|
||||||
|
if not company:
|
||||||
|
flash('Firma nicht gefunden.', 'danger')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
contacts = db.execute('SELECT * FROM contacts WHERE company_id = ? ORDER BY last_name, first_name',
|
||||||
|
(company_id,)).fetchall()
|
||||||
|
return render_template('contact_list.html', company=company, contacts=contacts)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/companies/<int:company_id>/contacts/create', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def contact_create(company_id):
|
||||||
|
db = get_db()
|
||||||
|
company = db.execute('SELECT * FROM companies WHERE id = ? AND user_id = ?',
|
||||||
|
(company_id, session['user_id'])).fetchone()
|
||||||
|
if not company:
|
||||||
|
flash('Firma nicht gefunden.', 'danger')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
if request.method == 'POST':
|
||||||
|
db.execute('INSERT INTO contacts (first_name, last_name, email, phone, position, notes, company_id) VALUES (?,?,?,?,?,?,?)',
|
||||||
|
(request.form['first_name'], request.form['last_name'], request.form.get('email',''),
|
||||||
|
request.form.get('phone',''), request.form.get('position',''), request.form.get('notes',''), company_id))
|
||||||
|
db.commit()
|
||||||
|
flash('Kontaktperson erstellt.', 'success')
|
||||||
|
return redirect(url_for('contact_list', company_id=company_id))
|
||||||
|
return render_template('contact_form.html', company=company, contact=None)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/contacts/<int:id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def contact_edit(id):
|
||||||
|
db = get_db()
|
||||||
|
contact = db.execute('''SELECT c.*, co.name as company_name, co.user_id
|
||||||
|
FROM contacts c JOIN companies co ON c.company_id = co.id
|
||||||
|
WHERE c.id = ? AND co.user_id = ?''',
|
||||||
|
(id, session['user_id'])).fetchone()
|
||||||
|
if not contact:
|
||||||
|
flash('Kontakt nicht gefunden.', 'danger')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
if request.method == 'POST':
|
||||||
|
db.execute('UPDATE contacts SET first_name=?, last_name=?, email=?, phone=?, position=?, notes=? WHERE id=?',
|
||||||
|
(request.form['first_name'], request.form['last_name'], request.form.get('email',''),
|
||||||
|
request.form.get('phone',''), request.form.get('position',''), request.form.get('notes',''), id))
|
||||||
|
db.commit()
|
||||||
|
flash('Kontakt aktualisiert.', 'success')
|
||||||
|
return redirect(url_for('contact_list', company_id=contact['company_id']))
|
||||||
|
return render_template('contact_form.html', company={'id': contact['company_id'], 'name': contact['company_name']}, contact=contact)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/contacts/<int:id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def contact_delete(id):
|
||||||
|
db = get_db()
|
||||||
|
contact = db.execute('''SELECT c.company_id, co.user_id
|
||||||
|
FROM contacts c JOIN companies co ON c.company_id = co.id
|
||||||
|
WHERE c.id = ?''', (id,)).fetchone()
|
||||||
|
if contact and contact['user_id'] == session['user_id']:
|
||||||
|
db.execute('DELETE FROM contacts WHERE id = ?', (id,))
|
||||||
|
db.commit()
|
||||||
|
flash('Kontakt gelöscht.', 'info')
|
||||||
|
return redirect(url_for('contact_list', company_id=contact['company_id']))
|
||||||
|
flash('Kontakt nicht gefunden.', 'danger')
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_db()
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LeoCRM</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: Arial, sans-serif; background: #f5f5f5; color: #333; }
|
||||||
|
nav { background: #2c3e50; color: white; padding: 1rem; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
nav a { color: white; text-decoration: none; margin-left: 1rem; }
|
||||||
|
nav a:hover { text-decoration: underline; }
|
||||||
|
.container { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
.flash { padding: 0.75rem; margin-bottom: 1rem; border-radius: 4px; }
|
||||||
|
.flash.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.flash.danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
|
.flash.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
|
||||||
|
.flash.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
||||||
|
th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
|
||||||
|
th { background: #f8f9fa; }
|
||||||
|
.btn { display: inline-block; padding: 0.4rem 0.8rem; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 0.9rem; }
|
||||||
|
.btn-primary { background: #007bff; color: white; }
|
||||||
|
.btn-danger { background: #dc3545; color: white; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
.btn-sm { padding: 0.2rem 0.5rem; font-size: 0.8rem; }
|
||||||
|
form label { display: block; margin-top: 0.5rem; font-weight: bold; }
|
||||||
|
form input, form textarea { width: 100%; padding: 0.4rem; margin-top: 0.2rem; border: 1px solid #ccc; border-radius: 4px; }
|
||||||
|
form textarea { resize: vertical; min-height: 60px; }
|
||||||
|
form button { margin-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<span><strong>LeoCRM</strong></span>
|
||||||
|
<span>
|
||||||
|
{% if session.username %}
|
||||||
|
<span>{{ session.username }}</span>
|
||||||
|
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||||
|
<a href="{{ url_for('logout') }}">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login') }}">Login</a>
|
||||||
|
<a href="{{ url_for('register') }}">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
<div class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if company %}Firma bearbeiten{% else %}Neue Firma{% endif %}</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Name *</label>
|
||||||
|
<input type="text" name="name" value="{{ company.name if company else '' }}" required>
|
||||||
|
<label>Adresse</label>
|
||||||
|
<input type="text" name="address" value="{{ company.address if company else '' }}">
|
||||||
|
<label>Telefon</label>
|
||||||
|
<input type="text" name="phone" value="{{ company.phone if company else '' }}">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" name="email" value="{{ company.email if company else '' }}">
|
||||||
|
<label>Website</label>
|
||||||
|
<input type="text" name="website" value="{{ company.website if company else '' }}">
|
||||||
|
<label>Notizen</label>
|
||||||
|
<textarea name="notes">{{ company.notes if company else '' }}</textarea>
|
||||||
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Abbrechen</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if contact %}Kontakt bearbeiten{% else %}Neuer Kontakt{% endif %} – {{ company.name }}</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Vorname *</label>
|
||||||
|
<input type="text" name="first_name" value="{{ contact.first_name if contact else '' }}" required>
|
||||||
|
<label>Nachname *</label>
|
||||||
|
<input type="text" name="last_name" value="{{ contact.last_name if contact else '' }}" required>
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" name="email" value="{{ contact.email if contact else '' }}">
|
||||||
|
<label>Telefon</label>
|
||||||
|
<input type="text" name="phone" value="{{ contact.phone if contact else '' }}">
|
||||||
|
<label>Position</label>
|
||||||
|
<input type="text" name="position" value="{{ contact.position if contact else '' }}">
|
||||||
|
<label>Notizen</label>
|
||||||
|
<textarea name="notes">{{ contact.notes if contact else '' }}</textarea>
|
||||||
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||||
|
<a href="{{ url_for('contact_list', company_id=company.id) }}" class="btn btn-secondary">Abbrechen</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ company.name }} – Kontaktpersonen</h2>
|
||||||
|
<a href="{{ url_for('contact_create', company_id=company.id) }}" class="btn btn-primary">Neuer Kontakt</a>
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Zurück</a>
|
||||||
|
{% if contacts %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Telefon</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for c in contacts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ c.first_name }} {{ c.last_name }}</td>
|
||||||
|
<td>{{ c.position or '-' }}</td>
|
||||||
|
<td>{{ c.email or '-' }}</td>
|
||||||
|
<td>{{ c.phone or '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('contact_edit', id=c.id) }}" class="btn btn-sm btn-secondary">Edit</a>
|
||||||
|
<form method="post" action="{{ url_for('contact_delete', id=c.id) }}" style="display:inline;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Wirklich löschen?')">Del</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>Keine Kontaktpersonen erfasst.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Dashboard</h2>
|
||||||
|
<a href="{{ url_for('company_create') }}" class="btn btn-primary">Neue Firma</a>
|
||||||
|
{% if companies %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Telefon</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for c in companies %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for('contact_list', company_id=c.id) }}">{{ c.name }}</a></td>
|
||||||
|
<td>{{ c.phone or '-' }}</td>
|
||||||
|
<td>{{ c.email or '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('company_edit', id=c.id) }}" class="btn btn-sm btn-secondary">Edit</a>
|
||||||
|
<form method="post" action="{{ url_for('company_delete', id=c.id) }}" style="display:inline;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Wirklich löschen?')">Del</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>Noch keine Firmen erfasst.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Benutzername</label>
|
||||||
|
<input type="text" name="username" required>
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
<p style="margin-top:1rem;">Noch kein Konto? <a href="{{ url_for('register') }}">Registrieren</a></p>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Registrieren</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Benutzername</label>
|
||||||
|
<input type="text" name="username" required>
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
<button type="submit" class="btn btn-primary">Registrieren</button>
|
||||||
|
</form>
|
||||||
|
<p style="margin-top:1rem;">Bereits registriert? <a href="{{ url_for('login') }}">Anmelden</a></p>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# LeoCRM – Requirements
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A minimal web CRM for managing companies and their contact persons, with user authentication.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
### FR1: User Authentication
|
||||||
|
- Users can register with username and password
|
||||||
|
- Users can login/logout
|
||||||
|
- Passwords are hashed with bcrypt
|
||||||
|
- Session management via Flask-Login
|
||||||
|
|
||||||
|
### FR2: Company Management
|
||||||
|
- Authenticated users can create, view, edit, and delete companies
|
||||||
|
- Company fields: name, address, phone, email, website, notes
|
||||||
|
- List view with search/filter
|
||||||
|
|
||||||
|
### FR3: Contact Person Management
|
||||||
|
- Each contact person belongs to a company
|
||||||
|
- Fields: first_name, last_name, email, phone, position, notes
|
||||||
|
- CRUD operations, linked to company
|
||||||
|
|
||||||
|
### FR4: Web UI
|
||||||
|
- Jinja2 templates with basic styling
|
||||||
|
- Navigation between sections
|
||||||
|
- Forms with validation
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
- SQLite database (no external DB needed)
|
||||||
|
- Runs on single server (Flask built-in or gunicorn)
|
||||||
|
- Deployable via Coolify (Docker)
|
||||||
|
- Code in Git (Forgejo)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- Multi-tenancy
|
||||||
|
- REST API
|
||||||
|
- Advanced search
|
||||||
|
- File uploads
|
||||||
|
- Email notifications
|
||||||
Reference in New Issue
Block a user