Immobilien & User hinzugefügt
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
api_platform:
|
||||
title: Hello API Platform
|
||||
version: 1.0.0
|
||||
mapping:
|
||||
paths: ['%kernel.project_dir%/src/Entity']
|
||||
defaults:
|
||||
stateless: true
|
||||
cache_headers:
|
||||
|
||||
31
migrations/Version20251108172941.php
Normal file
31
migrations/Version20251108172941.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251108172941 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE immobilien (id INT AUTO_INCREMENT NOT NULL, adresse VARCHAR(255) NOT NULL, preis NUMERIC(10, 2) NOT NULL, flaeche NUMERIC(8, 2) NOT NULL, garage TINYINT(1) NOT NULL, zimmer INT NOT NULL, baujahr INT DEFAULT NULL, typ VARCHAR(255) NOT NULL, beschreibung LONGTEXT DEFAULT NULL, verfuegbar TINYINT(1) NOT NULL, balkon_flaeche INT DEFAULT NULL, keller_flaeche INT DEFAULT NULL, etage INT DEFAULT NULL, heizungstyp VARCHAR(100) DEFAULT NULL, nebenkosten NUMERIC(6, 2) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE immobilien');
|
||||
}
|
||||
}
|
||||
42
migrations/Version20251108175151.php
Normal file
42
migrations/Version20251108175151.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251108175151 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
// First add the column as nullable
|
||||
$this->addSql('ALTER TABLE immobilien ADD verwalter_id INT DEFAULT NULL');
|
||||
|
||||
// Assign all existing immobilien to the first user
|
||||
$this->addSql('UPDATE immobilien SET verwalter_id = (SELECT id FROM users ORDER BY id LIMIT 1)');
|
||||
|
||||
// Now make it NOT NULL and add constraints
|
||||
$this->addSql('ALTER TABLE immobilien MODIFY verwalter_id INT NOT NULL');
|
||||
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789D3E5F66D3 FOREIGN KEY (verwalter_id) REFERENCES users (id)');
|
||||
$this->addSql('CREATE INDEX IDX_2C789D3E5F66D3 ON immobilien (verwalter_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789D3E5F66D3');
|
||||
$this->addSql('DROP INDEX IDX_2C789D3E5F66D3 ON immobilien');
|
||||
$this->addSql('ALTER TABLE immobilien DROP verwalter_id');
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
325
src/Entity/Immobilie.php
Normal file
325
src/Entity/Immobilie.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Enum\ImmobilienTyp;
|
||||
use App\Repository\ImmobilieRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ImmobilieRepository::class)]
|
||||
#[ORM\Table(name: 'immobilien')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
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: 'decimal', precision: 10, scale: 2)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Positive]
|
||||
private float $preis;
|
||||
|
||||
#[ORM\Column(type: 'decimal', precision: 8, scale: 2)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Positive]
|
||||
private float $flaeche;
|
||||
|
||||
#[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: 'boolean')]
|
||||
private bool $verfuegbar = true;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?int $balkonFlaeche = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?int $kellerFlaeche = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\Min(0)]
|
||||
#[Assert\Max(10)]
|
||||
private ?int $etage = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $heizungstyp = null;
|
||||
|
||||
#[ORM\Column(type: 'decimal', precision: 6, scale: 2, nullable: true)]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?float $nebenkosten = 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;
|
||||
}
|
||||
|
||||
#[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 getPreis(): float
|
||||
{
|
||||
return $this->preis;
|
||||
}
|
||||
|
||||
public function setPreis(float $preis): self
|
||||
{
|
||||
$this->preis = $preis;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFlaeche(): float
|
||||
{
|
||||
return $this->flaeche;
|
||||
}
|
||||
|
||||
public function setFlaeche(float $flaeche): self
|
||||
{
|
||||
$this->flaeche = $flaeche;
|
||||
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 isVerfuegbar(): bool
|
||||
{
|
||||
return $this->verfuegbar;
|
||||
}
|
||||
|
||||
public function setVerfuegbar(bool $verfuegbar): self
|
||||
{
|
||||
$this->verfuegbar = $verfuegbar;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBalkonFlaeche(): ?int
|
||||
{
|
||||
return $this->balkonFlaeche;
|
||||
}
|
||||
|
||||
public function setBalkonFlaeche(?int $balkonFlaeche): self
|
||||
{
|
||||
$this->balkonFlaeche = $balkonFlaeche;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getKellerFlaeche(): ?int
|
||||
{
|
||||
return $this->kellerFlaeche;
|
||||
}
|
||||
|
||||
public function setKellerFlaeche(?int $kellerFlaeche): self
|
||||
{
|
||||
$this->kellerFlaeche = $kellerFlaeche;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEtage(): ?int
|
||||
{
|
||||
return $this->etage;
|
||||
}
|
||||
|
||||
public function setEtage(?int $etage): self
|
||||
{
|
||||
$this->etage = $etage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeizungstyp(): ?string
|
||||
{
|
||||
return $this->heizungstyp;
|
||||
}
|
||||
|
||||
public function setHeizungstyp(?string $heizungstyp): self
|
||||
{
|
||||
$this->heizungstyp = $heizungstyp;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNebenkosten(): ?float
|
||||
{
|
||||
return $this->nebenkosten;
|
||||
}
|
||||
|
||||
public function setNebenkosten(?float $nebenkosten): self
|
||||
{
|
||||
$this->nebenkosten = $nebenkosten;
|
||||
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 getVerwalter(): User
|
||||
{
|
||||
return $this->verwalter;
|
||||
}
|
||||
|
||||
public function setVerwalter(User $verwalter): self
|
||||
{
|
||||
$this->verwalter = $verwalter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet den Preis pro Quadratmeter
|
||||
*/
|
||||
public function getPreisProQm(): float
|
||||
{
|
||||
if ($this->flaeche > 0) {
|
||||
return round($this->preis / $this->flaeche, 2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Gesamtfläche inkl. Balkon und Keller
|
||||
*/
|
||||
public function getGesamtflaeche(): float
|
||||
{
|
||||
$gesamt = $this->flaeche;
|
||||
if ($this->balkonFlaeche) {
|
||||
$gesamt += $this->balkonFlaeche;
|
||||
}
|
||||
if ($this->kellerFlaeche) {
|
||||
$gesamt += $this->kellerFlaeche;
|
||||
}
|
||||
return $gesamt;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use ApiPlatform\Metadata\Put;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
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\Validator\Constraints as Assert;
|
||||
|
||||
@@ -47,10 +49,14 @@ class User
|
||||
#[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();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -101,4 +107,28 @@ class User
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
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 %}
|
||||
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 %}
|
||||
Reference in New Issue
Block a user