Browse Source

display menu

isundil 6 years ago
parent
commit
744da3962f

+ 26 - 0
Makefile

@@ -0,0 +1,26 @@
+
+SRC=	src/js/resources.js	\
+		src/js/periods.js
+
+OUTPUT=	public/js/script.min.js
+
+MAPFILE=	${OUTPUT}.map
+
+CLOSURE= src/js/closure-compiler-v20190301.jar
+
+$(OUTPUT):all
+
+all:
+	java -jar ${CLOSURE} --compilation_level ADVANCED --language_in=ECMASCRIPT6 --language_out=ECMASCRIPT6_STRICT --warning_level=VERBOSE --js_output_file=${OUTPUT} ${SRC}
+
+debug:
+	echo "//# sourceMappingURL=${MAPFILE}" > ${OUTPUT}
+	java -jar ${CLOSURE} --compilation_level WHITESPACE_ONLY --language_in=ECMASCRIPT6 --language_out=ECMASCRIPT6_STRICT --warning_level=VERBOSE --create_source_map=${MAPFILE} ${SRC} >> ${OUTPUT}
+
+clean:
+	$(RM) ${OUTPUT} ${MAPFILE}
+
+re:	clean $(OUTPUT)
+
+.PHONY:	all debug clean re
+

+ 1 - 0
public/css/style.css

@@ -1 +1,2 @@
 .icon { display: inline-block; height: 1em; }
+ul { list-style-type: none; }

+ 6 - 0
public/js/script.min.js

@@ -0,0 +1,6 @@
+'use strict';function g(a){var b=document.createElement("ul");a.sort((c,b)=>c.toLowerCase().localeCompare(b.toLowerCase())).forEach(c=>{var a=document.createElement("li");a.className="badge badge-info";a.style.marginLeft="5px";a.textContent=c;b.appendChild(a)});b.style.padding=0;return b}
+function h(a){var b=document.createDocumentFragment(),c=document.createElement("a");c.href="event/edit?id="+a;c.innerHTML='<img alt="Edit" class="icon" src="public/img/note-outlined-symbol_icon-icons.com_73198.svg">';b.appendChild(c);c=document.createElement("a");c.href="event/add?from="+a;c.innerHTML='<img alt="Duplicate" class="icon" src="public/img/copy-two-paper-sheets-interface-symbol_icon-icons.com_73283.svg">';b.appendChild(c);c=document.createElement("a");c.href="event/delete?id="+a;c.innerHTML=
+'<img alt="Delete" class="icon" src="public/img/recycling-bin_icon-icons.com_73179.svg">';b.appendChild(c);return b}function k(a){for(var b=document.createElement("tr"),c=0,e=arguments.length;c<e;++c){var d=arguments[c],f=document.createElement("td");d instanceof DocumentFragment||d instanceof HTMLElement?f.appendChild(d):f.textContent=d;b.appendChild(f)}return b}
+function l(a,b){a=document.getElementById(a);var c=document.createDocumentFragment(),e=[];a.textContent="";for(var d in b)e.push([d,b[d]]);e.sort((a,c)=>c[1]-a[1]);e.forEach(a=>{c.appendChild(k(a[0],Math.round(100*a[1])/100))});a.appendChild(c)}
+function m(a){var b=document.getElementById("events"),c=document.createDocumentFragment(),e={},d={},f=0;b.textContent="";a.forEach(a=>{c.appendChild(k(a.date,a.label,a.beneficiary,g(a.context),a.amount,h(a.id)));var b=parseFloat(a.amount);f+=b;e[a.beneficiary]=(e[a.beneficiary]||0)+b;a.context.forEach(a=>d[a]=(d[a]||0)+b)});a=k("Total",Math.round(100*f)/100,"");a.children[0].colSpan=4;c.appendChild(a);b.appendChild(c);l("beneficiaries",e);l("contexts",d)}
+function n(a){var b=(new Date(a)).getTime();a=window.EVENTS.filter(a=>(new Date(a.date)).getTime()>=b);m(a)}var p={};document.addEventListener("DOMContentLoaded",()=>{window.PERIODS.length&&n(window.PERIODS[0].start)});window.setPeriodStart=n;window.setPeriodId=function(a){if(p[a])m(p[a]);else{var b=new XMLHttpRequest;b.open("GET","event/list?period="+a,!0);b.onreadystatechange=function(){if(b.readyState===XMLHttpRequest.DONE&&200===b.status){var c=p[a]=JSON.parse(b.responseText);m(c)}};b.send(null)}};

