Initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from django.contrib import admin
|
||||
from .models import CompanySetting
|
||||
|
||||
|
||||
@admin.register(CompanySetting)
|
||||
class CompanySettingAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"city",
|
||||
"updated_at",
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'core'
|
||||
@@ -0,0 +1,21 @@
|
||||
from projects.models import ProjectMembership
|
||||
from .models import CompanySetting
|
||||
|
||||
def project_switcher(request):
|
||||
if not request.user.is_authenticated:
|
||||
return {"user_projects": []}
|
||||
|
||||
memberships = ProjectMembership.objects.select_related("project").filter(
|
||||
user=request.user,
|
||||
project__active=True
|
||||
)
|
||||
|
||||
return {
|
||||
"user_projects": [m.project for m in memberships]
|
||||
}
|
||||
|
||||
|
||||
def company_settings(request):
|
||||
return {
|
||||
"company_settings": CompanySetting.objects.first()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class BaseForm(forms.ModelForm):
|
||||
|
||||
default_class = (
|
||||
"w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 "
|
||||
"bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white "
|
||||
"placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 "
|
||||
"focus:border-transparent transition"
|
||||
)
|
||||
|
||||
def apply_style(self):
|
||||
for field_name, field in self.fields.items():
|
||||
existing = field.widget.attrs.get("class", "")
|
||||
field.widget.attrs["class"] = f"{self.default_class} {existing}".strip()
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CompanySetting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('companyname', models.CharField(max_length=255, verbose_name='Firmenname')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Geschäftsführer')),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='company/', verbose_name='Logo')),
|
||||
('slogan', models.CharField(blank=True, max_length=255, verbose_name='Slogan')),
|
||||
('color', models.CharField(default='#0f172a', max_length=20, verbose_name='Farbe')),
|
||||
('street', models.CharField(max_length=255, verbose_name='Straße')),
|
||||
('zip', models.CharField(max_length=20, verbose_name='PLZ')),
|
||||
('city', models.CharField(max_length=255, verbose_name='Ort')),
|
||||
('country', models.CharField(default='Deutschland', max_length=100, verbose_name='Land')),
|
||||
('phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon')),
|
||||
('email', models.EmailField(blank=True, max_length=100, verbose_name='E-Mail')),
|
||||
('website', models.URLField(blank=True, max_length=100, verbose_name='Webseite')),
|
||||
('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer')),
|
||||
('vat_number', models.CharField(blank=True, max_length=100, verbose_name='USt-IdNr.')),
|
||||
('is_small_business', models.BooleanField(default=False, verbose_name='Kleinunternehmer (§19 UStG)')),
|
||||
('small_business_text', models.TextField(blank=True, verbose_name='Kleinunternehmer Hinweis')),
|
||||
('invoice_header_text', models.TextField(blank=True, verbose_name='Rechnungszusatztext')),
|
||||
('bank_name', models.CharField(blank=True, max_length=255, verbose_name='Bank')),
|
||||
('iban', models.CharField(blank=True, max_length=50, verbose_name='IBAN')),
|
||||
('bic', models.CharField(blank=True, max_length=50, verbose_name='BIC')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Firmeneinstellung',
|
||||
'verbose_name_plural': 'Firmeneinstellungen',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-24 09:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='companysetting',
|
||||
name='invoice_number',
|
||||
field=models.CharField(blank=True, max_length=50, verbose_name='BIC'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DocumentNumberSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('invoice_prefix', models.CharField(default='RE', max_length=20)),
|
||||
('invoice_next_number', models.PositiveIntegerField(default=1)),
|
||||
('cancel_prefix', models.CharField(default='ST', max_length=20)),
|
||||
('cancel_next_number', models.PositiveIntegerField(default=1)),
|
||||
('offer_prefix', models.CharField(default='ANG', max_length=20)),
|
||||
('offer_next_number', models.PositiveIntegerField(default=1)),
|
||||
('company', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-24 09:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_companysetting_invoice_number_documentnumbersettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='DocumentNumberSettings',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-25 14:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_delete_documentnumbersettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='companysetting',
|
||||
name='offer_header_text',
|
||||
field=models.TextField(blank=True, verbose_name='Angebotszusatztext'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-26 06:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_companysetting_offer_header_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='companysetting',
|
||||
name='invoice_number',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-27 08:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_companysetting_invoice_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='companysetting',
|
||||
name='reminder_header_text',
|
||||
field=models.TextField(blank=True, verbose_name='Mahnungszusatztext'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CompanySetting(models.Model):
|
||||
companyname = models.CharField(max_length=255,verbose_name="Firmenname")
|
||||
name = models.CharField(max_length=255,verbose_name="Geschäftsführer")
|
||||
logo = models.ImageField(upload_to="company/",blank=True,null=True,verbose_name="Logo")
|
||||
slogan = models.CharField(max_length=255,blank=True,verbose_name="Slogan")
|
||||
color = models.CharField(max_length=20,default="#0f172a",verbose_name="Farbe")
|
||||
street = models.CharField(max_length=255,verbose_name="Straße")
|
||||
zip = models.CharField(max_length=20,verbose_name="PLZ")
|
||||
city = models.CharField(max_length=255,verbose_name="Ort")
|
||||
country = models.CharField(max_length=100,default="Deutschland",verbose_name="Land")
|
||||
phone = models.CharField(max_length=100,blank=True,verbose_name="Telefon" )
|
||||
email = models.EmailField(max_length=100,blank=True,verbose_name="E-Mail")
|
||||
website = models.URLField(max_length=100,blank=True,verbose_name="Webseite")
|
||||
|
||||
tax_number = models.CharField(max_length=100,blank=True,verbose_name="Steuernummer")
|
||||
vat_number = models.CharField(max_length=100,blank=True,verbose_name="USt-IdNr.")
|
||||
|
||||
is_small_business = models.BooleanField(default=False,verbose_name="Kleinunternehmer (§19 UStG)")
|
||||
small_business_text = models.TextField(blank=True,verbose_name="Kleinunternehmer Hinweis")
|
||||
invoice_header_text = models.TextField(blank=True,verbose_name="Rechnungszusatztext")
|
||||
offer_header_text = models.TextField(blank=True,verbose_name="Angebotszusatztext")
|
||||
reminder_header_text = models.TextField(blank=True,verbose_name="Mahnungszusatztext")
|
||||
|
||||
bank_name = models.CharField(max_length=255,blank=True,verbose_name="Bank")
|
||||
iban = models.CharField(max_length=50,blank=True,verbose_name="IBAN")
|
||||
bic = models.CharField(max_length=50,blank=True,verbose_name="BIC")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Firmeneinstellung"
|
||||
verbose_name_plural = "Firmeneinstellungen"
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from masterdata.registry import register_setting
|
||||
from .models import CompanySetting
|
||||
|
||||
|
||||
register_setting(
|
||||
group="System",
|
||||
name="Firmeneinstellungen",
|
||||
model=CompanySetting,
|
||||
setting_type="single",
|
||||
)
|
||||
@@ -0,0 +1,342 @@
|
||||
{% load static tailwind_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>
|
||||
{% block title %}
|
||||
Projektverwaltung
|
||||
{% endblock %}
|
||||
</title>
|
||||
<script>
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
</script>
|
||||
{% tailwind_css %}
|
||||
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--company-color: {{ company_settings.color }};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-100 dark:bg-slate-900 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex min-h-screen">
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div id="overlay"
|
||||
class="fixed inset-0 bg-black/40 hidden z-30 lg:hidden"
|
||||
onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside id="sidebar" class="fixed top-0 z-40 w-72 h-screen bg-slate-800 text-white transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col border-r border-white/20">
|
||||
|
||||
<div class="p-6 border-b border-white/20 " style="background-color: {{ company_settings.color }};">
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ company_settings.companyname|default:"Projektverwaltung" }}
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 p-4 space-y-2">
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'core:dashboard' %}">
|
||||
Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
|
||||
<!-- Projekt wechseln -->
|
||||
<div class="rounded-lg overflow-hidden">
|
||||
|
||||
<button
|
||||
onclick="toggleProjects()"
|
||||
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
|
||||
<span>{{ current_project.name|default:"Projekt wählen" }}</span>
|
||||
|
||||
<svg id="projectArrow"
|
||||
class="w-4 h-4 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
|
||||
<div id="projectMenu" class="hidden border-t border-slate-700">
|
||||
|
||||
{% for p in user_projects %}
|
||||
<a href="{% url 'projects:overview' p.id %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700">
|
||||
{{ p.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rechnungswesen -->
|
||||
<div class="rounded-lg overflow-hidden">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick="toggleInvoicesMenu()"
|
||||
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700 transition">
|
||||
|
||||
<span>
|
||||
Rechnungswesen
|
||||
</span>
|
||||
|
||||
<svg id="invoiceArrow"
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
|
||||
<div id="invoiceMenu" class="hidden border-t border-slate-700">
|
||||
|
||||
<a href="{% url 'invoices:overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Übersicht
|
||||
</a>
|
||||
|
||||
<a href="{% url 'invoices:invoices_offers_overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Angebote
|
||||
</a>
|
||||
|
||||
<a href="{% url 'invoices:invoices_invoices_overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Rechnungen
|
||||
</a>
|
||||
|
||||
<a href="{% url 'invoices:invoices_reminders_overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Mahnungen
|
||||
</a>
|
||||
|
||||
<a href="{% url 'invoices:customers_overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Kunden
|
||||
</a>
|
||||
|
||||
<a href="{% url 'invoices:items_overview' %}"
|
||||
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||
Posten
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'core:dashboard' %}">
|
||||
Finanzübersicht
|
||||
</a>
|
||||
</div>
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'core:dashboard' %}">
|
||||
Arbeitszeiten
|
||||
</a>
|
||||
</div>
|
||||
-->
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'inventory_global:overview' %}">
|
||||
Inventar
|
||||
</a>
|
||||
</div>
|
||||
<!--
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'core:dashboard' %}">
|
||||
Investitionen
|
||||
</a>
|
||||
</div>
|
||||
-->
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'accounts:overview' %}">
|
||||
Mitarbeiter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||
<a href="{% url 'masterdata:overview' %}">
|
||||
Einstellungen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
<div class="p-4 border-t border-white/20">
|
||||
{% if user.is_superuser %}
|
||||
<div class="w-full rounded-xl flex items-center justify-between px-4 py-3 hover:bg-slate-700 mb-4 border-white/20 border">
|
||||
<a href="{% url 'admin:index' %}">Admin</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex gap-2 w-full">
|
||||
<button id="theme-toggle" class="rounded-xl px-4 py-3 hover:bg-slate-700 border-white/20 border">🌙</button>
|
||||
<form method="post" action="{% url 'accounts:logout' %}" class="w-full">
|
||||
{% csrf_token %}
|
||||
<button class="w-full rounded-xl px-4 py-3 border-white/20 border" style="background-color: {{ company_settings.color }};">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hauptbereich -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex-1 flex flex-col lg:ml-72">
|
||||
{% else %}
|
||||
<div class="flex-1 flex flex-col">
|
||||
{% endif %}
|
||||
<header class="lg:hidden bg-white shadow h-16 flex items-center px-4 dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<button onclick="toggleSidebar()" class="text-3xl">☰</button>
|
||||
<span class="ml-4 font-bold">{{ company_settings.companyname|default:"Projektverwaltung" }}</span>
|
||||
</header>
|
||||
<main class="flex-1 p-6 overflow-x-auto w-screen lg:w-auto">
|
||||
<div class="p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<h1 class="text-3xl font-bold">
|
||||
{% block header_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
<p class="text-slate-400 mt-1">
|
||||
{% block header_description %}
|
||||
{% endblock %}
|
||||
</p>
|
||||
</div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if messages %}
|
||||
<div class="fixed top-5 right-5 z-50 space-y-3">
|
||||
{% for message in messages %}
|
||||
<div
|
||||
class="rounded-xl shadow-lg px-5 py-4 flex items-center gap-4
|
||||
{% if message.tags == 'success' %}
|
||||
bg-green-100 border border-green-300 text-green-800
|
||||
{% elif message.tags == 'error' %}
|
||||
bg-red-100 border border-red-300 text-red-800
|
||||
{% elif message.tags == 'warning' %}
|
||||
bg-yellow-100 border border-yellow-300 text-yellow-800
|
||||
{% else %}
|
||||
bg-blue-100 border border-blue-300 text-blue-800
|
||||
{% endif %}
|
||||
"
|
||||
>
|
||||
<div>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
document
|
||||
.querySelectorAll(".fixed.top-5.right-5 > div")
|
||||
.forEach(el => {
|
||||
|
||||
el.style.opacity = "0";
|
||||
|
||||
setTimeout(() => {
|
||||
el.remove();
|
||||
}, 500);
|
||||
|
||||
});
|
||||
|
||||
}, 3000);
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
function toggleProjects() {
|
||||
|
||||
const menu = document.getElementById("projectMenu");
|
||||
const arrow = document.getElementById("projectArrow");
|
||||
|
||||
menu.classList.toggle("hidden");
|
||||
arrow.classList.toggle("rotate-180");
|
||||
|
||||
}
|
||||
|
||||
function toggleInvoicesMenu() {
|
||||
|
||||
const menu = document.getElementById("invoiceMenu");
|
||||
const arrow = document.getElementById("invoiceArrow");
|
||||
|
||||
menu.classList.toggle("hidden");
|
||||
arrow.classList.toggle("rotate-180");
|
||||
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const overlay = document.getElementById("overlay");
|
||||
|
||||
sidebar.classList.toggle("-translate-x-full");
|
||||
overlay.classList.toggle("hidden");
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const btn = document.getElementById("theme-toggle");
|
||||
const html = document.documentElement;
|
||||
|
||||
// Gespeichertes Theme anwenden
|
||||
const saved = localStorage.getItem("theme");
|
||||
if (saved === "dark") {
|
||||
html.classList.add("dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
|
||||
// Toggle
|
||||
btn.addEventListener("click", () => {
|
||||
html.classList.toggle("dark");
|
||||
localStorage.setItem("theme", html.classList.contains("dark") ? "dark" : "light");
|
||||
btn.textContent = html.classList.contains("dark") ? "☀️" : "🌙";
|
||||
});
|
||||
|
||||
// Icon initial setzen
|
||||
btn.textContent = html.classList.contains("dark") ? "☀️" : "🌙";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block header_title %}
|
||||
Dashboard
|
||||
{% endblock %}
|
||||
|
||||
{% block header_description %}
|
||||
Willkommen {{ user.username }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-8">
|
||||
<a href="{% url 'invoices:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Rechnungswesen</h2>
|
||||
</div>
|
||||
</a>
|
||||
<!--
|
||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Finanzübersicht</h2>
|
||||
</div>
|
||||
</a>
|
||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Arbeitszeiten</h2>
|
||||
</div>
|
||||
</a> -->
|
||||
<a href="{% url 'inventory_global:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Inventar</h2>
|
||||
</div>
|
||||
</a>
|
||||
<!--
|
||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Investitionen</h2>
|
||||
</div>
|
||||
</a>
|
||||
-->
|
||||
<a href="{% url 'accounts:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Mitarbeiter</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'masterdata:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<h2 class="text-xl font-bold">Einstellungen</h2>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<a href="{% url 'projects:add' %}" class="btn btn-add">+ Projekt</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if memberships %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for m in memberships %}
|
||||
<a href="{% url 'projects:overview' m.project.id %}"
|
||||
class="p-6 rounded-xl shadow hover:shadow-lg transition block text-white"
|
||||
style="background: linear-gradient(135deg, {{ m.project.color }}, {{ m.project.color }}ff);">
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ m.project.name }}
|
||||
</h2>
|
||||
<p class="text-sm mt-2">
|
||||
Rolle: {{ m.get_role_display }}
|
||||
</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||
Du bist noch keinem aktiven Projekt zugewiesen.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if archived_count %}
|
||||
<a href="{% url 'projects:archived' %}" class="text-sm text-slate-400 hover:text-slate-500">
|
||||
{{ archived_count }} archivierte
|
||||
{% if archived_count == 1 %}
|
||||
Projekt anzeigen
|
||||
{% else %}
|
||||
Projekte anzeigen
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% load django_tables2 %}
|
||||
|
||||
<table {% render_attrs table.attrs %}>
|
||||
|
||||
<thead class="bg-slate-100 dark:bg-slate-700 text-left">
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
<th class="p-3">
|
||||
{{ column.header }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
|
||||
{% for row in table.paginated_rows %}
|
||||
|
||||
<tr class="border-t">
|
||||
|
||||
{% for column, cell in row.items %}
|
||||
|
||||
<td class="p-3">
|
||||
{{ cell }}
|
||||
</td>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tr>
|
||||
|
||||
{% empty %}
|
||||
|
||||
<tr class="border-t">
|
||||
<td colspan="{{ table.columns|length }}"
|
||||
class="p-6 text-center text-slate-400">
|
||||
|
||||
Keine Daten vorhanden
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
@@ -0,0 +1,95 @@
|
||||
{% load django_tables2 %}
|
||||
|
||||
<div class="overflow-x-auto rounded-xl">
|
||||
|
||||
<table {% render_attrs table.attrs %}>
|
||||
|
||||
<thead class="bg-slate-100 dark:bg-slate-700 text-left">
|
||||
<tr>
|
||||
<th class="p-3"></th>
|
||||
|
||||
{% for column in table.columns %}
|
||||
<th class="p-3">
|
||||
{{ column.header }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
|
||||
{% for row in table.paginated_rows %}
|
||||
|
||||
<tr class="border-t">
|
||||
|
||||
<td class="p-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick="toggleItem('{{ table.toggle_prefix }}-{{ row.record.id }}')"
|
||||
class="font-bold">
|
||||
▼
|
||||
</button>
|
||||
</td>
|
||||
|
||||
|
||||
{% for column, cell in row.items %}
|
||||
|
||||
<td class="p-3">
|
||||
{{ cell }}
|
||||
</td>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
{% if table.detail_template %}
|
||||
|
||||
<tr id="{{ table.toggle_prefix }}-{{ row.record.id }}"
|
||||
class="hidden border-t">
|
||||
|
||||
<td colspan="{{ table.columns|length|add:'1' }}"
|
||||
class="p-6">
|
||||
|
||||
{% include table.detail_template with item=row.record categories=table.categories locations=table.locations users=table.users %}
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% empty %}
|
||||
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length|add:1 }}"
|
||||
class="p-6 text-center text-slate-400">
|
||||
Keine Daten vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function toggleItem(id) {
|
||||
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if (!element) {
|
||||
console.log("Nicht gefunden:", id);
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.toggle("hidden");
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from .views import dashboard
|
||||
|
||||
app_name = "core"
|
||||
|
||||
urlpatterns = [
|
||||
path("", dashboard, name="dashboard"),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from projects.models import ProjectMembership
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
memberships = ProjectMembership.objects.select_related("project").filter(user=request.user,project__active=True)
|
||||
archived_count = ProjectMembership.objects.filter(user=request.user,project__active=False).count()
|
||||
return render(request,"core/dashboard.html",{"memberships": memberships,"archived_count": archived_count,})
|
||||
Reference in New Issue
Block a user