Эх сурвалжийг харах

Continue working on project, but faster, add auto complete from Google API's

Cixo Develop 5 сар өмнө
parent
commit
59eed14736

+ 79 - 0
application/scripts/autocomplete_database.js

@@ -0,0 +1,79 @@
+import { autocomplete_request } from "./autocomplete_request";
+import { product } from "./product";
+
+export class autocomplete_database {
+    #dataset;
+    #completed;
+    #current;
+    #on_autocomplete;
+    #on_create;
+    #on_single_fail;
+    #on_skip;
+    #on_single_success;
+    #finally;
+
+    constructor(dataset) {
+        this.#dataset = dataset;
+        this.#completed = new Array();
+        this.#current = null;
+    }
+
+    on_autocomplete(target) {
+        this.#on_autocomplete = target;
+        return this;
+    }
+
+    on_create(target) {
+        this.#on_create = target;
+        return this;
+    }
+
+    on_single_fail(target) {
+        this.#on_single_fail = target;
+        return this;
+    }
+
+    on_skip(target) {
+        this.#on_skip = target;
+        return this;
+    }
+
+    on_single_success(target) {
+        this.#on_single_success = target;
+        return this;
+    }
+
+    finally(target) {
+        this.#finally = target;
+        return this;
+    }
+
+    get current() {
+        return this.#current;
+    }
+
+    async process() {
+        for (const count of this.#dataset) {
+            this.#current = count;
+
+            if (query !== null) {
+                query(count);
+            }
+
+            const request = new autocomplete_request(count.barcode);
+            const response = await request.connect();
+
+            if (!response.result) {
+                throw new Error("Product barcode is not correct.");
+            }
+
+            const found = response.found;
+            count.image = found.image;
+            count.description = found.description;
+
+            this.#completed.push(count);
+        }
+
+        return this;
+    }
+}

+ 30 - 0
application/scripts/autocomplete_request.js

@@ -0,0 +1,30 @@
+import { request } from "./request.js";
+import { autocomplete_response } from "./autocomplete_response.js";
+
+export class autocomplete_request extends request {
+    #barcode;
+
+    constructor(barcode) {
+        super()
+
+        this.#barcode = barcode;
+    }
+
+    get _response() {
+        return autocomplete_response;
+    }
+
+    get data() {
+        return {
+            "apikey": this._apikey
+        };
+    }
+
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return "/complete/barcode/" + this.#barcode;
+    }
+}

+ 23 - 0
application/scripts/autocomplete_response.js

@@ -0,0 +1,23 @@
+import { bool_response } from "./bool_response";
+
+export class autocomplete_response extends bool_response {
+    #found;
+
+    constructor(target) {
+        super(target);
+
+        this.#found = null;
+        
+        if (this.result) {
+            this.#found = target["found"];
+        }
+    }
+
+    get found() {
+        if (this.#found === null) {
+            throw new Error("Server response is not complete.");
+        }
+
+        return this.#found;
+    }
+}

+ 86 - 0
application/scripts/database.js

@@ -0,0 +1,86 @@
+import { import_process_fail } from "./import_process_fail.js";
+import { product_base } from "./product_base";
+
+export class database {
+    #content;
+    #processed;
+    #on_skip;
+
+    constructor(content) {
+        this.#content = content
+        this.#processed = new Map();
+        this.#on_skip = null;
+    }
+
+    #append(target) {
+        if (this.#processed.has(target.barcode)) {
+            this.#processed.get(target.barcode).stock_count += 1;
+            return;
+        }
+
+        this.#processed.set(target.barcode, target)
+    }
+
+    #validate(target) {
+        if (!("id" in target)) {
+            throw new Error("One of item has no ID.");
+        }
+
+        if (!("title" in target)) {
+            throw new Error("Product " + target.barcode + " has no title.")
+        }
+
+        if (!("author" in target)) {
+            throw new Error("Product " + target.barcode + " has no author.")
+        }
+    }
+
+    #convert(target) {
+        this.#validate(target);
+
+        const product = new product_base();
+        product.name = target.title;
+        product.description = "";
+        product.author = target.author;
+        product.stock_count = 1;
+        product.barcode = target.id;
+
+        return product;
+    }
+
+    on_skip(target) {
+        this.#on_skip = target;
+        return this;
+    }
+
+    process() {
+        this.#processed.clear();
+
+        if (!(this.#content instanceof Array)) {
+            throw new Error("Database woud be array of objects.")
+        }
+
+        this.#content.forEach(count => {
+            try {
+                const product = this.#convert(count);
+                this.#append(product);
+            } catch (error) {
+                if (this.#on_skip === null) {
+                    return ;
+                }
+
+                try {
+                    this.#on_skip(new import_process_fail(count, error));
+                } catch (fail) {
+                    console.log(fail);
+                }
+            }
+        });
+
+        return this;
+    }
+
+    results() {
+        return Array.from(this.#processed.values());
+    }
+}

+ 6 - 2
application/scripts/formscreen.js