+ 15 - 0
src/entities/events.php

@@ -31,6 +31,21 @@ class Event
     public function getLabel() { return $this->label; }
     public function getAmount() { return $this->amount; }
 
+    public function toArray()
+    {
+        $result = array(
+            "id"            => $this->getId(),
+            "date"          => $this->paymentDate->format("Y-m-d"),
+            "context"       => array(),
+            "beneficiary"   => $this->beneficiary->getLabel(),
+            "label"         => $this->label,
+            "amount"        => $this->amount
+        );
+        foreach ($this->context as $i)
+            array_push($result["context"], $i->getLabel());
+        return $result;
+    }
+
     public function save()
     {
         global $account;

+ 36 - 3
src/entities/periods.php

@@ -3,16 +3,25 @@
 class Period
 {
     private $id;
+    protected $label;
     protected $start;
     protected $end;
 
     protected function Period($data)
     {
         $this->id = $data["id"];
+        $this->label = $data["label"];
         $this->start = $data["start"];
         $this->end = $data["end"];
     }
 
+    public function getId() { return md5(SALT .$this->id); }
+
+    public function getLabel()
+    {
+        return $this->label;
+    }
+
     public function getStart()
     {
         return $this->start;
@@ -28,12 +37,36 @@ class Period
         return $this->end != null;
     }
 
-    public static function LoadPendingPeriod()
+    public static function LoadPendingPeriods()
     {
         global $account;
 
-        $data = rawQuery("select * from periods where end is null and userid=?", array($account->getId()));
-        return count($data) ? new Period($data[0]) : null;
+        $data = rawQuery("select * from periods where end is null and userid=? order by start desc", array($account->getId()));
+        $result = [];
+        foreach ($data as $i)
+            array_push($result, new Period($i));
+        return $result;
+    }
+
+    public static function LoadPastPeriods()
+    {
+        global $account;
+
+        $data = rawQuery("select * from periods where end is not null and userid=? order by end DESC, start DESC", array($account->getId()));
+        $result = [];
+        foreach ($data as $i)
+            array_push($result, new Period($i));
+        return $result;
+    }
+
+    public static function FromHashed($hash) {
+        global $account;
+
+        $data = rawQuery("select * from periods where userid=? and md5(concat(?, id)) =? LIMIT 1", array($account->getId(), SALT, $hash));
+        $result = array();
+        foreach ($data as $i)
+            return new Period($i);
+        return null;
     }
 }
 

BIN
src/js/closure-compiler-v20190301.jar


+ 130 - 0
src/js/periods.js

@@ -0,0 +1,130 @@
+
+function createContextLabelList(labels) {
+    var result = document.createElement("ul");
+    labels.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).forEach(i => {
+        var li = document.createElement("li");
+        li.className = "badge badge-info";
+        li.style.marginLeft = "5px";
+        li.textContent = i;
+        result.appendChild(li);
+    });
+    result.style.padding = 0;
+    return result;
+}
+
+function createEventActionButtons(eventId) {
+    var result = document.createDocumentFragment();
+    var editLink = document.createElement("a");
+    editLink.href = "event/edit?id=" +eventId;
+    editLink.innerHTML = '<img alt="Edit" class="icon" src="public/img/note-outlined-symbol_icon-icons.com_73198.svg">';
+    result.appendChild(editLink);
+
+    var dupLink = document.createElement("a");
+    dupLink.href = "event/add?from=" +eventId;
+    dupLink.innerHTML = '<img alt="Duplicate" class="icon" src="public/img/copy-two-paper-sheets-interface-symbol_icon-icons.com_73283.svg">';
+    result.appendChild(dupLink);
+
+    var rmLink = document.createElement("a");
+    rmLink.href = "event/delete?id=" +eventId;
+    rmLink.innerHTML = '<img alt="Delete" class="icon" src="public/img/recycling-bin_icon-icons.com_73179.svg">';
+    result.appendChild(rmLink);
+    return result;
+}
+
+/**
+ * @param {...(number|string|DocumentFragment|HTMLElement)} args
+ */
+function createTableRow(args) {
+    var row = document.createElement("tr");
+    for (var i =0, nbArgs = arguments.length; i < nbArgs; ++i) {
+        var currentArg = arguments[i],
+            col = document.createElement("td");
+
+        if (currentArg instanceof DocumentFragment || currentArg instanceof HTMLElement) {
+            col.appendChild(currentArg);
+        } else {
+            col.textContent = currentArg;
+        }
+        row.appendChild(col);
+    }
+    return row;
+}
+
+function createResultRow(tableId, dataObj) {
+    var tableBody = document.getElementById(tableId),
+        frag = document.createDocumentFragment(),
+        data = [];
+
+    tableBody.textContent = "";
+    for (var i in dataObj)
+        data.push([i, dataObj[i]]);
+    data.sort((a, b) => b[1] -a[1]);
+    data.forEach(i => {
+        frag.appendChild(createTableRow(i[0], Math.round(i[1] *100) /100));
+    });
+    tableBody.appendChild(frag);
+}
+
+function displayEvents(events) {
+    var eventTable = document.getElementById(htmlIds.tables.events),
+        eventFrag = document.createDocumentFragment();
+    var amountSum = {
+        beneficiary: {},
+        context: {},
+        total: 0
+    };
+    eventTable.textContent = "";
+    events.forEach(e => {
+        eventFrag.appendChild(createTableRow(e["date"],
+            e["label"],
+            e["beneficiary"],
+            createContextLabelList(e["context"]),
+            e["amount"],
+            createEventActionButtons(e["id"])));
+        var amount = parseFloat(e["amount"]);
+        amountSum.total += amount;
+        amountSum.beneficiary[e["beneficiary"]] = (amountSum.beneficiary[e["beneficiary"]] || 0) +amount;
+        e["context"].forEach(i => amountSum.context[i] = (amountSum.context[i] || 0) +amount);
+    });
+    var totalRow = createTableRow("Total", Math.round(amountSum.total * 100) /100, "");
+    totalRow.children[0].colSpan = 4;
+    eventFrag.appendChild(totalRow);
+    eventTable.appendChild(eventFrag);
+
+    createResultRow(htmlIds.tables.beneficiaries, amountSum.beneficiary);
+    createResultRow(htmlIds.tables.contexts, amountSum.context);
+}
+
+function setPeriodStart(startDate) {
+    var date = (new Date(startDate)).getTime(),
+        eventFiltered = window["EVENTS"].filter(e => (new Date(e["date"])).getTime() >= date);
+    displayEvents(eventFiltered);
+}
+
+var cachedPeriods = {};
+function setPeriodId(id) {
+    if (cachedPeriods[id]) {
+        displayEvents(cachedPeriods[id]);
+    } else {
+        var get = new XMLHttpRequest();
+        get.open("GET", "event/list?period=" +id, true);
+        get.onreadystatechange = function() {
+            if (get.readyState === XMLHttpRequest.DONE) {
+                if (get.status === 200) {
+                    var evts = cachedPeriods[id] = JSON.parse(get.responseText);
+                    displayEvents(evts);
+                }
+            }
+        };
+        get.send(null);
+    }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+    if (window["PERIODS"].length)
+        setPeriodStart(window["PERIODS"][0]["start"]);
+});
+
+window["setPeriodStart"] = setPeriodStart;
+window["setPeriodId"] = setPeriodId;
+

