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