Einleitung
Da ich ein Ferienhaus verwalten soll, habe ich mich mal nach passenden Extensions umgesehen.
Für die 3.1er Version von Contao finde ich da
- zibepla
- und diverse andere, die allerdings kommerziell sind
Also habe ich mir zibepla mal angesehen, aber so richtig passen wollte es nicht. Vor allem kann man eine Belegung keinem Kunden zuordnen.
Also habe ich mir gedacht, jetzt baue ich mal selber eine Extension 🙂
Die Planung
Backend
Im Backend möchte ich Termine für ein Ferienhaus reservieren können, mit dem Namen des Mieters, Anreise- und Abreisedatum und einem Textfeld für Anmerkungen.
Außerdem möchte ich Terminüberschneidungen optisch angezeigt bekommen.
Frontend
Im Frontend soll der Besucher den Belegungsplan möglichst übersichtlich zu sehen bekommen.
Tabelle
Da ich nur ein Haus verwalten möchte, und diese Extension erst mal auch noch einfach bleiben soll, komme ich mit einer Tabelle tl_bepla aus.
An Feldern benötige ich kunde, hinweis, von und bis (und natürlich die von Contao benötigten Felder id und timestamp).
Die Vorbereitung
Ich lege unter /system/modules einen neuen Ordner bepla an. Dieser erhält diese Ordnerstruktur:
bepla/assets
bepla/config
bepla/dca
bepla/languages
bepla/languages/de
bepla/modules
bepla/templates
Die Datei config.php
Im config-Ordner lege ich die Datei config.php mit diesem Inhalt (gekürzt) an:
<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
// BACK END MODULES
// Eintrag im Backend unter "Inhalte" (content) anlegen
$GLOBALS['BE_MOD']['content']['bepla'] = array(
'tables' => array('tl_bepla'), // Verwendete Tabelle
'icon' => 'system/modules/bepla/assets/icon.gif' // Verwendetes Icon
);
// FRONT END MODULES
$GLOBALS['FE_MOD']['Belegungsplan'] = array
(
'bepla_table' => 'ModuleBeplaTable',
);
?>
Damit das Icon auch angezeigt wird, kopiere ich die Icon-Datei in den Ordner assets.
Die Datei tl_bepla.php
Teil 1: DCA-Definitionen
Diese Datei ist die DCA-Datei, die beschreibt, was es im Backend für Möglichkeiten gibt. Außerdem definiert diese Datei auch die benötigte Tabelle tl_bepla.
Wichtig: Tabelle und DCA-Datei müssen den gleichen Namen haben.
<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
/**
* Table tl_bepla
*/
$GLOBALS['TL_DCA']['tl_bepla'] = array
(
// Config
'config' => array
(
'dataContainer' => 'Table',
'enableVersioning' => true,
'sql' => array
(
'keys' => array
(
'id' => 'primary'
)
)
),
// List
'list' => array
(
'sorting' => array
(
'mode' => 1,
'fields' => array('von'),
'flag' => 7,
'panelLayout' => 'filter,search,limit'
),
'label' => array
(
'fields' => array('kunde'),
'format' => '%s',
'label_callback' => array('tl_bepla','listBepla')
),
'operations' => array
(
'edit' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['edit'],
'href' => 'act=edit',
'icon' => 'edit.gif'
),
'copy' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['copy'],
'href' => 'act=copy',
'icon' => 'copy.gif'
),
'delete' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['delete'],
'href' => 'act=delete',
'icon' => 'delete.gif',
'attributes' => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
),
'show' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['show'],
'href' => 'act=show',
'icon' => 'show.gif'
)
)
),
// Edit
'edit' => array
(
'buttons_callback' => array()
),
// Palettes
'palettes' => array
(
//'__selector__' => array(''),
'default' => '{title_legend},kunde,von,bis,hinweis'
),
// Subpalettes
'subpalettes' => array
(
'' => ''
),
// Fields
'fields' => array
(
'id' => array
(
'sql' => "int(10) unsigned NOT NULL auto_increment"
),
'tstamp' => array
(
'sql' => "int(10) unsigned NOT NULL default '0'"
),
'kunde' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['kunde'],
'inputType' => 'text',
'exclude' => true,
'search' => true,
'eval' => array('mandatory'=>true, 'maxlength'=>64),
'sql' => "varchar(64) NOT NULL default ''"
),
'hinweis' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['hinweis'],
'inputType' => 'textarea',
'eval' => array('rte' => 'tinyFlash'),
'sql' => "text NULL"
),
'von' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['von'],
'inputType' => 'text',
'default' => time(),
'eval' => array('mandatory'=>true, 'rgxp'=>'date', 'datepicker'=>$this->getDatePickerString(), 'tl_class'=>'w50 wizard'),
'sql' => "int(10) unsigned NOT NULL default '0'"
),
'bis' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_bepla']['bis'],
'inputType' => 'text',
'default' => time(),
'eval' => array('mandatory'=>true, 'rgxp'=>'date', 'datepicker'=>$this->getDatePickerString(), 'tl_class'=>'w50 wizard'),
'sql' => "int(10) unsigned NOT NULL default '0'"
)
)
);
// ***** An diese Stelle kommt die Routine zur Anpassung der Übersichtsliste im Backend
?>
Für ausführliche Erklärungen zum Aufbau einer DCA-Datei verweise ich auf die offizielle Doku.
Hier nun ein paar zusätzliche Erklärungen:
Im List-Bereich unter ‚label‘ rufe ich eine eigene Routine auf, mit der ich die Ausgabe der Belegungsliste im Backend nach meinen Wünschen anpasse. So werden mir etwa Terminüberschneidungen gezeigt.
Der Bereich ‚fields‘ definiert die Tabelle tl_bepla, das ging früher (vor Version 3) über eine Datei database.sql im config-Ordner.
Teil 2: Anpassung der Listenausgabe
Da die default-Listenausgabe nur sehr eingeschränkte Daten ausgibt, wollte ich die auf jeden Fall ändern. Ich möchte nämlich den Belegungszeitraum, den Namen des Mieters, die Hinweise und vor allem Meldungen über Terminprobleme direkt sehen können.
Also habe ich den list-Eintrag ‚label‘ um eine Callback-Funktion erweitert.
Folgender Code gehört ganz an’s Ende der DCA-Datei tl_bepla.php:
class tl_bepla extends Backend {
public function listBepla($row, $label)
{
// $row enthält alle Felder der Datenbanktabelle
// $label enthält die Daten aus $GLOBALS['TL_DCA']['tl_bepla']['list']['label'] und wird hier nicht benötigt
$erg="<div>";
$erg=$erg."<div><strong>".date('d.m.Y',$row['von'])." - ".date('d.m.Y',$row['bis'])." ".$row['kunde']."</strong></div>";
$erg=$erg."<div>".$row['hinweis']."</div>";
$erg=$erg."</div>";
// Plausibilitätsprüfung
$style="style=\"font-weight:bold; color:#f00;\"";
$error[0]="";
$error[1]="";
// v und sind die aktuellen Werte, von und bis die Datenbankspalten
// 1. v > b
if ($row['von'] > $row['bis']) $error[0]="<div $style>Anreisetag liegt nach Abreisetag</div>";
// 2. v > von und v < bis : Starttermin liegt in belegtem Bereich
$objData = $this->Database->execute("SELECT id from tl_bepla where ".$row['von']." > von and ".$row['von']." < bis");
$c= $objData->numRows; // Anzahl der Ergebnisse
if ($c>0) $error[1]="<div $style>Buchungsüberschneidung</div>";
// 3. b > von und b < bis : Endetermin liegt in belegtem Bereich
$objData = $this->Database->execute("SELECT id from tl_bepla where ".$row['bis']." > von and ".$row['bis']." < bis");
$c= $objData->numRows; // Anzahl der Ergebnisse
if ($c>0) $error[1]="<div $style>Buchungsüberschneidung</div>";
// 4. v < von und b > bis : Komplette Überschneidung
$objData = $this->Database->execute("SELECT id from tl_bepla where ".$row['von']." < von and ".$row['bis']." > bis");
$c= $objData->numRows; // Anzahl der Ergebnisse
if ($c>0) $error[1]="<div $style>Buchungsüberschneidung</div>";
$erg=$error[0].$error[1].$erg;
return $erg;
}
}
Die Language-Dateien
Unter languages/de erstelle ich zwei Dateien, mit denen ich die korrekte Beschriftung steuere.
Die Datei modules.php
<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
// Back end modules
// Deutsche Beschriftung für den BackEnd-Eintrag unter Inhalte
$GLOBALS['TL_LANG']['MOD']['bepla'] = array('Belegungsplan', 'Verwalten Sie die Belegung hier');
// Front end modules
$GLOBALS['TL_LANG']['FMD']['bepla_table'] = array('Belegungsplan-Tabelle', 'Zeigt die aktuelle Belegung an');
?>
Die Datei tl_bepla.php
<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
// Fields
$GLOBALS['TL_LANG']['tl_bepla']['von'] = array('Anreisetag', 'Geben Sie den Anreisetag hier ein.');
$GLOBALS['TL_LANG']['tl_bepla']['bis'] = array('Abreisetag', 'Geben Sie den Abreisetag hier ein.');
$GLOBALS['TL_LANG']['tl_bepla']['kunde'] = array('Kundenname', 'Geben Sie den Namen des Kunden hier ein.');
$GLOBALS['TL_LANG']['tl_bepla']['hinweis'] = array('Hinweise', 'Geben Sie zusätzliche Hinweise zum Kunden hier ein.');
// Legends
$GLOBALS['TL_LANG']['tl_bepla']['title_legend'] = 'Buchungsdaten';
// Buttons
$GLOBALS['TL_LANG']['tl_bepla']['new'] = array('Neue Buchung', 'Neue Buchung anlegen');
$GLOBALS['TL_LANG']['tl_bepla']['show'] = array('Buchungsdetails', 'Details der Buchung ID %s anzeigen');
$GLOBALS['TL_LANG']['tl_bepla']['edit'] = array('Buchung bearbeiten', 'Buchung ID %s bearbeiten');
$GLOBALS['TL_LANG']['tl_bepla']['copy'] = array('Buchung kopieren', 'Buchung ID %s kopieren');
$GLOBALS['TL_LANG']['tl_bepla']['delete'] = array('Buchung löschen', 'Buchung ID %s löschen');
?>
Das Frontend-Modul
Teil 1: Das Modul
Der Name des Moduls, das unter bepla/modules gespeichert ist, muß mit dem in der config-Datei definierten Namen für das Frontend-Modul übereinstimmen, in meinem Fall also ModuleBeplaTable.
In diesem Modul wird eigentlich nichts weiter gemacht, als die Daten aus der Tabelle tl_bepla zu lesen und sie an das FE-Template weiterzugeben.
<?php
namespace bepla;
if (!defined('TL_ROOT')) die('You can not access this file directly!');
class ModuleBeplaTable extends \Module
{
protected $strTemplate = 'mod_bepla_table';
// Generate the module
protected function compile()
{
$arrBelegung = array();
$objBelegung = $this->Database->execute("SELECT * FROM tl_bepla order by von ");
while ($objBelegung->next())
{
$arrBelegung[] = array
(
'von' => $objBelegung->von,
'bis' => $objBelegung->bis,
'kunde' => $objBelegung->kunde
);
}
$this->Template->belegung = $arrBelegung;
}
}
?>
Teil 2: Das Templat mod_bepla_table.html5
Das Template sorgt für die Erstellung der Tabelle im Frontend und muß den Namen haben, der im obigen Modul benutzt wurde, in meinem Fall also mod_bepla_table.php.
<div class="<?php echo $this->class; ?> block"<?php echo $this->cssID; ?><?php if ($this->style): ?> style="<?php echo $this->style; ?>"<?php endif; ?>>
<?php
$monate = array(1=>"Jan",
2=>"Feb",
3=>"Mär",
4=>"Apr",
5=>"Mai",
6=>"Jun",
7=>"Jul",
8=>"Aug",
9=>"Sep",
10=>"Okt",
11=>"Nov",
12=>"Dez");
$wochentage = array(1=>"Montag",
2=>"Dienstag",
3=>"Mittwoch",
4=>"Donnerstag",
5=>"Freitag",
6=>"Samstag",
7=>"Sonntag");
$wtk = array( 1=>"Mo",
2=>"Di",
3=>"Mi",
4=>"Do",
5=>"Fr",
6=>"Sa",
7=>"So");
$GLOBALS['TL_CSS']['bepla'] = 'system/modules/bepla/assets/styles.css';
?>
<?php if ($this->headline): ?>
<<?php echo $this->hl; ?>><?php echo $this->headline; ?></<?php echo $this->hl; ?>>
<?php endif; ?>
<?php
// letzte Buchung ermitteln
$last=0;
foreach ($this->belegung as $data) if ($data['bis']>$last) $last=$data['bis'];
$monat_ende=date("n",$last);
if ($last==0) $monat_ende=$monat=date("n");
$jahr_ende=date("Y",$last);
$monat_ende=$monat_ende+3;
if ($monat_ende>12)
{
$monat_ende=$monat_ende-12;
$jahr_ende=$jahr_ende+1;
}
?>
<table border="0" cellpadding="0" class="table_bepla">
<tr>
<td class="monat"></td>
<?php
for ($t=1;$t<=31;$t++) echo "<td class=\"tag\">$t</td>";
?>
</tr>
<?php
$jahr=date("Y");
$monat=date("n");
while ($jahr<$jahr_ende || $monat<=$monat_ende)
{
echo "<tr>";
echo "<td class=\"monat\">".$monate[$monat]." ".$jahr."</td>";
for ($tag=1;$tag<=31;$tag++)
{
$class="nodate";
$title="Diesen Tag gibt es nicht";
$wota="";
if (checkdate($monat,$tag,$jahr))
{
$class="frei";
$curdate=strtotime($jahr."-".$monat."-".$tag);
$wt=$wochentage[date("N",$curdate)];
$title="$wt, $tag.$monat.$jahr";
$wota=$wtk[date("N",$curdate)];
foreach ($this->belegung as $data)
{
$von=$data['von'];
$bis=$data['bis'];
if ($von==$curdate && $class=="abreise")
{
$class="wechsel";
}
elseif ($von==$curdate && $bis==$curdate)
{
$class="wechsel";
}
elseif ($von==$curdate)
{
$class="anreise";
}
elseif ($bis==$curdate)
{
$class="abreise";
}
elseif ($von<$curdate && $bis>$curdate)
{
$class="belegt";
}
}
}
echo "<td class=\"$class tag small\" title=\"$title\">$wota</td>";
}
echo "</tr>";
$monat++;
if ($monat>12)
{
$monat=1;
$jahr++;
}
}
?>
</table>
<table border="0" cellpadding="0" class="table_legende">
<tr>
<td class="tag frei"></td><td class="legende">Frei</td>
<td class="tag anreise"></td><td class="legende">Anreise</td>
<td class="tag belegt"></td><td class="legende">Belegt</td>
<td class="tag abreise"></td><td class="legende">Abreise</td>
<td class="tag wechsel"></td><td class="legende">Wechsel</td>
</tr>
</table>
</div>
Das einzige interessante hier ist die Einbindung der CSS-Datei, diese liegt, genau wie die benötigten Bilder für die Tabelle, im assets-Ordner.
Die Extension in Contao registrieren
Um die Extension in Contao zum Laufen zu bringen, rufe ich jetzt einmal den Autoload-Creator auf und erzeuge die Dateien für mein Modul bepla.
Danach rufe ich in der Erweiterungsverwaltung den Befehl Datenbank aktualisieren auf.
Hier sollte jetzt die Abfrage zur Erstellung der Tabelle tl_bepla erscheinen.
Das Ergebnis
Jetzt noch ein paar Bilder, wie das Ganze aussehen kann.


Download
Die komplette Erweiterung kann man hier herunterladen.
Geplante Erweiterungen
Die komplette Extension ist natürlich (wie meistens) Work in progress 🙂
Außerdem gibt es natürlich noch viel Optimierungspotential.
- Verwaltung für mehrere Häuser erweitern
- Optimierte Anzeige im Frontend, idealerweise kombiniert mit einer Buchungsanfrage
- Integration in meinen OwnCloud-Kalender
- Komplette Mieterverwaltung, am Besten mit Rechnungserstellung