+ 9 - 0
src/js/resources.js

@@ -0,0 +1,9 @@
+
+var htmlIds = {
+    tables: {
+        events: "events",
+        beneficiaries: "beneficiaries",
+        contexts: "contexts"
+    }
+};
+

+ 25 - 2
src/router.php

@@ -7,7 +7,22 @@ function route($route, $args)
 
     if (!$nbParts)
         require(__DIR__."/../templates/index.php");
-    else if ($nbParts == 2 && $route[0] == 'event' && $route[1] == "add") {
+    else if ($nbParts == 2 && $route[0] == 'event' && $route[1] == "list" && isset($_GET["period"])) {
+        $period = Period::FromHashed($_GET["period"]);
+        if (!$period) {
+            header("404 Not Found");
+            exit;
+        }
+        echo '[';
+        $written = false;
+        $events = Event::LoadForPeriod($period);
+        foreach ($events as $i) {
+            echo ($written ? ',' : '') .json_encode($i->toArray());
+            $written = true;
+        }
+        echo ']';
+        exit;
+    } else if ($nbParts == 2 && $route[0] == 'event' && $route[1] == "add") {
         if (isset($_POST["label"])) {
             // create
             $ctx = ',' .implode(',', array_unique($_POST["context"])) .',';
@@ -29,7 +44,6 @@ function route($route, $args)
         require(__DIR__."/../templates/eventAdd.php");
     } else if ($nbParts == 2 && $route[0] == 'event' && $route[1] == "edit") {
         $template = Event::FromHashed($_GET["id"]);
-        var_dump($template);
         if (isset($_POST["label"])) {
             // update
             $ctx = ',' .implode(',', array_unique($_POST["context"])) .',';
@@ -59,6 +73,15 @@ function route($route, $args)
         } else {
             require(__DIR__."/../templates/eventDel.php");
         }
+    } else if ($route[0] == "periods") {
+        if ($nbParts == 1) {
+            require(__DIR__."/../templates/periods.php");
+            exit;
+        }
+    } else if ($nbParts == 1 && $route[0] == 'logout') {
+        session_destroy();
+        header("Location: ./");
+        exit;
     } else {
         require(__DIR__."/../templates/403.php");
     }

+ 2 - 1
src/security.php

@@ -21,7 +21,8 @@ function login()
         if ($account)
         {
             $_SESSION["accountId"] = $account->getId();
-            return true;
+            header("Location: ./");
+            exit;
         }
     }
     return false;

+ 10 - 8
templates/eventAdd.php

@@ -5,8 +5,9 @@ global $account;
 Beneficiary::LoadAll();
 Context::LoadAll();
 ?><!DOCTYPE html5><html><body><form method="post" action="#">
-    <label><span>label</span><input type="text" name="label" value="<?php echo $template ? $template->getLabel() : ""; ?>"/></label>
-    <label><span>context</span><select name="context[]" multiple value="<?php echo $template ? Context::Serialize($template->getContext()) : ""; ?>"><?php
+<div class="container"><div class="row">
+    <div class="form-group"><label><span>label</span><input type="text" name="label" value="<?php echo $template ? $template->getLabel() : ""; ?>"/></label></div>
+    <div class="form-group"><label><span>context</span><select name="context[]" multiple value="<?php echo $template ? Context::Serialize($template->getContext()) : ""; ?>"><?php
     $contexts = Context::ListAll();
 usort($contexts, function($a, $b) { return strcasecmp($a->getLabel(), $b->getLabel());});
 foreach ($contexts as $ctx) {
@@ -21,14 +22,15 @@ foreach ($contexts as $ctx) {
     }
     echo '<option value="' .$ctx->getId() .'" ' .$selected .'>' .$ctx->getLabel() ."</option>";
 }
-?></select></label>
-<label><span>beneficiary</span><select name="beneficiary" value="<?php echo $template ? $template->getBeneficiary()->getId() : ""; ?>"><?php
+?></select></label></div>
+    <div class="form-group"><label><span>beneficiary</span><select name="beneficiary" value="<?php echo $template ? $template->getBeneficiary()->getId() : ""; ?>"><?php
 $beneficiaries = Beneficiary::ListAll();
 usort($beneficiaries, function($a, $b) {return strcasecmp($a->getLabel(), $b->getLabel());});
 foreach ($beneficiaries as $beneficiary) { ?>
-<option value="<?php echo $beneficiary->getId(); ?>"><?php echo $beneficiary->getLabel(); ?></option><?php } ?></select></label>
-<label><span>date</span><input type="date" name="paymentDate" value="<?php echo $template ? $template->getPaymentDate()->format("Y-m-d") : ""; ?>"/></label>
-<label><span>amount</span><input type="number" name="amount" step="0.01" min="0" value="<?php echo $template ? $template->getAmount() : ""; ?>"/></label>
-<input type="submit" value="Add"/>
+    <option value="<?php echo $beneficiary->getId(); ?>"<?php echo (($template && $template->getBeneficiary()->getId() == $beneficiary->getId()) ? " selected" : ""); ?>><?php echo $beneficiary->getLabel(); ?></option><?php } ?></select></label></div>
+    <div class="form-group"><label><span>date</span><input type="date" name="paymentDate" value="<?php echo $template ? $template->getPaymentDate()->format("Y-m-d") : ""; ?>"/></label></div>
+    <div class="form-group"><label><span>amount</span><input type="number" name="amount" step="0.01" min="0" value="<?php echo $template ? $template->getAmount() : ""; ?>"/></label></div>
+    <div class="form-group"><input type="submit" value="Add"/></div>
+</div></div>
 </form>
 </html>

