API updadte
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
96
public/css/auth.css
Normal 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
146
public/css/calculator.css
Normal 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
112
public/js/calculator.js
Normal 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();
|
||||
});
|
||||
96
src/Controller/AuthController.php
Normal file
96
src/Controller/AuthController.php
Normal 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!');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
119
src/Controller/ImmobilienSaveController.php
Normal file
119
src/Controller/ImmobilienSaveController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
46
templates/auth/login.html.twig
Normal file
46
templates/auth/login.html.twig
Normal 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 %}
|
||||
48
templates/auth/register.html.twig
Normal file
48
templates/auth/register.html.twig
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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-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>
|
||||
|
||||
<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="calculator-container">
|
||||
<div class="form-section">
|
||||
<h2>Immobiliendaten</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<p>Die REST-API ist über API Platform verfügbar und bietet automatische Dokumentation.</p>
|
||||
<div class="results-section">
|
||||
<h2>Berechnungsergebnisse</h2>
|
||||
|
||||
<a href="/api" class="api-link">Zur API-Dokumentation</a>
|
||||
<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 %}
|
||||
|
||||
152
templates/immobilie/my_immobilien.html.twig
Normal file
152
templates/immobilie/my_immobilien.html.twig
Normal 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 %}
|
||||
Reference in New Issue
Block a user