Touren application master
authorJoey Schulze <joey@infodrom.org>
Thu, 12 Sep 2019 14:47:19 +0000 (16:47 +0200)
committerJoey Schulze <joey@infodrom.org>
Fri, 8 Nov 2019 11:34:14 +0000 (12:34 +0100)
24 files changed:
class/tour.class.php [new file with mode: 0644]
class/tour_date.class.php [new file with mode: 0644]
class/tour_date_status.class.php [new file with mode: 0644]
class/tour_log.class.php [new file with mode: 0644]
class/tour_status.class.php [new file with mode: 0644]
controller/indexcontroller.class.php
controller/tourcontroller.class.php [new file with mode: 0644]
core/sys_user.class.php
templates/page/list.phtml [new file with mode: 0644]
templates/tour/admin.phtml [new file with mode: 0644]
templates/tour/datelist.phtml [new file with mode: 0644]
templates/tour/dates.phtml [new file with mode: 0644]
templates/tour/index.phtml [new file with mode: 0644]
templates/tour/invite.phtml [new file with mode: 0644]
templates/tour/invitemail.phtml [new file with mode: 0644]
templates/tour/log.phtml [new file with mode: 0644]
templates/tour/matrix.phtml [new file with mode: 0644]
templates/tour/notepreview.phtml [new file with mode: 0644]
templates/tour/notes.phtml [new file with mode: 0644]
templates/tour/plan.phtml [new file with mode: 0644]
templates/tour/pov.phtml [new file with mode: 0644]
templates/tour/povlist.phtml [new file with mode: 0644]
touren.css
touren.js