+ 36 - 105
templates/index.php

@@ -2,119 +2,50 @@
 // Icons from https://icon-icons.com/fr/pack/BigMug-Line-icons/935
 
 global $account;
-$period = Period::LoadPendingPeriod();
+$periods = Period::LoadPendingPeriods();
 $events = [];
-if ($period)
-    $events = Event::LoadForPeriod($period);
-
-class Total {
-    public function Total() {
-    }
-
-    public function add($event) {
-        $amount = $event->getAmount();
-        $beneficiaryId = $event->getBeneficiary()->getId();
-        if (!isset($this->beneficiaries[$beneficiaryId]))
-            $this->beneficiaries[$beneficiaryId] = $amount;
-        else
-            $this->beneficiaries[$beneficiaryId] += $amount;
-        foreach ($event->getContext() as $ctx) {
-            if (!isset($this->contexts[$ctx->getId()]))
-                $this->contexts[$ctx->getId()] = $amount;
-            else
-                $this->contexts[$ctx->getId()] += $amount;
-        }
-        $this->total += $amount;
-    }
-
-    public function getTotal() {
-        return $this->total;
-    }
-
-    public function getBeneficiaries() {
-        $result = array();
-        foreach ($this->beneficiaries as $i => $amount)
-            array_push($result, array(Beneficiary::Get($i), $amount));
-        usort($result, function($a, $b) { return $b[1] -$a[1]; });
-        return $result;
-    }
-
-    public function getContexts() {
-        $result = array();
-        foreach ($this->contexts as $i => $amount) {
-            array_push($result, array(Context::Get($i), $amount));
-        }
-        usort($result, function($a, $b) { return $b[1] -$a[1]; });
-        return $result;
-    }
-
-    private $beneficiaries = array();
-    private $contexts = array();
-    private $total = 0;
-}
-
-function printContext($ctx) {
-    $result = "<ul style='padding: 0;'>";
-    usort($ctx, function($a, $b) { return strcasecmp($a->getLabel(), $b->getLabel()); });
-    foreach ($ctx as $i) {
-        $label = $i->getLabel();
-        $result .= "<li class='badge badge-info' style='margin-left: 5px;'>${label}</li>";
-    }
-    $result .= "</ul>";
-    return $result;
-}
-
-function printEvent($e, $totalAmount) {
-    $id = $e->getId();
-    $amount = $e->getAmount();
-    $date = $e->getPaymentDate()->format("d/m/Y");
-    $label = $e->getLabel();
-    $beneficiary = $e->getBeneficiary()->getLabel();
-    $context = printContext($e->getContext());
-
-    echo "<tr><td>${date}</td>";
-    echo "<td>${label}</td>";
-    echo "<td>${beneficiary}</td>";
-    echo "<td>${context}</td>";
-    echo "<td>${amount}</td>";
-    echo "<td>
-        <a href='event/edit?id=${id}'><img alt='Edit' class='icon' src='public/img/note-outlined-symbol_icon-icons.com_73198.svg'/></a>
-        <a href='event/add?from=${id}'><img alt='Duplicate' class='icon' src='public/img/copy-two-paper-sheets-interface-symbol_icon-icons.com_73283.svg'/></a>
-        <a href='event/delete?id=${id}'><img alt='Delete' class='icon' src='public/img/recycling-bin_icon-icons.com_73179.svg'/></a>
-    </td></tr>";
-    $totalAmount->add($e);
-}
-
-$totalAmount = new Total();
+$nbPeriods = count($periods);
+if ($nbPeriods)
+    $events = Event::LoadForPeriod($periods[$nbPeriods -1]);
 
 ?><!DOCTYPE html5><html><head>
 <link rel="stylesheet" href="public/css/bootstrap.min.css" />
 <link rel="stylesheet" href="public/css/style.css" />
 </head><body>
