boka3/include/Product.php
Erik Thuning df796f6564 Consistency changes to handle dropped users being present in the system
Printing the users' names as unknown in various places, and excluding
purged users from autocomplete suggestions
2025-09-25 15:04:53 +02:00

604 lines
20 KiB
PHP

<?php
class Product extends Entity {
private $id = 0;
private $brand = '';
private $name = '';
private $invoice = '';
private $serial = '';
private $createtime = null;
private $discardtime = null;
private $info = array();
private $tags = array();
public static function create_product(
$brand,
$name,
$invoice,
$serial,
$info = array(),
$tags = array()
) {
$now = time();
begin_trans();
try {
$stmt = 'insert into `product`
(`brand`, `name`, `invoice`, `serial`, `createtime`)
values (?, ?, ?, ?, ?)';
$ins_prod = prepare($stmt);
bind($ins_prod, 'ssssi', $brand, $name, $invoice, $serial, $now);
execute($ins_prod);
$product = new Product($serial, 'serial');
foreach($info as $field => $value) {
$product->set_info($field, $value);
}
foreach($tags as $tag) {
$product->add_tag($tag);
}
commit_trans();
return $product;
} catch(Exception $e) {
revert_trans();
throw $e;
}
}
public static function compare($prod1, $prod2) {
return strcmp(strtolower($prod1->get_name()),
strtolower($prod2->get_name()));
}
public function __construct($clue, $type = 'id') {
parent::__construct();
$search = null;
switch($type) {
case 'id':
$search = prepare('select `id` from `product`
where `id`=?');
bind($search, 'i', $clue);
break;
case 'serial':
$search = prepare('select `id` from `product`
where `serial`=?');
bind($search, 's', $clue);
break;
default:
throw new Exception('Invalid type.');
}
execute($search);
$result = result_single($search);
if($result === null) {
throw new Exception('Product does not exist.');
}
$this->id = $result['id'];
$this->update_fields();
$this->update_info();
$this->update_tags();
# 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() {
$get = prepare('select * from `product` where `id`=?');
bind($get, 'i', $this->id);
execute($get);
$product = result_single($get);
$this->brand = $product['brand'];
$this->name = $product['name'];
$this->invoice = $product['invoice'];
$this->serial = $product['serial'];
$this->createtime = $product['createtime'];
$this->discardtime = $product['discardtime'];
return true;
}
private function update_info() {
$get = prepare('select * from `product_info`
where `product`=? order by `field`');
bind($get, 'i', $this->id);
execute($get);
foreach(result_list($get) as $row) {
$field = $row['field'];
$data = $row['data'];
$this->info[$field] = $data;
}
return true;
}
private function update_tags() {
$get = prepare('select * from `product_tag`
where `product`=? order by `tag`');
bind($get, 'i', $this->id);
execute($get);
$newtags = array();
foreach(result_list($get) as $row) {
$newtags[] = $row['tag'];
}
$this->tags = $newtags;
return true;
}
/*
Return a list of field-value mappings containing all matching search terms.
*/
public function matches($terms, $ldap) {
$matches = array();
// 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($term, $value)) {
//Record a successful match
$matches[$key] = $value;
$matched = true;
}
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($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($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($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();
}
}
return $matches;
}
private function match_tags($term) {
$tags = $this->get_tags();
$matches = array();
foreach($tags as $tag) {
if(match_term($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;
}
public function get_createtime() {
return $this->createtime;
}
public function get_discardtime() {
return $this->discardtime;
}
public function discard() {
if($this->get_status() != 'available') {
return false;
}
$now = time();
$update = prepare('update `product` set `discardtime`=? where `id`=?');
bind($update, 'ii', $now, $this->id);
execute($update);
$this->discardtime = $now;
if($this->discard_email_address) {
$this->send_discard_email();
}
return true;
}
private function send_discard_email() {
$product_data = array('Märke' => $this->get_brand(),
'Namn' => $this->get_name(),
'Serienummer' => $this->get_serial(),
'Fakturanummer' => $this->get_invoice(),
);
$createtime = format_date($this->get_createtime());
$discardtime = format_date($this->get_discardtime());
$subject = $this->email_subject_prefix;
$subject .= $this->get_brand().' '.$this->get_name().' skrotad';
$message = <<<EOF
Hej!
Följande artikel har skrotats i Boka:
EOF;
foreach($product_data as $key => $value) {
$message .= "\n$key: $value";
}
foreach($this->get_info() as $key => $value) {
$uckey = ucfirst($key);
$message .= "\n$uckey: $value";
}
$message .= "\nTaggar: ".join(', ', $this->get_tags());
$message .= "\n\nHistorik:";
$message .= "\nSkrotad $discardtime";
foreach($this->get_history() as $item) {
$starttime = format_date($item->get_starttime());
$endtime = format_date($item->get_returntime());
$event = "Service";
if($item instanceof Loan) {
$user = $item->get_user();
$user_name = $user->get_name();
if($user_name === null) {
$user_name = 'Raderad användare';
}
$event = "Utlånad till ".$user_name;
}
$message .= "\n$event $starttime - $endtime";
}
$message .= "\nRegistrerad $createtime";
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($initiator) {
$status = $this->get_status();
$now = time();
$update = '';
if($status == 'service') {
return $this->get_active_service()->end();
} else if($status == 'available') {
Service::create_service($this, $initiator);
return true;
}
$id = $this->get_id();
throw new Exception("The state ($status) of this product (id $id) "
."does not allow servicing.");
}
public function get_active_service() {
$find = prepare("select `id` from `event` where
`type`='service'
and `returntime` is null
and product=?");
bind($find, 'i', $this->id);
execute($find);
$result = result_single($find);
if($result === null) {
return null;
}
return new Service($result['id']);
}
public function get_brand() {
return $this->brand;
}
public function set_brand($newbrand) {
$update = prepare('update `product` set `brand`=? where `id`=?');
bind($update, 'si', $newbrand, $this->id);
execute($update);
$this->brand = $newbrand;
return true;
}
public function get_name() {
return $this->name;
}
public function set_name($newname) {
$update = prepare('update `product` set `name`=? where `id`=?');
bind($update, 'si', $newname, $this->id);
execute($update);
$this->name = $newname;
return true;
}
public function get_invoice() {
return $this->invoice;
}
public function set_invoice($newinvoice) {
$update = prepare('update `product` set `invoice`=? where `id`=?');
bind($update, 'si', $newinvoice, $this->id);
execute($update);
$this->invoice = $newinvoice;
return true;
}
public function get_serial() {
return $this->serial;
}
public function set_serial($newserial) {
$update = prepare('update `product` set `serial`=? where `id`=?');
bind($update, 'si', $newserial, $this->id);
execute($update);
$this->serial = $newserial;
return true;
}
public function get_info() {
return $this->info;
}
public function set_info($field, $value) {
if(!$value) {
return true;
}
$find = prepare('select * from `product_info`
where `product`=? and `field`=?');
bind($find, 'is', $this->id, $field);
execute($find);
if(result_single($find) === null) {
$update = prepare('insert into
`product_info`(`data`, `product`, `field`)
values (?, ?, ?)');
} else {
$update = prepare('update `product_info` set `data`=?
where `product`=? and `field`=?');
}
bind($update, 'sis', $value, $this->id, $field);
execute($update);
$this->update_info();
return true;
}
public function remove_info($field) {
$find = prepare('select * from `product_info`
where `product`=? and `field`=?');
bind($find, 'is', $this->id, $field);
execute($find);
if(result_single($find) === null) {
return true;
}
$update = prepare('delete from `product_info`
where `field`=? and `product`=?');
bind($update, 'si', $field, $this->id);
execute($update);
$this->update_info();
return true;
}
public function get_tags() {
return $this->tags;
}
public function add_tag($tag) {
if(!$tag) {
return true;
}
$find = prepare('select * from `product_tag`
where `product`=? and `tag`=?');
bind($find, 'is', $this->id, $tag);
execute($find);
if(result_single($find) === null) {
$update = prepare('insert into `product_tag`(`tag`, `product`)
values (?, ?)');
bind($update, 'si', $tag, $this->id);
execute($update);
$this->update_tags();
}
return true;
}
public function remove_tag($tag) {
$find = prepare('select * from `product_tag`
where `product`=? and `tag`=?');
bind($find, 'is', $this->id, $tag);
execute($find);
if(result_single($find) === null) {
return true;
}
$update = prepare('delete from `product_tag`
where `tag`=? and `product`=?');
bind($update, 'si', $tag, $this->id);
execute($update);
$this->update_tags();
return true;
}
public function get_status() {
if($this->get_discardtime()) {
return 'discarded';
}
if($this->get_active_service()) {
return 'service';
}
$loan = $this->get_active_loan();
if(!$loan) {
return 'available';
}
if($loan->is_overdue()) {
return 'overdue';
}
return 'on_loan';
}
public function get_historic_event($time) {
$search = prepare("select `id`,`type` from `event`
where `product` = ?
and `starttime` < ?
and ( `returntime` > ?
or `returntime` is null )");
bind($search, 'iii', $this->id, $time, $time);
execute($search);
$result = result_single($search);
if(!$result) {
return null;
}
$id = $result['id'];
$type = $result['type'];
switch($type) {
case 'service':
return new Service($id);
break;
case 'loan':
return new Loan($id);
break;
default:
throw new Exception("Invalid type '$type'");
}
}
public function get_active_loan() {
$find = prepare("select `id` from `event`
where `type`='loan'
and `returntime` is null
and product=?");
bind($find, 'i', $this->id);
execute($find);
$result = result_single($find);
if($result === null) {
return null;
}
return new Loan($result['id']);
}
public function get_history() {
$out = array();
$find = prepare('select `id`,`type` from `event` '
.'where `product`=? order by `starttime` desc');
bind($find, 'i', $this->id);
execute($find);
$items = result_list($find);
foreach($items as $item) {
$id = $item['id'];
switch($item['type']) {
case 'service':
$out[] = new Service($id);
break;
case 'loan':
$out[] = new Loan($id);
break;
default:
$type = $item['type'];
throw new Exception("Invalid type '$type'");
}
}
return $out;
}
public function get_attachments() {
$out = array();
$find = prepare('select `id` from `attachment`
where `product`=? and `deletetime` is NULL
order by `uploadtime` asc');
bind($find, 'i', $this->id);
execute($find);
$items = result_list($find);
foreach($items as $item) {
$out[] = new Attachment($item['id']);
}
return $out;
}
}
?>