diff --git a/Dockerfile b/Dockerfile index 8a2c3f3..b8c22d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,8 @@ -FROM python:3.11-slim +FROM python:3.13-slim -# Get runtime dependencies -# glx for OpenCV, ghostscript for datasheet PDF rendering, zbar for barcode scanning, git for cloning repos -#RUN apt-get update && apt-get install -y libgl1-mesa-glx ghostscript libzbar0 git && apt-get clean && rm -rf /var/lib/apt/lists COPY requirements.txt ./ -#COPY config-server.yml config.yml RUN pip3 install -r requirements.txt COPY *.py *.txt *.toml ./ COPY inventory ./inventory CMD ["rio", "run", "--release", "--public", "--port", "8000"] -EXPOSE 8000 +EXPOSE 8000 \ No newline at end of file diff --git a/db_classes.py b/db_classes.py index 53aff93..5b89a00 100644 --- a/db_classes.py +++ b/db_classes.py @@ -24,7 +24,7 @@ class office(Model): class location(Model): name = CharField() - locationid = AutoField() + locationid = CharField(unique=True, primary_key=True) description = CharField(null=True) parent = ForeignKeyField('self', null=True, backref="sublocations") @@ -42,6 +42,7 @@ class item(Model): description = CharField(null=True) serial = CharField(null=True) checkout = BooleanField(default=False) + checkout_loc = ForeignKeyField(location, backref="items_checkedout_here", null=True) checkout_user = ForeignKeyField(user, backref="items_held", null=True) checkout_start = DateTimeField(null=True) checkout_end = DateTimeField(null=True) @@ -70,7 +71,13 @@ class component(Model): def init(): print("Connecting to database...") - db.connect() + import time + while True: + try: + db.connect() + break + except: + time.sleep(1) print("Checking & creating tables...") db.create_tables([location, office, item, component, user]) print("Database initialized.") @@ -81,6 +88,10 @@ def init(): #print(add) #print(type(add)) for itm in add: + try: + itm["location"] = item.select().where(item.barcode==itm["barcode"])[0].loc.name + except: + pass print(itm) #print(type(itm)) search.add_document(itm) @@ -115,17 +126,65 @@ def search_item(query, filters: dict={}): def find_item(barcode): return search.get_barcode(barcode) -def create_item(fullname, serial, officename, barcode, location=None, description=None, manufacturer=None, mac=None, fwver=None): +def find_item_location(barcode): + try: + return item.select().where(item.barcode==barcode).loc + except: + return False + +def create_item(fullname, serial, officename, barcode, locationid=None, description=None, manufacturer=None, mac=None, fwver=None): try: off = office(name=officename) off.save(force_insert=True) except IntegrityError: pass try: + loc = get_location_id(locationid) + if loc == False: + loc = None + else: + print("Found location: " + loc.name) off = office.select().where(office.name == officename)[0] - itm = item(office=off, barcode=barcode, fullname=fullname, description=description, loc=location, serial=serial, mac=mac, fwver=fwver, manufacturer=manufacturer) + itm = item(office=off, barcode=barcode, fullname=fullname, description=description, loc=loc, serial=serial, mac=mac, fwver=fwver, manufacturer=manufacturer) itm.save(force_insert=True) - search.add_document(item.select().where(item.barcode==barcode).dicts()[0]) + itmdict= item.select().where(item.barcode==barcode).dicts()[0] + try: + itmdict["location"] = loc.name + #print(locationid) + #print(itmdict["location"]) + except: + pass + search.add_document(itmdict) + print("item: " + itm.fullname) + return itm + except IntegrityError: + print("Duplicate item " + fullname) + return False + + +def update_item(fullname, serial, officename, barcode, locationid=None, description=None, manufacturer=None, mac=None, fwver=None): + try: + off = office(name=officename) + off.save(force_insert=True) + except IntegrityError: + pass + try: + loc = get_location_id(locationid) + if loc == False: + loc = None + else: + print("Found location: " + loc.name) + off = office.select().where(office.name == officename)[0] + itm = item(office=off, barcode=barcode, fullname=fullname, description=description, loc=loc, serial=serial, mac=mac, fwver=fwver, manufacturer=manufacturer) + itm.save() + itmdict= item.select().where(item.barcode==barcode).dicts()[0] + try: + itmdict["location"] = loc.name + #print(locationid) + #print(itmdict["location"]) + except: + pass + search.add_document(itmdict) print("item: " + itm.fullname) return itm except IntegrityError: @@ -196,7 +255,7 @@ def checkout(user, barcode, loc=None): if itm: itm.checkout = True itm.checkout_user = user - itm.loc = loc + itm.checkout_loc = loc itm.save() return itm else: @@ -207,19 +266,21 @@ def checkin(user, barcode, loc=None): if itm: itm.checkout = False itm.last_user = user - itm.loc = loc + if loc is not None: + itm.loc = loc itm.save() return itm else: return False -def create_location(name, parent=None): - if parent is not None: - loc = location(name=name, parent=parent) - loc.save() +def create_location(name, barcode, parent=None, description=None): + try: + loc = location(name=name, locationid=barcode, parent=parent, description=description) + loc.save(force_insert=True) + print(loc.name, loc.locationid) return loc - else: + except: return False def _find_parent(loc, parent): @@ -247,6 +308,21 @@ def get_location(name, parent=None): return False except: return False + +def get_location_id(barcode): + try: + print("str" + barcode + "str") + if len(barcode) > 0: + query = location.select() + for loc in query: + print(loc.name, loc.locationid) + if loc.locationid == barcode: + return loc + return False + else: + return False + except: + return False def get_user(name): query = user.select().where(user.username == name) diff --git a/inventory/__init__.py b/inventory/__init__.py index 48bd443..e9be5e8 100644 --- a/inventory/__init__.py +++ b/inventory/__init__.py @@ -35,26 +35,31 @@ app = rio.App( comps.Settings(), ], pages=[ - rio.Page( + rio.ComponentPage( name="Home", - page_url='', + url_segment='', build=pages.BrowsePage, ), - rio.Page( - name="AboutPage", - page_url='about-page', - build=pages.AboutPage, + rio.ComponentPage( + name="SettingsPage", + url_segment='settings-page', + build=pages.SettingsPage, ), - rio.Page( + rio.ComponentPage( name="AddPage", - page_url='add', + url_segment='add', build=pages.AddPage, ), - rio.Page( + rio.ComponentPage( + name="AddLocationPage", + url_segment='addlocation', + build=pages.AddLocationPage, + ), + rio.ComponentPage( name="ItemPage", - page_url='item', + url_segment='item', build=pages.ItemPage, ), ], diff --git a/inventory/components/footer.py b/inventory/components/footer.py index 09648e8..90b727c 100644 --- a/inventory/components/footer.py +++ b/inventory/components/footer.py @@ -16,7 +16,6 @@ class Footer(rio.Component): def build(self) -> rio.Component: return rio.Card( content=rio.Column( - rio.Icon("rio/logo:fill", width=5, height=5), rio.Text("Buzzwordz Inc."), rio.Text( "Hyper Dyper Website", diff --git a/inventory/components/navbar.py b/inventory/components/navbar.py index 6dbe6e3..098eeaf 100644 --- a/inventory/components/navbar.py +++ b/inventory/components/navbar.py @@ -60,7 +60,7 @@ class Navbar(rio.Component): # you've passed the app during creation. Since multiple pages can be # active at a time (e.g. /foo/bar/baz), this is a list. active_page = self.session.active_page_instances[0] - active_page_url_segment = active_page.page_url + active_page_url_segment = active_page.url_segment # The navbar should appear above all other components. This is easily # done by using a `rio.Overlay` component. @@ -80,7 +80,7 @@ class Navbar(rio.Component): style=( "major" if active_page_url_segment == "" - else "plain" + else "plain-text" ), ), "/", @@ -121,23 +121,35 @@ class Navbar(rio.Component): style=( "major" if active_page_url_segment == "add" - else "plain" + else "plain-text" ), ), "/add", ), + rio.Link( + rio.Button( + "Locations", + icon="material/news", + style=( + "major" + if active_page_url_segment == "addlocation" + else "plain-text" + ), + ), + "/addlocation", + ), # Same game, different button rio.Link( rio.Button( - "About", + "Settings", icon="material/info", style=( "major" - if active_page_url_segment == "about-page" - else "plain" + if active_page_url_segment == "settings-page" + else "plain-text" ), ), - "/about-page", + "/settings-page", ), spacing=1, margin=1, diff --git a/inventory/pages/__init__.py b/inventory/pages/__init__.py index 978a679..d6384a5 100644 --- a/inventory/pages/__init__.py +++ b/inventory/pages/__init__.py @@ -1,6 +1,7 @@ from .root_page import RootPage -from .about_page import AboutPage +from .settings_page import SettingsPage from .add_page import AddPage +from .add_location import AddLocationPage from .browse_page import BrowsePage from .login_page import LoginPage from .item_page import ItemPage diff --git a/inventory/pages/about_page.py b/inventory/pages/about_page.py deleted file mode 100644 index d4396ec..0000000 --- a/inventory/pages/about_page.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -from dataclasses import KW_ONLY, field -from typing import * # type: ignore - -import rio - -from .. import components as comps - -class AboutPage(rio.Component): - """ - A sample page, which displays a humorous description of the company. - """ - - def build(self) -> rio.Component: - return rio.Markdown( - """ -# About Us - -Welcome to Buzzwordz Inc.! Unleashing Synergistic Paradigms for Unprecedented -Excellence since the day after yesterday. - -## About Our Company - -At buzzwordz, we are all talk and no action. Our mission is to be the vanguards -of industry-leading solutions, leveraging bleeding-edge technologies to catapult -your business into the stratosphere of success. Our unparalleled team of ninjas, -gurus, and rockstars is dedicated to disrupting the status quo and actualizing -your wildest business dreams. We live, breathe, and eat operational excellence -and groundbreaking innovation. - -## Synergistic Consulting - -Unlock your business's quantum potential with our bespoke, game-changing -strategies. Our consulting services synergize cross-functional paradigms to -create a holistic ecosystem of perpetual growth and exponential ROI. Did I -mention paradigm-shifts? We've got those too. - -## Agile Hyper-Development - -We turn moonshot ideas into reality with our agile, ninja-level development -techniques. Our team of coding wizards crafts robust, scalable, and future-proof -solutions that redefine industry standards. 24/7 Proactive Hyper-Support - -Experience next-gen support that anticipates your needs before you do. Our -omnipresent customer happiness engineers ensure seamless integration, -frictionless operation, and infinite satisfaction, day and night. - -Embark on a journey of transformational growth and stratospheric success. Don't -delay, give us your money today. - -Phone: (123) 456-7890 - -Email: info@yourwebsite.com - -Address: 123 Main Street, City, Country - """, - width=60, - margin_bottom=4, - align_x=0.5, - align_y=0, - ) - diff --git a/inventory/pages/add_location.py b/inventory/pages/add_location.py new file mode 100644 index 0000000..d6c768f --- /dev/null +++ b/inventory/pages/add_location.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from dataclasses import KW_ONLY, field +from typing import * # type: ignore + +import rio +import datetime +from mac_vendor_lookup import AsyncMacLookup + +from db_classes import * +from .. import components as comps +import asyncio + +class AddLocationPage(rio.Component): + code: str = "" + popup_message: str = "" + popup_show: bool = False + popup_color: str = 'warning' + description: str = "" + name: str = "" + parent_code: str = "" + parent: str = "" + + @rio.event.periodic(1) + def set_office_init(self): + self.office = self.session[comps.Settings].office + #print("Populated:", self.office) + + async def _update_location(self, event: rio.TextInputChangeEvent): + print("Checking " + self.parent) + if get_location_id(self.parent) != False: + self.parent_code = self.parent + print("Found location " + get_location_id(self.parent).name) + self.parent = get_location_id(self.parent).name + + async def add_part(self): + if self.code == "": + # FAIL + self.popup_message = "\n Missing barcode! \n\n" + self.popup_show = True + self.popup_color = 'danger' + await asyncio.sleep(1) + self.popup_show = False + else: + # OK, add part + if get_location_id(self.parent_code) != False: + self.parent = get_location_id(self.parent_code) + else: + self.parent = None + if create_location(name=self.name, barcode=self.code, parent=self.parent, description=self.description) == False: + self.popup_message = "\n Duplicate barcode! \n\n" + self.popup_show = True + self.popup_color = 'warning' + await asyncio.sleep(2) + self.popup_show = False + else: + self.popup_message = "\n Part added! \n\n" + self.popup_show = True + self.popup_color = 'success' + self.name: str = "" + self.code: str = "" + self.description: str = "" + self.parent_code: str = "" + self.parent: str = "" + await asyncio.sleep(2) + self.popup_show = False + + + async def _add_part_enter(self, event: rio.TextInputConfirmEvent): + await self.add_part() + + async def _add_part_button(self): + await self.add_part() + + def build(self) -> rio.Component: + return rio.Column( + rio.Popup( + anchor=rio.Text( + text="Add a part below:", + style='heading1', + align_x = 0.5 + ), + color=self.bind().popup_color, + is_open=self.bind().popup_show, + content=rio.Text( + text=self.bind().popup_message, + ), + + ), + rio.TextInput( + label="Barcode", + text=self.bind().code + ), + rio.TextInput( + label="Parent Location (optional)", + text=self.bind().parent, + on_change=self._update_location + ), + rio.TextInput( + label="Location Name", + text=self.bind().name + ), + rio.MultiLineTextInput( + label="Description (optional)", + text=self.bind().description + ), + rio.Button( + content="Add", + on_press=self._add_part_button + ), + + spacing=2, + min_width=60, + margin_bottom=4, + align_x=0.5, + align_y=0, + ) + diff --git a/inventory/pages/add_page.py b/inventory/pages/add_page.py index f4fa6e2..7e9a8da 100644 --- a/inventory/pages/add_page.py +++ b/inventory/pages/add_page.py @@ -10,7 +10,14 @@ from mac_vendor_lookup import AsyncMacLookup from db_classes import * from .. import components as comps +import asyncio + class AddPage(rio.Component): + + """ + A set of fields for adding/editing items. + """ + partnum: str = "" mac: str = "" serial: str = "" @@ -27,6 +34,9 @@ class AddPage(rio.Component): manu: str = "" manufield: str = "" office: str = "" + description: str = "" + location: str = "" + location_code: str = "" @rio.event.periodic(1) def set_office_init(self): @@ -60,12 +70,16 @@ class AddPage(rio.Component): self.popup_message = "\n Missing barcode! \n\n" self.popup_show = True self.popup_color = 'danger' + await asyncio.sleep(2) + self.popup_show = False else: # OK, add part - if create_item(self.partnum, self.serial, self.office, self.code, location=None, description=None, manufacturer=self.manu, mac=self.mac, fwver=self.fwver) == False: + if create_item(self.partnum, self.serial, self.office, self.code, locationid=self.location_code, description=self.description, manufacturer=self.manu, mac=self.mac, fwver=self.fwver) == False: self.popup_message = "\n Duplicate barcode! \n\n" self.popup_show = True self.popup_color = 'warning' + await asyncio.sleep(2) + self.popup_show = False else: self.popup_message = "\n Part added! \n\n" self.popup_show = True @@ -79,6 +93,11 @@ class AddPage(rio.Component): self.macvendor: str = "" self.manu: str = "" self.manufield: str = "" + self.description: str = "" + self.location: str = "" + self.location_code: str = "" + await asyncio.sleep(2) + self.popup_show = False async def _add_part_enter(self, event: rio.TextInputConfirmEvent): @@ -107,8 +126,16 @@ class AddPage(rio.Component): async def _update_mac(self, event: rio.TextInputChangeEvent): await self.check_mac(event.text) + async def _update_location(self, event: rio.TextInputChangeEvent): + print("Checking " + self.location) + if get_location_id(self.location) != False: + self.location_code = self.location + print("Found location " + get_location_id(self.location).name) + self.location = get_location_id(self.location).name + + def _update_partnum(self, event: rio.TextInputChangeEvent): - def __find_hm_header(txt): + def __find_hm_header_static(txt): searchlist = ["RSPS", "RSPE", "RSP", "RSB", "LRS", "RS", "OS", "RED", "MSP", "MSM", "MS", "MM", "EESX", "EES", "OZD", "OBR"] for header in searchlist: if txt.find(header) >= 0: @@ -129,6 +156,8 @@ class AddPage(rio.Component): if dash and acount <= 5 and ncount <= 5: return acount+ncount + elif dash and acount >5 and acount <= 10 and ncount == 0: + return acount+ncount return -1 def __find_hm_fwver(txt): @@ -143,7 +172,7 @@ class AddPage(rio.Component): if txt.find("BRS") == 0: a -= 1 if txt[a] in ['S', 'A']: - return txt[a] + return '2' + txt[a] a = __find_hm_fwver(txt) if txt.find("GRS") == 0: a -= 4 @@ -162,7 +191,7 @@ class AddPage(rio.Component): print(txt,txt[a]) if txt[a] in ['P', 'E']: - return txt[a] + return '2' + txt[a] a = __find_hm_fwver(txt) if txt.find("EAGLE") == 0: a -= 2 @@ -179,7 +208,7 @@ class AddPage(rio.Component): if __find_hm_fwver(pn) >= 0: self.fwver = pn[__find_hm_fwver(pn):] if len(__find_hm_fwmode(pn)) > 0: - self.fwver += " SWL-" + __find_hm_fwmode(pn) + self.fwver += " SW-L" + __find_hm_fwmode(pn) def build(self) -> rio.Component: return rio.Column( @@ -207,33 +236,39 @@ class AddPage(rio.Component): ), rio.TextInput( label="Serial", - text=self.bind().serial + text=self.bind().serial, + on_confirm=self._add_part_enter ), rio.TextInput( label="MAC", text=self.bind().mac, - on_change=self._update_mac + on_change=self._update_mac, + on_confirm=self._add_part_enter + ), + rio.TextInput( + label="Location (optional)", + text=self.bind().location, + on_change=self._update_location, + on_confirm=self._add_part_enter ), rio.TextInput( label="Manufacturer", - text=self.bind().manufield + text=self.bind().manufield, + on_confirm=self._add_part_enter ), rio.TextInput( label="FW Ver", - text=self.bind().fwver, - on_confirm=self._add_part_enter + text=self.bind().fwver + ), + rio.MultiLineTextInput( + label="Description (optional)", + text=self.bind().description ), - rio.Row( - # rio.DateInput( - # label="Timestamp", - # value=self.bind().date - # ), - rio.TextInput( - #text= - is_sensitive=False, - text = self.bind().time_start, - #on_change=self._set_time - ) + rio.TextInput( + label="Timestamp", + is_sensitive=False, + text = self.bind().time_start, + #on_change=self._set_time ), rio.Button( content="Add", @@ -241,7 +276,7 @@ class AddPage(rio.Component): ), spacing=2, - width=60, + min_width=60, margin_bottom=4, align_x=0.5, align_y=0, diff --git a/inventory/pages/browse_page.py b/inventory/pages/browse_page.py index fe0cfef..a2bd854 100644 --- a/inventory/pages/browse_page.py +++ b/inventory/pages/browse_page.py @@ -6,15 +6,36 @@ from typing import * # type: ignore import rio from .. import components as comps +from .add_page import AddPage from db_classes import * import functools +import asyncio class BrowsePage(rio.Component): searchtext: str = "" - items: dict = {} + items: list = [] office: str = "" filters: dict = {} + + popup_message: str = "" + popup_show: bool = False + popup_color: str = 'success' + + + elocation: str = "" + elocation_code: str = "" + epartnum: str = "" + emac: str = "" + eserial: str = "" + efwver: str = "" + ecode: str = "" + emacvendor: str = "" + emanu: str = "" + emanufield: str = "" + eoffice: str = "" + edescription: str = "" + @rio.event.on_populate async def _search(self, query=searchtext): self.office = self.session[comps.Settings].office @@ -35,24 +56,331 @@ class BrowsePage(rio.Component): async def _search_trigger(self, event: rio.TextInputChangeEvent): await self._search(event.text) - def click_item(self, code): + async def _create_dialog_info(self, code: str) -> str | None: + async def copy_info(text: str): + await self.session.set_clipboard(text) + # TODO: show "Copied!" popup + self.popup_message = "\n Copied! \n\n" + self.popup_color = 'success' + self.popup_show = True + await asyncio.sleep(1.5) + self.popup_show = False + + def build_dialog_info() -> rio.Component: + # Build the dialog + + itm: dict = find_item(code) + + try: + loc = itm["location"] + except: + loc = "" + if itm["checkout"]: + checkout = itm["checkout_user"] + " - " + loc + else: + checkout = loc + + details: rio.ListView = rio.ListView(grow_y=True, min_width=40) + # for key, val in itm.items(): + # details.add(rio.SimpleListItem(text=key,secondary_text=val)) + #functools.partial(copy_info, text=item["barcode"]) + name=str(itm["fullname"]) + manu=str(itm["manufacturer"]) + serial=str(itm["serial"]) + mac=str(itm["mac"]) + fw=str(itm["fwver"]) + loc=str(checkout) + checkouts=str(itm["checkout"]) + checkout_times=str(itm["checkout_start"]) + " to " + str(itm["checkout_end"]) + #office=str(itm["office"]) + barcode=str(itm["barcode"]) + desc=str(itm["description"]) + details.add(rio.SimpleListItem(text=name, on_press=functools.partial(copy_info, text=name))) + details.add(rio.SimpleListItem(text=manu, on_press=functools.partial(copy_info, text=manu))) + details.add(rio.SimpleListItem(text="Serial",secondary_text=serial, on_press=functools.partial(copy_info, text=serial))) + details.add(rio.SimpleListItem(text="MAC",secondary_text=mac, on_press=functools.partial(copy_info, text=mac))) + details.add(rio.SimpleListItem(text="FW Version",secondary_text=fw, on_press=functools.partial(copy_info, text=fw))) + details.add(rio.SimpleListItem(text="Location",secondary_text=loc, on_press=functools.partial(copy_info, text=loc))) + details.add(rio.SimpleListItem(text="Checked out?",secondary_text=checkouts, on_press=functools.partial(copy_info, text=checkouts))) + details.add(rio.SimpleListItem(text="Checkout start/end",secondary_text=checkout_times, on_press=functools.partial(copy_info, text=checkout_times))) + #details.add(rio.SimpleListItem(text="Office",secondary_text=office, on_press=functools.partial(copy_info, text=office))) + details.add(rio.SimpleListItem(text="Barcode",secondary_text=barcode, on_press=functools.partial(copy_info, text=barcode))) + details.add(rio.SimpleListItem(text="Description",secondary_text=desc, on_press=functools.partial(copy_info, text=desc))) + + return rio.Card( + rio.Column( + details, + rio.Row( + rio.Button( + content="Close", + on_press=_close_dialog_info + ), + rio.Button( + content="Edit", + on_press=functools.partial(self._create_dialog_edit, code=code) + ), + spacing=2, + margin=2 + ), + spacing=1, + margin=2, + ), + align_x=0.5, + align_y=0.5, + ) + + async def _close_dialog_info() -> None: + # This function will be called whenever the user selects an + # Item. It simply closes the dialog with the selected value. + await dialog.close() + + dialog = await self.session.show_custom_dialog( + build=build_dialog_info, + # Prevent the user from interacting with the rest of the app + # while the dialog is open + modal=True, + # Don't close the dialog if the user clicks outside of it + user_closeable=True, + ) + + # Wait for the user to select an option + result = await dialog.wait_for_close() + + # Return the selected value + return result + + + async def _create_dialog_edit(self, code: str) -> str | None: + itm: dict = find_item(code) + from mac_vendor_lookup import AsyncMacLookup + try: + self.elocation: str = itm["location"] + self.elocation_code: str = find_item_location(code).locationid + except: + self.elocation: str = "" + self.elocation_code: str = "" + + self.epartnum: str = itm["fullname"] + self.emac: str = itm["mac"] + self.eserial: str = itm["serial"] + self.efwver: str = itm["fwver"] + self.ecode: str = code + self.popup_message: str = "" + self.popup_show: bool = False + self.popup_color: str = 'warning' + self.emacvendor: str = "" + self.emanu: str = itm["manufacturer"] + self.emanufield: str = itm["manufacturer"] + self.eoffice: str = itm["office"] + self.edescription: str = itm["description"] + + async def check_mac(mac): + print("Checking", mac) + self.emac = mac + try: + macvendor = await AsyncMacLookup().lookup(mac) + if self.emanufield == "" or self.emanufield == self.emacvendor: # blank or set by MAC already + self.emanu = macvendor + self.emanufield = macvendor + else: + self.emanu = self.emanufield + self.emacvendor = macvendor + #print(macvendor) + except: + pass + # not valid MAC? + + async def check_all(): + await check_mac(self.emac) + # check part number + # lookup in PL_Export_rel + + async def add_part(): + await check_all() + if self.ecode == "": + # FAIL + self.popup_message = "\n Missing barcode! \n\n" + self.popup_show = True + self.popup_color = 'danger' + + else: + # OK, add part + if update_item(self.epartnum, self.eserial, self.office, self.ecode, locationid=self.elocation_code, description=self.edescription, manufacturer=self.emanu, mac=self.emac, fwver=self.efwver) == False: + self.popup_message = "\n Unable to update! \n\n" + self.popup_show = True + self.popup_color = 'warning' + else: + self.popup_message = "\n Part updated! \n\n" + self.popup_show = True + self.popup_color = 'success' + #self.ename: str = "" + self.epartnum: str = "" + self.emac: str = "" + self.eserial: str = "" + self.efwver: str = "" + self.ecode: str = "" + self.emacvendor: str = "" + self.emanu: str = "" + self.emanufield: str = "" + self.edescription: str = "" + self.elocation: str = "" + self.elocation_code: str = "" + + + async def _add_part_enter(event: rio.TextInputConfirmEvent): + await add_part() + + async def _add_part_button(): + await add_part() + + async def _update_mac(event: rio.TextInputChangeEvent): + await check_mac(event.text) + + async def _update_location(event: rio.TextInputChangeEvent): + print("Checking " + event.text) + self.elocation = event.text + if get_location_id(event.text) != False: + self.elocation_code = event.text + print("Found location " + get_location_id(event.text).name) + self.elocation = get_location_id(event.text).name + + async def _update_epartnum(event: rio.TextInputChangeEvent): + self.epartnum = event.text + + async def _update_eserial(event: rio.TextInputChangeEvent): + self.eserial = event.text + + async def _update_emanufield(event: rio.TextInputChangeEvent): + self.emanufield = event.text + self.emanu = event.text + + async def _update_efwver(event: rio.TextInputChangeEvent): + self.efwver = event.text + + async def _update_edescription(event: rio.TextInputChangeEvent): + self.edescription = event.text + + def build_dialog_edit() -> rio.Component: + # Build the dialog + return rio.Card( + rio.Column( + rio.TextInput( + label="Barcode", + text=self.ecode, + is_sensitive=False + ), + rio.TextInput( + label="Full part number", + text=self.epartnum, + on_change=_update_epartnum + ), + rio.TextInput( + label="Serial", + text=self.eserial, + on_change=_update_eserial + ), + rio.TextInput( + label="MAC", + text=self.emac, + on_change=_update_mac, + ), + rio.TextInput( + label="Location (optional)", + text=self.elocation, + on_change=_update_location, + ), + rio.TextInput( + label="Manufacturer", + text=self.emanufield, + on_change=_update_emanufield + ), + rio.TextInput( + label="FW Ver", + text=self.efwver, + on_change=_update_efwver + ), + rio.MultiLineTextInput( + label="Description (optional)", + text=self.edescription, + on_change=_update_edescription + ), + + rio.Row( + rio.Button( + content="Cancel", + on_press=_close_dialog_edit, + color='warning' + ), + rio.Button( + content="Delete", + on_press=_delete_dialog_edit, + color='danger' + ), + rio.Button( + content="Save", + on_press=_save_dialog_edit + ), + spacing=2, + margin=2 + ), + spacing=1, + margin=2 + ), + align_x=0.5, + align_y=0.5 + ) + + async def _close_dialog_edit() -> None: + await dialog.close() + + async def _save_dialog_edit() -> None: + await dialog.close() + await _add_part_button() + await asyncio.sleep(2) + self.popup_show = False + + async def _delete_dialog_edit() -> None: + await dialog.close() + + dialog = await self.session.show_custom_dialog( + build=build_dialog_edit, + # Prevent the user from interacting with the rest of the app + # while the dialog is open + modal=True, + # Don't close the dialog if the user clicks outside of it + user_closeable=False, + ) + + # Wait for the user to select an option + result = await dialog.wait_for_close() + + # Return the selected value + return result + + def click_item_page(self, code): self.session[comps.Settings].selected_item = code self.session.attach(self.session[comps.Settings]) self.session.navigate_to("/item") + async def click_item_dialog(self, code): + self.session[comps.Settings].selected_item = code + #self.session.attach(self.session[comps.Settings]) + #self.session.navigate_to("/item") + ret = await self._create_dialog_info(code) + def build(self) -> rio.Component: - searchview: rio.ListView = rio.ListView(height='grow') + searchview: rio.ListView = rio.ListView(grow_y=True) for item in self.items: - if item["loc"] is not None: - loc = item["loc"]["name"] - else: + try: + loc = item["location"] + except: loc = "" if item["checkout"]: checkout = item["checkout_user"] + " - " + loc else: checkout = loc searchview.add(rio.SimpleListItem(text=item["fullname"],secondary_text=(item["manufacturer"] + " - Serial: " + item["serial"] + "\n" + checkout), on_press=functools.partial( - self.click_item, + self.click_item_dialog, code=item["barcode"]))) return rio.Column( rio.Row( @@ -63,8 +391,21 @@ class BrowsePage(rio.Component): ) ), searchview, + rio.Popup( + anchor=rio.Text( + text="", + style='heading1', + align_x = 0.5 + ), + color=self.popup_color, + is_open=self.popup_show, + content=rio.Text( + text=self.popup_message, + ), + + ), spacing=2, - width=60, + min_width=60, align_x=0.5, align_y=0, ) diff --git a/inventory/pages/item_page.py b/inventory/pages/item_page.py index debbff7..76243c8 100644 --- a/inventory/pages/item_page.py +++ b/inventory/pages/item_page.py @@ -92,7 +92,7 @@ class ItemPage(rio.Component): align_x = 0.5, ), spacing=2, - width=60, + min_width=60, align_x=0.5, align_y=0, ) diff --git a/inventory/pages/root_page.py b/inventory/pages/root_page.py index 6cff810..13b821a 100644 --- a/inventory/pages/root_page.py +++ b/inventory/pages/root_page.py @@ -27,11 +27,11 @@ class RootPage(rio.Component): # of all other components. comps.Navbar(), # Add some empty space so the navbar doesn't cover the content. - rio.Spacer(height=10), + rio.Spacer(min_height=10), # The page view will display the content of the current page. rio.PageView( # Make sure the page view takes up all available space. - height="grow", + grow_y=True ), # The footer is also common to all pages, so place it here. comps.Footer(), diff --git a/inventory/pages/settings_page.py b/inventory/pages/settings_page.py new file mode 100644 index 0000000..5e1c1bf --- /dev/null +++ b/inventory/pages/settings_page.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import KW_ONLY, field +from typing import * # type: ignore + +import rio + +from .. import components as comps + +class SettingsPage(rio.Component): + """ + A sample page, which displays a humorous description of the company. + """ + + def build(self) -> rio.Component: + return rio.Markdown( + """ +Belden Inventory manager v0.5 +WIP + """, + min_width=60, + margin_bottom=4, + align_x=0.5, + align_y=0, + ) + diff --git a/requirements.txt b/requirements.txt index f402887..8d5ad65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ peewee pymysql -flask -rio-ui -meilisearch +rio-ui==0.10.4 +meilisearch #==0.31.5 mac-vendor-lookup \ No newline at end of file diff --git a/search.py b/search.py index 2a1fac1..9e6be66 100644 --- a/search.py +++ b/search.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Interactions with the Meilisearch API for adding and searching cables.""" +import sys + from meilisearch import Client from meilisearch.task import TaskInfo from meilisearch.errors import MeilisearchApiError @@ -45,6 +47,8 @@ class InventorySearch: # make a variable to easily reference the index self.idxref = self.client.index(self.index) time.sleep(0.05) + # disable typos, we have serial numbers and such that should be exact match + self.idxref.update_typo_tolerance({'enabled': False}) # update filterable attributes if needed self.idxref.update_distinct_attribute('barcode') self.update_filterables(filterable_attrs) @@ -109,4 +113,4 @@ class InventorySearch: # entrypoint if __name__ == "__main__": - jbs = InventorySearch() + ivs = InventorySearch() diff --git a/update.sh b/update.sh old mode 100644 new mode 100755