+<div class="row">
+<div class="col-12 col-md-3 col-xl-2 bd-sidebar">
+<h3>Pending periods</h3>
+<ul class="container">
+<?php foreach ($periods as $i) echo '<a href="#" onclick="setPeriodStart(\'' .$i->getStart() .'\')"><li>' .$i->getLabel() .' (from ' .$i->getStart() .')</li></a>'; ?>
+</ul>
+<h3>Past periods</h3>
+<ul class="container">
+<?php
+$past = Period::LoadPastPeriods();
+foreach ($past as $i) echo '<a href="#" onclick="setPeriodId(\'' .$i->getId() .'\')"><li>' .$i->getLabel() .' (from ' .$i->getStart() .' to ' .$i->getEnd() .')</li></a>'; ?>
+</ul>
+<h3>Settings</h3>
 <div class="container">
-<table class="table table-striped table-hover"><tr><th>Date</th><th>Label</th><th>Beneficiary</th><th>Context</th><th>Amount</th><th>Action</th></tr>
-<?php foreach ($events as $i) {
-    printEvent($i, $totalAmount);
-} ?>
-<tr><td colspan=4>Total</td><td><?php echo $totalAmount->getTotal(); ?></td><td></td></tr>
-</table>
-<button onclick="javascript:document.location.href='event/add'" class="btn btn-primary">Add</button></div>
-<div class="container"><div class="row"><div class="col-sm">
-<table class="table table-striped table-hover"><tr><th>Beneficiary</th><th>Amount</th></tr>
-<?php 
-    foreach ($totalAmount->getBeneficiaries() as $i) {
-        echo "<tr><td>" .$i[0]->getLabel() ."</td><td>" .$i[1] ."</td></tr>";
-    }
-?>
-</table></div><div class="col-sm">
-<table class="table table-striped table-hover"><tr><th>Context</th><th>Amount</th></tr>
-<?php 
-    foreach ($totalAmount->getContexts() as $i) {
-        echo "<tr><td>" .$i[0]->getLabel() ."</td><td>" .$i[1] ."</td></tr>";
-    }
-?>
-</table></div></div></div>
+    <a href="periods"/>Periods</a>
+    <a href="logout"/>Logout</a>
+</div></div>
+<div class="col-12 col-md-9 col-xl-8 py-md-3 pl-md-5 bd-content">
+<table class="table table-striped table-hover"><thead><tr><th>Date</th><th>Label</th><th>Beneficiary</th><th>Context</th><th>Amount</th><th>Action</th></tr></thead><tbody id="events"></tbody></table>
+<button onclick="javascript:document.location.href='event/add'" class="btn btn-primary">Add</button>
+<div class="row"><div class="col-sm">
+<table class="table table-striped table-hover"><thead><tr><th>Beneficiary</th><th>Amount</th></tr></thead><tbody id="beneficiaries"></tbody></table></div><div class="col-sm">
+<table class="table table-striped table-hover"><thead><tr><th>Context</th><th>Amount</th></tr></thead><tbody id="contexts"></tbody></table></div></div>
+<script>window["PERIODS"] = [<?php $written = false;
+foreach ($periods as $i) {
+    echo ($written ? ',' : '') .'{start:"' .$i->getStart() .'"}';
+    $written = true;
+}; ?>];window["EVENTS"]= [<?php $written = false;
+foreach ($events as $i) {
+    echo ($written ? ',' : '') .json_encode($i->toArray());
+    $written = true;
+}?>];</script>
 <script src="public/js/jquery-3.3.1.slim.min.js"></script>
 <script src="public/js/popper.min.js"></script>
 <script src="public/js/bootstrap.min.js"></script>
