254 lines
7.2 KiB
Python
Executable File
254 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# dependencies = [
|
|
# "nicegui==3.16.0",
|
|
# ]
|
|
# ///
|
|
|
|
from nicegui import events
|
|
from nicegui import ui
|
|
|
|
type TableValue = str | int | float
|
|
type TableRow = dict[str, TableValue]
|
|
|
|
STATUS_COLORS = {
|
|
"Ready": "positive",
|
|
"Low": "warning",
|
|
"Backorder": "negative",
|
|
}
|
|
|
|
COLUMNS = [
|
|
{
|
|
"name": "actions",
|
|
"label": "Actions",
|
|
"required": True,
|
|
"align": "center",
|
|
},
|
|
{
|
|
"name": "name",
|
|
"label": "Product",
|
|
"field": "name",
|
|
"required": True,
|
|
"sortable": True,
|
|
"align": "left",
|
|
"headerClasses": "bg-grey-2 text-grey-9 font-bold",
|
|
"classes": "font-medium",
|
|
"headerStyle": "width: 40%; min-width: 12rem",
|
|
"style": "width: 40%; min-width: 12rem",
|
|
},
|
|
{
|
|
"name": "category",
|
|
"label": "Category",
|
|
"field": "category",
|
|
"sortable": True,
|
|
"align": "left",
|
|
"headerStyle": "width: 9rem",
|
|
"style": "width: 9rem",
|
|
},
|
|
{
|
|
"name": "stock",
|
|
"label": "In stock",
|
|
"field": "stock",
|
|
"sortable": True,
|
|
"align": "right",
|
|
"headerStyle": "width: 7rem",
|
|
"style": "width: 7rem",
|
|
":classes": "row => row.stock < 10 ? 'text-negative font-bold' : ''",
|
|
":format": "value => value == null ? '' : `${value} units`",
|
|
},
|
|
{
|
|
"name": "price",
|
|
"label": "Unit price",
|
|
"field": "price",
|
|
"sortable": True,
|
|
"align": "right",
|
|
"headerStyle": "width: 8rem",
|
|
"style": "width: 8rem",
|
|
":format": "value => value == null ? '' : `$${value.toFixed(2)}`",
|
|
},
|
|
{
|
|
"name": "status",
|
|
"label": "Status",
|
|
"field": "status",
|
|
"sortable": True,
|
|
"align": "center",
|
|
"headerStyle": "width: 8rem",
|
|
"style": "width: 8rem",
|
|
"colorByValue": STATUS_COLORS,
|
|
},
|
|
{
|
|
"name": "updated_at",
|
|
"label": "Updated",
|
|
"field": "updated_at",
|
|
"sortable": True,
|
|
"align": "left",
|
|
"headerStyle": "width: 13rem",
|
|
"style": "width: 13rem",
|
|
":sort": "(left, right) => Date.parse(left) - Date.parse(right)",
|
|
":format": """(() => {
|
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
timeZone: 'UTC',
|
|
});
|
|
return value => {
|
|
if (!value) return '';
|
|
const timestamp = Date.parse(value);
|
|
return Number.isNaN(timestamp) ? 'Invalid date' : formatter.format(timestamp);
|
|
};
|
|
})()""",
|
|
},
|
|
]
|
|
|
|
ROWS: list[TableRow] = [
|
|
{
|
|
"id": 101,
|
|
"name": "Desk lamp",
|
|
"category": "Lighting",
|
|
"stock": 7,
|
|
"price": 42.5,
|
|
"status": "Low",
|
|
"updated_at": "2026-08-31T16:20:00Z",
|
|
},
|
|
{
|
|
"id": 102,
|
|
"name": "Task chair",
|
|
"category": "Seating",
|
|
"stock": 18,
|
|
"price": 289.0,
|
|
"status": "Ready",
|
|
"updated_at": "2026-09-01T08:45:00Z",
|
|
},
|
|
{
|
|
"id": 103,
|
|
"name": "Monitor arm",
|
|
"category": "Hardware",
|
|
"stock": 0,
|
|
"price": 119.95,
|
|
"status": "Backorder",
|
|
"updated_at": "2026-08-29T11:05:00Z",
|
|
},
|
|
{
|
|
"id": 104,
|
|
"name": "Cable tray",
|
|
"category": "Hardware",
|
|
"stock": 34,
|
|
"price": 31.25,
|
|
"status": "Ready",
|
|
"updated_at": "2026-09-01T14:30:00Z",
|
|
},
|
|
{
|
|
"id": 105,
|
|
"name": "Side table",
|
|
"category": "Furniture",
|
|
"stock": 9,
|
|
"price": 164.5,
|
|
"status": "Low",
|
|
"updated_at": "2026-08-30T19:15:00Z",
|
|
},
|
|
{
|
|
"id": 106,
|
|
"name": "Floor light",
|
|
"category": "Lighting",
|
|
"stock": 15,
|
|
"price": 98.0,
|
|
"status": "Ready",
|
|
"updated_at": "2026-09-01T10:10:00Z",
|
|
},
|
|
]
|
|
|
|
|
|
def render_table() -> ui.table:
|
|
table = ui.table(
|
|
columns=COLUMNS,
|
|
column_defaults={"headerClasses": "text-grey-8"},
|
|
rows=ROWS,
|
|
row_key="id",
|
|
pagination={
|
|
"sortBy": "stock",
|
|
"descending": True,
|
|
"rowsPerPage": 5,
|
|
},
|
|
).classes("w-full max-w-5xl")
|
|
|
|
(
|
|
table.props(
|
|
# Surface and cell layout.
|
|
"flat bordered separator=horizontal wrap-cells"
|
|
)
|
|
.props(
|
|
# Local sorting and page-size choices; zero means "all rows".
|
|
'binary-state-sort :rows-per-page-options="[5, 10, 0]"'
|
|
)
|
|
.props(
|
|
# Compact cells only below Quasar's medium breakpoint.
|
|
':dense="Quasar.Screen.lt.md"'
|
|
)
|
|
.props(
|
|
# Distinguish an empty dataset from a filter with no matches.
|
|
'no-data-label="No inventory items" no-results-label="No matching inventory items"'
|
|
)
|
|
)
|
|
|
|
with table.add_slot("top-left"), ui.row(align_items="center").classes("gap-2"):
|
|
ui.icon("inventory_2", size="sm", color="primary")
|
|
ui.label("Inventory").classes("text-xl font-medium")
|
|
ui.badge(str(len(ROWS)), color="grey-3", text_color="grey-9")
|
|
|
|
with table.add_slot("top-right"):
|
|
ui.input(placeholder="Search inventory").props("dense outlined clearable debounce=250").bind_value_to(
|
|
table,
|
|
"filter",
|
|
)
|
|
|
|
with table.add_slot("body-cell-status"), table.cell("status"):
|
|
ui.badge(outline=True).props(':label="props.value" :color="props.col.colorByValue[props.value] ?? \'grey\'"')
|
|
|
|
def open_product(event: events.GenericEventArguments) -> None:
|
|
product = next((row for row in table.rows if row[table.row_key] == event.args), None)
|
|
if product is None:
|
|
ui.notify("Product no longer exists", type="negative")
|
|
return
|
|
ui.notify(f"Opening {product['name']}")
|
|
|
|
with (
|
|
table.add_slot("body-cell-actions"),
|
|
table.cell("actions"),
|
|
ui.button(icon="open_in_new")
|
|
.props("round flat size='sm'")
|
|
.on(
|
|
"click.stop",
|
|
handler=open_product,
|
|
js_handler="() => emit(props.key)",
|
|
),
|
|
):
|
|
ui.tooltip("Open product")
|
|
|
|
with table.add_slot("no-data"), ui.row(align_items="center").classes("w-full justify-center gap-2 p-6 text-grey-7"):
|
|
ui.icon("inventory_2", size="2em").props(":name=\"props.filter ? 'filter_alt_off' : 'inventory_2'\"")
|
|
ui.element("span").props(':textContent="props.message"')
|
|
|
|
optional_columns = [column for column in table.columns if not column.get("required")]
|
|
|
|
def set_visible_columns(names: list[str]) -> None:
|
|
table.props["visible-columns"] = names
|
|
table.update()
|
|
|
|
ui.select(
|
|
{column["name"]: column["label"] for column in optional_columns},
|
|
value=[column["name"] for column in optional_columns],
|
|
label="Visible columns",
|
|
multiple=True,
|
|
clearable=True,
|
|
on_change=lambda event: set_visible_columns(event.value),
|
|
).props("outlined dense options-dense").classes("w-64")
|
|
|
|
return table
|
|
|
|
|
|
if __name__ in {"__main__", "__mp_main__"}:
|
|
with ui.column(align_items="center").classes("w-full gap-4 p-4"):
|
|
render_table()
|
|
|
|
ui.run(port=8888, reload=True)
|