@@ -46,7 +46,7 @@ export class formscreen extends fullscreen {
         const form = document.createElement("form");
         center.appendChild(form);
 
-        form.addEventListener("click", () => {
+        form.addEventListener("change", () => {
             this._clear_results();
         });
 
@@ -128,7 +128,11 @@ export class formscreen extends fullscreen {
 
         this._append_child(container);
 
-        return () => {
+        return (target = null) => {
+            if (target !== null) {
+                input.value = target;
+            }
+
             return input.value;
         };
     }

+ 154 - 0
application/scripts/import_loop.js

@@ -0,0 +1,154 @@
+import { autocomplete_request } from "./autocomplete_request";
+import { create_request } from "./create_request";
+import { product_get_request } from "./product_get_request.js";
+
+export class import_loop {
+    #content;
+    #failed;
+    #on_autocomplete;
+    #on_create;
+    #on_single_fail;
+    #on_skip;
+    #on_single_success;
+    #finally;
+
+    on_autocomplete(target) {
+        this.#on_autocomplete = target;
+        return this;
+    }
+
+    on_create(target) {
+        this.#on_create = target;
+        return this;
+    }
+
+    on_single_fail(target) {
+        this.#on_single_fail = target;
+        return this;
+    }
+
+    on_skip(target) {
+        this.#on_skip = target;
+        return this;
+    }
+
+    on_single_success(target) {
+        this.#on_single_success = target;
+        return this;
+    }
+
+    finally(target) {
+        this.#finally = target;
+        return this;
+    }
+
+    constructor(dataset) {
+        this.#content = dataset;
+        this.#failed = new Array();
+        this.#on_autocomplete = null;
+        this.#on_create = null;
+        this.#on_single_fail = null;
+        this.#on_skip = null;
+        this.#on_single_success = null;
+        this.#finally = null;
+    }
+
+    async #autocomplete(target) {
+        if (this.#on_autocomplete !== null) {
+            try {
+                this.#on_autocomplete(target);
+            } catch (error) {
+                console.log(error);
+            }
+        }
+
+        const request = new autocomplete_request(target.barcode);
+        const response = await request.connect();
+
+        if (!response.result) {
+            throw new Error(response.cause);
+        }
+
+        const found = response.found;
+        target.description = found.description;
+
+        if (found.image.length === 0) {
+            throw new Error("Image for " + target.barcode + " not found.");
+        }
+
+        return new create_request(target, found.image);
+    }
+
+    async process() {
+        for (const count of this.#content) {
+            try {
+                await this.#create(count);
+            } catch(error) {
+                console.log(error);
+
+                if (this.#on_single_fail !== null) {
+                    try {
+                        this.#on_single_fail(count);
+                    } catch (error) {
+                        console.log(error);
+                    }
+                }
+
+                this.#failed.push(count);
+            }
+        }
+
+        if (this.#finally !== null) {
+            try {
+                this.#finally(this.#failed);
+            } catch (error) {
+                console.log(error);
+            }
+        }
+
+        return this;
+    }
+
+    async #exists(target) {
+        const request = new product_get_request(target.barcode);
+        const response = await request.connect();
+
+        return response.product !== null;
+    }
+
+    async #create(target) {
+        if (await this.#exists(target)) {
+            try {
+                this.#on_skip(target);
+            } catch (error) {
+                console.log(error);
+            }
+
+            return;
+        }
+
+        const request = await this.#autocomplete(target);
+
+        if (this.on_create !== null) {
+            try {
+                this.#on_create(target);
+            } catch (error) {
+                console.log(error);
+            }
+        }
+
+        const response = await request.connect();
+
+        if (!response.result) {
+            throw new Error(response.cause);
+        }
+
+        if (this.#on_single_success !== null) {
+            try {
+                this.#on_single_success(target);
+            } catch (error) {
+                console.log(error);
+            }
+        }
+    }
+}

+ 17 - 0
application/scripts/import_process_fail.js

@@ -0,0 +1,17 @@
+export class import_process_fail {
+    #product;
+    #error;
+
+    constructor(product, error) {
+        this.#product = product;
+        this.#error = error;
+    }
+
+    get error() {
+        return this.#error;
+    }
+
+    get product() {
+        return this.#product;
+    }
+}

+ 84 - 0
application/scripts/import_products.js

@@ -0,0 +1,84 @@
+import { formscreen } from "./formscreen";
+import { database } from "./database.js";
+import { autocomplete_database } from "./autocomplete_database.js";
+import { import_loop } from "./import_loop.js";
+import { searcher } from "./searcher.js";
+
+export class import_products extends formscreen {
+    #file;
+    #content;
+
+    get _name() {
+        return "Import products JSON";
+    }
+
+    _build_form() {
+        this._create_input("file", "Database:", "", (input) => {
+            this.#file = input;
+            input.type = "file";
+            input.accept = "application/json";
+        });
+    }
+
+    async #load_file() {
+        if (this.#file.files.length === 0) {
+            throw new Error("Select JSON products database first.");
+        }
+
+        const file = this.#file.files.item(0);
+        const text = await file.text();
+        
+        return JSON.parse(text);
+    }
+
+    async _process() {
+        try {
+            this._info = "Loading file...";
+            this.#content = await this.#load_file();
+            
+            this._info = "Parsing file to dataset...";
+            
+            const dataset = new database(this.#content)
+            .on_skip((fail) => {
+                this._info = "Skipping " + fail.product.barcode + "...";
+            })
+            .process()
+            .results();
+
+            const loop = new import_loop(dataset)
+            .on_autocomplete((target) => {
+                this._info = "Searching for " + target.barcode + "...";
+            })
+            .on_create((target) => {
+                this._info = "Creating " + target.barcode + "...";
+            })
+            .on_single_fail((target) => {
+                this._info = "Can not add " + target.barcode + "...";
+            })
+            .on_skip((target) => {
+                this._info = "Skipping " + target.barcode + "...";
+            })
+            .on_single_success((target) => {
+                this._info = "Created " + target.barcode + " success.";
+            })
+            .finally((broken) => {
+                searcher.reload();
+                
+                if (broken.length === 0) {
+                    this._success = "All items imported.";
+
+                    setTimeout(() => {
+                        this.hide();
+                    });
+                } else {
+                    console.log(broken);
+                    this._success = "Not all items imported...";
+                }
+            })
+            .process();
+        } catch (error) {
+            console.log(error);
+            this._error = new String(error);
+        }        
+    }
+}

+ 11 - 0
application/scripts/login_bar.js

@@ -1,6 +1,7 @@
 import { login_manager } from "./login_manager.js";
 import { login_prompt } from "./login_prompt.js";
 import { product_adder } from "./product_adder.js";
+import { import_products } from "./import_products.js";
 
 export class login_bar {
     #manager;
@@ -54,10 +55,20 @@ export class login_bar {
         add_product_button.classList.add("material-icons");
         target.appendChild(add_product_button);
 
+        const import_products_button = document.createElement("button");
+        import_products_button.innerText = "dataset_linked";
+        import_products_button.classList.add("material-icons");
+        import_products_button.classList.add("import-products-button");
+        target.appendChild(import_products_button);
+
         add_product_button.addEventListener("click", () => {
             new product_adder().show();
         });
 
+        import_products_button.addEventListener("click", () => {
+            new import_products().show();
+        });
+
         logout_button.addEventListener("click", () => {
             this.#manager.logout();
             location.reload();

+ 20 - 8
application/scripts/product.js

@@ -37,21 +37,33 @@ export class product {
         if ("barcode" in target) this.barcode = target["barcode"];
         if ("thumbnail" in target) this.thumbnail = target["thumbnail"];
         if ("on_stock" in target) this.on_stock = target["on_stock"];
+
+        try {
+            this.stock_count = Number(this.stock_count);
+        } catch {
+            this.stock_count = 0;
+        }
+
+        try {
+            this.on_stock = Number(this.on_stock);        
+        } catch {
+            this.on_stock = 0;
+        }
     }
 
     get dump() {
         const dumped = {
-            "name": this.name,
-            "description": this.description,
-            "author": this.author,
-            "image": this.image,
-            "stock_count": this.stock_count,
-            "barcode": this.barcode,
-            "thumbnail": this.thumbnail
+            "name": new String(this.name),
+            "description": new String(this.description),
+            "author": new String(this.author),
+            "image": new String(this.image),
+            "barcode": new String(this.barcode),
+            "thumbnail": new String(this.thumbnail),
+            "stock_count": new String(this.stock_count)
         };
 
         if (this.on_stock !== null) {
-            dumped["on_stock"] = this.on_stock;
+            dumped["on_stock"] = new String(this.on_stock);
         }
 
         return dumped;

+ 112 - 12
application/scripts/product_adder.js

@@ -3,6 +3,9 @@ import { create_request } from "./create_request.js";
 import { bool_response } from "./bool_response.js";
 import { product_base } from "./product_base.js";
 import { searcher } from "./searcher.js";
+import { autocomplete_request } from "./autocomplete_request.js";
+import { autocomplete_response } from "./autocomplete_response.js";
+import { product_give_back } from "./product_give_back.js";
 
 export class product_adder extends formscreen {
     #name;
@@ -11,12 +14,81 @@ export class product_adder extends formscreen {
     #barcode;
     #stock_count;
     #image;
+    #loaded_image_type;
+    #loaded_image;
+    #image_preview;
 
     get _name() {
         return "Add product";
     }
 
+    async #autocomplete() {
+        const barcode = this.#barcode();
+
+        if (barcode.length === 0) {
+            this._info = "Fill barcode first.";
+            return;
+        }
+        
+        this._info = "Searching in the web..."
+
+        try {
+            const request = new autocomplete_request(barcode);
+            const response = await request.connect();
+
+            if (!response.result) {
+                throw new Error(response.cause);
+            }
+            
+            const product = response.found;
+
+            this.#name(product.title);
+            this.#description(product.description);
+            this.#author(product.author);
+            this.#barcode(product.barcode);
+            this.#loaded_image = product.image;
+            this.#loaded_image_type = product.image_type;
+            
+            this.#update_image_preview();
+            
+            this._info = "Ready. Check results.";
+        } catch (error) {
+            this._error = new String(error);
+        }
+    }
+
+    #update_image_preview() {
+        this.#image_preview.src = (
+            "data:" 
+            + this.#loaded_image_type 
+            + ";base64,"
+            + this.#loaded_image
+        );
+        this.#image_preview.style.opacity = "1";
+    }
+
+    get #autocomplete_button() {
+        const button = document.createElement("div");
+        button.classList.add("autocomplete-button");
+        button.classList.add("button");
+
+        const icon = document.createElement("span");
+        icon.classList.add("material-icons")
+        icon.innerText = "auto_fix_normal";
+        button.appendChild(icon);
+
+        const text = document.createElement("span");
+        text.classList.add("text");
+        text.innerText = "Autocomplete";
+        button.appendChild(text);
+
+        return button;
+    }
+
     _build_form() {
+        this.#loaded_image = null;
+        this.#loaded_image_type = null;
+
         this.#name = this._create_input(
             "name", 
             "Name:", 
@@ -57,27 +129,57 @@ export class product_adder extends formscreen {
                 this.#image = input;
                 input.type = "file";
                 input.accept = "image/*";
+                
+                input.addEventListener("change", () => {
+                    this.#load_image_from_file();
+                });
             }
         );
+
+        this.#image_preview = document.createElement("img");
+        this.#image_preview.style.opacity = "0";
+        this._append_child(this.#image_preview);
+
+        const autocomplete = this.#autocomplete_button;
+        this._append_child(autocomplete);
+
+        autocomplete.addEventListener("click", () => {
+            this.#autocomplete();
+        });
     }
 
-    async #code_image() {
+    #reset_image() {
+        this.#loaded_image = null;
+        this.#loaded_image_type = null;
+        this.#image_preview.style.opacity = "0";
+        this.#image_preview.src = "";
+    }
+
+    async #load_image_from_file() {
         if (this.#image.files.length === 0) {
-            throw new Error("Upload image for product.");
+            this.#reset_image();
         }
 
         const file = this.#image.files.item(0);
         const buffer = await file.arrayBuffer();
-        const list = new Uint8Array(buffer);
-        
-        let content = new String();
+        let as_string = new String();
 
-        list.forEach((code) => {
-            content += String.fromCharCode(code);
+        new Uint8Array(buffer).forEach(letter => {
+            as_string += String.fromCharCode(letter);
         });
 
-        return btoa(content);
-    }   
+        this.#loaded_image = btoa(as_string);
+        this.#loaded_image_type = file.type;
+        this.#update_image_preview();
+    }
+
+    get #ready_image() {
+        if (this.#loaded_image === null) {
+            throw new Error("Loady any image first.");
+        }
+
+        return this.#loaded_image;
+    }
 
     async #submit() {
         const product = new product_base();
@@ -87,9 +189,7 @@ export class product_adder extends formscreen {
         product.stock_count = this.#stock_count();
         product.barcode = this.#barcode();
 
-        const image = await this.#code_image();
-
-        const request = new create_request(product, image);
+        const request = new create_request(product, this.#ready_image);
         const response = await request.connect();
 
         if (!response.result) {

+ 10 - 6
application/scripts/product_base.js

@@ -13,17 +13,21 @@ export class product_base {
         this.stock_count = this._extract(target, "stock_count");
 
         if (this.stock_count !== null) {
-            this.stock_count = Number(this.stock_count);
+            try {
+                this.stock_count = Number(this.stock_count);
+            } catch {
+                this.stock_count = 0;
+            }
         }
     }
 
     get dump() {
         return {
-            "name": this.name,
-            "description": this.description,
-            "author": this.author,
-            "barcode": this.barcode,
-            "stock_count": this.stock_count
+            "name": new String(this.name),
+            "description": new String(this.description),
+            "author": new String(this.author),
+            "barcode": new String(this.barcode),
+            "stock_count": new String(this.stock_count)
         }
     }
 

+ 1 - 0
application/scripts/product_container.js

@@ -159,6 +159,7 @@ export class product_container {
         image.classList.add("image");
         image.src = this.#target.thumbnail + this.#cache_bypass;
         image.alt = this.#target.name;
+        image.loading = "lazy";
 
         image.addEventListener("click", () => {
             new product_fullscreen(this.#target).show();

+ 46 - 10
application/scripts/product_editor.js

@@ -11,6 +11,9 @@ export class product_editor extends formscreen {
     #barcode;
     #stock_count;
     #image;
+    #loaded_image_type;
+    #loaded_image;
+    #image_preview;
 
     constructor(target) {
         super();
@@ -27,7 +30,10 @@ export class product_editor extends formscreen {
     }
 
     _build_form() {
-         this.#name = this._create_input(
+        this.#loaded_image = null;
+        this.#loaded_image_type = null;
+
+        this.#name = this._create_input(
             "name", 
             "Name:", 
             "Sample...",
@@ -82,26 +88,56 @@ export class product_editor extends formscreen {
                 this.#image = input;
                 input.type = "file";
                 input.accept = "image/*";
+
+                input.addEventListener("change", () => {
+                    this.#load_image_from_file();
+                });
             }
         );
+
+        this.#image_preview = document.createElement("img");
+        this.#image_preview.style.opacity = "1";
+        this.#image_preview.src = this.#target.image;
+        this._append_child(this.#image_preview);
+    }
+    
+    get #ready_image() {
+        return this.#loaded_image;
+    }
+
+    #update_image_preview() {
+        this.#image_preview.src = (
+            "data:" 
+            + this.#loaded_image_type 
+            + ";base64,"
+            + this.#loaded_image
+        );
+        this.#image_preview.style.opacity = "1";
     }
 
-    async #code_image() {
+    #reset_image() {
+        this.#loaded_image = null;
+        this.#loaded_image_type = null;
+        this.#image_preview.style.opacity = "0";
+        this.#image_preview.src = "";
+    }
+
+    async #load_image_from_file() {
         if (this.#image.files.length === 0) {
-            return null;
+            this.#reset_image();
         }
 
         const file = this.#image.files.item(0);
         const buffer = await file.arrayBuffer();
-        const list = new Uint8Array(buffer);
-
-        let content = new String();
+        let as_string = new String();
 
-        list.forEach((code) => {
-            content += String.fromCharCode(code);
+        new Uint8Array(buffer).forEach(letter => {
+            as_string += String.fromCharCode(letter);
         });
 
-        return btoa(content);
+        this.#loaded_image = btoa(as_string);
+        this.#loaded_image_type = file.type;
+        this.#update_image_preview();
     }
 
     async #submit() {
@@ -123,7 +159,7 @@ export class product_editor extends formscreen {
     }   
 
     async #image_submit() {
-        const image = await this.#code_image();
+        const image = await this.#ready_image;
 
         if (image === null) {
             return;

+ 28 - 0
application/scripts/product_get_request.js

@@ -0,0 +1,28 @@
+import { request } from "./request.js";
+import { product_response } from "./product_response.js";
+
+export class product_get_request extends request {
+    #barcode;
+
+    constructor(barcode) {
+        super();
+
+        this.#barcode = barcode;
+    }
+
+    get _response() {
+        return product_response;
+    }
+
+    get data() {
+        return null;
+    }
+
+    get method() {
+        return "GET";
+    }
+
+    get url() {
+        return "/product/get/barcode/" + new String(this.#barcode);
+    }
+}

+ 24 - 0
application/scripts/product_response.js

@@ -0,0 +1,24 @@
+import { bool_response } from "./bool_response";
+import { product } from "./product";
+
+export class product_response extends bool_response {
+    #product;
+
+    constructor(target) {
+        super(target);
+
+        this.#product = null;
+        
+        if (this.result) {
+            if (!("product" in target)) {
+                throw new Error("Incomplete response with good status.");
+            }
+
+            this.#product = new product(target.product);
+        }
+    }
+
+    get product() {
+        return this.#product;
+    }
+}

+ 24 - 4
application/scripts/request.js

@@ -2,11 +2,21 @@ import { login_manager } from "./login_manager.js";
 
 export class request {
     get settings() {
-        return {
+        const settings = {
             "method":  this.method,
             "headers": this.headers,
-            "body": this.body
         };
+
+        if (this.method === "GET" || this.method === "HEAD") {
+            return settings
+        }
+
+        if (this.body === null) {
+            return settings;
+        }
+
+        settings.body = this.body;
+        return settings;
     }
 
     get _apikey() {
@@ -28,10 +38,16 @@ export class request {
     }
     
     get headers() {
-        if (this.method === "GET") {
+        if (this.method === "GET" || this.method === "HEAD") {
             return {};
         }
 
+        if (typeof(this.data) === "string") {
+            return {
+                "Content-Type": "text/plain"
+            };
+        }
+
         return {
             "Content-Type": "application/json"
         };  
@@ -39,7 +55,11 @@ export class request {
 
     get body() {
         if (this.data === null) {
-            return "";
+            return null;
+        }
+
+        if (typeof(this.data) === "string") {
+            return this.data;
         }
 
         return JSON.stringify(this.data);

+ 24 - 0
application/theme/fullscreen.sass

@@ -14,6 +14,8 @@
         box-sizing: border-box
         width: 600px
         max-width: 100%
+        max-height: 80%
+        overflow-y: auto
         background-color: $primary
         color: $font
         border: $primary 2px solid
@@ -23,6 +25,28 @@
         align-items: stretch
         justify-content: stretch
 
+        img
+            transition: opacity 0.5s
+            width: 60%
+
+        .button
+            background-color: $primary
+            color: $secondary
+            display: flex
+            vertical-align: center
+            align-items: center
+            justify-content: center
+            flex-direction: row
+            padding: 10px
+            border-radius: 10px
+            border: none
+            gap: 5px
+            margin: 5px
+            transition: transform 0.5s
+
+            &:hover
+                transform: scale(1.1)
+
         .result
             background-color: $background
 

+ 27 - 5
assets/autoadder.py

@@ -1,4 +1,6 @@
 import json
+import base64
+import requests
 import googlesearch
 import google_images_search
 
@@ -20,17 +22,33 @@ class autoadder:
     def __search(self, phrase: str) -> object:
         for count in googlesearch.search(phrase, advanced= True):
             return count
+        
+        return phrase
 
     def __check_barcode(self, barcode: str) -> None:
         if barcode_validator(barcode).invalid:
             raise bad_request_exception("Invalid barcode")
 
+    def __image_request(self, source: str) -> str:
+        request = requests.get(source)
+
+        if not request.ok or not "Content-Type" in request.headers:
+            raise bad_request_exception("Can nor fetch image")
+
+        image = request.content
+        encoded = base64.b64encode(image)
+        extension = request.headers["Content-Type"]
+
+        return encoded, extension
+
     def find(self, barcode: str) -> dict:
         self.__check_barcode(barcode)
 
         title = self.__search(barcode).title
         description = title
         author = "Somebody"
+        image = ""
+        image_type = ""
 
         try:
             image_search = self.__images()
@@ -44,15 +62,19 @@ class autoadder:
         except Exception as error:
             raise autoadder_exception("Google API not work. Check API key.")
 
-        if len(images) < 1:
-            image = ""
-        else:
-            image = images[0].url
+        if len(images) > 0:
+            image, image_type = self.__image_request(images[0].url)
+
+        splited = title.split(" ")
+
+        if len(splited) > 3:
+            title = " ".join(splited[0:3])
 
         return {
             "title": title,
             "description": description,
             "author": author,
             "barcode": barcode,
-            "image": image
+            "image": image,
+            "image_type": image_type
         }

+ 4 - 0
assets/exception.py

@@ -58,6 +58,10 @@ class incomplete_request_exception(Exception):
 class reservation_exception(Exception):
     pass
 
+class empty_field_exception(Exception):
+    def __init__(self, name: str) -> None:
+        super().__init__("Field " + name + " is empty. Fill it.")
+
 class reservation_loader_exception(Exception):
     pass
 

+ 10 - 1
assets/product_builder.py

@@ -1,5 +1,6 @@
 from .product import product
 from .product import product_factory
+from .exception import empty_field_exception
 
 class product_builder:
     def __init__(self, target: product | None = None) -> None:
@@ -21,7 +22,15 @@ class product_builder:
             factory.author = target["author"]
 
         if "stock_count" in target:
-            factory.stock_count = int(target["stock_count"])
+            stock_count = target["stock_count"]
+
+            if type(stock_count) is str:
+                stock_count = stock_count.strip()
+            
+            if type(stock_count) is str and len(stock_count) == 0:
+                raise empty_field_exception("stock_count")
+            
+            factory.stock_count = int(stock_count)
 
         if "barcode" in target:
             factory.barcode = target["barcode"]

+ 1 - 0
requirements.txt

@@ -6,3 +6,4 @@ pillow
 beautifulsoup4
 googlesearch-python
 Google-Images-Search
+requests

+ 526 - 38
static/bundle/app.js

@@ -53,19 +53,29 @@
       if ("barcode" in target) this.barcode = target["barcode"];
       if ("thumbnail" in target) this.thumbnail = target["thumbnail"];
       if ("on_stock" in target) this.on_stock = target["on_stock"];
+      try {
+        this.stock_count = Number(this.stock_count);
+      } catch {
+        this.stock_count = 0;
+      }
+      try {
+        this.on_stock = Number(this.on_stock);
+      } catch {
+        this.on_stock = 0;
+      }
     }
     get dump() {
       const dumped = {
-        "name": this.name,
-        "description": this.description,
-        "author": this.author,
-        "image": this.image,
-        "stock_count": this.stock_count,
-        "barcode": this.barcode,
-        "thumbnail": this.thumbnail
+        "name": new String(this.name),
+        "description": new String(this.description),
+        "author": new String(this.author),
+        "image": new String(this.image),
+        "barcode": new String(this.barcode),
+        "thumbnail": new String(this.thumbnail),
+        "stock_count": new String(this.stock_count)
       };
       if (this.on_stock !== null) {
-        dumped["on_stock"] = this.on_stock;
+        dumped["on_stock"] = new String(this.on_stock);
       }
       return dumped;
     }
@@ -412,11 +422,18 @@
   // application/scripts/request.js
   var request = class {
     get settings() {
-      return {
+      const settings = {
         "method": this.method,
-        "headers": this.headers,
-        "body": this.body
+        "headers": this.headers
       };
+      if (this.method === "GET" || this.method === "HEAD") {
+        return settings;
+      }
+      if (this.body === null) {
+        return settings;
+      }
+      settings.body = this.body;
+      return settings;
     }
     get _apikey() {
       const manager = new login_manager();
@@ -432,16 +449,24 @@
       throw new TypeError("It must be overwrite.");
     }
     get headers() {
-      if (this.method === "GET") {
+      if (this.method === "GET" || this.method === "HEAD") {
         return {};
       }
+      if (typeof this.data === "string") {
+        return {
+          "Content-Type": "text/plain"
+        };
+      }
       return {
         "Content-Type": "application/json"
       };
     }
     get body() {
       if (this.data === null) {
-        return "";
+        return null;
+      }
+      if (typeof this.data === "string") {
+        return this.data;
       }
       return JSON.stringify(this.data);
     }
@@ -695,7 +720,7 @@
       title.appendChild(title_content);
       const form = document.createElement("form");
       center.appendChild(form);
-      form.addEventListener("click", () => {
+      form.addEventListener("change", () => {
         this._clear_results();
       });
       this.#form = document.createElement("div");
@@ -758,7 +783,10 @@
         throw new Error("Screen is not visible yet!");
       }
       this._append_child(container);
-      return () => {
+      return (target = null) => {
+        if (target !== null) {
+          input.value = target;
+        }
         return input.value;
       };
     }
@@ -816,16 +844,20 @@
       this.barcode = this._extract(target, "barcode");
       this.stock_count = this._extract(target, "stock_count");
       if (this.stock_count !== null) {
-        this.stock_count = Number(this.stock_count);
+        try {
+          this.stock_count = Number(this.stock_count);
+        } catch {
+          this.stock_count = 0;
+        }
       }
     }
     get dump() {
       return {
-        "name": this.name,
-        "description": this.description,
-        "author": this.author,
-        "barcode": this.barcode,
-        "stock_count": this.stock_count
+        "name": new String(this.name),
+        "description": new String(this.description),
+        "author": new String(this.author),
+        "barcode": new String(this.barcode),
+        "stock_count": new String(this.stock_count)
       };
     }
     _extract(dict, name) {
@@ -902,6 +934,9 @@
     #barcode;
     #stock_count;
     #image;
+    #loaded_image_type;
+    #loaded_image;
+    #image_preview;
     constructor(target) {
       super();
       this.#target = target;
@@ -913,6 +948,8 @@
       return "Product editor";
     }
     _build_form() {
+      this.#loaded_image = null;
+      this.#loaded_image_type = null;
       this.#name = this._create_input(
         "name",
         "Name:",
@@ -963,21 +1000,42 @@
           this.#image = input;
           input.type = "file";
           input.accept = "image/*";
+          input.addEventListener("change", () => {
+            this.#load_image_from_file();
+          });
         }
       );
+      this.#image_preview = document.createElement("img");
+      this.#image_preview.style.opacity = "1";
+      this.#image_preview.src = this.#target.image;
+      this._append_child(this.#image_preview);
+    }
+    get #ready_image() {
+      return this.#loaded_image;
+    }
+    #update_image_preview() {
+      this.#image_preview.src = "data:" + this.#loaded_image_type + ";base64," + this.#loaded_image;
+      this.#image_preview.style.opacity = "1";
     }
-    async #code_image() {
+    #reset_image() {
+      this.#loaded_image = null;
+      this.#loaded_image_type = null;
+      this.#image_preview.style.opacity = "0";
+      this.#image_preview.src = "";
+    }
+    async #load_image_from_file() {
       if (this.#image.files.length === 0) {
-        return null;
+        this.#reset_image();
       }
       const file = this.#image.files.item(0);
       const buffer = await file.arrayBuffer();
-      const list = new Uint8Array(buffer);
-      let content = new String();
-      list.forEach((code) => {
-        content += String.fromCharCode(code);
+      let as_string = new String();
+      new Uint8Array(buffer).forEach((letter) => {
+        as_string += String.fromCharCode(letter);
       });
-      return btoa(content);
+      this.#loaded_image = btoa(as_string);
+      this.#loaded_image_type = file.type;
+      this.#update_image_preview();
     }
     async #submit() {
       const copy = this.#target.copy();
@@ -994,7 +1052,7 @@
       this.#target = copy;
     }
     async #image_submit() {
-      const image = await this.#code_image();
+      const image = await this.#ready_image;
       if (image === null) {
         return;
       }
@@ -1511,6 +1569,7 @@
       image.classList.add("image");
       image.src = this.#target.thumbnail + this.#cache_bypass;
       image.alt = this.#target.name;
+      image.loading = "lazy";
       image.addEventListener("click", () => {
         new product_fullscreen(this.#target).show();
       });
@@ -1635,6 +1694,47 @@
     }
   };
 
+  // application/scripts/autocomplete_response.js
+  var autocomplete_response = class extends bool_response {
+    #found;
+    constructor(target) {
+      super(target);
+      this.#found = null;
+      if (this.result) {
+        this.#found = target["found"];
+      }
+    }
+    get found() {
+      if (this.#found === null) {
+        throw new Error("Server response is not complete.");
+      }
+      return this.#found;
+    }
+  };
+
+  // application/scripts/autocomplete_request.js
+  var autocomplete_request = class extends request {
+    #barcode;
+    constructor(barcode) {
+      super();
+      this.#barcode = barcode;
+    }
+    get _response() {
+      return autocomplete_response;
+    }
+    get data() {
+      return {
+        "apikey": this._apikey
+      };
+    }
+    get method() {
+      return "POST";
+    }
+    get url() {
+      return "/complete/barcode/" + this.#barcode;
+    }
+  };
+
   // application/scripts/product_adder.js
   var product_adder = class extends formscreen {
     #name;
@@ -1643,10 +1743,59 @@
     #barcode;
     #stock_count;
     #image;
+    #loaded_image_type;
+    #loaded_image;
+    #image_preview;
     get _name() {
       return "Add product";
     }
+    async #autocomplete() {
+      const barcode = this.#barcode();
+      if (barcode.length === 0) {
+        this._info = "Fill barcode first.";
+        return;
+      }
+      this._info = "Searching in the web...";
+      try {
+        const request2 = new autocomplete_request(barcode);
+        const response = await request2.connect();
+        if (!response.result) {
+          throw new Error(response.cause);
+        }
+        const product2 = response.found;
+        this.#name(product2.title);
+        this.#description(product2.description);
+        this.#author(product2.author);
+        this.#barcode(product2.barcode);
+        this.#loaded_image = product2.image;
+        this.#loaded_image_type = product2.image_type;
+        this.#update_image_preview();
+        this._info = "Ready. Check results.";
+      } catch (error) {
+        this._error = new String(error);
+      }
+    }
+    #update_image_preview() {
+      this.#image_preview.src = "data:" + this.#loaded_image_type + ";base64," + this.#loaded_image;
+      this.#image_preview.style.opacity = "1";
+    }
+    get #autocomplete_button() {
+      const button = document.createElement("div");
+      button.classList.add("autocomplete-button");
+      button.classList.add("button");
+      const icon = document.createElement("span");
+      icon.classList.add("material-icons");
+      icon.innerText = "auto_fix_normal";
+      button.appendChild(icon);
+      const text = document.createElement("span");
+      text.classList.add("text");
+      text.innerText = "Autocomplete";
+      button.appendChild(text);
+      return button;
+    }
     _build_form() {
+      this.#loaded_image = null;
+      this.#loaded_image_type = null;
       this.#name = this._create_input(
         "name",
         "Name:",
@@ -1686,21 +1835,45 @@
           this.#image = input;
           input.type = "file";
           input.accept = "image/*";
+          input.addEventListener("change", () => {
+            this.#load_image_from_file();
+          });
         }
       );
+      this.#image_preview = document.createElement("img");
+      this.#image_preview.style.opacity = "0";
+      this._append_child(this.#image_preview);
+      const autocomplete = this.#autocomplete_button;
+      this._append_child(autocomplete);
+      autocomplete.addEventListener("click", () => {
+        this.#autocomplete();
+      });
+    }
+    #reset_image() {
+      this.#loaded_image = null;
+      this.#loaded_image_type = null;
+      this.#image_preview.style.opacity = "0";
+      this.#image_preview.src = "";
     }
-    async #code_image() {
+    async #load_image_from_file() {
       if (this.#image.files.length === 0) {
-        throw new Error("Upload image for product.");
+        this.#reset_image();
       }
       const file = this.#image.files.item(0);
       const buffer = await file.arrayBuffer();
-      const list = new Uint8Array(buffer);
-      let content = new String();
-      list.forEach((code) => {
-        content += String.fromCharCode(code);
+      let as_string = new String();
+      new Uint8Array(buffer).forEach((letter) => {
+        as_string += String.fromCharCode(letter);
       });
-      return btoa(content);
+      this.#loaded_image = btoa(as_string);
+      this.#loaded_image_type = file.type;
+      this.#update_image_preview();
+    }
+    get #ready_image() {
+      if (this.#loaded_image === null) {
+        throw new Error("Loady any image first.");
+      }
+      return this.#loaded_image;
     }
     async #submit() {
       const product2 = new product_base();
@@ -1709,8 +1882,7 @@
       product2.author = this.#author();
       product2.stock_count = this.#stock_count();
       product2.barcode = this.#barcode();
-      const image = await this.#code_image();
-      const request2 = new create_request(product2, image);
+      const request2 = new create_request(product2, this.#ready_image);
       const response = await request2.connect();
       if (!response.result) {
         throw new Error(response.cause);
@@ -1731,6 +1903,314 @@
     }
   };
 
+  // application/scripts/import_process_fail.js
+  var import_process_fail = class {
+    #product;
+    #error;
+    constructor(product2, error) {
+      this.#product = product2;
+      this.#error = error;
+    }
+    get error() {
+      return this.#error;
+    }
+    get product() {
+      return this.#product;
+    }
+  };
+
+  // application/scripts/database.js
+  var database = class {
+    #content;
+    #processed;
+    #on_skip;
+    constructor(content) {
+      this.#content = content;
+      this.#processed = /* @__PURE__ */ new Map();
+      this.#on_skip = null;
+    }
+    #append(target) {
+      if (this.#processed.has(target.barcode)) {
+        this.#processed.get(target.barcode).stock_count += 1;
+        return;
+      }
+      this.#processed.set(target.barcode, target);
+    }
+    #validate(target) {
+      if (!("id" in target)) {
+        throw new Error("One of item has no ID.");
+      }
+      if (!("title" in target)) {
+        throw new Error("Product " + target.barcode + " has no title.");
+      }
+      if (!("author" in target)) {
+        throw new Error("Product " + target.barcode + " has no author.");
+      }
+    }
+    #convert(target) {
+      this.#validate(target);
+      const product2 = new product_base();
+      product2.name = target.title;
+      product2.description = "";
+      product2.author = target.author;
+      product2.stock_count = 1;
+      product2.barcode = target.id;
+      return product2;
+    }
+    on_skip(target) {
+      this.#on_skip = target;
+      return this;
+    }
+    process() {
+      this.#processed.clear();
+      if (!(this.#content instanceof Array)) {
+        throw new Error("Database woud be array of objects.");
+      }
+      this.#content.forEach((count) => {
+        try {
+          const product2 = this.#convert(count);
+          this.#append(product2);
+        } catch (error) {
+          if (this.#on_skip === null) {
+            return;
+          }
+          try {
+            this.#on_skip(new import_process_fail(count, error));
+          } catch (fail) {
+            console.log(fail);
+          }
+        }
+      });
+      return this;
+    }
+    results() {
+      return Array.from(this.#processed.values());
+    }
+  };
+
+  // application/scripts/product_response.js
+  var product_response = class extends bool_response {
+    #product;
+    constructor(target) {
+      super(target);
+      this.#product = null;
+      if (this.result) {
+        if (!("product" in target)) {
+          throw new Error("Incomplete response with good status.");
+        }
+        this.#product = new product(target.product);
+      }
+    }
+    get product() {
+      return this.#product;
+    }
+  };
+
+  // application/scripts/product_get_request.js
+  var product_get_request = class extends request {
+    #barcode;
+    constructor(barcode) {
+      super();
+      this.#barcode = barcode;
+    }
+    get _response() {
+      return product_response;
+    }
+    get data() {
+      return null;
+    }
+    get method() {
+      return "GET";
+    }
+    get url() {
+      return "/product/get/barcode/" + new String(this.#barcode);
+    }
+  };
+
+  // application/scripts/import_loop.js
+  var import_loop = class {
+    #content;
+    #failed;
+    #on_autocomplete;
+    #on_create;
+    #on_single_fail;
+    #on_skip;
+    #on_single_success;
+    #finally;
+    on_autocomplete(target) {
+      this.#on_autocomplete = target;
+      return this;
+    }
+    on_create(target) {
+      this.#on_create = target;
+      return this;
+    }
+    on_single_fail(target) {
+      this.#on_single_fail = target;
+      return this;
+    }
+    on_skip(target) {
+      this.#on_skip = target;
+      return this;
+    }
+    on_single_success(target) {
+      this.#on_single_success = target;
+      return this;
+    }
+    finally(target) {
+      this.#finally = target;
+      return this;
+    }
+    constructor(dataset) {
+      this.#content = dataset;
+      this.#failed = new Array();
+      this.#on_autocomplete = null;
+      this.#on_create = null;
+      this.#on_single_fail = null;
+      this.#on_skip = null;
+      this.#on_single_success = null;
+      this.#finally = null;
+    }
+    async #autocomplete(target) {
+      if (this.#on_autocomplete !== null) {
+        try {
+          this.#on_autocomplete(target);
+        } catch (error) {
+          console.log(error);
+        }
+      }
+      const request2 = new autocomplete_request(target.barcode);
+      const response = await request2.connect();
+      if (!response.result) {
+        throw new Error(response.cause);
+      }
+      const found = response.found;
+      target.description = found.description;
+      if (found.image.length === 0) {
+        throw new Error("Image for " + target.barcode + " not found.");
+      }
+      return new create_request(target, found.image);
+    }
+    async process() {
+      for (const count of this.#content) {
+        try {
+          await this.#create(count);
+        } catch (error) {
+          console.log(error);
+          if (this.#on_single_fail !== null) {
+            try {
+              this.#on_single_fail(count);
+            } catch (error2) {
+              console.log(error2);
+            }
+          }
+          this.#failed.push(count);
+        }
+      }
+      if (this.#finally !== null) {
+        try {
+          this.#finally(this.#failed);
+        } catch (error) {
+          console.log(error);
+        }
+      }
+      return this;
+    }
+    async #exists(target) {
+      const request2 = new product_get_request(target.barcode);
+      const response = await request2.connect();
+      return response.product !== null;
+    }
+    async #create(target) {
+      if (await this.#exists(target)) {
+        try {
+          this.#on_skip(target);
+        } catch (error) {
+          console.log(error);
+        }
+        return;
+      }
+      const request2 = await this.#autocomplete(target);
+      if (this.on_create !== null) {
+        try {
+          this.#on_create(target);
+        } catch (error) {
+          console.log(error);
+        }
+      }
+      const response = await request2.connect();
+      if (!response.result) {
+        throw new Error(response.cause);
+      }
+      if (this.#on_single_success !== null) {
+        try {
+          this.#on_single_success(target);
+        } catch (error) {
+          console.log(error);
+        }
+      }
+    }
+  };
+
+  // application/scripts/import_products.js
+  var import_products = class extends formscreen {
+    #file;
+    #content;
+    get _name() {
+      return "Import products JSON";
+    }
+    _build_form() {
+      this._create_input("file", "Database:", "", (input) => {
+        this.#file = input;
+        input.type = "file";
+        input.accept = "application/json";
+      });
+    }
+    async #load_file() {
+      if (this.#file.files.length === 0) {
+        throw new Error("Select JSON products database first.");
+      }
+      const file = this.#file.files.item(0);
+      const text = await file.text();
+      return JSON.parse(text);
+    }
+    async _process() {
+      try {
+        this._info = "Loading file...";
+        this.#content = await this.#load_file();
+        this._info = "Parsing file to dataset...";
+        const dataset = new database(this.#content).on_skip((fail) => {
+          this._info = "Skipping " + fail.product.barcode + "...";
+        }).process().results();
+        const loop = new import_loop(dataset).on_autocomplete((target) => {
+          this._info = "Searching for " + target.barcode + "...";
+        }).on_create((target) => {
+          this._info = "Creating " + target.barcode + "...";
+        }).on_single_fail((target) => {
+          this._info = "Can not add " + target.barcode + "...";
+        }).on_skip((target) => {
+          this._info = "Skipping " + target.barcode + "...";
+        }).on_single_success((target) => {
+          this._info = "Created " + target.barcode + " success.";
+        }).finally((broken) => {
+          searcher.reload();
+          if (broken.length === 0) {
+            this._success = "All items imported.";
+            setTimeout(() => {
+              this.hide();
+            });
+          } else {
+            console.log(broken);
+            this._success = "Not all items imported...";
+          }
+        }).process();
+      } catch (error) {
+        console.log(error);
+        this._error = new String(error);
+      }
+    }
+  };
+
   // application/scripts/login_bar.js
   var login_bar = class {
     #manager;
@@ -1772,9 +2252,17 @@
       add_product_button.classList.add("add-product-button");
       add_product_button.classList.add("material-icons");
       target.appendChild(add_product_button);
+      const import_products_button = document.createElement("button");
+      import_products_button.innerText = "dataset_linked";
+      import_products_button.classList.add("material-icons");
+      import_products_button.classList.add("import-products-button");
+      target.appendChild(import_products_button);
       add_product_button.addEventListener("click", () => {
         new product_adder().show();
       });
+      import_products_button.addEventListener("click", () => {
+        new import_products().show();
+      });
       logout_button.addEventListener("click", () => {
         this.#manager.logout();
         location.reload();

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
static/bundle/theme.css


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно