diff --git a/README.md b/README.md
index 3f2333b..c8475f3 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,5 @@
 There should be a description here
 
 Additional line
+
+additional line 2
diff --git a/config.php.example b/config.php.example
index 982e551..32cc899 100644
--- a/config.php.example
+++ b/config.php.example
@@ -6,8 +6,11 @@ $db_user = 'dbname';
 $db_pass = 'dbpassword';
 $db_name = 'dbuser';
 
+# Email subject prefix
+$email_subject_prefix = "System name: ";
+
 # Address to use as the sender for reminder emails
-$reminder_sender = 'noreply@example.com';
+$sender = 'noreply@example.com';
 
 # Address to send cron error messages to
 $error_address = 'root@example.com';
@@ -15,7 +18,7 @@ $error_address = 'root@example.com';
 # Discard notifications
 # If this is set to an email address, the system will send a notification
 # there each time a product is discarded.
-#$notify_discard = 'inventory-tracking@example.com';
+#$discard_notify = 'inventory-tracking@example.com';
 $discard_notify = false;
 
 # Directory to save attachments to
diff --git a/cron.php b/cron.php
index d2170df..7761e2f 100755
--- a/cron.php
+++ b/cron.php
@@ -7,7 +7,7 @@ require('./include/functions.php');
 
 header('Content-Type: text/html; charset=UTF-8');
 
-$cron = new Cron($reminder_sender, $error_address);
+$cron = new Cron($sender, $error_address, $email_subject_prefix);
 $cron->run();
 
 ?>
diff --git a/database.sql b/database.sql
index 840e57b..7f459e7 100644
--- a/database.sql
+++ b/database.sql
@@ -147,3 +147,24 @@ create table `kvs` (
   `value` varchar(64) not null default ''
 ) character set utf8mb4,
   collate utf8mb4_unicode_ci;
+
+create table `pending_receipt` (
+  `user` bigint(20) not null,
+  primary key (`user`),
+  constraint `pr_f_user`
+    foreign key(`user`) references `user`(`id`),
+  `send_time` bigint(20) not null,
+  `since_time` bigint(20) not null
+) character set utf8mb4,
+  collate utf8mb4_unicode_ci;
+
+create table `loan_extension` (
+  `loan` bigint(20) not null,
+  constraint `le_f_loan`
+    foreign key(`loan`) references `loan`(`event`),
+  `extend_time` bigint(20) not null,
+  primary key (`loan`, `extend_time`),
+  `old_end` bigint(20) not null,
+  `new_end` bigint(20) not null
+) character set utf8mb4,
+  collate utf8mb4_unicode_ci;
diff --git a/html/fragments.html b/html/fragments.html
index b09a101..dcaca53 100644
--- a/html/fragments.html
+++ b/html/fragments.html
@@ -85,7 +85,7 @@
       </th>
     </tr>
   </thead>
-  <tbody>
+  <tbody class="¤type¤">
     ¤rows¤
   </tbody>
 </table>
@@ -105,6 +105,25 @@
   </td>
 </tr>
 
+¤¤ product_detail_row ¤¤
+<tr>
+  <td class="status ¤status¤">
+  </td>
+  <td colspan="3">
+    <dl>
+      ¤details¤
+    </dl>
+  </td>
+</tr>
+
+¤¤ product_detail ¤¤
+<dt>
+  ¤name¤:
+</dt>
+<dd>
+  ¤value¤
+</dd>
+
 ¤¤ template_management ¤¤
 <div>
   <h2>Mallar</h2>
@@ -133,7 +152,7 @@
   </form>
 </div>
 
-¤¤ product_details ¤¤
+¤¤ product_form ¤¤
 <div id="product-details">
   <h2>Artikeldata</h2>
   <form id="product-data"
@@ -287,16 +306,54 @@
     <button>Ladda upp</button>
   </form>
 </div>
-<div id="product-history"
-     class="¤hidden¤">
+
+¤¤ product_meta ¤¤
+<div id="product-history">
   <h2>Artikelhistorik</h2>
   ¤history¤
 </div>
 <div id="product-label"
-     class="¤hidden¤">
+     class="¤label_hidden¤">
   <h2>Etikett</h2>
   ¤label¤
 </div>
+<div id="product-direct-checkout" class="¤checkout_hidden¤">
+  <h2>Låna ut</h2>
+  <form class="light"
+        onSubmit="JavaScript:checkoutProduct(event)">
+    <datalist id="user_suggest"></datalist>
+    <input type="hidden"
+           name="page"
+           value="checkout" />
+    <label for="user">Användarnamn:</label>
+    <input onFocus="JavaScript:suggest(this, 'user')"
+           type="text"
+           name="user"
+           list="user_suggest"
+           autocomplete="off"
+           placeholder="Användarnamn"
+           required />
+    <input type="hidden"
+           id="product"
+           name="product"
+           value="¤serial¤" />
+    <button>
+      Låna ut
+    </button>
+    <br/>
+    <label>Löptid:</label>
+    <button onClick="JavaScript:loanLength(event, 7, 'day')">1 vecka</button>
+    <button onClick="JavaScript:loanLength(event, 1, 'year')">1 år</button>
+    <button onClick="JavaScript:loanLength(event, 3, 'year')">3 år</button>
+    <br/>
+    <label for="end">Slutdatum:</label>
+    <input type="text"
+           id="end"
+           onClick="JavaScript:calendar(event)"
+           name="end"
+           value="¤end¤" />
+  </form>
+</div>
 
 ¤¤ attachment_list ¤¤
 <ul class="attachment-list">
@@ -534,12 +591,22 @@
              list="user_suggest"
              autocomplete="off"
              placeholder="Användarnamn"
-             value="¤user¤"
-             required />
+             value="¤user¤" />
       <button type="submit" >
         Välj
       </button>
     </div>
+    <div>
+      <label for="email">
+        E-post:
+      </label>
+      <input type="text"
+             name="email"
+             id="email"
+             autocomplete="off"
+             placeholder="E-post"
+             value="¤email¤" />
+    </div>
     <div>
       <label for="displayname">
         Namn:
@@ -581,6 +648,11 @@
       Låna ut
     </button>
     <br/>
+    <label>Löptid:</label>
+    <button onClick="JavaScript:loanLength(event, 7, 'day')">1 vecka</button>
+    <button onClick="JavaScript:loanLength(event, 1, 'year')">1 år</button>
+    <button onClick="JavaScript:loanLength(event, 3, 'year')">3 år</button>
+    <br/>
     <label for="end">Slutdatum:</label>
     <input type="text"
            id="end"
