working prototype
This commit is contained in:
200
src/invoices.py
Normal file
200
src/invoices.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from typing import List
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Footer, Header, Tabs, TabbedContent, TabPane, Select
|
||||
from textual.containers import Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import ListView, DataTable, ListItem, Label, Static
|
||||
from textual.reactive import Reactive
|
||||
from textual import work
|
||||
from services.pancake import ListResponse, PancakeAPI, ClientResponse, ProjectResponse
|
||||
from datetime import datetime, UTC, timedelta
|
||||
class InvoicesApp(App):
|
||||
"""A Textual app to manage stopwatches."""
|
||||
CSS_PATH = "styles/invoices.tcss"
|
||||
clients: List[ClientResponse]
|
||||
projects: List[ProjectResponse]
|
||||
client_project_id: str
|
||||
BINDINGS = [
|
||||
("d", "toggle_dark", "Toggle dark mode"),
|
||||
("q", "quit", "Quit the App"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets for the app."""
|
||||
yield Header()
|
||||
with Horizontal():
|
||||
with Vertical(id="sidebar"):
|
||||
yield ListView(
|
||||
id="client_list",
|
||||
)
|
||||
with ScrollableContainer(id="main"):
|
||||
with TabbedContent():
|
||||
with TabPane("Projects"):
|
||||
yield DataTable(id="project_list")
|
||||
with TabPane("Invoices"):
|
||||
yield DataTable(id="invoice_list")
|
||||
with TabPane("Timesheet"):
|
||||
with ScrollableContainer(id="timesheets"):
|
||||
yield Select([("loading", 0)],id="timesheet_select")
|
||||
yield DataTable(id="timesheet_list")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
#load data from pancake api
|
||||
self.pancake = PancakeAPI()
|
||||
self.load_sidebar()
|
||||
self.query_one("#timesheet_list").add_columns(
|
||||
"ID",
|
||||
"Is Billed",
|
||||
"Date",
|
||||
"Minutes",
|
||||
"Notes"
|
||||
)
|
||||
self.query_one("#invoice_list", DataTable).add_columns(
|
||||
"#",
|
||||
"Payment Date",
|
||||
"Amount",
|
||||
"Due Date",
|
||||
"Is Paid",
|
||||
"Is Viewable",
|
||||
"Last Viewed",
|
||||
"Paid",
|
||||
"Overdue")
|
||||
self.query_one("#project_list", DataTable).add_columns(
|
||||
"ID",
|
||||
"Name",
|
||||
"Is Archived",
|
||||
"Is Completed",
|
||||
"Created At",
|
||||
"Updated At"
|
||||
)
|
||||
|
||||
@work
|
||||
async def load_sidebar(self) -> None:
|
||||
client_resp = await self.pancake.get_all_clients({"limit": 100, "sort_by": "id", "sort_dir": "asc"})
|
||||
self.clients = sorted(client_resp.get("clients", []), key=lambda x: x['total'], reverse=True)
|
||||
project_resp = await self.pancake.get_all_projects({"limit": 100})
|
||||
self.projects = project_resp.get("projects", [])
|
||||
# self.query_one("#invoice_list", ListView).extend(ListItem(f"{invoice['id']} {invoice['client']}") for invoice in self.invoices)
|
||||
client_list = self.query_one("#client_list", ListView)
|
||||
client_list.clear()
|
||||
client_list.border_title = "Clients"
|
||||
|
||||
for client in self.clients:
|
||||
client_list.append(ListItem(Label(f"{client.get('company', '-')} {client.get('email', '@')}"), id="client_" + str(client["id"]))) if client.get("archived") != "1" else None
|
||||
|
||||
|
||||
|
||||
# if self.clients:
|
||||
# self.fetch_invoices(self.clients[0]["id"])
|
||||
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
"""Handle the selection of an invoice."""
|
||||
if event.list_view.id == "client_list":
|
||||
selected_client_id = event.item.id.split("_")[1]
|
||||
if selected_client_id:
|
||||
self.fetch_invoices(selected_client_id)
|
||||
self.show_projects(selected_client_id)
|
||||
@work
|
||||
async def fetch_invoices(self, client_id: str) -> None:
|
||||
invoices = await self.pancake.get_all_invoices(params={"client_id": client_id, "limit": 100, "sort_by": "id", "sort_dir": "desc"})
|
||||
invoice_details = invoices.get("invoices", [])
|
||||
table = self.query_one("#invoice_list", DataTable)
|
||||
table.clear()
|
||||
table.border_title = "Invoices"
|
||||
|
||||
|
||||
for detail in invoice_details:
|
||||
table.add_row(
|
||||
detail["invoice_number"],
|
||||
detail["payment_date"],
|
||||
str(detail["amount"]),
|
||||
detail["due_date"],
|
||||
detail["is_paid"],
|
||||
detail["is_viewable"],
|
||||
detail.get("last_viewed", "N/A"),
|
||||
detail.get("paid", "No"),
|
||||
detail.get("overdue", "No"),
|
||||
key=detail["id"]
|
||||
)
|
||||
|
||||
|
||||
|
||||
def show_projects(self, client_id: str) -> None:
|
||||
"""Show projects for the selected client."""
|
||||
table = self.query_one("#project_list", DataTable)
|
||||
table.clear()
|
||||
|
||||
table.border_title = "Projects"
|
||||
|
||||
# create a new list that is sorted by id and only contains the projects for the selected client
|
||||
client_projects = sorted(
|
||||
self.projects,
|
||||
key=lambda x: x["id"],
|
||||
reverse=True
|
||||
)
|
||||
# filter the projects by client_id
|
||||
client_projects = [project for project in client_projects if project["client_id"] == client_id]
|
||||
|
||||
|
||||
select = self.query_one("#timesheet_select", Select)
|
||||
select.clear()
|
||||
select.set_options((project["name"], project["id"],) for project in client_projects)
|
||||
self.selected_project_id = client_projects[0]["id"] if client_projects else None
|
||||
if (self.selected_project_id):
|
||||
select.value = self.selected_project_id
|
||||
self.fetch_timesheet(self.selected_project_id)
|
||||
for project in client_projects:
|
||||
if project["client_id"] == client_id:
|
||||
|
||||
table.add_row(
|
||||
str(project["id"]),
|
||||
project["name"],
|
||||
project["is_archived"],
|
||||
project.get("completed", "No"),
|
||||
project["date_entered"],
|
||||
project["date_updated"],
|
||||
)
|
||||
|
||||
@work
|
||||
async def fetch_timesheet(self, project_id: str) -> None:
|
||||
"""Fetch timesheet for the selected project."""
|
||||
#is project_id a string of digits
|
||||
if not project_id or not str(project_id).isdigit():
|
||||
return
|
||||
|
||||
table = self.query_one("#timesheet_list", DataTable)
|
||||
table.clear()
|
||||
|
||||
table.border_title = "Timesheets"
|
||||
|
||||
# fetch timesheet data from pancake api
|
||||
timesheet_resp = await self.pancake.get_one_project(project_id)
|
||||
timesheet_details = timesheet_resp.get("times", [])
|
||||
timesheet_details = sorted(timesheet_details, key=lambda x: x["date"], reverse=True)
|
||||
for detail in timesheet_details:
|
||||
table.add_row(
|
||||
str(detail["id"]),
|
||||
"Yes" if detail.get("invoice_item_id") else "No",
|
||||
datetime.fromtimestamp(int(detail["date"])).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
round(float(detail.get("minutes", 0))), #round the minutes to int
|
||||
detail.get("note", "N/A")
|
||||
)
|
||||
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
"""Handle the selection of a project."""
|
||||
if event.select.id == "timesheet_select":
|
||||
selected_project_id = event.select.value
|
||||
if selected_project_id:
|
||||
self.fetch_timesheet(selected_project_id)
|
||||
|
||||
|
||||
def action_toggle_dark(self) -> None:
|
||||
"""An action to toggle dark mode."""
|
||||
self.theme = (
|
||||
"textual-dark" if self.theme == "textual-light" else "textual-light"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InvoicesApp()
|
||||
app.run()
|
||||
Reference in New Issue
Block a user