From 0923435462bd4e57aecbe2e0810b49568ad625f4 Mon Sep 17 00:00:00 2001
From: Erik Thuning <boooink@gmail.com>
Date: Wed, 12 Jun 2019 20:39:55 +0200
Subject: [PATCH] Added a function to temporariliy suspend availability of
 products for service. Also did some refactoring in related areas.

---
 database.sql            |  11 ++++
 html/fragments.html     |  21 ++++----
 include/Ajax.php        |  22 +++++++-
 include/Loan.php        |  44 +++++++++++++++-
 include/Page.php        |  94 ++++++++++++++++++----------------
 include/Product.php     |  76 +++++++++++++++++++++-------
 include/ProductPage.php |  28 ++++++-----
 include/SearchPage.php  |   6 ++-
 include/Service.php     | 109 ++++++++++++++++++++++++++++++++++++++++
 include/User.php        |  24 ---------
 script.js               |  13 +++++
 style.css               |   4 ++
 12 files changed, 339 insertions(+), 113 deletions(-)
 create mode 100644 include/Service.php

diff --git a/database.sql b/database.sql
index 3d5f035..5bf343f 100644
--- a/database.sql
+++ b/database.sql
@@ -89,6 +89,17 @@ create table `loan` (
 ) character set utf8mb4,
   collate utf8mb4_unicode_ci;
 
+create table `service` (
+  `id` bigint(20) not null auto_increment,
+  primary key(`id`),
+  `product` bigint(20) not null,
+  constraint `s_f_product`
+    foreign key(`product`) references `product`(`id`),
+  `starttime` bigint(20) not null,
+  `returntime` bigint(20) default null
+) character set utf8mb4,
+  collate utf8mb4_unicode_ci;
+
 create table `inventory` (
   `id` bigint(20) not null auto_increment,
   primary key(`id`),
diff --git a/html/fragments.html b/html/fragments.html
index d97a63f..192d5e2 100644
--- a/html/fragments.html
+++ b/html/fragments.html
@@ -244,6 +244,17 @@
 </form>
 ¤label¤
 <div class="clear"></div>
+<form class="¤hidden¤">
+  <input type="hidden"
+         name="id"
+         value="¤id¤" />
+  <button onClick="JavaScript:discardProduct(event)">
+    Skrota artikel
+  </button>
+  <button onClick="JavaScript:toggleService(event)">
+    ¤service¤
+  </button>
+</form>
 
 ¤¤ product_label ¤¤
 <div class="qr left">
@@ -271,16 +282,6 @@
   </body>
 </html>
 
-¤¤ discard_button ¤¤
-<form>
-  <input type="hidden"
-         name="id"
-         value="¤id¤" />
-  <button onClick="JavaScript:discardProduct(event)">
-    Skrota artikel
-  </button>
-</form>
-
 ¤¤ info_item ¤¤
 <tr>
   <td>
diff --git a/include/Ajax.php b/include/Ajax.php
index 7496423..82ee28d 100644
--- a/include/Ajax.php
+++ b/include/Ajax.php
@@ -54,6 +54,8 @@ class Ajax extends Responder {
             case 'discardproduct':
                 $out = $this->discard_product();
                 break;
+            case 'toggleservice':
+                $out = $this->toggle_service();
         }
         print($out->toJson());
     }
