diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8475f3 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Boka2 + +There should be a description here + +Additional line + +additional line 2 diff --git a/html/fragments.html b/html/fragments.html index 409dc08..1e0e633 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" diff --git a/include/Entity.php b/include/Entity.php new file mode 100644 index 0000000..37c5f8e --- /dev/null +++ b/include/Entity.php @@ -0,0 +1,9 @@ +<?php +abstract class Entity { + protected function __construct() { + + } + + abstract public function matches($term, $ldap); +} +?> 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 696bed7..1d2607e 100644 --- a/include/Page.php +++ b/include/Page.php @@ -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) { @@ -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 9ea76ad..1f4468c 100644 --- a/include/Product.php +++ b/include/Product.php @@ -1,5 +1,5 @@ <?php -class Product { +class Product extends Entity { private $id = 0; private $brand = ''; private $name = ''; @@ -43,6 +43,7 @@ class Product { } public function __construct($clue, $type = 'id') { + parent::__construct(); $search = null; switch($type) { case 'id': @@ -109,37 +110,145 @@ class Product { return true; } - // Ldap object must be passed to keep the arglist in sync - // with User->matches() + /* + Return a list of field-value mappings containing all matching search terms. + */ public function matches($terms, $ldap) { - foreach($terms as $field => $values) { - $matchvalues = array(); - if(property_exists($this, $field)) { - $matchvalues[] = $this->$field; - } else if(array_key_exists($field, $this->get_info())) { - $matchvalues[] = $this->get_info()[$field]; - } else { - switch($field) { - case 'tag': - $matchvalues = $this->get_tags(); - case 'status': - $matchvalues[] = $this->get_status(); - case 'fritext': - $matchvalues[] = $this->brand; - $matchvalues[] = $this->name; - $matchvalues[] = $this->serial; - $matchvalues[] = $this->invoice; - $matchvalues = array_merge($matchvalues, - $this->get_tags(), - array_values( - $this->get_info())); - } + $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, $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, $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(!match($values, $matchvalues)) { - return false; + // 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 true; + 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() { diff --git a/include/ProductPage.php b/include/ProductPage.php index e385f62..afb25b1 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), @@ -81,7 +81,7 @@ class ProductPage extends Page { $fields['service'] = 'Avsluta service'; } } - return replace($fields, $this->fragments['product_details']); + return replace($fields, $this->fragments['product_form']); } private function build_history_table($history) { diff --git a/include/SearchPage.php b/include/SearchPage.php index 0eb095c..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,48 +165,96 @@ class SearchPage extends Page { } return $translated; } - - private function search($type, $terms) { - $items = get_items($type); - $out = array(); - foreach($items as $item) { - if($item->matches($terms, $this->ldap)) { - $out[] = $item; - } - } - 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 = $this->build_product_table($this->product_hits); + $products = ''; + foreach($this->product_hits as $hit) { + $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 = $this->build_user_table($this->user_hits); + $users = array(); + foreach($this->user_hits as $hit) { + $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'])); } } + +class SearchTerm { + public const MANDATORY = '+'; + public const OPTIONAL = '~'; + public const NEGATIVE = '-'; + + private $key; + private $query; + private $flag; + + public function __construct($key, $query, $flag=SearchTerm::OPTIONAL) { + $this->key = $key; + $this->query = $query; + $this->flag = $flag; + } + + public function get_key() { + return $this->key; + } + + 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 f2cbfe0..d77475f 100644 --- a/include/User.php +++ b/include/User.php @@ -1,5 +1,5 @@ <?php -class User { +class User extends Entity { private $id = 0; private $name = ''; private $notes = ''; @@ -12,6 +12,7 @@ class User { } public function __construct($clue, $type = 'id') { + parent::__construct(); $find = null; switch($type) { case 'id': @@ -45,25 +46,61 @@ class User { } public function matches($terms, $ldap) { - foreach($terms as $field => $values) { - $matchvalues = array(); - if($field == 'name') { - $matchvalues[] = $this->name; - $matchvalues[] = $this->get_displayname($ldap); - } else if(property_exists($this, $field)) { - $matchvalues[] = $this->$field; - } else if($field == 'fritext') { - $matchvalues[] = $this->name; - $matchvalues[] = $this->get_displayname($ldap); - $matchvalues[] = $this->notes; - } else { - return false; + $matches = array(); + foreach($terms as $term) { + // Iterate over the terms + $matched = false; + $key = $term->get_key(); + switch($key) { + case 'name': + // If the key is name, check username and displayname + $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; + 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': + $email = $this->get_email($ldap, false); + if($email && match($term, $email)) { + $matches['email'] = $email; + $matched = true; + } + break; + 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; + } } - if(!match($values, $matchvalues)) { - return false; + if($term->is_mandatory() && !$matched) { + return array(); + } + if($term->is_negative() && $matched) { + return array(); } } - return true; + return $matches; } public function get_displayname($ldap) { @@ -78,7 +115,10 @@ class User { try { return $ldap->get_user_email($this->name); } catch(Exception $e) { - return 'Mailadress saknas'; + if($format) { + return 'Mailadress saknas'; + } + return false; } } diff --git a/include/functions.php b/include/functions.php index 712ee91..b85f1b3 100644 --- a/include/functions.php +++ b/include/functions.php @@ -253,22 +253,21 @@ function suggest_content($fieldname) { return $out; } -function match($testvalues, $matchvalues) { - # match only presence of field (if no value given) - if(!$testvalues && $matchvalues) { +function match($term, $subject) { + if(fnmatch('*'.$term->get_query().'*', $subject, FNM_CASEFOLD)) { return true; } - if(!is_array($testvalues)) { - $testvalues = array($testvalues); - } - foreach($testvalues as $value) { - foreach($matchvalues as $candidate) { - if(fnmatch('*'.$value.'*', $candidate, FNM_CASEFOLD)) { - return true; - } + return false; +} + +function match_tags($searchterm, $tags) { + $found = array(); + foreach($tags as $tag) { + if(fnmatch('*'.$tag.'*', $searchterm, FNM_CASEFOLD)) { + $found[] = $tag; } } - return false; + return $found; } function format_date($date) { diff --git a/style.css b/style.css index c1ec303..61c9529 100644 --- a/style.css +++ b/style.css @@ -104,10 +104,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; }