diff --git a/class/tour.class.php b/class/tour.class.php
new file mode 100644 (file)
index 0000000..4bdaf31
--- /dev/null
@@ -0,0 +1,333 @@
+<?php
+
+class Tour extends DatabaseTable
+{
+    protected $_is_admin;
+    protected $_is_planned;
+
+    public function __construct($id, $column=false)
+    {
+       parent::__construct('tour', $id, $column);
+    }
+
+    public function isAdmin()
+    {
+       if (is_null($this->_is_admin)) {
+           $sql = sprintf("SELECT admin FROM tour_member WHERE tour_id = %d AND member_id = %d",
+                          $this->id, $_SESSION['userid']);
+           $this->_is_admin = $this->db->fetchValue($sql);
+       }
+
+       return $this->_is_admin;
+    }
+
+    public function isPlanned()
+    {
+       if (is_null($this->_is_planned)) {
+           $sql = sprintf("SELECT key = 'plan' FROM tour_status WHERE id = %d", $this->data->tour_status_id);
+           $this->_is_planned = $this->db->fetchValue($sql);
+       }
+
+       return $this->_is_planned;
+    }
+
+    public function getBaseData()
+    {
+       $sql = <<<EOS
+           SELECT
+               tour.name,
+               year,
+               duration,
+               information,
+               start_date,
+               extract(day from start_date) ||'.'|| extract(month from start_date) ||'.'|| extract(year from start_date) AS start_datum,
+               tour_status.key AS status_key,
+               tour_status.name AS status
+           FROM tour
+           JOIN tour_status ON tour_status_id = tour_status.id
+           LEFT JOIN tour_date ON tour_date_id = tour_date.id
+           WHERE tour.id = {$this->id}
+EOS;
+
+       return $this->db->fetchObject($sql);
+    }
+
+    public function toggleDateStatus($tour_date_id, $sys_user_id)
+    {
+       $date = new Tour_Date($tour_date_id);
+
+       $sql =<<<EOS
+           SELECT tour_date_member.id,tour_date_status_id
+           FROM tour_date
+           JOIN tour_date_member ON tour_date_id = tour_date.id
+           JOIN tour_member ON tour_member_id = tour_member.id
+           WHERE tour_date.id = %d AND tour_member.member_id = %d
+EOS;
+       $sql = sprintf($sql, $tour_date_id, $sys_user_id);
+
+       $row = $this->db->fetchObject($sql);
+
+       if ($row === false) {
+           $sql =<<<EOS
+               SELECT tour_member.id
+               FROM tour_date
+               JOIN tour ON tour_id = tour.id
+               JOIN tour_member using(tour_id)
+               WHERE tour_date.id = %d AND member_id = %d
+EOS;
+           $sql = sprintf($sql, $tour_date_id, $sys_user_id);
+           $member_id = $this->db->fetchValue($sql);
+
+           $sql = "SELECT id FROM tour_date_status ORDER BY priority OFFSET 1 LIMIT 1";
+           $status_id = $this->db->fetchValue($sql);
+
+           $this->db->insertInto('tour_date_member', ['tour_date_id' => $tour_date_id,
+                                                      'tour_member_id' => $member_id,
+                                                      'tour_date_status_id' => $status_id]);
+           $status = new Tour_Date_Status($status_id);
+           if ($sys_user_id == $_SESSION['userid'])
+               Tour_Log::add($this->id(), sprintf("Verfügbarkeit am %s: %s",
+                                                  $date->germanDate(), $status->get('name')));
+           else {
+               $user = new Sys_User($sys_user_id);
+               Tour_Log::add($this->id(), sprintf("Verfügbarkeit von %s am %s: %s",
+                                                  $user->get('name'),
+                                                        $date->germanDate(), $status->get('name')));
+           }
+       } else {
+           $sql = sprintf("SELECT id FROM tour_date_status WHERE id > %d ORDER BY priority LIMIT 1",
+                          $row->tour_date_status_id);
+           $status_id = $this->db->fetchValue($sql);
+
+           if ($status_id === false) {
+               $sql = "SELECT id FROM tour_date_status ORDER BY priority LIMIT 1";
+               $status_id = $this->db->fetchValue($sql);
+           }
+
+           $this->db->update('tour_date_member', ['tour_date_status_id' => $status_id], 'id = ' . $row->id);
+
+           $status = new Tour_Date_Status($status_id);
+           $status_name = $status->get('name');
+           if (!strlen($status_name))
+               $status_name = 'n/a';
+
+           if ($sys_user_id == $_SESSION['userid']) {
+               Tour_Log::addRewrite($this->id(),
+                                    sprintf("Verfügbarkeit am %s: ", $date->germanDate()),
+                                    $status_name);
+           } else {
+               $user = new Sys_User($sys_user_id);
+               Tour_Log::addRewrite($this->id(),
+                                    sprintf("Verfügbarkeit von %s am %s: ", $user->get('name'), $date->germanDate()),
+                                    $status_name);
+           }
+       }
+    }
+
+    public function inviteMember($sys_user_id)
+    {
+       if (!$this->isAdmin())
+           throw new Exception("Keine Berechtigung zum Einladen");
+
+       $user = new Sys_User($sys_user_id);
+
+       if (!$user->id())
+           throw new Exception("User $sys_user_id not found");
+
+       $this->db->insertInto('tour_member',
+           ['tour_id' => $this->id(),
+            'member_id' => $sys_user_id]);
+
+       $myself = new Sys_User($_SESSION['userid']);
+       $request = Application::get()->getRequest();
+       $url = sprintf('%s://%s%s', $request->getScheme(), $request->getHttpHost(), Application::get()->getBaseURL());
+       $body = Template::render('tour/invitemail',
+                                ['inviter' => $myself->get('name'),
+                                 'tour_name' => $this->get('name'),
+                                 'tour_year' => $this->get('year'),
+                                 'tour_duration' => $this->get('duration'),
+                                 'name' => $user->get('name'),
+                                 'email' => $user->get('email'),
+                                 'url' => $url,
+                                 ]);
+       $mail = new Mail();
+       $mail->env_from(MAIL_FROM);
+       $mail->set('From', mb_encode_mimeheader(utf8_decode(sprintf("%s <%s>", MAIL_FROM_NAME, MAIL_FROM)),'latin1'));
+       $mail->set('To', $user->get('email'));
+       $mail->set('Subject', mb_encode_mimeheader(utf8_decode(sprintf("Einladung zur Tour %s", $this->get('name'))),'latin1'));
+
+       $mail->send($body);
+    }
+
+    public function getDates()
+    {
+       $sql = <<<EOS
+           SELECT
+               tour_date.id,
+               start_date,
+               extract(day from start_date) ||'.'|| extract(month from start_date) ||'.'|| extract(year from start_date) AS start_datum
+           FROM tour_date
+           WHERE tour_date.tour_id = {$this->id()}
+           ORDER BY start_date
+EOS;
+
+       $list = $this->db->fetchObjectList($sql);
+
+       foreach ($list as &$row) {
+           $dt = new DateTime($row->start_date);
+           $row->year = $dt->format('Y');
+           $row->start_short = $dt->format('d.m.');
+           $dt->add(new DateInterval('P'.$this->get('duration').'D'));
+           $row->end_short = $dt->format('d.m.');
+       }
+
+       return $list;
+    }
+
+    public function getMembers($admin=null)
+    {
+       if (!is_null($admin))
+           $cond_admin = ' AND tour_member.admin = ' . ($admin ? 'true' : 'false');
+       $sql = <<<EOS
+           SELECT
+               tour_member.id,
+               sys_user.name,
+               sys_user.email,
+               sys_user.mobile,
+               sys_user.single_room,
+               tour_member.comment,
+               tour_member.admin
+           FROM sys_user
+           JOIN tour_member ON member_id = sys_user.id
+           WHERE tour_id = {$this->id}{$cond_admin}
+           ORDER BY name
+EOS;
+
+       $list = $this->db->fetchObjectList($sql);
+
+       return $list;
+    }
+
+    public function getAvailability($user_id)
+    {
+       $list = $this->getDates();
+
+       if (!count($list)) return [];
+
+       $ids = [];
+       foreach ($list as &$row)
+           $ids[] = $row->id;
+       $ids = join(',', $ids);
+
+       $sql = <<<EOS
+           SELECT
+             tour_date_id,
+             tour_date_status.key AS status_key,
+             tour_date_status.name AS status_text
+           FROM tour_date_member
+           JOIN tour_member ON tour_member_id = tour_member.id AND member_id = {$user_id}
+           JOIN tour_date_status ON tour_date_status_id = tour_date_status.id
+           WHERE tour_date_id IN ({$ids})
+EOS;
+
+       foreach ($this->db->fetchObjectList($sql) as $item) {
+           foreach ($list as &$row) {
+               if ($row->id == $item->tour_date_id) {
+                   $row->status_key = $item->status_key;
+                   $row->status_text = $item->status_text;
+                   break;
+               }
+           }
+       }
+
+       return $list;
+    }
+
+    public function getAvailabilityMatrix()
+    {
+       $list = $this->getDates();
+
+       $ids = [];
+       foreach ($list as &$row)
+           $ids[] = $row->id;
+       $ids = join(',', $ids);
+
+       $sql = <<<EOS
+           SELECT
+             tour_date_id,
+             member_id,
+             sys_user.name,
+             tour_date_status.key AS status_key,
+             tour_date_status.name AS status_text
+           FROM tour_date_member
+           JOIN tour_member ON tour_member_id = tour_member.id
+           JOIN sys_user ON sys_user.id = member_id
+           JOIN tour_date_status ON tour_date_status_id = tour_date_status.id
+           WHERE tour_date_id IN ({$ids})
+EOS;
+
+       $userlist = [];
+       foreach ($this->db->fetchObjectList($sql) as $item) {
+           foreach ($list as &$row) {
+               if ($row->id == $item->tour_date_id) {
+                   if (!is_array($row->avail))
+                       $row->avail = [];
+
+                   if (!in_array($item->member_id, $userlist)) {
+                       $nick = '';
+                       foreach (explode(' ', $item->name) as $word)
+                           $nick .= substr($word,0,1);
+
+                       $user = new stdClass();
+                       $user->nick = $nick;
+                       $user->name = $item->name;
+                       $userlist[$item->member_id] = $user;
+                   }
+
+                   $av = new stdClass();
+                   $av->member_id = $item->member_id;
+                   $av->nick = $userlist[$item->member_id]->nick;
+                   $av->status_key = $item->status_key;
+                   $av->status_text = $item->status_text;
+
+                   $row->avail[$item->member_id] = $av;
+                   break;
+               }
+           }
+       }
+
+       return ['user' => $userlist, 'list' => $list];
+    }
+
+    public function getNotes($all=false)
+    {
+       if ($all !== true)
+           $condition = ' AND tour_note_seen.id IS NULL';
+       $sql = <<<EOS
+           SELECT
+               tour_note.id,
+               tour_note.note,
+               tour_note.sys_user_id = {$_SESSION['userid']} AS own,
+               sys_user.name,
+               sys_user.sys_edit,
+               extract(day from sys_user.sys_edit) ||'.'|| extract(month from sys_user.sys_edit) ||'.'|| extract(year from sys_user.sys_edit) AS datum
+           FROM tour_note
+           JOIN sys_user ON tour_note.sys_user_id = sys_user.id
+           LEFT JOIN tour_note_seen ON tour_note_id = tour_note.id
+           WHERE tour_id = {$this->id} AND deleted = false{$condition}
+           ORDER BY tour_note.sys_edit DESC
+EOS;
+
+       $list = $this->db->fetchObjectList($sql);
+
+       if (count($list)) {
+           foreach ($list as &$row) {
+               $row->note_html = Wiki::renderHTML($row->note);
+               if ($all !== true)
+                   $this->db->insertInto('tour_note_seen', ['tour_note_id' => $row->id]);
+           }
+       }
+
+       return $list;
+    }
+}
\ No newline at end of file
diff --git a/class/tour_date.class.php b/class/tour_date.class.php
new file mode 100644 (file)
index 0000000..2daa964
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+class Tour_Date extends DatabaseTable
+{
+    public function __construct($id, $column=false)
+    {
+       parent::__construct('tour_date', $id, $column);
+    }
+
+    public function germanDate($withYear=false)
+    {
+       $parts = explode('-', $this->data->start_date);
+
+       if ($withYear)
+           return sprintf('%d.%d.%04d', $parts[2], $parts[1], $parts[0]);
+       else
+           return sprintf('%d.%d.', $parts[2], $parts[1]);
+    }
+}
\ No newline at end of file
diff --git a/class/tour_date_status.class.php b/class/tour_date_status.class.php
new file mode 100644 (file)
index 0000000..4d2c4c7
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+class Tour_Date_Status extends DatabaseTable
+{
+    public function __construct($id, $column=false)
+    {
+       parent::__construct('tour_date_status', $id, $column);
+    }
+}
\ No newline at end of file
diff --git a/class/tour_log.class.php b/class/tour_log.class.php
new file mode 100644 (file)
index 0000000..620a6bd
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+class Tour_Log extends DatabaseTable
+{
+    protected $seconds = 10;
+
+    public static function add($tour_id, $text)
+    {
+       $log = new Tour_Log();
+       $log->addLog($tour_id, $text);
+    }
+
+    public static function addRewrite($tour_id, $base_text, $appendix)
+    {
+       $log = new Tour_Log();
+       $log->addRewriteLog($tour_id, $base_text, $appendix);
+    }
+
+    protected function __construct($id=false)
+    {
+       parent::__construct('tour_log', $id);
+    }
+
+    protected function addLog($tour_id, $text)
+    {
+       return $this->db->insertInto($this->table,
+                                    ['tour_id' => $tour_id,
+                                     'logdate' => 'now()',
+                                     'logtext' => $text]);
+    }
+
+    protected function addRewriteLog($tour_id, $base_text, $appendix)
+    {
+       $sql = sprintf("SELECT id FROM %s WHERE tour_id = %d AND sys_user_id = %d AND logtext LIKE %s AND sys_edit > (now() - INTERVAL '%d seconds')",
+                      $this->table,
+                      $tour_id,
+                      $_SESSION['userid'],
+                      $this->db->quote($base_text . '%'),
+                      $this->seconds);
+       $id = $this->db->fetchValue($sql);
+
+       if ($id) {
+           return $this->db->update($this->table,
+                                    ['logtext' => $base_text . $appendix],
+                                    'id='.$id);
+       } else {
+           return $this->db->insertInto($this->table,
+                                        ['tour_id' => $tour_id,
+                                         'logdate' => 'now()',
+                                         'logtext' => $base_text . $appendix]);
+       }
+    }
+}
\ No newline at end of file
diff --git a/class/tour_status.class.php b/class/tour_status.class.php
new file mode 100644 (file)
index 0000000..d3f97dc
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+class Tour_Status extends DatabaseTable
+{
+    public function __construct($id, $column=false)
+    {
+       parent::__construct('tour_status', $id, $column);
+    }
+}
\ No newline at end of file
index 09df359..adde583 100644 (file)
@@ -17,6 +17,12 @@ class IndexController extends ControllerBase implements ControllerInterface
                   'title' => 'Home'];
        $list[] = ['url' => $this->app->getBaseURL() . 'index/settings',
                   'title' => 'Einstellungen'];
+       if ($this->app->isAdmin()) {
+           $list[] = ['url' => $this->app->getBaseURL() . 'tour/newmember',
+                      'title' => 'Neuer Biker'];
+           $list[] = ['url' => $this->app->getBaseURL() . 'tour/new',
+                      'title' => 'Neue Tour'];
+       }
        $list[] = ['url' => $this->app->getBaseURL() . 'account/logout',
                   'title' => 'Logout'];
 
