Compare commits

...

34 Commits

Author SHA1 Message Date
Erik Thuning
3765795dbb Merge branch 'test' 2024-02-09 11:39:45 +01:00
Erik Thuning
2f6cc432b1 Missed using the configured subject prefix in a spot 2024-02-08 16:36:35 +01:00
Erik Thuning
4f8bc9486d Fixed the inventory registry field not trimming whitespace 2024-02-08 15:38:54 +01:00
Erik Thuning
1f33f73938 Implemented notification emails when discarding products. 2024-02-08 15:34:04 +01:00
Erik Thuning
c31c827b36 Upped the default font size a bit. 2024-02-08 15:30:33 +01:00
Erik Thuning
019725dade Missed replacing a placeholder in the product_form fragment 2024-02-08 13:39:09 +01:00
root
d3e6ee7ab2 Fixed too many fields showing up when creating a new product 2023-03-08 10:05:44 +01:00
Erik Thuning
a7d961c4d3 If a product was checked out and immediately returned, an empty email receipt was being sent. Fixed. 2023-01-13 16:14:41 +01:00
Erik Thuning
b10bf1eed8 Added ability to check out a product from the product page directly 2023-01-11 11:11:42 +01:00
Erik Thuning
fa46874ae3 Mail notifications get pushed forward in time when a new loan is created if there is a pending notification.
This way the user can add multiple loans over time without the notification getting sent prematurely.
2023-01-11 10:43:57 +01:00
Erik Thuning
9307604aa0 Implemented receipts for loan extensions 2022-07-27 13:53:18 +02:00
Erik Thuning
1b8c2e1e18 Corrected a database field name 2022-07-27 13:52:54 +02:00
Erik Thuning
ae8b73cb88 Trim whitespace from serials when checking out and returning products 2022-07-27 13:15:06 +02:00
Erik Thuning
4e9e5b93af Added a function to get the latest loan extension.
Broke receipt queueing into a function.
Changed $this->id to $this->get_id() for the sake of consistency.
2022-07-27 11:06:49 +02:00
Erik Thuning
b967c7dde2 Added the new tables pending_receipt and loan_extension to the schema 2022-07-27 10:58:11 +02:00
Erik Thuning
b3434dbb6a Changed loan extension form to have a saner default end time. 2022-07-27 10:52:52 +02:00
Erik Thuning
22d760a0af Whitespace cleanup 2022-07-27 10:48:53 +02:00
Erik Thuning
1d0caf9513 Sorted the bugs when picking an interval via button 2022-07-26 10:56:59 +02:00
Erik Thuning
dbe2a0ee90 Removed debug line 2022-07-26 10:24:16 +02:00
Erik Thuning
0af33c5dd6 Added buttons to quickly change the runtime of a loan 2022-07-26 10:23:09 +02:00
Erik Thuning
cd627f811d Made it work 2022-07-20 10:41:35 +02:00
Erik Thuning
311402e1b8 Initial implementation of loan receipts 2022-07-19 17:00:08 +02:00
Erik Thuning
04af074849 Whitespace cleanup 2022-07-19 16:57:42 +02:00
Erik Thuning
a16e5f2479 Whitespace cleanup 2022-07-19 15:49:28 +02:00
Erik Thuning
396a3f067e Config item $reminder_sender changed to $sender 2022-07-19 15:29:58 +02:00
Erik Thuning
c1ba468807 Whitespace cleanup 2022-07-19 15:27:31 +02:00
Erik Thuning
79d9f45c38 Refactored the reminder generaion 2022-05-16 13:49:22 +02:00
Erik Thuning
15f4597637 Fixed formatting bugs in the outgoing emails 2022-05-02 15:21:56 +02:00
Erik Thuning
0ee0f81024 Changed cron behaviour to warn users 3 days before their loans end 2022-04-29 15:58:38 +02:00
Erik Thuning
3fae0ce1ff Search overhaul possibly finished? 2022-03-03 11:37:35 +01:00
Erik Thuning
0f66c8212f User results are printed properly now 2022-03-02 10:18:54 +01:00
Erik Thuning
8502fbf0a1 Initial implementation of search result detail output 2022-03-01 16:33:00 +01:00
a20ffeb3c2 Testing a modified match function 2021-09-08 11:03:46 +02:00
ec6196b6a8 Testing a modified match function 2021-09-08 11:01:25 +02:00
18 changed files with 722 additions and 207 deletions

@ -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

@ -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();
?>

@ -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;

@ -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">
@ -591,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"

@ -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.');
}
@ -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']));
}

@ -5,23 +5,193 @@ class Cron {
private $error = '';
private $kvs;
private $ldap;
public function __construct($sender, $error) {
$this->now = time();
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);
@ -29,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 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 snart som möjligt, alternativt svara 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($this->ldap),
'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($this->ldap),
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);
}
}
}

@ -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;

@ -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) {

@ -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,23 +13,26 @@ 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) {
$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) {
$err = "LDAP search for '$uid' did not return exactly one result";
throw new Exception($err);
}
return $data[0]['mail'][0];
return $this->get_attribute($uid, 'mail');
}
public function search_email($email) {

@ -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';

@ -49,7 +49,7 @@ class NewPage extends Page {
'info' => $fields,
'label' => '',
'hidden' => 'hidden'),
$this->fragments['product_details']);
$this->fragments['product_form']);
return $out;
}
}

@ -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']);
}
@ -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($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;
}
$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']);
@ -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']);
}

@ -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() {
@ -214,6 +221,9 @@ class Product extends Entity {
return array();
}
}
if($matchAll && array_diff_assoc($terms, $matches)) {
return array();
}
return $matches;
}
@ -228,6 +238,29 @@ class Product extends Entity {
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;
}
@ -249,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();

@ -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) {

@ -115,7 +115,7 @@ class SearchPage extends Page {
$value = substr($value, 1);
}
// Collect the new SearchTerm
$translated[] = new SearchTerm($key, $value, $flag);
$translated[] = new SearchTerm($newkey, $value, $flag);
}
}
return $translated;
@ -182,50 +182,31 @@ class SearchPage extends Page {
$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']);
$data = print_r($matches, true);
return $link . '<br/>'
. $data . '<br/>';
}
private function render_user($user, $matches) {
$link = replace(array('id' => $user->get_id(),
'name' => $user->get_name(),
'page' => 'users'),
$this->fragments['item_link']);
$data = print_r($matches, true);
return $link . '<br/>'
. $data . '<br/>';
}
}
class SearchTerm {

@ -3,7 +3,7 @@ class User extends Entity {
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);
@ -34,7 +34,7 @@ class User extends Entity {
$this->id = $id;
$this->update_fields();
}
private function update_fields() {
$get = prepare('select * from `user` where `id`=?');
bind($get, 'i', $this->id);
@ -125,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);
@ -141,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);
@ -151,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`=?";
@ -189,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;
}
}
?>

@ -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
}

@ -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;
}