@@ -67,7 +69,12 @@ class Ajax extends Responder {
     }
 
     private function checkout_product() {
-        $user = new User($_POST['user'], 'name');
+        $user = null;
+        try {
+            $user = new User($_POST['user'], 'name');
+        } catch(Exception $e) {
+            return new Failure('Ogiltigt användar-id.');
+        }
         $product = null;
         try {
             $product = new Product($_POST['product'], 'serial');
@@ -75,7 +82,7 @@ class Ajax extends Responder {
             return new Failure('Ogiltigt serienummer.');
         }
         try {
-            $user->create_loan($product, $_POST['end']);
+            Loan::create_loan($user, $product, $_POST['end']);
             return new Success($product->get_name() . 'utlånad.');
         } catch(Exception $e) {
             return new Failure('Artikeln är redan utlånad.');
@@ -345,5 +352,16 @@ class Ajax extends Responder {
             return new Failure('Artikeln är redan skrotad.');
         }
     }
+
+    private function toggle_service() {
+        $product = new Product($_POST['id']);
+        try {
+            $product->toggle_service();
+            return new Success('Service-status uppdaterad.');
+        } catch(Exception $e) {
+            return new Failure('Service kan inte registreras '
+                              .'på den här artikeln nu.');
+        }
+    }
 }
 ?>
diff --git a/include/Loan.php b/include/Loan.php
index 87d57d7..1fb4fd6 100644
--- a/include/Loan.php
+++ b/include/Loan.php
@@ -6,9 +6,51 @@ class Loan {
     private $starttime = 0;
     private $endtime = 0;
     private $returntime = null;
+
+    public static function create_loan($user, $product, $endtime) {
+        $status = $product->get_status();
+        if($status != 'available') {
+            $emsg = '';
+            $prod_id = $product->get_id();
+            switch($status) {
+                case 'on_loan':
+                case 'overdue':
+                    $loan_id = $product->get_active_loan()->get_id();
+                    $emsg = "Product $prod_id has an active ";
+                    $emsg .= "loan (id $loan_id) already.";
+                    break;
+                case 'discarded':
+                    $emsg = "Product $prod_id has been discarded.";
+                    break;
+                case 'service':
+                    $service_id = $product->get_active_service()->get_id();
+                    $emsg = "Product $prod_id is on service (id $service_id).";
+                    break;
+            }
+            throw new Exception($emsg);
+        }
+        $now = time();
+        $insert = prepare('insert into
+                               `loan`(`user`, `product`, `starttime`, `endtime`)
+                               values (?, ?, ?, ?)');
+        bind($insert, 'iiii',
+             $user->get_id(), $product->get_id(),
+             $now, strtotime($endtime . ' 13:00'));
+        execute($insert);
+        $loan_id = $insert->insert_id;
+        return new Loan($loan_id);
+    }
     
     public function __construct($id) {
-        $this->id = $id;
+        $search = prepare('select `id` from `loan`
+                           where `id`=?');
+        bind($search, 'i', $id);
+        execute($search);
+        $result = result_single($search);
+        if($result === null) {
+            throw new Exception('Loan does not exist.');
+        }
+        $this->id = $result['id'];
         $this->update_fields();
     }
     
diff --git a/include/Page.php b/include/Page.php
index 3f3902a..0fb4bf3 100644
--- a/include/Page.php
+++ b/include/Page.php
@@ -135,14 +135,19 @@ abstract class Page extends Responder {
                                       'page' => 'products'),
                                 $this->fragments['item_link']);
             $available = 'Tillgänglig';
-            $status = 'available';
-            $discarded = $product->get_discardtime();
-            if($discarded) {
-                $available = 'Skrotad '.$discarded;
-                $status = 'discarded';
-            } else {
-                $loan = $product->get_active_loan();
-                if($loan) {
+            $status = $product->get_status();
+            switch($status) {
+                case 'discarded':
+                    $available = 'Skrotad '.$discarded;
+                    break;
+                case 'service':
+                    $service = $product->get_active_service();
+                    $available = 'På service sedan '
+                                .$service->get_duration()['start'];
+                    break;
+                case 'on_loan':
+                case 'overdue':
+                    $loan = $product->get_active_loan();
                     $user = $loan->get_user();
                     $userlink = replace(array('name' => $user->get_displayname(),
                                               'id' => $user->get_id(),
@@ -150,13 +155,11 @@ abstract class Page extends Responder {
                                         $this->fragments['item_link']);
                     $available = 'Utlånad till '.$userlink;
                     if($loan->is_overdue()) {
-                        $status = 'overdue';
                         $available .= ', försenad';
                     } else {
-                        $status = 'on_loan';
                         $available .= ', åter '.$loan->get_duration()['end'];
                     }
-                }
+                    break;
             }
             $rows .= replace(array('available' => $available,
                                    'serial' => $product->get_serial(),
@@ -194,7 +197,6 @@ abstract class Page extends Responder {
                                       'name' => $product->get_name(),
                                       'page' => 'products'),
                                 $this->fragments['item_link']);
-            $available = '';
             $duration = $loan->get_duration();
             $status = 'on_loan';
             if($loan->is_overdue()) {
@@ -222,41 +224,45 @@ abstract class Page extends Responder {
                        $this->fragments['loan_table']);
     }
 
-    final protected function build_product_loan_table($loans) {
+    final protected function build_product_history_table($history) {
         $rows = '';
         $renew_column_visible = 'hidden';
-        foreach($loans as $loan) {
-            $user = $loan->get_user();
-            $product = $loan->get_product();
-            $userlink = replace(array('id' => $user->get_id(),
-                                      'name' => $user->get_name(),
-                                      'page' => 'users'),
-                                $this->fragments['item_link']);
-            $available = '';
-            $duration = $loan->get_duration();
-            $status = 'on_loan';
-            if($loan->is_overdue()) {
-                $status = 'overdue';
+        foreach($history as $event) {
+            $duration = $event->get_duration();
+            $product = $event->get_product();
+            $fields = array('item_link' => 'Service',
+                            'start_date' => $duration['start'],
+                            'end_date' => $duration['end'],
+                            'return_date' => $duration['return'],
+                            'status' => 'available',
+                            'vis_renew' => $renew_column_visible,
+                            'vis_renew_button' => 'hidden',
+                            'vis_return' => '',
+                            'id' => $product->get_id(),
+                            'end_new' => '');
+            if($event instanceof Loan) {
+                $user = $event->get_user();
+                $id = $user->get_id();
+                $name = $user->get_name();
+                $fields['item_link'] = replace(array('id' => $id,
+                                                     'name' => $name,
+                                                     'page' => 'users'),
+                                               $this->fragments['item_link']);
+                $fields['end_new'] = $duration['end_renew'];
+                if($event->is_active()) {
+                    $fields['vis_renew_button'] = '';
+                    $fields['vis_renew'] = '';
+                    $renew_column_visible = '';
+                    if($event->is_overdue()) {
+                        $fields['status'] = 'overdue';
+                    } else {
+                        $fields['status'] = 'on_loan';
+                    }
+                }
+            } else if ($event instanceof Service) {
+                $fields['status'] = 'service';
             }
-            $returndate = '';
-            $renew_visible = '';
-            if($duration['return']) {
-                $returndate = $duration['return'];
-                $renew_visible = 'hidden';
-            } else {
-                $renew_column_visible = '';
-            }
-            $rows .= replace(array('item_link' => $userlink,
-                                   'start_date' => $duration['start'],
-                                   'end_date' => $duration['end'],
-                                   'return_date' => $returndate,
-                                   'status' => $status,
-                                   'vis_renew' => $renew_column_visible,
-                                   'vis_renew_button' => $renew_visible,
-                                   'vis_return' => '',
-                                   'id' => $product->get_id(),
-                                   'end_new' => $duration['end_renew']),
-                             $this->fragments['loan_row']);
+            $rows .= replace($fields, $this->fragments['loan_row']);
         }
         return replace(array('rows' => $rows,
                              'vis_renew' => $renew_column_visible,
diff --git a/include/Product.php b/include/Product.php
index 0b8a806..9bfabdc 100644
--- a/include/Product.php
+++ b/include/Product.php
@@ -9,13 +9,11 @@ class Product {
     private $info = array();
     private $tags = array();
     
-    public static function create_product(
-        $name = '',
-        $invoice = '',
-        $serial = '',
-        $info = array(),
-        $tags = array()
-    ) {
+    public static function create_product($name = '',
+                                          $invoice = '',
+                                          $serial = '',
+                                          $info = array(),
+                                          $tags = array()) {
         $now = time();
         begin_trans();
         try {
@@ -59,7 +57,7 @@ class Product {
         execute($search);
         $result = result_single($search);
         if($result === null) {
-            throw new Exception('Product does not exist..');
+            throw new Exception('Product does not exist.');
         }
         $this->id = $result['id'];
         $this->update_fields();
@@ -118,7 +116,7 @@ class Product {
                     case 'tag':
                         $matchvalues = $this->get_tags();
                     case 'status':
-                        $matchvalues[] = $this->get_loan_status();
+                        $matchvalues[] = $this->get_status();
                     case 'fritext':
                         $matchvalues[] = $this->name;
                         $matchvalues[] = $this->serial;
@@ -152,6 +150,9 @@ class Product {
     }
 
     public function discard() {
+        if($this->get_status() != 'available') {
+            return false;
+        }
         $now = time();
         $update = prepare('update `product` set `discardtime`=? where `id`=?');
         bind($update, 'ii', $now, $this->id);
@@ -160,6 +161,33 @@ class Product {
         return true;
     }
     
+    public function toggle_service() {
+        $status = $this->get_status();
+        $now = time();
+        $update = '';
+        if($status == 'service') {
+            return $this->get_active_service()->end();
+        } else if($status == 'available') {
+            Service::create_service($this);
+            return true;
+        }
+        $id = $this->get_id();
+        throw new Exception("The state ($status) of this product (id $id) "
+                           ."does not allow servicing.");
+    }
+
+    public function get_active_service() {
+        $find = prepare('select `id` from `service`'
+                       .'where `returntime` is null and product=?');
+        bind($find, 'i', $this->id);
+        execute($find);
+        $result = result_single($find);
+        if($result === null) {
+            return null;
+        }
+        return new Service($result['id']);
+    }
+    
     public function get_name() {
         return $this->name;
     }
@@ -276,13 +304,16 @@ class Product {
         return true;
     }
 
-    public function get_loan_status() {
+    public function get_status() {
         if($this->get_discardtime(false)) {
             return 'discarded';
         }
+        if($this->get_active_service()) {
+            return 'service';
+        }
         $loan = $this->get_active_loan();
         if(!$loan) {
-            return 'no_loan';
+            return 'available';
         }
         if($loan->is_overdue()) {
             return 'overdue';
@@ -302,16 +333,23 @@ class Product {
         return new Loan($result['id']);
     }
 
-    public function get_loan_history() {
-        $find = prepare('select `id` from `loan`
-                         where product=? order by `starttime` desc');
-        bind($find, 'i', $this->id);
-        execute($find);
-        $loans = result_list($find);
+    public function get_history() {
         $out = array();
-        foreach($loans as $loan) {
-            $out[] = new Loan($loan['id']);
+        foreach(array('loan'    => function($id) { return new Loan($id);},
+                      'service' => function($id) { return new Service($id);})
+                          as $type => $func) {
+            $find = prepare("select `id` from `$type`"
+                           .'where `product`=? order by `starttime` desc');
+            bind($find, 'i', $this->id);
+            execute($find);
+            $items = result_list($find);
+            foreach($items as $item) {
+                $out[] = $func($item['id']);
+            }
         }
+        usort($out, function($a, $b) {
+            return $a->get_duration()['start'] < $b->get_duration()['start'];
+        });
         return $out;
     }
 }
diff --git a/include/ProductPage.php b/include/ProductPage.php
index e9d23fe..065bdaa 100644
--- a/include/ProductPage.php
+++ b/include/ProductPage.php
@@ -78,25 +78,29 @@ class ProductPage extends Page {
                         'serial' => $this->product->get_serial(),
                         'invoice' => $this->product->get_invoice(),
                         'tags' => $tags,
-                        'info' => $info);
-        $label = '';
+                        'info' => $info,
+                        'label' => '',
+                        'hidden' => 'hidden',
+                        'service' => 'Starta service');
         if(class_exists('QRcode')) {
-            $label = replace($fields, $this->fragments['product_label']);
+            $fields['label'] = replace($fields,
+                                       $this->fragments['product_label']);
         }
-        $fields['label'] = $label;
-        $out = replace($fields, $this->fragments['product_details']);
         if(!$this->product->get_discardtime()) {
-            $out .= replace(array('id' => $this->product->get_id()),
-                            $this->fragments['discard_button']);
+            $fields['hidden'] = '';
+            if($this->product->get_status() == 'service') {
+                $fields['service'] = 'Avsluta service';
+            }
         }
-        $out .= replace(array('title' => 'Lånehistorik'),
+        $out = replace($fields, $this->fragments['product_details']);
+        $out .= replace(array('title' => 'Artikelhistorik'),
                         $this->fragments['subtitle']);
-        $loan_table = 'Inga lån att visa.';
-        $history = $this->product->get_loan_history();
+        $history_table = 'Ingen historik att visa.';
+        $history = $this->product->get_history();
         if($history) {
-            $loan_table = $this->build_product_loan_table($history);
+            $history_table = $this->build_product_history_table($history);
         }
-        $out .= $loan_table;
+        $out .= $history_table;
         return $out;
     }
 
diff --git a/include/SearchPage.php b/include/SearchPage.php
index 49d6970..0536cdb 100644
--- a/include/SearchPage.php
+++ b/include/SearchPage.php
@@ -80,7 +80,7 @@ class SearchPage extends Page {
                 case 'ledigt':
                 case 'tillgänglig':
                 case 'tillgängligt':
-                    $newitem = 'no_loan';
+                    $newitem = 'available';
                     break;
                 case 'sen':
                 case 'sent':
@@ -95,6 +95,10 @@ class SearchPage extends Page {
                 case 'slängt':
                     $newitem = 'discarded';
                     break;
+                case 'lagning':
+                case 'reparation':
+                    $newitem = 'service';
+                    break;
             }
             $translated[] = $newitem;
         }
diff --git a/include/Service.php b/include/Service.php
new file mode 100644
index 0000000..5f19014
--- /dev/null
+++ b/include/Service.php
@@ -0,0 +1,109 @@
+<?php
+class Service {
+    private $id = 0;
+    private $product = 0;
+    private $starttime = 0;
+    private $returntime = null;
+
+    public static function create_service($product) {
+        $status = $product->get_status();
+        if($status != 'available') {
+            $emsg = '';
+            $prod_id = $product->get_id();
+            switch($status) {
+                case 'on_loan':
+                case 'overdue':
+                    $loan_id = $product->get_active_loan()->get_id();
+                    $emsg = "Product $prod_id has an active ";
+                    $emsg .= "loan (id $loan_id).";
+                    break;
+                case 'discarded':
+                    $emsg = "Product $prod_id has been discarded.";
+                    break;
+                case 'service':
+                    $service_id = $product->get_active_service()->get_id();
+                    $emsg = "Product $prod_id is on service "
+                           ."(id $service_id) already.";
+                    break;
+            }
+            throw new Exception($emsg);
+        }
+        $now = time();
+        $insert = prepare('insert into
+                               `service`(`product`, `starttime`)
+                               values (?, ?)');
+        bind($insert, 'ii', $product->get_id(), $now);
+        execute($insert);
+        $service_id = $insert->insert_id;
+        return new Loan($service_id);
+    }
+    
+    public function __construct($id) {
+        $search = prepare('select `id` from `service`
+                           where `id`=?');
+        bind($search, 'i', $id);
+        execute($search);
+        $result = result_single($search);
+        if($result === null) {
+            throw new Exception('Service does not exist.');
+        }
+        $this->id = $result['id'];
+        $this->update_fields();
+    }
+    
+    private function update_fields() {
+        $get = prepare('select * from `service` where `id`=?');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $loan = result_single($get);
+        $this->product = $loan['product'];
+        $this->starttime = $loan['starttime'];
+        $this->returntime = $loan['returntime'];
+    }
+
+    public function get_id() {
+        return $this->id;
+    }
+
+    public function get_user() {
+        return 'Service';
+    }
+    
+    public function get_product() {
+        return new Product($this->product);
+    }
+
+    public function get_duration($format = true) {
+        $style = function($time) {
+            return $time;
+        };
+        if($format) {
+            $style = function($time) {
+                if($time) {
+                    return gmdate('Y-m-d', $time);
+                }
+                return $time;
+            };
+        }
+        return array('start' => $style($this->starttime),
+                     'end' => '',
+                     'return' => $style($this->returntime));
+    }
+
+    public function is_active() {
+        if($this->returntime === null) {
+            return true;
+        }
+        return false;
+    }
+
+    public function end() {
+        $now = time();
+        $query = prepare('update `service` set `returntime`=? where `id`=?');
+        bind($query, 'ii', $now, $this->id);
+        execute($query);
+        $this->returntime = $now;
+        return true;
+    }
+}
+?>
diff --git a/include/User.php b/include/User.php
index d21c0a4..7e79fcb 100644
--- a/include/User.php
+++ b/include/User.php
@@ -148,29 +148,5 @@ class User {
         }
         return $overdue;
     }
-    
-    public function create_loan($product, $endtime) {
-        $find = prepare('select * from `loan`
-                         where `product`=? and `returntime` is null');
-        $prod_id = $product->get_id();
-        bind($find, 'i', $prod_id);
-        execute($find);
-        $loan = result_single($find);
-        if($loan !== null) {
-            $loan_id = $loan['id'];
-            throw new Exception(
-                "Product $prod_id has an active loan (id $loan_id) already.");
-        }
-        $now = time();
-        $insert = prepare('insert into
-                               `loan`(`user`, `product`, `starttime`, `endtime`)
-                               values (?, ?, ?, ?)');
-        bind($insert, 'iiii',
-             $this->id, $prod_id,
-             $now, strtotime($endtime . ' 13:00'));
-        execute($insert);
-        $loan_id = $insert->insert_id;
-        return new Loan($loan_id);
-    }
 }
 ?>
diff --git a/script.js b/script.js
index 48b3140..8cd1a91 100644
--- a/script.js
+++ b/script.js
@@ -434,6 +434,19 @@ function discardProduct(event) {
     ajaxRequest('discardproduct', dataListFromForm(form), render)
 }
 
+function toggleService(event) {
+    event.preventDefault()
+    var form = event.currentTarget.parentNode
+    var render = function(result) {
+        if(result.type == 'success') {
+            window.location.reload(false)
+        } else {
+            showResult(result)
+        }
+    }
+    ajaxRequest('toggleservice', dataListFromForm(form), render)
+}
+
 function searchInput(event) {
     if(event.key != "Enter") {
         return
diff --git a/style.css b/style.css
index 7f42a55..4354786 100644
--- a/style.css
+++ b/style.css
@@ -84,6 +84,10 @@ td.discarded {
     background-color: #a0a0a0;
 }
 
+td.service {
+    background-color: #e7e08d;
+}
+
 tbody tr {
     background-color: #d7e0eb;
 }