diff --git a/controller/tourcontroller.class.php b/controller/tourcontroller.class.php
new file mode 100644 (file)
index 0000000..91ff5da
--- /dev/null
@@ -0,0 +1,763 @@
+<?php
+
+class TourController extends ControllerBase implements ControllerInterface
+{
+    public function allowUnauthenticated()
+    {
+       return [];
+    }
+
+    public function getNavigation()
+    {
+       if (empty($_SESSION['userid']))
+           return [];
+
+       $list = [];
+       $list[] = ['title' => 'Home',
+                  'url' => Application::url()];
+       $list[] = ['title' => 'Status',
+                  'url' => Application::url('tour', 'index', $this->tour)];
+       if ($this->tour && $this->tour->isAdmin() && $this->tour->isPlanned())
+           $list[] = ['title' => 'Neu einladen',
+                      'url' => Application::url('tour', 'invite', $this->tour)];
+       if ($this->tour && $this->tour->isAdmin() && $this->tour->isPlanned())
+           $list[] = ['title' => 'Termine',
+                      'url' => Application::url('tour', 'dates', $this->tour)];
+       if ($this->tour && $this->tour->isPlanned())
+           $list[] = ['title' => 'Planung',
+                      'url' => Application::url('tour', 'plan', $this->tour)];
+       if ($this->tour && $this->tour->isAdmin())
+           $list[] = ['title' => 'Matrix',
+                      'url' => Application::url('tour', 'matrix', $this->tour)];
+       $list[] = ['title' => 'Notizen',
+                  'url' => Application::url('tour', 'notes', $this->tour)];
+       $list[] = ['title' => 'Zwischenziele',
+                  'url' => Application::url('tour', 'pov', $this->tour)];
+       if ($this->tour && $this->tour->isAdmin()) {
+           $list[] = ['title' => 'Administration',
+                      'url' => Application::url('tour', 'admin', $this->tour)];
+           $list[] = ['title' => 'Logbuch',
+                      'url' => Application::url('tour', 'log', $this->tour)];
+       }
+
+       return $list;
+    }
+
+    public function indexAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $data = $this->tour->getBaseData();
+       $data->information_html = Wiki::renderHTML($data->information);
+
+       Application::get()->addJavascriptCode("load_dates();");
+
+       $vars = array_merge(get_object_vars($data),
+                           ['admin' => $this->tour->isAdmin(),
+                            'notes' => $this->tour->getNotes(),
+                            'members' => $this->tour->getMembers()]);
+
+       $response->setData(Template::render('tour/index', $vars));
+    }
+
+    public function adminAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $form = new Form('tourplan');
+       $form->add(new FormElement('text', ['name' => 'name',
+                                           'title' => 'Name',
+                                           'help' => 'z.B. Sauerland 2019',
+                                           'value' => $this->tour->get('name')]));
+       $form->add(new FormElement('text', ['name' => 'urlkey',
+                                           'title' => 'URL-Key',
+                                           'help' => 'Keine Leer- oder Sonderzeichen, z.B. sl2019',
+                                           'value' => $this->tour->get('key')]));
+       $form->add(new FormElement('number', ['name' => 'year',
+                                             'title' => 'Jahr',
+                                             'min' => date('Y'),
+                                             'max' => 2050,
+                                             'value' => $this->tour->get('year')]));
+       $form->add(new FormElement('number', ['name' => 'duration',
+                                             'title' => 'Dauer in Tagen',
+                                             'min' => 1,
+                                             'value' => $this->tour->get('duration')]));
+
+       $sql = "SELECT id AS value, name AS text FROM tour_status ORDER BY priority";
+       $form->add(new FormElement('select', ['name' => 'tour_status_id',
+                                             'title' => 'Tour-Status',
+                                             'selected' => $this->tour->get('tour_status_id'),
+                                             'options' => $this->db->fetchObjectList($sql)]));
+       $options = [];
+       foreach ($this->tour->getDates() as $row)
+           $options[] = new Storage(['value' => $row->id, 'text' => $row->start_datum]);
+
+       $form->add(new FormElement('select', ['name' => 'tour_date_id',
+                                             'title' => 'Datum',
+                                             'empty' => '-- bitte wählen --',
+                                             'selected' => $this->tour->get('tour_date_id'),
+                                             'options' => $options]));
+
+       $form->add(new FormElement('textarea', ['name' => 'information',
+                                               'title' => 'Tourinformationen',
+                                               'help' => 'MoinMoin-typische Syntax erlaubt',
+                                               'rows' => 5,
+                                               'value' => $this->tour->get('information')]));
+
+       $options = [];
+       foreach ($this->tour->getMembers(false) as $row)
+           $options[] = new Storage(['value' => $row->id, 'text' => $row->name]);
+
+       if (count($options)) {
+           $adminform = new Form('touradmin');
+           $adminform->add(new FormElement('select', ['name' => 'tour_member_id',
+                                                      'title' => 'Weiterer Tourleiter',
+                                                      'empty' => '-- Bitte wählen --',
+                                                      'options' => $options]));
+           $adminformstr = $adminform->toString();
+       } else {
+           $adminformstr = '';
+       }
+
+       $adminlist = $this->tour->getMembers(true);
+
+       $response->setData(Template::render('tour/admin', ['form' => $form->toString(),
+                                                          'adminform' => $adminformstr,
+                                                          'adminlist' => $adminlist]));
+    }
+
+    public function ajaxAdmin($request, $response, $data)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       foreach (['name', 'urlkey', 'year', 'duration'] as $name)
+           if (!strlen($data[$name]))
+               throw new Exception('Nicht alle Pflichtfelder ausgefüllt');
+
+       if (!preg_match('/^[a-zA-Z0-9_\.-]+$/', $data['urlkey']))
+           throw new Exception('Sonderzeichen nicht im URL-Key erlaubt');
+
+       $status_plan = new Tour_Status('plan', 'key');
+
+       $ok = $this->db->update('tour', ['key' => $data['urlkey'],
+                                        'name' => $data['name'],
+                                        'year' => intval($data['year']),
+                                        'duration' => intval($data['duration']),
+                                        'tour_status_id' => $data['tour_status_id'],
+                                        'tour_date_id' => strlen($data['tour_date_id']) ? intval($data['tour_date_id']) : NULL,
+                                        'information' => strlen($data['information']) ? $data['information'] : NULL,
+                                        ], 'id='.$this->tour->id());
+
+       if (!$ok)
+           throw new Exception('Tour-Daten konnten nicht aktualisiert werden');
+    }
+
+    public function ajaxTouradmin($request, $response, $data)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $this->db->update('tour_member', ['admin' => true], 'id='.$data['tour_member_id']);
+
+       $sql = sprintf("SELECT name FROM sys_user JOIN tour_member ON member_id = sys_user.id WHERE tour_member.id = %d",
+                      $data['tour_member_id']);
+       Tour_Log::add($this->tour->id(), sprintf("%s als Leiter hinzugefügt", $this->db->fetchValue($sql)));
+    }
+
+    public function ajaxDeladmin($request, $response, $data)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $this->db->update('tour_member', ['admin' => false], 'id='.$data['id']);
+
+       $sql = sprintf("SELECT name FROM sys_user JOIN tour_member ON member_id = sys_user.id WHERE tour_member.id = %d",
+                      $data['id']);
+       Tour_Log::add($this->tour->id(), sprintf("%s als Leiter gelöscht", $this->db->fetchValue($sql)));
+    }
+
+    protected function formatDates()
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $ok = new Tour_Date_Status('ok', 'key');
+       $maybe = new Tour_Date_Status('maybe', 'key');
+       $not = new Tour_Date_Status('nope', 'key');
+       $sql = <<<EOS
+           SELECT
+             id,
+             (SELECT count(*) FROM tour_date_member WHERE tour_date_id = tour_date.id AND tour_date_status_id = {$ok->id()}) AS sum_ok,
+             (SELECT count(*) FROM tour_date_member WHERE tour_date_id = tour_date.id AND tour_date_status_id = {$maybe->id()}) AS sum_maybe,
+             (SELECT count(*) FROM tour_date_member WHERE tour_date_id = tour_date.id AND tour_date_status_id = {$not->id()}) AS sum_not,
+             start_date
+           FROM tour_date
+           WHERE tour_id = {$this->tour->id()}
+           ORDER BY start_date
+EOS;
+
+       $list = $this->db->fetchObjectList($sql);
+
+       foreach ($list as &$row) {
+           $dt = new DateTime($row->start_date);
+           $row->year = $dt->format('Y');
+           $row->start_short = $dt->format('d.m.');
+           $dt->add(new DateInterval('P'.$this->tour->get('duration').'D'));
+           $row->end_short = $dt->format('d.m.');
+
+           if ($row->sum_ok >= 4)
+               $row->maybe = true;
+           elseif ($row->sum_ok >= 3 && $row->sum_not == 0)
+               $row->maybe = true;
+           else
+               $row->maybe = false;
+       }
+
+       return Template::render('tour/datelist', ['tour_date_id' => $this->tour->get('tour_date_id'),
+                                                 'list' => $list]);
+    }
+
+    public function ajaxDates($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $response->setData(['table' => $this->formatDates()]);
+    }
+
+    public function datesAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $plan = new Tour_Status('plan', 'key');
+
+       $formstr = '';
+       if ($this->tour->isAdmin() && $this->tour->get('tour_status_id') == $plan->id()) {
+           $form = new Form('newdate');
+           $form->setTitle('Neuer Termin');
+           $form->add(new FormElement('date', ['name' => 'start_date',
+                                               'placeholder' => '2019-01-12',
+                                               'value' => '']));
+           $formstr = '<hr>'.$form->toString();
+       }
+
+       Application::get()->addJavascriptCode("load_dates();");
+
+       $response->setData(Template::render('tour/dates', ['admin' => $this->tour->isAdmin(),
+                                                          'form' => $formstr]));
+    }
+
+    public function planAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $plan = new Tour_Status('plan', 'key');
+       if ($this->tour->get('tour_status_id') != $plan->id())
+           return $response->setLocation(Application::url('tour', 'index', $this->tour));
+
+       $list = $this->tour->getAvailability($_SESSION['userid']);
+
+       $sql = sprintf("SELECT comment FROM tour_member WHERE tour_id = %d AND member_id = %d",
+                      $this->tour->id(), $_SESSION['userid']);
+       $comment = $this->db->fetchValue($sql);
+
+       $form = new Form('tourmember');
+       $form->setTitle('Allgemeiner Status');
+       $form->add(new FormElement('text', ['name' => 'comment',
+                                           'help' => 'z.B. kein fahrbereites Motorrad',
+                                           'value' => $comment]));
+       $formstr = '<hr>'.$form->toString();
+
+       $response->setData(Template::render('tour/plan', ['list' => $list,
+                                                         'form' => $formstr]));
+    }
+
+    public function ajaxTourmember($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $ok = $this->db->update('tour_member', ['comment' => strlen($data['comment']) ? $data['comment'] : NULL],
+                               sprintf('tour_id = %d AND member_id = %d', $this->tour->id(), $_SESSION['userid']));
+
+       if (strlen($data['comment']))
+           Tour_Log::add($this->tour->id(), sprintf("Neuer Status %s", $data['comment']));
+       else
+           Tour_Log::add($this->tour->id(), "Status gelöscht");
+    }
+
+    public function inviteAction($request, $response)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $user = new Sys_User();
+       $userlist = $user->getUserList($this->tour->id());
+
+       $response->setData(Template::render('tour/invite', ['list' => $userlist]));
+    }
+
+    public function ajaxTogglestatus($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       if (empty($data['user']))
+           $uid = $_SESSION['userid'];
+       elseif ($this->tour->isAdmin())
+           $uid = $data['user'];
+       else
+           $uid = $_SESSION['userid'];
+
+       $this->tour->toggleDateStatus($data['id'], $uid);
+
+       $sql =<<<EOS
+           SELECT
+             tour_date_status.key AS status_key,
+             tour_date_status.name AS status_text
+           FROM tour_date_member
+           LEFT JOIN tour_date_status ON tour_date_status_id = tour_date_status.id
+           LEFT JOIN tour_member ON tour_member_id = tour_member.id
+           WHERE tour_date_id = %d AND tour_member.member_id = %d
+EOS;
+       $sql = sprintf($sql, $data['id'], $uid);
+
+       $response->setData($this->db->fetchAssoc($sql));
+    }
+
+    public function ajaxInvite($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $this->tour->inviteMember($data['sys_user_id']);
+
+       $user = new Sys_User($data['sys_user_id']);
+       Tour_Log::add($this->tour->id(), sprintf("%s eingeladen", $user->get('name')));
+    }
+
+    public function ajaxNewdate($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $start_date = false;
+
+       if (preg_match('/^(\d+)-(\d+)-(\d+)$/', $data['start_date'], $match))
+           $start_date = $data['start_date'];
+       elseif (preg_match('/^(\d+).(\d+).(\d+)$/', $data['start_date'], $match))
+           $start_date = sprintf('%d-%d-%d', $match[3], $match[2], $match[1]);
+       else
+           throw new Exception('Kein Datum angegeben');
+
+       $ok = $this->db->insertInto('tour_date', ['tour_id' => $this->tour->id(),
+                                                 'start_date' => $start_date]);
+
+       if (!$ok)
+           $response->setError('Fehler beim Speichern');
+    }
+
+    protected function formatPovList()
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $sql = <<<EOS
+           SELECT
+             id,
+             destination
+           FROM tour_pov
+           WHERE tour_id = {$this->tour->id()}
+           ORDER BY seqnum, destination
+EOS;
+
+       $list = $this->db->fetchObjectList($sql);
+
+       foreach ($list as &$row) {
+           $dt = new DateTime($row->start_date);
+           $row->year = $dt->format('Y');
+           $row->start_short = $dt->format('d.m.');
+           $dt->add(new DateInterval('P'.$this->tour->get('duration').'D'));
+           $row->end_short = $dt->format('d.m.');
+       }
+
+       return Template::render('tour/povlist', ['is_planned' => $this->tour->isPlanned(),
+                                                'list' => $list]);
+    }
+
+    public function ajaxPov($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $response->setData(['table' => $this->formatPovList()]);
+    }
+
+    public function povAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $plan = new Tour_Status('plan', 'key');
+       if ($this->tour->get('tour_status_id') == $plan->id()) {
+           $form = new Form('newpov');
+           $form->setTitle('Neues Zwischenziel');
+           $form->add(new FormElement('text', ['name' => 'destination',
+                                               'value' => '']));
+           $formstr = '<hr>'.$form->toString();
+       }
+
+       Application::get()->addJavascriptCode("load_pov();");
+
+       $response->setData(Template::render('tour/pov', ['form' => $formstr]));
+    }
+
+    public function ajaxNewpov($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $plan = new Tour_Status('plan', 'key');
+       if ($this->tour->get('tour_status_id') != $plan->id())
+           throw new Exception("Nicht mehr in der Planungsphase");
+
+       $sql = sprintf("SELECT max(seqnum) FROM tour_pov WHERE tour_id = %d",
+                      $this->tour->id());
+       $max = intval($this->db->fetchValue($sql));
+
+       $ok = $this->db->insertInto('tour_pov', ['tour_id' => $this->tour->id(),
+                                                'seqnum' => $max + 1,
+                                                'destination' => $data['destination']]);
+
+       if (!$ok)
+           $response->setError('Fehler beim Speichern');
+
+       Tour_Log::add($this->tour->id(), sprintf("Zwischenziel %s hinzugefügt", $data['destination']));
+    }
+
+    public function ajaxPovmove($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $sql = sprintf("SELECT seqnum FROM tour_pov WHERE tour_id = %d AND id = %d",
+                      $this->tour->id(), $data['pov']);
+       $seqnum = $this->db->fetchvalue($sql);
+
+       if ($seqnum === false)
+           return $response->setError('Zwischenziel nicht gefunden');
+
+       if ($data['direction'] == 'up') {
+           $sql = sprintf("SELECT id,seqnum FROM tour_pov WHERE tour_id = %d AND seqnum < %d ORDER BY seqnum DESC LIMIT 1",
+                          $this->tour->id(), $seqnum);
+       } elseif ($data['direction'] == 'down') {
+           $sql = sprintf("SELECT id,seqnum FROM tour_pov WHERE tour_id = %d AND seqnum > %d ORDER BY seqnum ASC LIMIT 1",
+                          $this->tour->id(), $seqnum);
+       } else {
+           return $response->setError("Unknown direction");
+       }
+       $other = $this->db->fetchObject($sql);
+
+       $this->db->execute(sprintf("UPDATE tour_pov SET seqnum = %d WHERE id = %d", $seqnum, $other->id));
+       $this->db->execute(sprintf("UPDATE tour_pov SET seqnum = %d WHERE id = %d", $other->seqnum, $data['pov']));
+
+       return $ok;
+    }
+
+    public function ajaxPovdel($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $sql = sprintf("SELECT destination FROM tour_pov WHERE tour_id = %d AND id = %d",
+                      $this->tour->id(), $data['pov']);
+       $destination = $this->db->fetchvalue($sql);
+
+       if ($destination === false)
+           return $response->setError('Zwischenziel nicht gefunden');
+
+       $ok = $this->db->execute(sprintf("DELETE FROM tour_pov WHERE id = %d", $data['pov']));
+
+       if (!$ok)
+           throw new Exception("Zwischenziel konne nicht gelöscht werden");
+
+       Tour_Log::add($this->tour->id(), sprintf("Zwischenziel %s gelöscht", $destination));
+    }
+
+    public function matrixAction($request, $response)
+    {
+       if (empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $info = $this->tour->getAvailabilityMatrix();
+
+       $response->setData(Template::render('tour/matrix', ['is_planned' => $this->tour->isPlanned(),
+                                                           'user' => $info['user'],
+                                                           'list' => $info['list']]));
+    }
+
+    public function newmemberAction($request, $response)
+    {
+       if (!$this->app->isAdmin() && empty($this->tour))
+           return $response->setLocation($this->app->getBaseURL());
+
+       $form = new Form('newbiker');
+       $form->setTitle('Neuer Biker');
+       $form->add(new FormElement('text', ['name' => 'name',
+                                           'title' => 'Name',
+                                           'value' => '']));
+       $form->add(new FormElement('text', ['name' => 'email',
+                                           'title' => 'E-Mail',
+                                           'value' => '']));
+       $form->add(new FormElement('text', ['name' => 'mobile',
+                                           'title' => 'Mobiltelefon',
+                                           'help' => 'Nur sichtbar für Tour-Mitglieder',
+                                           'placeholder' => '0150-1234567',
+                                           'value' => '']));
+
+       $formstr = '<hr>'.$form->toString();
+
+       if (!empty($this->tour))
+           Application::get()->addJavascriptCode("load_dates();");
+
+       $response->setData($formstr);
+    }
+
+    public function ajaxNewbiker($request, $response, $data)
+    {
+       if (!strlen($data['name']))
+           throw new Exception("Name muß ausgfüllt werden");
+
+       if (!strlen($data['email']))
+           throw new Exception("E-Mail muß ausgfüllt werden");
+
+       $ok = $this->db->insertInto('sys_user', ['name' => $data['name'],
+                                                'email' => strtolower($data['email']),
+                                                'active' => true,
+                                                'passwd' => '',
+                                                'mobile' => strlen($data['mobile']) ? $data['mobile'] : NULL]);
+
+       if ($ok && !empty($this->tour)) {
+           $id = $this->db->lastInsertId();
+
+           $ok = $this->db->insertInto('tour_member', ['tour_id' => $this->tour->id(),
+                                                       'member_id' => $id]);
+           if (!$ok)
+               throw new Exception('Teilnehmer konnte nicht gesetzt werden');
+
+           $user = new Sys_User($id);
+           Tour_Log::add($this->tour->id(), sprintf("%s hinzugefügt", $user->get('name')));
+       }
+
+       return $ok;
+    }
+
+    public function newAction($request, $response)
+    {
+       if (!$this->app->isAdmin())
+           throw new Exception("Nur Administratoren können Touren anlegen");
+
+       $user = new Sys_User();
+       $userlist = $user->getUserList();
+
+       $options = [];
+       foreach ($userlist as $row)
+           $options[] = new Storage(['value' => $row->id, 'text' => $row->name]);
+
+       $form = new Form('newtour');
+       $form->setTitle('Neue Tour anlegen');
+       $form->add(new FormElement('text', ['name' => 'name',
+                                           'title' => 'Name',
+                                           'help' => 'z.B. Sauerland 2019',
+                                           'value' => '']));
+       $form->add(new FormElement('text', ['name' => 'urlkey',
+                                           'title' => 'URL-Key',
+                                           'help' => 'Keine Leer- oder Sonderzeichen, z.B. sl2019',
+                                           'value' => '']));
+       $form->add(new FormElement('number', ['name' => 'year',
+                                             'title' => 'Jahr',
+                                             'min' => date('Y'),
+                                             'max' => 2050,
+                                             'value' => '']));
+       $form->add(new FormElement('number', ['name' => 'duration',
+                                             'title' => 'Dauer',
+                                             'min' => 1,
+                                             'value' => '']));
+       $form->add(new FormElement('select', ['name' => 'leader',
+                                             'title' => 'Tourleiter',
+                                             'empty' => '-- Bitte wählen --',
+                                             'options' => $options]));
+
+       $response->setData(Template::render('page/formpage', ['form' => $form->toString()]));
+    }
+
+    public function ajaxNewtour($request, $response, $data)
+    {
+       if (!$this->app->isAdmin())
+           throw new Exception("Nur Administratoren können Touren anlegen");
+
+       foreach (['name', 'urlkey', 'year', 'duration', 'leader'] as $name)
+           if (!strlen($data[$name]))
+               throw new Exception('Alle Felder müssen ausgefüllt sein');
+
+       if (!preg_match('/^[a-zA-Z0-9_\.-]+$/', $data['urlkey']))
+           throw new Exception('Sonderzeichen nicht im URL-Key erlaubt');
+
+       $status_plan = new Tour_Status('plan', 'key');
+
+       $ok = $this->db->insertInto('tour', ['key' => $data['urlkey'],
+                                            'name' => $data['name'],
+                                            'year' => intval($data['year']),
+                                            'duration' => intval($data['duration']),
+                                            'tour_status_id' => $status_plan->id(),
+                                            ]);
+
+       if (!$ok)
+           throw new Exception('Neue Tour konnte nicht gespeichert werden');
+
+       $tour_id = $this->db->lastInsertId();
+
+       Tour_Log::add($tour_id, sprintf("Tour %s angelegt", $data['name']));
+
+       $ok = $this->db->insertInto('tour_member', ['tour_id' => $tour_id,
+                                                   'admin' => true,
+                                                   'member_id' => $data['leader']]);
+       if (!$ok)
+           throw new Exception('Tourleiter konnte nicht gesetzt werden');
+
+       $user = new Sys_User($data['leader']);
+       Tour_Log::add($tour_id, sprintf("%s als Leiter hinzugefügt", $user->get('name')));
+    }
+
+    public function notesAction($request, $response)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $list = $this->tour->getNotes(true);
+
+       $response->setData(Template::render('tour/notes', ['list' => $list]));
+    }
+
+    public function ajaxNotedel($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       $sql = sprintf("SELECT id FROM tour_note WHERE id = %d AND tour_id = %d AND sys_user_id = %d",
+                      $data['id'], $this->tour->id(), $_SESSION['userid']);
+
+       if (!$this->db->fetchValue($sql))
+           return $response->setError('Notiz nicht gefunden');
+
+       $this->db->update('tour_note', ['deleted' => true], 'id='.intval($data['id']));
+    }
+
+    public function logAction($request, $response)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       if (!$this->tour->isAdmin())
+           throw new Exception("Keine Berechtigung");
+
+       $sql = <<<EOS
+           SELECT
+             tour_log.id,
+             extract(day from logdate) || '.' || extract(month from logdate) || '.' || extract(year from logdate) AS date,
+             logdate,
+             logtext,
+             name
+           FROM tour_log
+           JOIN sys_user ON tour_log.sys_user_id = sys_user.id
+           WHERE tour_id = {$this->tour->id()}
+           ORDER BY logdate DESC, tour_log.sys_edit DESC
+EOS;
+       $list = $this->db->fetchObjectList($sql);
+
+       $response->setData(Template::render('tour/log', ['list' => $list]));
+    }
+
+    public function notenewAction($request, $response)
+    {
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $form = new Form('newnote');
+       $form->setTitle('Neue Notiz');
+       $form->setAction(Application::url('tour', 'preview', $this->tour));
+       $form->setSecondButton('Preview');
+       $form->setDescription(Template::render('wikisyntax', []));
+       $form->add(new FormElement('textarea', ['name' => 'note',
+                                               'title' => 'Text',
+                                               'help' => 'MoinMoin-typische Syntax erlaubt',
+                                               'rows' => 5,
+                                               'value' => '']));
+
+       $response->setData(Template::render('page/formpage', ['form' => $form->toString()]));
+    }
+
+    public function previewAction($request, $response)
+    {
+       if (!$request->isPost())
+           throw new Exception("Only POST requests allowed here");
+
+       if (empty($this->tour))
+           throw new Exception("Keine Tour angegeben");
+
+       $data = $request->getPost();
+
+       $form = new Form('newnote');
+       $form->setTitle('Neue Notiz');
+       $form->setAction(Application::url('tour', 'preview', $this->tour));
+       $form->setSecondButton('Preview');
+       $form->setDescription(Template::render('wikisyntax', []));
+       $form->add(new FormElement('textarea', ['name' => 'note',
+                                               'title' => 'Text',
+                                               'help' => 'MoinMoin-typische Syntax erlaubt',
+                                               'rows' => 5,
+                                               'value' => $data['note']]));
+
+       $preview = Template::render('tour/notepreview', ['text' => Wiki::renderHTML($data['note'])]);
+       $response->setData(Template::render('page/formpage', ['form' => $form->toString()
+                                                             .$preview]));
+    }
+
+    public function ajaxNotenew($request, $response, $data)
+    {
+       if (empty($this->tour))
+           throw new Exception('Keine Tour ausgewählt');
+
+       if (!strlen($data['note']))
+           throw new Exception('Kein Text angegeben');
+
+       $data = ['tour_id' => $this->tour->id(),
+                'note' => trim($data['note'])];
+
+       $this->db->insertInto('tour_note', $data);
+    }
+
+}
index c03b23a..5611726 100644 (file)
@@ -21,4 +21,20 @@ class Sys_User extends DatabaseTable
                              'pwkey' => NULL,
                              'pwkey_valid' => NULL]);
     }
