Cixo Develop 5 сар өмнө
parent
commit
b46245cbe8

+ 22 - 16
application/scripts/confirm_action.js

@@ -19,6 +19,10 @@ export class confirm_action {
         throw new TypeError("It must be overwriten.");
     }
 
+    get _info() {
+        return false;
+    }
+
     show() {
         if (this.#node !== null) {
             return;
@@ -86,25 +90,27 @@ export class confirm_action {
         cancel_button.classList.add("material-icons");
         cancel_button.innerText = "clear";
         buttons.appendChild(cancel_button);
-
-        const confirm_button = document.createElement("button");
-        confirm_button.classList.add("confirm");
-        confirm_button.classList.add("material-icons");
-        confirm_button.innerText = "send";
-        buttons.appendChild(confirm_button);
-
+ 
         cancel_button.addEventListener("click", () => {
             this.hide();
         });
-
-        confirm_button.addEventListener("click", () => {
-            if (this.#action === false) {
-                return;
-            }
-
-            this._action();
-            this.hide();
-        });
+       
+        if (!this._info) {
+            const confirm_button = document.createElement("button");
+            confirm_button.classList.add("confirm");
+            confirm_button.classList.add("material-icons");
+            confirm_button.innerText = "send";
+            buttons.appendChild(confirm_button);
+            
+            confirm_button.addEventListener("click", () => {
+                if (this.#action === false) {
+                    return;
+                }
+
+                this._action();
+                this.hide();
+            });
+        }
 
         return container;
     }

+ 28 - 10
application/scripts/formscreen.js

@@ -27,6 +27,10 @@ export class formscreen extends fullscreen {
         return this.get_query("input[name=\"" + name + "\"]");
     }
 
+    get _has_submit() {
+        return true;
+    }
+
     _build_node() {
         const center = document.createElement("div");
         center.classList.add("center");
@@ -64,13 +68,15 @@ export class formscreen extends fullscreen {
         close_button.innerText = "close";
         close_button.type = "button";
         bottom.appendChild(close_button);
-       
-        const send_button = document.createElement("button");
-        send_button.classList.add("send");
-        send_button.classList.add("material-icons");
-        send_button.innerText = "send";
-        send_button.type = "submit";
-        bottom.appendChild(send_button);
+      
+        if (this._has_submit) {
+            const send_button = document.createElement("button");
+            send_button.classList.add("send");
+            send_button.classList.add("material-icons");
+            send_button.innerText = "send";
+            send_button.type = "submit";
+            bottom.appendChild(send_button);
+        }
 
         close_button.addEventListener("click", () => {
             this.hide();
@@ -82,11 +88,19 @@ export class formscreen extends fullscreen {
             this._process();
         });
 
-        this._build_form();
-
+        this._refresh();
+        
         return center;
     }
 
+    _refresh() {
+        while (this.#form.lastChild) {
+            this.#form.lastChild.remove();
+        }
+
+        this._build_form();
+    }
+
     _create_input(name, label_text, placeholder, worker = null) {
         const container = document.createElement("div");
         container.classList.add("input-container");
@@ -112,13 +126,17 @@ export class formscreen extends fullscreen {
             throw new Error("Screen is not visible yet!");
         }
 
-        this.#form.appendChild(container);
+        this._append_child(container);
 
         return () => {
             return input.value;
         };
     }
 
+    _append_child(target) {
+        this.#form.appendChild(target);
+    }
+
     _clear_results() {
         if (!this.#result) {
             return;

+ 127 - 0
application/scripts/product_all_rents.js

@@ -0,0 +1,127 @@
+import { formscreen } from "./formscreen";
+import { product_give_back_request } from "./product_give_back_request.js";
+import { product_reservations_request } from "./product_reservations_request";
+import { searcher } from "./searcher.js";
+
+export class product_all_rents extends formscreen {
+    #target;
+
+    constructor(target) {
+        super();
+
+        this.#target = target;
+    }
+
+    get _name() {
+        return "All rents";
+    }
+
+    get _has_submit() {
+        return false;
+    }
+
+    #create_single(target) {
+        const container = document.createElement("div");
+        container.classList.add("reservation-info");
+        
+        if (target.phone_number !== null) {
+            const phone_icon = document.createElement("span");
+            phone_icon.classList.add("material-icons");
+            phone_icon.innerText = "phone";
+
+            const phone_number = document.createElement("span");
+            phone_number.classList.add("numbers")
+            phone_number.innerText = target.phone_number;
+
+            const phone_number_container = document.createElement("p");
+            phone_number_container.appendChild(phone_icon);
+            phone_number_container.appendChild(phone_number);
+            container.appendChild(phone_number_container);
+        }
+
+        if (target.email !== null) {
+            const email_icon = document.createElement("span");
+            email_icon.classList.add("material-icons");
+            email_icon.innerText = "mail";
+
+            const email = document.createElement("span");
+            email.innerText = target.email;
+
+            const email_container = document.createElement("p");
+            email_container.appendChild(email_icon);
+            email_container.appendChild(email);
+            container.appendChild(email_container);
+        }
+
+        return container;
+    }
+
+    #create_single_button(target) {
+        const button = document.createElement("button");
+        button.classList.add("material-icons");
+        button.classList.add("give-back-button");
+        button.innerText = "save_alt";
+
+        button.addEventListener("click", async () => {
+            try {
+                this._info = "Processing...";
+
+                const request = new product_give_back_request(target);
+                const response = await request.connect();
+
+                if (!response.result) {
+                    throw new Error(response.cause);
+                }
+
+                this._refresh();
+                searcher.reload();
+            } catch (error) {
+                this._error = String(error);
+            }
+        });
+
+        return button;
+    }
+
+    _process() {
+        return;
+    }
+
+    async _build_form() {
+        try {
+            this._info = "Loading...";
+
+            const request = new product_reservations_request(this.#target);
+            const response = await request.connect();
+
+            const list = document.createElement("div");
+            list.classList.add("reservations-list");
+
+            let empty = true;
+
+            response.collection.forEach(count => {
+                const item = document.createElement("div");
+                item.classList.add("reservation");
+
+                const left = this.#create_single(count);
+                const right = this.#create_single_button(count);
+
+                empty = false;
+
+                item.appendChild(left);
+                item.appendChild(right);
+                list.appendChild(item);
+            });
+            
+            this._append_child(list);
+            
+            if (empty) {
+                this._success = "Not found any reservations.";
+            } else {
+                this._clear_results();
+            }
+        } catch (error) {
+            this._error = String(error);
+        }
+    }
+}

+ 7 - 1
application/scripts/product_container.js

@@ -5,6 +5,8 @@ import { delete_product_window } from "./delete_product_window.js";
 import { product_editor } from "./product_editor.js";
 import { product_rent } from "./product_rent.js";
 import { product_give_back } from "./product_give_back.js";
+import { product_all_rents } from "./product_all_rents.js";
+import { product_not_avairable } from "./product_not_avairable.js";
 
 export class product_container {
     #target;
@@ -71,7 +73,11 @@ export class product_container {
         });
 
         rent_button.addEventListener("click", () => {
-            new product_rent(this.#target).show();
+            if (this.#target.on_stock > 0) {
+                new product_rent(this.#target).show();
+            } else {
+                new product_not_avairable().show();
+            }
         });
 
         give_back_button.addEventListener("click", () => {

+ 6 - 0
application/scripts/product_give_back.js

@@ -2,6 +2,7 @@ import { rents_screen } from "./rents_screen.js";
 import { reservation } from "./reservation.js";
 import { reservation_factory } from "./reservation_factory.js";
 import { product_give_back_request } from "./product_give_back_request.js";
+import { searcher } from "./searcher.js";
 
 export class product_give_back extends rents_screen {
     get _name() {
@@ -26,6 +27,11 @@ export class product_give_back extends rents_screen {
             }
 
             this._success = "Success!";
+            searcher.reload();
+
+            setTimeout(() => {
+                this.hide();
+            }, 500);
         } catch (error) {
             this._error = String(error);
         }

+ 15 - 0
application/scripts/product_not_avairable.js

@@ -0,0 +1,15 @@
+import { confirm_action } from "./confirm_action";
+
+export class product_not_avairable extends confirm_action {
+    get _title() {
+        return "Error";
+    }    
+
+    get _description() {
+        return "This product is not avairable. Anybody can not rent it.";
+    }
+
+    get _info() {
+        return true;
+    }
+}

+ 6 - 0
application/scripts/product_rent.js

@@ -2,6 +2,7 @@ import { rents_screen } from "./rents_screen.js";
 import { product_rent_request } from "./product_rent_request.js";
 import { reservation } from "./reservation.js";
 import { reservation_factory } from "./reservation_factory.js";
+import { searcher } from "./searcher.js";
 
 export class product_rent extends rents_screen {
     get _name() {
@@ -26,6 +27,11 @@ export class product_rent extends rents_screen {
             }
 
             this._success = "New rent added.";
+            searcher.reload();
+
+            setTimeout(() => {
+                this.hide();
+            }, 500);
         } catch (error) {
             this._error = String(error);
         }

+ 30 - 0
application/scripts/product_reservations_request.js

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

+ 10 - 2
application/scripts/rents_screen.js

@@ -36,10 +36,18 @@ export class rents_screen extends formscreen {
         this.#phone = this._create_input(
             "phone",
             "Phone number:",
-            "123-456-789",
+            "+1 123-456-789",
             (input) => {
                 input.type = "tel";
-                input.pattern = "[0-9]{3}-[0-9]{3}-[0-9]{3}";
+               
+                const add_prefix = () => {
+                    if (input.value.length === 0) {
+                        input.value = "+48 ";
+                    }
+                };
+
+                input.addEventListener("click", add_prefix);
+                input.addEventListener("focus", add_prefix);
             }
         );
     }

+ 1 - 1
application/scripts/reservation_factory.js

@@ -8,7 +8,7 @@ export class reservation_factory {
     }
 
     phone_number(target) {
-        target = target.trim();
+        target = target.trim().replaceAll("-", "");
 
         if (target.length === 0) {
             target = null;

+ 28 - 0
application/scripts/reservations_response.js

@@ -0,0 +1,28 @@
+import { bool_response } from "./bool_response";
+import { reservation } from "./reservation.js";
+
+export class reservations_response extends bool_response {
+    #collection;
+
+    constructor(target) {
+        super(target);
+
+        if (!this.result) {
+            return;
+        }
+
+        this.#collection = new Array();
+
+        target["reservations"].forEach(count => {
+            this.#collection.push(new reservation(count));            
+        });
+    }
+
+    get collection() {
+        if (!this.result) {
+            throw new Error(this.cause)
+        }
+
+        return this.#collection;
+    }
+}

+ 1 - 0
application/theme/core.sass

@@ -6,3 +6,4 @@
 @import fullscreen
 @import scroll
 @import confirm_window
+@import reservations

+ 3 - 1
application/theme/positions.sass

@@ -95,9 +95,11 @@
             .manage
                 display: flex
                 flex-direction: row
-                justify-content: center
+                justify-content: right
                 flex-wrap: wrap
                 gap: 10px
+                width: 40%
+                margin-bottom: 10px
 
             button
                 background-color: $primary

+ 31 - 0
application/theme/reservations.sass

@@ -0,0 +1,31 @@
+.reservations-list
+    display: flex
+    flex-direction: column
+    gap: 10px
+
+    .reservation
+        display: flex
+        flex-direction: row
+        align-items: center
+        justify-content: space-between
+        gap: 10px
+
+        button  
+            padding: 10px
+            border-radius: 10px
+            background-color: $primary
+            color: $secondary
+            border: none
+            transition: transform 0.5s
+
+            &:hover
+                transform: scale(1.1)
+
+        p
+            display: inline-flex
+            flex-align: center
+            align-items: center
+            gap: 10px
+            margin: 0px
+            margin-right: 10px
+            padding: 0px

+ 4 - 0
assets/__init__.py

@@ -61,3 +61,7 @@ from .reservation import reservation_factory
 from .reservations_collection import reservations_collection
 from .reservation_loader import reservation_loader
 from .reservation_app import reservation_app
+
+""" Auto adder. """
+from .autoadder import autoadder
+from .autoadder_app import autoadder_app

+ 21 - 1
assets/app_config.py

@@ -9,12 +9,32 @@ class app_config(config):
             "database_uri": "sqlite:///database.db",
             "users_file": "users.json",
             "covers_dir": "covers/",
-            "thumbnails_dimension": "400"
+            "thumbnails_dimension": "400",
+            "google_api_key": "",
+            "google_cx": ""
         }
 
     def __init__(self):
         super().__init__(app_config.__defaults())
 
+    @property
+    def google_api_key(self) -> str | None:
+        api_key = self._get("google_api_key")
+
+        if len(api_key.strip()) == 0:
+            return None
+        
+        return api_key
+
+    @property
+    def google_cx(self) -> str | None:
+        cx = self._get("google_cx")
+
+        if len(cx.strip()) == "0":
+            return None
+
+        return cx
+
     @property
     def database_uri(self) -> str:
         return self._get("database_uri")

+ 22 - 0
assets/app_resources.py

@@ -9,6 +9,8 @@ from .product_app import product_app
 from .users_app import users_app
 from .directory_image import directory_image
 from .reservation_app import reservation_app
+from .autoadder import autoadder
+from .autoadder_app import autoadder_app
 
 class app_resources:
     def __init__(self, config: object) -> None:
@@ -25,6 +27,13 @@ class app_resources:
             self.config.covers_path,
             self.config.thumbnails_dimension
         )
+
+        self.__autoadder = None
+        google_api_key = self.config.google_api_key
+        google_cx = self.config.google_cx
+
+        if google_api_key is not None and google_cx is not None: 
+            self.__autoadder = autoadder(google_api_key, google_cx)
         
         self.__product_app = product_app(
             self.database, 
@@ -37,6 +46,19 @@ class app_resources:
             self.users
         )
 
+        self.__autoadder_app = autoadder_app(
+            self.users,
+            self.autoadder
+        )
+
+    @property
+    def autoadder(self) -> autoadder:
+        return self.__autoadder
+
+    @property
+    def autoadder_app(self) -> autoadder_app:
+        return self.__autoadder_app
+
     @property
     def users_app(self) -> users_app:
         return self.__users_app

+ 58 - 0
assets/autoadder.py

@@ -0,0 +1,58 @@
+import json
+import googlesearch
+import google_images_search
+
+from .validator import barcode_validator
+from .exception import bad_request_exception
+from .exception import autoadder_exception
+
+class autoadder:
+    def __init__(self, apikey: str, engine: str) -> None:
+        self.__apikey = apikey
+        self.__engine = engine
+
+    def __images(self) -> object:
+        return google_images_search.GoogleImagesSearch(
+            self.__apikey,
+            self.__engine
+        )
+
+    def __search(self, phrase: str) -> object:
+        for count in googlesearch.search(phrase, advanced= True):
+            return count
+
+    def __check_barcode(self, barcode: str) -> None:
+        if barcode_validator(barcode).invalid:
+            raise bad_request_exception("Invalid barcode")
+
+    def find(self, barcode: str) -> dict:
+        self.__check_barcode(barcode)
+
+        title = self.__search(barcode).title
+        description = title
+        author = "Somebody"
+
+        try:
+            image_search = self.__images()
+            image_search.search(search_params = {
+                "q": barcode,
+                "num": 1,
+                "filetype": "jpg"
+            })
+
+            images = image_search.results()
+        except Exception as error:
+            raise autoadder_exception("Google API not work. Check API key.")
+
+        if len(images) < 1:
+            image = ""
+        else:
+            image = images[0].url
+
+        return {
+            "title": title,
+            "description": description,
+            "author": author,
+            "barcode": barcode,
+            "image": image
+        }

+ 44 - 0
assets/autoadder_app.py

@@ -0,0 +1,44 @@
+from .autoadder import autoadder
+from .app_route import app_route
+from .exception import autoadder_exception
+from .exception import incomplete_request_exception
+from .exception import access_denied_exception
+from .users_collection import users_collection
+
+class autoadder_app(app_route):
+    def __init__(
+        self, 
+        users: users_collection, 
+        target: autoadder | None
+    ) -> None:
+        super().__init__()
+
+        self.__users = users
+        self.__worker = target
+
+    def __login(self, params: dict) -> None:
+        if not "apikey" in params:
+            raise incomplete_request_exception("apikey")
+
+        if self.__users.get(params["apikey"]) is None:
+            raise access_denied_exception()
+
+    @property 
+    def worker(self) -> autoadder:
+        if self.__worker is None:
+            raise autoadder_exception("Google Clous API key and CS not set.")
+        
+        return self.__worker
+
+    def find(self, params: dict) -> dict:
+        try:
+            self.__login
+
+            if not "barcode" in params:
+                raise incomplete_request_exception("barcode")
+
+            result = self.worker.find(params["barcode"])
+            return self._success(found = result)
+
+        except Exception as error:
+            return self._fail(str(error))

+ 1 - 1
assets/config.py

@@ -109,7 +109,7 @@ class config_loader:
     def load(self, where: pathlib.Path) -> object:
         if not where.is_file():
             content = "Can not found required config file \""
-            content = content + where.absolute() + "\"."
+            content = content + str(where.absolute()) + "\"."
 
             raise config_exception(content)
         

+ 3 - 0
assets/exception.py

@@ -41,6 +41,9 @@ class image_exception(Exception):
 class directory_image_exception(Exception):
     pass
 
+class autoadder_exception(Exception):
+    pass
+
 class image_save_exception(Exception):
     def __init__(self, before: Exception) -> None:
         super().__init__("Can not save image: " + str(before))

+ 0 - 4
assets/product.py

@@ -54,10 +54,6 @@ class product(sqlmodel.SQLModel, table = True):
 
         return current
     
-    @property
-    def avairable(self) -> bool:
-        return self.on_stock > 0
-
     def save_copy(self) -> object:
         """
         This return clone of the product, which is not connect with 

+ 1 - 0
assets/product_response.py

@@ -26,6 +26,7 @@ class product_response:
             "image": covers + self.images.get_full_name(target),
             "thumbnail": covers + self.images.get_thumbnail_name(target),
             "stock_count": target.stock_count,
+            "on_stock": target.on_stock,
             "barcode": target.barcode
         }
 

+ 4 - 4
assets/requests.py

@@ -18,10 +18,10 @@ class user_login_request(pydantic.BaseModel):
 
 class reservation_request(pydantic.BaseModel):
     apikey: str
-    target_name: typing.Optional[str]
-    target_barcode: typing.Optional[str]
-    email: typing.Optional[str]
-    phone_number: typing.Optional[str]
+    target_name: typing.Optional[str] = None
+    target_barcode: typing.Optional[str] = None
+    email: typing.Optional[str] = None
+    phone_number: typing.Optional[str] = None
 
     model_config = {
         "json_schema_extra": {

+ 9 - 0
assets/reservation_app.py

@@ -88,6 +88,9 @@ class reservation_app(app_route_database):
             rent = self.__create_factory(request).target(item).result()
             
             with self.__reservation_loader as loader:
+                if not self.__product_avairable(item):
+                    raise bad_request_exception("Item already not avairable") 
+
                 if not loader.store(rent):
                     raise database_exception()
 
@@ -141,6 +144,7 @@ class reservation_app(app_route_database):
 
         return True
 
+
     def __get_reservation(self, request: deep_request) -> reservation:
         with self.__reservation_loader as loader:
             item = self.__load_product(request)
@@ -213,11 +217,16 @@ class reservation_app(app_route_database):
 
             raise not_found_exception()
 
+    def __product_avairable(self, target: product) -> bool:
+        with self.__product_loader as loader:
+            return loader.get_by_barcode(target.barcode).on_stock > 0
+
     def __product_by_name(self, request: deep_request) -> product:
         with self.__product_loader as loader:
             product = loader.get_by_name(request.target_name) 
 
             if product is not None:
+                product.on_stock
                 return product
             
             raise not_found_exception()

+ 24 - 8
assets/server.py

@@ -21,6 +21,7 @@ class server(fastapi.FastAPI):
         self.__route_product()
         self.__route_users()
         self.__route_reservations()
+        self.__route_autoadder()
 
     @property
     def resources(self) -> app_resources:
@@ -38,6 +39,10 @@ class server(fastapi.FastAPI):
     def reservation_app(self) -> reservation_app:
         return self.__resources.reservation_app
 
+    @property
+    def autoadder_app(self) -> autoadder_app:
+        return self.__resources.autoadder_app
+
     def __static(self) -> None:
         @self.get("/")
         async def root() -> str:
@@ -67,6 +72,17 @@ class server(fastapi.FastAPI):
             name = "covers"
         )
 
+    def __route_autoadder(self) -> None:
+        @self.post("/complete/barcode/{barcode}")
+        async def autoadder_complete_barcode(
+            barcode: str,
+            request: apikey_request
+        ) -> dict:
+            body = request.dict()
+            body["barcode"] = barcode
+
+            return self.autoadder_app.find(body)
+
     def __route_reservations(self) -> None:
         @self.post("/reservations/user/email/{email}")
         async def reservations_email(
@@ -90,11 +106,11 @@ class server(fastapi.FastAPI):
 
         @self.post("/reservations/product/barcode/{barcode}")
         async def reservations_barcode(
-            barcode: int,
+            barcode: str,
             reservation: reservation_request
         ) -> dict:
             body = reservation.dict()
-            body["target_barcode"] = barcode
+            body["target_barcode"] = str(barcode)
 
             return self.reservation_app.get_by_product(body)
 
@@ -110,11 +126,11 @@ class server(fastapi.FastAPI):
 
         @self.post("/rent/product/barcode/{barcode}")
         async def rent_barcode(
-            barcode: int,
+            barcode: str,
             reservation: reservation_request
         ) -> dict:
             body = reservation.dict()
-            body["target_barcode"] = barcode
+            body["target_barcode"] = str(barcode)
             
             return self.reservation_app.rent_product(body)
 
@@ -130,11 +146,11 @@ class server(fastapi.FastAPI):
 
         @self.post("/give_back/product/barcode/{barcode}")
         async def give_back_barcode(
-            barcode: int,
+            barcode: str,
             reservation: reservation_request
         ) -> dict:
             body = reservation.dict()
-            body["target_barcode"] = barcode
+            body["target_barcode"] = str(barcode)
 
             return self.reservation_app.give_back_product(body)
 
@@ -154,7 +170,7 @@ class server(fastapi.FastAPI):
             return self.product_app.all()
 
         @self.get("/product/get/barcode/{barcode}")
-        async def product_get_barcode(barcode: int) -> dict:
+        async def product_get_barcode(barcode: str) -> dict:
             return self.product_app.get_barcode(str(barcode))
 
         @self.get("/product/get/name/{name}")
@@ -170,7 +186,7 @@ class server(fastapi.FastAPI):
             return self.product_app.search_author(author)
 
         @self.get("/product/check/barcode/{barcode}")
-        async def product_check_barcode(barcode: int) -> dict:
+        async def product_check_barcode(barcode: str) -> dict:
             return self.product_app.check_barcode(str(barcode))
 
         @self.get("/product/check/name/{name}")

+ 3 - 0
requirements.txt

@@ -3,3 +3,6 @@ fastapi
 typer-slim
 uvicorn
 pillow
+beautifulsoup4
+googlesearch-python
+Google-Images-Search

+ 381 - 26
static/bundle/app.js

@@ -35,6 +35,7 @@
     stock_count;
     barcode;
     thumbnail;
+    on_stock;
     constructor(target) {
       this.name = null;
       this.description = null;
@@ -43,6 +44,7 @@
       this.stock_count = null;
       this.barcode = null;
       this.thumbnail = null;
+      this.on_stock = null;
       if ("name" in target) this.name = target["name"];
       if ("description" in target) this.description = target["description"];
       if ("author" in target) this.author = target["author"];
@@ -50,9 +52,10 @@
       if ("stock_count" in target) this.stock_count = target["stock_count"];
       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"];
     }
     get dump() {
-      return {
+      const dumped = {
         "name": this.name,
         "description": this.description,
         "author": this.author,
@@ -61,6 +64,10 @@
         "barcode": this.barcode,
         "thumbnail": this.thumbnail
       };
+      if (this.on_stock !== null) {
+        dumped["on_stock"] = this.on_stock;
+      }
+      return dumped;
     }
     get ready() {
       if (this.name === null || this.description === null) return false;
@@ -325,6 +332,9 @@
     _action() {
       throw new TypeError("It must be overwriten.");
     }
+    get _info() {
+      return false;
+    }
     show() {
       if (this.#node !== null) {
         return;
@@ -378,21 +388,23 @@
       cancel_button.classList.add("material-icons");
       cancel_button.innerText = "clear";
       buttons.appendChild(cancel_button);
-      const confirm_button = document.createElement("button");
-      confirm_button.classList.add("confirm");
-      confirm_button.classList.add("material-icons");
-      confirm_button.innerText = "send";
-      buttons.appendChild(confirm_button);
       cancel_button.addEventListener("click", () => {
         this.hide();
       });
-      confirm_button.addEventListener("click", () => {
-        if (this.#action === false) {
-          return;
-        }
-        this._action();
-        this.hide();
-      });
+      if (!this._info) {
+        const confirm_button = document.createElement("button");
+        confirm_button.classList.add("confirm");
+        confirm_button.classList.add("material-icons");
+        confirm_button.innerText = "send";
+        buttons.appendChild(confirm_button);
+        confirm_button.addEventListener("click", () => {
+          if (this.#action === false) {
+            return;
+          }
+          this._action();
+          this.hide();
+        });
+      }
       return container;
     }
   };
@@ -669,6 +681,9 @@
     _get_input(name) {
       return this.get_query('input[name="' + name + '"]');
     }
+    get _has_submit() {
+      return true;
+    }
     _build_node() {
       const center = document.createElement("div");
       center.classList.add("center");
@@ -698,12 +713,14 @@
       close_button.innerText = "close";
       close_button.type = "button";
       bottom.appendChild(close_button);
-      const send_button = document.createElement("button");
-      send_button.classList.add("send");
-      send_button.classList.add("material-icons");
-      send_button.innerText = "send";
-      send_button.type = "submit";
-      bottom.appendChild(send_button);
+      if (this._has_submit) {
+        const send_button = document.createElement("button");
+        send_button.classList.add("send");
+        send_button.classList.add("material-icons");
+        send_button.innerText = "send";
+        send_button.type = "submit";
+        bottom.appendChild(send_button);
+      }
       close_button.addEventListener("click", () => {
         this.hide();
       });
@@ -711,9 +728,15 @@
         target.preventDefault();
         this._process();
       });
-      this._build_form();
+      this._refresh();
       return center;
     }
+    _refresh() {
+      while (this.#form.lastChild) {
+        this.#form.lastChild.remove();
+      }
+      this._build_form();
+    }
     _create_input(name, label_text, placeholder, worker = null) {
       const container = document.createElement("div");
       container.classList.add("input-container");
@@ -734,11 +757,14 @@
       if (!this.#form) {
         throw new Error("Screen is not visible yet!");
       }
-      this.#form.appendChild(container);
+      this._append_child(container);
       return () => {
         return input.value;
       };
     }
+    _append_child(target) {
+      this.#form.appendChild(target);
+    }
     _clear_results() {
       if (!this.#result) {
         return;
@@ -1000,6 +1026,9 @@
     #target;
     #email;
     #phone;
+    get _email() {
+      return this.#email();
+    }
     get _phone() {
       return this.#phone();
     }
@@ -1022,21 +1051,167 @@
       this.#phone = this._create_input(
         "phone",
         "Phone number:",
-        "123-456-789",
+        "+1 123-456-789",
         (input) => {
           input.type = "tel";
-          input.pattern = "[0-9]{3}-[0-9]{3}-[0-9]{3}";
+          const add_prefix = () => {
+            if (input.value.length === 0) {
+              input.value = "+48 ";
+            }
+          };
+          input.addEventListener("click", add_prefix);
+          input.addEventListener("focus", add_prefix);
         }
       );
     }
   };
 
+  // application/scripts/reservation.js
+  var reservation = class _reservation {
+    email;
+    phone_number;
+    product_barcode;
+    constructor(target = null) {
+      this.email = null;
+      this.phone_number = null;
+      this.product_barcode = null;
+      if (target === null) {
+        return;
+      }
+      if ("email" in target) {
+        this.email = target["email"];
+      }
+      if ("target_barcode" in target) {
+        this.product_barcode = target["target_barcode"];
+      }
+      if ("phone_number" in target) {
+        this.phone_number = target["phone_number"];
+      }
+    }
+    get dump() {
+      const dumped = {
+        "target_barcode": this.product_barcode
+      };
+      if (this.email !== null) {
+        dumped["email"] = this.email;
+      }
+      if (this.phone_number !== null) {
+        dumped["phone_number"] = this.phone_number;
+      }
+      return dumped;
+    }
+    get ready() {
+      if (this.product_barcode === null) return false;
+      if (this.email === null && this.phone_number === null) return false;
+      return true;
+    }
+    copy() {
+      return new _reservation(this.dump);
+    }
+  };
+
+  // application/scripts/product_rent_request.js
+  var product_rent_request = class extends request {
+    #reservation;
+    constructor(reservation2) {
+      super();
+      this.#reservation = reservation2;
+    }
+    get data() {
+      return Object.assign(this.#reservation.dump, {
+        "apikey": this._apikey
+      });
+    }
+    get _response() {
+      return bool_response;
+    }
+    get method() {
+      return "POST";
+    }
+    get url() {
+      return "/rent/product/barcode/" + this.#reservation.product_barcode;
+    }
+  };
+
+  // application/scripts/reservation_factory.js
+  var reservation_factory = class {
+    #target;
+    constructor() {
+      this.#target = new reservation();
+    }
+    phone_number(target) {
+      target = target.trim().replaceAll("-", "");
+      if (target.length === 0) {
+        target = null;
+      }
+      this.#target.phone_number = target;
+      return this;
+    }
+    email(target) {
+      target = target.trim();
+      if (target.length === 0) {
+        target = null;
+      }
+      this.#target.email = target;
+      return this;
+    }
+    product(target) {
+      this.#target.product_barcode = target.barcode;
+      return this;
+    }
+    result() {
+      if (this.#target.ready) {
+        return this.#target;
+      }
+      throw new Error("Target reservation is not ready yet.");
+    }
+  };
+
   // application/scripts/product_rent.js
   var product_rent = class extends rents_screen {
     get _name() {
       return "Product rent";
     }
-    _process() {
+    async _process() {
+      try {
+        this._info = "Processing...";
+        const target = new reservation_factory().email(this._email).phone_number(this._phone).product(this._target).result();
+        const request2 = new product_rent_request(target);
+        const response = await request2.connect();
+        if (!response.result) {
+          throw new Error(response.cause);
+        }
+        this._success = "New rent added.";
+        searcher.reload();
+        setTimeout(() => {
+          this.hide();
+        }, 500);
+      } catch (error) {
+        this._error = String(error);
+      }
+    }
+  };
+
+  // application/scripts/product_give_back_request.js
+  var product_give_back_request = class extends request {
+    #reservation;
+    constructor(reservation2) {
+      super();
+      this.#reservation = reservation2;
+    }
+    get data() {
+      return Object.assign(this.#reservation.dump, {
+        "apikey": this._apikey
+      });
+    }
+    get _response() {
+      return bool_response;
+    }
+    get method() {
+      return "POST";
+    }
+    get url() {
+      return "/give_back/product/barcode/" + this.#reservation.product_barcode;
     }
   };
 
@@ -1045,7 +1220,175 @@
     get _name() {
       return "Product give back";
     }
+    async _process() {
+      try {
+        this._info = "Processing...";
+        const target = new reservation_factory().email(this._email).phone_number(this._phone).product(this._target).result();
+        const request2 = new product_give_back_request(target);
+        const response = await request2.connect();
+        if (!response.result) {
+          throw new Error(response.cause);
+        }
+        this._success = "Success!";
+        searcher.reload();
+        setTimeout(() => {
+          this.hide();
+        }, 500);
+      } catch (error) {
+        this._error = String(error);
+      }
+    }
+  };
+
+  // application/scripts/reservations_response.js
+  var reservations_response = class extends bool_response {
+    #collection;
+    constructor(target) {
+      super(target);
+      if (!this.result) {
+        return;
+      }
+      this.#collection = new Array();
+      target["reservations"].forEach((count) => {
+        this.#collection.push(new reservation(count));
+      });
+    }
+    get collection() {
+      if (!this.result) {
+        throw new Error(this.cause);
+      }
+      return this.#collection;
+    }
+  };
+
+  // application/scripts/product_reservations_request.js
+  var product_reservations_request = class extends request {
+    #target;
+    constructor(target) {
+      super();
+      this.#target = target;
+    }
+    get _response() {
+      return reservations_response;
+    }
+    get data() {
+      return {
+        "apikey": this._apikey
+      };
+    }
+    get method() {
+      return "POST";
+    }
+    get url() {
+      return "/reservations/product/barcode/" + this.#target.barcode;
+    }
+  };
+
+  // application/scripts/product_all_rents.js
+  var product_all_rents = class extends formscreen {
+    #target;
+    constructor(target) {
+      super();
+      this.#target = target;
+    }
+    get _name() {
+      return "All rents";
+    }
+    get _has_submit() {
+      return false;
+    }
+    #create_single(target) {
+      const container = document.createElement("div");
+      container.classList.add("reservation-info");
+      if (target.phone_number !== null) {
+        const phone_icon = document.createElement("span");
+        phone_icon.classList.add("material-icons");
+        phone_icon.innerText = "phone";
+        const phone_number = document.createElement("span");
+        phone_number.classList.add("numbers");
+        phone_number.innerText = target.phone_number;
+        const phone_number_container = document.createElement("p");
+        phone_number_container.appendChild(phone_icon);
+        phone_number_container.appendChild(phone_number);
+        container.appendChild(phone_number_container);
+      }
+      if (target.email !== null) {
+        const email_icon = document.createElement("span");
+        email_icon.classList.add("material-icons");
+        email_icon.innerText = "mail";
+        const email = document.createElement("span");
+        email.innerText = target.email;
+        const email_container = document.createElement("p");
+        email_container.appendChild(email_icon);
+        email_container.appendChild(email);
+        container.appendChild(email_container);
+      }
+      return container;
+    }
+    #create_single_button(target) {
+      const button = document.createElement("button");
+      button.classList.add("material-icons");
+      button.classList.add("give-back-button");
+      button.innerText = "save_alt";
+      button.addEventListener("click", async () => {
+        try {
+          this._info = "Processing...";
+          const request2 = new product_give_back_request(target);
+          const response = await request2.connect();
+          if (!response.result) {
+            throw new Error(response.cause);
+          }
+          this._refresh();
+          searcher.reload();
+        } catch (error) {
+          this._error = String(error);
+        }
+      });
+      return button;
+    }
     _process() {
+      return;
+    }
+    async _build_form() {
+      try {
+        this._info = "Loading...";
+        const request2 = new product_reservations_request(this.#target);
+        const response = await request2.connect();
+        const list = document.createElement("div");
+        list.classList.add("reservations-list");
+        let empty = true;
+        response.collection.forEach((count) => {
+          const item = document.createElement("div");
+          item.classList.add("reservation");
+          const left = this.#create_single(count);
+          const right = this.#create_single_button(count);
+          empty = false;
+          item.appendChild(left);
+          item.appendChild(right);
+          list.appendChild(item);
+        });
+        this._append_child(list);
+        if (empty) {
+          this._success = "Not found any reservations.";
+        } else {
+          this._clear_results();
+        }
+      } catch (error) {
+        this._error = String(error);
+      }
+    }
+  };
+
+  // application/scripts/product_not_avairable.js
+  var product_not_avairable = class extends confirm_action {
+    get _title() {
+      return "Error";
+    }
+    get _description() {
+      return "This product is not avairable. Anybody can not rent it.";
+    }
+    get _info() {
+      return true;
     }
   };
 
@@ -1073,6 +1416,11 @@
     get #manage() {
       const manage = document.createElement("div");
       manage.classList.add("manage");
+      const all_rents_button = document.createElement("button");
+      all_rents_button.classList.add("material-icons");
+      all_rents_button.classList.add("all-rents-button");
+      all_rents_button.innerText = "list";
+      manage.appendChild(all_rents_button);
       const rent_button = document.createElement("button");
       rent_button.classList.add("material-icons");
       rent_button.classList.add("rent-button");
@@ -1093,8 +1441,15 @@
       delete_button.classList.add("delete-button");
       delete_button.innerText = "remove_circle_outline";
       manage.appendChild(delete_button);
+      all_rents_button.addEventListener("click", () => {
+        new product_all_rents(this.#target).show();
+      });
       rent_button.addEventListener("click", () => {
-        new product_rent(this.#target).show();
+        if (this.#target.on_stock > 0) {
+          new product_rent(this.#target).show();
+        } else {
+          new product_not_avairable().show();
+        }
       });
       give_back_button.addEventListener("click", () => {
         new product_give_back(this.#target).show();
@@ -1125,7 +1480,7 @@
       const stock_count = document.createElement("p");
       stock_count.classList.add("stock-count");
       stock_count.classList.add("material-icons");
-      if (this.#target.stock_count > 0) {
+      if (this.#target.on_stock > 0) {
         stock_count.innerText = "check_circle";
         stock_count.classList.add("avairable");
       } else {

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


+ 39 - 0
tests/012-autoadder.py

@@ -0,0 +1,39 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+""" Make sure You have API key and CX in config file. """
+""" https://programmablesearchengine.google.com/ """
+
+location = root / pathlib.Path("config.json")
+resources = assets.config_loader(assets.app_config).load(location).resources
+autoadder = resources.autoadder
+
+print("Questing 9788382752656...")
+print(autoadder.find("9788382752656"))
+print()
+
+print("Testing app:")
+
+factory = assets.user_factory()
+factory.nick = "sample"
+factory.password = "password"
+user = factory.result
+
+collection = assets.users_collection()
+collection.add(user)
+
+app = assets.autoadder_app(collection, resources.autoadder)
+
+print("Questing 9788382752656...")
+print(app.find({
+    "apikey": user.apikey,
+    "barcode": "9788382752656"
+}))
+print()

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