Compare commits
16 Commits
a072419989
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52b3a23b46 | |||
| 05fe1fedf0 | |||
| 75612064d9 | |||
| 14c83635f9 | |||
| 5f216f1317 | |||
| 4953d192c0 | |||
| 77206224a2 | |||
| 7548e241be | |||
| 5835eb15ed | |||
| 81374cc659 | |||
| c0c346a9ed | |||
| 0fb028d19a | |||
| 58c7907915 | |||
| 320f2f30af | |||
| cba9aef518 | |||
| 7b7ca33781 |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
40
.env
Normal file
40
.env
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||||
|
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> nelmio/cors-bundle ###
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
###< nelmio/cors-bundle ###
|
||||||
21
.env.example
21
.env.example
@@ -1,11 +1,12 @@
|
|||||||
# Database Configuration
|
# Symfony Application Configuration
|
||||||
DB_HOST=db
|
APP_ENV=dev
|
||||||
DB_PORT=3306
|
APP_SECRET=change_this_to_a_random_secret_key
|
||||||
DB_DATABASE=immorechner
|
|
||||||
DB_USERNAME=immorechner_user
|
|
||||||
DB_PASSWORD=immorechner_pass
|
|
||||||
DB_ROOT_PASSWORD=root
|
|
||||||
|
|
||||||
# Application Configuration
|
# Database Configuration (MariaDB)
|
||||||
APP_ENV=local
|
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||||
APP_DEBUG=true
|
|
||||||
|
# CORS Configuration
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
|
||||||
|
# Routing Configuration
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
|||||||
17
.gitea/workflows/test.yml
Normal file
17
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: PHPUnit & CS-Fixer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
- name: Run php-cs-fixer
|
||||||
|
run: vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
- name: Run PHPUnit
|
||||||
|
run: vendor/bin/phpunit
|
||||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -6,9 +6,11 @@
|
|||||||
.idea_modules/
|
.idea_modules/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Claude AI
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Composer
|
# Composer
|
||||||
/vendor/
|
/vendor/
|
||||||
composer.lock
|
|
||||||
composer.phar
|
composer.phar
|
||||||
|
|
||||||
# PHP
|
# PHP
|
||||||
@@ -18,14 +20,17 @@ composer.phar
|
|||||||
php_errors.log
|
php_errors.log
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
|
.env.prod
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
|
nul
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
@@ -69,3 +74,23 @@ clover.xml
|
|||||||
# Docker
|
# Docker
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> phpunit/phpunit ###
|
||||||
|
/phpunit.xml
|
||||||
|
/.phpunit.cache/
|
||||||
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
|
###> friendsofphp/php-cs-fixer ###
|
||||||
|
/.php-cs-fixer.php
|
||||||
|
/.php-cs-fixer.cache
|
||||||
|
###< friendsofphp/php-cs-fixer ###
|
||||||
|
|||||||
30
.php-cs-fixer.dist.php
Normal file
30
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$finder = (new PhpCsFixer\Finder())
|
||||||
|
->in(__DIR__)
|
||||||
|
->exclude('var')
|
||||||
|
->exclude('vendor')
|
||||||
|
->exclude('migrations')
|
||||||
|
->notPath('bin/console')
|
||||||
|
->notPath('public/index.php')
|
||||||
|
;
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setRules([
|
||||||
|
'@Symfony' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'not_operator_with_successor_space' => true,
|
||||||
|
'trailing_comma_in_multiline' => true,
|
||||||
|
'phpdoc_scalar' => true,
|
||||||
|
'unary_operator_spaces' => true,
|
||||||
|
'binary_operator_spaces' => true,
|
||||||
|
'blank_line_before_statement' => [
|
||||||
|
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
|
||||||
|
],
|
||||||
|
'phpdoc_single_line_var_spacing' => true,
|
||||||
|
'phpdoc_var_without_name' => true,
|
||||||
|
])
|
||||||
|
->setFinder($finder)
|
||||||
|
;
|
||||||
55
Dockerfile
55
Dockerfile
@@ -1,38 +1,57 @@
|
|||||||
FROM php:8.3-apache
|
# Production-ready Symfony Dockerfile
|
||||||
|
FROM php:8.4-apache
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
libpng-dev \
|
libpng-dev \
|
||||||
libonig-dev \
|
libonig-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
zip \
|
libicu-dev \
|
||||||
unzip \
|
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
git \
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions needed by Symfony
|
||||||
RUN docker-php-ext-install pdo_mysql mysqli mbstring exif pcntl bcmath gd zip
|
RUN docker-php-ext-configure intl \
|
||||||
|
&& docker-php-ext-install -j$(nproc) \
|
||||||
|
pdo_mysql \
|
||||||
|
mbstring \
|
||||||
|
exif \
|
||||||
|
pcntl \
|
||||||
|
bcmath \
|
||||||
|
gd \
|
||||||
|
zip \
|
||||||
|
intl \
|
||||||
|
opcache
|
||||||
|
|
||||||
# Enable Apache mod_rewrite
|
# Enable Apache modules
|
||||||
RUN a2enmod rewrite
|
RUN a2enmod rewrite headers
|
||||||
|
|
||||||
# Enable Apache headers module
|
# Copy Apache configuration
|
||||||
RUN a2enmod headers
|
|
||||||
|
|
||||||
# Copy custom Apache configuration
|
|
||||||
COPY ./docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
|
COPY ./docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||||
|
|
||||||
# Install Composer
|
# Copy Composer from official image
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
# Change ownership
|
# Copy Symfony app
|
||||||
|
COPY . /var/www/html
|
||||||
|
|
||||||
|
# Optimize permissions
|
||||||
RUN chown -R www-data:www-data /var/www/html
|
RUN chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
# Configure opcache for production
|
||||||
|
RUN { \
|
||||||
|
echo "opcache.enable=1"; \
|
||||||
|
echo "opcache.memory_consumption=256"; \
|
||||||
|
echo "opcache.max_accelerated_files=20000"; \
|
||||||
|
echo "opcache.validate_timestamps=0"; \
|
||||||
|
} > /usr/local/etc/php/conf.d/opcache.ini
|
||||||
|
|
||||||
# Expose port 80
|
# Expose port 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
|||||||
154
README.md
Normal file
154
README.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Immorechner
|
||||||
|
|
||||||
|
Eine moderne Webanwendung zur Immobilienberechnung mit interaktivem Frontend und vollständiger REST-API.
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Repository klonen
|
||||||
|
git clone <repository-url>
|
||||||
|
cd immorechner
|
||||||
|
|
||||||
|
# Docker-Container starten
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Dependencies installieren
|
||||||
|
docker-compose exec web composer install
|
||||||
|
|
||||||
|
# Datenbank initialisieren
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# Anwendung öffnen
|
||||||
|
# Frontend: http://localhost:8080
|
||||||
|
# API-Docs: http://localhost:8080/api/docs.html
|
||||||
|
# phpMyAdmin: http://localhost:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Frontend (Web-Interface)
|
||||||
|
- 🧮 **Interaktiver Immobilienrechner** mit Live-Berechnungen
|
||||||
|
- 🔗 **URL-Sharing** für anonyme Nutzer (keine Registrierung nötig)
|
||||||
|
- 👤 **Benutzer-Registrierung & Login**
|
||||||
|
- 💾 **Immobilien speichern** (nur für registrierte Nutzer)
|
||||||
|
- 📊 **Automatische Berechnungen**: Grunderwerbsteuer, Gesamtkosten, Abschreibung, etc.
|
||||||
|
- 📱 **Responsive Design** (Mobile-optimiert)
|
||||||
|
|
||||||
|
### Backend (REST-API)
|
||||||
|
- 🔌 **Vollständige REST-API** mit API Platform 4.2
|
||||||
|
- 🔐 **Dual-Authentifizierung**: Session-basiert (Frontend) & API-Key (API)
|
||||||
|
- 🔑 **Rollenbasierte Zugriffskontrolle**: USER, ADMIN, MODERATOR, TECHNICAL
|
||||||
|
- 📖 **Swagger/OpenAPI** Dokumentation
|
||||||
|
- 🏢 **Mandantenfähigkeit**: Jeder User sieht nur seine eigenen Immobilien
|
||||||
|
- 🌍 **Öffentliche Ressourcen**: Bundesländer und Heizungstypen ohne Authentifizierung
|
||||||
|
|
||||||
|
### Technologie
|
||||||
|
- **Backend**: PHP 8.4, Symfony 7.3
|
||||||
|
- **Frontend**: Twig, jQuery, separates CSS/JS
|
||||||
|
- **Datenbank**: MariaDB
|
||||||
|
- **Container**: Docker & Docker Compose
|
||||||
|
- **ORM**: Doctrine 3.0
|
||||||
|
- **API**: API Platform 4.2
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
📚 **Detaillierte Dokumentation:**
|
||||||
|
|
||||||
|
- **[Installation & Setup](docs/installation.md)** - Schritt-für-Schritt Installationsanleitung
|
||||||
|
- **[Features & Funktionalität](docs/features.md)** - Übersicht aller Funktionen
|
||||||
|
- **[Technische Dokumentation](docs/technical.md)** - Architektur, Datenbank-Schema, Konfiguration
|
||||||
|
- **[API-Dokumentation](docs/api.md)** - REST-API Endpunkte, Authentifizierung, Beispiele
|
||||||
|
- **[Entwicklung](docs/development.md)** - Testing, Code Quality, Entwickler-Workflow
|
||||||
|
- **[Docker](docs/docker.md)** - Docker-Befehle und Container-Management
|
||||||
|
- **[Fehlerbehebung](docs/troubleshooting.md)** - Lösungen für häufige Probleme
|
||||||
|
|
||||||
|
## Projekt-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
immorechner/
|
||||||
|
├── config/ # Symfony Konfiguration
|
||||||
|
├── docs/ # Dokumentation
|
||||||
|
├── migrations/ # Datenbank-Migrationen
|
||||||
|
├── public/ # Web-Root, CSS, JS, Assets
|
||||||
|
├── src/ # PHP-Quellcode
|
||||||
|
│ ├── Controller/ # Controller (Frontend & API)
|
||||||
|
│ ├── Entity/ # Doctrine Entities
|
||||||
|
│ ├── Repository/ # Doctrine Repositories
|
||||||
|
│ └── Security/ # Authentifizierung
|
||||||
|
├── templates/ # Twig-Templates
|
||||||
|
├── docker-compose.yml # Docker Compose-Konfiguration
|
||||||
|
└── Dockerfile # Docker-Image für PHP/Apache
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
**Startseite (Rechner):** http://localhost:8080
|
||||||
|
- Immobiliendaten eingeben
|
||||||
|
- Live-Berechnungen ansehen
|
||||||
|
- Link zum Teilen generieren
|
||||||
|
- Bei Anmeldung: Immobilie speichern
|
||||||
|
|
||||||
|
**Registrierung:** http://localhost:8080/register
|
||||||
|
|
||||||
|
**Login:** http://localhost:8080/login
|
||||||
|
|
||||||
|
**Meine Immobilien:** http://localhost:8080/meine-immobilien (nach Login)
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
**API-Dokumentation (Swagger UI):** http://localhost:8080/api/docs.html
|
||||||
|
|
||||||
|
**API Entrypoint:** http://localhost:8080/api
|
||||||
|
|
||||||
|
**Öffentliche Endpunkte (ohne Authentifizierung):**
|
||||||
|
- `GET /api/bundeslands` - Alle Bundesländer
|
||||||
|
- `GET /api/heizungstyps` - Alle Heizungstypen
|
||||||
|
|
||||||
|
**Geschützte Endpunkte (API-Key erforderlich):**
|
||||||
|
- `/api/immobilies` - Immobilien-Management
|
||||||
|
- `/api/users` - Benutzer-Management
|
||||||
|
|
||||||
|
## Schnelle Befehle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container starten
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Container stoppen
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Logs anzeigen
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# In Container einloggen
|
||||||
|
docker-compose exec web bash
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Tests ausführen
|
||||||
|
docker-compose exec web php bin/phpunit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheitshinweise
|
||||||
|
|
||||||
|
**⚠️ Wichtig für Produktion:**
|
||||||
|
|
||||||
|
1. Ändern Sie alle Standardpasswörter in `docker-compose.yml` und `.env`
|
||||||
|
2. Generieren Sie einen neuen `APP_SECRET`
|
||||||
|
3. Setzen Sie `APP_ENV=prod`
|
||||||
|
4. Aktivieren Sie HTTPS
|
||||||
|
5. Entfernen Sie phpMyAdmin
|
||||||
|
6. Konfigurieren Sie CORS für Ihre Domain
|
||||||
|
|
||||||
|
Details siehe [Technische Dokumentation](docs/technical.md#sicherheit).
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
[Lizenz hier einfügen]
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Problemen siehe [Fehlerbehebung](docs/troubleshooting.md) oder öffnen Sie ein Issue.
|
||||||
21
bin/console
Normal file
21
bin/console
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
|
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||||
|
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
||||||
|
return new Application($kernel);
|
||||||
|
};
|
||||||
4
bin/phpunit
Normal file
4
bin/phpunit
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||||
55
composer.json
Normal file
55
composer.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "immorechner/app",
|
||||||
|
"type": "project",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4",
|
||||||
|
"symfony/runtime": "^7.3",
|
||||||
|
"symfony/dotenv": "^7.3",
|
||||||
|
"symfony/flex": "^2.9",
|
||||||
|
"symfony/framework-bundle": "^7.3",
|
||||||
|
"symfony/yaml": "^7.3",
|
||||||
|
"doctrine/orm": "^3.5",
|
||||||
|
"doctrine/doctrine-bundle": "^3.0",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^3.6",
|
||||||
|
"api-platform/doctrine-orm": "^4.2",
|
||||||
|
"api-platform/symfony": "^4.2",
|
||||||
|
"nelmio/cors-bundle": "^2.6",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
"symfony/asset": "^7.3",
|
||||||
|
"symfony/expression-language": "^7.3",
|
||||||
|
"symfony/security-bundle": "^7.3",
|
||||||
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"symfony/property-access": "^7.3",
|
||||||
|
"symfony/property-info": "^7.3",
|
||||||
|
"symfony/serializer": "^7.3",
|
||||||
|
"symfony/twig-bundle": "^7.3",
|
||||||
|
"symfony/validator": "^7.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"symfony/runtime": true,
|
||||||
|
"symfony/flex": true
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"php": "8.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^12.4",
|
||||||
|
"symfony/browser-kit": "^7.3",
|
||||||
|
"symfony/css-selector": "^7.3",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.89"
|
||||||
|
}
|
||||||
|
}
|
||||||
10720
composer.lock
generated
Normal file
10720
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
config/bundles.php
Normal file
11
config/bundles.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
|
];
|
||||||
40
config/packages/api_platform.yaml
Normal file
40
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
api_platform:
|
||||||
|
title: 'Immorechner API'
|
||||||
|
description: |
|
||||||
|
# Immobilien-Verwaltungs-API
|
||||||
|
|
||||||
|
Diese REST-API bietet umfassende Funktionen zur Verwaltung von Immobilien und Benutzern.
|
||||||
|
|
||||||
|
## Hauptfunktionen
|
||||||
|
- Verwaltung von Immobilien (CRUD-Operationen)
|
||||||
|
- Benutzerverwaltung mit Rollensystem
|
||||||
|
- Beziehungsverwaltung zwischen Usern und Immobilien
|
||||||
|
- Automatische Berechnungen (Preis pro m², Gesamtfläche)
|
||||||
|
|
||||||
|
## Datenformate
|
||||||
|
- JSON-LD (Hydra)
|
||||||
|
- JSON
|
||||||
|
- HAL+JSON
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
Derzeit keine Authentifizierung erforderlich (Entwicklungsversion)
|
||||||
|
version: 1.0.0
|
||||||
|
mapping:
|
||||||
|
paths: ['%kernel.project_dir%/src/Entity']
|
||||||
|
defaults:
|
||||||
|
stateless: true
|
||||||
|
cache_headers:
|
||||||
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
formats:
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
json: ['application/json']
|
||||||
|
docs_formats:
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
jsonopenapi: ['application/vnd.openapi+json']
|
||||||
|
html: ['text/html']
|
||||||
|
swagger:
|
||||||
|
versions: [3]
|
||||||
|
api_keys:
|
||||||
|
apiKey:
|
||||||
|
name: Authorization
|
||||||
|
type: header
|
||||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
48
config/packages/doctrine.yaml
Normal file
48
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '16'
|
||||||
|
|
||||||
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
|
orm:
|
||||||
|
validate_xml_mapping: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
identity_generation_preferences:
|
||||||
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
type: attribute
|
||||||
|
is_bundle: false
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App
|
||||||
|
controller_resolver:
|
||||||
|
auto_mapping: false
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
# "TEST_TOKEN" is typically set by ParaTest
|
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
||||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||||
|
enable_profiler: false
|
||||||
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
10
config/packages/nelmio_cors.yaml
Normal file
10
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
nelmio_cors:
|
||||||
|
defaults:
|
||||||
|
origin_regex: true
|
||||||
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
expose_headers: ['Link']
|
||||||
|
max_age: 3600
|
||||||
|
paths:
|
||||||
|
'^/': null
|
||||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
||||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
default_uri: '%env(DEFAULT_URI)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
71
config/packages/security.yaml
Normal file
71
config/packages/security.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
security:
|
||||||
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
|
providers:
|
||||||
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: email
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
|
security: false
|
||||||
|
|
||||||
|
# API-Dokumentation (öffentlich zugänglich)
|
||||||
|
api_docs:
|
||||||
|
pattern: ^/api/docs
|
||||||
|
stateless: true
|
||||||
|
security: false
|
||||||
|
|
||||||
|
# Öffentliche Endpunkte (Bundesländer, Heizungstypen)
|
||||||
|
public_api:
|
||||||
|
pattern: ^/api/(bundeslands|heizungstyps)
|
||||||
|
stateless: true
|
||||||
|
security: false
|
||||||
|
|
||||||
|
# API mit API-Key-Authentifizierung
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\ApiKeyAuthenticator
|
||||||
|
|
||||||
|
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
|
||||||
|
access_control:
|
||||||
|
# API-Dokumentation ist öffentlich
|
||||||
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
# Öffentlicher Zugriff auf Bundesländer und Heizungstypen (GET)
|
||||||
|
- { path: ^/api/(bundeslands|heizungstyps), methods: [GET], roles: PUBLIC_ACCESS }
|
||||||
|
# Admin und Technical User Zugriff für Änderungen an Bundesländern und Heizungstypen
|
||||||
|
- { path: ^/api/(bundeslands|heizungstyps), methods: [POST, PUT, DELETE, PATCH], roles: [ROLE_ADMIN, ROLE_TECHNICAL] }
|
||||||
|
# Alle anderen API-Endpunkte erfordern Authentifizierung
|
||||||
|
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
# By default, password hashers are resource intensive and take time. This is
|
||||||
|
# important to generate secure password hashes. In tests however, secure hashes
|
||||||
|
# are not important, waste resources and increase test times. The following
|
||||||
|
# reduces the work factor to the lowest possible values.
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
|
algorithm: auto
|
||||||
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
time_cost: 3 # Lowest possible value for argon
|
||||||
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
twig:
|
||||||
|
file_name_pattern: '*.twig'
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
twig:
|
||||||
|
strict_variables: true
|
||||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
||||||
5
config/preload.php
Normal file
5
config/preload.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||||
|
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||||
|
}
|
||||||
5
config/routes.yaml
Normal file
5
config/routes.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
controllers:
|
||||||
|
resource:
|
||||||
|
path: ../src/Controller/
|
||||||
|
namespace: App\Controller
|
||||||
|
type: attribute
|
||||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
api_platform:
|
||||||
|
resource: .
|
||||||
|
type: api_platform
|
||||||
|
prefix: /api
|
||||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
|
prefix: /_error
|
||||||
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
_security_logout:
|
||||||
|
resource: security.route_loader.logout
|
||||||
|
type: service
|
||||||
34
config/services.yaml
Normal file
34
config/services.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
|
||||||
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# default configuration for services in *this* file
|
||||||
|
_defaults:
|
||||||
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
# makes classes in src/ available to be used as 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
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerAdmin webmaster@localhost
|
ServerAdmin webmaster@localhost
|
||||||
DocumentRoot /var/www/html
|
DocumentRoot /var/www/html/public
|
||||||
|
|
||||||
<Directory /var/www/html>
|
<Directory /var/www/html/public>
|
||||||
Options Indexes FollowSymLinks
|
Options -Indexes +FollowSymLinks
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
|
FallbackResource /index.php
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# Enable mod_rewrite
|
# Symfony recommended: disable .htaccess if using FallbackResource
|
||||||
RewriteEngine On
|
<Directory /var/www/html/public/bundles>
|
||||||
|
FallbackResource disabled
|
||||||
|
</Directory>
|
||||||
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||||
|
|
||||||
# Optional: Disable directory listing
|
|
||||||
<Directory /var/www/html>
|
|
||||||
Options -Indexes +FollowSymLinks
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|||||||
185
docs/api.md
Normal file
185
docs/api.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# API-Dokumentation
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Die Immorechner-API ist eine vollständige REST-API basierend auf API Platform 4.2 mit OpenAPI/Swagger-Dokumentation.
|
||||||
|
|
||||||
|
**Basis-URL:** `http://localhost:8080/api`
|
||||||
|
|
||||||
|
**Formate:**
|
||||||
|
- `application/ld+json` (Standard, JSON-LD mit Hydra)
|
||||||
|
- `application/json` (Einfaches JSON)
|
||||||
|
|
||||||
|
## Interaktive Dokumentation
|
||||||
|
|
||||||
|
**Swagger UI:** http://localhost:8080/api/docs.html
|
||||||
|
|
||||||
|
Hier können Sie alle Endpunkte direkt im Browser testen.
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### API-Key (für API-Zugriff)
|
||||||
|
|
||||||
|
Fügen Sie den API-Key im Header hinzu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-KEY: ihr-api-key-hier" http://localhost:8080/api/immobilies
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-Key erhalten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# User erstellen
|
||||||
|
curl -X POST http://localhost:8080/api/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Max Mustermann",
|
||||||
|
"email": "max@example.com",
|
||||||
|
"role": "user"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Response enthält apiKey
|
||||||
|
{
|
||||||
|
"apiKey": "a1b2c3d4e5f6...", # Diesen Key verwenden
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpunkte
|
||||||
|
|
||||||
|
### Bundesländer (Öffentlich)
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung | Auth |
|
||||||
|
|---------|----------|--------------|------|
|
||||||
|
| GET | `/api/bundeslands` | Alle Bundesländer | Nein |
|
||||||
|
| GET | `/api/bundeslands/{id}` | Einzelnes Bundesland | Nein |
|
||||||
|
| POST | `/api/bundeslands` | Neues Bundesland erstellen | ADMIN/TECHNICAL |
|
||||||
|
| PUT | `/api/bundeslands/{id}` | Bundesland aktualisieren | ADMIN/TECHNICAL |
|
||||||
|
| DELETE | `/api/bundeslands/{id}` | Bundesland löschen | ADMIN/TECHNICAL |
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/bundeslands
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heizungstypen (Öffentlich)
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung | Auth |
|
||||||
|
|---------|----------|--------------|------|
|
||||||
|
| GET | `/api/heizungstyps` | Alle Heizungstypen | Nein |
|
||||||
|
| GET | `/api/heizungstyps/{id}` | Einzelner Heizungstyp | Nein |
|
||||||
|
| POST | `/api/heizungstyps` | Neuen Typ erstellen | ADMIN/TECHNICAL |
|
||||||
|
| PUT | `/api/heizungstyps/{id}` | Typ aktualisieren | ADMIN/TECHNICAL |
|
||||||
|
| DELETE | `/api/heizungstyps/{id}` | Typ löschen | ADMIN/TECHNICAL |
|
||||||
|
|
||||||
|
### Immobilien (Geschützt)
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung | Auth |
|
||||||
|
|---------|----------|--------------|------|
|
||||||
|
| GET | `/api/immobilies` | Eigene Immobilien | API-Key |
|
||||||
|
| GET | `/api/immobilies/{id}` | Einzelne Immobilie | API-Key |
|
||||||
|
| POST | `/api/immobilies` | Neue Immobilie | API-Key |
|
||||||
|
| PATCH | `/api/immobilies/{id}` | Immobilie aktualisieren | API-Key |
|
||||||
|
| DELETE | `/api/immobilies/{id}` | Immobilie löschen | API-Key |
|
||||||
|
|
||||||
|
**Beispiel - Immobilie erstellen:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/immobilies \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-KEY: your-api-key" \
|
||||||
|
-d '{
|
||||||
|
"verwalter": "/api/users/1",
|
||||||
|
"adresse": "Hauptstraße 123, 12345 Musterstadt",
|
||||||
|
"wohnflaeche": 85,
|
||||||
|
"nutzflaeche": 15,
|
||||||
|
"zimmer": 3,
|
||||||
|
"typ": "wohnung",
|
||||||
|
"kaufpreis": 300000,
|
||||||
|
"bundesland": "/api/bundeslands/1",
|
||||||
|
"heizungstyp": "/api/heizungstyps/1"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### User (Geschützt)
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung | Auth |
|
||||||
|
|---------|----------|--------------|------|
|
||||||
|
| GET | `/api/users` | Alle User | API-Key |
|
||||||
|
| GET | `/api/users/{id}` | Einzelner User | API-Key |
|
||||||
|
| POST | `/api/users` | Neuen User erstellen | Nein* |
|
||||||
|
| PUT | `/api/users/{id}` | User aktualisieren | API-Key |
|
||||||
|
| DELETE | `/api/users/{id}` | User löschen | API-Key |
|
||||||
|
|
||||||
|
*POST ohne Auth zum Registrieren neuer User
|
||||||
|
|
||||||
|
## Rollen & Berechtigungen
|
||||||
|
|
||||||
|
| Rolle | Rechte |
|
||||||
|
|-------|--------|
|
||||||
|
| `user` | Eigene Immobilien verwalten |
|
||||||
|
| `admin` | Alle Ressourcen, alle Immobilien |
|
||||||
|
| `technical` | Bundesländer & Heizungstypen verwalten |
|
||||||
|
| `moderator` | Erweiterte Rechte |
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
### ImmobilienTyp
|
||||||
|
- `wohnung` - Wohnung
|
||||||
|
- `haus` - Haus
|
||||||
|
- `grundstueck` - Grundstück
|
||||||
|
- `gewerbe` - Gewerbe
|
||||||
|
- `buero` - Büro
|
||||||
|
|
||||||
|
### UserRole
|
||||||
|
- `user` - Benutzer
|
||||||
|
- `admin` - Administrator
|
||||||
|
- `moderator` - Moderator
|
||||||
|
- `technical` - Technischer User
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
Alle Collection-Endpunkte unterstützen Pagination:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Erste Seite (Standard: 30 Items)
|
||||||
|
curl http://localhost:8080/api/immobilies
|
||||||
|
|
||||||
|
# Zweite Seite
|
||||||
|
curl http://localhost:8080/api/immobilies?page=2
|
||||||
|
|
||||||
|
# Custom Page Size
|
||||||
|
curl "http://localhost:8080/api/immobilies?itemsPerPage=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nach Typ filtern
|
||||||
|
curl "http://localhost:8080/api/immobilies?typ=wohnung"
|
||||||
|
|
||||||
|
# Nach Bundesland filtern
|
||||||
|
curl "http://localhost:8080/api/immobilies?bundesland=/api/bundeslands/1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehler-Codes
|
||||||
|
|
||||||
|
| Status Code | Bedeutung |
|
||||||
|
|-------------|-----------|
|
||||||
|
| 200 | OK - Erfolgreiche Anfrage |
|
||||||
|
| 201 | Created - Ressource erstellt |
|
||||||
|
| 204 | No Content - Erfolgreich gelöscht |
|
||||||
|
| 400 | Bad Request - Ungültige Daten |
|
||||||
|
| 401 | Unauthorized - Authentifizierung fehlgeschlagen |
|
||||||
|
| 403 | Forbidden - Keine Berechtigung |
|
||||||
|
| 404 | Not Found - Ressource nicht gefunden |
|
||||||
|
| 500 | Internal Server Error - Server-Fehler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Weitere Informationen:**
|
||||||
|
- [Features](features.md) - Funktionsübersicht
|
||||||
|
- [Technical](technical.md) - Datenbank-Schema & Architektur
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
302
docs/development.md
Normal file
302
docs/development.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# Entwicklung
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Entwickler-Workflow
|
||||||
|
|
||||||
|
### Entwicklungsumgebung einrichten
|
||||||
|
|
||||||
|
Siehe [Installation](installation.md)
|
||||||
|
|
||||||
|
### Code-Änderungen testen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container starten
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# In Web-Container einloggen
|
||||||
|
docker-compose exec web bash
|
||||||
|
|
||||||
|
# Cache leeren nach Änderungen
|
||||||
|
php bin/console cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### PHPUnit Tests
|
||||||
|
|
||||||
|
Das Projekt verwendet PHPUnit 12.4 für Tests.
|
||||||
|
|
||||||
|
#### Alle Tests ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/phpunit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Spezifische Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nur Entity-Tests
|
||||||
|
docker-compose exec web php bin/phpunit tests/Entity
|
||||||
|
|
||||||
|
# Nur API-Tests
|
||||||
|
docker-compose exec web php bin/phpunit tests/Api
|
||||||
|
|
||||||
|
# Einzelne Testklasse
|
||||||
|
docker-compose exec web php bin/phpunit tests/Entity/UserTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mit Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/phpunit --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code Coverage (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/phpunit --coverage-text
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── Entity/
|
||||||
|
│ ├── UserTest.php # User-Entity Tests
|
||||||
|
│ ├── ImmobilieTest.php # Immobilie-Entity Tests
|
||||||
|
│ ├── BundeslandTest.php # Bundesland-Entity Tests
|
||||||
|
│ └── HeizungstypTest.php # Heizungstyp-Entity Tests
|
||||||
|
└── Api/
|
||||||
|
├── BundeslandApiTest.php # Bundesländer-API Tests
|
||||||
|
├── HeizungstypApiTest.php # Heizungstypen-API Tests
|
||||||
|
└── ApiDocumentationTest.php # API-Docs Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
### PHP-CS-Fixer
|
||||||
|
|
||||||
|
Für konsistenten Code-Style nach Symfony Standards.
|
||||||
|
|
||||||
|
#### Code-Style prüfen (Dry-Run)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code automatisch formatieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web vendor/bin/php-cs-fixer fix
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bestimmte Verzeichnisse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nur src/
|
||||||
|
docker-compose exec web vendor/bin/php-cs-fixer fix src
|
||||||
|
|
||||||
|
# Nur tests/
|
||||||
|
docker-compose exec web vendor/bin/php-cs-fixer fix tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code-Style Regeln
|
||||||
|
|
||||||
|
Konfiguration in `.php-cs-fixer.dist.php`:
|
||||||
|
- Symfony Coding Standards
|
||||||
|
- PSR-12 kompatibel
|
||||||
|
- Short Array Syntax
|
||||||
|
- Sortierte Imports
|
||||||
|
- Trailing Commas in Arrays
|
||||||
|
|
||||||
|
## Datenbank
|
||||||
|
|
||||||
|
### Neue Entity erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console make:entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration erstellen
|
||||||
|
|
||||||
|
Nach Änderungen an Entities:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration rückgängig machen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Letzte Migration
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate prev
|
||||||
|
|
||||||
|
# Zu spezifischer Version
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate DoctrineMigrations\\Version20251109100000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration-Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Schema validieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console doctrine:schema:validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controller & Routes
|
||||||
|
|
||||||
|
### Controller erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console make:controller
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routes anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Routes
|
||||||
|
docker-compose exec web php bin/console debug:router
|
||||||
|
|
||||||
|
# Spezifische Route
|
||||||
|
docker-compose exec web php bin/console debug:router app_home
|
||||||
|
```
|
||||||
|
|
||||||
|
## Symfony Console
|
||||||
|
|
||||||
|
### Cache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Cache warmup
|
||||||
|
docker-compose exec web php bin/console cache:warmup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services debuggen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Services
|
||||||
|
docker-compose exec web php bin/console debug:container
|
||||||
|
|
||||||
|
# Spezifischer Service
|
||||||
|
docker-compose exec web php bin/console debug:container UserRepository
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ereignisse anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console debug:event-dispatcher
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konfiguration anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console debug:config framework
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
### Feature-Branch erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/neue-funktion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Änderungen committen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Feature: Beschreibung der Änderung"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Request erstellen
|
||||||
|
|
||||||
|
1. Push auf Remote Branch
|
||||||
|
2. Pull Request in GitHub/GitLab erstellen
|
||||||
|
3. Code Review abwarten
|
||||||
|
4. Nach Approval mergen
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Code-Organisation
|
||||||
|
|
||||||
|
1. **Controller:** Dünn halten, Logik in Services auslagern
|
||||||
|
2. **Entities:** Nur Datenmodell, keine Business-Logik
|
||||||
|
3. **Services:** Wiederverwendbare Business-Logik
|
||||||
|
4. **Repositories:** Nur Datenbank-Queries
|
||||||
|
|
||||||
|
### Namenskonventionen
|
||||||
|
|
||||||
|
- **Controller:** `XyzController.php`
|
||||||
|
- **Entity:** `Xyz.php`
|
||||||
|
- **Repository:** `XyzRepository.php`
|
||||||
|
- **Service:** `XyzService.php`
|
||||||
|
- **Test:** `XyzTest.php`
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Nie Passwörter im Klartext speichern
|
||||||
|
- Immer UserPasswordHasher verwenden
|
||||||
|
- CSRF-Tokens bei allen Forms
|
||||||
|
- Input-Validierung auf Server-Seite
|
||||||
|
- Output-Escaping (Twig macht automatisch)
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Doctrine Query Cache nutzen
|
||||||
|
- Eager Loading für Relationen
|
||||||
|
- Opcache in Produktion aktivieren
|
||||||
|
- Assets kompilieren für Produktion
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Vorbereitung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.local für Produktion erstellen
|
||||||
|
cp .env .env.local
|
||||||
|
|
||||||
|
# Produktions-Werte setzen
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET=<neues-secret-generieren>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build für Produktion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Composer Dependencies ohne Dev
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Cache warmup
|
||||||
|
php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
# Assets installieren
|
||||||
|
php bin/console assets:install public --symlink --relative --env=prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction --env=prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Siehe auch:**
|
||||||
|
- [Technical](technical.md) - Architektur & Konfiguration
|
||||||
|
- [Docker](docker.md) - Container-Management
|
||||||
|
- [Troubleshooting](troubleshooting.md) - Fehler beheben
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
337
docs/docker.md
Normal file
337
docs/docker.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Container-Übersicht
|
||||||
|
|
||||||
|
Das Projekt verwendet 3 Docker-Container:
|
||||||
|
|
||||||
|
| Container | Image | Port | Beschreibung |
|
||||||
|
|-----------|-------|------|--------------|
|
||||||
|
| `immorechner_web` | Custom (PHP 8.4 + Apache) | 8080 | Web-Server & PHP |
|
||||||
|
| `immorechner_db` | mariadb:latest | 3306 | Datenbank |
|
||||||
|
| `immorechner_phpmyadmin` | phpmyadmin:latest | 8081 | DB-Verwaltung |
|
||||||
|
|
||||||
|
## Container-Management
|
||||||
|
|
||||||
|
### Starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Container starten
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Mit Build (nach Dockerfile-Änderungen)
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Im Vordergrund (mit Logs)
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stoppen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container stoppen
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Container stoppen und entfernen
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Container stoppen, entfernen + Volumes löschen
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neustarten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Container
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Einzelner Container
|
||||||
|
docker-compose restart web
|
||||||
|
docker-compose restart db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container-Status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Alle Container (auch gestoppte)
|
||||||
|
docker ps -a
|
||||||
|
|
||||||
|
# Resource-Usage
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
### Container-Logs anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Container (Live)
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Nur Web-Container
|
||||||
|
docker-compose logs -f web
|
||||||
|
|
||||||
|
# Nur Datenbank
|
||||||
|
docker-compose logs -f db
|
||||||
|
|
||||||
|
# Nur phpMyAdmin
|
||||||
|
docker-compose logs -f phpmyadmin
|
||||||
|
|
||||||
|
# Letzte 100 Zeilen
|
||||||
|
docker-compose logs --tail=100 web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apache-Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access Log
|
||||||
|
docker-compose exec web tail -f /var/log/apache2/access.log
|
||||||
|
|
||||||
|
# Error Log
|
||||||
|
docker-compose exec web tail -f /var/log/apache2/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Symfony-Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dev Log
|
||||||
|
docker-compose exec web tail -f /var/www/html/var/log/dev.log
|
||||||
|
|
||||||
|
# Prod Log
|
||||||
|
docker-compose exec web tail -f /var/www/html/var/log/prod.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container-Zugriff
|
||||||
|
|
||||||
|
### In Container einloggen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Web-Container (Bash)
|
||||||
|
docker-compose exec web bash
|
||||||
|
|
||||||
|
# Datenbank-Container
|
||||||
|
docker-compose exec db bash
|
||||||
|
|
||||||
|
# phpMyAdmin-Container
|
||||||
|
docker-compose exec phpmyadmin bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Als Root einloggen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -u root web bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Befehle im Container ausführen
|
||||||
|
|
||||||
|
### PHP/Symfony
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Symfony Console
|
||||||
|
docker-compose exec web php bin/console
|
||||||
|
|
||||||
|
# PHP-Version prüfen
|
||||||
|
docker-compose exec web php -v
|
||||||
|
|
||||||
|
# PHP-Module anzeigen
|
||||||
|
docker-compose exec web php -m
|
||||||
|
|
||||||
|
# PHP-Konfiguration
|
||||||
|
docker-compose exec web php -i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
docker-compose exec web composer install
|
||||||
|
|
||||||
|
# Update
|
||||||
|
docker-compose exec web composer update
|
||||||
|
|
||||||
|
# Paket hinzufügen
|
||||||
|
docker-compose exec web composer require vendor/package
|
||||||
|
|
||||||
|
# Paket entfernen
|
||||||
|
docker-compose exec web composer remove vendor/package
|
||||||
|
|
||||||
|
# Autoloader neu generieren
|
||||||
|
docker-compose exec web composer dump-autoload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MariaDB-Client öffnen
|
||||||
|
docker-compose exec db mariadb -u root -proot immorechner
|
||||||
|
|
||||||
|
# SQL-Datei importieren
|
||||||
|
docker-compose exec -T db mariadb -u root -proot immorechner < backup.sql
|
||||||
|
|
||||||
|
# Datenbank exportieren
|
||||||
|
docker-compose exec db mariadb-dump -u root -proot immorechner > backup.sql
|
||||||
|
|
||||||
|
# Tabellen anzeigen
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SHOW TABLES" immorechner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
### Volumes anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume ls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume-Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume inspect immorechner_db_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume löschen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ACHTUNG: Löscht alle Daten!
|
||||||
|
docker volume rm immorechner_db_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Datenbank-Backup
|
||||||
|
docker-compose exec db mariadb-dump -u root -proot immorechner > backup_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Volume-Backup
|
||||||
|
docker run --rm -v immorechner_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
### Images anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker images
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image neu bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose build web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image mit Tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t immorechner:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image löschen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rmi immorechner-web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Netzwerk
|
||||||
|
|
||||||
|
### Netzwerke anzeigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network ls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netzwerk-Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network inspect immorechner_immorechner_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container im Netzwerk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network inspect immorechner_immorechner_network | grep Name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
### Gestoppte Container entfernen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker container prune
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ungenutzte Images entfernen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker image prune
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ungenutzte Volumes entfernen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume prune
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alles aufräumen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ACHTUNG: Löscht alles Ungenutzte!
|
||||||
|
docker system prune -a --volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container startet nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs prüfen
|
||||||
|
docker-compose logs web
|
||||||
|
|
||||||
|
# Container im Debug-Modus starten
|
||||||
|
docker-compose up web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port bereits belegt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ports prüfen
|
||||||
|
netstat -ano | findstr :8080 # Windows
|
||||||
|
lsof -i :8080 # Mac/Linux
|
||||||
|
|
||||||
|
# In docker-compose.yml ändern:
|
||||||
|
ports:
|
||||||
|
- "8090:80" # Statt 8080:80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Berechtigungsprobleme
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Berechtigungen setzen
|
||||||
|
docker-compose exec -u root web chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
# Schreibrechte für var/
|
||||||
|
docker-compose exec -u root web chmod -R 777 /var/www/html/var
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container-Reset
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Komplett neu starten
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d --build
|
||||||
|
docker-compose exec web composer install
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Siehe auch:**
|
||||||
|
- [Installation](installation.md) - Setup-Anleitung
|
||||||
|
- [Development](development.md) - Entwickler-Workflow
|
||||||
|
- [Troubleshooting](troubleshooting.md) - Fehler beheben
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
275
docs/features.md
Normal file
275
docs/features.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# Features & Funktionalität
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Der Immorechner bietet zwei Hauptkomponenten:
|
||||||
|
1. **Web-Frontend** - Interaktive Benutzeroberfläche für Endanwender
|
||||||
|
2. **REST-API** - Programmgesteuerter Zugriff für Integrationen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend-Features
|
||||||
|
|
||||||
|
### 1. Immobilienrechner (Startseite)
|
||||||
|
|
||||||
|
**URL:** http://localhost:8080/
|
||||||
|
|
||||||
|
Der Immobilienrechner ermöglicht die schnelle Berechnung wichtiger Kennzahlen einer Immobilie.
|
||||||
|
|
||||||
|
#### Eingabefelder
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung | Pflicht |
|
||||||
|
|------|-----|--------------|---------|
|
||||||
|
| Adresse | Text | Vollständige Adresse der Immobilie | Nein |
|
||||||
|
| Kaufpreis | Zahl | Kaufpreis in Euro | Empfohlen |
|
||||||
|
| Wohnfläche | Zahl | Wohnfläche in m² | Ja |
|
||||||
|
| Nutzfläche | Zahl | Nutzfläche in m² | Nein |
|
||||||
|
| Zimmer | Zahl | Anzahl der Zimmer | Empfohlen |
|
||||||
|
| Baujahr | Zahl | Baujahr der Immobilie | Nein |
|
||||||
|
| Etage | Zahl | Stockwerk | Nein |
|
||||||
|
| Immobilientyp | Auswahl | Wohnung, Haus oder Gewerbe | Empfohlen |
|
||||||
|
| Bundesland | Auswahl | Deutsches Bundesland | Empfohlen |
|
||||||
|
| Heizungstyp | Auswahl | Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung | Nein |
|
||||||
|
| Abschreibungszeit | Zahl | Jahre für Abschreibung (Standard: 50) | Nein |
|
||||||
|
| Garage | Checkbox | Garage vorhanden? | Nein |
|
||||||
|
|
||||||
|
#### Live-Berechnungen
|
||||||
|
|
||||||
|
Alle Werte werden **in Echtzeit** berechnet (ohne Seitenreload):
|
||||||
|
|
||||||
|
1. **Gesamtfläche**
|
||||||
|
- Formel: Wohnfläche + Nutzfläche
|
||||||
|
- Einheit: m²
|
||||||
|
|
||||||
|
2. **Preis pro m² Wohnfläche**
|
||||||
|
- Formel: Kaufpreis / Wohnfläche
|
||||||
|
- Einheit: €/m²
|
||||||
|
|
||||||
|
3. **Grunderwerbsteuer**
|
||||||
|
- Formel: Kaufpreis × Grunderwerbsteuersatz des Bundeslandes
|
||||||
|
- Sätze: 3,5% (Bayern) bis 6,5% (Brandenburg, NRW, Saarland, SH)
|
||||||
|
- Einheit: €
|
||||||
|
|
||||||
|
4. **Gesamtkosten**
|
||||||
|
- Formel: Kaufpreis + Grunderwerbsteuer
|
||||||
|
- Einheit: €
|
||||||
|
|
||||||
|
5. **Jährliche Abschreibung**
|
||||||
|
- Formel: Kaufpreis / Abschreibungszeit
|
||||||
|
- Standard-Abschreibungszeit: 50 Jahre
|
||||||
|
- Einheit: €/Jahr
|
||||||
|
|
||||||
|
6. **Alter der Immobilie**
|
||||||
|
- Formel: Aktuelles Jahr - Baujahr
|
||||||
|
- Einheit: Jahre
|
||||||
|
|
||||||
|
### 2. URL-Sharing für anonyme Nutzer
|
||||||
|
|
||||||
|
**Funktion:** "Link teilen" Button
|
||||||
|
|
||||||
|
- Kodiert alle Formulardaten in URL-Parameter
|
||||||
|
- Ermöglicht Teilen der Berechnung ohne Registrierung
|
||||||
|
- Link funktioniert auch nach Tagen/Wochen
|
||||||
|
- Daten werden **nicht** in der Datenbank gespeichert
|
||||||
|
- Ideal für schnelle Berechnungen und Vergleiche
|
||||||
|
|
||||||
|
**Beispiel-URL:**
|
||||||
|
```
|
||||||
|
http://localhost:8080/?adresse=Teststr+123&kaufpreis=300000&wohnflaeche=85&...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Benutzer-Authentifizierung
|
||||||
|
|
||||||
|
#### Registrierung
|
||||||
|
|
||||||
|
**URL:** http://localhost:8080/register
|
||||||
|
|
||||||
|
- Name (Pflicht, mind. 2 Zeichen)
|
||||||
|
- E-Mail (Pflicht, muss gültig und eindeutig sein)
|
||||||
|
- Passwort (Pflicht, mind. 6 Zeichen)
|
||||||
|
- Passwort bestätigen
|
||||||
|
|
||||||
|
**Sicherheit:**
|
||||||
|
- Passwort wird mit bcrypt gehasht
|
||||||
|
- E-Mail muss eindeutig sein
|
||||||
|
- Validierung auf Client- und Server-Seite
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
|
||||||
|
**URL:** http://localhost:8080/login
|
||||||
|
|
||||||
|
- E-Mail
|
||||||
|
- Passwort
|
||||||
|
|
||||||
|
**Session-basiert:**
|
||||||
|
- Bleibt aktiv bis zum Logout
|
||||||
|
- Cookie-basierte Session
|
||||||
|
- CSRF-Schutz integriert
|
||||||
|
|
||||||
|
#### Logout
|
||||||
|
|
||||||
|
- Link im Header "Abmelden"
|
||||||
|
- Beendet Session
|
||||||
|
- Redirect zur Startseite
|
||||||
|
|
||||||
|
### 4. Immobilien speichern (nur eingeloggte Nutzer)
|
||||||
|
|
||||||
|
**Funktion:** "Speichern" Button (erscheint nur nach Login)
|
||||||
|
|
||||||
|
- Speichert alle eingegebenen Daten in der Datenbank
|
||||||
|
- Immobilie wird dem eingeloggten Nutzer zugeordnet
|
||||||
|
- Nutzer kann nur eigene Immobilien sehen
|
||||||
|
- AJAX-basiert (kein Seitenreload)
|
||||||
|
- Erfolgs-/Fehlermeldung als Alert
|
||||||
|
|
||||||
|
### 5. Meine Immobilien
|
||||||
|
|
||||||
|
**URL:** http://localhost:8080/meine-immobilien (nur nach Login)
|
||||||
|
|
||||||
|
**Funktionen:**
|
||||||
|
- Übersicht aller gespeicherten Immobilien
|
||||||
|
- Anzeige aller Details pro Immobilie
|
||||||
|
- Berechnete Werte werden angezeigt
|
||||||
|
- Sortierung nach Erstellungsdatum (neueste zuerst)
|
||||||
|
|
||||||
|
**Angezeigt werden:**
|
||||||
|
- Typ, Kaufpreis, Flächen
|
||||||
|
- Zimmer, Baujahr, Etage
|
||||||
|
- Bundesland, Heizungstyp
|
||||||
|
- Garage (Ja/Nein)
|
||||||
|
- Erstellungsdatum
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Features
|
||||||
|
|
||||||
|
Vollständige Details siehe [API-Dokumentation](api.md)
|
||||||
|
|
||||||
|
### 1. Dual-Authentifizierung
|
||||||
|
|
||||||
|
**Session-basiert (Frontend):**
|
||||||
|
- Form-Login mit E-Mail/Passwort
|
||||||
|
- Cookie-basierte Sessions
|
||||||
|
- Für Web-Interface
|
||||||
|
|
||||||
|
**API-Key-basiert (API):**
|
||||||
|
- Header: `X-API-KEY`
|
||||||
|
- Automatisch generiert bei User-Erstellung
|
||||||
|
- Für programmatischen Zugriff
|
||||||
|
|
||||||
|
### 2. Rollenbasierte Zugriffskontrolle
|
||||||
|
|
||||||
|
| Rolle | Rechte |
|
||||||
|
|-------|--------|
|
||||||
|
| **USER** | Eigene Immobilien verwalten |
|
||||||
|
| **ADMIN** | Alle Immobilien verwalten, alle User sehen |
|
||||||
|
| **MODERATOR** | Erweiterte Rechte |
|
||||||
|
| **TECHNICAL** | Bundesländer & Heizungstypen verwalten |
|
||||||
|
|
||||||
|
### 3. Mandantenfähigkeit
|
||||||
|
|
||||||
|
- Jeder User sieht nur eigene Immobilien
|
||||||
|
- Admins sehen alle Immobilien
|
||||||
|
- Automatische Filterung auf Basis des eingeloggten Users
|
||||||
|
- Keine manuelle Filterung nötig
|
||||||
|
|
||||||
|
### 4. Öffentliche Ressourcen
|
||||||
|
|
||||||
|
**Ohne Authentifizierung verfügbar:**
|
||||||
|
- `GET /api/bundeslands` - Alle Bundesländer mit Grunderwerbsteuersätzen
|
||||||
|
- `GET /api/heizungstyps` - Alle Heizungstypen
|
||||||
|
|
||||||
|
**Vorbefüllt:**
|
||||||
|
- 16 deutsche Bundesländer mit aktuellen Steuersätzen
|
||||||
|
- 4 Heizungstypen (Öl, Gas, Wärmepumpe, Pellet)
|
||||||
|
|
||||||
|
### 5. Swagger/OpenAPI-Dokumentation
|
||||||
|
|
||||||
|
**URL:** http://localhost:8080/api/docs.html
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Interaktive API-Dokumentation
|
||||||
|
- "Try it out" Funktion für alle Endpunkte
|
||||||
|
- Request/Response Beispiele
|
||||||
|
- Schema-Dokumentation
|
||||||
|
- Export als JSON/YAML
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technische Features
|
||||||
|
|
||||||
|
### 1. Responsive Design
|
||||||
|
|
||||||
|
- Mobile-optimiert
|
||||||
|
- Grid-Layout passt sich an Bildschirmgröße an
|
||||||
|
- Touch-freundliche Buttons und Formulare
|
||||||
|
|
||||||
|
### 2. Performance
|
||||||
|
|
||||||
|
- **Live-Berechnungen** ohne Server-Anfragen
|
||||||
|
- **AJAX** für Speichern-Funktion (kein Reload)
|
||||||
|
- **Opcache** aktiviert in Produktion
|
||||||
|
- **Doctrine Query Cache**
|
||||||
|
|
||||||
|
### 3. Datenvalidierung
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- HTML5-Validierung
|
||||||
|
- Required-Felder
|
||||||
|
- Type-Checking (number, email, etc.)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Symfony Validator
|
||||||
|
- Doctrine Constraints
|
||||||
|
- Custom Validation Rules
|
||||||
|
|
||||||
|
### 4. Sicherheit
|
||||||
|
|
||||||
|
- **CSRF-Protection** für alle Forms
|
||||||
|
- **Passwort-Hashing** mit bcrypt
|
||||||
|
- **SQL-Injection-Schutz** durch Doctrine ORM
|
||||||
|
- **XSS-Schutz** durch Twig Auto-Escaping
|
||||||
|
- **CORS-Konfiguration** für API
|
||||||
|
|
||||||
|
### 5. Separation of Concerns
|
||||||
|
|
||||||
|
- **CSS** in separaten Dateien (`public/css/`)
|
||||||
|
- **JavaScript** in separaten Dateien (`public/js/`)
|
||||||
|
- **Templates** mit Twig
|
||||||
|
- **Controller** für Logik
|
||||||
|
- **Entities** für Datenmodell
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Berechnungslogik
|
||||||
|
|
||||||
|
### Grunderwerbsteuer
|
||||||
|
|
||||||
|
Bundesland-spezifisch (Stand 2025):
|
||||||
|
|
||||||
|
| Bundesland | Steuersatz |
|
||||||
|
|------------|-----------|
|
||||||
|
| Bayern | 3,5% |
|
||||||
|
| Baden-Württemberg, Bremen, Niedersachsen, Rheinland-Pfalz, Sachsen-Anhalt, Thüringen | 5,0% |
|
||||||
|
| Hamburg, Sachsen | 5,5% |
|
||||||
|
| Berlin, Hessen, Mecklenburg-Vorpommern | 6,0% |
|
||||||
|
| Brandenburg, NRW, Saarland, Schleswig-Holstein | 6,5% |
|
||||||
|
|
||||||
|
### Kaufnebenkosten (nur API)
|
||||||
|
|
||||||
|
Bei API-Anfragen werden zusätzlich berechnet:
|
||||||
|
- **Notarkosten:** ca. 1,5% des Kaufpreises
|
||||||
|
- **Grundbuchkosten:** ca. 0,5% des Kaufpreises
|
||||||
|
- **Grunderwerbsteuer:** siehe oben
|
||||||
|
- **Gesamt:** Summe aller Nebenkosten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Siehe auch:**
|
||||||
|
- [Technische Dokumentation](technical.md) - Architektur & Datenbank
|
||||||
|
- [API-Dokumentation](api.md) - REST-API Details
|
||||||
|
- [Installation](installation.md) - Setup-Anleitung
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
291
docs/installation.md
Normal file
291
docs/installation.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# Installation & Setup
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- **Docker Desktop** (Windows/Mac) oder **Docker Engine + Docker Compose** (Linux)
|
||||||
|
- **Git**
|
||||||
|
- Mindestens 2GB freier RAM für Docker
|
||||||
|
- Ports 8080, 8081 und 3306 verfügbar
|
||||||
|
|
||||||
|
## Installations-Schritte
|
||||||
|
|
||||||
|
### 1. Repository klonen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd immorechner
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker-Container starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Befehl:
|
||||||
|
- Baut das PHP 8.4 Image mit allen benötigten Extensions
|
||||||
|
- Startet MariaDB Container
|
||||||
|
- Startet phpMyAdmin Container
|
||||||
|
- Startet den Apache-Webserver
|
||||||
|
|
||||||
|
**Hinweis:** Der erste Build kann 5-10 Minuten dauern.
|
||||||
|
|
||||||
|
### 3. Dependencies installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Bundle-Assets installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Schritt ist wichtig für die Swagger UI (CSS, JS, Bilder).
|
||||||
|
|
||||||
|
### 5. Datenbank-Schema erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Migration erstellt:
|
||||||
|
- Users-Tabelle mit Passwort- und API-Key-Feldern
|
||||||
|
- Bundesländer-Tabelle (vorbefüllt mit allen 16 deutschen Bundesländern)
|
||||||
|
- Heizungstypen-Tabelle (vorbefüllt mit Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung)
|
||||||
|
- Immobilien-Tabelle mit allen Relationen
|
||||||
|
|
||||||
|
### 6. Cache leeren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Überprüfung der Installation
|
||||||
|
|
||||||
|
### Anwendung testen
|
||||||
|
|
||||||
|
Öffnen Sie die folgenden URLs in Ihrem Browser:
|
||||||
|
|
||||||
|
1. **Frontend (Rechner):** http://localhost:8080
|
||||||
|
- Sie sollten den Immobilienrechner sehen
|
||||||
|
- Bundesländer-Dropdown sollte befüllt sein
|
||||||
|
- Heizungstypen-Dropdown sollte befüllt sein
|
||||||
|
|
||||||
|
2. **API-Dokumentation:** http://localhost:8080/api/docs.html
|
||||||
|
- Swagger UI sollte mit CSS/Styling laden
|
||||||
|
- Alle API-Endpunkte sollten sichtbar sein
|
||||||
|
|
||||||
|
3. **phpMyAdmin:** http://localhost:8081
|
||||||
|
- Server: `db`
|
||||||
|
- Benutzer: `root`
|
||||||
|
- Passwort: `root`
|
||||||
|
- Datenbank `immorechner` sollte existieren
|
||||||
|
|
||||||
|
### Datenbank überprüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Container einloggen
|
||||||
|
docker-compose exec db bash
|
||||||
|
|
||||||
|
# MariaDB-Client starten
|
||||||
|
mariadb -u root -proot immorechner
|
||||||
|
|
||||||
|
# Tabellen anzeigen
|
||||||
|
SHOW TABLES;
|
||||||
|
|
||||||
|
# Bundesländer überprüfen (sollte 16 Einträge haben)
|
||||||
|
SELECT COUNT(*) FROM bundeslaender;
|
||||||
|
|
||||||
|
# Heizungstypen überprüfen (sollte 4 Einträge haben)
|
||||||
|
SELECT COUNT(*) FROM heizungstypen;
|
||||||
|
|
||||||
|
# Beenden
|
||||||
|
EXIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erste Schritte nach der Installation
|
||||||
|
|
||||||
|
### 1. Testbenutzer anlegen
|
||||||
|
|
||||||
|
#### Via Frontend (Empfohlen)
|
||||||
|
|
||||||
|
1. Öffnen Sie http://localhost:8080/register
|
||||||
|
2. Registrieren Sie sich mit:
|
||||||
|
- Name: "Test User"
|
||||||
|
- E-Mail: "test@example.com"
|
||||||
|
- Passwort: "test123" (min. 6 Zeichen)
|
||||||
|
3. Nach der Registrierung werden Sie zum Login weitergeleitet
|
||||||
|
4. Melden Sie sich an
|
||||||
|
|
||||||
|
#### Via API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"role": "user"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Die API-Response enthält den `apiKey`, den Sie für API-Anfragen benötigen.
|
||||||
|
|
||||||
|
### 2. Erste Immobilie berechnen
|
||||||
|
|
||||||
|
#### Via Frontend
|
||||||
|
|
||||||
|
1. Gehen Sie zu http://localhost:8080/
|
||||||
|
2. Füllen Sie das Formular aus (mindestens: Adresse, Kaufpreis, Wohnfläche)
|
||||||
|
3. Sehen Sie die Live-Berechnungen rechts
|
||||||
|
4. Optional: Klicken Sie auf "Link teilen" zum Teilen
|
||||||
|
5. Wenn angemeldet: Klicken Sie auf "Speichern"
|
||||||
|
|
||||||
|
#### Via API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ersetzen Sie YOUR_API_KEY mit dem API-Key aus Schritt 1
|
||||||
|
curl -X POST http://localhost:8080/api/immobilies \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-KEY: YOUR_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"verwalter": "/api/users/1",
|
||||||
|
"adresse": "Teststraße 123, 12345 Teststadt",
|
||||||
|
"wohnflaeche": 85,
|
||||||
|
"nutzflaeche": 15,
|
||||||
|
"zimmer": 3,
|
||||||
|
"typ": "wohnung",
|
||||||
|
"kaufpreis": 300000,
|
||||||
|
"bundesland": "/api/bundeslands/1"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Umgebungsvariablen anpassen
|
||||||
|
|
||||||
|
Die Datei `.env` enthält alle wichtigen Konfigurationen:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Symfony
|
||||||
|
APP_ENV=dev # Für Produktion: prod
|
||||||
|
APP_SECRET=<generiert> # Für Produktion: neu generieren
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||||
|
|
||||||
|
# CORS (nur für API relevant)
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Für Produktion
|
||||||
|
|
||||||
|
Erstellen Sie eine `.env.local` Datei (wird von Git ignoriert):
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET=<neue-generierte-secret-hier>
|
||||||
|
DATABASE_URL="mysql://prod_user:sichere_passwort@db-host:3306/prod_db?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(ihre-domain\.com)(:[0-9]+)?$'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ports anpassen
|
||||||
|
|
||||||
|
Falls die Standard-Ports bereits belegt sind, können Sie diese in `docker-compose.yml` ändern:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
ports:
|
||||||
|
- "8090:80" # Statt 8080:80
|
||||||
|
|
||||||
|
db:
|
||||||
|
ports:
|
||||||
|
- "3307:3306" # Statt 3306:3306
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
ports:
|
||||||
|
- "8091:80" # Statt 8081:80
|
||||||
|
```
|
||||||
|
|
||||||
|
## Häufige Probleme bei der Installation
|
||||||
|
|
||||||
|
### Container starten nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container-Logs prüfen
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Spezifischen Container-Log prüfen
|
||||||
|
docker-compose logs web
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
|
# Container-Status prüfen
|
||||||
|
docker ps -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission Denied" Fehler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container neu bauen und Berechtigungen setzen
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Berechtigungen im Container setzen
|
||||||
|
docker-compose exec web chown -R www-data:www-data /var/www/html/var
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Verbindungsfehler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfen ob DB-Container läuft
|
||||||
|
docker ps | grep db
|
||||||
|
|
||||||
|
# DB-Logs prüfen
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
|
# Warten bis DB bereit ist (kann 30-60 Sekunden dauern)
|
||||||
|
docker-compose exec web php bin/console doctrine:query:sql "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composer-Fehler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Composer Cache leeren
|
||||||
|
docker-compose exec web composer clear-cache
|
||||||
|
|
||||||
|
# Dependencies neu installieren
|
||||||
|
docker-compose exec web rm -rf vendor
|
||||||
|
docker-compose exec web composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deinstallation
|
||||||
|
|
||||||
|
### Nur Container stoppen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container und Volumes löschen (Datenbank wird gelöscht!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alles entfernen (inkl. Images)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down -v --rmi all
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Nächste Schritte:**
|
||||||
|
- [Features & Funktionalität](features.md) - Übersicht aller Funktionen
|
||||||
|
- [API-Dokumentation](api.md) - REST-API verwenden
|
||||||
|
- [Entwicklung](development.md) - Development-Workflow
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
424
docs/technical.md
Normal file
424
docs/technical.md
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
# Technische Dokumentation
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Technologie-Stack
|
||||||
|
|
||||||
|
- **Backend**: PHP 8.4
|
||||||
|
- **Framework**: Symfony 7.3
|
||||||
|
- **Datenbank**: MariaDB 11.7
|
||||||
|
- **ORM**: Doctrine 3.0
|
||||||
|
- **API**: API Platform 4.2
|
||||||
|
- **Template Engine**: Twig 3.22
|
||||||
|
- **Webserver**: Apache 2.4 mit mod_rewrite
|
||||||
|
- **Container**: Docker & Docker Compose
|
||||||
|
- **Frontend**: jQuery 3.7.1, separates CSS/JS
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
### Schichtenmodell
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Presentation Layer │
|
||||||
|
│ (Twig Templates, CSS, JavaScript) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Controller Layer │
|
||||||
|
│ (HomeController, AuthController, │
|
||||||
|
│ ImmobilienSaveController) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Service Layer │
|
||||||
|
│ (Security, Validation, Business │
|
||||||
|
│ Logic) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Persistence Layer │
|
||||||
|
│ (Doctrine ORM, Repositories) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Database Layer │
|
||||||
|
│ (MariaDB) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Komponenten
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **Templates:** `templates/` (Twig)
|
||||||
|
- **CSS:** `public/css/` (calculator.css, auth.css)
|
||||||
|
- **JavaScript:** `public/js/` (calculator.js mit jQuery)
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- **Controller:** `src/Controller/` (Request Handling)
|
||||||
|
- **Entity:** `src/Entity/` (Datenmodelle)
|
||||||
|
- **Repository:** `src/Repository/` (Datenbank-Queries)
|
||||||
|
- **Security:** `src/Security/` (Authentifizierung)
|
||||||
|
- **Enum:** `src/Enum/` (PHP Enums)
|
||||||
|
|
||||||
|
## Datenbank-Schema
|
||||||
|
|
||||||
|
### Entity-Relationship-Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌────────────────┐
|
||||||
|
│ User │ 1 * │ Immobilie │
|
||||||
|
│──────────────│◄────────│────────────────│
|
||||||
|
│ id │ │ id │
|
||||||
|
│ name │ │ verwalter_id │
|
||||||
|
│ email │ │ adresse │
|
||||||
|
│ password │ │ wohnflaeche │
|
||||||
|
│ role │ │ nutzflaeche │
|
||||||
|
│ api_key │ │ zimmer │
|
||||||
|
│ created_at │ │ kaufpreis │
|
||||||
|
└──────────────┘ │ typ │
|
||||||
|
│ ... │
|
||||||
|
└────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────┴──────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────┐ ┌─────────────────┐
|
||||||
|
│ Bundesland │ │ Heizungstyp │
|
||||||
|
│───────────────│ │─────────────────│
|
||||||
|
│ id │ │ id │
|
||||||
|
│ name │ │ name │
|
||||||
|
│ grunderwerbst.│ └─────────────────┘
|
||||||
|
└───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabellen-Details
|
||||||
|
|
||||||
|
#### User-Tabelle
|
||||||
|
|
||||||
|
| Feld | Typ | Constraints | Beschreibung |
|
||||||
|
|------|-----|-------------|--------------|
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||||
|
| name | VARCHAR(255) | NOT NULL | Benutzername |
|
||||||
|
| email | VARCHAR(255) | NOT NULL, UNIQUE | E-Mail-Adresse |
|
||||||
|
| password | VARCHAR(255) | NULL | Bcrypt-Hash des Passworts |
|
||||||
|
| role | VARCHAR(255) | NOT NULL | Benutzerrolle (Enum) |
|
||||||
|
| api_key | VARCHAR(64) | NOT NULL, UNIQUE | SHA256 API-Key |
|
||||||
|
| created_at | DATETIME | NOT NULL | Erstellungsdatum |
|
||||||
|
|
||||||
|
**Benutzerrollen (Enum):**
|
||||||
|
- `user` - Normaler Benutzer
|
||||||
|
- `admin` - Administrator
|
||||||
|
- `moderator` - Moderator
|
||||||
|
- `technical` - Technischer User
|
||||||
|
|
||||||
|
#### Bundesland-Tabelle
|
||||||
|
|
||||||
|
| Feld | Typ | Constraints | Beschreibung |
|
||||||
|
|------|-----|-------------|--------------|
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL, UNIQUE | Name des Bundeslandes |
|
||||||
|
| grunderwerbsteuer | DECIMAL(4,2) | NOT NULL | Steuersatz in % |
|
||||||
|
|
||||||
|
**Vorbefüllt mit 16 deutschen Bundesländern**
|
||||||
|
|
||||||
|
#### Heizungstyp-Tabelle
|
||||||
|
|
||||||
|
| Feld | Typ | Constraints | Beschreibung |
|
||||||
|
|------|-----|-------------|--------------|
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL, UNIQUE | Name des Heizungstyps |
|
||||||
|
|
||||||
|
**Vorbefüllt mit:** Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung
|
||||||
|
|
||||||
|
#### Immobilien-Tabelle
|
||||||
|
|
||||||
|
| Feld | Typ | Constraints | Beschreibung |
|
||||||
|
|------|-----|-------------|--------------|
|
||||||
|
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||||
|
| verwalter_id | INT | FK → users.id, NOT NULL | Besitzer der Immobilie |
|
||||||
|
| bundesland_id | INT | FK → bundeslaender.id, NULL | Bundesland |
|
||||||
|
| heizungstyp_id | INT | FK → heizungstypen.id, NULL | Heizungstyp |
|
||||||
|
| adresse | VARCHAR(255) | NOT NULL | Vollständige Adresse |
|
||||||
|
| wohnflaeche | INT | NOT NULL | Wohnfläche in m² |
|
||||||
|
| nutzflaeche | INT | NOT NULL | Nutzfläche in m² |
|
||||||
|
| garage | BOOLEAN | NOT NULL, DEFAULT false | Garage vorhanden? |
|
||||||
|
| zimmer | INT | NOT NULL | Anzahl Zimmer |
|
||||||
|
| baujahr | INT | NULL | Baujahr (1800-2100) |
|
||||||
|
| typ | VARCHAR(255) | NOT NULL | Immobilientyp (Enum) |
|
||||||
|
| beschreibung | TEXT | NULL | Freitextbeschreibung |
|
||||||
|
| etage | INT | NULL | Stockwerk (0-10) |
|
||||||
|
| kaufpreis | INT | NULL | Kaufpreis in Euro |
|
||||||
|
| abschreibungszeit | INT | NULL | Abschreibungszeit in Jahren |
|
||||||
|
| created_at | DATETIME | NOT NULL | Erstellungsdatum |
|
||||||
|
| updated_at | DATETIME | NOT NULL | Letzte Änderung |
|
||||||
|
|
||||||
|
**Immobilientyp (Enum):**
|
||||||
|
- `wohnung` - Wohnung
|
||||||
|
- `haus` - Haus
|
||||||
|
- `grundstueck` - Grundstück
|
||||||
|
- `gewerbe` - Gewerbe
|
||||||
|
- `buero` - Büro
|
||||||
|
|
||||||
|
### Indizes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- User
|
||||||
|
CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email);
|
||||||
|
CREATE UNIQUE INDEX UNIQ_1483A5E9C912ED9D ON users (api_key);
|
||||||
|
|
||||||
|
-- Bundesland
|
||||||
|
CREATE UNIQUE INDEX UNIQ_DF7DFAB25E237E06 ON bundeslaender (name);
|
||||||
|
|
||||||
|
-- Heizungstyp
|
||||||
|
CREATE UNIQUE INDEX UNIQ_6161C2A65E237E06 ON heizungstypen (name);
|
||||||
|
|
||||||
|
-- Immobilie
|
||||||
|
CREATE INDEX IDX_2C789D3E5F66D3 ON immobilien (verwalter_id);
|
||||||
|
CREATE INDEX IDX_2C789DC1B4DB52 ON immobilien (heizungstyp_id);
|
||||||
|
CREATE INDEX IDX_2C789DB74FDBEB ON immobilien (bundesland_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Umgebungsvariablen (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Symfony
|
||||||
|
APP_ENV=dev # Umgebung: dev | prod | test
|
||||||
|
APP_SECRET=<32-zeichen-hex> # Symfony Secret für Verschlüsselung
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||||
|
|
||||||
|
# Routing
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
|
||||||
|
# CORS (für API)
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services (config/services.yaml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
exclude:
|
||||||
|
- '../src/Entity/'
|
||||||
|
- '../src/Repository/'
|
||||||
|
- '../src/Kernel.php'
|
||||||
|
|
||||||
|
App\Controller\:
|
||||||
|
resource: '../src/Controller/'
|
||||||
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
|
App\Repository\:
|
||||||
|
resource: '../src/Repository/'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security (config/packages/security.yaml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
|
providers:
|
||||||
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: email
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
# API mit API-Key
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\ApiKeyAuthenticator
|
||||||
|
|
||||||
|
# Frontend mit Form-Login
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Platform (config/packages/api_platform.yaml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api_platform:
|
||||||
|
title: 'Immorechner API'
|
||||||
|
version: 1.0.0
|
||||||
|
mapping:
|
||||||
|
paths: ['%kernel.project_dir%/src/Entity']
|
||||||
|
defaults:
|
||||||
|
stateless: true
|
||||||
|
formats:
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
json: ['application/json']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker-Konfiguration
|
||||||
|
|
||||||
|
### docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: immorechner_web
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: immorechner_db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: immorechner
|
||||||
|
MYSQL_USER: immorechner_user
|
||||||
|
MYSQL_PASSWORD: immorechner_pass
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin:latest
|
||||||
|
container_name: immorechner_phpmyadmin
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
PMA_USER: root
|
||||||
|
PMA_PASSWORD: root
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM php:8.4-apache
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libpng-dev libonig-dev libxml2-dev libicu-dev \
|
||||||
|
libzip-dev git curl unzip
|
||||||
|
|
||||||
|
# Install PHP extensions
|
||||||
|
RUN docker-php-ext-install pdo_mysql mbstring exif \
|
||||||
|
pcntl bcmath gd zip intl opcache
|
||||||
|
|
||||||
|
# Enable Apache modules
|
||||||
|
RUN a2enmod rewrite headers
|
||||||
|
|
||||||
|
# Copy Composer
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY . /var/www/html
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
RUN chown -R www-data:www-data /var/www/html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance-Optimierungen
|
||||||
|
|
||||||
|
### Opcache (Produktion)
|
||||||
|
|
||||||
|
In Produktion ist Opcache aktiviert:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
opcache.enable=1
|
||||||
|
opcache.memory_consumption=256
|
||||||
|
opcache.max_accelerated_files=20000
|
||||||
|
opcache.validate_timestamps=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Doctrine Query Cache
|
||||||
|
|
||||||
|
Doctrine nutzt automatisch Query-Cache in Produktion.
|
||||||
|
|
||||||
|
### Asset Compilation
|
||||||
|
|
||||||
|
Assets werden bei Deployment kompiliert:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console assets:install public --symlink --relative
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
### CSRF-Schutz
|
||||||
|
|
||||||
|
Alle Forms verwenden CSRF-Tokens:
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passwort-Hashing
|
||||||
|
|
||||||
|
Passwörter werden mit bcrypt gehasht:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$hashedPassword = $passwordHasher->hashPassword($user, $password);
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-Key-Generierung
|
||||||
|
|
||||||
|
API-Keys sind SHA256-Hashes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return hash('sha256', random_bytes(32).microtime(true));
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL-Injection-Schutz
|
||||||
|
|
||||||
|
Doctrine ORM verhindert SQL-Injection durch Prepared Statements.
|
||||||
|
|
||||||
|
### XSS-Schutz
|
||||||
|
|
||||||
|
Twig escaped automatisch alle Ausgaben.
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Migrationen werden mit Doctrine Migrations verwaltet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Neue Migration erstellen
|
||||||
|
php bin/console doctrine:migrations:diff
|
||||||
|
|
||||||
|
# Migrationen ausführen
|
||||||
|
php bin/console doctrine:migrations:migrate
|
||||||
|
|
||||||
|
# Migration-Status anzeigen
|
||||||
|
php bin/console doctrine:migrations:status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Siehe auch:**
|
||||||
|
- [Installation](installation.md) - Setup-Anleitung
|
||||||
|
- [Development](development.md) - Entwickler-Workflow
|
||||||
|
- [Docker](docker.md) - Container-Management
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
432
docs/troubleshooting.md
Normal file
432
docs/troubleshooting.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# Fehlerbehebung
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
|
|
||||||
|
## Häufige Probleme
|
||||||
|
|
||||||
|
### Frontend-Probleme
|
||||||
|
|
||||||
|
#### Seite lädt ohne CSS/JavaScript
|
||||||
|
|
||||||
|
**Problem:** Seite wird angezeigt, aber ohne Styling oder JavaScript-Funktionalität.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Assets installieren
|
||||||
|
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Browser-Cache leeren (Strg+F5)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Live-Berechnungen funktionieren nicht
|
||||||
|
|
||||||
|
**Problem:** Formular-Eingaben triggern keine Berechnungen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Browser-Konsole öffnen (F12)
|
||||||
|
# Nach JavaScript-Fehlern suchen
|
||||||
|
|
||||||
|
# Prüfen ob jQuery geladen wurde
|
||||||
|
# Sollte in Netzwerk-Tab sichtbar sein: jquery-3.7.1.min.js
|
||||||
|
|
||||||
|
# calculator.js prüfen
|
||||||
|
curl http://localhost:8080/js/calculator.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Login funktioniert nicht
|
||||||
|
|
||||||
|
**Problem:** Login-Formular gibt Fehler oder leitet nicht weiter.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Prüfen ob User existiert
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SELECT email FROM users" immorechner
|
||||||
|
|
||||||
|
# Session-Verzeichnis prüfen
|
||||||
|
docker-compose exec web ls -la /var/www/html/var/cache/dev/sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API-Probleme
|
||||||
|
|
||||||
|
#### Swagger UI lädt ohne CSS
|
||||||
|
|
||||||
|
**Problem:** http://localhost:8080/api/docs.html lädt, aber ohne Formatierung.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Bundle-Assets installieren
|
||||||
|
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Prüfen ob Assets existieren
|
||||||
|
docker-compose exec web ls -la /var/www/html/public/bundles/apiplatform
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API gibt 401 Unauthorized
|
||||||
|
|
||||||
|
**Problem:** API-Anfrage mit API-Key schlägt fehl.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# API-Key prüfen
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SELECT id, email, api_key FROM users" immorechner
|
||||||
|
|
||||||
|
# Korrekten Header verwenden
|
||||||
|
curl -H "X-API-KEY: ihr-api-key" http://localhost:8080/api/immobilies
|
||||||
|
|
||||||
|
# Nicht "Authorization" Header!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API gibt 500 Internal Server Error
|
||||||
|
|
||||||
|
**Problem:** API-Anfrage schlägt mit Server-Fehler fehl.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Logs prüfen
|
||||||
|
docker-compose logs web
|
||||||
|
|
||||||
|
# Symfony Logs prüfen
|
||||||
|
docker-compose exec web tail -f /var/www/html/var/log/dev.log
|
||||||
|
|
||||||
|
# Datenbank-Verbindung testen
|
||||||
|
docker-compose exec web php bin/console doctrine:query:sql "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Datenbank-Probleme
|
||||||
|
|
||||||
|
#### Connection refused
|
||||||
|
|
||||||
|
**Problem:** Kann nicht zur Datenbank verbinden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# DB-Container läuft?
|
||||||
|
docker ps | grep db
|
||||||
|
|
||||||
|
# DB-Container-Logs
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
|
# Warten (DB braucht ~30-60 Sekunden)
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# DB-Verbindung testen
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SELECT 1"
|
||||||
|
|
||||||
|
# Neustart
|
||||||
|
docker-compose restart db
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migrations schlagen fehl
|
||||||
|
|
||||||
|
**Problem:** `doctrine:migrations:migrate` gibt Fehler.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Migration-Status prüfen
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:status
|
||||||
|
|
||||||
|
# Datenbank droppen und neu erstellen
|
||||||
|
docker-compose exec web php bin/console doctrine:database:drop --force
|
||||||
|
docker-compose exec web php bin/console doctrine:database:create
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# Oder: Container komplett neu aufsetzen
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d --build
|
||||||
|
docker-compose exec web composer install
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tabellen fehlen
|
||||||
|
|
||||||
|
**Problem:** "Table 'immorechner.users' doesn't exist"
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Migrations ausführen
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# Tabellen prüfen
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SHOW TABLES" immorechner
|
||||||
|
|
||||||
|
# Falls leer: Migration erneut ausführen
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Docker-Probleme
|
||||||
|
|
||||||
|
#### Container starten nicht
|
||||||
|
|
||||||
|
**Problem:** `docker-compose up` schlägt fehl.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Logs prüfen
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Alte Container stoppen
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Neu starten mit Build
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Docker neu starten
|
||||||
|
# Windows: Docker Desktop neu starten
|
||||||
|
# Linux: sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Port bereits belegt
|
||||||
|
|
||||||
|
**Problem:** "Port 8080 is already allocated"
|
||||||
|
|
||||||
|
**Lösung 1 - Port freigeben:**
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :8080
|
||||||
|
taskkill /PID <prozess-id> /F
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
lsof -i :8080
|
||||||
|
kill -9 <prozess-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lösung 2 - Port ändern:**
|
||||||
|
```yaml
|
||||||
|
# In docker-compose.yml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
ports:
|
||||||
|
- "8090:80" # Statt 8080:80
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Volumes löschen nicht möglich
|
||||||
|
|
||||||
|
**Problem:** "Volume is in use"
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Alle Container stoppen
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Volume prüfen
|
||||||
|
docker volume ls
|
||||||
|
|
||||||
|
# Container mit Volume finden
|
||||||
|
docker ps -a --filter volume=immorechner_db_data
|
||||||
|
|
||||||
|
# Container löschen
|
||||||
|
docker rm <container-id>
|
||||||
|
|
||||||
|
# Volume löschen
|
||||||
|
docker volume rm immorechner_db_data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Composer-Probleme
|
||||||
|
|
||||||
|
#### "Class not found" Fehler
|
||||||
|
|
||||||
|
**Problem:** PHP kann Klasse nicht finden.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Autoloader neu generieren
|
||||||
|
docker-compose exec web composer dump-autoload
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Dependencies neu installieren
|
||||||
|
docker-compose exec web rm -rf vendor
|
||||||
|
docker-compose exec web composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Composer sehr langsam
|
||||||
|
|
||||||
|
**Problem:** `composer install` braucht ewig.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Im Container ausführen (schneller als via Volume)
|
||||||
|
docker-compose exec web composer install
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web composer clear-cache
|
||||||
|
|
||||||
|
# Parallel Downloads aktivieren
|
||||||
|
docker-compose exec web composer config --global process-timeout 2000
|
||||||
|
docker-compose exec web composer config --global repos.packagist composer https://packagist.org
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Berechtigungs-Probleme
|
||||||
|
|
||||||
|
#### "Permission denied" in var/
|
||||||
|
|
||||||
|
**Problem:** Kann nicht in `var/cache` oder `var/log` schreiben.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Berechtigungen setzen
|
||||||
|
docker-compose exec -u root web chown -R www-data:www-data /var/www/html/var
|
||||||
|
|
||||||
|
# Oder: 777 Rechte (nur Development!)
|
||||||
|
docker-compose exec -u root web chmod -R 777 /var/www/html/var
|
||||||
|
|
||||||
|
# Cache neu erstellen
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Performance-Probleme
|
||||||
|
|
||||||
|
#### Seite lädt sehr langsam
|
||||||
|
|
||||||
|
**Problem:** Frontend/API antwortet langsam.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Opcache Status prüfen
|
||||||
|
docker-compose exec web php -i | grep opcache
|
||||||
|
|
||||||
|
# Cache warmup
|
||||||
|
docker-compose exec web php bin/console cache:warmup
|
||||||
|
|
||||||
|
# Für Produktion: APP_ENV=prod setzen in .env
|
||||||
|
# Opcache aktiviert sich automatisch
|
||||||
|
|
||||||
|
# Docker-Resources erhöhen
|
||||||
|
# Docker Desktop -> Settings -> Resources -> Memory: 4GB+
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Datenbank-Queries langsam
|
||||||
|
|
||||||
|
**Problem:** API-Anfragen langsam bei vielen Datensätzen.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Indizes prüfen
|
||||||
|
docker-compose exec web php bin/console doctrine:schema:validate
|
||||||
|
|
||||||
|
# Query-Log aktivieren (temporär)
|
||||||
|
# In config/packages/doctrine.yaml:
|
||||||
|
logging: true
|
||||||
|
profiling: true
|
||||||
|
|
||||||
|
# Slow Query Log in MariaDB aktivieren
|
||||||
|
docker-compose exec db mariadb -u root -proot -e "SET GLOBAL slow_query_log = 'ON'"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Browser-Probleme
|
||||||
|
|
||||||
|
#### Alte Daten werden angezeigt
|
||||||
|
|
||||||
|
**Problem:** Änderungen werden nicht sichtbar.
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Browser-Cache leeren
|
||||||
|
# Chrome/Firefox: Strg+Shift+Delete
|
||||||
|
|
||||||
|
# Hard Reload
|
||||||
|
# Chrome/Firefox: Strg+F5
|
||||||
|
# Mac: Cmd+Shift+R
|
||||||
|
|
||||||
|
# Server-Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug-Modus aktivieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_DEBUG=true
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
|
||||||
|
# Symfony Profiler in Browser verwenden
|
||||||
|
# Unten auf der Seite erscheint Toolbar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komplett-Reset (Letzter Ausweg)
|
||||||
|
|
||||||
|
**WARNUNG:** Löscht alle Daten!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alles stoppen und löschen
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Docker-Images löschen
|
||||||
|
docker rmi immorechner-web
|
||||||
|
|
||||||
|
# Neu aufbauen
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Dependencies installieren
|
||||||
|
docker-compose exec web composer install
|
||||||
|
|
||||||
|
# Datenbank initialisieren
|
||||||
|
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# Cache leeren
|
||||||
|
docker-compose exec web php bin/console cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Wenn das Problem weiterhin besteht:
|
||||||
|
|
||||||
|
1. **Logs sammeln:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs > debug_logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Systeminfo sammeln:**
|
||||||
|
```bash
|
||||||
|
docker version > system_info.txt
|
||||||
|
docker-compose version >> system_info.txt
|
||||||
|
php -v >> system_info.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Issue erstellen** mit:
|
||||||
|
- Problembeschreibung
|
||||||
|
- Fehlermeldung
|
||||||
|
- Logs (debug_logs.txt)
|
||||||
|
- Systeminfo (system_info.txt)
|
||||||
|
- Schritte zum Reproduzieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Siehe auch:**
|
||||||
|
- [Docker](docker.md) - Container-Management
|
||||||
|
- [Development](development.md) - Entwickler-Workflow
|
||||||
|
- [Installation](installation.md) - Setup-Anleitung
|
||||||
|
|
||||||
|
[← Zurück zur Hauptseite](../README.md)
|
||||||
0
migrations/.gitignore
vendored
Normal file
0
migrations/.gitignore
vendored
Normal file
120
migrations/Version20251109100000.php
Normal file
120
migrations/Version20251109100000.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial migration: Creates all tables and populates reference data
|
||||||
|
*/
|
||||||
|
final class Version20251109100000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Initial database schema with all tables and reference data';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Create users table
|
||||||
|
$this->addSql('CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
role VARCHAR(255) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
api_key VARCHAR(64) NOT NULL,
|
||||||
|
UNIQUE INDEX UNIQ_1483A5E9E7927C74 (email),
|
||||||
|
UNIQUE INDEX UNIQ_1483A5E9C912ED9D (api_key),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
|
||||||
|
// Create bundeslaender table
|
||||||
|
$this->addSql('CREATE TABLE bundeslaender (
|
||||||
|
id INT AUTO_INCREMENT NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
grunderwerbsteuer NUMERIC(4, 2) NOT NULL,
|
||||||
|
UNIQUE INDEX UNIQ_DF7DFAB25E237E06 (name),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
|
||||||
|
// Create heizungstypen table
|
||||||
|
$this->addSql('CREATE TABLE heizungstypen (
|
||||||
|
id INT AUTO_INCREMENT NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
UNIQUE INDEX UNIQ_6161C2A65E237E06 (name),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
|
||||||
|
// Create immobilien table
|
||||||
|
$this->addSql('CREATE TABLE immobilien (
|
||||||
|
id INT AUTO_INCREMENT NOT NULL,
|
||||||
|
verwalter_id INT NOT NULL,
|
||||||
|
heizungstyp_id INT DEFAULT NULL,
|
||||||
|
bundesland_id INT DEFAULT NULL,
|
||||||
|
adresse VARCHAR(255) NOT NULL,
|
||||||
|
wohnflaeche INT NOT NULL,
|
||||||
|
nutzflaeche INT NOT NULL,
|
||||||
|
garage TINYINT(1) NOT NULL,
|
||||||
|
zimmer INT NOT NULL,
|
||||||
|
baujahr INT DEFAULT NULL,
|
||||||
|
typ VARCHAR(255) NOT NULL,
|
||||||
|
beschreibung LONGTEXT DEFAULT NULL,
|
||||||
|
etage INT DEFAULT NULL,
|
||||||
|
kaufpreis INT DEFAULT NULL,
|
||||||
|
abschreibungszeit INT DEFAULT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
INDEX IDX_2C789D3E5F66D3 (verwalter_id),
|
||||||
|
INDEX IDX_2C789DC1B4DB52 (heizungstyp_id),
|
||||||
|
INDEX IDX_2C789DB74FDBEB (bundesland_id),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
|
||||||
|
// Add foreign keys
|
||||||
|
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789D3E5F66D3 FOREIGN KEY (verwalter_id) REFERENCES users (id)');
|
||||||
|
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789DC1B4DB52 FOREIGN KEY (heizungstyp_id) REFERENCES heizungstypen (id)');
|
||||||
|
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789DB74FDBEB FOREIGN KEY (bundesland_id) REFERENCES bundeslaender (id)');
|
||||||
|
|
||||||
|
// Populate Bundesländer with Grunderwerbsteuer rates
|
||||||
|
$this->addSql("INSERT INTO bundeslaender (name, grunderwerbsteuer) VALUES
|
||||||
|
('Baden-Württemberg', 5.00),
|
||||||
|
('Bayern', 3.50),
|
||||||
|
('Berlin', 6.00),
|
||||||
|
('Brandenburg', 6.50),
|
||||||
|
('Bremen', 5.00),
|
||||||
|
('Hamburg', 5.50),
|
||||||
|
('Hessen', 6.00),
|
||||||
|
('Mecklenburg-Vorpommern', 6.00),
|
||||||
|
('Niedersachsen', 5.00),
|
||||||
|
('Nordrhein-Westfalen', 6.50),
|
||||||
|
('Rheinland-Pfalz', 5.00),
|
||||||
|
('Saarland', 6.50),
|
||||||
|
('Sachsen', 5.50),
|
||||||
|
('Sachsen-Anhalt', 5.00),
|
||||||
|
('Schleswig-Holstein', 6.50),
|
||||||
|
('Thüringen', 5.00)");
|
||||||
|
|
||||||
|
// Populate Heizungstypen
|
||||||
|
$this->addSql("INSERT INTO heizungstypen (name) VALUES
|
||||||
|
('Ölheizung'),
|
||||||
|
('Gasheizung'),
|
||||||
|
('Wärmepumpe'),
|
||||||
|
('Pelletheizung')");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Drop all tables
|
||||||
|
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789D3E5F66D3');
|
||||||
|
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789DC1B4DB52');
|
||||||
|
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789DB74FDBEB');
|
||||||
|
$this->addSql('DROP TABLE immobilien');
|
||||||
|
$this->addSql('DROP TABLE bundeslaender');
|
||||||
|
$this->addSql('DROP TABLE heizungstypen');
|
||||||
|
$this->addSql('DROP TABLE users');
|
||||||
|
}
|
||||||
|
}
|
||||||
22
migrations/bundeslaender_data.sql
Normal file
22
migrations/bundeslaender_data.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Bundesländer mit Grunderwerbsteuer-Sätzen (Stand 2025)
|
||||||
|
-- Dieses Script wird nach der Migration automatisch ausgeführt
|
||||||
|
|
||||||
|
INSERT INTO bundeslaender (name, grunderwerbsteuer) VALUES
|
||||||
|
('Baden-Württemberg', 5.00),
|
||||||
|
('Bayern', 3.50),
|
||||||
|
('Berlin', 6.00),
|
||||||
|
('Brandenburg', 6.50),
|
||||||
|
('Bremen', 5.00),
|
||||||
|
('Hamburg', 5.50),
|
||||||
|
('Hessen', 6.00),
|
||||||
|
('Mecklenburg-Vorpommern', 6.00),
|
||||||
|
('Niedersachsen', 5.00),
|
||||||
|
('Nordrhein-Westfalen', 6.50),
|
||||||
|
('Rheinland-Pfalz', 5.00),
|
||||||
|
('Saarland', 6.50),
|
||||||
|
('Sachsen', 5.50),
|
||||||
|
('Sachsen-Anhalt', 5.00),
|
||||||
|
('Schleswig-Holstein', 6.50),
|
||||||
|
('Thüringen', 5.00)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
grunderwerbsteuer = VALUES(grunderwerbsteuer);
|
||||||
1
openapi.json
Normal file
1
openapi.json
Normal file
File diff suppressed because one or more lines are too long
44
phpunit.dist.xml
Normal file
44
phpunit.dist.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="display_errors" value="1" />
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
<server name="APP_ENV" value="test" force="true" />
|
||||||
|
<server name="SHELL_VERBOSITY" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Project Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source ignoreSuppressionOfDeprecations="true"
|
||||||
|
ignoreIndirectDeprecations="true"
|
||||||
|
restrictNotices="true"
|
||||||
|
restrictWarnings="true"
|
||||||
|
>
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
|
||||||
|
<deprecationTrigger>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::trigger</method>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
|
||||||
|
<function>trigger_deprecation</function>
|
||||||
|
</deprecationTrigger>
|
||||||
|
</source>
|
||||||
|
|
||||||
|
<extensions>
|
||||||
|
</extensions>
|
||||||
|
</phpunit>
|
||||||
66
public/.htaccess
Normal file
66
public/.htaccess
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Use the front controller as index file. It serves as a fallback solution when
|
||||||
|
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||||
|
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||||
|
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||||
|
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||||
|
DirectoryIndex index.php
|
||||||
|
|
||||||
|
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||||
|
# feature in your server configuration. Uncomment the following line if you
|
||||||
|
# install assets as symlinks or if you experience problems related to symlinks
|
||||||
|
# when compiling LESS/Sass/CoffeeScript assets.
|
||||||
|
# Options +FollowSymlinks
|
||||||
|
|
||||||
|
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||||
|
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Determine the RewriteBase automatically and set it as environment variable.
|
||||||
|
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||||
|
# project in a subdirectory, the base path will be prepended to allow proper
|
||||||
|
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||||
|
# work in environments without path prefix as well, providing a safe, one-size
|
||||||
|
# fits all solution. But as you do not need it in this case, you can comment
|
||||||
|
# the following 2 lines to eliminate the overhead.
|
||||||
|
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||||
|
RewriteRule .* - [E=BASE:%1]
|
||||||
|
|
||||||
|
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||||
|
RewriteCond %{HTTP:Authorization} .+
|
||||||
|
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||||
|
|
||||||
|
# Redirect to URI without front controller to prevent duplicate content
|
||||||
|
# (with and without `/index.php`). Only do this redirect on the initial
|
||||||
|
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||||
|
# endless redirect loop (request -> rewrite to front controller ->
|
||||||
|
# redirect -> request -> ...).
|
||||||
|
# So in case you get a "too many redirects" error or you always get redirected
|
||||||
|
# to the start page because your Apache does not expose the REDIRECT_STATUS
|
||||||
|
# environment variable, you have 2 choices:
|
||||||
|
# - disable this feature by commenting the following 2 lines or
|
||||||
|
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||||
|
# following RewriteCond (best solution)
|
||||||
|
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||||
|
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||||
|
|
||||||
|
# If the requested filename exists, simply serve it.
|
||||||
|
# We only want to let Apache serve files and not directories.
|
||||||
|
# Rewrite all other queries to the front controller.
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule !mod_rewrite.c>
|
||||||
|
<IfModule mod_alias.c>
|
||||||
|
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||||
|
# the start page to the front controller explicitly so that the website
|
||||||
|
# and the generated links can still be used.
|
||||||
|
RedirectMatch 307 ^/$ /index.php/
|
||||||
|
# RedirectTemp cannot be used instead
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
public/index.php
Normal file
9
public/index.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
};
|
||||||
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();
|
||||||
|
});
|
||||||
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
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!');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Controller/HomeController.php
Normal file
46
src/Controller/HomeController.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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(
|
||||||
|
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', [
|
||||||
|
'bundeslaender' => $bundeslaender,
|
||||||
|
'heizungstypen' => $heizungstypen,
|
||||||
|
'immobilienData' => $immobilienData,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Controller/ImmobilieController.php
Normal file
37
src/Controller/ImmobilieController.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Immobilie;
|
||||||
|
use App\Repository\ImmobilieRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/immobilien')]
|
||||||
|
class ImmobilieController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/', name: 'app_immobilie_index')]
|
||||||
|
public function index(ImmobilieRepository $repository): Response
|
||||||
|
{
|
||||||
|
$immobilien = $repository->findVerfuegbare();
|
||||||
|
|
||||||
|
return $this->render('immobilie/index.html.twig', [
|
||||||
|
'immobilien' => $immobilien,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', name: 'app_immobilie_show', requirements: ['id' => '\d+'])]
|
||||||
|
public function show(Immobilie $immobilie): Response
|
||||||
|
{
|
||||||
|
return $this->render('immobilie/show.html.twig', [
|
||||||
|
'immobilie' => $immobilie,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/suche', name: 'app_immobilie_suche')]
|
||||||
|
public function suche(ImmobilieRepository $repository): Response
|
||||||
|
{
|
||||||
|
return $this->render('immobilie/suche.html.twig');
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Doctrine/CurrentUserExtension.php
Normal file
58
src/Doctrine/CurrentUserExtension.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Doctrine;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use App\Entity\Immobilie;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Enum\UserRole;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
|
||||||
|
{
|
||||||
|
$this->addWhere($queryBuilder, $resourceClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void
|
||||||
|
{
|
||||||
|
$this->addWhere($queryBuilder, $resourceClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
|
||||||
|
{
|
||||||
|
// Nur für Immobilie-Entity
|
||||||
|
if (Immobilie::class !== $resourceClass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
// Wenn nicht eingeloggt, keine Ergebnisse
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
$queryBuilder->andWhere('1 = 0');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin sieht alles
|
||||||
|
if (UserRole::ADMIN === $user->getRole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normale User sehen nur eigene Immobilien
|
||||||
|
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||||
|
$queryBuilder->andWhere(sprintf('%s.verwalter = :current_user', $rootAlias))
|
||||||
|
->setParameter('current_user', $user);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
71
src/Entity/Bundesland.php
Normal file
71
src/Entity/Bundesland.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
|
use App\Repository\BundeslandRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: BundeslandRepository::class)]
|
||||||
|
#[ORM\Table(name: 'bundeslaender')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||||
|
new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||||
|
new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class Bundesland
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(min: 2, max: 100)]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'decimal', precision: 4, scale: 2)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Range(min: 0, max: 100)]
|
||||||
|
private float $grunderwerbsteuer;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGrunderwerbsteuer(): float
|
||||||
|
{
|
||||||
|
return $this->grunderwerbsteuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGrunderwerbsteuer(float $grunderwerbsteuer): self
|
||||||
|
{
|
||||||
|
$this->grunderwerbsteuer = $grunderwerbsteuer;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/Entity/Heizungstyp.php
Normal file
54
src/Entity/Heizungstyp.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
|
use App\Repository\HeizungstypRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: HeizungstypRepository::class)]
|
||||||
|
#[ORM\Table(name: 'heizungstypen')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||||
|
new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||||
|
new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class Heizungstyp
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(min: 2, max: 100)]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
359
src/Entity/Immobilie.php
Normal file
359
src/Entity/Immobilie.php
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Enum\ImmobilienTyp;
|
||||||
|
use App\Repository\ImmobilieRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImmobilieRepository::class)]
|
||||||
|
#[ORM\Table(name: 'immobilien')]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: 'is_granted("view", object)'),
|
||||||
|
new GetCollection(),
|
||||||
|
new Post(),
|
||||||
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
],
|
||||||
|
security: 'is_granted("ROLE_USER")'
|
||||||
|
)]
|
||||||
|
class Immobilie
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'immobilien')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Assert\NotNull]
|
||||||
|
private User $verwalter;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(min: 5, max: 255)]
|
||||||
|
private string $adresse;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Positive]
|
||||||
|
private int $wohnflaeche;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\PositiveOrZero]
|
||||||
|
private int $nutzflaeche;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean')]
|
||||||
|
private bool $garage = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Positive]
|
||||||
|
private int $zimmer;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer', nullable: true)]
|
||||||
|
#[Assert\Range(min: 1800, max: 2100)]
|
||||||
|
private ?int $baujahr = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', enumType: ImmobilienTyp::class)]
|
||||||
|
private ImmobilienTyp $typ;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $beschreibung = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer', nullable: true)]
|
||||||
|
#[Assert\Min(0)]
|
||||||
|
#[Assert\Max(10)]
|
||||||
|
private ?int $etage = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Heizungstyp::class, inversedBy: 'immobilien')]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Heizungstyp $heizungstyp = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer', nullable: true)]
|
||||||
|
#[Assert\Range(min: 0, max: 100)]
|
||||||
|
private ?int $abschreibungszeit = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Bundesland::class, inversedBy: 'immobilien')]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Bundesland $bundesland = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer', nullable: true)]
|
||||||
|
#[Assert\PositiveOrZero]
|
||||||
|
private ?int $kaufpreis = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private \DateTimeInterface $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private \DateTimeInterface $updatedAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTime();
|
||||||
|
$this->updatedAt = new \DateTime();
|
||||||
|
$this->typ = ImmobilienTyp::WOHNUNG;
|
||||||
|
$this->nutzflaeche = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PreUpdate]
|
||||||
|
public function setUpdatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->updatedAt = new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAdresse(): string
|
||||||
|
{
|
||||||
|
return $this->adresse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAdresse(string $adresse): self
|
||||||
|
{
|
||||||
|
$this->adresse = $adresse;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWohnflaeche(): int
|
||||||
|
{
|
||||||
|
return $this->wohnflaeche;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWohnflaeche(int $wohnflaeche): self
|
||||||
|
{
|
||||||
|
$this->wohnflaeche = $wohnflaeche;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNutzflaeche(): int
|
||||||
|
{
|
||||||
|
return $this->nutzflaeche;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNutzflaeche(int $nutzflaeche): self
|
||||||
|
{
|
||||||
|
$this->nutzflaeche = $nutzflaeche;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGarage(): bool
|
||||||
|
{
|
||||||
|
return $this->garage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isGarage(): bool
|
||||||
|
{
|
||||||
|
return $this->garage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGarage(bool $garage): self
|
||||||
|
{
|
||||||
|
$this->garage = $garage;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getZimmer(): int
|
||||||
|
{
|
||||||
|
return $this->zimmer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setZimmer(int $zimmer): self
|
||||||
|
{
|
||||||
|
$this->zimmer = $zimmer;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaujahr(): ?int
|
||||||
|
{
|
||||||
|
return $this->baujahr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBaujahr(?int $baujahr): self
|
||||||
|
{
|
||||||
|
$this->baujahr = $baujahr;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTyp(): ImmobilienTyp
|
||||||
|
{
|
||||||
|
return $this->typ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTyp(ImmobilienTyp $typ): self
|
||||||
|
{
|
||||||
|
$this->typ = $typ;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBeschreibung(): ?string
|
||||||
|
{
|
||||||
|
return $this->beschreibung;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBeschreibung(?string $beschreibung): self
|
||||||
|
{
|
||||||
|
$this->beschreibung = $beschreibung;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEtage(): ?int
|
||||||
|
{
|
||||||
|
return $this->etage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEtage(?int $etage): self
|
||||||
|
{
|
||||||
|
$this->etage = $etage;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeizungstyp(): ?Heizungstyp
|
||||||
|
{
|
||||||
|
return $this->heizungstyp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeizungstyp(?Heizungstyp $heizungstyp): self
|
||||||
|
{
|
||||||
|
$this->heizungstyp = $heizungstyp;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(\DateTimeInterface $createdAt): self
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAbschreibungszeit(): ?int
|
||||||
|
{
|
||||||
|
return $this->abschreibungszeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAbschreibungszeit(?int $abschreibungszeit): self
|
||||||
|
{
|
||||||
|
$this->abschreibungszeit = $abschreibungszeit;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBundesland(): ?Bundesland
|
||||||
|
{
|
||||||
|
return $this->bundesland;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBundesland(?Bundesland $bundesland): self
|
||||||
|
{
|
||||||
|
$this->bundesland = $bundesland;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKaufpreis(): ?int
|
||||||
|
{
|
||||||
|
return $this->kaufpreis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKaufpreis(?int $kaufpreis): self
|
||||||
|
{
|
||||||
|
$this->kaufpreis = $kaufpreis;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVerwalter(): User
|
||||||
|
{
|
||||||
|
return $this->verwalter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVerwalter(User $verwalter): self
|
||||||
|
{
|
||||||
|
$this->verwalter = $verwalter;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Gesamtfläche (Wohnfläche + Nutzfläche).
|
||||||
|
*/
|
||||||
|
public function getGesamtflaeche(): int
|
||||||
|
{
|
||||||
|
return $this->wohnflaeche + $this->nutzflaeche;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Kaufnebenkosten basierend auf dem Bundesland
|
||||||
|
* Rückgabe: Array mit Notar, Grundbuch, Grunderwerbsteuer und Gesamt.
|
||||||
|
*/
|
||||||
|
public function getKaufnebenkosten(): array
|
||||||
|
{
|
||||||
|
if (! $this->getKaufpreis() || ! $this->bundesland) {
|
||||||
|
return [
|
||||||
|
'notar' => 0,
|
||||||
|
'grundbuch' => 0,
|
||||||
|
'grunderwerbsteuer' => 0,
|
||||||
|
'gesamt' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notarkosten: ca. 1,5% des Kaufpreises
|
||||||
|
$notar = $this->getKaufpreis() * 0.015;
|
||||||
|
|
||||||
|
// Grundbuchkosten: ca. 0,5% des Kaufpreises
|
||||||
|
$grundbuch = $this->getKaufpreis() * 0.005;
|
||||||
|
|
||||||
|
// Grunderwerbsteuer: abhängig vom Bundesland
|
||||||
|
$grunderwerbsteuerSatz = $this->bundesland->getGrunderwerbsteuer() / 100;
|
||||||
|
$grunderwerbsteuer = $this->getKaufpreis() * $grunderwerbsteuerSatz;
|
||||||
|
|
||||||
|
$gesamt = $notar + $grundbuch + $grunderwerbsteuer;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'notar' => $notar,
|
||||||
|
'grundbuch' => $grundbuch,
|
||||||
|
'grunderwerbsteuer' => $grunderwerbsteuer,
|
||||||
|
'gesamt' => $gesamt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/Entity/User.php
Normal file
210
src/Entity/User.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
|
use App\Enum\UserRole;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
|
#[ORM\Table(name: 'users')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(),
|
||||||
|
new GetCollection(),
|
||||||
|
new Post(),
|
||||||
|
new Put(),
|
||||||
|
new Delete(),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class User implements UserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Length(min: 2, max: 255)]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Assert\Email]
|
||||||
|
private string $email;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', enumType: UserRole::class)]
|
||||||
|
private UserRole $role;
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(targetEntity: Immobilie::class, mappedBy: 'verwalter', orphanRemoval: true)]
|
||||||
|
private Collection $immobilien;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTime();
|
||||||
|
$this->role = UserRole::USER;
|
||||||
|
$this->immobilien = new ArrayCollection();
|
||||||
|
$this->apiKey = $this->generateApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen eindeutigen API-Key.
|
||||||
|
*/
|
||||||
|
private function generateApiKey(): string
|
||||||
|
{
|
||||||
|
return hash('sha256', random_bytes(32).microtime(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(string $email): self
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRole(): UserRole
|
||||||
|
{
|
||||||
|
return $this->role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRole(UserRole $role): self
|
||||||
|
{
|
||||||
|
$this->role = $role;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(\DateTimeInterface $createdAt): self
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Immobilie>
|
||||||
|
*/
|
||||||
|
public function getImmobilien(): Collection
|
||||||
|
{
|
||||||
|
return $this->immobilien;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addImmobilie(Immobilie $immobilie): self
|
||||||
|
{
|
||||||
|
if (! $this->immobilien->contains($immobilie)) {
|
||||||
|
$this->immobilien->add($immobilie);
|
||||||
|
$immobilie->setVerwalter($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeImmobilie(Immobilie $immobilie): self
|
||||||
|
{
|
||||||
|
$this->immobilien->removeElement($immobilie);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApiKey(): string
|
||||||
|
{
|
||||||
|
return $this->apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen neuen API-Key.
|
||||||
|
*/
|
||||||
|
public function regenerateApiKey(): self
|
||||||
|
{
|
||||||
|
$this->apiKey = $this->generateApiKey();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(?string $password): self
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserInterface Methods.
|
||||||
|
*/
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = ['ROLE_USER'];
|
||||||
|
|
||||||
|
if (UserRole::ADMIN === $this->role) {
|
||||||
|
$roles[] = 'ROLE_ADMIN';
|
||||||
|
} elseif (UserRole::MODERATOR === $this->role) {
|
||||||
|
$roles[] = 'ROLE_MODERATOR';
|
||||||
|
} elseif (UserRole::TECHNICAL === $this->role) {
|
||||||
|
$roles[] = 'ROLE_TECHNICAL';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eraseCredentials(): void
|
||||||
|
{
|
||||||
|
// Nothing to erase
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Enum/Bundesland.php
Normal file
71
src/Enum/Bundesland.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum Bundesland: string
|
||||||
|
{
|
||||||
|
case BADEN_WUERTTEMBERG = 'baden_wuerttemberg';
|
||||||
|
case BAYERN = 'bayern';
|
||||||
|
case BERLIN = 'berlin';
|
||||||
|
case BRANDENBURG = 'brandenburg';
|
||||||
|
case BREMEN = 'bremen';
|
||||||
|
case HAMBURG = 'hamburg';
|
||||||
|
case HESSEN = 'hessen';
|
||||||
|
case MECKLENBURG_VORPOMMERN = 'mecklenburg_vorpommern';
|
||||||
|
case NIEDERSACHSEN = 'niedersachsen';
|
||||||
|
case NORDRHEIN_WESTFALEN = 'nordrhein_westfalen';
|
||||||
|
case RHEINLAND_PFALZ = 'rheinland_pfalz';
|
||||||
|
case SAARLAND = 'saarland';
|
||||||
|
case SACHSEN = 'sachsen';
|
||||||
|
case SACHSEN_ANHALT = 'sachsen_anhalt';
|
||||||
|
case SCHLESWIG_HOLSTEIN = 'schleswig_holstein';
|
||||||
|
case THUERINGEN = 'thueringen';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::BADEN_WUERTTEMBERG => 'Baden-Württemberg',
|
||||||
|
self::BAYERN => 'Bayern',
|
||||||
|
self::BERLIN => 'Berlin',
|
||||||
|
self::BRANDENBURG => 'Brandenburg',
|
||||||
|
self::BREMEN => 'Bremen',
|
||||||
|
self::HAMBURG => 'Hamburg',
|
||||||
|
self::HESSEN => 'Hessen',
|
||||||
|
self::MECKLENBURG_VORPOMMERN => 'Mecklenburg-Vorpommern',
|
||||||
|
self::NIEDERSACHSEN => 'Niedersachsen',
|
||||||
|
self::NORDRHEIN_WESTFALEN => 'Nordrhein-Westfalen',
|
||||||
|
self::RHEINLAND_PFALZ => 'Rheinland-Pfalz',
|
||||||
|
self::SAARLAND => 'Saarland',
|
||||||
|
self::SACHSEN => 'Sachsen',
|
||||||
|
self::SACHSEN_ANHALT => 'Sachsen-Anhalt',
|
||||||
|
self::SCHLESWIG_HOLSTEIN => 'Schleswig-Holstein',
|
||||||
|
self::THUERINGEN => 'Thüringen',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Grunderwerbsteuer in Prozent für das Bundesland zurück
|
||||||
|
* Stand: 2025.
|
||||||
|
*/
|
||||||
|
public function getGrunderwerbsteuer(): float
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::BADEN_WUERTTEMBERG => 5.0,
|
||||||
|
self::BAYERN => 3.5,
|
||||||
|
self::BERLIN => 6.0,
|
||||||
|
self::BRANDENBURG => 6.5,
|
||||||
|
self::BREMEN => 5.0,
|
||||||
|
self::HAMBURG => 5.5,
|
||||||
|
self::HESSEN => 6.0,
|
||||||
|
self::MECKLENBURG_VORPOMMERN => 6.0,
|
||||||
|
self::NIEDERSACHSEN => 5.0,
|
||||||
|
self::NORDRHEIN_WESTFALEN => 6.5,
|
||||||
|
self::RHEINLAND_PFALZ => 5.0,
|
||||||
|
self::SAARLAND => 6.5,
|
||||||
|
self::SACHSEN => 5.5,
|
||||||
|
self::SACHSEN_ANHALT => 5.0,
|
||||||
|
self::SCHLESWIG_HOLSTEIN => 6.5,
|
||||||
|
self::THUERINGEN => 5.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Enum/Heizungstyp.php
Normal file
21
src/Enum/Heizungstyp.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum Heizungstyp: string
|
||||||
|
{
|
||||||
|
case GASHEIZUNG = 'gasheizung';
|
||||||
|
case WAERMEPUMPE = 'waermepumpe';
|
||||||
|
case OELHEIZUNG = 'oelheizung';
|
||||||
|
case PELLETHEIZUNG = 'pelletheizung';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::GASHEIZUNG => 'Gasheizung',
|
||||||
|
self::WAERMEPUMPE => 'Wärmepumpe',
|
||||||
|
self::OELHEIZUNG => 'Ölheizung',
|
||||||
|
self::PELLETHEIZUNG => 'Pelletheizung',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Enum/ImmobilienTyp.php
Normal file
23
src/Enum/ImmobilienTyp.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum ImmobilienTyp: string
|
||||||
|
{
|
||||||
|
case WOHNUNG = 'wohnung';
|
||||||
|
case HAUS = 'haus';
|
||||||
|
case GRUNDSTUECK = 'grundstueck';
|
||||||
|
case GEWERBE = 'gewerbe';
|
||||||
|
case BUERO = 'buero';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::WOHNUNG => 'Wohnung',
|
||||||
|
self::HAUS => 'Haus',
|
||||||
|
self::GRUNDSTUECK => 'Grundstück',
|
||||||
|
self::GEWERBE => 'Gewerbe',
|
||||||
|
self::BUERO => 'Büro',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Enum/UserRole.php
Normal file
21
src/Enum/UserRole.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum UserRole: string
|
||||||
|
{
|
||||||
|
case USER = 'user';
|
||||||
|
case ADMIN = 'admin';
|
||||||
|
case MODERATOR = 'moderator';
|
||||||
|
case TECHNICAL = 'technical';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::USER => 'Benutzer',
|
||||||
|
self::ADMIN => 'Administrator',
|
||||||
|
self::MODERATOR => 'Moderator',
|
||||||
|
self::TECHNICAL => 'Technischer User',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Kernel.php
Normal file
11
src/Kernel.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
}
|
||||||
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
15
src/Repository/BundeslandRepository.php
Normal file
15
src/Repository/BundeslandRepository.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Bundesland;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class BundeslandRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Bundesland::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Repository/HeizungstypRepository.php
Normal file
15
src/Repository/HeizungstypRepository.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Heizungstyp;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class HeizungstypRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Heizungstyp::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/Repository/ImmobilieRepository.php
Normal file
137
src/Repository/ImmobilieRepository.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Immobilie;
|
||||||
|
use App\Enum\ImmobilienTyp;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Immobilie>
|
||||||
|
*/
|
||||||
|
class ImmobilieRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Immobilie::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find available properties.
|
||||||
|
*/
|
||||||
|
public function findVerfuegbare(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->orderBy('i.createdAt', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find properties by type.
|
||||||
|
*/
|
||||||
|
public function findByTyp(ImmobilienTyp $typ): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.typ = :typ')
|
||||||
|
->setParameter('typ', $typ)
|
||||||
|
->orderBy('i.preis', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find properties within price range.
|
||||||
|
*/
|
||||||
|
public function findByPreisRange(float $minPreis, float $maxPreis): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.preis BETWEEN :minPreis AND :maxPreis')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('minPreis', $minPreis)
|
||||||
|
->setParameter('maxPreis', $maxPreis)
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->orderBy('i.preis', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find properties within area range.
|
||||||
|
*/
|
||||||
|
public function findByFlaecheRange(float $minFlaeche, float $maxFlaeche): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.flaeche BETWEEN :minFlaeche AND :maxFlaeche')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('minFlaeche', $minFlaeche)
|
||||||
|
->setParameter('maxFlaeche', $maxFlaeche)
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->orderBy('i.flaeche', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find properties with garage.
|
||||||
|
*/
|
||||||
|
public function findMitGarage(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.garage = :garage')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('garage', true)
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->orderBy('i.preis', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find properties by minimum number of rooms.
|
||||||
|
*/
|
||||||
|
public function findByMinZimmer(int $minZimmer): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.zimmer >= :minZimmer')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('minZimmer', $minZimmer)
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->orderBy('i.zimmer', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average price per sqm by type.
|
||||||
|
*/
|
||||||
|
public function getAveragePreisProQmByTyp(ImmobilienTyp $typ): float
|
||||||
|
{
|
||||||
|
$result = $this->createQueryBuilder('i')
|
||||||
|
->select('AVG(i.preis / i.flaeche) as avgPreisProQm')
|
||||||
|
->andWhere('i.typ = :typ')
|
||||||
|
->andWhere('i.verfuegbar = :verfuegbar')
|
||||||
|
->setParameter('typ', $typ)
|
||||||
|
->setParameter('verfuegbar', true)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
|
||||||
|
return round((float) $result, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search properties by address.
|
||||||
|
*/
|
||||||
|
public function searchByAdresse(string $search): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('i')
|
||||||
|
->andWhere('i.adresse LIKE :search')
|
||||||
|
->setParameter('search', '%'.$search.'%')
|
||||||
|
->orderBy('i.createdAt', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Repository/UserRepository.php
Normal file
43
src/Repository/UserRepository.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<User>
|
||||||
|
*/
|
||||||
|
class UserRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find users by role.
|
||||||
|
*/
|
||||||
|
public function findByRole(string $role): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('u')
|
||||||
|
->andWhere('u.role = :role')
|
||||||
|
->setParameter('role', $role)
|
||||||
|
->orderBy('u.createdAt', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by email.
|
||||||
|
*/
|
||||||
|
public function findOneByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('u')
|
||||||
|
->andWhere('u.email = :email')
|
||||||
|
->setParameter('email', $email)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Security/ApiKeyAuthenticator.php
Normal file
62
src/Security/ApiKeyAuthenticator.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||||
|
|
||||||
|
class ApiKeyAuthenticator extends AbstractAuthenticator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private UserRepository $userRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
return $request->headers->has('X-API-KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
$apiKey = $request->headers->get('X-API-KEY');
|
||||||
|
|
||||||
|
if (null === $apiKey) {
|
||||||
|
throw new CustomUserMessageAuthenticationException('No API key provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SelfValidatingPassport(
|
||||||
|
new UserBadge($apiKey, function ($apiKey) {
|
||||||
|
$user = $this->userRepository->findOneBy(['apiKey' => $apiKey]);
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
throw new CustomUserMessageAuthenticationException('Invalid API key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
// On success, let the request continue
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
return new JsonResponse([
|
||||||
|
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
|
||||||
|
], Response::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Security/ImmobilieVoter.php
Normal file
67
src/Security/ImmobilieVoter.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\Immobilie;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Enum\UserRole;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
class ImmobilieVoter extends Voter
|
||||||
|
{
|
||||||
|
public const VIEW = 'view';
|
||||||
|
public const EDIT = 'edit';
|
||||||
|
public const DELETE = 'delete';
|
||||||
|
|
||||||
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
|
{
|
||||||
|
// Voter unterstützt nur diese Attribute und nur Immobilie-Objekte
|
||||||
|
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
|
||||||
|
&& $subject instanceof Immobilie;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
// User muss eingeloggt sein
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Immobilie $immobilie */
|
||||||
|
$immobilie = $subject;
|
||||||
|
|
||||||
|
// Admin hat uneingeschränkten Zugriff
|
||||||
|
if (UserRole::ADMIN === $user->getRole()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe je nach Attribut
|
||||||
|
return match ($attribute) {
|
||||||
|
self::VIEW => $this->canView($immobilie, $user),
|
||||||
|
self::EDIT => $this->canEdit($immobilie, $user),
|
||||||
|
self::DELETE => $this->canDelete($immobilie, $user),
|
||||||
|
default => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canView(Immobilie $immobilie, User $user): bool
|
||||||
|
{
|
||||||
|
// User kann nur eigene Immobilien sehen
|
||||||
|
return $immobilie->getVerwalter() === $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canEdit(Immobilie $immobilie, User $user): bool
|
||||||
|
{
|
||||||
|
// User kann nur eigene Immobilien bearbeiten
|
||||||
|
return $immobilie->getVerwalter() === $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canDelete(Immobilie $immobilie, User $user): bool
|
||||||
|
{
|
||||||
|
// User kann nur eigene Immobilien löschen
|
||||||
|
return $immobilie->getVerwalter() === $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
symfony.lock
Normal file
208
symfony.lock
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"api-platform/symfony": {
|
||||||
|
"version": "4.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "4.0",
|
||||||
|
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/api_platform.yaml",
|
||||||
|
"./config/routes/api_platform.yaml",
|
||||||
|
"./src/ApiResource/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "3.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "86b1fbac469b8b1c05e5ff28a7a2cffcaacf5068"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/doctrine.yaml",
|
||||||
|
"./src/Entity/.gitignore",
|
||||||
|
"./src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
|
"version": "3.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.1",
|
||||||
|
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/doctrine_migrations.yaml",
|
||||||
|
"./migrations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"friendsofphp/php-cs-fixer": {
|
||||||
|
"version": "3.89",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".php-cs-fixer.dist.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nelmio/cors-bundle": {
|
||||||
|
"version": "2.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/nelmio_cors.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"phpunit/phpunit": {
|
||||||
|
"version": "12.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "11.1",
|
||||||
|
"ref": "1117deb12541f35793eec9fff7494d7aa12283fc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env.test",
|
||||||
|
"phpunit.dist.xml",
|
||||||
|
"tests/bootstrap.php",
|
||||||
|
"bin/phpunit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.9",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./.env",
|
||||||
|
"./.env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/cache.yaml",
|
||||||
|
"./config/packages/framework.yaml",
|
||||||
|
"./config/preload.php",
|
||||||
|
"./config/routes/framework.yaml",
|
||||||
|
"./config/services.yaml",
|
||||||
|
"./public/index.php",
|
||||||
|
"./src/Controller/.gitignore",
|
||||||
|
"./src/Kernel.php",
|
||||||
|
"./.editorconfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "ab1e60e2afd5c6f4a6795908f646e235f2564eb2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/routing.yaml",
|
||||||
|
"./config/routes.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/security.yaml",
|
||||||
|
"./config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/twig-bundle": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/twig.yaml",
|
||||||
|
"./templates/base.html.twig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/uid": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symfony/validator": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./config/packages/validator.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
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 %}
|
||||||
81
templates/base.html.twig
Normal file
81
templates/base.html.twig
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Immorechner{% endblock %}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{% block stylesheets %}
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
margin-top: 50px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="container">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<p>© {{ "now"|date("Y") }} Immorechner - Powered by Symfony</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{% block javascripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
167
templates/home/index.html.twig
Normal file
167
templates/home/index.html.twig
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Immobilienwert berechnen - {{ parent() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/calculator.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<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 %}
|
||||||
153
templates/immobilie/index.html.twig
Normal file
153
templates/immobilie/index.html.twig
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Immobilien - {{ parent() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
.immobilien-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.immobilie-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.immobilie-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.immobilie-header {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.immobilie-typ {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.immobilie-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.immobilie-preis {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.immobilie-details {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.immobilie-details li {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.immobilie-details li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.badge-success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
.btn-details {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.btn-details:hover {
|
||||||
|
background: #45a049;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #45a049;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Verfügbare Immobilien</h2>
|
||||||
|
<div>
|
||||||
|
<a href="{{ path('app_immobilie_suche') }}" class="btn-primary">Erweiterte Suche</a>
|
||||||
|
<a href="/api" class="btn-primary">API</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if immobilien|length > 0 %}
|
||||||
|
<div class="immobilien-grid">
|
||||||
|
{% for immobilie in immobilien %}
|
||||||
|
<div class="immobilie-card">
|
||||||
|
<div class="immobilie-header">
|
||||||
|
<div class="immobilie-typ">{{ immobilie.typ.label }}</div>
|
||||||
|
<strong>{{ immobilie.adresse }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="immobilie-body">
|
||||||
|
<div class="immobilie-preis">{{ immobilie.preis|number_format(2, ',', '.') }} €</div>
|
||||||
|
|
||||||
|
<ul class="immobilie-details">
|
||||||
|
<li>📐 Fläche: {{ immobilie.flaeche }} m²</li>
|
||||||
|
<li>🛏️ Zimmer: {{ immobilie.zimmer }}</li>
|
||||||
|
<li>💰 Preis/m²: {{ immobilie.preisProQm|number_format(2, ',', '.') }} €</li>
|
||||||
|
{% if immobilie.baujahr %}
|
||||||
|
<li>📅 Baujahr: {{ immobilie.baujahr }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if immobilie.garage %}
|
||||||
|
<li>🚗 <span class="badge badge-success">Mit Garage</span></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="{{ path('app_immobilie_show', {id: immobilie.id}) }}" class="btn-details">
|
||||||
|
Details ansehen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-results">
|
||||||
|
<h3>Keine Immobilien verfügbar</h3>
|
||||||
|
<p>Aktuell sind keine Immobilien in unserem System verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% 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 %}
|
||||||
211
templates/immobilie/show.html.twig
Normal file
211
templates/immobilie/show.html.twig
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}{{ immobilie.adresse }} - {{ parent() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
.immobilie-detail {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
padding: 30px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.immobilie-header {
|
||||||
|
border-bottom: 2px solid #4CAF50;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.immobilie-typ-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.immobilie-adresse {
|
||||||
|
font-size: 28px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.immobilie-preis-box {
|
||||||
|
background: #e8f5e9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.preis-haupt {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
.preis-pro-qm {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.detail-box {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
.detail-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.detail-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.beschreibung-box {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.status-verfuegbar {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-nicht-verfuegbar {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #4CAF50;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.features-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.features-list li {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.features-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<a href="{{ path('app_immobilie_index') }}" class="back-link">← Zurück zur Übersicht</a>
|
||||||
|
|
||||||
|
<div class="immobilie-detail">
|
||||||
|
<div class="immobilie-header">
|
||||||
|
<span class="immobilie-typ-badge">{{ immobilie.typ.label }}</span>
|
||||||
|
<h2 class="immobilie-adresse">{{ immobilie.adresse }}</h2>
|
||||||
|
<span class="status-badge {{ immobilie.verfuegbar ? 'status-verfuegbar' : 'status-nicht-verfuegbar' }}">
|
||||||
|
{{ immobilie.verfuegbar ? 'Verfügbar' : 'Nicht verfügbar' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="immobilie-preis-box">
|
||||||
|
<div class="preis-haupt">{{ immobilie.preis|number_format(2, ',', '.') }} €</div>
|
||||||
|
<div class="preis-pro-qm">{{ immobilie.preisProQm|number_format(2, ',', '.') }} € pro m²</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Hauptmerkmale</h3>
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="detail-label">Wohnfläche</div>
|
||||||
|
<div class="detail-value">{{ immobilie.flaeche }} m²</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="detail-label">Zimmer</div>
|
||||||
|
<div class="detail-value">{{ immobilie.zimmer }}</div>
|
||||||
|
</div>
|
||||||
|
{% if immobilie.baujahr %}
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="detail-label">Baujahr</div>
|
||||||
|
<div class="detail-value">{{ immobilie.baujahr }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if immobilie.etage is not null %}
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="detail-label">Etage</div>
|
||||||
|
<div class="detail-value">{{ immobilie.etage }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Ausstattung & Extras</h3>
|
||||||
|
<ul class="features-list">
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">🚗</span>
|
||||||
|
<strong>Garage:</strong> {{ immobilie.garage ? 'Ja' : 'Nein' }}
|
||||||
|
</li>
|
||||||
|
{% if immobilie.balkonFlaeche %}
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">🌿</span>
|
||||||
|
<strong>Balkon:</strong> {{ immobilie.balkonFlaeche }} m²
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if immobilie.kellerFlaeche %}
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">📦</span>
|
||||||
|
<strong>Keller:</strong> {{ immobilie.kellerFlaeche }} m²
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if immobilie.heizungstyp %}
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">🔥</span>
|
||||||
|
<strong>Heizung:</strong> {{ immobilie.heizungstyp }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if immobilie.nebenkosten %}
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">💶</span>
|
||||||
|
<strong>Nebenkosten:</strong> {{ immobilie.nebenkosten|number_format(2, ',', '.') }} € / Monat
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<span class="feature-icon">📊</span>
|
||||||
|
<strong>Gesamtfläche:</strong> {{ immobilie.gesamtflaeche }} m²
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if immobilie.beschreibung %}
|
||||||
|
<h3>Beschreibung</h3>
|
||||||
|
<div class="beschreibung-box">
|
||||||
|
{{ immobilie.beschreibung|nl2br }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
||||||
|
Erstellt am: {{ immobilie.createdAt|date('d.m.Y H:i') }} Uhr<br>
|
||||||
|
Zuletzt aktualisiert: {{ immobilie.updatedAt|date('d.m.Y H:i') }} Uhr
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
103
templates/immobilie/suche.html.twig
Normal file
103
templates/immobilie/suche.html.twig
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Immobiliensuche - {{ parent() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
.search-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
padding: 30px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.api-links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.api-link-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
.api-link-card h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
.api-link-card a {
|
||||||
|
color: #4CAF50;
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.api-link-card a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.api-link-card p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #4CAF50;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<a href="{{ path('app_immobilie_index') }}" class="back-link">← Zurück zur Übersicht</a>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
|
<h2>Immobiliensuche über API</h2>
|
||||||
|
<p>Nutzen Sie die REST-API für erweiterte Suchfunktionen. Hier sind einige nützliche Endpoints:</p>
|
||||||
|
|
||||||
|
<div class="api-links">
|
||||||
|
<div class="api-link-card">
|
||||||
|
<h4>Alle Immobilien</h4>
|
||||||
|
<a href="/api/immobilies">
|
||||||
|
/api/immobilies
|
||||||
|
</a>
|
||||||
|
<p>Liste aller Immobilien (JSON-LD Format)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-link-card">
|
||||||
|
<h4>API-Dokumentation</h4>
|
||||||
|
<a href="/api">/api</a>
|
||||||
|
<p>Vollständige API-Dokumentation mit allen verfügbaren Operationen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-link-card">
|
||||||
|
<h4>Einzelne Immobilie</h4>
|
||||||
|
<a href="/api/immobilies/1">
|
||||||
|
/api/immobilies/{id}
|
||||||
|
</a>
|
||||||
|
<p>Details zu einer bestimmten Immobilie</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 40px;">Beispiel-Abfragen</h3>
|
||||||
|
|
||||||
|
<div class="api-link-card" style="margin-top: 20px;">
|
||||||
|
<h4>Repository-Methoden (Backend)</h4>
|
||||||
|
<p>Das ImmobilieRepository bietet folgende Suchmethoden:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>findVerfuegbare()</code> - Nur verfügbare Immobilien</li>
|
||||||
|
<li><code>findByTyp(ImmobilienTyp)</code> - Nach Typ filtern</li>
|
||||||
|
<li><code>findByPreisRange($min, $max)</code> - Preisspanne</li>
|
||||||
|
<li><code>findByFlaecheRange($min, $max)</code> - Flächenbereich</li>
|
||||||
|
<li><code>findMitGarage()</code> - Immobilien mit Garage</li>
|
||||||
|
<li><code>findByMinZimmer($anzahl)</code> - Mindestanzahl Zimmer</li>
|
||||||
|
<li><code>searchByAdresse($search)</code> - Adresssuche</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
28
tests/Api/ApiDocumentationTest.php
Normal file
28
tests/Api/ApiDocumentationTest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Api;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
class ApiDocumentationTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testSwaggerUIAccessible(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Swagger UI ist öffentlich zugänglich
|
||||||
|
$client->request('GET', '/api/docs.html');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOpenAPIJsonLdAccessible(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: OpenAPI JSON-LD ist öffentlich zugänglich
|
||||||
|
$client->request('GET', '/api/docs.jsonld');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
tests/Api/BundeslandApiTest.php
Normal file
50
tests/Api/BundeslandApiTest.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Api;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
class BundeslandApiTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testGetBundeslaenderPublicAccess(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Bundesländer können ohne API-Key abgerufen werden
|
||||||
|
$client->request('GET', '/api/bundeslands');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSingleBundeslandPublicAccess(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Einzelnes Bundesland kann ohne API-Key abgerufen werden
|
||||||
|
$client->request('GET', '/api/bundeslands/1');
|
||||||
|
|
||||||
|
// Response kann 200 (OK) oder 404 (Not Found) sein, beides ist akzeptabel
|
||||||
|
$this->assertResponseStatusCodeSame(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateBundeslandRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Bundesland erstellen ohne API-Key sollte fehlschlagen
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/bundeslands',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
['CONTENT_TYPE' => 'application/json'],
|
||||||
|
json_encode([
|
||||||
|
'name' => 'Test Bundesland',
|
||||||
|
'grunderwerbsteuer' => 5.0,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(403); // Access Denied (no authentication on this firewall)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
tests/Api/HeizungstypApiTest.php
Normal file
49
tests/Api/HeizungstypApiTest.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Api;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
class HeizungstypApiTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testGetHeizungstypenPublicAccess(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Heizungstypen können ohne API-Key abgerufen werden
|
||||||
|
$client->request('GET', '/api/heizungstyps');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSingleHeizungstypPublicAccess(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Einzelner Heizungstyp kann ohne API-Key abgerufen werden
|
||||||
|
$client->request('GET', '/api/heizungstyps/1');
|
||||||
|
|
||||||
|
// Response kann 200 (OK) oder 404 (Not Found) sein
|
||||||
|
$this->assertResponseStatusCodeSame(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateHeizungstypRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Test: Heizungstyp erstellen ohne API-Key sollte fehlschlagen
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/heizungstyps',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
['CONTENT_TYPE' => 'application/json'],
|
||||||
|
json_encode([
|
||||||
|
'name' => 'Test Heizung',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(401); // Unauthorized
|
||||||
|
}
|
||||||
|
}
|
||||||
65
tests/Entity/BundeslandTest.php
Normal file
65
tests/Entity/BundeslandTest.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Bundesland;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class BundeslandTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testBundeslandCreation(): void
|
||||||
|
{
|
||||||
|
$bundesland = new Bundesland();
|
||||||
|
$bundesland->setName('Bayern');
|
||||||
|
$bundesland->setGrunderwerbsteuer(3.5);
|
||||||
|
|
||||||
|
$this->assertEquals('Bayern', $bundesland->getName());
|
||||||
|
$this->assertEquals(3.5, $bundesland->getGrunderwerbsteuer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGrunderwerbsteuerValues(): void
|
||||||
|
{
|
||||||
|
$testCases = [
|
||||||
|
['Baden-Württemberg', 5.0],
|
||||||
|
['Bayern', 3.5],
|
||||||
|
['Berlin', 6.0],
|
||||||
|
['Brandenburg', 6.5],
|
||||||
|
['Bremen', 5.0],
|
||||||
|
['Hamburg', 5.5],
|
||||||
|
['Hessen', 6.0],
|
||||||
|
['Mecklenburg-Vorpommern', 6.0],
|
||||||
|
['Niedersachsen', 5.0],
|
||||||
|
['Nordrhein-Westfalen', 6.5],
|
||||||
|
['Rheinland-Pfalz', 5.0],
|
||||||
|
['Saarland', 6.5],
|
||||||
|
['Sachsen', 5.5],
|
||||||
|
['Sachsen-Anhalt', 5.0],
|
||||||
|
['Schleswig-Holstein', 6.5],
|
||||||
|
['Thüringen', 5.0],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testCases as [$name, $steuer]) {
|
||||||
|
$bundesland = new Bundesland();
|
||||||
|
$bundesland->setName($name);
|
||||||
|
$bundesland->setGrunderwerbsteuer($steuer);
|
||||||
|
|
||||||
|
$this->assertEquals($name, $bundesland->getName());
|
||||||
|
$this->assertEquals($steuer, $bundesland->getGrunderwerbsteuer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMinMaxGrunderwerbsteuer(): void
|
||||||
|
{
|
||||||
|
// Niedrigster Satz: Bayern mit 3.5%
|
||||||
|
$bayern = new Bundesland();
|
||||||
|
$bayern->setName('Bayern');
|
||||||
|
$bayern->setGrunderwerbsteuer(3.5);
|
||||||
|
$this->assertEquals(3.5, $bayern->getGrunderwerbsteuer());
|
||||||
|
|
||||||
|
// Höchster Satz: Brandenburg, NRW, Saarland, SH mit 6.5%
|
||||||
|
$nrw = new Bundesland();
|
||||||
|
$nrw->setName('Nordrhein-Westfalen');
|
||||||
|
$nrw->setGrunderwerbsteuer(6.5);
|
||||||
|
$this->assertEquals(6.5, $nrw->getGrunderwerbsteuer());
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tests/Entity/HeizungstypTest.php
Normal file
29
tests/Entity/HeizungstypTest.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Heizungstyp;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class HeizungstypTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testHeizungstypCreation(): void
|
||||||
|
{
|
||||||
|
$heizungstyp = new Heizungstyp();
|
||||||
|
$heizungstyp->setName('Wärmepumpe');
|
||||||
|
|
||||||
|
$this->assertEquals('Wärmepumpe', $heizungstyp->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCommonHeizungstypen(): void
|
||||||
|
{
|
||||||
|
$typen = ['Gasheizung', 'Wärmepumpe', 'Ölheizung', 'Fernwärme', 'Pelletheizung'];
|
||||||
|
|
||||||
|
foreach ($typen as $typName) {
|
||||||
|
$heizungstyp = new Heizungstyp();
|
||||||
|
$heizungstyp->setName($typName);
|
||||||
|
|
||||||
|
$this->assertEquals($typName, $heizungstyp->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
tests/Entity/ImmobilieTest.php
Normal file
160
tests/Entity/ImmobilieTest.php
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Bundesland;
|
||||||
|
use App\Entity\Immobilie;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Enum\ImmobilienTyp;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ImmobilieTest extends TestCase
|
||||||
|
{
|
||||||
|
private User $verwalter;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->verwalter = new User();
|
||||||
|
$this->verwalter->setName('Test Verwalter');
|
||||||
|
$this->verwalter->setEmail('verwalter@example.com');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testImmobilieCreation(): void
|
||||||
|
{
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Teststraße 123, 12345 Teststadt');
|
||||||
|
$immobilie->setWohnflaeche(100);
|
||||||
|
$immobilie->setNutzflaeche(20);
|
||||||
|
$immobilie->setZimmer(4);
|
||||||
|
$immobilie->setTyp(ImmobilienTyp::WOHNUNG);
|
||||||
|
|
||||||
|
$this->assertEquals('Teststraße 123, 12345 Teststadt', $immobilie->getAdresse());
|
||||||
|
$this->assertEquals(100, $immobilie->getWohnflaeche());
|
||||||
|
$this->assertEquals(20, $immobilie->getNutzflaeche());
|
||||||
|
$this->assertEquals(4, $immobilie->getZimmer());
|
||||||
|
$this->assertEquals(ImmobilienTyp::WOHNUNG, $immobilie->getTyp());
|
||||||
|
$this->assertEquals($this->verwalter, $immobilie->getVerwalter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGesamtflaecheCalculation(): void
|
||||||
|
{
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Test');
|
||||||
|
$immobilie->setWohnflaeche(85);
|
||||||
|
$immobilie->setNutzflaeche(15);
|
||||||
|
$immobilie->setZimmer(3);
|
||||||
|
|
||||||
|
$this->assertEquals(100, $immobilie->getGesamtflaeche());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKaufnebenkostenWithoutBundesland(): void
|
||||||
|
{
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Test');
|
||||||
|
$immobilie->setWohnflaeche(100);
|
||||||
|
$immobilie->setNutzflaeche(0);
|
||||||
|
$immobilie->setZimmer(3);
|
||||||
|
$immobilie->setKaufpreis(300000);
|
||||||
|
|
||||||
|
$kosten = $immobilie->getKaufnebenkosten();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $kosten['notar']);
|
||||||
|
$this->assertEquals(0, $kosten['grundbuch']);
|
||||||
|
$this->assertEquals(0, $kosten['grunderwerbsteuer']);
|
||||||
|
$this->assertEquals(0, $kosten['gesamt']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKaufnebenkostenWithBundesland(): void
|
||||||
|
{
|
||||||
|
$bundesland = new Bundesland();
|
||||||
|
$bundesland->setName('Bayern');
|
||||||
|
$bundesland->setGrunderwerbsteuer(3.5);
|
||||||
|
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Test');
|
||||||
|
$immobilie->setWohnflaeche(100);
|
||||||
|
$immobilie->setNutzflaeche(0);
|
||||||
|
$immobilie->setZimmer(3);
|
||||||
|
$immobilie->setKaufpreis(300000);
|
||||||
|
$immobilie->setBundesland($bundesland);
|
||||||
|
|
||||||
|
$kosten = $immobilie->getKaufnebenkosten();
|
||||||
|
|
||||||
|
// Notar: 1.5% von 300000 = 4500
|
||||||
|
$this->assertEquals(4500, $kosten['notar']);
|
||||||
|
|
||||||
|
// Grundbuch: 0.5% von 300000 = 1500
|
||||||
|
$this->assertEquals(1500, $kosten['grundbuch']);
|
||||||
|
|
||||||
|
// Grunderwerbsteuer: 3.5% von 300000 = 10500
|
||||||
|
$this->assertEqualsWithDelta(10500, $kosten['grunderwerbsteuer'], 0.01);
|
||||||
|
|
||||||
|
// Gesamt: 4500 + 1500 + 10500 = 16500
|
||||||
|
$this->assertEquals(16500, $kosten['gesamt']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKaufnebenkostenWithDifferentBundesland(): void
|
||||||
|
{
|
||||||
|
$bundesland = new Bundesland();
|
||||||
|
$bundesland->setName('Nordrhein-Westfalen');
|
||||||
|
$bundesland->setGrunderwerbsteuer(6.5);
|
||||||
|
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Test');
|
||||||
|
$immobilie->setWohnflaeche(100);
|
||||||
|
$immobilie->setNutzflaeche(0);
|
||||||
|
$immobilie->setZimmer(3);
|
||||||
|
$immobilie->setKaufpreis(400000);
|
||||||
|
$immobilie->setBundesland($bundesland);
|
||||||
|
|
||||||
|
$kosten = $immobilie->getKaufnebenkosten();
|
||||||
|
|
||||||
|
// Notar: 1.5% von 400000 = 6000
|
||||||
|
$this->assertEquals(6000, $kosten['notar']);
|
||||||
|
|
||||||
|
// Grundbuch: 0.5% von 400000 = 2000
|
||||||
|
$this->assertEquals(2000, $kosten['grundbuch']);
|
||||||
|
|
||||||
|
// Grunderwerbsteuer: 6.5% von 400000 = 26000
|
||||||
|
$this->assertEquals(26000, $kosten['grunderwerbsteuer']);
|
||||||
|
|
||||||
|
// Gesamt: 6000 + 2000 + 26000 = 34000
|
||||||
|
$this->assertEquals(34000, $kosten['gesamt']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultValues(): void
|
||||||
|
{
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
|
||||||
|
$this->assertEquals(ImmobilienTyp::WOHNUNG, $immobilie->getTyp());
|
||||||
|
$this->assertEquals(0, $immobilie->getNutzflaeche());
|
||||||
|
$this->assertFalse($immobilie->getGarage());
|
||||||
|
$this->assertNotNull($immobilie->getCreatedAt());
|
||||||
|
$this->assertNotNull($immobilie->getUpdatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOptionalFields(): void
|
||||||
|
{
|
||||||
|
$immobilie = new Immobilie();
|
||||||
|
$immobilie->setVerwalter($this->verwalter);
|
||||||
|
$immobilie->setAdresse('Test');
|
||||||
|
$immobilie->setWohnflaeche(100);
|
||||||
|
$immobilie->setNutzflaeche(0);
|
||||||
|
$immobilie->setZimmer(3);
|
||||||
|
|
||||||
|
$immobilie->setBaujahr(2020);
|
||||||
|
$immobilie->setEtage(3);
|
||||||
|
$immobilie->setAbschreibungszeit(50);
|
||||||
|
$immobilie->setBeschreibung('Schöne Wohnung');
|
||||||
|
|
||||||
|
$this->assertEquals(2020, $immobilie->getBaujahr());
|
||||||
|
$this->assertEquals(3, $immobilie->getEtage());
|
||||||
|
$this->assertEquals(50, $immobilie->getAbschreibungszeit());
|
||||||
|
$this->assertEquals('Schöne Wohnung', $immobilie->getBeschreibung());
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tests/Entity/UserTest.php
Normal file
92
tests/Entity/UserTest.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Enum\UserRole;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class UserTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testUserCreation(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setName('Max Mustermann');
|
||||||
|
$user->setEmail('max@example.com');
|
||||||
|
$user->setRole(UserRole::USER);
|
||||||
|
|
||||||
|
$this->assertEquals('Max Mustermann', $user->getName());
|
||||||
|
$this->assertEquals('max@example.com', $user->getEmail());
|
||||||
|
$this->assertEquals(UserRole::USER, $user->getRole());
|
||||||
|
$this->assertNotNull($user->getApiKey());
|
||||||
|
$this->assertNotNull($user->getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiKeyGeneration(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setName('Test User');
|
||||||
|
$user->setEmail('test@example.com');
|
||||||
|
|
||||||
|
$apiKey = $user->getApiKey();
|
||||||
|
|
||||||
|
$this->assertNotEmpty($apiKey);
|
||||||
|
$this->assertEquals(64, strlen($apiKey)); // SHA256 hash length
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegenerateApiKey(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setName('Test User');
|
||||||
|
$user->setEmail('test@example.com');
|
||||||
|
|
||||||
|
$originalKey = $user->getApiKey();
|
||||||
|
$user->regenerateApiKey();
|
||||||
|
$newKey = $user->getApiKey();
|
||||||
|
|
||||||
|
$this->assertNotEquals($originalKey, $newKey);
|
||||||
|
$this->assertEquals(64, strlen($newKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserRoles(): void
|
||||||
|
{
|
||||||
|
// Test USER role
|
||||||
|
$user = new User();
|
||||||
|
$user->setRole(UserRole::USER);
|
||||||
|
$this->assertContains('ROLE_USER', $user->getRoles());
|
||||||
|
|
||||||
|
// Test ADMIN role
|
||||||
|
$admin = new User();
|
||||||
|
$admin->setRole(UserRole::ADMIN);
|
||||||
|
$this->assertContains('ROLE_ADMIN', $admin->getRoles());
|
||||||
|
$this->assertContains('ROLE_USER', $admin->getRoles());
|
||||||
|
|
||||||
|
// Test TECHNICAL role
|
||||||
|
$technical = new User();
|
||||||
|
$technical->setRole(UserRole::TECHNICAL);
|
||||||
|
$this->assertContains('ROLE_TECHNICAL', $technical->getRoles());
|
||||||
|
$this->assertContains('ROLE_USER', $technical->getRoles());
|
||||||
|
|
||||||
|
// Test MODERATOR role
|
||||||
|
$moderator = new User();
|
||||||
|
$moderator->setRole(UserRole::MODERATOR);
|
||||||
|
$this->assertContains('ROLE_MODERATOR', $moderator->getRoles());
|
||||||
|
$this->assertContains('ROLE_USER', $moderator->getRoles());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserIdentifier(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail('identifier@example.com');
|
||||||
|
|
||||||
|
$this->assertEquals('identifier@example.com', $user->getUserIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultRole(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
// Default role should be USER
|
||||||
|
$this->assertEquals(UserRole::USER, $user->getRole());
|
||||||
|
}
|
||||||
|
}
|
||||||
13
tests/bootstrap.php
Normal file
13
tests/bootstrap.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (method_exists(Dotenv::class, 'bootEnv')) {
|
||||||
|
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['APP_DEBUG']) {
|
||||||
|
umask(0000);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user