+
+    public function getUserlist($tour_id=false)
+    {
+       $tour_id = intval($tour_id);
+       $sql =<<<EOS
+           SELECT DISTINCT
+             sys_user.id,sys_user.name,sys_user.email,tour_id IS NOT NULL AS active
+           FROM sys_user
+           LEFT JOIN tour_member ON sys_user.id = member_id
+                                AND ({$tour_id} = 0 OR tour_id = {$tour_id})
+           WHERE sys_user.id > 1 AND active = true
+           ORDER by sys_user.name
+EOS;
+
+       return $this->db->fetchObjectList($sql);
+    }
 }
diff --git a/templates/page/list.phtml b/templates/page/list.phtml
new file mode 100644 (file)
index 0000000..5a647fc
--- /dev/null
@@ -0,0 +1,13 @@
+<div class="tourlist container">
+<?php foreach ($list as $row) { ?>
+     <div class="row">
+     <div class="col-sm"></div>
+     <div class="col-sm">
+         <a href="<?php echo $row->url; ?>">
+         <button type="button" class="btn <?php echo $row->plan ? 'btn-primary' : ($row->cancel ? 'btn-danger' : 'btn-success'); ?>"><?php echo $row->name; ?></button>
+         </a>
+     </div>
+     <div class="col-sm"></div>
+     </div>
+<?php } ?>
+</div>
\ No newline at end of file
diff --git a/templates/tour/admin.phtml b/templates/tour/admin.phtml
new file mode 100644 (file)
index 0000000..b37bc9f
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="tour container" style="padding-bottom: 2ex;">
+
+<h3>Tourplanung</h3>
+
+<?php echo $form; ?>
+
+<br>
+<h3>Tourleitung</h3>
+
+<?php echo $adminform; ?>
+
+<div id="touradmin">
+<?php foreach ($adminlist as $item) { ?>
+<div class="form-row" style="margin-top: 2ex;">
+  <div class="col-md-4 mb-3">
+    <label for="admin_<?php echo $item->id; ?>" class="onlynarrow">Tourleiter</label>
+    <input type="text" class="form-control" id="admin_<?php echo $item->id; ?>" value="<?php echo $item->name; ?>" disabled="disabled">
+  </div>
+  <div class="col-md-8 mb-3">
+    <button id="touradmin_del" type="button" class="btn btn-danger" data-id="<?php echo $item->id; ?>">Löschen</button>
+  </div>
+</div>
+<?php } ?>
+</div>
+
+</div>
diff --git a/templates/tour/datelist.phtml b/templates/tour/datelist.phtml
new file mode 100644 (file)
index 0000000..9d1f861
--- /dev/null
@@ -0,0 +1,20 @@
+<table class="table table-striped table-sm">
+  <thead>
+    <tr>
+      <th scope="col">Datum</th>
+      <th scope="col">#ok</th>
+      <th scope="col">#evtl.</th>
+      <th scope="col">#nein</th>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($list as $row) { ?>
+    <tr<?php echo $tour_date_id == $row->id ? ' class="bg-ok"' : ($row->maybe ? ' class="bg-maybe"' : '') ?>>
+        <td><?php echo $row->start_short; ?> &ndash; <?php echo $row->end_short; ?><?php echo $row->year; ?></td>
+      <td><?php echo $row->sum_ok; ?></td>
+      <td><?php echo $row->sum_maybe; ?></td>
+      <td><?php echo $row->sum_not; ?></td>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
diff --git a/templates/tour/dates.phtml b/templates/tour/dates.phtml
new file mode 100644 (file)
index 0000000..011d048
--- /dev/null
@@ -0,0 +1,9 @@
+<div class="tour container">
+
+<h3>Termine</h3>
+
+<div id="datelist"></div>
+
+<?php if ($admin) echo $form . $tourplan; ?>
+
+</div>
diff --git a/templates/tour/index.phtml b/templates/tour/index.phtml
new file mode 100644 (file)
index 0000000..db4ee00
--- /dev/null
@@ -0,0 +1,73 @@
+<div class="tour container index">
+
+<div class="row">
+<div class="col-sm">
+     <div class="alert <?php echo $status_key == 'plan' ? 'alert-primary' : ($status_key == 'run' ? 'alert-success' : 'alert-danger') ?>" role="alert">
+     <h4><?php echo $status . ($status_key == 'run' ? ' am ' . $start_datum . ' für ' . $duration . ' Tage' : ''); ?></h4>
+     </div>
+</div>
+</div>
+
+<?php if (count($notes)) { ?>
+    <div class="row">
+    <div class="col-sm">
+        <div class="alert alert-secondary">
+           <?php foreach ($notes as $row) { ?>
+                <small><strong><?php echo $row->name; ?></strong></small>
+                <div class="alert alert-primary">
+                     <?php echo $row->note_html; ?>
+                     <div class="right"><small><?php echo $row->datum; ?></small></div>
+                </div>
+           <?php } ?>
+        </div>
+    </div>
+    </div>
+<?php } ?>
+
+<?php if ($status_key == 'plan') { ?>
+<h3>Termine</h3>
+<div id="datelist" style="margin-bottom:1ex;"></div>
+<?php } ?>
+
+ <?php if (!empty($information)) { ?>
+
+<div class="row">
+    <div class="col-sm">
+    <div class="alert alert-info">
+     <?php echo $information_html; ?>
+    </div>
+    </div>
+</div>
+
+<?php } ?>
+
+<h3>Eingeladen sind</h3>
+
+<table class="table table-striped">
+  <thead>
+    <tr>
+      <th scope="col">Name</th>
+      <th scope="col">Mobil</th>
+      <th scope="col">Bemerkung</th>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($members as $row) { ?>
+    <tr>
+      <td><?php echo $row->name; ?><span class="onlywide"> &lt;<?php echo $row->email?>&gt;</span></td>
+      <td><?php echo $row->mobile; ?></td>
+      <td>
+          <?php echo $admin && $row->single_room ? '<b>EZ</b> ' : ''; ?>
+          <?php echo $row->comment; ?>
+      </td>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
+
+<?php if ($status_key != 'plan') { ?>
+<h3>Termine</h3>
+<div id="datelist" style="margin-bottom:1ex;"></div>
+<?php } ?>
+
+</div>
\ No newline at end of file
diff --git a/templates/tour/invite.phtml b/templates/tour/invite.phtml
new file mode 100644 (file)
index 0000000..3e7b95d
--- /dev/null
@@ -0,0 +1,28 @@
+<div class="invite container">
+<?php foreach ($list as $row) { ?>
+     <div class="row">
+     <div class="col-sm"></div>
+     <div class="col-sm">
+         <button type="button"
+                 class="btn <?php echo $row->active ? 'btn-success' : 'btn-primary'; ?>"
+                 data-id="<?php echo $row->id; ?>"
+                 <?php echo $row->active ? 'disabled' : ''; ?>><?php echo $row->name; ?></button>
+     </div>
+     <div class="col-sm"></div>
+     </div>
+<?php } ?>
+     <div class="row">
+     <div class="col-sm"></div>
+     <div class="col-sm">
+         <button type="button" class="btn btn-warning">Neuer Biker</button>
+     </div>
+     <div class="col-sm"></div>
+     </div>
+
+<div id="legende">
+<h4>Legende</h4>
+<button type="button" class="btn btn-success" style="margin-bottom: 1ex;" disabled>ist eingeladen</button><br>
+<button type="button" class="btn btn-primary">nicht eingeladen</button><br>
+</div>
+
+</div>
diff --git a/templates/tour/invitemail.phtml b/templates/tour/invitemail.phtml
new file mode 100644 (file)
index 0000000..37a6b8e
--- /dev/null
@@ -0,0 +1,17 @@
+Moin <?php echo $name; ?>!
+
+<?php echo $inviter; ?> lädt Dich ein, an der Motorrad-Tour <?php echo $tour_name; ?> teilzunehmen.
+Die Tour ist für <?php echo $tour_year; ?> geplant und soll <?php echo $tour_duration; ?> Tag(e) dauern.
+
+Die Tour wird online unter folgender Adresse geplant:
+
+    <<?php echo $url; ?>>
+
+Bitte melde Dich dort mit Deiner Mail Adresse "<?php echo $email; ?>" an.
+Über die Funktion "Passwort vergessen" setzt Du ein neues Passwort.
+
+Bitte hinterlege dort, zu welchen Zeiten Du an der Tour teilnehmen
+kannst, zu welchen Zeiten Du nur evtl. Zeit hast und zu welchen Zeiten
+Du ganz sicher nicht teilnehmen kannst.  Diese Einstellungen kannst Du
+jederzeit bearbeiten.
+
diff --git a/templates/tour/log.phtml b/templates/tour/log.phtml
new file mode 100644 (file)
index 0000000..486a9a4
--- /dev/null
@@ -0,0 +1,22 @@
+<div class="tour container">
+
+<h3>Logbuch</h3>
+
+<table class="table table-sm" id="logbook">
+  <thead>
+    <tr>
+      <th scope="col">Datum</th>
+      <th scope="col">Text</th>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($list as $row) { ?>
+    <tr data-id="<?php echo $row->id; ?>">
+      <td><?php echo $row->date; ?><br><?php echo $row->name; ?></td>
+      <td><?php echo $row->logtext; ?></td>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
+
+</div>
\ No newline at end of file
diff --git a/templates/tour/matrix.phtml b/templates/tour/matrix.phtml
new file mode 100644 (file)
index 0000000..aeae3b6
--- /dev/null
@@ -0,0 +1,53 @@
+<div class="tour container">
+
+<h3>Terminmatrix</h3>
+
+<table class="table table-sm" id="matrix">
+  <thead>
+    <tr>
+      <th scope="col">Datum</th>
+<?php foreach ($user as $row) { ?>
+      <th scope="col" title="<?php echo $row->name; ?>"><?php echo $row->nick; ?></th>
+<?php } ?>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($list as $row) { ?>
+    <tr>
+      <td><?php echo $row->start_short; ?> &ndash; <?php echo $row->end_short; ?></td>
+      <?php
+          foreach ($user as $uid => $item) {
+             if (array_key_exists($uid, $row->avail)) {
+      ?>
+                  <td class="bg-<?php echo $row->avail[$uid]->status_key; ?>"
+                     id="date-<?php echo $is_planned ? 'toggle' : 'status' ?>"
+                     data-user-id="<?php echo $uid; ?>"
+                     data-date-id="<?php echo $row->id; ?>">&nbsp;</td>
+      <?php
+              } else {
+      ?>
+                  <td class="bg-unknown"
+                     id="date-<?php echo $is_planned ? 'toggle' : 'status' ?>"
+                     data-user-id="<?php echo $uid; ?>"
+                     data-date-id="<?php echo $row->id; ?>">&nbsp;</td>
+        <?php } ?>
+    <?php } ?>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
+
+<?php if (count($user)) { ?>
+<div id="legende">
+<h4>Legende</h4>
+<?php foreach ($user as $row) { ?>
+<?php echo $row->nick; ?> ist <?php echo $row->name; ?><br>
+<?php } ?>
+</div>
+<?php } ?>
+
+<?php if (!$is_planned) { ?>
+<p>Die Terminfindung ist abgeschlossen.</p>
+<?php } ?>
+
+</div>
\ No newline at end of file
diff --git a/templates/tour/notepreview.phtml b/templates/tour/notepreview.phtml
new file mode 100644 (file)
index 0000000..f890186
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="container">
+<div class="form_description">
+    <?php echo $text; ?>
+</div>
+</div>
diff --git a/templates/tour/notes.phtml b/templates/tour/notes.phtml
new file mode 100644 (file)
index 0000000..b65a23b
--- /dev/null
@@ -0,0 +1,35 @@
+<div class="tour container notes">
+
+<div class="row">
+<h3 style="margin-left:10px;">Alle Notizen</h3>
+<button id="btn_new" type="button" class="btn btn-primary btn-sm" style="position:absolute;right:10px;padding-top:0.125rem;padding-bottom:0.125rem;">Neu</button>
+</div>
+
+<div id="note_del" class="alert alert-warning" role="alert" style="display:none">
+<div style="margin-bottom: 2ex;"><b>Notiz wirklich löschen?</b></div>
+<div style="text-align: right;">
+<button id="btn_del" type="button" class="btn btn-primary" data-id="">Löschen</button>
+<button id="btn_close" type="button" class="btn btn-primary">Schließen</button>
+</div>
+</div>
+
+
+<?php if (count($list)) { ?>
+    <div class="row">
+    <div class="col-sm">
+        <div class="alert alert-dark">
+           <?php foreach ($list as $row) { ?>
+               <div class="note" data-id="<?php echo $row->id; ?>">
+                <small><strong><?php echo $row->name; ?></strong></small>
+                <div class="alert <?php echo $row->own ? 'alert-primary' : 'alert-secondary'; ?>" data-id="<?php echo $row->id; ?>">
+                     <?php echo $row->note_html; ?>
+                     <div class="right"><small><?php echo $row->datum; ?></small></div>
+                </div>
+               </div>
+           <?php } ?>
+        </div>
+    </div>
+    </div>
+<?php } ?>
+
+</div>
\ No newline at end of file
diff --git a/templates/tour/plan.phtml b/templates/tour/plan.phtml
new file mode 100644 (file)
index 0000000..16e8bc4
--- /dev/null
@@ -0,0 +1,28 @@
+<div class="tour container">
+
+<h3>Terminplanung</h3>
+
+<table class="table" id="plan">
+  <thead>
+    <tr>
+      <th scope="col">Datum</th>
+      <th scope="col">Mein Status</th>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($list as $row) { ?>
+    <tr>
+      <td><?php echo $row->start_short; ?> &ndash; <?php echo $row->end_short; ?><?php echo $row->year; ?></td>
+      <td class="bg-<?php echo $row->status_key; ?>" id="date-status" data-id="<?php echo $row->id; ?>"><?php echo $row->status_text; ?></td>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
+
+<div id="legende">
+<small class="form-text text-muted">Der Status wird durch Druck auf die Spalte geändert</small>
+</div>
+
+<?php echo $form; ?>
+
+</div>
\ No newline at end of file
diff --git a/templates/tour/pov.phtml b/templates/tour/pov.phtml
new file mode 100644 (file)
index 0000000..6e474c9
--- /dev/null
@@ -0,0 +1,9 @@
+<div class="tour container">
+
+<h3><span class="onlywide">Gewünschte </span>Zwischenziele</h3>
+
+<div id="povlist"></div>
+
+<?php echo $form; ?>
+
+</div>
diff --git a/templates/tour/povlist.phtml b/templates/tour/povlist.phtml
new file mode 100644 (file)
index 0000000..d8fd071
--- /dev/null
@@ -0,0 +1,20 @@
+<table class="table table-striped table-sm">
+  <thead>
+    <tr>
+      <th scope="col">Zwischenziel</th>
+      <th scope="col">Aktionen</th>
+    </tr>
+  </thead>
+  <tbody>
+<?php foreach ($list as $row) { ?>
+    <tr data-id="<?php echo $row->id; ?>">
+      <td><?php echo $row->destination; ?></td>
+      <td>
+        <button class="btn btn-sm btn-warning" <?php echo $is_planned ? '' : 'disabled="disabled"'; ?>data-dir="up" title="Hoch"><small>&nwarr;</small></button>
+       <button class="btn btn-sm btn-warning" <?php echo $is_planned ? '' : 'disabled="disabled"'; ?>data-dir="down" title="Runter"><small>&swarr;</small></button>
+       <button class="btn btn-sm btn-danger"  <?php echo $is_planned ? '' : 'disabled="disabled"'; ?>title="Löschen">&times;</button>
+      </td>
+    </tr>
+<?php } ?>
+  </tbody>
+</table>
index 5c5fe33..19c387f 100644 (file)
@@ -2,6 +2,9 @@
     margin-bottom: 1ex;
     background-color: #007bff;
 }
+.navbar-brand span {
+    overflow: hidden;
+}
 div.header {
     background-color: #007bff;
     color: white;
@@ -50,7 +53,7 @@ div.loginform, div.lostpwform {
 }
 @media (max-width: 360px) {
     .onlynarrow {
-       display: block;
+       display: inline;
     }
     .onlywide {
        display: none;
@@ -61,7 +64,7 @@ div.loginform, div.lostpwform {
        display: none;
     }
     .onlywide {
-       display: block;
+       display: inline;
     }
 }
 div.loginform h3, div.lostpwform h3 {
@@ -76,3 +79,50 @@ div.lostpw {
 div.error h3 {
     border-bottom: 2px solid #ccc;
 }
+div.tourlist .row button.btn,
+div.invite .row button.btn {
+    width: 100%;
+    margin-bottom: 1ex;
+}
+.tour table#plan td:nth-child(2) {
+    text-align: center;
+    cursor: pointer;
+}
+.bg-unknown {
+    background-color: #FFFFFF;
+}
+.bg-ok {
+    background-color: #00FF00 !important;
+}
+.bg-maybe {
+    background-color: #FFFF00;
+}
+.bg-nope {
+    background-color: #FF0000;
+    color: white;
+}
+#datelist .table tbody tr.bg-maybe:nth-of-type(2n+1) {
+    background-color: #EEEE00 !important;
+}
+form[name="newdate"] .form-group {
+    float: left
+}
+form[name="newdate"] input {
+    width: 12em;
+    margin-right: 0.5em;
+}
+#povlist table button.btn {
+    font-weight: bold;
+}
+div.tour table#matrix td#date-status {
+    border-left: 1px solid white;
+}
+div.tour div.legende {
+    border-top: 2px solid #ccc;
+}
+div#note_del {
+    z-index: 50;
+}
+div#note_del.alert-warning {
+    border: 1px solid orange;
+}
index 0229de1..29d7fac 100644 (file)
--- a/touren.js
+++ b/touren.js
@@ -55,6 +55,39 @@ function ajax_request(route, parms, callback)
           });
 }
 