+<script src="public/js/script.min.js"></script>
 </body></html>

+ 27 - 0
templates/periods.php

@@ -0,0 +1,27 @@
+<?php
+// Icons from https://icon-icons.com/fr/pack/BigMug-Line-icons/935
+
+global $account;
+$periods = Period::LoadPendingPeriods();
+$nbPeriods = count($periods);
+
+?><!DOCTYPE html5><html><head>
+<link rel="stylesheet" href="public/css/bootstrap.min.css" />
+<link rel="stylesheet" href="public/css/style.css" />
+</head><body>
+<div class="row">
+<h3>Pending periods</h3>
+<ul class="container">
+<?php foreach ($periods as $i) echo '<a href="#" onclick="setPeriodStart(\'' .$i->getStart() .'\')"><li>' .$i->getLabel() .' (from ' .$i->getStart() .')</li></a>'; ?>
+</ul>
+<h3>Past periods</h3>
+<ul class="container">
+<?php
+$past = Period::LoadPastPeriods();
+foreach ($past as $i) echo '<a href="#" onclick="setPeriodId(\'' .$i->getId() .'\')"><li>' .$i->getLabel() .' (from ' .$i->getStart() .' to ' .$i->getEnd() .')</li></a>'; ?>
+</ul>
+</div>
+<script src="public/js/jquery-3.3.1.slim.min.js"></script>
+<script src="public/js/popper.min.js"></script>
+<script src="public/js/bootstrap.min.js"></script>
+</body></html>