diff --git a/include/Ajax.php b/include/Ajax.php
new file mode 100644
index 0000000..7496423
--- /dev/null
+++ b/include/Ajax.php
@@ -0,0 +1,349 @@
+<?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) {
+            default:
+                $out = new Success('ajax endpoint');
+                break;
+            case 'getfragment':
+                $out = $this->get_fragment();
+                break;
+            case 'checkout':
+                $out = $this->checkout_product();
+                break;
+            case 'return':
+                $out = $this->return_product();
+                break;
+            case 'extend':
+                $out = $this->extend_loan();
+                break;
+            case 'startinventory':
+                $out = $this->start_inventory();
+                break;
+            case 'endinventory':
+                $out = $this->end_inventory();
+                break;
+            case 'inventoryproduct':
+                $out = $this->inventory_product();
+                break;
+            case 'updateproduct':
+                $out = $this->update_product();
+                break;
+            case 'updateuser':
+                $out = $this->update_user();
+                break;
+            case 'savetemplate':
+                $out = $this->save_template();
+                break;
+            case 'deletetemplate':
+                $out = $this->delete_template();
+                break;
+            case 'suggest':
+                $out = $this->suggest();
+                break;
+            case 'discardproduct':
+                $out = $this->discard_product();
+                break;
+        }
+        print($out->toJson());
+    }
+
+    private function get_fragment() {
+        $fragment = $_POST['fragment'];
+        if(isset($this->fragments[$fragment])) {
+            return new Success($this->fragments[$fragment]);
+        }
+        return new Failure("Ogiltigt fragment '$fragment'");
+    }
+
+    private function checkout_product() {
+        $user = new User($_POST['user'], 'name');
+        $product = null;
+        try {
+            $product = new Product($_POST['product'], 'serial');
+        } catch(Exception $e) {
+            return new Failure('Ogiltigt serienummer.');
+        }
+        try {
+            $user->create_loan($product, $_POST['end']);
+            return new Success($product->get_name() . 'utlånad.');
+        } catch(Exception $e) {
+            return new Failure('Artikeln är redan utlånad.');
+        }
+    }
+    
+    private function return_product() {
+        $product = null;
+        try {
+            $product = new Product($_POST['serial'], 'serial');
+        } catch(Exception $e) {
+            return new Failure('Ogiltigt serienummer.');
+        }
+        $loan = $product->get_active_loan();
+        if($loan) {
+            $loan->end();
+            $user = $loan->get_user();
+            $userlink = replace(array('page' => 'users',
+                                      'id'   => $user->get_id(),
+                                      'name' => $user->get_displayname()),
+                                $this->fragments['item_link']);
+            $productlink = replace(array('page' => 'products',
+                                         'id'   => $product->get_id(),
+                                         'name' => $product->get_name()),
+                                   $this->fragments['item_link']);
+            $user = $loan->get_user();
+            return new Success($productlink . ' åter från ' . $userlink);
+        }
+        return new Failure('Artikeln är inte utlånad.');
+    }
+
+    private function extend_loan() {
+        $product = null;
+        try {
+            $product = new Product($_POST['product']);
+        } catch(Exception $e) {
+            return new Failure('Ogiltigt ID.');
+        }
+        $loan = $product->get_active_loan();
+        if($loan) {
+            $loan->extend($_POST['end']);
+            return new Success('Lånet förlängt');
+        }
+        return new Failure('Lån saknas.');
+    }
+    
+    private function start_inventory() {
+        try {
+            Inventory::begin();
+            return new Success('Inventering startad.');
+        } catch(Exception $e) {
+            return new Failure('Inventering redan igång.');
+        }
+    }
+    
+    private function end_inventory() {
+        $inventory = Inventory::get_active();
+        if($inventory === null) {
+            return new Failure('Ingen inventering pågår.');
+        }
+        $inventory->end();
+        return new Success('Inventering avslutad.');
+    }
+    
+    private function inventory_product() {
+        $inventory = Inventory::get_active();
+        if($inventory === null) {
+            return new Failure('Ingen inventering pågår.');
+        }
+        $product = null;
+        try {
+            $product = new Product($_POST['serial'], 'serial');
+        } catch(Exception $e) {
+            return new Failure('Ogiltigt serienummer.');
+        }
+        $result = $inventory->add_product($product);
+        if(!$result) {
+            return new Failure('Artikeln är redan registrerad.');
+        }
+        return new Success('Artikeln registrerad.');
+    }
+
+    private function update_product() {
+        $info = $_POST;
+        $id = $info['id'];
+        $name = $info['name'];
+        $serial = $info['serial'];
+        $invoice = $info['invoice'];
+        $tags = array();
+        if(isset($info['tag'])) {
+            $tags = $this->unescape_tags($info['tag']);
+        }
+        foreach(array('id', 'name', 'serial', 'invoice', 'tag') as $key) {
+            unset($info[$key]);
+        }
+        if(!$name) {
+            return new Failure('Artikeln måste ha ett namn.');
+        }
+        if(!$serial) {
+            return new Failure('Artikeln måste ha ett serienummer.');
+        }
+        if(!$invoice) {
+            return new Failure('Artikeln måste ha ett fakturanummer.');
+        }
+        $product = null;
+        if(!$id) {
+            try {
+                $temp = new Product($serial, 'serial');
+                return new Failure(
+                    'Det angivna serienumret finns redan på en annan artikel.');
+            } catch(Exception $e) {}
+            try {
+                $product = Product::create_product($name,
+                                                   $invoice,
+                                                   $serial,
+                                                   $info,
+                                                   $tags);
+                $prodlink = replace(array('page' => 'products',
+                                          'id' => $product->get_id(),
+                                          'name' => $product->get_name()),
+                                    $this->fragments['item_link']);
+                return new Success("Artikeln '$prodlink' sparad.");
+            } catch(Exception $e) {
+                return new Failure($e->getMessage());
+            }
+        }
+        $product = new Product($id);
+        if($product->get_discardtime()) {
+            return new Failure('Skrotade artiklar får inte modifieras.');
+        }
+        if($name != $product->get_name()) {
+            $product->set_name($name);
+        }
+        if($serial != $product->get_serial()) {
+            try {
+                $product->set_serial($serial);
+            } catch(Exception $e) {
+                return new Failure('Det angivna serienumret finns redan på en annan artikel.');
+            }
+        }
+        if($invoice != $product->get_invoice()) {
+            $product->set_invoice($invoice);
+        }
+        foreach($product->get_info() as $key => $prodvalue) {
+            if(!isset($info[$key]) || !$info[$key]) {
+                $product->remove_info($key);
+                continue;
+            }
+            if($prodvalue != $info[$key]) {
+                $product->set_info($key, $info[$key]);
+            }
+            unset($info[$key]);
+        }
+        foreach($info as $key => $invalue) {
+            if($invalue) {
+                $product->set_info($key, $invalue);
+            }
+        }
+        foreach($product->get_tags() as $tag) {
+            if(!in_array($tag, $tags)) {
+                $product->remove_tag($tag);
+                continue;
+            }
+            unset($tags[array_search($tag, $tags)]);
+        }
+        foreach($tags as $tag) {
+            $product->add_tag($tag);
+        }
+        return new Success('Ändringarna sparade.');
+    }
+    
+    private function update_user() {
+        $id = $_POST['id'];
+        $name = $_POST['name'];
+        $notes = $_POST['notes'];
+        if(!$name) {
+            return new Failure('Användarnamnet får inte vara tomt.');
+        }
+        $user = new User($id);
+        if($user->get_name() != $name) {
+            $user->set_name($name);
+        }
+        if($user->get_notes() != $notes) {
+            $user->set_notes($notes);
+        }
+        return new Success('Ändringarna sparade.');
+    }
+
+    private function save_template() {
+        $info = $_POST;
+        $name = $info['template'];
+        $tags = array();
+        if(isset($info['tag'])) {
+            $tags = $this->unescape_tags($info['tag']);
+        }
+        foreach(array('template',
+                      'id',
+                      'name',
+                      'serial',
+                      'invoice',
+                      'tags') as $key) {
+            unset($info[$key]);
+        }
+        if(!$name) {
+            return new Failure('Mallen måste ha ett namn.');
+        }
+        $template = null;
+        try {
+            $template = new Template($name, 'name');
+        } catch(Exception $e) {
+            $template = Template::create_template($name, $info, $tags);
+            $name = $template->get_name();
+            return new Success(
+                "Aktuella fält och taggar har sparats till mallen '$name'.");
+        }
+        foreach($template->get_fields() as $field) {
+            if(!isset($info[$field])) {
+                $template->remove_field($field);
+            }
+        }
+        $existingfields = $template->get_fields();
+        foreach($info as $field) {
+            if(!in_array($field, $existingfields)) {
+                $template->add_field($field);
+            }
+        }
+        foreach($template->get_tags() as $tag) {
+            if(!in_array($tag, $tags)) {
+                $template->remove_tag($tag);
+            }
+        }
+        $existingtags = $template->get_tags();
+        foreach($tags as $tag) {
+            if(!in_array($tag, $existingtags)) {
+                $template->add_tag($tag);
+            }
+        }
+        $name = $template->get_name();
+        return new Success("Mallen '$name' uppdaterad.");
+    }
+
+    private function delete_template() {
+        try {
+            $template = $_POST['template'];
+            Template::delete_template($template);
+            $name = ucfirst(strtolower($template));
+            return new Success("Mallen '$name' har raderats.");
+        } catch(Exception $e) {
+            return new Failure('Det finns ingen mall med det namnet.');
+        }
+    }
+    
+    private function suggest() {
+        return new Success(suggest($_POST['type']));
+    }
+
+    private function discard_product() {
+        $product = new Product($_POST['id']);
+        if(!$product->get_discardtime()) {
+            if($product->get_active_loan()) {
+                return new Failure('Artikeln har ett aktivt lån.<br/>'
+                                  .'Lånet måste avslutas innan artikeln skrotas.');
+            }
+            $product->discard();
+            return new Success('Artikeln skrotad.');
+        } else {
+            return new Failure('Artikeln är redan skrotad.');
+        }
+    }
+}
+?>
diff --git a/include/CheckoutPage.php b/include/CheckoutPage.php
new file mode 100644
index 0000000..f0fa652
--- /dev/null
+++ b/include/CheckoutPage.php
@@ -0,0 +1,58 @@
+<?php
+class CheckoutPage extends Page {
+    private $userstr = '';
+    private $user = null;
+
+    public function __construct() {
+        parent::__construct();
+        if(isset($_GET['user'])) {
+            $this->userstr = $_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.";
+                }
+            }
+        }
+    }
+
+    protected function render_body() {
+        $username = '';
+        $displayname = '';
+        $notes = '';
+        $loan_table = '';
+        $subhead = '';
+        $enddate = '';
+        $disabled = 'disabled';
+        if($this->user !== null) {
+            $username = $this->user->get_name();
+            $displayname = $this->user->get_displayname();
+            $notes = $this->user->get_notes();
+            $enddate = gmdate('Y-m-d', time() + 604800); # 1 week from now
+            $disabled = '';
+            $loans = $this->user->get_loans('active');
+            $loan_table = 'Inga pågående lån.';
+            if($loans) {
+                $loan_table = $this->build_user_loan_table($loans, 'renew');
+            }
+            $subhead = replace(array('title' => 'Lånade artiklar'),
+                               $this->fragments['subtitle']);
+        }
+        print(replace(array('user' => $this->userstr,
+                            'displayname' => $displayname,
+                            'notes' => $notes,
+                            'end' => $enddate,
+                            'subtitle' => $subhead,
+                            'disabled' => $disabled,
+                            'loan_table' => $loan_table),
+                      $this->fragments['checkout_page']));
+    }
+}
+?>
diff --git a/include/Failure.php b/include/Failure.php
new file mode 100644
index 0000000..95145d3
--- /dev/null
+++ b/include/Failure.php
@@ -0,0 +1,7 @@
+<?php
+class Failure extends Result {
+    public function __construct($message) {
+        parent::__construct('error', $message);
+    }
+}
+?>
diff --git a/include/HistoryPage.php b/include/HistoryPage.php
new file mode 100644
index 0000000..77eb0cd
--- /dev/null
+++ b/include/HistoryPage.php
@@ -0,0 +1,80 @@
+<?php
+class HistoryPage extends Page {
+    private $action = 'list';
+    private $inventory = null;
+    
+    public function __construct() {
+        parent::__construct();
+        if(isset($_GET['action'])) {
+            $this->action = $_GET['action'];
+        }
+        if(isset($_GET['id'])) {
+            try {
+                $this->inventory = new Inventory($_GET['id']);
+            } catch(Exception $e) {
+                $this->inventory = null;
+                $this->action = 'list';
+                $this->error = 'Det finns ingen inventering med det ID-numret.';
+            }
+        }
+        switch($this->action) {
+            case 'show':
+                $this->subtitle = 'Inventeringsdetaljer';
+                break;
+            case 'list':
+                $this->subtitle = 'Genomförda inventeringar';
+                break;
+        }
+    }
+
+    protected function render_body() {
+        switch($this->action) {
+            case 'list':
+                print($this->build_inventory_table());
+                print(replace(array('title' => 'Skrotade artiklar'),
+                              $this->fragments['title']));
+                $discards = get_items('product_discarded');
+                if($discards) {
+                    print($this->build_product_table($discards));
+                } else {
+                    print('Inga artiklar skrotade.');
+                }
+                break;
+            case 'show':
+                if($this->inventory &&
+                    Inventory::get_active() !== $this->inventory) {
+                    print($this->build_inventory_details($this->inventory,
+                                                         false));
+                }
+                break;
+        }
+    }
+
+    private function build_inventory_table() {
+        $items = get_items('inventory_old');
+        if(!$items) {
+            return 'Inga inventeringar gjorda.';
+        }
+        $rows = '';
+        foreach($items as $inventory) {
+            $id = $inventory->get_id();
+            $inventory_link = replace(array('id' => $id,
+                                            'name' => $id,
+                                            'page' => 'history'),
+                                      $this->fragments['item_link']);
+            $duration = $inventory->get_duration();
+            $num_seen = count($inventory->get_seen_products());
+            $num_unseen = count($inventory->get_unseen_products());
+            $rows .= replace(array('item_link' => $inventory_link,
+                                   'start_date' => $duration['start'],
+                                   'end_date' => $duration['end'],
+                                   'num_seen' => $num_seen,
+                                   'num_unseen' => $num_unseen),
+                             $this->fragments['inventory_row']);
+        }
+        return replace(array('item' => 'Tillfälle',
+                             'rows' => $rows),
+                       $this->fragments['inventory_table']);
+    }
+}
+?>
diff --git a/include/Inventory.php b/include/Inventory.php
new file mode 100644
index 0000000..6d45589
--- /dev/null
+++ b/include/Inventory.php
@@ -0,0 +1,141 @@
+<?php
+class Inventory {
+    private $id = '';
+    private $starttime = '';
+    private $endtime = null;
+    private $seen_products = array();
+
+    public static function begin() {
+        if(Inventory::get_active() !== null) {
+            throw new Exception('Inventory already in progress.');
+        }
+        $now = time();
+        $start = prepare('insert into `inventory`(`starttime`) values (?)');
+        bind($start, 'i', $now);
+        execute($start);
+        $invid = $start->insert_id;
+        $prodid = '';
+        $register = prepare('insert into
+                                 `inventory_product`(`inventory`, `product`)
+                                 values (?, ?)');
+        foreach(get_items('loan_active') as $loan) {
+            $prodid = $loan->get_product()->get_id();
+            bind($register, 'ii', $invid, $prodid);
+            execute($register);
+        }
+        return new Inventory($invid);
+    }
+
+    public static function get_active() {
+        $search = prepare('select * from `inventory` where `endtime` is null');
+        execute($search);
+        $result = result_single($search);
+        if($result === null) {
+            return null;
+        }
+        return new Inventory($result['id']);
+    }
+    
+    public function __construct($id) {
+        $search = prepare('select `id` from `inventory` where `id`=?');
+        bind($search, 'i', $id);
+        execute($search);
+        $result = result_single($search);
+        if($result === null) {
+            throw new Exception('Invalid id');
+        }
+        $this->id = $result['id'];
+        $this->update_fields();
+    }
+
+    private function update_fields() {
+        $get = prepare('select * from `inventory` where `id`=?');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $result = result_single($get);
+        $this->starttime = $result['starttime'];
+        $this->endtime = $result['endtime'];
+        $prodget = prepare('select * from `inventory_product`
+                            where `inventory`=?');
+        bind($prodget, 'i', $this->id);
+        execute($prodget);
+        foreach(result_list($prodget) as $row) {
+            $this->seen_products[] = $row['product'];
+        }
+    }
+
+    public function end() {
+        $now = time();
+        $update = prepare('update `inventory` set `endtime`=?
+                           where `id`=? and `endtime` is null');
+        bind($update, 'ii', $now, $this->id);
+        execute($update);
+        $this->endtime = $now;
+        return true;
+    }
+
+    public function add_product($product) {
+        $add = prepare('insert into `inventory_product`(`inventory`, `product`)
+                            values (?, ?)');
+        bind($add, 'ii', $this->id, $product->get_id());
+        try {
+            execute($add);
+        } catch(Exception $e) {
+            return false;
+        }
+        $this->products[] = $product->get_id();
+        return true;
+    }
+    
+    public function get_id() {
+        return $this->id;
+    }
+
+    public function get_duration($format = true) {
+        $style = function($time) {
+            return $time;
+        };
+        if($format) {
+            $style = function($time) {
+                return gmdate('Y-m-d', $time);
+            };
+        }
+        return array('start' => $style($this->starttime),
+                     'end' => $style($this->endtime));
+    }
+
+    public function get_seen_products() {
+        $out = array();
+        foreach($this->seen_products as $prodid) {
+            $out[] = new Product($prodid);
+        }
+        return $out;
+    }
+
+    public function get_unseen_products() {
+        $all = get_items('product');
+        $out = array();
+        $include = function($product) {
+            if(!in_array($product->get_id(), $this->seen_products)) {
+                return true;
+            }
+            return false;
+        };
+        if($this->endtime) {
+            $include = function($product) {
+                if($product->get_createtime() < $this->endtime
+                    && !in_array($product->get_id(), $this->seen_products)) {
+                    return true;
+                }
+                return false;
+            };
+        }
+        foreach($all as $product) {
+            if($include($product)) {
+                $out[] = $product;
+            }
+        }
+        return $out;
+    }
+}
+?>
diff --git a/include/InventoryPage.php b/include/InventoryPage.php
new file mode 100644
index 0000000..2e3320a
--- /dev/null
+++ b/include/InventoryPage.php
@@ -0,0 +1,18 @@
+<?php
+class InventoryPage extends Page {
+    private $inventory = null;
+    
+    public function __construct() {
+        parent::__construct();
+        $this->inventory = Inventory::get_active();
+    }
+
+    protected function render_body() {
+        if($this->inventory === null) {
+            print($this->fragments['inventory_start']);
+            return;
+        }
+        print($this->build_inventory_details($this->inventory));
+    }
+}
+?>
diff --git a/include/Kvs.php b/include/Kvs.php
new file mode 100644
index 0000000..e80edf8
--- /dev/null
+++ b/include/Kvs.php
@@ -0,0 +1,56 @@
+<?php
+class Kvs {
+    private $items = array();
+    
+    public function __construct() {
+        $get = prepare('select * from `kvs`');
+        execute($get);
+        foreach(result_list($get) as $row) {
+            $key = $row['key'];
+            $value = $row['value'];
+            $this->items[$key] = $value;
+        }
+    }
+    
+    public function get_keys() {
+        return array_keys($this->items);
+    }
+    
+    public function get_value($key) {
+        if(isset($this->items[$key])) {
+            return $this->items[$key];
+        }
+        return null;
+    }
+    
+    public function set_key($key, $value) {
+        $find = prepare('select * from `kvs` where `key`=?');
+        bind($find, 's', $key);
+        execute($find);
+        if(result_single($find) === null) {
+            $update = prepare('insert into `kvs`(`value`, `key`)
+                                   values (?, ?)');
+        } else {
+            $update = prepare('update `kvs` set `value`=? where `key`=?');
+        }
+        bind($update, 'ss', $value, $key);
+        execute($update);
+        $this->items[$key] = $value;
+        return true;
+    }
+    
+    public function remove_key($key) {
+        $find = prepare('select * from `kvs` where `key`=?');
+        bind($find, 's', $key);
+        execute($find);
+        if(result_single($find) === null) {
+            return true;
+        }
+        $update = prepare('delete from `kvs` where `key`=?');
+        bind($update, 's', $key);
+        execute($update);
+        unset($this->items[$key]);
+        return true;
+    }
+}
+?>
diff --git a/include/ldap.php b/include/Ldap.php
similarity index 98%
rename from include/ldap.php
rename to include/Ldap.php
index e2fef8d..91c7c84 100644
--- a/include/ldap.php
+++ b/include/Ldap.php
@@ -1,5 +1,4 @@
 <?php
-
 class Ldap {
     private $conn;
     private $base_dn = "dc=su,dc=se";
@@ -42,6 +41,4 @@ class Ldap {
         return $out;
     }
 }
-
-$ldap = new Ldap();
 ?>
diff --git a/include/Loan.php b/include/Loan.php
new file mode 100644
index 0000000..87d57d7
--- /dev/null
+++ b/include/Loan.php
@@ -0,0 +1,93 @@
+<?php
+class Loan {
+    private $id = 0;
+    private $user = 0;
+    private $product = 0;
+    private $starttime = 0;
+    private $endtime = 0;
+    private $returntime = null;
+    
+    public function __construct($id) {
+        $this->id = $id;
+        $this->update_fields();
+    }
+    
+    private function update_fields() {
+        $get = prepare('select * from `loan` where `id`=?');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $loan = result_single($get);
+        $this->user = $loan['user'];
+        $this->product = $loan['product'];
+        $this->starttime = $loan['starttime'];
+        $this->endtime = $loan['endtime'];
+        $this->returntime = $loan['returntime'];
+    }
+
+    public function get_id() {
+        return $this->id;
+    }
+
+    public function get_user() {
+        return new User($this->user);
+    }
+
+    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' => $style($this->endtime),
+                     'end_renew' => $style($this->endtime + 604800), # +1 week
+                     'return' => $style($this->returntime));
+    }
+
+    public function is_active() {
+        if($this->returntime === null) {
+            return true;
+        }
+        return false;
+    }
+
+    public function extend($time) {
+        $ts = strtotime($time . ' 13:00');
+        $query = prepare('update `loan` set `endtime`=? where `id`=?');
+        bind($query, 'ii', $ts, $this->id);
+        execute($query);
+        $this->endtime = $ts;
+        return true;
+    }
+    
+    public function end() {
+        $now = time();
+        $query = prepare('update `loan` set `returntime`=? where `id`=?');
+        bind($query, 'ii', $now, $this->id);
+        execute($query);
+        $this->returntime = $now;
+        return true;
+    }
+
+    public function is_overdue() {
+        if($this->returntime !== null) {
+            return false;
+        }
+        $now = time();
+        if($now > $this->endtime) {
+            return true;
+        }
+        return false;
+    }
+}
+?>
diff --git a/include/Page.php b/include/Page.php
new file mode 100644
index 0000000..e50799c
--- /dev/null
+++ b/include/Page.php
@@ -0,0 +1,306 @@
+<?php
+abstract class Page extends Responder {
+    protected abstract function render_body();
+    
+    protected $page = 'checkout';
+    protected $title = "DSV Utlåning";
+    protected $subtitle = '';
+    protected $error = null;
+    protected $menuitems = array('checkout' => 'Låna',
+                                 'return' => 'Lämna',
+                                 'products' => 'Artiklar',
+                                 'users' => 'Låntagare',
+                                 'inventory' => 'Inventera',
+                                 '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'];
+        }
+        if(isset($this->menuitems[$this->page])) {
+            $this->subtitle = $this->menuitems[$this->page];
+        }
+    }
+    
+    public function render() {
+        $this->render_head();
+        $this->render_body();
+        if($this->error) {
+            $this->render_error();
+        }
+        $this->render_foot();
+    }
+    
+    final private function render_head() {
+        $headtitle = $this->title;
+        $pagetitle = $this->title;
+        if($this->subtitle) {
+            $headtitle .= ' - '. $this->subtitle;
+            $pagetitle = $this->subtitle;
+        }
+        $query = '';
+        if(isset($_GET['q'])) {
+            $query = $_GET['q'];
+        }
+        print(replace(
+            array('title' => $headtitle,
+                  'menu' => $this->build_menu(),
+                  'query'=> $query),
+            $this->template_parts['head']
+        ));
+        print(replace(array('title' => $pagetitle),
+                      $this->fragments['title']));
+    }
+
+    private function build_menu() {
+        $menu = '';
+        foreach($this->menuitems as $page => $title) {
+            $align = 'left';
+            $active = '';
+            if($this->page == $page) {
+                $active = 'active';
+            }
+            if($page == 'search') {
+                $align = 'right';
+            }
+            $menu .= replace(array('title' => $title,
+                                   'page' => $page,
+                                   'align' => $align,
+                                   'active' => $active),
+                             $this->template_parts['menuitem']);
+        }
+        return $menu;
+    }
+
+    final private function render_error() {
+        print(replace(array('type' => 'error',
+                            'message' => $this->error),
+                      $this->fragments['message']));
+    }
+    
+    final private function render_foot() {
+        print($this->template_parts['foot']);
+    }
+
+    final protected function build_user_table($users) {
+        $rows = '';
+        foreach($users as $user) {
+            $replacements = array('name' => '',
+                                  'loan' => '',
+                                  'has_notes' => '',
+                                  'notes' => '',
+                                  'item_link' => '');
+            $replacements['name'] = $user->get_name();
+            $notes = $user->get_notes();
+            if($notes) {
+                $replacements['notes'] = $notes;
+                $replacements['has_notes'] = '*';
+            }
+            $userlink = replace(array('id' => $user->get_id(),
+                                      'name' => $user->get_displayname(),
+                                      'page' => 'users'),
+                                $this->fragments['item_link']);
+            $replacements['item_link'] = $userlink;
+            $loans = $user->get_loans('active');
+            $loan_str = '';
+            $count = count($loans);
+            switch($count) {
+                case 0:
+                    break;
+                case 1:
+                    $product = $loans[0]->get_product();
+                    $loan_str = $product->get_name();
+                    break;
+                default:
+                    $loan_str = $count .' artiklar';
+                    break;
+            }
+            $replacements['loan'] = $loan_str;
+            $rows .= replace($replacements, $this->fragments['user_row']);
+        }
+        return replace(array('rows' => $rows),
+                       $this->fragments['user_table']);
+    }
+
+    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']);
+            $available = 'Tillgänglig';
+            $status = 'available';
+            $discarded = $product->get_discardtime();
+            if($discarded) {
+                $available = 'Skrotad '.$discarded;
+                $status = 'discarded';
+            } else {
+                $loan = $product->get_active_loan();
+                if($loan) {
+                    $user = $loan->get_user();
+                    $userlink = replace(array('name' => $user->get_displayname(),
+                                              'id' => $user->get_id(),
+                                              'page' => 'users'),
+                                        $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'];
+                    }
+                }
+            }
+            $rows .= replace(array('available' => $available,
+                                   'status' => $status,
+                                   'item_link' => $prodlink),
+                             $this->fragments['product_row']);
+        }
+        return replace(array('rows' => $rows),
+                       $this->fragments['product_table']);
+    }
+
+    final protected function build_user_loan_table($loans, $show = 'none') {
+        $vis_return = 'hidden';
+        $vis_renew = 'hidden';
+        switch($show) {
+            case 'return':
+                $vis_return = '';
+                break;
+            case 'renew':
+                $vis_renew = '';
+                break;
+            case 'both':
+                $vis_return = '';
+                $vis_renew = '';
+                break;
+            case 'none':
+                break;
+            default:
+                throw new Exception('Invalid argument.');
+        }
+        $rows = '';
+        foreach($loans as $loan) {
+            $product = $loan->get_product();
+            $prodlink = replace(array('id' => $product->get_id(),
+                                      'name' => $product->get_name(),
+                                      'page' => 'products'),
+                                $this->fragments['item_link']);
+            $available = '';
+            $duration = $loan->get_duration();
+            $status = 'on_loan';
+            if($loan->is_overdue()) {
+                $status = 'overdue';
+            }
+            $returndate = '';
+            if($duration['return'] !== null) {
+                $returndate = $duration['return'];
+            }
+            $rows .= replace(array('id' => $product->get_id(),
+                                   'item_link' => $prodlink,
+                                   'start_date' => $duration['start'],
+                                   'end_date' => $duration['end'],
+                                   'return_date' => $returndate,
+                                   'status' => $status,
+                                   'vis_renew' => $vis_renew,
+                                   'vis_return' => $vis_return,
+                                   'end_new' => $duration['end_renew']),
+                             $this->fragments['loan_row']);
+        }
+        return replace(array('rows' => $rows,
+                             'vis_renew' => $vis_renew,
+                             'vis_return' => $vis_return,
+                             'item' => 'Artikel'),
+                       $this->fragments['loan_table']);
+    }
+
+    final protected function build_product_loan_table($loans) {
+        $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';
+            }
+            $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']);
+        }
+        return replace(array('rows' => $rows,
+                             'vis_renew' => $renew_column_visible,
+                             'vis_return' => '',
+                             'item' => 'Låntagare'),
+                       $this->fragments['loan_table']);
+    }
+
+    final protected function build_inventory_details($inventory,
+                                                     $interactive = true) {
+        $duration = $inventory->get_duration();
+        $all_products = get_items('product');
+        $seen = $inventory->get_seen_products();
+        $unseen = array();
+        foreach($all_products as $product) {
+            if(!in_array($product, $seen)) {
+                $unseen[] = $product;
+            }
+        }
+        $missing = 'Saknade artiklar';
+        $hidden = 'hidden';
+        if($interactive) {
+            $missing = 'Kvarvarande artiklar';
+            $hidden = '';
+        }
+        $out = replace(array('start_date' => $duration['start'],
+                             'total_count' => count($all_products),
+                             'seen_count' => count($seen),
+                             'hide' => $hidden),
+                       $this->fragments['inventory_do']);
+        $out .= replace(array('title' => $missing),
+                        $this->fragments['subtitle']);
+        if($unseen) {
+            $out .= $this->build_product_table($unseen);
+        } else {
+            $out .= 'Inga artiklar saknas.';
+        }
+        $out .= replace(array('title' => 'Inventerade artiklar'),
+                        $this->fragments['subtitle']);
+        if($seen) {
+            $out .= $this->build_product_table($seen);
+        } else {
+            $out .= 'Inga artiklar inventerade.';
+        }
+        return $out;
+    }
+}
+?>
diff --git a/include/Printer.php b/include/Printer.php
new file mode 100644
index 0000000..1b3a8e2
--- /dev/null
+++ b/include/Printer.php
@@ -0,0 +1,18 @@
+<?php
+class Printer extends QR {
+    public function __construct() {
+        parent::__construct();
+    }
+    
+    public function render() {
+        $label = replace(array('id' => $this->product->get_id(),
+                               'name' => $this->product->get_name(),
+                               'serial' => $this->product->get_serial()),
+                         $this->fragments['product_label']);
+        $title = 'Etikett för artikel '.$this->product->get_serial();
+        print(replace(array('title' => $title,
+                            'label' => $label),
+                      $this->fragments['label_page']));
+    }
+}
+?>
diff --git a/include/Product.php b/include/Product.php
new file mode 100644
index 0000000..0b8a806
--- /dev/null
+++ b/include/Product.php
@@ -0,0 +1,318 @@
+<?php
+class Product {
+    private $id = 0;
+    private $name = '';
+    private $invoice = '';
+    private $serial = '';
+    private $createtime = null;
+    private $discardtime = null;
+    private $info = array();
+    private $tags = array();
+    
+    public static function create_product(
+        $name = '',
+        $invoice = '',
+        $serial = '',
+        $info = array(),
+        $tags = array()
+    ) {
+        $now = time();
+        begin_trans();
+        try {
+            $stmt = 'insert into
+                         `product`(`name`, `invoice`, `serial`, `createtime`)
+                         values (?, ?, ?, ?)';
+            $ins_prod = prepare($stmt);
+            bind($ins_prod, 'sssi', $name, $invoice, $serial, $now);
+            execute($ins_prod);
+            $product = new Product($serial, 'serial');
+            foreach($info as $field => $value) {
+                $product->set_info($field, $value);
+            }
+            foreach($tags as $tag) {
+                $product->add_tag($tag);
+            }
+            commit_trans();
+            return $product;
+        } catch(Exception $e) {
+            revert_trans();
+            throw $e;
+        }
+    }
+    
+    public function __construct($clue, $type = 'id') {
+        $search = null;
+        switch($type) {
+            case 'id':
+                $search = prepare('select `id` from `product`
+                                   where `id`=?');
+                bind($search, 'i', $clue);
+                break;
+            case 'serial':
+                $search = prepare('select `id` from `product`
+                                   where `serial`=?');
+                bind($search, 's', $clue);
+                break;
+            default:
+                throw new Exception('Invalid type.');
+        }
+        execute($search);
+        $result = result_single($search);
+        if($result === null) {
+            throw new Exception('Product does not exist..');
+        }
+        $this->id = $result['id'];
+        $this->update_fields();
+        $this->update_info();
+        $this->update_tags();
+    }
+    
+    private function update_fields() {
+        $get = prepare('select * from `product` where `id`=?');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $product = result_single($get);
+        $this->name = $product['name'];
+        $this->invoice = $product['invoice'];
+        $this->serial = $product['serial'];
+        $this->createtime = $product['createtime'];
+        $this->discardtime = $product['discardtime'];
+        return true;
+    }
+    
+    private function update_info() {
+        $get = prepare('select * from `product_info`
+                        where `product`=? order by `field`');
+        bind($get, 'i', $this->id);
+        execute($get);
+        foreach(result_list($get) as $row) {
+            $field = $row['field'];
+            $data = $row['data'];
+            $this->info[$field] = $data;
+        }
+        return true;
+    }
+    
+    private function update_tags() {
+        $get = prepare('select * from `product_tag`
+                        where `product`=? order by `tag`');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $newtags = array();
+        foreach(result_list($get) as $row) {
+            $newtags[] = $row['tag'];
+        }
+        $this->tags = $newtags;
+        return true;
+    }
+
+    public function matches($terms) {
+        foreach($terms as $field => $values) {
+            $matchvalues = array();
+            if(property_exists($this, $field)) {
+                $matchvalues[] = $this->$field;
+            } else if(array_key_exists($field, $this->get_info())) {
+                $matchvalues[] = $this->get_info()[$field];
+            } else {
+                switch($field) {
+                    case 'tag':
+                        $matchvalues = $this->get_tags();
+                    case 'status':
+                        $matchvalues[] = $this->get_loan_status();
+                    case 'fritext':
+                        $matchvalues[] = $this->name;
+                        $matchvalues[] = $this->serial;
+                        $matchvalues[] = $this->invoice;
+                        $matchvalues = array_merge($matchvalues,
+                                                   $this->get_tags(),
+                                                   array_values(
+                                                       $this->get_info()));
+                }
+            }
+            if(!match($values, $matchvalues)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public function get_id() {
+        return $this->id;
+    }
+
+    public function get_createtime() {
+        return $this->createtime;
+    }
+
+    public function get_discardtime($format = true) {
+        if($this->discardtime && $format) {
+            return gmdate('Y-m-d', $this->discardtime);
+        }
+        return $this->discardtime;
+    }
+
+    public function discard() {
+        $now = time();
+        $update = prepare('update `product` set `discardtime`=? where `id`=?');
+        bind($update, 'ii', $now, $this->id);
+        execute($update);
+        $this->discardtime = $now;
+        return true;
+    }
+    
+    public function get_name() {
+        return $this->name;
+    }
+    
+    public function set_name($newname) {
+        $update = prepare('update `product` set `name`=? where `id`=?');
+        bind($update, 'si', $newname, $this->id);
+        execute($update);
+        $this->name = $newname;
+        return true;
+    }
+    
+    public function get_invoice() {
+        return $this->invoice;
+    }
+    
+    public function set_invoice($newinvoice) {
+        $update = prepare('update `product` set `invoice`=? where `id`=?');
+        bind($update, 'si', $newinvoice, $this->id);
+        execute($update);
+        $this->invoice = $newinvoice;
+        return true;
+    }
+    
+    public function get_serial() {
+        return $this->serial;
+    }
+    
+    public function set_serial($newserial) {
+        $update = prepare('update `product` set `serial`=? where `id`=?');
+        bind($update, 'si', $newserial, $this->id);
+        execute($update);
+        $this->serial = $newserial;
+        return true;
+    }
+    
+    public function get_info() {
+        return $this->info;
+    }
+    
+    public function set_info($field, $value) {
+        if(!$value) {
+            return true;
+        }
+        $find = prepare('select * from `product_info`
+                         where `product`=? and `field`=?');
+        bind($find, 'is', $this->id, $field);
+        execute($find);
+        if(result_single($find) === null) {
+            $update = prepare('insert into
+                                   `product_info`(`data`, `product`, `field`)
+                                   values (?, ?, ?)');
+        } else {
+            $update = prepare('update `product_info` set `data`=?
+                               where `product`=? and `field`=?');
+        }
+        bind($update, 'sis', $value, $this->id, $field);
+        execute($update);
+        $this->update_info();
+        return true;
+    }
+    
+    public function remove_info($field) {
+        $find = prepare('select * from `product_info`
+                         where `product`=? and `field`=?');
+        bind($find, 'is', $this->id, $field);
+        execute($find);
+        if(result_single($find) === null) {
+            return true;
+        }
+        $update = prepare('delete from `product_info`
+                           where `field`=? and `product`=?');
+        bind($update, 'si', $field, $this->id);
+        execute($update);
+        $this->update_info();
+        return true;
+    }
+    
+    public function get_tags() {
+        return $this->tags;
+    }
+    
+    public function add_tag($tag) {
+        if(!$tag) {
+            return true;
+        }
+        $find = prepare('select * from `product_tag`
+                         where `product`=? and `tag`=?');
+        bind($find, 'is', $this->id, $tag);
+        execute($find);
+        if(result_single($find) === null) {
+            $update = prepare('insert into `product_tag`(`tag`, `product`)
+                                   values (?, ?)');
+            bind($update, 'si', $tag, $this->id);
+            execute($update);
+            $this->update_tags();
+        }
+        return true;
+    }
+    
+    public function remove_tag($tag) {
+        $find = prepare('select * from `product_tag`
+                         where `product`=? and `tag`=?');
+        bind($find, 'is', $this->id, $tag);
+        execute($find);
+        if(result_single($find) === null) {
+            return true;
+        }
+        $update = prepare('delete from `product_tag`
+                           where `tag`=? and `product`=?');
+        bind($update, 'si', $tag, $this->id);
+        execute($update);
+        $this->update_tags();
+        return true;
+    }
+
+    public function get_loan_status() {
+        if($this->get_discardtime(false)) {
+            return 'discarded';
+        }
+        $loan = $this->get_active_loan();
+        if(!$loan) {
+            return 'no_loan';
+        }
+        if($loan->is_overdue()) {
+            return 'overdue';
+        }
+        return 'on_loan';
+    }
+
+    public function get_active_loan() {
+        $find = prepare('select `id` from `loan`
+                         where `returntime` is null and product=?');
+        bind($find, 'i', $this->id);
+        execute($find);
+        $result = result_single($find);
+        if($result === null) {
+            return null;
+        }
+        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);
+        $out = array();
+        foreach($loans as $loan) {
+            $out[] = new Loan($loan['id']);
+        }
+        return $out;
+    }
+}
+?>
diff --git a/include/ProductPage.php b/include/ProductPage.php
new file mode 100644
index 0000000..ae26645
--- /dev/null
+++ b/include/ProductPage.php
@@ -0,0 +1,133 @@
+<?php
+class ProductPage extends Page {
+    private $action = 'list';
+    private $template = null;
+    private $product = null;
+    
+    public function __construct() {
+        parent::__construct();
+        if(isset($_GET['action'])) {
+            $this->action = $_GET['action'];
+        }
+        if(isset($_GET['template'])) {
+            $template = $_GET['template'];
+            if($template) {
+                try {
+                    $this->template = new Template($template, 'name');
+                } catch(Exception $e) {
+                    $this->template = null;
+                    $this->error = 'Det finns ingen mall med det namnet.';
+                }
+            }
+        }
+        if(isset($_GET['id'])) {
+            $id = $_GET['id'];
+            if($id) {
+                try {
+                    $this->product = new Product($id);
+                } catch(Exception $e) {
+                    $this->action = 'list';
+                    $this->product = null;
+                    $this->error = 'Det finns ingen artikel med det ID-numret.';
+                }
+            }
+        }
+        switch($this->action) {
+            case 'show':
+                $this->subtitle = 'Artikeldetaljer';
+                break;
+            case 'new':
+                $this->subtitle = 'Ny artikel';
+                break;
+            case 'list':
+                $this->subtitle = 'Artikellista';
+                break;
+        }
+    }
+    
+    protected function render_body() {
+        switch($this->action) {
+            case 'list':
+                print($this->fragments['create_product']);
+                print($this->build_product_table(get_items('product')));
+                break;
+            case 'show':
+                print($this->build_product_details());
+                break;
+            case 'new':
+                print($this->build_new_page());
+                break;
+        }
+    }
+    
+    private function build_product_details() {
+        $info = '';
+        foreach($this->product->get_info() as $key => $value) {
+            $info .= replace(array('name' => ucfirst($key),
+                                   'key' => $key,
+                                   'value' => $value),
+                             $this->fragments['info_item']);
+        }
+        $tags = '';
+        foreach($this->escape_tags($this->product->get_tags()) as $tag) {
+            $tags .= replace(array('tag' => ucfirst($tag)),
+                             $this->fragments['tag']);
+        }
+        $fields = array('id' => $this->product->get_id(),
+                        'name' => $this->product->get_name(),
+                        'serial' => $this->product->get_serial(),
+                        'invoice' => $this->product->get_invoice(),
+                        'tags' => $tags,
+                        'info' => $info);
+        $label = '';
+        if(class_exists('QRcode', false)) {
+            $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']);
+        }
+        $out .= replace(array('title' => 'Lånehistorik'),
+                        $this->fragments['subtitle']);
+        $loan_table = 'Inga lån att visa.';
+        $history = $this->product->get_loan_history();
+        if($history) {
+            $loan_table = $this->build_product_loan_table($history);
+        }
+        $out .= $loan_table;
+        return $out;
+    }
+
+    private function build_new_page() {
+        $template = '';
+        $fields = '';
+        $tags = '';
+        if($this->template) {
+            $template = $this->template->get_name();
+            foreach($this->template->get_fields() as $field) {
+                $fields .= replace(array('name' => ucfirst($field),
+                                         'key' => $field,
+                                         'value' => ''),
+                                   $this->fragments['info_item']);
+            }
+            foreach($this->template->get_tags() as $tag) {
+                $tags .= replace(array('tag' => ucfirst($tag)),
+                                 $this->fragments['tag']);
+            }
+        }
+        $out = replace(array('template' => $template),
+                       $this->fragments['template_management']);
+        $out .= replace(array('id' => '',
+                              'name' => '',
+                              'serial' => '',
+                              'invoice' => '',
+                              'tags' => $tags,
+                              'info' => $fields,
+                              'label' => ''),
+                        $this->fragments['product_details']);
+        return $out;
+    }
+}
+?>
diff --git a/include/QR.php b/include/QR.php
new file mode 100644
index 0000000..9525b65
--- /dev/null
+++ b/include/QR.php
@@ -0,0 +1,18 @@
+<?php
+class QR extends Responder {
+    protected $product = '';
+
+    public function __construct() {
+        parent::__construct();
+        if(isset($_GET['id'])) {
+            $this->product = new Product($_GET['id']);
+        }
+    }
+
+    public function render() {
+        if(class_exists('QRcode', false)) {
+            QRcode::svg((string)$this->product->get_serial());
+        }
+    }
+}
+?>
diff --git a/include/Responder.php b/include/Responder.php
new file mode 100644
index 0000000..80a4953
--- /dev/null
+++ b/include/Responder.php
@@ -0,0 +1,31 @@
+<?php
+abstract class Responder {
+    protected $fragments = array();
+    
+    public function __construct() {
+        $this->fragments = get_fragments('./html/fragments.html');
+    }
+
+    final protected function escape_tags($tags) {
+        foreach($tags as $key => $tag) {
+            $tags[$key] = str_replace(array("'",
+                                            '"'),
+                                      array('&#39;',
+                                            '&#34;'),
+                                      strtolower($tag));
+        }
+        return $tags;
+    }
+    
+    final protected function unescape_tags($tags) {
+        foreach($tags as $key => $tag) {
+            $tags[$key] = str_replace(array('&#39;',
+                                            '&#34;'),
+                                      array("'",
+                                            '"'),
+                                      strtolower($tag));
+        }
+        return $tags;
+    }
+}
+?>
diff --git a/include/Result.php b/include/Result.php
new file mode 100644
index 0000000..2ef92f5
--- /dev/null
+++ b/include/Result.php
@@ -0,0 +1,18 @@
+<?php
+class Result {
+    private $type = '';
+    private $message = '';
+
+    public function __construct($type, $message) {
+        $this->type = $type;
+        $this->message = $message;
+    }
+
+    public function toJson() {
+        return json_encode(array(
+            'type' => $this->type,
+            'message' => $this->message
+        ));
+    }
+}
+?>
diff --git a/include/ReturnPage.php b/include/ReturnPage.php
new file mode 100644
index 0000000..bdb1690
--- /dev/null
+++ b/include/ReturnPage.php
@@ -0,0 +1,7 @@
+<?php
+class ReturnPage extends Page {
+    protected function render_body() {
+        print($this->fragments['return_page']);
+    }
+}
+?>
diff --git a/include/SearchPage.php b/include/SearchPage.php
new file mode 100644
index 0000000..49d6970
--- /dev/null
+++ b/include/SearchPage.php
@@ -0,0 +1,156 @@
+<?php
+class SearchPage extends Page {
+    private $terms = array();
+    
+    public function __construct() {
+        parent::__construct();
+        unset($_GET['page']);
+        if(isset($_GET['q']) && !$_GET['q']) {
+            unset($_GET['q']);
+        }
+        $this->terms = $this->translate_keys($_GET);
+    }
+    
+    private function do_search() {
+        $out = array();
+        if(!$this->terms) {
+            return $out;
+        }
+        foreach(array('user', 'product') as $type) {
+            $result = $this->search($type, $this->terms);
+            if($result) {
+                $out[$type] = $result;
+            }
+        }
+        return $out;
+    }
+
+    private function translate_keys($terms) {
+        $translated = array();
+        foreach($terms as $key => $value) {
+            $newkey = $key;
+            switch($key) {
+                case 'q':
+                    $newkey = 'fritext';
+                    break;
+                case 'namn':
+                    $newkey = 'name';
+                    break;
+                case 'faktura':
+                case 'fakturanummer':
+                    $newkey = 'invoice';
+                    break;
+                case 'serienummer':
+                    $newkey = 'serial';
+                    break;
+                case 'tagg':
+                    $newkey = 'tag';
+                    break;
+                case 'status':
+                    $value = $this->translate_values($value);
+                    break;
+            }
+            if(!array_key_exists($newkey, $translated)) {
+                $translated[$newkey] = $value;
+            } else {
+                $temp = $translated[$newkey];
+                $translated[$newkey] = array_merge((array)$temp, (array)$value);
+            }
+        }
+        return $translated;
+    }
+
+    private function translate_values($value) {
+        if(!is_array($value)) {
+            $value = array($value);
+        }
+        $translated = array();
+        foreach($value as $item) {
+            $newitem = $item;
+            switch($item) {
+                case 'ute':
+                case 'utlånad':
+                case 'utlånat':
+                case 'lånad':
+                case 'lånat':
+                    $newitem = 'on_loan';
+                    break;
+                case 'inne':
+                case 'ledig':
+                case 'ledigt':
+                case 'tillgänglig':
+                case 'tillgängligt':
+                    $newitem = 'no_loan';
+                    break;
+                case 'sen':
+                case 'sent':
+                case 'försenad':
+                case 'försenat':
+                case 'överdraget':
+                    $newitem = 'overdue';
+                    break;
+                case 'skrotad':
+                case 'skrotat':
+                case 'slängd':
+                case 'slängt':
+                    $newitem = 'discarded';
+                    break;
+            }
+            $translated[] = $newitem;
+        }
+        return $translated;
+    }
+
+    private function search($type, $terms) {
+        $items = get_items($type);
+        $out = array();
+        foreach($items as $item) {
+            if($item->matches($terms)) {
+                $out[] = $item;
+            }
+        }
+        return $out;
+    }
+    
+    protected function render_body() {
+        $terms = '';
+        foreach($this->terms as $key => $value) {
+            if(!is_array($value)) {
+                $terms .= replace(array('term' => ucfirst($key).": $value",
+                                        'key' => $key,
+                                        'value' => $value),
+                                  $this->fragments['search_term']);
+            } else {
+                foreach($value as $item) {
+                    $terms .= replace(array('term' => ucfirst($key).": $item",
+                                            'key' => $key,
+                                            'value' => $item),
+                                      $this->fragments['search_term']);
+                }
+            }
+        }
+        print(replace(array('terms' => $terms),
+                      $this->fragments['search_form']));
+        if($this->terms) {
+            $hits = $this->do_search();
+            print(replace(array('title' => 'Sökresultat'),
+                          $this->fragments['title']));
+            $result = '';
+            if(isset($hits['user'])) {
+                $result = replace(array('title' => 'Låntagare'),
+                                  $this->fragments['subtitle']);
+                $result .= $this->build_user_table($hits['user']);
+            }
+            if(isset($hits['product'])) {
+                $result .= replace(array('title' => 'Artiklar'),
+                                   $this->fragments['subtitle']);
+                $result .= $this->build_product_table($hits['product']);
+            }
+            if(!$result) {
+                $result = 'Inga träffar.';
+            }
+            print($result);
+        }
+    }
+}
+?>
diff --git a/include/Success.php b/include/Success.php
new file mode 100644
index 0000000..c4b534f
--- /dev/null
+++ b/include/Success.php
@@ -0,0 +1,7 @@
+<?php
+class Success extends Result {
+    public function __construct($message) {
+        parent::__construct('success', $message);
+    }
+}
+?>
diff --git a/include/Template.php b/include/Template.php
new file mode 100644
index 0000000..b069cc2
--- /dev/null
+++ b/include/Template.php
@@ -0,0 +1,196 @@
+<?php
+class Template {
+    private $id = 0;
+    private $name = '';
+    private $fields = array();
+    private $tags = array();
+    
+    public static function create_template(
+        $name = '',
+        $fields = array(),
+        $tags = array()
+    ) {
+        begin_trans();
+        try {
+            $stmt = 'insert into `template`(`name`) values (?)';
+            $ins_prod = prepare($stmt);
+            bind($ins_prod, 's', strtolower($name));
+            execute($ins_prod);
+            $template = new Template($name, 'name');
+            foreach(array_keys($fields) as $field) {
+                $template->add_field($field);
+            }
+            foreach($tags as $tag) {
+                $template->add_tag($tag);
+            }
+            commit_trans();
+            return $template;
+        } catch(Exception $e) {
+            revert_trans();
+            throw $e;
+        }
+    }
+
+    public static function delete_template($name) {
+        $template = new Template($name, 'name');
+        foreach($template->get_fields() as $field) {
+            $template->remove_field($field);
+        }
+        foreach($template->get_tags() as $tag) {
+            $template->remove_tag($tag);
+        }
+        $delete = prepare('delete from `template` where `id`=?');
+        bind($delete, 'i', $template->get_id());
+        execute($delete);
+        return true;
+    }
+    
+    public function __construct($clue, $type = 'id') {
+        switch($type) {
+            case 'id':
+                $this->id = $clue;
+                $search = prepare('select `name` from `template`
+                                   where `id`=?');
+                bind($search, 'i', $this->id);
+                execute($search);
+                $result = result_single($search);
+                if($result === null) {
+                    throw new Exception('Invalid id');
+                }
+                $this->name = $result['name'];
+                break;
+            case 'name':
+                $this->name = strtolower($clue);
+                $search = prepare('select `id` from `template`
+                                   where `name`=?');
+                bind($search, 's', $this->name);
+                execute($search);
+                $result = result_single($search);
+                if($result === null) {
+                    throw new Exception('Invalid name.');
+                }
+                $this->id = $result['id'];
+                break;
+            default:
+                throw new Exception('Invalid type.');
+        }
+        $this->update_fields();
+        $this->update_tags();
+    }
+
+    public function get_id() {
+        return $this->id;
+    }
+    
+    public function get_name() {
+        return ucfirst($this->name);
+    }
+
+    public function set_name($name) {
+        $update = prepare('update `template` set `name`=? where `id`=?');
+        bind($update, 'si', $name, $this->id);
+        execute($update);
+        $this->name = $name;
+        return true;
+    }
+    
+    private function update_fields() {
+        $get = prepare('select `field` from `template_info`
+                        where `template`=? order by `field`');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $fields = array();
+        foreach(result_list($get) as $row) {
+            $fields[] = $row['field'];
+        }
+        $this->fields = $fields;
+        return true;
+    }
+    
+    private function update_tags() {
+        $get = prepare('select * from `template_tag`
+                        where `template`=? order by `tag`');
+        bind($get, 'i', $this->id);
+        execute($get);
+        $newtags = array();
+        foreach(result_list($get) as $row) {
+            $newtags[] = $row['tag'];
+        }
+        $this->tags = $newtags;
+        return true;
+    }
+
+    public function get_fields() {
+        return $this->fields;
+    }
+
+    public function add_field($field) {
+        $find = prepare('select * from `template_info`
+                         where `template`=? and `field`=?');
+        bind($find, 'is', $this->id, $field);
+        execute($find);
+        if(result_single($find) === null) {
+            $update = prepare('insert into `template_info`(`template`, `field`)
+                                   values (?, ?)');
+            bind($update, 'is', $this->id, $field);
+            execute($update);
+            $this->update_fields();
+        }
+        return true;
+    }
+    
+    public function remove_field($field) {
+        $find = prepare('select * from `template_info`
+                         where `template`=? and `field`=?');
+        bind($find, 'is', $this->id, $field);
+        execute($find);
+        if(result_single($find) === null) {
+            return true;
+        }
+        $update = prepare('delete from `template_info`
+                           where `field`=? and `template`=?');
+        bind($update, 'si', $field, $this->id);
+        execute($update);
+        $this->update_fields();
+        return true;
+    }
+
+    public function get_tags() {
+        return $this->tags;
+    }
+
+    public function add_tag($tag) {
+        if(!$tag) {
+            return true;
+        }
+        $find = prepare('select * from `template_tag`
+                         where `template`=? and `tag`=?');
+        bind($find, 'is', $this->id, $tag);
+        execute($find);
+        if(result_single($find) === null) {
+            $update = prepare('insert into `template_tag`(`tag`, `template`)
+                                   values (?, ?)');
+            bind($update, 'si', $tag, $this->id);
+            execute($update);
+            $this->update_tags();
+        }
+        return true;
+    }
+    
+    public function remove_tag($tag) {
+        $find = prepare('select * from `template_tag`
+                         where `template`=? and `tag`=?');
+        bind($find, 'is', $this->id, $tag);
+        execute($find);
+        if(result_single($find) === null) {
+            return true;
+        }
+        $update = prepare('delete from `template_tag`
+                           where `tag`=? and `template`=?');
+        bind($update, 'si', $tag, $this->id);
+        execute($update);
+        $this->update_tags();
+        return true;
+    }
+}
+?>
diff --git a/include/User.php b/include/User.php
new file mode 100644
index 0000000..d21c0a4
--- /dev/null
+++ b/include/User.php
@@ -0,0 +1,176 @@
+<?php
+class User {
+    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);
+        execute($ins_user);
+        return new User($ins_user->insert_id);
+    }
+
+    public function __construct($clue, $type = 'id') {
+        $find = null;
+        switch($type) {
+            case 'id':
+                $find = prepare('select `id` from `user` where `id`=?');
+                bind($find, 'i', $clue);
+                break;
+            case 'name':
+                $find = prepare('select `id` from `user` where `name`=?');
+                bind($find, 's', $clue);
+                break;
+            default:
+                throw new Exception('Invalid type');
+        }
+        execute($find);
+        $id = result_single($find)['id'];
+        if($id === null) {
+            throw new Exception("Invalid username '$clue'");
+        }
+        $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);
+        execute($get);
+        $user = result_single($get);
+        $this->name = $user['name'];
+        $this->notes = $user['notes'];
+        return true;
+    }
+
+    public function matches($terms) {
+        foreach($terms as $field => $values) {
+            $matchvalues = array();
+            if($field == 'name') {
+                $matchvalues[] = $this->name;
+                $matchvalues[] = $this->get_displayname();
+            } else if(property_exists($this, $field)) {
+                $matchvalues[] = $this->$field;
+            } else if($field == 'fritext') {
+                $matchvalues[] = $this->name;
+                $matchvalues[] = $this->get_displayname();
+                $matchvalues[] = $this->notes;
+            } else {
+                return false;
+            }
+            if(!match($values, $matchvalues)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public function get_displayname() {
+        try {
+            return $this->ldap->get_user($this->name);
+        } catch(Exception $e) {
+            return 'Ej i SUKAT';
+        }
+    }
+
+    public function get_email() {
+        try {
+            return $this->ldap->get_user_email($this->name);
+        } catch(Exception $e) {
+            return 'Mailadress saknas';
+        }
+    }
+
+    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);
+        execute($update);
+        $this->name = $newname;
+        return true;
+    }
+
+    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);
+        execute($update);
+        $this->notes = $newnotes;
+        return true;
+    }
+
+    public function get_loans($type = 'both') {
+        $statement = 'select `id` from `loan` where `user`=?';
+        switch($type) {
+            case 'active':
+                $statement .= ' and `returntime` is null';
+                break;
+            case 'inactive':
+                $statement .= ' and `returntime` is not null';
+                break;
+            case 'both':
+                break;
+            default:
+                $err = "$type is not a valid argument. Valid arguments are active, inactive, both.";
+                throw new Exception($err);
+                break;
+        }
+        $statement .= ' order by `starttime` desc';
+        $get = prepare($statement);
+        bind($get, 'i', $this->id);
+        execute($get);
+        $loans = array();
+        foreach(result_list($get) as $row) {
+            $loans[] = new Loan($row['id']);
+        }
+        return $loans;
+    }
+
+    public function get_overdue_loans() {
+        $overdue = array();
+        foreach($this->get_loans('active') as $loan) {
+            if($loan->is_overdue()) {
+                $overdue[] = $loan;
+            }
+        }
+        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/include/UserPage.php b/include/UserPage.php
new file mode 100644
index 0000000..5335ab0
--- /dev/null
+++ b/include/UserPage.php
@@ -0,0 +1,65 @@
+<?php
+class UserPage extends Page {
+    private $action = 'list';
+    private $user = null;
+    
+    public function __construct() {
+        parent::__construct();
+        if(isset($_GET['action'])) {
+            $this->action = $_GET['action'];
+        }
+        if(isset($_GET['id'])) {
+            $id = $_GET['id'];
+            if($id) {
+                try {
+                    $this->user = new User($_GET['id']);
+                } catch(Exception $e) {
+                    $this->user = null;
+                    $this->action = 'list';
+                    $this->error = 'Det finns ingen användare med det ID-numret.';
+                }
+            }
+        }
+        switch($this->action) {
+            case 'show':
+                $this->subtitle = 'Låntagardetaljer';
+                break;
+            case 'list':
+                $this->subtitle = 'Låntagarlista';
+                break;
+        }
+    }
+
+    protected function render_body() {
+        switch($this->action) {
+            case 'list':
+                print($this->build_user_table(get_items('user')));
+                break;
+            case 'show':
+                print($this->build_user_details());
+                break;
+        }
+    }
+    
+    private function build_user_details() {
+        $active_loans = $this->user->get_loans('active');
+        $table_active = 'Inga aktuella lån.';
+        if($active_loans) {
+            $table_active = $this->build_user_loan_table($active_loans, 'renew');
+        }
+        $inactive_loans = $this->user->get_loans('inactive');
+        $table_inactive = 'Inga gamla lån.';
+        if($inactive_loans) {
+            $table_inactive = $this->build_user_loan_table($inactive_loans,
+                                                           'return');
+        }
+        return replace(array('active_loans' => $table_active,
+                             'inactive_loans' => $table_inactive,
+                             'id' => $this->user->get_id(),
+                             'name' => $this->user->get_name(),
+                             'displayname' => $this->user->get_displayname(),
+                             'notes' => $this->user->get_notes()),
+                       $this->fragments['user_details']);
+    }
+}
+?>
diff --git a/include/db.php b/include/db.php
deleted file mode 100644
index 876fe95..0000000
--- a/include/db.php
+++ /dev/null
@@ -1,1111 +0,0 @@
-<?php
-require_once('./include/sql.php');
-require_once('./include/ldap.php');
-
-function get_ids($type) {
-    $append = '';
-    switch($type) {
-        case 'user':
-            break;
-        case 'product':
-            $append = 'where `discardtime` is null';
-            break;
-        case 'loan':
-            break;
-        case 'inventory':
-            break;
-        case 'product_discarded':
-            $append = 'where `discardtime` is not null';
-            $type = 'product';
-            break;
-        case 'loan_active':
-            $append = 'where `returntime` is null';
-            $type = 'loan';
-            break;
-        case 'inventory_old':
-            $append = 'where `endtime` is not null order by `id` desc';
-            $type = 'inventory';
-            break;
-        default:
-            $err = "$type is not a valid argument.";
-            throw new Exception($err);
-            break;
-    }
-    $query = "select `id` from `$type`";
-    if($append) {
-        $query .= " $append";
-    }
-    $get = prepare($query);
-    execute($get);
-    $ids = array();
-    foreach(result_list($get) as $row) {
-        $ids[] = $row['id'];
-    }
-    return $ids;
-}
-
-function get_items($type) {
-    $construct = null;
-    switch($type) {
-        case 'user':
-            $construct = function($id) {
-                return new User($id);
-            };
-            break;
-        case 'product':
-        case 'product_discarded':
-            $construct = function($id) {
-                return new Product($id);
-            };
-            break;
-        case 'loan':
-        case 'loan_active':
-            $construct = function($id) {
-                return new Loan($id);
-            };
-            break;
-        case 'inventory':
-        case 'inventory_old':
-            $construct = function($id) {
-                return new Inventory($id);
-            };
-            break;
-        default:
-            $err = "$type is not a valid argument.";
-            throw new Exception($err);
-            break;
-    }
-    $ids = get_ids($type);
-    $list = array();
-    foreach($ids as $id) {
-        $list[] = $construct($id);
-    }
-    return $list;
-}
-
-function suggest($type) {
-    $search = '';
-    $typename = 'name';
-    switch($type) {
-        case 'user':
-            $search = prepare('select `name` from `user` order by `name`');
-            break;
-        case 'template':
-            $search = prepare('select `name` from `template` order by `name`');
-            break;
-        case 'tag':
-            $search = prepare(
-                '(select `tag` from `product_tag`)
-                 union
-                 (select `tag` from `template_tag`)
-                 order by `tag`');
-            $typename = 'tag';
-            break;
-        case 'field':
-            $search = prepare(
-                '(select `field` from `product_info`)
-                 union
-                 (select `field` from `template_info`)
-                 order by `field`');
-            $typename = 'field';
-            break;
-        default:
-            return array();
-    }
-    execute($search);
-    $out = array();
-    foreach(result_list($search) as $row) {
-        $out[] = $row[$typename];
-    }
-    return $out;
-}
-
-function match($testvalues, $matchvalues) {
-    if(!is_array($testvalues)) {
-        $testvalues = array($testvalues);
-    }
-    foreach($testvalues as $value) {
-        foreach($matchvalues as $candidate) {
-            if(fnmatch($value, $candidate, FNM_CASEFOLD)) {
-                return true;
-            }
-        }
-    }
-    return false;
-}
-
-class Product {
-    private $id = 0;
-    private $name = '';
-    private $invoice = '';
-    private $serial = '';
-    private $createtime = null;
-    private $discardtime = null;
-    private $info = array();
-    private $tags = array();
-    
-    public static function create_product(
-        $name = '',
-        $invoice = '',
-        $serial = '',
-        $info = array(),
-        $tags = array()
-    ) {
-        $now = time();
-        begin_trans();
-        try {
-            $stmt = 'insert into
-                         `product`(`name`, `invoice`, `serial`, `createtime`)
-                         values (?, ?, ?, ?)';
-            $ins_prod = prepare($stmt);
-            bind($ins_prod, 'sssi', $name, $invoice, $serial, $now);
-            execute($ins_prod);
-            $product = new Product($serial, 'serial');
-            foreach($info as $field => $value) {
-                $product->set_info($field, $value);
-            }
-            foreach($tags as $tag) {
-                $product->add_tag($tag);
-            }
-            commit_trans();
-            return $product;
-        } catch(Exception $e) {
-            revert_trans();
-            throw $e;
-        }
-    }
-    
-    public function __construct($clue, $type = 'id') {
-        $search = null;
-        switch($type) {
-            case 'id':
-                $search = prepare('select `id` from `product`
-                                   where `id`=?');
-                bind($search, 'i', $clue);
-                break;
-            case 'serial':
-                $search = prepare('select `id` from `product`
-                                   where `serial`=?');
-                bind($search, 's', $clue);
-                break;
-            default:
-                throw new Exception('Invalid type.');
-        }
-        execute($search);
-        $result = result_single($search);
-        if($result === null) {
-            throw new Exception('Product does not exist..');
-        }
-        $this->id = $result['id'];
-        $this->update_fields();
-        $this->update_info();
-        $this->update_tags();
-    }
-    
-    private function update_fields() {
-        $get = prepare('select * from `product` where `id`=?');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $product = result_single($get);
-        $this->name = $product['name'];
-        $this->invoice = $product['invoice'];
-        $this->serial = $product['serial'];
-        $this->createtime = $product['createtime'];
-        $this->discardtime = $product['discardtime'];
-        return true;
-    }
-    
-    private function update_info() {
-        $get = prepare('select * from `product_info`
-                        where `product`=? order by `field`');
-        bind($get, 'i', $this->id);
-        execute($get);
-        foreach(result_list($get) as $row) {
-            $field = $row['field'];
-            $data = $row['data'];
-            $this->info[$field] = $data;
-        }
-        return true;
-    }
-    
-    private function update_tags() {
-        $get = prepare('select * from `product_tag`
-                        where `product`=? order by `tag`');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $newtags = array();
-        foreach(result_list($get) as $row) {
-            $newtags[] = $row['tag'];
-        }
-        $this->tags = $newtags;
-        return true;
-    }
-
-    public function matches($terms) {
-        foreach($terms as $field => $values) {
-            $matchvalues = array();
-            if(property_exists($this, $field)) {
-                $matchvalues[] = $this->$field;
-            } else if(array_key_exists($field, $this->get_info())) {
-                $matchvalues[] = $this->get_info()[$field];
-            } else {
-                switch($field) {
-                    case 'tag':
-                        $matchvalues = $this->get_tags();
-                    case 'status':
-                        $matchvalues[] = $this->get_loan_status();
-                    case 'fritext':
-                        $matchvalues[] = $this->name;
-                        $matchvalues[] = $this->serial;
-                        $matchvalues[] = $this->invoice;
-                        $matchvalues = array_merge($matchvalues,
-                                                   $this->get_tags(),
-                                                   array_values(
-                                                       $this->get_info()));
-                }
-            }
-            if(!match($values, $matchvalues)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public function get_id() {
-        return $this->id;
-    }
-
-    public function get_createtime() {
-        return $this->createtime;
-    }
-
-    public function get_discardtime($format = true) {
-        if($this->discardtime && $format) {
-            return gmdate('Y-m-d', $this->discardtime);
-        }
-        return $this->discardtime;
-    }
-
-    public function discard() {
-        $now = time();
-        $update = prepare('update `product` set `discardtime`=? where `id`=?');
-        bind($update, 'ii', $now, $this->id);
-        execute($update);
-        $this->discardtime = $now;
-        return true;
-    }
-    
-    public function get_name() {
-        return $this->name;
-    }
-    
-    public function set_name($newname) {
-        $update = prepare('update `product` set `name`=? where `id`=?');
-        bind($update, 'si', $newname, $this->id);
-        execute($update);
-        $this->name = $newname;
-        return true;
-    }
-    
-    public function get_invoice() {
-        return $this->invoice;
-    }
-    
-    public function set_invoice($newinvoice) {
-        $update = prepare('update `product` set `invoice`=? where `id`=?');
-        bind($update, 'si', $newinvoice, $this->id);
-        execute($update);
-        $this->invoice = $newinvoice;
-        return true;
-    }
-    
-    public function get_serial() {
-        return $this->serial;
-    }
-    
-    public function set_serial($newserial) {
-        $update = prepare('update `product` set `serial`=? where `id`=?');
-        bind($update, 'si', $newserial, $this->id);
-        execute($update);
-        $this->serial = $newserial;
-        return true;
-    }
-    
-    public function get_info() {
-        return $this->info;
-    }
-    
-    public function set_info($field, $value) {
-        if(!$value) {
-            return true;
-        }
-        $find = prepare('select * from `product_info`
-                         where `product`=? and `field`=?');
-        bind($find, 'is', $this->id, $field);
-        execute($find);
-        if(result_single($find) === null) {
-            $update = prepare('insert into
-                                   `product_info`(`data`, `product`, `field`)
-                                   values (?, ?, ?)');
-        } else {
-            $update = prepare('update `product_info` set `data`=?
-                               where `product`=? and `field`=?');
-        }
-        bind($update, 'sis', $value, $this->id, $field);
-        execute($update);
-        $this->update_info();
-        return true;
-    }
-    
-    public function remove_info($field) {
-        $find = prepare('select * from `product_info`
-                         where `product`=? and `field`=?');
-        bind($find, 'is', $this->id, $field);
-        execute($find);
-        if(result_single($find) === null) {
-            return true;
-        }
-        $update = prepare('delete from `product_info`
-                           where `field`=? and `product`=?');
-        bind($update, 'si', $field, $this->id);
-        execute($update);
-        $this->update_info();
-        return true;
-    }
-    
-    public function get_tags() {
-        return $this->tags;
-    }
-    
-    public function add_tag($tag) {
-        if(!$tag) {
-            return true;
-        }
-        $find = prepare('select * from `product_tag`
-                         where `product`=? and `tag`=?');
-        bind($find, 'is', $this->id, $tag);
-        execute($find);
-        if(result_single($find) === null) {
-            $update = prepare('insert into `product_tag`(`tag`, `product`)
-                                   values (?, ?)');
-            bind($update, 'si', $tag, $this->id);
-            execute($update);
-            $this->update_tags();
-        }
-        return true;
-    }
-    
-    public function remove_tag($tag) {
-        $find = prepare('select * from `product_tag`
-                         where `product`=? and `tag`=?');
-        bind($find, 'is', $this->id, $tag);
-        execute($find);
-        if(result_single($find) === null) {
-            return true;
-        }
-        $update = prepare('delete from `product_tag`
-                           where `tag`=? and `product`=?');
-        bind($update, 'si', $tag, $this->id);
-        execute($update);
-        $this->update_tags();
-        return true;
-    }
-
-    public function get_loan_status() {
-        if($this->get_discardtime(false)) {
-            return 'discarded';
-        }
-        $loan = $this->get_active_loan();
-        if(!$loan) {
-            return 'no_loan';
-        }
-        if($loan->is_overdue()) {
-            return 'overdue';
-        }
-        return 'on_loan';
-    }
-
-    public function get_active_loan() {
-        $find = prepare('select `id` from `loan`
-                         where `returntime` is null and product=?');
-        bind($find, 'i', $this->id);
-        execute($find);
-        $result = result_single($find);
-        if($result === null) {
-            return null;
-        }
-        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);
-        $out = array();
-        foreach($loans as $loan) {
-            $out[] = new Loan($loan['id']);
-        }
-        return $out;
-    }
-}
-
-class Template {
-    private $id = 0;
-    private $name = '';
-    private $fields = array();
-    private $tags = array();
-    
-    public static function create_template(
-        $name = '',
-        $fields = array(),
-        $tags = array()
-    ) {
-        begin_trans();
-        try {
-            $stmt = 'insert into `template`(`name`) values (?)';
-            $ins_prod = prepare($stmt);
-            bind($ins_prod, 's', strtolower($name));
-            execute($ins_prod);
-            $template = new Template($name, 'name');
-            foreach(array_keys($fields) as $field) {
-                $template->add_field($field);
-            }
-            foreach($tags as $tag) {
-                $template->add_tag($tag);
-            }
-            commit_trans();
-            return $template;
-        } catch(Exception $e) {
-            revert_trans();
-            throw $e;
-        }
-    }
-
-    public static function delete_template($name) {
-        $template = new Template($name, 'name');
-        foreach($template->get_fields() as $field) {
-            $template->remove_field($field);
-        }
-        foreach($template->get_tags() as $tag) {
-            $template->remove_tag($tag);
-        }
-        $delete = prepare('delete from `template` where `id`=?');
-        bind($delete, 'i', $template->get_id());
-        execute($delete);
-        return true;
-    }
-    
-    public function __construct($clue, $type = 'id') {
-        switch($type) {
-            case 'id':
-                $this->id = $clue;
-                $search = prepare('select `name` from `template`
-                                   where `id`=?');
-                bind($search, 'i', $this->id);
-                execute($search);
-                $result = result_single($search);
-                if($result === null) {
-                    throw new Exception('Invalid id');
-                }
-                $this->name = $result['name'];
-                break;
-            case 'name':
-                $this->name = strtolower($clue);
-                $search = prepare('select `id` from `template`
-                                   where `name`=?');
-                bind($search, 's', $this->name);
-                execute($search);
-                $result = result_single($search);
-                if($result === null) {
-                    throw new Exception('Invalid name.');
-                }
-                $this->id = $result['id'];
-                break;
-            default:
-                throw new Exception('Invalid type.');
-        }
-        $this->update_fields();
-        $this->update_tags();
-    }
-
-    public function get_id() {
-        return $this->id;
-    }
-    
-    public function get_name() {
-        return ucfirst($this->name);
-    }
-
-    public function set_name($name) {
-        $update = prepare('update `template` set `name`=? where `id`=?');
-        bind($update, 'si', $name, $this->id);
-        execute($update);
-        $this->name = $name;
-        return true;
-    }
-    
-    private function update_fields() {
-        $get = prepare('select `field` from `template_info`
-                        where `template`=? order by `field`');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $fields = array();
-        foreach(result_list($get) as $row) {
-            $fields[] = $row['field'];
-        }
-        $this->fields = $fields;
-        return true;
-    }
-    
-    private function update_tags() {
-        $get = prepare('select * from `template_tag`
-                        where `template`=? order by `tag`');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $newtags = array();
-        foreach(result_list($get) as $row) {
-            $newtags[] = $row['tag'];
-        }
-        $this->tags = $newtags;
-        return true;
-    }
-
-    public function get_fields() {
-        return $this->fields;
-    }
-
-    public function add_field($field) {
-        $find = prepare('select * from `template_info`
-                         where `template`=? and `field`=?');
-        bind($find, 'is', $this->id, $field);
-        execute($find);
-        if(result_single($find) === null) {
-            $update = prepare('insert into `template_info`(`template`, `field`)
-                                   values (?, ?)');
-            bind($update, 'is', $this->id, $field);
-            execute($update);
-            $this->update_fields();
-        }
-        return true;
-    }
-    
-    public function remove_field($field) {
-        $find = prepare('select * from `template_info`
-                         where `template`=? and `field`=?');
-        bind($find, 'is', $this->id, $field);
-        execute($find);
-        if(result_single($find) === null) {
-            return true;
-        }
-        $update = prepare('delete from `template_info`
-                           where `field`=? and `template`=?');
-        bind($update, 'si', $field, $this->id);
-        execute($update);
-        $this->update_fields();
-        return true;
-    }
-
-    public function get_tags() {
-        return $this->tags;
-    }
-
-    public function add_tag($tag) {
-        if(!$tag) {
-            return true;
-        }
-        $find = prepare('select * from `template_tag`
-                         where `template`=? and `tag`=?');
-        bind($find, 'is', $this->id, $tag);
-        execute($find);
-        if(result_single($find) === null) {
-            $update = prepare('insert into `template_tag`(`tag`, `template`)
-                                   values (?, ?)');
-            bind($update, 'si', $tag, $this->id);
-            execute($update);
-            $this->update_tags();
-        }
-        return true;
-    }
-    
-    public function remove_tag($tag) {
-        $find = prepare('select * from `template_tag`
-                         where `template`=? and `tag`=?');
-        bind($find, 'is', $this->id, $tag);
-        execute($find);
-        if(result_single($find) === null) {
-            return true;
-        }
-        $update = prepare('delete from `template_tag`
-                           where `tag`=? and `template`=?');
-        bind($update, 'si', $tag, $this->id);
-        execute($update);
-        $this->update_tags();
-        return true;
-    }
-}
-
-class User {
-    private $id = 0;
-    private $name = '';
-    private $notes = '';
-    
-    public static function create_user($name) {
-        $ins_user = prepare('insert into `user`(`name`) values (?)');
-        bind($ins_user, 's', $name);
-        execute($ins_user);
-        return new User($ins_user->insert_id);
-    }
-
-    public function __construct($clue, $type = 'id') {
-        $find = null;
-        switch($type) {
-            case 'id':
-                $find = prepare('select `id` from `user` where `id`=?');
-                bind($find, 'i', $clue);
-                break;
-            case 'name':
-                $find = prepare('select `id` from `user` where `name`=?');
-                bind($find, 's', $clue);
-                break;
-            default:
-                throw new Exception('Invalid type');
-        }
-        execute($find);
-        $id = result_single($find)['id'];
-        if($id === null) {
-            throw new Exception("Invalid username '$clue'");
-        }
-        $this->id = $id;
-        $this->update_fields();
-    }
-    
-    private function update_fields() {
-        $get = prepare('select * from `user` where `id`=?');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $user = result_single($get);
-        $this->name = $user['name'];
-        $this->notes = $user['notes'];
-        return true;
-    }
-
-    public function matches($terms) {
-        foreach($terms as $field => $values) {
-            $matchvalues = array();
-            if($field == 'name') {
-                $matchvalues[] = $this->name;
-                $matchvalues[] = $this->get_displayname();
-            } else if(property_exists($this, $field)) {
-                $matchvalues[] = $this->$field;
-            } else if($field == 'fritext') {
-                $matchvalues[] = $this->name;
-                $matchvalues[] = $this->get_displayname();
-                $matchvalues[] = $this->notes;
-            } else {
-                return false;
-            }
-            if(!match($values, $matchvalues)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public function get_displayname() {
-        global $ldap;
-        try {
-            return $ldap->get_user($this->name);
-        } catch(Exception $e) {
-            return 'Ej i SUKAT';
-        }
-    }
-
-    public function get_email() {
-        global $ldap;
-        try {
-            return $ldap->get_user_email($this->name);
-        } catch(Exception $e) {
-            return 'Mailadress saknas';
-        }
-    }
-
-    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);
-        execute($update);
-        $this->name = $newname;
-        return true;
-    }
-
-    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);
-        execute($update);
-        $this->notes = $newnotes;
-        return true;
-    }
-
-    public function get_loans($type = 'both') {
-        $statement = 'select `id` from `loan` where `user`=?';
-        switch($type) {
-            case 'active':
-                $statement .= ' and `returntime` is null';
-                break;
-            case 'inactive':
-                $statement .= ' and `returntime` is not null';
-                break;
-            case 'both':
-                break;
-            default:
-                $err = "$type is not a valid argument. Valid arguments are active, inactive, both.";
-                throw new Exception($err);
-                break;
-        }
-        $statement .= ' order by `starttime` desc';
-        $get = prepare($statement);
-        bind($get, 'i', $this->id);
-        execute($get);
-        $loans = array();
-        foreach(result_list($get) as $row) {
-            $loans[] = new Loan($row['id']);
-        }
-        return $loans;
-    }
-
-    public function get_overdue_loans() {
-        $overdue = array();
-        foreach($this->get_loans('active') as $loan) {
-            if($loan->is_overdue()) {
-                $overdue[] = $loan;
-            }
-        }
-        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);
-    }
-}
-
-class Loan {
-    private $id = 0;
-    private $user = 0;
-    private $product = 0;
-    private $starttime = 0;
-    private $endtime = 0;
-    private $returntime = null;
-    
-    public function __construct($id) {
-        $this->id = $id;
-        $this->update_fields();
-    }
-    
-    private function update_fields() {
-        $get = prepare('select * from `loan` where `id`=?');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $loan = result_single($get);
-        $this->user = $loan['user'];
-        $this->product = $loan['product'];
-        $this->starttime = $loan['starttime'];
-        $this->endtime = $loan['endtime'];
-        $this->returntime = $loan['returntime'];
-    }
-
-    public function get_id() {
-        return $this->id;
-    }
-
-    public function get_user() {
-        return new User($this->user);
-    }
-
-    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' => $style($this->endtime),
-                     'end_renew' => $style($this->endtime + 604800), # +1 week
-                     'return' => $style($this->returntime));
-    }
-
-    public function is_active() {
-        if($this->returntime === null) {
-            return true;
-        }
-        return false;
-    }
-
-    public function extend($time) {
-        $ts = strtotime($time . ' 13:00');
-        $query = prepare('update `loan` set `endtime`=? where `id`=?');
-        bind($query, 'ii', $ts, $this->id);
-        execute($query);
-        $this->endtime = $ts;
-        return true;
-    }
-    
-    public function end() {
-        $now = time();
-        $query = prepare('update `loan` set `returntime`=? where `id`=?');
-        bind($query, 'ii', $now, $this->id);
-        execute($query);
-        $this->returntime = $now;
-        return true;
-    }
-
-    public function is_overdue() {
-        if($this->returntime !== null) {
-            return false;
-        }
-        $now = time();
-        if($now > $this->endtime) {
-            return true;
-        }
-        return false;
-    }
-}
-
-class Inventory {
-    private $id = '';
-    private $starttime = '';
-    private $endtime = null;
-    private $seen_products = array();
-
-    public static function begin() {
-        if(Inventory::get_active() !== null) {
-            throw new Exception('Inventory already in progress.');
-        }
-        $now = time();
-        $start = prepare('insert into `inventory`(`starttime`) values (?)');
-        bind($start, 'i', $now);
-        execute($start);
-        $invid = $start->insert_id;
-        $prodid = '';
-        $register = prepare('insert into
-                                 `inventory_product`(`inventory`, `product`)
-                                 values (?, ?)');
-        foreach(get_items('loan_active') as $loan) {
-            $prodid = $loan->get_product()->get_id();
-            bind($register, 'ii', $invid, $prodid);
-            execute($register);
-        }
-        return new Inventory($invid);
-    }
-
-    public static function get_active() {
-        $search = prepare('select * from `inventory` where `endtime` is null');
-        execute($search);
-        $result = result_single($search);
-        if($result === null) {
-            return null;
-        }
-        return new Inventory($result['id']);
-    }
-    
-    public function __construct($id) {
-        $search = prepare('select `id` from `inventory` where `id`=?');
-        bind($search, 'i', $id);
-        execute($search);
-        $result = result_single($search);
-        if($result === null) {
-            throw new Exception('Invalid id');
-        }
-        $this->id = $result['id'];
-        $this->update_fields();
-    }
-
-    private function update_fields() {
-        $get = prepare('select * from `inventory` where `id`=?');
-        bind($get, 'i', $this->id);
-        execute($get);
-        $result = result_single($get);
-        $this->starttime = $result['starttime'];
-        $this->endtime = $result['endtime'];
-        $prodget = prepare('select * from `inventory_product`
-                            where `inventory`=?');
-        bind($prodget, 'i', $this->id);
-        execute($prodget);
-        foreach(result_list($prodget) as $row) {
-            $this->seen_products[] = $row['product'];
-        }
-    }
-
-    public function end() {
-        $now = time();
-        $update = prepare('update `inventory` set `endtime`=?
-                           where `id`=? and `endtime` is null');
-        bind($update, 'ii', $now, $this->id);
-        execute($update);
-        $this->endtime = $now;
-        return true;
-    }
-
-    public function add_product($product) {
-        $add = prepare('insert into `inventory_product`(`inventory`, `product`)
-                            values (?, ?)');
-        bind($add, 'ii', $this->id, $product->get_id());
-        try {
-            execute($add);
-        } catch(Exception $e) {
-            return false;
-        }
-        $this->products[] = $product->get_id();
-        return true;
-    }
-    
-    public function get_id() {
-        return $this->id;
-    }
-
-    public function get_duration($format = true) {
-        $style = function($time) {
-            return $time;
-        };
-        if($format) {
-            $style = function($time) {
-                return gmdate('Y-m-d', $time);
-            };
-        }
-        return array('start' => $style($this->starttime),
-                     'end' => $style($this->endtime));
-    }
-
-    public function get_seen_products() {
-        $out = array();
-        foreach($this->seen_products as $prodid) {
-            $out[] = new Product($prodid);
-        }
-        return $out;
-    }
-
-    public function get_unseen_products() {
-        $all = get_items('product');
-        $out = array();
-        $include = function($product) {
-            if(!in_array($product->get_id(), $this->seen_products)) {
-                return true;
-            }
-            return false;
-        };
-        if($this->endtime) {
-            $include = function($product) {
-                if($product->get_createtime() < $this->endtime
-                    && !in_array($product->get_id(), $this->seen_products)) {
-                    return true;
-                }
-                return false;
-            };
-        }
-        foreach($all as $product) {
-            if($include($product)) {
-                $out[] = $product;
-            }
-        }
-        return $out;
-    }
-}
-
-class Kvs {
-    private $items = array();
-    
-    public function __construct() {
-        $get = prepare('select * from `kvs`');
-        execute($get);
-        foreach(result_list($get) as $row) {
-            $key = $row['key'];
-            $value = $row['value'];
-            $this->items[$key] = $value;
-        }
-    }
-    
-    public function get_keys() {
-        return array_keys($this->items);
-    }
-    
-    public function get_value($key) {
-        if(isset($this->items[$key])) {
-            return $this->items[$key];
-        }
-        return null;
-    }
-    
-    public function set_key($key, $value) {
-        $find = prepare('select * from `kvs` where `key`=?');
-        bind($find, 's', $key);
-        execute($find);
-        if(result_single($find) === null) {
-            $update = prepare('insert into `kvs`(`value`, `key`)
-                                   values (?, ?)');
-        } else {
-            $update = prepare('update `kvs` set `value`=? where `key`=?');
-        }
-        bind($update, 'ss', $value, $key);
-        execute($update);
-        $this->items[$key] = $value;
-        return true;
-    }
-    
-    public function remove_key($key) {
-        $find = prepare('select * from `kvs` where `key`=?');
-        bind($find, 's', $key);
-        execute($find);
-        if(result_single($find) === null) {
-            return true;
-        }
-        $update = prepare('delete from `kvs` where `key`=?');
-        bind($update, 's', $key);
-        execute($update);
-        unset($this->items[$key]);
-        return true;
-    }
-}
-
-?>
diff --git a/include/functions.php b/include/functions.php
index bcec9d9..9ef1b5a 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -1,7 +1,5 @@
 <?php
 
-require_once("./config.php");
-
 /*
    Takes an html file containing named fragments.
    Returns an associative array on the format array[name]=>fragment.
@@ -76,4 +74,237 @@ function replace($assoc_arr, $subject) {
     return str_replace($keys, $values, $subject);
 }
 
+function make_page($page) {
+    switch($page) {
+        default:
+        case 'checkout':
+            return new CheckoutPage();
+        case 'return':
+            return new ReturnPage();
+        case 'search':
+            return new SearchPage();
+        case 'products':
+            return new ProductPage();
+        case 'users':
+            return new UserPage();
+        case 'inventory':
+            return new InventoryPage();
+        case 'history':
+            return new HistoryPage();
+        case 'ajax':
+            return new Ajax();
+        case 'qr':
+            return new QR();
+        case 'print':
+            return new Printer();
+    }
+}
+
+function get_ids($type) {
+    $append = '';
+    switch($type) {
+        case 'user':
+            break;
+        case 'product':
+            $append = 'where `discardtime` is null';
+            break;
+        case 'loan':
+            break;
+        case 'inventory':
+            break;
+        case 'product_discarded':
+            $append = 'where `discardtime` is not null';
+            $type = 'product';
+            break;
+        case 'loan_active':
+            $append = 'where `returntime` is null';
+            $type = 'loan';
+            break;
+        case 'inventory_old':
+            $append = 'where `endtime` is not null order by `id` desc';
+            $type = 'inventory';
+            break;
+        default:
+            $err = "$type is not a valid argument.";
+            throw new Exception($err);
+            break;
+    }
+    $query = "select `id` from `$type`";
+    if($append) {
+        $query .= " $append";
+    }
+    $get = prepare($query);
+    execute($get);
+    $ids = array();
+    foreach(result_list($get) as $row) {
+        $ids[] = $row['id'];
+    }
+    return $ids;
+}
+
+function get_items($type) {
+    $construct = null;
+    switch($type) {
+        case 'user':
+            $construct = function($id) {
+                return new User($id);
+            };
+            break;
+        case 'product':
+        case 'product_discarded':
+            $construct = function($id) {
+                return new Product($id);
+            };
+            break;
+        case 'loan':
+        case 'loan_active':
+            $construct = function($id) {
+                return new Loan($id);
+            };
+            break;
+        case 'inventory':
+        case 'inventory_old':
+            $construct = function($id) {
+                return new Inventory($id);
+            };
+            break;
+        default:
+            $err = "$type is not a valid argument.";
+            throw new Exception($err);
+            break;
+    }
+    $ids = get_ids($type);
+    $list = array();
+    foreach($ids as $id) {
+        $list[] = $construct($id);
+    }
+    return $list;
+}
+
+function suggest($type) {
+    $search = '';
+    $typename = 'name';
+    switch($type) {
+        case 'user':
+            $search = prepare('select `name` from `user` order by `name`');
+            break;
+        case 'template':
+            $search = prepare('select `name` from `template` order by `name`');
+            break;
+        case 'tag':
+            $search = prepare(
+                '(select `tag` from `product_tag`)
+                 union
+                 (select `tag` from `template_tag`)
+                 order by `tag`');
+            $typename = 'tag';
+            break;
+        case 'field':
+            $search = prepare(
+                '(select `field` from `product_info`)
+                 union
+                 (select `field` from `template_info`)
+                 order by `field`');
+            $typename = 'field';
+            break;
+        default:
+            return array();
+    }
+    execute($search);
+    $out = array();
+    foreach(result_list($search) as $row) {
+        $out[] = $row[$typename];
+    }
+    return $out;
+}
+
+function match($testvalues, $matchvalues) {
+    if(!is_array($testvalues)) {
+        $testvalues = array($testvalues);
+    }
+    foreach($testvalues as $value) {
+        foreach($matchvalues as $candidate) {
+            if(fnmatch($value, $candidate, FNM_CASEFOLD)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+### Database interaction functions ###
+
+$db = new mysqli($db_host, $db_user, $db_pass, $db_name);
+if($db->connect_errno) {
+    $error = 'Failed to connect to db. The error was: '.$db->connect_error;
+    throw new Exception($error);
+}
+
+function prepare($statement) {
+    global $db;
+
+    if(!($s = $db->prepare($statement))) {
+        $error  = 'Failed to prepare the following statement: '.$statement;
+        $error .= '\n';
+        $error .= $db->error.' ('.$db->errno.')';
+        throw new Exception($error);
+    }
+
+    return $s;
+}
+
+function bind($statement, $types, ...$values) {
+    global $db;
+
+    return $statement->bind_param($types, ...$values);
+}
+
+function execute($statement) {
+    if(!$statement->execute()) {
+        $error  = 'Failed to execute statement.';
+        $error .= '\n';
+        $error .= $statement->error.' ('.$statement->errno.')';
+        throw new Exception($error);
+    }
+    return true;
+}
+
+function result_list($statement) {
+    return $statement->get_result()->fetch_all(MYSQLI_ASSOC);
+}
+
+function result_single($statement) {
+    $out = result_list($statement);
+    switch(count($out)) {
+        case 0:
+            return null;
+        case 1:
+            foreach($out as $value) {
+                return $value;
+            }
+        default:
+            throw new Exception('More than one result available.');
+    }
+}
+
+function begin_trans() {
+    global $db;
+
+    $db->begin_transaction(MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT);
+}
+
+function commit_trans() {
+    global $db;
+
+    $db->commit();
+    return true;
+}
+
+function revert_trans() {
+    global $db;
+
+    $db->rollback();
+    return false;
+}
+
 ?>
diff --git a/include/sql.php b/include/sql.php
deleted file mode 100644
index 7c1a68a..0000000
--- a/include/sql.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-require_once("./config.php");
-
-$db = new mysqli($db_host, $db_user, $db_pass, $db_name);
-if($db->connect_errno) {
-    $error = 'Failed to connect to db. The error was: '.$db->connect_error;
-    throw new Exception($error);
-}
-
-function prepare($statement) {
-    global $db;
-
-    if(!($s = $db->prepare($statement))) {
-        $error  = 'Failed to prepare the following statement: '.$statement;
-        $error .= '\n';
-        $error .= $db->error.' ('.$db->errno.')';
-        throw new Exception($error);
-    }
-
-    return $s;
-}
-
-function bind($statement, $types, ...$values) {
-    global $db;
-
-    return $statement->bind_param($types, ...$values);
-}
-
-function execute($statement) {
-    if(!$statement->execute()) {
-        $error  = 'Failed to execute statement.';
-        $error .= '\n';
-        $error .= $statement->error.' ('.$statement->errno.')';
-        throw new Exception($error);
-    }
-    return true;
-}
-
-function result_list($statement) {
-    return $statement->get_result()->fetch_all(MYSQLI_ASSOC);
-}
-
-function result_single($statement) {
-    $out = result_list($statement);
-    switch(count($out)) {
-        case 0:
-            return null;
-        case 1:
-            foreach($out as $value) {
-                return $value;
-            }
-        default:
-            throw new Exception('More than one result available.');
-    }
-}
-
-function begin_trans() {
-    global $db;
-
-    $db->begin_transaction(MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT);
-}
-
-function commit_trans() {
-    global $db;
-
-    $db->commit();
-    return true;
-}
-
-function revert_trans() {
-    global $db;
-
-    $db->rollback();
-    return false;
-}
-
-?>
diff --git a/include/view.php b/include/view.php
deleted file mode 100644
index c5e06e2..0000000
--- a/include/view.php
+++ /dev/null
@@ -1,1289 +0,0 @@
-<?php
-
-require_once('./include/db.php');
-require_once('./include/ldap.php');
-require_once('./include/functions.php');
-include_once('./phpqrcode/qrlib.php');
-
-function make_page($page) {
-    switch($page) {
-        default:
-        case 'checkout':
-            return new CheckoutPage();
-        case 'return':
-            return new ReturnPage();
-        case 'search':
-            return new SearchPage();
-        case 'products':
-            return new ProductPage();
-        case 'users':
-            return new UserPage();
-        case 'inventory':
-            return new InventoryPage();
-        case 'history':
-            return new HistoryPage();
-        case 'ajax':
-            return new Ajax();
-        case 'qr':
-            return new QR();
-        case 'print':
-            return new Printer();
-    }
-}
-
-abstract class Responder {
-    protected $fragments = array();
-    
-    public function __construct() {
-        $this->fragments = get_fragments('./html/fragments.html');
-    }
-
-    final protected function escape_tags($tags) {
-        foreach($tags as $key => $tag) {
-            $tags[$key] = str_replace(array("'",
-                                            '"'),
-                                      array('&#39;',
-                                            '&#34;'),
-                                      strtolower($tag));
-        }
-        return $tags;
-    }
-    
-    final protected function unescape_tags($tags) {
-        foreach($tags as $key => $tag) {
-            $tags[$key] = str_replace(array('&#39;',
-                                            '&#34;'),
-                                      array("'",
-                                            '"'),
-                                      strtolower($tag));
-        }
-        return $tags;
-    }
-}
-
-abstract class Page extends Responder {
-    protected abstract function render_body();
-    
-    protected $page = 'checkout';
-    protected $title = "DSV Utlåning";
-    protected $subtitle = '';
-    protected $error = null;
-    protected $menuitems = array('checkout' => 'Låna',
-                                 'return' => 'Lämna',
-                                 'products' => 'Artiklar',
-                                 'users' => 'Låntagare',
-                                 'inventory' => 'Inventera',
-                                 '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'];
-        }
-        if(isset($this->menuitems[$this->page])) {
-            $this->subtitle = $this->menuitems[$this->page];
-        }
-    }
-    
-    public function render() {
-        $this->render_head();
-        $this->render_body();
-        if($this->error) {
-            $this->render_error();
-        }
-        $this->render_foot();
-    }
-    
-    final private function render_head() {
-        $headtitle = $this->title;
-        $pagetitle = $this->title;
-        if($this->subtitle) {
-            $headtitle .= ' - '. $this->subtitle;
-            $pagetitle = $this->subtitle;
-        }
-        $query = '';
-        if(isset($_GET['q'])) {
-            $query = $_GET['q'];
-        }
-        print(replace(
-            array('title' => $headtitle,
-                  'menu' => $this->build_menu(),
-                  'query'=> $query),
-            $this->template_parts['head']
-        ));
-        print(replace(array('title' => $pagetitle),
-                      $this->fragments['title']));
-    }
-
-    private function build_menu() {
-        $menu = '';
-        foreach($this->menuitems as $page => $title) {
-            $align = 'left';
-            $active = '';
-            if($this->page == $page) {
-                $active = 'active';
-            }
-            if($page == 'search') {
-                $align = 'right';
-            }
-            $menu .= replace(array('title' => $title,
-                                   'page' => $page,
-                                   'align' => $align,
-                                   'active' => $active),
-                             $this->template_parts['menuitem']);
-        }
-        return $menu;
-    }
-
-    final private function render_error() {
-        print(replace(array('type' => 'error',
-                            'message' => $this->error),
-                      $this->fragments['message']));
-    }
-    
-    final private function render_foot() {
-        print($this->template_parts['foot']);
-    }
-
-    final protected function build_user_table($users) {
-        $rows = '';
-        foreach($users as $user) {
-            $replacements = array('name' => '',
-                                  'loan' => '',
-                                  'has_notes' => '',
-                                  'notes' => '',
-                                  'item_link' => '');
-            $replacements['name'] = $user->get_name();
-            $notes = $user->get_notes();
-            if($notes) {
-                $replacements['notes'] = $notes;
-                $replacements['has_notes'] = '*';
-            }
-            $userlink = replace(array('id' => $user->get_id(),
-                                      'name' => $user->get_displayname(),
-                                      'page' => 'users'),
-                                $this->fragments['item_link']);
-            $replacements['item_link'] = $userlink;
-            $loans = $user->get_loans('active');
-            $loan_str = '';
-            $count = count($loans);
-            switch($count) {
-                case 0:
-                    break;
-                case 1:
-                    $product = $loans[0]->get_product();
-                    $loan_str = $product->get_name();
-                    break;
-                default:
-                    $loan_str = $count .' artiklar';
-                    break;
-            }
-            $replacements['loan'] = $loan_str;
-            $rows .= replace($replacements, $this->fragments['user_row']);
-        }
-        return replace(array('rows' => $rows),
-                       $this->fragments['user_table']);
-    }
-
-    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']);
-            $available = 'Tillgänglig';
-            $status = 'available';
-            $discarded = $product->get_discardtime();
-            if($discarded) {
-                $available = 'Skrotad '.$discarded;
-                $status = 'discarded';
-            } else {
-                $loan = $product->get_active_loan();
-                if($loan) {
-                    $user = $loan->get_user();
-                    $userlink = replace(array('name' => $user->get_displayname(),
-                                              'id' => $user->get_id(),
-                                              'page' => 'users'),
-                                        $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'];
-                    }
-                }
-            }
-            $rows .= replace(array('available' => $available,
-                                   'status' => $status,
-                                   'item_link' => $prodlink),
-                             $this->fragments['product_row']);
-        }
-        return replace(array('rows' => $rows),
-                       $this->fragments['product_table']);
-    }
-
-    final protected function build_user_loan_table($loans, $show = 'none') {
-        $vis_return = 'hidden';
-        $vis_renew = 'hidden';
-        switch($show) {
-            case 'return':
-                $vis_return = '';
-                break;
-            case 'renew':
-                $vis_renew = '';
-                break;
-            case 'both':
-                $vis_return = '';
-                $vis_renew = '';
-                break;
-            case 'none':
-                break;
-            default:
-                throw new Exception('Invalid argument.');
-        }
-        $rows = '';
-        foreach($loans as $loan) {
-            $product = $loan->get_product();
-            $prodlink = replace(array('id' => $product->get_id(),
-                                      'name' => $product->get_name(),
-                                      'page' => 'products'),
-                                $this->fragments['item_link']);
-            $available = '';
-            $duration = $loan->get_duration();
-            $status = 'on_loan';
-            if($loan->is_overdue()) {
-                $status = 'overdue';
-            }
-            $returndate = '';
-            if($duration['return'] !== null) {
-                $returndate = $duration['return'];
-            }
-            $rows .= replace(array('id' => $product->get_id(),
-                                   'item_link' => $prodlink,
-                                   'start_date' => $duration['start'],
-                                   'end_date' => $duration['end'],
-                                   'return_date' => $returndate,
-                                   'status' => $status,
-                                   'vis_renew' => $vis_renew,
-                                   'vis_return' => $vis_return,
-                                   'end_new' => $duration['end_renew']),
-                             $this->fragments['loan_row']);
-        }
-        return replace(array('rows' => $rows,
-                             'vis_renew' => $vis_renew,
-                             'vis_return' => $vis_return,
-                             'item' => 'Artikel'),
-                       $this->fragments['loan_table']);
-    }
-
-    final protected function build_product_loan_table($loans) {
-        $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';
-            }
-            $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']);
-        }
-        return replace(array('rows' => $rows,
-                             'vis_renew' => $renew_column_visible,
-                             'vis_return' => '',
-                             'item' => 'Låntagare'),
-                       $this->fragments['loan_table']);
-    }
-
-    final protected function build_inventory_details($inventory,
-                                                     $interactive = true) {
-        $duration = $inventory->get_duration();
-        $all_products = get_items('product');
-        $seen = $inventory->get_seen_products();
-        $unseen = array();
-        foreach($all_products as $product) {
-            if(!in_array($product, $seen)) {
-                $unseen[] = $product;
-            }
-        }
-        $missing = 'Saknade artiklar';
-        $hidden = 'hidden';
-        if($interactive) {
-            $missing = 'Kvarvarande artiklar';
-            $hidden = '';
-        }
-        $out = replace(array('start_date' => $duration['start'],
-                             'total_count' => count($all_products),
-                             'seen_count' => count($seen),
-                             'hide' => $hidden),
-                       $this->fragments['inventory_do']);
-        $out .= replace(array('title' => $missing),
-                        $this->fragments['subtitle']);
-        if($unseen) {
-            $out .= $this->build_product_table($unseen);
-        } else {
-            $out .= 'Inga artiklar saknas.';
-        }
-        $out .= replace(array('title' => 'Inventerade artiklar'),
-                        $this->fragments['subtitle']);
-        if($seen) {
-            $out .= $this->build_product_table($seen);
-        } else {
-            $out .= 'Inga artiklar inventerade.';
-        }
-        return $out;
-    }
-}
-
-class SearchPage extends Page {
-    private $terms = array();
-    
-    public function __construct() {
-        parent::__construct();
-        unset($_GET['page']);
-        if(isset($_GET['q']) && !$_GET['q']) {
-            unset($_GET['q']);
-        }
-        $this->terms = $this->translate_keys($_GET);
-    }
-    
-    private function do_search() {
-        $out = array();
-        if(!$this->terms) {
-            return $out;
-        }
-        foreach(array('user', 'product') as $type) {
-            $result = $this->search($type, $this->terms);
-            if($result) {
-                $out[$type] = $result;
-            }
-        }
-        return $out;
-    }
-
-    private function translate_keys($terms) {
-        $translated = array();
-        foreach($terms as $key => $value) {
-            $newkey = $key;
-            switch($key) {
-                case 'q':
-                    $newkey = 'fritext';
-                    break;
-                case 'namn':
-                    $newkey = 'name';
-                    break;
-                case 'faktura':
-                case 'fakturanummer':
-                    $newkey = 'invoice';
-                    break;
-                case 'serienummer':
-                    $newkey = 'serial';
-                    break;
-                case 'tagg':
-                    $newkey = 'tag';
-                    break;
-                case 'status':
-                    $value = $this->translate_values($value);
-                    break;
-            }
-            if(!array_key_exists($newkey, $translated)) {
-                $translated[$newkey] = $value;
-            } else {
-                $temp = $translated[$newkey];
-                $translated[$newkey] = array_merge((array)$temp, (array)$value);
-            }
-        }
-        return $translated;
-    }
-
-    private function translate_values($value) {
-        if(!is_array($value)) {
-            $value = array($value);
-        }
-        $translated = array();
-        foreach($value as $item) {
-            $newitem = $item;
-            switch($item) {
-                case 'ute':
-                case 'utlånad':
-                case 'utlånat':
-                case 'lånad':
-                case 'lånat':
-                    $newitem = 'on_loan';
-                    break;
-                case 'inne':
-                case 'ledig':
-                case 'ledigt':
-                case 'tillgänglig':
-                case 'tillgängligt':
-                    $newitem = 'no_loan';
-                    break;
-                case 'sen':
-                case 'sent':
-                case 'försenad':
-                case 'försenat':
-                case 'överdraget':
-                    $newitem = 'overdue';
-                    break;
-                case 'skrotad':
-                case 'skrotat':
-                case 'slängd':
-                case 'slängt':
-                    $newitem = 'discarded';
-                    break;
-            }
-            $translated[] = $newitem;
-        }
-        return $translated;
-    }
-
-    private function search($type, $terms) {
-        $items = get_items($type);
-        $out = array();
-        foreach($items as $item) {
-            if($item->matches($terms)) {
-                $out[] = $item;
-            }
-        }
-        return $out;
-    }
-    
-    protected function render_body() {
-        $terms = '';
-        foreach($this->terms as $key => $value) {
-            if(!is_array($value)) {
-                $terms .= replace(array('term' => ucfirst($key).": $value",
-                                        'key' => $key,
-                                        'value' => $value),
-                                  $this->fragments['search_term']);
-            } else {
-                foreach($value as $item) {
-                    $terms .= replace(array('term' => ucfirst($key).": $item",
-                                            'key' => $key,
-                                            'value' => $item),
-                                      $this->fragments['search_term']);
-                }
-            }
-        }
-        print(replace(array('terms' => $terms),
-                      $this->fragments['search_form']));
-        if($this->terms) {
-            $hits = $this->do_search();
-            print(replace(array('title' => 'Sökresultat'),
-                          $this->fragments['title']));
-            $result = '';
-            if(isset($hits['user'])) {
-                $result = replace(array('title' => 'Låntagare'),
-                                  $this->fragments['subtitle']);
-                $result .= $this->build_user_table($hits['user']);
-            }
-            if(isset($hits['product'])) {
-                $result .= replace(array('title' => 'Artiklar'),
-                                   $this->fragments['subtitle']);
-                $result .= $this->build_product_table($hits['product']);
-            }
-            if(!$result) {
-                $result = 'Inga träffar.';
-            }
-            print($result);
-        }
-    }
-}
-
-class ProductPage extends Page {
-    private $action = 'list';
-    private $template = null;
-    private $product = null;
-    
-    public function __construct() {
-        parent::__construct();
-        if(isset($_GET['action'])) {
-            $this->action = $_GET['action'];
-        }
-        if(isset($_GET['template'])) {
-            $template = $_GET['template'];
-            if($template) {
-                try {
-                    $this->template = new Template($template, 'name');
-                } catch(Exception $e) {
-                    $this->template = null;
-                    $this->error = 'Det finns ingen mall med det namnet.';
-                }
-            }
-        }
-        if(isset($_GET['id'])) {
-            $id = $_GET['id'];
-            if($id) {
-                try {
-                    $this->product = new Product($id);
-                } catch(Exception $e) {
-                    $this->action = 'list';
-                    $this->product = null;
-                    $this->error = 'Det finns ingen artikel med det ID-numret.';
-                }
-            }
-        }
-        switch($this->action) {
-            case 'show':
-                $this->subtitle = 'Artikeldetaljer';
-                break;
-            case 'new':
-                $this->subtitle = 'Ny artikel';
-                break;
-            case 'list':
-                $this->subtitle = 'Artikellista';
-                break;
-        }
-    }
-    
-    protected function render_body() {
-        switch($this->action) {
-            case 'list':
-                print($this->fragments['create_product']);
-                print($this->build_product_table(get_items('product')));
-                break;
-            case 'show':
-                print($this->build_product_details());
-                break;
-            case 'new':
-                print($this->build_new_page());
-                break;
-        }
-    }
-    
-    private function build_product_details() {
-        $info = '';
-        foreach($this->product->get_info() as $key => $value) {
-            $info .= replace(array('name' => ucfirst($key),
-                                   'key' => $key,
-                                   'value' => $value),
-                             $this->fragments['info_item']);
-        }
-        $tags = '';
-        foreach($this->escape_tags($this->product->get_tags()) as $tag) {
-            $tags .= replace(array('tag' => ucfirst($tag)),
-                             $this->fragments['tag']);
-        }
-        $fields = array('id' => $this->product->get_id(),
-                        'name' => $this->product->get_name(),
-                        'serial' => $this->product->get_serial(),
-                        'invoice' => $this->product->get_invoice(),
-                        'tags' => $tags,
-                        'info' => $info);
-        $label = '';
-        if(class_exists('QRcode', false)) {
-            $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']);
-        }
-        $out .= replace(array('title' => 'Lånehistorik'),
-                        $this->fragments['subtitle']);
-        $loan_table = 'Inga lån att visa.';
-        $history = $this->product->get_loan_history();
-        if($history) {
-            $loan_table = $this->build_product_loan_table($history);
-        }
-        $out .= $loan_table;
-        return $out;
-    }
-
-    private function build_new_page() {
-        $template = '';
-        $fields = '';
-        $tags = '';
-        if($this->template) {
-            $template = $this->template->get_name();
-            foreach($this->template->get_fields() as $field) {
-                $fields .= replace(array('name' => ucfirst($field),
-                                         'key' => $field,
-                                         'value' => ''),
-                                   $this->fragments['info_item']);
-            }
-            foreach($this->template->get_tags() as $tag) {
-                $tags .= replace(array('tag' => ucfirst($tag)),
-                                 $this->fragments['tag']);
-            }
-        }
-        $out = replace(array('template' => $template),
-                       $this->fragments['template_management']);
-        $out .= replace(array('id' => '',
-                              'name' => '',
-                              'serial' => '',
-                              'invoice' => '',
-                              'tags' => $tags,
-                              'info' => $fields,
-                              'label' => ''),
-                        $this->fragments['product_details']);
-        return $out;
-    }
-}
-
-class UserPage extends Page {
-    private $action = 'list';
-    private $user = null;
-    
-    public function __construct() {
-        parent::__construct();
-        if(isset($_GET['action'])) {
-            $this->action = $_GET['action'];
-        }
-        if(isset($_GET['id'])) {
-            $id = $_GET['id'];
-            if($id) {
-                try {
-                    $this->user = new User($_GET['id']);
-                } catch(Exception $e) {
-                    $this->user = null;
-                    $this->action = 'list';
-                    $this->error = 'Det finns ingen användare med det ID-numret.';
-                }
-            }
-        }
-        switch($this->action) {
-            case 'show':
-                $this->subtitle = 'Låntagardetaljer';
-                break;
-            case 'list':
-                $this->subtitle = 'Låntagarlista';
-                break;
-        }
-    }
-
-    protected function render_body() {
-        switch($this->action) {
-            case 'list':
-                print($this->build_user_table(get_items('user')));
-                break;
-            case 'show':
-                print($this->build_user_details());
-                break;
-        }
-    }
-    
-    private function build_user_details() {
-        $active_loans = $this->user->get_loans('active');
-        $table_active = 'Inga aktuella lån.';
-        if($active_loans) {
-            $table_active = $this->build_user_loan_table($active_loans, 'renew');
-        }
-        $inactive_loans = $this->user->get_loans('inactive');
-        $table_inactive = 'Inga gamla lån.';
-        if($inactive_loans) {
-            $table_inactive = $this->build_user_loan_table($inactive_loans,
-                                                           'return');
-        }
-        return replace(array('active_loans' => $table_active,
-                             'inactive_loans' => $table_inactive,
-                             'id' => $this->user->get_id(),
-                             'name' => $this->user->get_name(),
-                             'displayname' => $this->user->get_displayname(),
-                             'notes' => $this->user->get_notes()),
-                       $this->fragments['user_details']);
-    }
-}
-
-class CheckoutPage extends Page {
-    private $userstr = '';
-    private $user = null;
-
-    public function __construct() {
-        parent::__construct();
-        if(isset($_GET['user'])) {
-            $this->userstr = $_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.";
-                }
-            }
-        }
-    }
-
-    protected function render_body() {
-        $username = '';
-        $displayname = '';
-        $notes = '';
-        $loan_table = '';
-        $subhead = '';
-        $enddate = '';
-        $disabled = 'disabled';
-        if($this->user !== null) {
-            $username = $this->user->get_name();
-            $displayname = $this->user->get_displayname();
-            $notes = $this->user->get_notes();
-            $enddate = gmdate('Y-m-d', time() + 604800); # 1 week from now
-            $disabled = '';
-            $loans = $this->user->get_loans('active');
-            $loan_table = 'Inga pågående lån.';
-            if($loans) {
-                $loan_table = $this->build_user_loan_table($loans, 'renew');
-            }
-            $subhead = replace(array('title' => 'Lånade artiklar'),
-                               $this->fragments['subtitle']);
-        }
-        print(replace(array('user' => $this->userstr,
-                            'displayname' => $displayname,
-                            'notes' => $notes,
-                            'end' => $enddate,
-                            'subtitle' => $subhead,
-                            'disabled' => $disabled,
-                            'loan_table' => $loan_table),
-                      $this->fragments['checkout_page']));
-    }
-}
-
-class ReturnPage extends Page {
-    protected function render_body() {
-        print($this->fragments['return_page']);
-    }
-}
-
-class InventoryPage extends Page {
-    private $inventory = null;
-    
-    public function __construct() {
-        parent::__construct();
-        $this->inventory = Inventory::get_active();
-    }
-
-    protected function render_body() {
-        if($this->inventory === null) {
-            print($this->fragments['inventory_start']);
-            return;
-        }
-        print($this->build_inventory_details($this->inventory));
-    }
-}
-
-class HistoryPage extends Page {
-    private $action = 'list';
-    private $inventory = null;
-    
-    public function __construct() {
-        parent::__construct();
-        if(isset($_GET['action'])) {
-            $this->action = $_GET['action'];
-        }
-        if(isset($_GET['id'])) {
-            try {
-                $this->inventory = new Inventory($_GET['id']);
-            } catch(Exception $e) {
-                $this->inventory = null;
-                $this->action = 'list';
-                $this->error = 'Det finns ingen inventering med det ID-numret.';
-            }
-        }
-        switch($this->action) {
-            case 'show':
-                $this->subtitle = 'Inventeringsdetaljer';
-                break;
-            case 'list':
-                $this->subtitle = 'Genomförda inventeringar';
-                break;
-        }
-    }
-
-    protected function render_body() {
-        switch($this->action) {
-            case 'list':
-                print($this->build_inventory_table());
-                print(replace(array('title' => 'Skrotade artiklar'),
-                              $this->fragments['title']));
-                $discards = get_items('product_discarded');
-                if($discards) {
-                    print($this->build_product_table($discards));
-                } else {
-                    print('Inga artiklar skrotade.');
-                }
-                break;
-            case 'show':
-                if($this->inventory &&
-                    Inventory::get_active() !== $this->inventory) {
-                    print($this->build_inventory_details($this->inventory,
-                                                         false));
-                }
-                break;
-        }
-    }
-
-    private function build_inventory_table() {
-        $items = get_items('inventory_old');
-        if(!$items) {
-            return 'Inga inventeringar gjorda.';
-        }
-        $rows = '';
-        foreach($items as $inventory) {
-            $id = $inventory->get_id();
-            $inventory_link = replace(array('id' => $id,
-                                            'name' => $id,
-                                            'page' => 'history'),
-                                      $this->fragments['item_link']);
-            $duration = $inventory->get_duration();
-            $num_seen = count($inventory->get_seen_products());
-            $num_unseen = count($inventory->get_unseen_products());
-            $rows .= replace(array('item_link' => $inventory_link,
-                                   'start_date' => $duration['start'],
-                                   'end_date' => $duration['end'],
-                                   'num_seen' => $num_seen,
-                                   'num_unseen' => $num_unseen),
-                             $this->fragments['inventory_row']);
-        }
-        return replace(array('item' => 'Tillfälle',
-                             'rows' => $rows),
-                       $this->fragments['inventory_table']);
-    }
-}
-
-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) {
-            default:
-                $out = new Success('ajax endpoint');
-                break;
-            case 'getfragment':
-                $out = $this->get_fragment();
-                break;
-            case 'checkout':
-                $out = $this->checkout_product();
-                break;
-            case 'return':
-                $out = $this->return_product();
-                break;
-            case 'extend':
-                $out = $this->extend_loan();
-                break;
-            case 'startinventory':
-                $out = $this->start_inventory();
-                break;
-            case 'endinventory':
-                $out = $this->end_inventory();
-                break;
-            case 'inventoryproduct':
-                $out = $this->inventory_product();
-                break;
-            case 'updateproduct':
-                $out = $this->update_product();
-                break;
-            case 'updateuser':
-                $out = $this->update_user();
-                break;
-            case 'savetemplate':
-                $out = $this->save_template();
-                break;
-            case 'deletetemplate':
-                $out = $this->delete_template();
-                break;
-            case 'suggest':
-                $out = $this->suggest();
-                break;
-            case 'discardproduct':
-                $out = $this->discard_product();
-                break;
-        }
-        print($out->toJson());
-    }
-
-    private function get_fragment() {
-        $fragment = $_POST['fragment'];
-        if(isset($this->fragments[$fragment])) {
-            return new Success($this->fragments[$fragment]);
-        }
-        return new Failure("Ogiltigt fragment '$fragment'");
-    }
-
-    private function checkout_product() {
-        $user = new User($_POST['user'], 'name');
-        $product = null;
-        try {
-            $product = new Product($_POST['product'], 'serial');
-        } catch(Exception $e) {
-            return new Failure('Ogiltigt serienummer.');
-        }
-        try {
-            $user->create_loan($product, $_POST['end']);
-            return new Success($product->get_name() . 'utlånad.');
-        } catch(Exception $e) {
-            return new Failure('Artikeln är redan utlånad.');
-        }
-    }
-    
-    private function return_product() {
-        $product = null;
-        try {
-            $product = new Product($_POST['serial'], 'serial');
-        } catch(Exception $e) {
-            return new Failure('Ogiltigt serienummer.');
-        }
-        $loan = $product->get_active_loan();
-        if($loan) {
-            $loan->end();
-            $user = $loan->get_user();
-            $userlink = replace(array('page' => 'users',
-                                      'id'   => $user->get_id(),
-                                      'name' => $user->get_displayname()),
-                                $this->fragments['item_link']);
-            $productlink = replace(array('page' => 'products',
-                                         'id'   => $product->get_id(),
-                                         'name' => $product->get_name()),
-                                   $this->fragments['item_link']);
-            $user = $loan->get_user();
-            return new Success($productlink . ' åter från ' . $userlink);
-        }
-        return new Failure('Artikeln är inte utlånad.');
-    }
-
-    private function extend_loan() {
-        $product = null;
-        try {
-            $product = new Product($_POST['product']);
-        } catch(Exception $e) {
-            return new Failure('Ogiltigt ID.');
-        }
-        $loan = $product->get_active_loan();
-        if($loan) {
-            $loan->extend($_POST['end']);
-            return new Success('Lånet förlängt');
-        }
-        return new Failure('Lån saknas.');
-    }
-    
-    private function start_inventory() {
-        try {
-            Inventory::begin();
-            return new Success('Inventering startad.');
-        } catch(Exception $e) {
-            return new Failure('Inventering redan igång.');
-        }
-    }
-    
-    private function end_inventory() {
-        $inventory = Inventory::get_active();
-        if($inventory === null) {
-            return new Failure('Ingen inventering pågår.');
-        }
-        $inventory->end();
-        return new Success('Inventering avslutad.');
-    }
-    
-    private function inventory_product() {
-        $inventory = Inventory::get_active();
-        if($inventory === null) {
-            return new Failure('Ingen inventering pågår.');
-        }
-        $product = null;
-        try {
-            $product = new Product($_POST['serial'], 'serial');
-        } catch(Exception $e) {
-            return new Failure('Ogiltigt serienummer.');
-        }
-        $result = $inventory->add_product($product);
-        if(!$result) {
-            return new Failure('Artikeln är redan registrerad.');
-        }
-        return new Success('Artikeln registrerad.');
-    }
-
-    private function update_product() {
-        $info = $_POST;
-        $id = $info['id'];
-        $name = $info['name'];
-        $serial = $info['serial'];
-        $invoice = $info['invoice'];
-        $tags = array();
-        if(isset($info['tag'])) {
-            $tags = $this->unescape_tags($info['tag']);
-        }
-        foreach(array('id', 'name', 'serial', 'invoice', 'tag') as $key) {
-            unset($info[$key]);
-        }
-        if(!$name) {
-            return new Failure('Artikeln måste ha ett namn.');
-        }
-        if(!$serial) {
-            return new Failure('Artikeln måste ha ett serienummer.');
-        }
-        if(!$invoice) {
-            return new Failure('Artikeln måste ha ett fakturanummer.');
-        }
-        $product = null;
-        if(!$id) {
-            try {
-                $temp = new Product($serial, 'serial');
-                return new Failure(
-                    'Det angivna serienumret finns redan på en annan artikel.');
-            } catch(Exception $e) {}
-            try {
-                $product = Product::create_product($name,
-                                                   $invoice,
-                                                   $serial,
-                                                   $info,
-                                                   $tags);
-                $prodlink = replace(array('page' => 'products',
-                                          'id' => $product->get_id(),
-                                          'name' => $product->get_name()),
-                                    $this->fragments['item_link']);
-                return new Success("Artikeln '$prodlink' sparad.");
-            } catch(Exception $e) {
-                return new Failure($e->getMessage());
-            }
-        }
-        $product = new Product($id);
-        if($product->get_discardtime()) {
-            return new Failure('Skrotade artiklar får inte modifieras.');
-        }
-        if($name != $product->get_name()) {
-            $product->set_name($name);
-        }
-        if($serial != $product->get_serial()) {
-            try {
-                $product->set_serial($serial);
-            } catch(Exception $e) {
-                return new Failure('Det angivna serienumret finns redan på en annan artikel.');
-            }
-        }
-        if($invoice != $product->get_invoice()) {
-            $product->set_invoice($invoice);
-        }
-        foreach($product->get_info() as $key => $prodvalue) {
-            if(!isset($info[$key]) || !$info[$key]) {
-                $product->remove_info($key);
-                continue;
-            }
-            if($prodvalue != $info[$key]) {
-                $product->set_info($key, $info[$key]);
-            }
-            unset($info[$key]);
-        }
-        foreach($info as $key => $invalue) {
-            if($invalue) {
-                $product->set_info($key, $invalue);
-            }
-        }
-        foreach($product->get_tags() as $tag) {
-            if(!in_array($tag, $tags)) {
-                $product->remove_tag($tag);
-                continue;
-            }
-            unset($tags[array_search($tag, $tags)]);
-        }
-        foreach($tags as $tag) {
-            $product->add_tag($tag);
-        }
-        return new Success('Ändringarna sparade.');
-    }
-    
-    private function update_user() {
-        $id = $_POST['id'];
-        $name = $_POST['name'];
-        $notes = $_POST['notes'];
-        if(!$name) {
-            return new Failure('Användarnamnet får inte vara tomt.');
-        }
-        $user = new User($id);
-        if($user->get_name() != $name) {
-            $user->set_name($name);
-        }
-        if($user->get_notes() != $notes) {
-            $user->set_notes($notes);
-        }
-        return new Success('Ändringarna sparade.');
-    }
-
-    private function save_template() {
-        $info = $_POST;
-        $name = $info['template'];
-        $tags = array();
-        if(isset($info['tag'])) {
-            $tags = $this->unescape_tags($info['tag']);
-        }
-        foreach(array('template',
-                      'id',
-                      'name',
-                      'serial',
-                      'invoice',
-                      'tags') as $key) {
-            unset($info[$key]);
-        }
-        if(!$name) {
-            return new Failure('Mallen måste ha ett namn.');
-        }
-        $template = null;
-        try {
-            $template = new Template($name, 'name');
-        } catch(Exception $e) {
-            $template = Template::create_template($name, $info, $tags);
-            $name = $template->get_name();
-            return new Success(
-                "Aktuella fält och taggar har sparats till mallen '$name'.");
-        }
-        foreach($template->get_fields() as $field) {
-            if(!isset($info[$field])) {
-                $template->remove_field($field);
-            }
-        }
-        $existingfields = $template->get_fields();
-        foreach($info as $field) {
-            if(!in_array($field, $existingfields)) {
-                $template->add_field($field);
-            }
-        }
-        foreach($template->get_tags() as $tag) {
-            if(!in_array($tag, $tags)) {
-                $template->remove_tag($tag);
-            }
-        }
-        $existingtags = $template->get_tags();
-        foreach($tags as $tag) {
-            if(!in_array($tag, $existingtags)) {
-                $template->add_tag($tag);
-            }
-        }
-        $name = $template->get_name();
-        return new Success("Mallen '$name' uppdaterad.");
-    }
-
-    private function delete_template() {
-        try {
-            $template = $_POST['template'];
-            Template::delete_template($template);
-            $name = ucfirst(strtolower($template));
-            return new Success("Mallen '$name' har raderats.");
-        } catch(Exception $e) {
-            return new Failure('Det finns ingen mall med det namnet.');
-        }
-    }
-    
-    private function suggest() {
-        return new Success(suggest($_POST['type']));
-    }
-
-    private function discard_product() {
-        $product = new Product($_POST['id']);
-        if(!$product->get_discardtime()) {
-            if($product->get_active_loan()) {
-                return new Failure('Artikeln har ett aktivt lån.<br/>'
-                                  .'Lånet måste avslutas innan artikeln skrotas.');
-            }
-            $product->discard();
-            return new Success('Artikeln skrotad.');
-        } else {
-            return new Failure('Artikeln är redan skrotad.');
-        }
-    }
-}
-
-class QR extends Responder {
-    protected $product = '';
-
-    public function __construct() {
-        parent::__construct();
-        if(isset($_GET['id'])) {
-            $this->product = new Product($_GET['id']);
-        }
-    }
-
-    public function render() {
-        if(class_exists('QRcode', false)) {
-            QRcode::svg((string)$this->product->get_serial());
-        }
-    }
-}
-
-class Printer extends QR {
-    public function __construct() {
-        parent::__construct();
-    }
-    
-    public function render() {
-        $label = replace(array('id' => $this->product->get_id(),
-                               'name' => $this->product->get_name(),
-                               'serial' => $this->product->get_serial()),
-                         $this->fragments['product_label']);
-        $title = 'Etikett för artikel '.$this->product->get_serial();
-        print(replace(array('title' => $title,
-                            'label' => $label),
-                      $this->fragments['label_page']));
-    }
-}
-
-class Result {
-    private $type = '';
-    private $message = '';
-
-    public function __construct($type, $message) {
-        $this->type = $type;
-        $this->message = $message;
-    }
-
-    public function toJson() {
-        return json_encode(array(
-            'type' => $this->type,
-            'message' => $this->message
-        ));
-    }
-}
-
-class Success extends Result {
-    public function __construct($message) {
-        parent::__construct('success', $message);
-    }
-}
-
-class Failure extends Result {
-    public function __construct($message) {
-        parent::__construct('error', $message);
-    }
-}
-?>
diff --git a/index.php b/index.php
index b687c0b..5ad5a7d 100755
--- a/index.php
+++ b/index.php
@@ -1,5 +1,13 @@
 <?php
-require_once('./include/view.php');
+
+set_include_path(get_include_path().PATH_SEPARATOR.'include/');
+spl_autoload_register(function ($class) {
+    if($class == 'qrcode') {
+        include('./phpqrcode/qrlib.php');
+    }
+});
+require('./config.php');
+require('functions.php');
 
 header('Content-Type: text/html; charset=UTF-8');