diff --git a/include/Ajax.php b/include/Ajax.php
index 8ddadd3..bc4a4cc 100644
--- a/include/Ajax.php
+++ b/include/Ajax.php
@@ -1,14 +1,14 @@
 <?php
 class Ajax extends Responder {
     private $action = '';
-    
+
     public function __construct() {
         parent::__construct();
         if(isset($_GET['action'])) {
             $this->action = $_GET['action'];
         }
     }
-    
+
     public function render() {
         $out = '';
         switch($this->action) {
@@ -87,7 +87,7 @@ class Ajax extends Responder {
         }
         $product = null;
         try {
-            $product = new Product($_POST['product'], 'serial');
+            $product = new Product(trim($_POST['product']), 'serial');
         } catch(Exception $e) {
             return new Failure('Ogiltigt serienummer.');
         }
@@ -98,11 +98,11 @@ class Ajax extends Responder {
             return new Failure('Artikeln är redan utlånad.');
         }
     }
-    
+
     private function return_product() {
         $product = null;
         try {
-            $product = new Product($_POST['serial'], 'serial');
+            $product = new Product(trim($_POST['serial']), 'serial');
         } catch(Exception $e) {
             return new Failure('Ogiltigt serienummer.');
         }
@@ -112,7 +112,7 @@ class Ajax extends Responder {
             $user = $loan->get_user();
             $userlink = replace(array('page' => 'users',
                                       'id'   => $user->get_id(),
-                                      'name' => $user->get_displayname()),
+                                      'name' => $user->get_displayname($this->ldap)),
                                 $this->fragments['item_link']);
             $productlink = replace(array('page' => 'products',
                                          'id'   => $product->get_id(),
@@ -138,7 +138,7 @@ class Ajax extends Responder {
         }
         return new Failure('Lån saknas.');
     }
-    
+
     private function start_inventory() {
         try {
             Inventory::begin();
@@ -147,7 +147,7 @@ class Ajax extends Responder {
             return new Failure('Inventering redan igång.');
         }
     }
-    
+
     private function end_inventory() {
         $inventory = Inventory::get_active();
         if($inventory === null) {
@@ -156,7 +156,7 @@ class Ajax extends Responder {
         $inventory->end();
         return new Success('Inventering avslutad.');
     }
-    
+
     private function inventory_product() {
         $inventory = Inventory::get_active();
         if($inventory === null) {
@@ -164,7 +164,7 @@ class Ajax extends Responder {
         }
         $product = null;
         try {
-            $product = new Product($_POST['serial'], 'serial');
+            $product = new Product(trim($_POST['serial']), 'serial');
         } catch(Exception $e) {
             return new Failure('Ogiltigt serienummer.');
         }
@@ -273,7 +273,7 @@ class Ajax extends Responder {
         }
         return new Success('Ändringarna sparade.');
     }
-    
+
     private function update_user() {
         $id = $_POST['id'];
         $name = $_POST['name'];
@@ -355,7 +355,7 @@ class Ajax extends Responder {
             return new Failure('Det finns ingen mall med det namnet.');
         }
     }
-    
+
     private function suggest() {
         return new Success(suggest($_POST['type']));
     }
diff --git a/include/CheckoutPage.php b/include/CheckoutPage.php
index e07f199..e209b81 100644
--- a/include/CheckoutPage.php
+++ b/include/CheckoutPage.php
@@ -1,30 +1,72 @@
 <?php
 class CheckoutPage extends Page {
     private $userstr = '';
+    private $emailstr = '';
     private $user = null;
 
     public function __construct() {
         parent::__construct();
         if(isset($_GET['user'])) {
             $this->userstr = trim(strtolower($_GET['user']));
-            try {
-                $this->user = new User($this->userstr, 'name');
-            } catch(Exception $ue) {
-                try {
-                    $ldap = new Ldap();
-                    $ldap->get_user($this->userstr);
-                    $this->user = User::create_user($this->userstr);
-                } catch(Exception $le) {
-                    $this->error = "Användarnamnet '";
-                    $this->error .= $this->userstr;
-                    $this->error .= "' kunde inte hittas.";
-                }
-            }
+        }
+        if(isset($_GET['email'])) {
+            $this->emailstr = trim(strtolower($_GET['email']));
+        }
+        try {
+            $this->user = $this->user_init($this->userstr,
+                                           $this->emailstr);
+        } catch(Exception $e) {
+            $this->error = $e->getMessage();
         }
     }
 
+    protected function user_init($name, $email) {
+        $nameuser = null;
+        $emailuser = null;
+        if($name) {
+            try {
+                $nameuser = new User($name, 'name');
+            } catch(Exception $ue) {
+                # The user wasn't found locally
+                try {
+                    $this->ldap->get_user($name);
+                    $nameuser = User::create_user($name);
+                } catch(Exception $le) {
+                    $err = "Användarnamnet '$name' kunde inte hittas.";
+                    throw new Exception($err);
+                }
+            }
+        }
+        if($email) {
+            try {
+                $search = $email;
+                if(strpos($email, '@') === false) {
+                    $search = $email .'@dsv.su.se';
+                }
+                # Lookup email directly in ldap since we don't store it
+                $emailuser = new User($this->ldap->search_email($search),
+                                      'name');
+            } catch(Exception $ue) {
+                $err = "E-postadressen '$search' kunde inte hittas.";
+                throw new Exception($err);
+            }
+        }
+        if($nameuser && $emailuser) {
+            if($nameuser != $emailuser) {
+                $err = "Användarnamn och e-post matchar olika användare.";
+                throw new Exception($err);
+            }
+            return $nameuser;
+        }
+        if($nameuser) {
+            return $nameuser;
+        }
+        return $emailuser;
+    }
+
     protected function render_body() {
-        $username = '';
+        $username = $this->userstr;
+        $email = $this->emailstr;
         $displayname = '';
         $notes = '';
         $loan_table = '';
@@ -33,7 +75,8 @@ class CheckoutPage extends Page {
         $disabled = 'disabled';
         if($this->user !== null) {
             $username = $this->user->get_name();
-            $displayname = $this->user->get_displayname();
+            $email = $this->user->get_email($this->ldap);
+            $displayname = $this->user->get_displayname($this->ldap);
             $notes = $this->user->get_notes();
             $enddate = format_date(default_loan_end(time()));
             $disabled = '';
@@ -45,7 +88,8 @@ class CheckoutPage extends Page {
             $subhead = replace(array('title' => 'Lånade artiklar'),
                                $this->fragments['subtitle']);
         }
-        print(replace(array('user' => $this->userstr,
+        print(replace(array('user' => $username,
+                            'email' => $email,
                             'displayname' => $displayname,
                             'notes' => $notes,
                             'end' => $enddate,
diff --git a/include/Cron.php b/include/Cron.php
index 50682f7..9fcaed8 100644
--- a/include/Cron.php
+++ b/include/Cron.php
@@ -4,22 +4,194 @@ class Cron {
     private $sender = '';
     private $error = '';
     private $kvs;
-    public function __construct($sender, $error) {
-        $this->now = time();
+    private $ldap;
+    public function __construct($sender, $error, $prefix) {
+        $this->now = new DateTimeImmutable();
         $this->sender = $sender;
         $this->error = $error;
+        $this->subject_prefix = $prefix;
+        $this->warn_time = DateInterval::createFromDateString('3 days');
+        $this->warn_date = $this->now->add($this->warn_time);
+        $this->run_interval = DateInterval::createFromDateString('1 day');
         $this->kvs = new Kvs();
+        $this->ldap = new Ldap();
+
+        $days = $this->warn_time->d;
+        $this->strings = array(
+            'en' => array(
+                'new' => array(
+                    'single' => "The following loan has been registered in your name:",
+                    'multi'  => "The following loans have been registered in your name:",
+                    'expiry' => "expires on",
+                    'serial' => "serial number",
+                ),
+                'extend' => array(
+                    'single' => "The following loan has been extended:",
+                    'multi'  => "The following loans have been extended:",
+                    'expiry' => "extended to",
+                    'serial' => "serial number",
+                ),
+                'expiring' => array(
+                    'single' => "The following loan expires in less than $days days:",
+                    'multi'  => "The following loans expire in less than $days days:",
+                    'expiry' => "expires on",
+                    'serial' => "serial number",
+                ),
+                'overdue' => array(
+                    'single' => "The following loan has expired:",
+                    'multi'  => "The following loans have expired:",
+                    'expiry' => "expired on",
+                    'serial' => "serial number",
+                ),
+            ),
+            'sv' => array(
+                'new' => array(
+                    'single' => "Följande lån har registrerats på din användare:",
+                    'multi'  => "Följande lån har registrerats på din användare:",
+                    'expiry' => "går ut",
+                    'serial' => "artikelnummer",
+                ),
+                'extend' => array(
+                    'single' => "Följande lån har förlängts:",
+                    'multi'  => "Följande lån har förlängts:",
+                    'expiry' => "förlängt till",
+                    'serial' => "artikelnummer",
+                ),
+                'expiring' => array(
+                    'single' => "Följande lån går ut om mindre än $days dagar:",
+                    'multi'  => "Följande lån går ut om mindre än $days dagar:",
+                    'expiry' => "går ut",
+                    'serial' => "artikelnummer",
+                ),
+                'overdue' => array(
+                    'single' => "Följande lån har gått ut:",
+                    'multi'  => "Följande lån har gått ut:",
+                    'expiry' => "gick ut",
+                    'serial' => "artikelnummer",
+                ),
+            ),
+        );
     }
 
     public function run() {
-        $lastrun = $this->kvs->get_value('lastrun');
-        $interval = 3600*24; //1 day in seconds
-        
-        if($lastrun && $this->now - $lastrun < $interval) {
+        $this->run_receipts();
+        $this->run_reminders();
+    }
+
+    private function run_receipts() {
+        begin_trans();
+        $get = prepare('select * from `pending_receipt`
+                        where `send_time` < ?');
+        bind($get, 'i', $this->now->getTimestamp());
+        execute($get);
+        foreach(result_list($get) as $row) {
+            $user = new User($row['user']);
+            $since_time = $row['since_time'];
+
+            $new_loans = array();
+            $extended_loans = array();
+            foreach($user->get_loans('active') as $loan) {
+                if($loan->get_starttime() >= $since_time) {
+                    $new_loans[] = $loan;
+                } else if($loan->get_last_extension() >= $since_time) {
+                    $extended_loans[] = $loan;
+                }
+            }
+            if($new_loans || $extended_loans) {
+                $this->send_receipt($user, $new_loans, $extended_loans);
+            }
+            $delete = prepare('delete from `pending_receipt`
+                               where `user` = ? and `send_time` < ?');
+            bind($delete, 'ii', $user->get_id(), $this->now->getTimestamp());
+            execute($delete);
+        }
+        commit_trans();
+    }
+
+    private function make_receipt_subject($num_new, $num_extended) {
+        $subject = $this->subject_prefix;
+        $messages = array();
+        if($num_new > 1) {
+            $messages[] = $num_new." nya";
+        } else if($num_new > 0) {
+            $messages[] = $num_new." nytt";
+        }
+        if($num_extended > 1) {
+            $messages[] = $num_extended." förlängda";
+        } else if($num_extended > 0) {
+            $messages[] = $num_extended." förlängt";
+        }
+        return $subject.implode(" och ", $messages)." lån";
+    }
+
+    private function send_receipt($user, $new, $extended) {
+        $uid = $user->get_name();
+        $name = $this->ldap->get_firstname($uid);
+
+        $new_count = count($new);
+        $extended_count = count($extended);
+        $subject = $this->make_receipt_subject($new_count, $extended_count);
+
+        $list_sv = array();
+        $list_en = array();
+        if($new_count > 0) {
+            $list_sv[] = $this->make_notice('sv', 'new', $new);
+            $list_en[] = $this->make_notice('en', 'new', $new);
+        }
+        if($extended_count > 0) {
+            $list_sv[] = $this->make_notice('sv', 'extend', $extended);
+            $list_en[] = $this->make_notice('en', 'extend', $extended);
+        }
+        $list_sv = implode("\n\n", $list_sv);
+        $list_en = implode("\n\n", $list_en);
+
+        $info_sv = array();
+        $info_en = array();
+        if($new_count > 0) {
+            $info_sv[] = "Eventuella artiklar du inte hämtat ut redan kan hämtas från Helpdesk.";
+            $info_en[] = "Any products you haven't already picked up can be collected from Helpdesk.";
+        }
+        $info_sv[] = "Svara på det här mailet om du har några frågor.";
+        $info_en[] = "Please reply to this email if you have any questions.";
+        $info_sv = implode(' ', $info_sv);
+        $info_en = implode(' ', $info_en);
+
+        $message = <<<EOF
+Hej $name!
+
+Det här är ett automatiskt meddelande från Helpdesk.
+
+$list_sv
+
+$info_sv
+
+----
+
+This is an automated message from Helpdesk.
+
+$list_en
+
+$info_en
+
+Mvh
+DSV Helpdesk
+helpdesk@dsv.su.se
+08 - 16 16 48
+
+EOF;
+        $this->send_email($uid, $subject, $message);
+    }
+
+    private function run_reminders() {
+        $lastrun = $this->kvs->get_value('lastrun', 0);
+        $nextrun = $this->now
+                        ->setTimestamp($lastrun)
+                        ->add($this->run_interval);
+        if($nextrun > $this->now) {
             return;
         }
-        $this->kvs->set_key('lastrun', $this->now);
-        
+        $this->kvs->set_key('lastrun', $this->now->getTimestamp());
+
         $users = get_items('user');
         foreach($users as $user) {
             $this->check_loans($user);
@@ -27,89 +199,151 @@ class Cron {
     }
 
     private function check_loans($user) {
+        $expiring = $user->get_expiring_loans($this->warn_date);
         $overdue = $user->get_overdue_loans();
-        if($overdue) {
-            $this->send_reminder($user, $overdue);
+        if($expiring || $overdue) {
+            $this->send_reminder($user, $expiring, $overdue);
         }
     }
-    
-    private function send_reminder($user, $loans) {
-        $subject_template = "DSV Helpdesk: Du har ¤count¤ ¤late¤ lån";
-        $reminder_template_sv = "¤brand¤ ¤name¤, försenad sedan ¤due¤\n";
-        $reminder_template_en = "¤brand¤ ¤name¤, late since ¤due¤\n";
-        $message_template = <<<EOF
-Hej ¤name¤
 
-Vi vill påminna dig om att ditt lån har gått ut på följande ¤product_sv¤:
+    private function make_reminder_subject($num_expiring, $num_expired) {
+        $subject = $this->subject_prefix;
+        $messages = array();
+        if($num_expiring > 0) {
+            $messages[] = $num_expiring." utgående";
+        }
+        if($num_expired > 1) {
+            $messages[] = $num_expired." försenade";
+        } elseif($num_expired > 0) {
+            $messages[] = $num_expired." försenat";
+        }
+        return $subject.implode(" och ", $messages)." lån";
+    }
 
-¤list_sv¤
+    private function make_notice($lang, $type, $list) {
+        if(!$list) {
+            return '';
+        }
+        if(!array_key_exists($lang, $this->strings)) {
+            throw new Exception("Invalid languange: $lang");
+        }
+        $strings = $this->strings[$lang];
+        if(!array_key_exists($type, $strings)) {
+            throw new Exception("Invalid type: $type");
+        }
+        $strings = $strings[$type];
 
-Vänligen återlämna ¤it_sv¤ till Helpdesk så snart som möjligt, alternativt svara på det här meddelandet för att förlänga ¤loan_sv¤.
+        $lines = array();
+        foreach($list as $loan) {
+            $product = $loan->get_product();
+            $serial = $product->get_serial();
+            $brand = $product->get_brand();
+            $name = $product->get_name();
+            $endtime = format_date($loan->get_endtime());
+
+            $lines[] = "$brand $name, ".$strings['serial']
+                      ." $serial, ".$strings['expiry']." $endtime";
+        }
+
+        $msg = $strings['single'];
+        if(count($list) > 1) {
+            $msg = $strings['multi'];
+        }
+        return $msg."\n\n".implode("\n", $lines);
+    }
+
+    private function make_return_info($lang, $count) {
+        switch($lang) {
+            case 'sv':
+                if($count > 1) {
+                    $loan = "lånen";
+                    $product = "artiklarna";
+                } else {
+                    $loan = "lånet";
+                    $product = "artikeln";
+                }
+                return "Vänligen kontakta Helpdesk för att förlänga $loan eller lämna tillbaka $product.";
+                break;
+            case 'en':
+                if($count > 1) {
+                    $loan = "loans";
+                    $product = "items";
+                } else {
+                    $loan = "loan";
+                    $product = "item";
+                }
+                return "Please contact Helpdesk in order to extend the $loan or return the $product.";
+                break;
+            default:
+                throw new Exception("Invalid language: ".$lang);
+        }
+
+    }
+
+    private function send_reminder($user, $expiring, $overdue) {
+        $uid = $user->get_name();
+        $name = $this->ldap->get_firstname($uid);
+
+        $expiring_count = count($expiring);
+        $overdue_count = count($overdue);
+        $total = $expiring_count + $overdue_count;
+
+        $subject = $this->make_reminder_subject($expiring_count,
+                                                $overdue_count);
+
+        $info_sv = array();
+        $info_en = array();
+        if($expiring_count > 0) {
+            $info_sv[] = $this->make_notice('sv', 'expiring', $expiring);
+            $info_en[] = $this->make_notice('en', 'expiring', $expiring);
+        }
+        if($overdue_count > 0) {
+            $info_sv[] = $this->make_notice('sv', 'overdue', $overdue);
+            $info_en[] = $this->make_notice('en', 'overdue', $overdue);
+        }
+        $info_sv = implode("\n\n", $info_sv);
+        $returns_sv = $this->make_return_info('sv', $total);
+
+        $info_en = implode("\n\n", $info_en);
+        $returns_en = $this->make_return_info('en', $total);
+
+        $message = <<<EOF
+Hej $name!
+
+Det här är en automatisk påminnelse om lånade artiklar från Helpdesk.
+
+$info_sv
+
+$returns_sv
 
 ----
 
-We would like to remind you that your loan has expired on the following ¤product_en¤:
+This is an automated reminder regarding items on loan from Helpdesk.
 
-¤list_en¤
+$info_en
 
-Please return ¤it_en¤ to the Helpdesk as soon as possible, or reply to this message in order to extend the ¤loan_en¤.
+$returns_en
 
 Mvh
 DSV Helpdesk
 helpdesk@dsv.su.se
 08 - 16 16 48
+
 EOF;
+        $this->send_email($uid, $subject, $message);
+    }
 
-        $overdue_count = count($loans);
-        $reminder_list_sv = '';
-        $reminder_list_en = '';
-        $late = 'försenat';
-        $product_sv = 'artikel';
-        $product_en = 'product';
-        $it_sv = 'den';
-        $it_en = 'it';
-        $loan_sv = 'lånet';
-        $loan_en = 'loan';
-        if($overdue_count > 1) {
-            $late = 'försenade';
-            $product_sv = 'artiklar';
-            $product_en = 'products';
-            $it_sv = 'dem';
-            $it_en = 'them';
-            $loan_sv = 'lånen';
-            $loan_en = 'loans';
-        }
-        foreach($loans as $loan) {
-            $replacements = array('name'  => $loan->get_product()->get_name(),
-                                  'brand' => $loan->get_product()->get_brand(),
-                                  'due'   => format_date($loan->get_endtime()));
-            $reminder_list_sv .= replace($replacements, $reminder_template_sv);
-            $reminder_list_en .= replace($replacements, $reminder_template_en);
-        }
-
-        $subject = replace(array('count' => $overdue_count,
-                                 'late'  => $late), $subject_template);
-        $message = replace(array('name'       => $user->get_displayname(),
-                                 'list_sv'    => $reminder_list_sv,
-                                 'product_sv' => $product_sv,
-                                 'it_sv'      => $it_sv,
-                                 'loan_sv'    => $loan_sv,
-                                 'list_en'    => $reminder_list_en,
-                                 'product_en' => $product_en,
-                                 'it_en'      => $it_en,
-                                 'loan_en'    => $loan_en),
-                           $message_template);
-
+    private function send_email($uid, $subject, $message) {
         try {
-            mb_send_mail($user->get_email(),
+            mb_send_mail($this->ldap->get_user_email($uid),
                          $subject,
                          $message,
                          'From: '.$this->sender);
         } catch(Exception $e) {
+            error_log($e->getMessage());
             mb_send_mail($this->error,
-                         "Kunde inte skicka påminnelse",
-                         "Påminnelse kunde inte skickas till "
-                         .$user->get_name());
+                         "Kunde inte skicka mail",
+                         "Mail kunde inte skickas till ".$uid);
         }
     }
 }
diff --git a/include/Entity.php b/include/Entity.php
index 4edcd23..37c5f8e 100644
--- a/include/Entity.php
+++ b/include/Entity.php
@@ -1,27 +1,9 @@
 <?php
-class Entity {
+abstract class Entity {
     protected function __construct() {
         
     }
-    
-    protected function specify_search($searchterms, $searchfields) {
-        if(array_key_exists('fritext', $searchterms)) {
-            $freeterm = $searchterms['fritext'];
-            unset($searchterms['fritext']);
-            foreach($searchfields as $field) {
-                if(array_key_exists($field, $searchterms)) {
-                    $term = $searchterms[$field];
-                    if(is_array($term)) {
-                        $term[] = $freeterm;
-                    } else {
-                        $searchterms[$field] = array($term, $freeterm);
-                    }
-                } else {
-                    $searchterms[$field] = $freeterm;
-                }
-            }
-        }
-        return $searchterms;
-    }
+
+    abstract public function matches($term, $ldap);
 }
 ?>
diff --git a/include/Event.php b/include/Event.php
index 865a5d3..711bb5b 100644
--- a/include/Event.php
+++ b/include/Event.php
@@ -44,7 +44,7 @@ class Event {
         $event_id = $insert->insert_id;
         return new Event($event_id);
     }
-    
+
     public function __construct($id) {
         $search = prepare('select `id` from `event`
                            where `id`=?');
@@ -57,7 +57,7 @@ class Event {
         $this->id = $result['id'];
         $this->update_fields();
     }
-    
+
     protected function update_fields() {
         $get = prepare('select * from `event` where `id`=?');
         bind($get, 'i', $this->id);
@@ -83,7 +83,7 @@ class Event {
     public function get_returntime() {
         return $this->returntime;
     }
-    
+
     public function is_active() {
         if($this->returntime === null) {
             return true;
diff --git a/include/Kvs.php b/include/Kvs.php
index e80edf8..2900dcd 100644
--- a/include/Kvs.php
+++ b/include/Kvs.php
@@ -16,11 +16,11 @@ class Kvs {
         return array_keys($this->items);
     }
     
-    public function get_value($key) {
+    public function get_value($key, $default=null) {
         if(isset($this->items[$key])) {
             return $this->items[$key];
         }
-        return null;
+        return $default;
     }
     
     public function set_key($key, $value) {
diff --git a/include/Ldap.php b/include/Ldap.php
index 91c7c84..f43d7f9 100644
--- a/include/Ldap.php
+++ b/include/Ldap.php
@@ -2,7 +2,7 @@
 class Ldap {
     private $conn;
     private $base_dn = "dc=su,dc=se";
-    
+
     public function __construct() {
         $this->conn = ldap_connect('ldaps://ldap.su.se');
         ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3);
@@ -13,32 +13,36 @@ class Ldap {
         $result = ldap_search($this->conn, $this->base_dn, $term, $attributes);
         return ldap_get_entries($this->conn, $result);
     }
-    
-    public function get_user($uid) {
-        $data = $this->search("uid=$uid", 'cn', 'uid');
+
+    public function get_attribute($uid, $attribute) {
+        $data = $this->search("uid=$uid", $attribute);
         if($data['count'] !== 1) {
-            throw new Exception("LDAP search for '$uid' did not return exactly one result");
+            $err = "LDAP search for '$uid' did not return exactly one result";
+            throw new Exception($err);
         }
-        return $data[0]['cn'][0];
+        return $data[0][strtolower($attribute)][0];
+    }
+
+    public function get_user($uid) {
+        return $this->get_attribute($uid, 'cn');
+    }
+
+    public function get_firstname($uid) {
+        return $this->get_attribute($uid, 'givenName');
     }
 
     public function get_user_email($uid) {
-        $data = $this->search("uid=$uid", 'mail', 'uid');
-        if($data['count'] !== 1) {
-            throw new Exception("LDAP search for '$uid' did not return exactly one result");
-        }
-        return $data[0]['mail'][0];
+        return $this->get_attribute($uid, 'mail');
     }
 
-    public function search_user($uid) {
-        $data = $this->search("uid=$uid", 'cn', 'uid');
+    public function search_email($email) {
+        $data = $this->search("mail=$email", 'mail', 'uid');
         $out = array();
-        foreach($data as $result) {
-            if(isset($result['uid'])) {
-                $out[$result['uid'][0]] = $result['cn'][0];
-            }
+        if($data['count'] !== 1) {
+            $err = "LDAP search for '$email' did not return exactly one result.";
+            throw new Exception($err);
         }
-        return $out;
+        return $data[0]['uid'][0];
     }
 }
 ?>
diff --git a/include/Loan.php b/include/Loan.php
index 9bf38c8..26254ed 100644
--- a/include/Loan.php
+++ b/include/Loan.php
@@ -12,10 +12,12 @@ class Loan extends Event {
         $endtime .= '13:00';
         bind($insert, 'iii', $event_id, $user->get_id(), strtotime($endtime));
         execute($insert);
+        $loan = new Loan($event_id);
+        $loan->queue_receipt($user);
         commit_trans();
-        return new Loan($event_id);
+        return $loan;
     }
-    
+
     public function __construct($id) {
         parent::__construct($id);
         $search = prepare('select * from `loan` where `event`=?');
@@ -27,17 +29,40 @@ class Loan extends Event {
         }
         $this->update_fields();
     }
-    
+
     protected function update_fields() {
         parent::update_fields();
         $get = prepare('select * from `loan` where `event`=?');
-        bind($get, 'i', $this->id);
+        bind($get, 'i', $this->get_id());
         execute($get);
         $loan = result_single($get);
         $this->user = $loan['user'];
         $this->endtime = $loan['endtime'];
     }
 
+    protected function queue_receipt($user) {
+        $now = time();
+        $sendtime = $now + 3600;
+        $pending = prepare('select * from `pending_receipt` where `user` = ?
+                                and `since_time` < ? and `send_time` > ?');
+        bind($pending, 'iii', $user->get_id(), $now, $now);
+        execute($pending);
+        $result = result_single($pending);
+        if($result === null) {
+            $add = prepare('insert into `pending_receipt`
+                                (`user`, `since_time`, `send_time`)
+                                values(?, ?, ?)');
+            bind($add, 'iii', $user->get_id(), $now, $sendtime);
+            execute($add);
+        } else {
+            $update = prepare('update `pending_receipt` set `send_time` = ?
+                               where `user` = ? and `since_time` < ?
+                                   and `send_time` > ?');
+            bind($update, 'iiii', $sendtime, $user->get_id(), $now, $now);
+            execute($update);
+        }
+    }
+
     public function get_user() {
         return new User($this->user);
     }
@@ -47,18 +72,40 @@ class Loan extends Event {
     }
 
     public function extend($time) {
+        $oldend = $this->get_endtime();
+        $now = time();
         $ts = strtotime($time . ' 13:00');
-        $query = prepare('update `loan` set `endtime`=? where `event`=?');
-        bind($query, 'ii', $ts, $this->id);
-        execute($query);
+
+        begin_trans();
+        $extend = prepare('update `loan` set `endtime`=? where `event`=?');
+        bind($extend, 'ii', $ts, $this->get_id());
+        execute($extend);
+
+        $log = prepare('insert into `loan_extension`
+                            (`loan`, `extend_time`, `old_end`, `new_end`)
+                            values (?, ?, ?, ?)');
+        bind($log, 'iiii', $this->get_id(), $now, $oldend, $ts);
+        execute($log);
+
+        $this->queue_receipt($this->get_user());
+
         $this->endtime = $ts;
+        commit_trans();
         return true;
     }
-    
+
+    public function get_last_extension() {
+        $select = prepare('select max(`extend_time`) as `extend_time`
+                           from `loan_extension` where `loan`=?');
+        bind($select, 'i', $this->get_id());
+        execute($select);
+        return result_single($select)['extend_time'];
+    }
+
     public function end() {
         $now = time();
         $query = prepare('update `event` set `returntime`=? where `id`=?');
-        bind($query, 'ii', $now, $this->id);
+        bind($query, 'ii', $now, $this->get_id());
         execute($query);
         $this->returntime = $now;
         return true;
@@ -75,6 +122,18 @@ class Loan extends Event {
         return false;
     }
 
+    public function expires_before($datetime) {
+        if($this->returntime !== null) {
+            return false;
+        }
+        $endtime = new DateTime();
+        $endtime->setTimestamp($this->endtime);
+        if(!$this->is_overdue() && $endtime < $datetime) {
+            return true;
+        }
+        return false;
+    }
+
     public function get_status() {
         if($this->is_overdue()) {
             return 'overdue_loan';
diff --git a/include/NewPage.php b/include/NewPage.php
index ffe6a48..dd0e15e 100644
--- a/include/NewPage.php
+++ b/include/NewPage.php
@@ -49,7 +49,7 @@ class NewPage extends Page {
                               'info' => $fields,
                               'label' => '',
                               'hidden' => 'hidden'),
-                        $this->fragments['product_details']);
+                        $this->fragments['product_form']);
         return $out;
     }
 }
diff --git a/include/Page.php b/include/Page.php
index b783547..7c4060a 100644
--- a/include/Page.php
+++ b/include/Page.php
@@ -1,7 +1,7 @@
 <?php
 abstract class Page extends Responder {
     protected abstract function render_body();
-    
+
     protected $page = 'checkout';
     protected $title = "DSV Utlåning";
     protected $subtitle = '';
@@ -15,11 +15,11 @@ abstract class Page extends Responder {
                                  'history' => 'Historik',
                                  'search' => 'Sök');
     private $template_parts = array();
-    
+
     public function __construct() {
         parent::__construct();
         $this->template_parts = get_fragments('./html/base.html');
-        
+
         if(isset($_GET['page'])) {
             $this->page = $_GET['page'];
         }
@@ -27,7 +27,7 @@ abstract class Page extends Responder {
             $this->subtitle = $this->menuitems[$this->page];
         }
     }
-    
+
     public function render() {
         $this->render_head();
         $this->render_body();
@@ -36,7 +36,7 @@ abstract class Page extends Responder {
         }
         $this->render_foot();
     }
-    
+
     final private function render_head() {
         $headtitle = $this->title;
         $pagetitle = $this->title;
@@ -83,7 +83,7 @@ abstract class Page extends Responder {
                             'message' => $this->error),
                       $this->fragments['message']));
     }
-    
+
     final private function render_foot() {
         print($this->template_parts['foot']);
     }
@@ -103,7 +103,7 @@ abstract class Page extends Responder {
                 $replacements['has_notes'] = '*';
             }
             $userlink = replace(array('id' => $user->get_id(),
-                                      'name' => $user->get_displayname(),
+                                      'name' => $user->get_displayname($this->ldap),
                                       'page' => 'users'),
                                 $this->fragments['item_link']);
             $replacements['item_link'] = $userlink;
@@ -131,50 +131,75 @@ abstract class Page extends Responder {
     final protected function build_product_table($products) {
         $rows = '';
         foreach($products as $product) {
-            $prodlink = replace(array('id' => $product->get_id(),
-                                      'name' => $product->get_name(),
-                                      'page' => 'products'),
-                                $this->fragments['item_link']);
-            $note = 'Tillgänglig';
-            $status = $product->get_status();
-            switch($status) {
-                case 'discarded':
-                    $discarded = format_date($product->get_discardtime());
-                    $note = 'Skrotad '.$discarded;
-                    break;
-                case 'service':
-                    $service = $product->get_active_service();
-                    $note = 'På service sedan '
-                                .format_date($service->get_starttime());
-                    break;
-                case 'on_loan':
-                case 'overdue':
-                    $loan = $product->get_active_loan();
-                    $user = $loan->get_user();
-                    $replacements = array('name' => $user->get_displayname(),
-                                          'id'   => $user->get_id(),
-                                          'page' => 'users');
-                    $userlink = replace($replacements,
-                                        $this->fragments['item_link']);
-                    $note = 'Utlånad till '.$userlink;
-                    if($loan->is_overdue()) {
-                        $note .= ', försenad';
-                    } else {
-                        $note .= ', slutdatum '
-                                .format_date($loan->get_endtime());
-                    }
-                    break;
-            }
-            $rows .= replace(array('status' => $status,
-                                   'item_link' => $prodlink,
-                                   'serial' => $product->get_serial(),
-                                   'note' => $note),
-                             $this->fragments['product_row']);
+            $rows .= $this->build_product_row($product);
         }
-        return replace(array('rows' => $rows),
+        return replace(array('rows' => $rows,
+                             'type' => 'single'),
                        $this->fragments['product_table']);
     }
 
+    final protected function build_product_row($product, $matches = null) {
+        $prodlink = replace(array('id' => $product->get_id(),
+                                  'name' => $product->get_name(),
+                                  'page' => 'products'),
+                            $this->fragments['item_link']);
+        $note = 'Tillgänglig';
+        $status = $product->get_status();
+        switch($status) {
+        case 'discarded':
+            $discarded = format_date($product->get_discardtime());
+            $note = 'Skrotad '.$discarded;
+            break;
+        case 'service':
+            $service = $product->get_active_service();
+            $note = 'På service sedan '
+                  .format_date($service->get_starttime());
+            break;
+        case 'on_loan':
+        case 'overdue':
+            $loan = $product->get_active_loan();
+            $user = $loan->get_user();
+            $replacements = array('name' => $user->get_displayname($this->ldap),
+                                  'id'   => $user->get_id(),
+                                  'page' => 'users');
+            $userlink = replace($replacements,
+                                $this->fragments['item_link']);
+            $note = 'Utlånad till '.$userlink;
+            if($loan->is_overdue()) {
+                $note .= ', försenad';
+            } else {
+                $note .= ', slutdatum '
+                      .format_date($loan->get_endtime());
+            }
+            break;
+        }
+        $out = replace(array('status'    => $status,
+                             'item_link' => $prodlink,
+                             'serial'    => $product->get_serial(),
+                             'note'      => $note),
+                       $this->fragments['product_row']);
+        if($matches) {
+            $details = $this->build_product_details($product, $matches);
+            $out .= replace(array('status'  => $status,
+                                  'details' => $details),
+                            $this->fragments['product_detail_row']);
+        }
+        return $out;
+    }
+
+    final protected function build_product_details($product, $matches) {
+        $out = '';
+        foreach($matches as $name => $value) {
+            if(is_array($value)) {
+                $value = implode(', ', $value);
+            }
+            $out .= replace(array('name'   => $product->get_label($name),
+                                  'value'  => $value),
+                            $this->fragments['product_detail']);
+        }
+        return $out;
+    }
+
     final protected function build_user_loan_table($loans) {
         $rows = '';
         foreach($loans as $loan) {
@@ -186,7 +211,7 @@ abstract class Page extends Responder {
             $status = $loan->get_status();
             $note = '';
             if($status !== 'inactive_loan') {
-                $extend = format_date(default_loan_end(time()));
+                $extend = format_date($loan->get_endtime());
                 $note = replace(array('id' => $product->get_id(),
                                       'end_new' => $extend),
                                 $this->fragments['loan_extend_form']);
@@ -237,7 +262,7 @@ abstract class Page extends Responder {
                 }
             } else if($event instanceof Loan) {
                 $user = $event->get_user();
-                $userlink = replace(array('name' => $user->get_displayname(),
+                $userlink = replace(array('name' => $user->get_displayname($this->ldap),
                                           'id' => $user->get_id(),
                                           'page' => 'users'),
                                     $this->fragments['item_link']);
@@ -260,7 +285,8 @@ abstract class Page extends Responder {
                                    'note' => $note),
                              $this->fragments['product_row']);
         }
-        return replace(array('rows' => $rows),
+        return replace(array('rows' => $rows,
+                             'type' => 'single'),
                        $this->fragments['product_table']);
 
     }
diff --git a/include/Product.php b/include/Product.php
index d84ec1d..b6f1478 100644
--- a/include/Product.php
+++ b/include/Product.php
@@ -68,6 +68,13 @@ class Product extends Entity {
         $this->update_fields();
         $this->update_info();
         $this->update_tags();
+
+        # Global variables are bad, but passing these email properties
+        # around everywhere would be worse
+        global $sender, $notify_discard, $email_subject_prefix;
+        $this->discard_email_address = $notify_discard;
+        $this->email_sender = $sender;
+        $this->email_subject_prefix = $email_subject_prefix;
     }
     
     private function update_fields() {
@@ -110,34 +117,108 @@ class Product extends Entity {
         return true;
     }
 
-    public function matches($terms, $matchAll=false) {
-        $terms = $this->specify_search($terms, array('brand',
-                                                     'name',
-                                                     'serial',
-                                                     'invoice',
-                                                     'status',
-                                                     'tag'));
+    /*
+      Return a list of field-value mappings containing all matching search terms.
+     */
+    public function matches($terms, $ldap) {
         $matches = array();
-        foreach($terms as $field => $values) {
-            if(property_exists($this, $field)) {
-                if(match($values, $this->$field)) {
-                    $matches[$field] = $this->$field;
-                }
-            } else if(array_key_exists($field, $this->get_info())) {
-                if(match($values, $this->get_info()[$field])) {
-                    $matches[$field] = $this->get_info()[$field];
-                }
-            } else if($field == 'tag') {
-                foreach($this->get_tags() as $tag) {
-                    if(match($values, $tag)) {
-                        $matches['tags'] = $this->get_tags();
-                        break;
+
+        // Create a list mapping all basic fields to getters
+        $fields = array('brand' => 'get_brand',
+                        'name' => 'get_name',
+                        'invoice' => 'get_invoice',
+                        'serial' => 'get_serial',
+                        'status' => 'get_status');
+
+        foreach($terms as $term) {
+            $key = $term->get_key();
+            $matched = false;
+            switch($key) {
+                case 'brand':
+                case 'name':
+                case 'invoice':
+                case 'serial':
+                case 'status':
+                    // If $key is a standard field, check against its value
+                    $getter = $fields[$key];
+                    $value = $this->$getter();
+                    if(match($term, $value)) {
+                        //Record a successful match
+                        $matches[$key] = $value;
+                        $matched = true;
                     }
-                }
-            } else if($field == 'status') {
-                if(match($values, $this->get_status())) {
-                    $matches['status'] = $this->get_status();
-                }
+                    break;
+                case 'tag':
+                    // If $key is tag, iterate over the tags
+                    $matched_tags = $this->match_tags($term);
+                    if($matched_tags) {
+                        // Record a successful match
+                        $matched = true;
+                        if(!isset($matches['tags'])) {
+                            // This is the first list of matching tags
+                            $matches['tags'] = $matched_tags;
+                        } else {
+                            // Merge these results with existing results
+                            $matches['tags'] = array_unique(
+                                array_merge($matches['tags'],
+                                            $matched_tags));
+                        }
+                    }
+                    break;
+                case 'fritext':
+                    // if $key is fritext:
+                    // First check basic fields
+                    foreach($fields as $field => $getter) {
+                        $value = $this->$getter();
+                        if(match($term, $value)) {
+                            $matches[$field] = $value;
+                            $matched = true;
+                        }
+                    }
+                    // Then tags
+                    $matched_tags = $this->match_tags($term);
+                    if($matched_tags) {
+                        $matched = true;
+                        if(!isset($matches['tags'])) {
+                            $matches['tags'] = $matched_tags;
+                        } else {
+                            $matches['tags'] = array_unique(
+                                array_merge($matches['tags'],
+                                            $matched_tags));
+                        }
+                    }
+                    // Then custom fields
+                    foreach($this->get_info() as $field => $value) {
+                        if(match($term, $value)) {
+                            //Record a successful match
+                            $matches[$field] = $value;
+                            $matched = true;
+                        }
+                    }
+                    break;
+                default:
+                    // Handle custom fields
+                    $info = $this->get_info();
+                    if(isset($info[$key])) {
+                        // If $key is a valid custom field on this product
+                        $value = $info[$key];
+                        if(match($term, $value)) {
+                            //Record a successful match
+                            $matches[$key] = $value;
+                            $matched = true;
+                        }
+                    }
+                    break;
+            }
+            // If a mandatory match failed, the entire search has failed
+            // and we return an empty result.
+            if($term->is_mandatory() && !$matched) {
+                return array();
+            }
+            // If a negative match succeeded, the entire search has failed
+            // and we return an empty result.
+            if($term->is_negative() && $matched) {
+                return array();
             }
         }
         if($matchAll && array_diff_assoc($terms, $matches)) {
@@ -146,6 +227,40 @@ class Product extends Entity {
         return $matches;
     }
 
+    private function match_tags($term) {
+        $tags = $this->get_tags();
+        $matches = array();
+        foreach($tags as $tag) {
+            if(match($term, $tag)) {
+                $matches[] = $tag;
+            }
+        }
+        return $matches;
+    }
+
+    public function get_label($name) {
+        switch($name) {
+            case 'brand':
+                return 'Tillverkare';
+                break;
+            case 'name':
+                return 'Namn';
+                break;
+            case 'invoice':
+                return 'Fakturanummer';
+                break;
+            case 'serial':
+                return 'Serienummer';
+                break;
+            case 'tags':
+                return 'Taggar';
+                break;
+            default:
+                return ucfirst($name);
+                break;
+        }
+    }
+
     public function get_id() {
         return $this->id;
     }
@@ -167,9 +282,42 @@ class Product extends Entity {
         bind($update, 'ii', $now, $this->id);
         execute($update);
         $this->discardtime = $now;
+        if($this->discard_email_address) {
+            $this->send_discard_email();
+        }
         return true;
     }
-    
+
+    private function send_discard_email() {
+        $brand = $this->brand;
+        $name = $this->name;
+        $invoice = $this->invoice;
+        $serial = $this->serial;
+        $discardtime = format_date($this->discardtime);
+
+        $subject = $this->email_subject_prefix.$brand.' '.$name.' skrotad';
+        $message = <<<EOF
+Hej!
+
+Följande artikel har skrotats i Boka:
+
+$brand $name, serienummer: $serial, fakturanummer: $invoice
+
+EOF;
+        try {
+            mb_send_mail($this->discard_email_address,
+                         $subject,
+                         $message,
+                         'From: '.$this->email_sender);
+        } catch(Exception $e) {
+            error_log($e->getMessage());
+            mb_send_mail($this->error,
+                         "Kunde inte skicka mail",
+                         "Mail kunde inte skickas till "
+                          . $this->discard_email_address);
+        }
+    }
+
     public function toggle_service() {
         $status = $this->get_status();
         $now = time();
diff --git a/include/ProductPage.php b/include/ProductPage.php
index e385f62..bef9807 100644
--- a/include/ProductPage.php
+++ b/include/ProductPage.php
@@ -38,12 +38,12 @@ class ProductPage extends Page {
                               $this->fragments['product_page']));
                 break;
             case 'show':
-                print($this->build_product_details());
+                print($this->build_product_form());
                 break;
         }
     }
     
-    private function build_product_details() {
+    private function build_product_form() {
         $info = '';
         foreach($this->product->get_info() as $key => $value) {
             $info .= replace(array('name' => ucfirst($key),
@@ -67,21 +67,29 @@ class ProductPage extends Page {
                         'tags'        => $tags,
                         'info'        => $info,
                         'label'       => '',
-                        'hidden'      => 'hidden',
+                        'label_hidden' => 'hidden',
+                        'checkout_hidden'      => 'hidden',
+                        'hidden'      => '',
                         'service'     => 'Starta service',
                         'history'     => $history,
-                        'attachments' => $attachments);
+                        'attachments' => $attachments,
+                        'end'         => format_date(default_loan_end(time())));
         if(class_exists('QRcode')) {
             $fields['label'] = replace($fields,
                                        $this->fragments['product_label']);
         }
         if(!$this->product->get_discardtime()) {
-            $fields['hidden'] = '';
+            $fields['label_hidden'] = '';
             if($this->product->get_status() == 'service') {
                 $fields['service'] = 'Avsluta service';
             }
+            if($this->product->get_status() == 'available') {
+                $fields['checkout_hidden'] = '';
+            }
         }
-        return replace($fields, $this->fragments['product_details']);
+        $out = replace($fields, $this->fragments['product_form']);
+        $out .= replace($fields, $this->fragments['product_meta']);
+        return $out;
     }
 
     private function build_history_table($history) {
diff --git a/include/Responder.php b/include/Responder.php
index 6d05f51..ff621cd 100644
--- a/include/Responder.php
+++ b/include/Responder.php
@@ -1,9 +1,11 @@
 <?php
 abstract class Responder {
     protected $fragments = array();
+    protected $ldap = null;
     
     public function __construct() {
         $this->fragments = get_fragments('./html/fragments.html');
+        $this->ldap = new Ldap();
     }
     
     final protected function escape_tags($tags) {
diff --git a/include/SearchPage.php b/include/SearchPage.php
index e1a3f8e..1d47bf3 100644
--- a/include/SearchPage.php
+++ b/include/SearchPage.php
@@ -37,16 +37,33 @@ class SearchPage extends Page {
         return $out;
     }
 
+    private function search($type, $terms) {
+        $matches = array();
+        foreach(get_items($type) as $item) {
+            if($result = $item->matches($terms, $this->ldap)) {
+                $matches[] = array($item, $result);
+            }
+        }
+        return $matches;
+    }
+
     private function translate_terms($terms) {
         $matches = array();
+
+        // If there is a q-query
+        // and it contains a : character
         if(isset($terms['q']) && preg_match('/([^:]+):(.*)/',
                                             $terms['q'],
                                             $matches)) {
+            // remove the q key
             unset($terms['q']);
+            // insert the term, using whatever came before
+            // the : as the key and whatever came after as the value
             $terms[$matches[1]] = $matches[2];
         }
         $translated = array();
-        foreach($terms as $key => $value) {
+        // Translate all keys into a standard format
+        foreach($terms as $key => $values) {
             $newkey = $key;
             switch($key) {
                 case 'q':
@@ -67,17 +84,38 @@ class SearchPage extends Page {
                     $newkey = 'serial';
                     break;
                 case 'tagg':
+                case 'tags':
                     $newkey = 'tag';
                     break;
+                case 'anteckning':
+                    $newkey = 'note';
+                    break;
+                case 'e-post':
+                case 'epost':
+                case 'mail':
+                    $newkey = 'email';
+                    break;
                 case 'status':
-                    $value = $this->translate_values($value);
+                    // Translate all status values into a standard format
+                    $values = $this->translate_values($values);
                     break;
             }
-            if(!array_key_exists($newkey, $translated)) {
-                $translated[$newkey] = $value;
-            } else {
-                $temp = $translated[$newkey];
-                $translated[$newkey] = array_merge((array)$temp, (array)$value);
+            // Wrap the value in an array if it isn't one
+            if(!is_array($values)) {
+                $values = array($values);
+            }
+            // Make a SearchTerm object from each term
+            foreach($values as $value) {
+                // Check for flags
+                $flag = SearchTerm::OPTIONAL;
+                if(in_array($value[0], array(SearchTerm::MANDATORY,
+                                             SearchTerm::OPTIONAL,
+                                             SearchTerm::NEGATIVE))) {
+                    $flag = $value[0];
+                    $value = substr($value, 1);
+                }
+                // Collect the new SearchTerm
+                $translated[] = new SearchTerm($newkey, $value, $flag);
             }
         }
         return $translated;
@@ -127,79 +165,96 @@ class SearchPage extends Page {
         }
         return $translated;
     }
-
-    private function search($type, $terms) {
-        $items = get_items($type);
-        $out = array();
-        foreach($items as $item) {
-            $result = $item->matches($terms);
-            if($result) {
-                $out[] = array($item, $result);
-            }
-        }
-        return $out;
-    }
     
     protected function render_body() {
         $hidden = 'hidden';
         $terms = '';
         if($this->terms) {
             $hidden = '';
-            foreach($this->terms as $key => $value) {
-                if(!is_array($value)) {
-                    $value = array($value);
-                }
-                foreach($value as $item) {
-                    $terms .= replace(array('term' => ucfirst($key).": $item",
-                                            'key' => $key,
-                                            'value' => $item),
-                                      $this->fragments['search_term']);
-                }
+            foreach($this->terms as $term) {
+                $key = $term->get_key();
+                $flag = $term->get_flag();
+                $query = $term->get_query();
+                $fullterm = ucfirst($key).": ".$flag.$query;
+                $terms .= replace(array('term' => $fullterm,
+                                        'key' => $key,
+                                        'value' => $flag.$query),
+                                  $this->fragments['search_term']);
             }
         }
-        $products = 'Inga artiklar hittade.';
+        $prod_table = 'Inga artiklar hittade.';
         if($this->product_hits) {
             $products = '';
             foreach($this->product_hits as $hit) {
-                $products .= $this->render_product($hit[0], $hit[1]);
+                $products .= $this->build_product_row($hit[0], $hit[1]);
             }
+            $prod_table = replace(array('rows' => $products,
+                                        'type' => 'double'),
+                                  $this->fragments['product_table']);
         }
-        $users = 'Inga användare hittade.';
+        $user_table = 'Inga användare hittade.';
         if($this->user_hits) {
-            $users = '';
+            $users = array();
             foreach($this->user_hits as $hit) {
-                $users .= $this->render_user($hit[0], $hit[1]);
+                $users[] = $hit[0];
             }
+            $user_table = $this->build_user_table($users);
         }
+
         print(replace(array('terms' => $terms,
                             'hidden' => $hidden,
-                            'product_results' => $products,
-                            'user_results' => $users),
+                            'product_results' => $prod_table,
+                            'user_results' => $user_table),
                       $this->fragments['search_form']));
     }
+}
 
-    private function render_product($product, $matches) {
-        $link = replace(array('id' => $product->get_id(),
-                              'name' => $product->get_name(),
-                              'page' => 'products'),
-                        $this->fragments['item_link']);
+class SearchTerm {
+    public const MANDATORY = '+';
+    public const OPTIONAL = '~';
+    public const NEGATIVE = '-';
 
-        $data =  print_r($matches, true);
-        
-        return $link . '<br/>'
-             . $data . '<br/>';
+    private $key;
+    private $query;
+    private $flag;
+
+    public function __construct($key, $query, $flag=SearchTerm::OPTIONAL) {
+        $this->key = $key;
+        $this->query = $query;
+        $this->flag = $flag;
     }
 
-    private function render_user($user, $matches) {
-        $link = replace(array('id' => $user->get_id(),
-                              'name' => $user->get_name(),
-                              'page' => 'users'),
-                        $this->fragments['item_link']);
+    public function get_key() {
+        return $this->key;
+    }
 
-        $data =  print_r($matches, true);
-        
-        return $link . '<br/>'
-             . $data . '<br/>';
+    public function get_query() {
+        return $this->query;
+    }
+
+    public function get_flag() {
+        return $this->flag;
+    }
+
+    public function is_optional() {
+        if($this->flag == SearchTerm::OPTIONAL) {
+            return true;
+        }
+        return false;
+    }
+
+    public function is_mandatory() {
+        if($this->flag == SearchTerm::MANDATORY) {
+            return true;
+        }
+        return false;
+    }
+
+    public function is_negative() {
+        if($this->flag == SearchTerm::NEGATIVE) {
+            return true;
+        }
+        return false;
     }
 }
 ?>
diff --git a/include/User.php b/include/User.php
index 18a619d..ba218b4 100644
--- a/include/User.php
+++ b/include/User.php
@@ -3,8 +3,7 @@ class User extends Entity {
     private $id = 0;
     private $name = '';
     private $notes = '';
-    private $ldap = null;
-    
+
     public static function create_user($name) {
         $ins_user = prepare('insert into `user`(`name`) values (?)');
         bind($ins_user, 's', $name);
@@ -34,9 +33,8 @@ class User extends Entity {
         }
         $this->id = $id;
         $this->update_fields();
-        $this->ldap = new Ldap();
     }
-    
+
     private function update_fields() {
         $get = prepare('select * from `user` where `id`=?');
         bind($get, 'i', $this->id);
@@ -47,51 +45,75 @@ class User extends Entity {
         return true;
     }
 
-    public function matches($terms, $matchAll=false) {
-        $terms = $this->specify_search($terms, array('name',
-                                                     'email',
-                                                     'notes'));
+    public function matches($terms, $ldap) {
         $matches = array();
-        foreach($terms as $field => $values) {
-            switch($field) {
+        foreach($terms as $term) {
+            // Iterate over the terms
+            $matched = false;
+            $key = $term->get_key();
+            switch($key) {
                 case 'name':
-                    if(match($values, $this->name)) {
-                        $matches['name'] = $this->name;
+                    // If the key is name, check username and displayname
+                    $name = $this->get_name();
+                    if(match($term, $name)) {
+                        $matches['name'] = $name;
+                        $matched = true;
                     }
-                    if(match($values, $this->get_displayname())) {
-                        $matches['displayname'] = $this->get_displayname();
+                    $dname = $this->get_displayname($ldap);
+                    if(match($term, $dname)) {
+                        $matches['displayname'] = $dname;
+                        $matched = true;
+                    }
+                    break;
+                case 'note':
+                    // If the key is note, check it.
+                    $note = $this->get_note();
+                    if($note && match($term, $note)) {
+                        $matches['note'] = $note;
+                        $matched = true;
                     }
                     break;
                 case 'email':
-                    if($this->get_email(false) && match($values,
-                                                        $this->get_email())) {
-                        $matches['email'] = $this->get_email();
+                    $email = $this->get_email($ldap, false);
+                    if($email && match($term, $email)) {
+                        $matches['email'] = $email;
+                        $matched = true;
                     }
                     break;
-                case 'notes':
-                    if(match($values, $this->notes)) {
-                        $matches['notes'] = $this->notes;
+                case 'fritext':
+                    //Check everything if the key is fritext
+                    $name = $this->get_name();
+                    if(match($term, $name)) {
+                        $matches['name'] = $name;
+                        $matched = true;
+                    }
+                    $dname = $this->get_displayname($ldap);
+                    if(match($term, $dname)) {
+                        $matches['displayname'] = $dname;
+                        $matched = true;
                     }
-                    break;
             }
-        }
-        if($matchAll && array_diff_assoc($terms, $matches)) {
-            return array();
+            if($term->is_mandatory() && !$matched) {
+                return array();
+            }
+            if($term->is_negative() && $matched) {
+                return array();
+            }
         }
         return $matches;
     }
 
-    public function get_displayname() {
+    public function get_displayname($ldap) {
         try {
-            return $this->ldap->get_user($this->name);
+            return $ldap->get_user($this->name);
         } catch(Exception $e) {
             return 'Ej i SUKAT';
         }
     }
 
-    public function get_email($format = true) {
+    public function get_email($ldap, $format = true) {
         try {
-            return $this->ldap->get_user_email($this->name);
+            return $ldap->get_user_email($this->name);
         } catch(Exception $e) {
             if($format) {
                 return 'Mailadress saknas';
@@ -103,11 +125,11 @@ class User extends Entity {
     public function get_id() {
         return $this->id;
     }
-    
+
     public function get_name() {
         return $this->name;
     }
-    
+
     public function set_name($newname) {
         $update = prepare('update `user` set `name`=? where `id`=?');
         bind($update, 'si', $newname, $this->id);
@@ -119,7 +141,7 @@ class User extends Entity {
     public function get_notes() {
         return $this->notes;
     }
-    
+
     public function set_notes($newnotes) {
         $update = prepare('update `user` set `notes`=? where `id`=?');
         bind($update, 'si', $newnotes, $this->id);
@@ -129,7 +151,7 @@ class User extends Entity {
     }
 
     public function get_loans($type = 'both') {
-        $statement = "select `id` from `event` 
+        $statement = "select `id` from `event`
                       left join `loan` on `event`.`id` = `loan`.`event`
                       where
                           `type`='loan' and `user`=?";
@@ -167,5 +189,15 @@ class User extends Entity {
         }
         return $overdue;
     }
+
+    public function get_expiring_loans($end_date) {
+        $expiring = array();
+        foreach($this->get_loans('active') as $loan) {
+            if($loan->expires_before($end_date)) {
+                $expiring[] = $loan;
+            }
+        }
+        return $expiring;
+    }
 }
 ?>
diff --git a/include/UserPage.php b/include/UserPage.php
index ab1b544..c5ff4e1 100644
--- a/include/UserPage.php
+++ b/include/UserPage.php
@@ -56,7 +56,7 @@ class UserPage extends Page {
                              'inactive_loans' => $table_inactive,
                              'id' => $this->user->get_id(),
                              'name' => $this->user->get_name(),
-                             'displayname' => $this->user->get_displayname(),
+                             'displayname' => $this->user->get_displayname($this->ldap),
                              'notes' => $this->user->get_notes()),
                        $this->fragments['user_details']);
     }
diff --git a/include/functions.php b/include/functions.php
index 099b4bb..b85f1b3 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -253,18 +253,23 @@ function suggest_content($fieldname) {
     return $out;
 }
 
-function match($searchterms, $subject) {
-    if(!is_array($searchterms)) {
-        $searchterms = array($searchterms);
-    }
-    foreach($searchterms as $term) {
-        if(fnmatch('*'.$term.'*', $subject, FNM_CASEFOLD)) {
-            return true;
-        }
+function match($term, $subject) {
+    if(fnmatch('*'.$term->get_query().'*', $subject, FNM_CASEFOLD)) {
+        return true;
     }
     return false;
 }
 
+function match_tags($searchterm, $tags) {
+    $found = array();
+    foreach($tags as $tag) {
+        if(fnmatch('*'.$tag.'*', $searchterm, FNM_CASEFOLD)) {
+            $found[] = $tag;
+        }
+    }
+    return $found;
+}
+
 function format_date($date) {
     if($date) {
         return gmdate('Y-m-d', $date);
diff --git a/script.js b/script.js
index a708d23..3478b9a 100644
--- a/script.js
+++ b/script.js
@@ -97,7 +97,7 @@ function getFragment(name, callback) {
             console.log(result);
         }
     }
-    
+
     var data = new FormData()
     data.append('fragment', name)
     ajaxRequest('getfragment', data, unpack)
@@ -483,6 +483,7 @@ function calendar(event) {
     if(!input.cal) {
         var cal = new dhtmlXCalendarObject(input.id)
         cal.hideTime()
+        cal.setDate(input.value)
         input.cal = cal
         cal.show()
     }
@@ -573,3 +574,27 @@ function showFile(event) {
     var filefield = event.currentTarget.parentNode.filename
     filefield.value = event.currentTarget.files[0].name
 }
+
+function loanLength(event, length, unit) {
+    event.preventDefault()
+    var end = document.getElementById('end')
+    var enddate = new Date()
+    switch(unit) {
+    case 'day':
+        enddate.setDate(enddate.getDate() + length)
+        break
+    case 'year':
+        enddate.setFullYear(enddate.getFullYear() + length)
+        break;
+    }
+    // javascript zero-indexes months because of course
+    var month = enddate.getMonth() + 1
+    if(month < 10) {
+        month = '0' + month
+    }
+    var day = enddate.getDate()
+    if(day < 10) {
+        day = '0' + day
+    }
+    end.value = enddate.getFullYear() + '-' + month + '-' + day
+}
diff --git a/style.css b/style.css
index c1ec303..d3eeec8 100644
--- a/style.css
+++ b/style.css
@@ -1,3 +1,7 @@
+body {
+    font-size: 90%;
+}
+
 textarea {
     height: 80px;
 }
@@ -104,10 +108,25 @@ tbody tr {
     background-color: #d7e0eb;
 }
 
-tbody tr:nth-child(odd) {
+tbody.single tr:nth-child(odd) {
     background-color: #ebf0f5;
 }
 
+tbody.double tr:is(:nth-child(4n+1), :nth-child(4n+2)) {
+    background-color: #ebf0f5;
+}
+
+tbody dl {
+    margin: 0;
+    display: grid;
+    grid-template-columns: 2fr 5fr;
+}
+
+tbody dd {
+    margin: 0;
+    grid-column: 2;
+}
+
 thead th, tfoot tr {
     background-color: #c3d1e2;
 }