API updadte

This commit is contained in:
2025-11-09 11:12:48 +01:00
parent 7548e241be
commit 77206224a2
14 changed files with 1048 additions and 57 deletions

View File

@@ -37,6 +37,13 @@ security:
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
default_target_path: app_home
logout:
path: app_logout
target: app_home
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used

View File

@@ -15,6 +15,20 @@ services:
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Repository/'
- '../src/Kernel.php'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
# Explicitly register repositories
App\Repository\:
resource: '../src/Repository/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

96
public/css/auth.css Normal file
View File

@@ -0,0 +1,96 @@
.auth-container {
max-width: 450px;
margin: 50px auto;
}
.auth-box {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.auth-box h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid #4CAF50;
}
.auth-form .form-group {
margin-bottom: 20px;
}
.auth-form label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.auth-form input[type="text"],
.auth-form input[type="email"],
.auth-form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.auth-form input:focus {
outline: none;
border-color: #4CAF50;
}
.auth-form .btn-submit {
width: 100%;
padding: 14px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
}
.auth-form .btn-submit:hover {
background-color: #45a049;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #c62828;
}
.success-message {
background-color: #e8f5e9;
color: #2e7d32;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #2e7d32;
}
.auth-links {
text-align: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.auth-links a {
color: #4CAF50;
text-decoration: none;
}
.auth-links a:hover {
text-decoration: underline;
}

146
public/css/calculator.css Normal file
View File

@@ -0,0 +1,146 @@
.calculator-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.form-section, .results-section {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-section h2, .results-section h2 {
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4CAF50;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
.result-item {
padding: 15px;
margin-bottom: 15px;
background-color: #f8f9fa;
border-left: 4px solid #4CAF50;
border-radius: 4px;
}
.result-item h3 {
margin: 0 0 5px 0;
color: #333;
font-size: 16px;
}
.result-item .value {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
}
.result-item .description {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-primary:hover {
background-color: #45a049;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.share-link {
margin-top: 15px;
padding: 10px;
background-color: #e8f5e9;
border-radius: 4px;
display: none;
}
.share-link input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 5px;
}
.info-banner {
background-color: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #2196F3;
}
@media (max-width: 768px) {
.calculator-container {
grid-template-columns: 1fr;
}
}

112
public/js/calculator.js Normal file
View File

@@ -0,0 +1,112 @@
$(document).ready(function() {
// Live calculation function
function calculate() {
const kaufpreis = parseFloat($('#kaufpreis').val()) || 0;
const wohnflaeche = parseFloat($('#wohnflaeche').val()) || 0;
const nutzflaeche = parseFloat($('#nutzflaeche').val()) || 0;
const baujahr = parseInt($('#baujahr').val()) || 0;
const abschreibungszeit = parseFloat($('#abschreibungszeit').val()) || 50;
const bundeslandSteuer = parseFloat($('#bundesland_id option:selected').data('steuer')) || 0;
// Gesamtfläche
const gesamtflaeche = wohnflaeche + nutzflaeche;
$('#result-gesamtflaeche').text(gesamtflaeche.toLocaleString('de-DE') + ' m²');
// Preis pro m²
const preisProQm = wohnflaeche > 0 ? kaufpreis / wohnflaeche : 0;
$('#result-preis-pro-qm').text(preisProQm.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
// Grunderwerbsteuer
const grunderwerbsteuer = kaufpreis * (bundeslandSteuer / 100);
$('#result-grunderwerbsteuer').text(grunderwerbsteuer.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
// Gesamtkosten
const gesamtkosten = kaufpreis + grunderwerbsteuer;
$('#result-gesamtkosten').text(gesamtkosten.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
// Jährliche Abschreibung
const abschreibung = abschreibungszeit > 0 ? kaufpreis / abschreibungszeit : 0;
$('#result-abschreibung').text(abschreibung.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
// Alter der Immobilie
const currentYear = new Date().getFullYear();
const alter = baujahr > 0 ? currentYear - baujahr : 0;
$('#result-alter').text(alter + ' Jahre');
}
// Trigger calculation on any input change
$('#immo-calculator-form input, #immo-calculator-form select').on('input change', function() {
calculate();
});
// Generate shareable link
$('#share-link-btn').click(function() {
const formData = $('#immo-calculator-form').serializeArray();
const params = new URLSearchParams();
formData.forEach(item => {
if (item.value) {
params.append(item.name, item.value);
}
});
const shareUrl = window.location.origin + window.location.pathname + '?' + params.toString();
$('#share-url').val(shareUrl);
$('#share-link-container').slideDown();
});
// Copy link to clipboard
$('#copy-link-btn').click(function() {
const shareUrl = $('#share-url');
shareUrl.select();
document.execCommand('copy');
alert('Link wurde in die Zwischenablage kopiert!');
});
// Reset form
$('#reset-btn').click(function() {
$('#immo-calculator-form')[0].reset();
$('#share-link-container').slideUp();
calculate();
});
// Save immobilie (for logged in users)
$('#save-immobilie-btn').click(function() {
const formData = $('#immo-calculator-form').serializeArray();
const data = {};
formData.forEach(item => {
if (item.name === 'garage') {
data[item.name] = $('#garage').is(':checked');
} else {
data[item.name] = item.value;
}
});
$.ajax({
url: '/immobilie/save',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(response) {
if (response.success) {
alert(response.message + '\n\nSie können Ihre gespeicherten Immobilien unter "Meine Immobilien" einsehen.');
} else {
alert('Fehler: ' + response.message);
}
},
error: function(xhr) {
if (xhr.status === 401) {
alert('Sie müssen angemeldet sein, um Immobilien zu speichern.');
window.location.href = '/login';
} else {
const response = xhr.responseJSON;
alert('Fehler: ' + (response ? response.message : 'Unbekannter Fehler'));
}
}
});
});
// Initial calculation on page load
calculate();
});

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class AuthController extends AbstractController
{
#[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// Redirect if already logged in
if ($this->getUser()) {
return $this->redirectToRoute('app_home');
}
// Get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// Last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('auth/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route('/register', name: 'app_register')]
public function register(
Request $request,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $entityManager,
UserRepository $userRepository
): Response {
// Redirect if already logged in
if ($this->getUser()) {
return $this->redirectToRoute('app_home');
}
$error = null;
if ($request->isMethod('POST')) {
$name = $request->request->get('name');
$email = $request->request->get('email');
$password = $request->request->get('password');
$passwordConfirm = $request->request->get('password_confirm');
// Validation
if (empty($name) || empty($email) || empty($password)) {
$error = 'Bitte füllen Sie alle Felder aus.';
} elseif ($password !== $passwordConfirm) {
$error = 'Die Passwörter stimmen nicht überein.';
} elseif (strlen($password) < 6) {
$error = 'Das Passwort muss mindestens 6 Zeichen lang sein.';
} elseif ($userRepository->findOneBy(['email' => $email])) {
$error = 'Diese E-Mail-Adresse ist bereits registriert.';
} else {
// Create new user
$user = new User();
$user->setName($name);
$user->setEmail($email);
// Hash the password
$hashedPassword = $passwordHasher->hashPassword($user, $password);
$user->setPassword($hashedPassword);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'Registrierung erfolgreich! Sie können sich jetzt anmelden.');
return $this->redirectToRoute('app_login');
}
}
return $this->render('auth/register.html.twig', [
'error' => $error,
]);
}
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
// This method can be blank - it will be intercepted by the logout key on your firewall
throw new \Exception('This should never be reached!');
}
}

View File

@@ -2,17 +2,45 @@
namespace App\Controller;
use App\Repository\BundeslandRepository;
use App\Repository\HeizungstypRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function index(): Response
public function index(
Request $request,
BundeslandRepository $bundeslandRepository,
HeizungstypRepository $heizungstypRepository
): Response
{
$bundeslaender = $bundeslandRepository->findAll();
$heizungstypen = $heizungstypRepository->findAll();
// Load data from URL parameters if present
$immobilienData = [
'adresse' => $request->query->get('adresse', ''),
'kaufpreis' => $request->query->get('kaufpreis', ''),
'wohnflaeche' => $request->query->get('wohnflaeche', ''),
'nutzflaeche' => $request->query->get('nutzflaeche', ''),
'zimmer' => $request->query->get('zimmer', ''),
'baujahr' => $request->query->get('baujahr', ''),
'garage' => $request->query->get('garage', false),
'etage' => $request->query->get('etage', ''),
'typ' => $request->query->get('typ', ''),
'bundesland_id' => $request->query->get('bundesland_id', ''),
'heizungstyp_id' => $request->query->get('heizungstyp_id', ''),
'abschreibungszeit' => $request->query->get('abschreibungszeit', '50'),
];
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
'bundeslaender' => $bundeslaender,
'heizungstypen' => $heizungstypen,
'immobilienData' => $immobilienData,
]);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Controller;
use App\Entity\Immobilie;
use App\Enum\ImmobilienTyp;
use App\Repository\BundeslandRepository;
use App\Repository\HeizungstypRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ImmobilienSaveController extends AbstractController
{
#[Route('/immobilie/save', name: 'app_immobilie_save', methods: ['POST'])]
public function save(
Request $request,
EntityManagerInterface $entityManager,
BundeslandRepository $bundeslandRepository,
HeizungstypRepository $heizungstypRepository
): JsonResponse {
// Check if user is logged in
if (!$this->getUser()) {
return new JsonResponse([
'success' => false,
'message' => 'Sie müssen angemeldet sein, um Immobilien zu speichern.',
], 401);
}
$data = json_decode($request->getContent(), true);
// Validation
if (empty($data['adresse']) || empty($data['kaufpreis']) || empty($data['wohnflaeche'])) {
return new JsonResponse([
'success' => false,
'message' => 'Bitte füllen Sie mindestens Adresse, Kaufpreis und Wohnfläche aus.',
], 400);
}
// Create new Immobilie
$immobilie = new Immobilie();
$immobilie->setVerwalter($this->getUser());
$immobilie->setAdresse($data['adresse']);
$immobilie->setKaufpreis((int) $data['kaufpreis']);
$immobilie->setWohnflaeche((int) $data['wohnflaeche']);
$immobilie->setNutzflaeche((int) ($data['nutzflaeche'] ?? 0));
$immobilie->setZimmer((int) ($data['zimmer'] ?? 0));
// Set Typ from string to enum
$typ = ImmobilienTyp::WOHNUNG; // default
if (!empty($data['typ'])) {
$typ = match(strtolower($data['typ'])) {
'haus' => ImmobilienTyp::HAUS,
'gewerbe' => ImmobilienTyp::GEWERBE,
'grundstück', 'grundstueck' => ImmobilienTyp::GRUNDSTUECK,
'büro', 'buero' => ImmobilienTyp::BUERO,
default => ImmobilienTyp::WOHNUNG,
};
}
$immobilie->setTyp($typ);
$immobilie->setGarage((bool) ($data['garage'] ?? false));
if (!empty($data['baujahr'])) {
$immobilie->setBaujahr((int) $data['baujahr']);
}
if (!empty($data['etage'])) {
$immobilie->setEtage((int) $data['etage']);
}
if (!empty($data['abschreibungszeit'])) {
$immobilie->setAbschreibungszeit((int) $data['abschreibungszeit']);
}
if (!empty($data['beschreibung'])) {
$immobilie->setBeschreibung($data['beschreibung']);
}
// Set Bundesland
if (!empty($data['bundesland_id'])) {
$bundesland = $bundeslandRepository->find($data['bundesland_id']);
if ($bundesland) {
$immobilie->setBundesland($bundesland);
}
}
// Set Heizungstyp
if (!empty($data['heizungstyp_id'])) {
$heizungstyp = $heizungstypRepository->find($data['heizungstyp_id']);
if ($heizungstyp) {
$immobilie->setHeizungstyp($heizungstyp);
}
}
$entityManager->persist($immobilie);
$entityManager->flush();
return new JsonResponse([
'success' => true,
'message' => 'Immobilie erfolgreich gespeichert!',
'id' => $immobilie->getId(),
]);
}
#[Route('/meine-immobilien', name: 'app_my_immobilien')]
public function myImmobilien(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$immobilien = $this->getUser()->getImmobilien();
return $this->render('immobilie/my_immobilien.html.twig', [
'immobilien' => $immobilien,
]);
}
}

View File

@@ -50,6 +50,9 @@ class User implements UserInterface
#[ORM\Column(type: 'string', length: 64, unique: true)]
private string $apiKey;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $password = null;
#[ORM\Column(type: 'datetime')]
private \DateTimeInterface $createdAt;
@@ -165,6 +168,18 @@ class User implements UserInterface
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): self
{
$this->password = $password;
return $this;
}
/**
* UserInterface Methods.
*/
@@ -190,6 +205,6 @@ class User implements UserInterface
public function eraseCredentials(): void
{
// Nothing to erase as we use API keys
// Nothing to erase
}
}

View File

@@ -0,0 +1,46 @@
{% extends 'base.html.twig' %}
{% block title %}Login - {{ parent() }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/auth.css') }}">
{% endblock %}
{% block body %}
<div class="auth-container">
<div class="auth-box">
<h2>Anmelden</h2>
{% for message in app.flashes('success') %}
<div class="success-message">{{ message }}</div>
{% endfor %}
{% if error %}
<div class="error-message">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
<form class="auth-form" method="post" action="{{ path('app_login') }}">
<div class="form-group">
<label for="username">E-Mail</label>
<input type="email" id="username" name="_username" value="{{ last_username }}" required autofocus>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="_password" required>
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<button class="btn-submit" type="submit">Anmelden</button>
</form>
<div class="auth-links">
<p>Noch kein Konto? <a href="{{ path('app_register') }}">Jetzt registrieren</a></p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends 'base.html.twig' %}
{% block title %}Registrierung - {{ parent() }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/auth.css') }}">
{% endblock %}
{% block body %}
<div class="auth-container">
<div class="auth-box">
<h2>Registrieren</h2>
{% if error %}
<div class="error-message">{{ error }}</div>
{% endif %}
<form class="auth-form" method="post" action="{{ path('app_register') }}">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required autofocus>
</div>
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Passwort (min. 6 Zeichen)</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="password_confirm">Passwort bestätigen</label>
<input type="password" id="password_confirm" name="password_confirm" required>
</div>
<button class="btn-submit" type="submit">Registrieren</button>
</form>
<div class="auth-links">
<p>Bereits registriert? <a href="{{ path('app_login') }}">Jetzt anmelden</a></p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -48,7 +48,19 @@
<body>
<header>
<div class="container">
<h1>Immorechner</h1>
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1><a href="{{ path('app_home') }}" style="color: white; text-decoration: none;">Immorechner</a></h1>
<nav>
{% if app.user %}
<span style="color: white; margin-right: 15px;">Hallo, {{ app.user.name }}!</span>
<a href="{{ path('app_my_immobilien') }}" style="color: white; text-decoration: none; margin-right: 10px;">Meine Immobilien</a>
<a href="{{ path('app_logout') }}" style="color: white; text-decoration: none; padding: 8px 16px; background-color: rgba(255,255,255,0.2); border-radius: 4px;">Abmelden</a>
{% else %}
<a href="{{ path('app_login') }}" style="color: white; text-decoration: none; margin-right: 10px;">Anmelden</a>
<a href="{{ path('app_register') }}" style="color: white; text-decoration: none; padding: 8px 16px; background-color: rgba(255,255,255,0.2); border-radius: 4px;">Registrieren</a>
{% endif %}
</nav>
</div>
</div>
</header>

View File

@@ -1,67 +1,167 @@
{% extends 'base.html.twig' %}
{% block title %}Willkommen - {{ parent() }}{% endblock %}
{% block title %}Immobilienwert berechnen - {{ parent() }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.welcome-box {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
margin-bottom: 20px;
}
.info {
margin: 20px 0;
padding: 15px;
background-color: #e8f5e9;
border-left: 4px solid #4CAF50;
}
.api-link {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 4px;
}
.api-link:hover {
background-color: #45a049;
}
ul {
margin-left: 20px;
}
ul li {
margin: 8px 0;
}
</style>
<link rel="stylesheet" href="{{ asset('css/calculator.css') }}">
{% endblock %}
{% block body %}
<div class="welcome-box">
<h2>Willkommen bei Immorechner</h2>
<div class="info">
<p><strong>Symfony-Anwendung erfolgreich installiert!</strong></p>
<p>Diese Anwendung verfügt über:</p>
<ul>
<li>Symfony 7.3 Framework</li>
<li>MariaDB Datenbank (mit Docker)</li>
<li>Doctrine ORM</li>
<li>API Platform für REST-API</li>
<li>Twig Template Engine für UI</li>
</ul>
<div class="info-banner">
<h2 style="margin-bottom: 10px; border: none;">Immobilienwert-Rechner</h2>
<p>Berechnen Sie die Werthaltigkeit Ihrer Immobilie. Alle Berechnungen erfolgen in Echtzeit.</p>
<p><strong>Ohne Registrierung:</strong> Teilen Sie Ihre Berechnung über einen Link.</p>
<p><strong>Mit Registrierung:</strong> Speichern Sie Ihre Immobilien dauerhaft.</p>
</div>
<p>Die REST-API ist über API Platform verfügbar und bietet automatische Dokumentation.</p>
<div class="calculator-container">
<div class="form-section">
<h2>Immobiliendaten</h2>
<a href="/api" class="api-link">Zur API-Dokumentation</a>
<form id="immo-calculator-form">
<div class="form-group">
<label for="adresse">Adresse</label>
<input type="text" id="adresse" name="adresse" value="{{ immobilienData.adresse }}" placeholder="z.B. Musterstraße 123, 12345 Berlin">
</div>
<div class="form-group">
<label for="kaufpreis">Kaufpreis (€)</label>
<input type="number" id="kaufpreis" name="kaufpreis" value="{{ immobilienData.kaufpreis }}" placeholder="z.B. 250000">
</div>
<div class="form-group">
<label for="wohnflaeche">Wohnfläche (m²)</label>
<input type="number" id="wohnflaeche" name="wohnflaeche" value="{{ immobilienData.wohnflaeche }}" placeholder="z.B. 80">
</div>
<div class="form-group">
<label for="nutzflaeche">Nutzfläche (m²)</label>
<input type="number" id="nutzflaeche" name="nutzflaeche" value="{{ immobilienData.nutzflaeche }}" placeholder="z.B. 100">
</div>
<div class="form-group">
<label for="zimmer">Anzahl Zimmer</label>
<input type="number" id="zimmer" name="zimmer" value="{{ immobilienData.zimmer }}" placeholder="z.B. 3">
</div>
<div class="form-group">
<label for="baujahr">Baujahr</label>
<input type="number" id="baujahr" name="baujahr" value="{{ immobilienData.baujahr }}" placeholder="z.B. 2015">
</div>
<div class="form-group">
<label for="etage">Etage</label>
<input type="number" id="etage" name="etage" value="{{ immobilienData.etage }}" placeholder="z.B. 2">
</div>
<div class="form-group">
<label for="typ">Immobilientyp</label>
<select id="typ" name="typ">
<option value="">-- Bitte wählen --</option>
<option value="Wohnung" {% if immobilienData.typ == 'Wohnung' %}selected{% endif %}>Wohnung</option>
<option value="Haus" {% if immobilienData.typ == 'Haus' %}selected{% endif %}>Haus</option>
<option value="Gewerbe" {% if immobilienData.typ == 'Gewerbe' %}selected{% endif %}>Gewerbe</option>
</select>
</div>
<div class="form-group">
<label for="bundesland_id">Bundesland</label>
<select id="bundesland_id" name="bundesland_id">
<option value="">-- Bitte wählen --</option>
{% for bundesland in bundeslaender %}
<option value="{{ bundesland.id }}"
data-steuer="{{ bundesland.grunderwerbsteuer }}"
{% if immobilienData.bundesland_id == bundesland.id %}selected{% endif %}>
{{ bundesland.name }} ({{ bundesland.grunderwerbsteuer }}%)
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="heizungstyp_id">Heizungstyp</label>
<select id="heizungstyp_id" name="heizungstyp_id">
<option value="">-- Bitte wählen --</option>
{% for heizungstyp in heizungstypen %}
<option value="{{ heizungstyp.id }}"
{% if immobilienData.heizungstyp_id == heizungstyp.id %}selected{% endif %}>
{{ heizungstyp.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="abschreibungszeit">Abschreibungszeit (Jahre)</label>
<input type="number" id="abschreibungszeit" name="abschreibungszeit" value="{{ immobilienData.abschreibungszeit }}" placeholder="z.B. 50">
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="garage" name="garage" {% if immobilienData.garage %}checked{% endif %}>
<label for="garage">Garage vorhanden</label>
</div>
<div class="button-group">
{% if app.user %}
<button type="button" class="btn btn-primary" id="save-immobilie-btn">Speichern</button>
{% endif %}
<button type="button" class="btn btn-primary" id="share-link-btn">Link teilen</button>
<button type="button" class="btn btn-secondary" id="reset-btn">Zurücksetzen</button>
</div>
<div class="share-link" id="share-link-container">
<strong>Teilen Sie diese Berechnung:</strong>
<input type="text" id="share-url" readonly>
<button type="button" class="btn btn-secondary" style="margin-top: 10px;" id="copy-link-btn">Link kopieren</button>
</div>
</form>
</div>
<div class="results-section">
<h2>Berechnungsergebnisse</h2>
<div class="result-item">
<h3>Gesamtfläche</h3>
<div class="value" id="result-gesamtflaeche">0 m²</div>
<div class="description">Wohnfläche + Nutzfläche</div>
</div>
<div class="result-item">
<h3>Preis pro m² Wohnfläche</h3>
<div class="value" id="result-preis-pro-qm">0,00 €</div>
<div class="description">Kaufpreis / Wohnfläche</div>
</div>
<div class="result-item">
<h3>Grunderwerbsteuer</h3>
<div class="value" id="result-grunderwerbsteuer">0,00 €</div>
<div class="description">Abhängig vom Bundesland</div>
</div>
<div class="result-item">
<h3>Gesamtkosten</h3>
<div class="value" id="result-gesamtkosten">0,00 €</div>
<div class="description">Kaufpreis + Grunderwerbsteuer</div>
</div>
<div class="result-item">
<h3>Jährliche Abschreibung</h3>
<div class="value" id="result-abschreibung">0,00 €</div>
<div class="description">Kaufpreis / Abschreibungszeit</div>
</div>
<div class="result-item">
<h3>Alter der Immobilie</h3>
<div class="value" id="result-alter">0 Jahre</div>
<div class="description">Aktuelles Jahr - Baujahr</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="{{ asset('js/calculator.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,152 @@
{% extends 'base.html.twig' %}
{% block title %}Meine Immobilien - {{ parent() }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.immobilien-list {
margin-top: 20px;
}
.immobilie-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.immobilie-card h3 {
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #4CAF50;
}
.immobilie-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.detail-item {
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
}
.detail-item label {
display: block;
font-weight: 500;
color: #666;
font-size: 12px;
margin-bottom: 5px;
}
.detail-item value {
display: block;
color: #333;
font-size: 16px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.empty-state h2 {
color: #666;
margin-bottom: 20px;
}
.empty-state a {
display: inline-block;
padding: 12px 24px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.empty-state a:hover {
background-color: #45a049;
}
</style>
{% endblock %}
{% block body %}
<h1>Meine Immobilien</h1>
{% if immobilien|length > 0 %}
<div class="immobilien-list">
{% for immobilie in immobilien %}
<div class="immobilie-card">
<h3>{{ immobilie.adresse }}</h3>
<div class="immobilie-details">
<div class="detail-item">
<label>Typ</label>
<value>{{ immobilie.typ.label }}</value>
</div>
<div class="detail-item">
<label>Kaufpreis</label>
<value>{{ immobilie.kaufpreis|number_format(0, ',', '.') }} €</value>
</div>
<div class="detail-item">
<label>Wohnfläche</label>
<value>{{ immobilie.wohnflaeche }} m²</value>
</div>
<div class="detail-item">
<label>Nutzfläche</label>
<value>{{ immobilie.nutzflaeche }} m²</value>
</div>
<div class="detail-item">
<label>Gesamtfläche</label>
<value>{{ immobilie.gesamtflaeche }} m²</value>
</div>
<div class="detail-item">
<label>Zimmer</label>
<value>{{ immobilie.zimmer }}</value>
</div>
{% if immobilie.baujahr %}
<div class="detail-item">
<label>Baujahr</label>
<value>{{ immobilie.baujahr }}</value>
</div>
{% endif %}
{% if immobilie.bundesland %}
<div class="detail-item">
<label>Bundesland</label>
<value>{{ immobilie.bundesland.name }}</value>
</div>
{% endif %}
{% if immobilie.heizungstyp %}
<div class="detail-item">
<label>Heizungstyp</label>
<value>{{ immobilie.heizungstyp.name }}</value>
</div>
{% endif %}
<div class="detail-item">
<label>Garage</label>
<value>{{ immobilie.garage ? 'Ja' : 'Nein' }}</value>
</div>
<div class="detail-item">
<label>Erstellt am</label>
<value>{{ immobilie.createdAt|date('d.m.Y H:i') }}</value>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<h2>Sie haben noch keine Immobilien gespeichert</h2>
<p>Nutzen Sie den Immobilienrechner, um Ihre erste Immobilie zu berechnen und zu speichern.</p>
<a href="{{ path('app_home') }}">Zum Rechner</a>
</div>
{% endif %}
{% endblock %}