+function load_dates()
+{
+    ajax_request('tour/'+tour_key+'/dates', '', function(data){
+       $('#datelist').html(data.table);
+    });
+}
+
+function load_pov()
+{
+    ajax_request('tour/'+tour_key+'/pov', '', function(data){
+       $('#povlist').html(data.table);
+
+       $('#povlist table button.btn-warning').click(function(e){
+           ajax_request('tour/'+tour_key+'/povmove',
+                        {'pov': $(this).parents('tr:first').attr('data-id'),
+                         'direction': $(this).attr('data-dir')},
+                        function(data){
+                            load_pov();
+                        });
+       });
+       $('#povlist table button.btn-warning:first').attr('disabled', 'disabled');
+       $('#povlist table button.btn-warning:last').attr('disabled', 'disabled');
+
+       $('#povlist table button.btn-danger').click (function(e){
+           ajax_request('tour/'+tour_key+'/povdel',
+                        {'pov': $(this).parents('tr:first').attr('data-id')},
+                        function(data){
+                            load_pov();
+                        });
+       });
+    });
+}
+
 $(function(){
     $('input').on('keydown', function(e){
        if (e.which === 13)
@@ -101,4 +134,167 @@ $(function(){
            setTimeout(function(){window.location.href = tour_base_url + 'index/index';}, 4000);
        });
     });
+
+    // Create new tour
+    $('#newtour_save').click(function(e){
+       if (!$('form[name="newtour"] #name').val().length ||
+           !$('form[name="newtour"] #urlkey').val().length ||
+           !$('form[name="newtour"] #year').val().length ||
+           !$('form[name="newtour"] #duration').val().length ||
+           !$('form[name="newtour"] #leader').val().length)
+           return show_message('Alle Felder müssen ausgefüllt sein', 'error');
+
+       if ($('form[name="newtour"] #urlkey').val().indexOf(' ') != -1)
+           return show_message('Leerzeichen nicht erlaubt im URL-Key', 'error');
+
+       ajax_request('tour/newtour', $('form[name="newtour"]').serialize(), function(data){
+           show_message('Tour gespeichert', 'info');
+           setTimeout(function(){window.location.href = tour_base_url + 'index/index';}, 4000);
+       });
+    });
+
+    // Toggle status for date
+    $('div.tour table#plan td#date-status').click(function(e){
+       var cell = $(this);
+       ajax_request('tour/'+tour_key+'/togglestatus',
+                    'id='+cell.attr('data-id'),
+                    function(data){
+                        cell.attr('class', 'bg-'+data.status_key);
+                        cell.text(data.status_text);
+                    });
+    });
+
+    // Toggle status for date in matrix
+    $('div.tour table#matrix td#date-toggle').click(function(e){
+       var cell = $(this);
+       ajax_request('tour/'+tour_key+'/togglestatus',
+                    {id: cell.attr('data-date-id'),
+                     user: cell.attr('data-user-id')},
+                    function(data){
+                        cell.attr('class', 'bg-'+data.status_key);
+                    });
+    });
+
+    // Set tour member status
+    $('#tourmember_save').click(function(e){
+       ajax_request('tour/'+tour_key+'/tourmember', $('form[name="tourmember"]').serialize(), function(data){
+           show_message('Status gespeichert', 'info');
+       });
+    });
+
+    // Add new date to tour
+    $('#newdate_save').click(function(e){
+       if (!$('#start_date').val().length) return;
+       ajax_request('tour/'+tour_key+'/newdate', $('form[name="newdate"]').serialize(), function(data){
+           load_dates();
+           show_message('Termin gespeichert', 'info');
+       });
+    });
+
+    // Add new POV
+    $('#newpov_save').click(function(e){
+       if (!$('form[name="newpov"] #destination').val().length) return;
+       ajax_request('tour/'+tour_key+'/newpov', $('form[name="newpov"]').serialize(), function(data){
+           load_pov();
+           show_message('Zwischenziel gespeichert', 'info');
+           $('form[name="newpov"] #destination').val('');
+       });
+    });
+
+    // Add new member to tour
+    $('div.invite button.btn-primary').click(function(e){
+       var button = $(this);
+       if (!button.attr('data-id').length) return;
+       ajax_request('tour/'+tour_key+'/invite',
+                    'sys_user_id='+button.attr('data-id'),
+                    function(data){
+                        button.removeClass('btn-primary').addClass('btn-success').attr('disabled', 'disabled');
+                    });
+    });
+    // Create new biker and add new member to tour
+    $('div.invite button.btn-warning').click(function(e){
+       window.location.href = tour_base_url + 'tour/' + tour_key + '/newmember';
+    });
+    $('#newbiker_save').click(function(e){
+       if (!$('form[name="newbiker"] #name').val().length ||
+           !$('form[name="newbiker"] #email').val().length)
+           return show_message('Name und E-Mail müssen ausgefüllt sein', 'error');
+
+       if (typeof tour_key == 'string')
+           var backend = 'tour/'+tour_key+'/newbiker';
+       else
+           var backend = 'tour/newbiker';
+       ajax_request(backend, $('form[name="newbiker"]').serialize(), function(data){
+           show_message('Biker gespeichert', 'info');
+           if (typeof tour_key == 'string')
+               setTimeout(function(){window.location.href = tour_base_url + 'tour/' + tour_key + '/invite';}, 4000);
+       });
+    });
+
+    // Notizen
+    $('.container.notes #btn_new').click(function(e){
+       window.location.href = tour_base_url + 'tour/'+tour_key+'/notenew';
+    });
+    $('.container.notes .alert-dark .alert-primary').click(function(e){
+       $('div#note_del').center().show();
+       $('div#note_del button').attr('data-id', $(this).attr('data-id'));
+    });
+    $('div#note_del button#btn_del').click(function(e){
+       var note_id = $(this).attr('data-id');
+
+       ajax_request('tour/'+tour_key+'/notedel', 'id='+note_id, function(data){
+           $('div#note_del').hide();
+           $('div.note[data-id="'+note_id+'"]').hide();
+           show_message('Notiz gelöscht', 'info');
+       });
+    });
+    $('div#note_del button#btn_close').click(function(e){
+       $('div#note_del').hide();
+    });
+    $('#newnote_save').click(function(e){
+       var dest_url = tour_base_url + 'tour/'+tour_key+'/notes';
+       if (!$('form[name="newnote"] #note').val().length) {
+           window.location.href = dest_url;
+           return;
+       }
+
+       ajax_request('tour/'+tour_key+'/notenew', $('form[name="newnote"]').serialize(), function(data){
+           show_message('Notiz gespeichert', 'info');
+               setTimeout(function(){window.location.href = dest_url;}, 2000);
+       });
+    });
+    $('#newnote_second').click(function(e){
+       if (!$('form[name="newnote"] #note').val().length)
+           return;
+
+       $('form[name="newnote"]').submit();
+    });
+
+    // Administration
+    $('#tourplan_save').click(function(e){
+       if (!$('form[name="tourplan"] #name').val().length ||
+           !$('form[name="tourplan"] #urlkey').val().length ||
+           !$('form[name="tourplan"] #year').val().length ||
+           !$('form[name="tourplan"] #duration').val().length)
+           return show_message('Name, Key, Jahr und Dauer müssen ausgefüllt sein', 'error');
+
+       ajax_request('tour/'+tour_key+'/admin', $('form[name="tourplan"]').serialize(), function(data){
+           show_message('Informationen gespeichert', 'info');
+       });
+    });
+    $('#touradmin_save').click(function(e){
+       if (!$('form[name="touradmin"] #tour_member_id').val().length) return;
+
+       ajax_request('tour/'+tour_key+'/touradmin', $('form[name="touradmin"]').serialize(), function(data){
+           show_message('Tourleiter gesetzt', 'info');
+           $('form[name="touradmin"] #tour_member_id option[value="'+$('form[name="touradmin"] #tour_member_id').val()+'"]').remove();
+       });
+    });
+    $('#touradmin .btn-danger').click(function(e){
+       var button = $(this);
+       ajax_request('tour/'+tour_key+'/deladmin', 'id='+button.attr('data-id'), function(data){
+           show_message('Tourleiter gelöscht', 'info');
+           button.parents('div.form-row:first').hide();
+       });
+    });
 });