diff --git a/src/ui/app.py b/src/ui/app.py index 010cb0d..cabda57 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -1,7 +1,6 @@ from flask import Flask from items import items_page from persons import persons_page -from distribute import distribute_page from extras import extras_page from payments import payments_page import os @@ -10,6 +9,5 @@ app.secret_key = os.environ["FLASK_SECRET_KEY"] app.register_blueprint(items_page) app.register_blueprint(persons_page) -app.register_blueprint(distribute_page) app.register_blueprint(extras_page) app.register_blueprint(payments_page) diff --git a/src/ui/distribute.py b/src/ui/distribute.py deleted file mode 100644 index c972e28..0000000 --- a/src/ui/distribute.py +++ /dev/null @@ -1,99 +0,0 @@ -from flask import ( - render_template, - session, - Blueprint, - request, - jsonify, -) -from logging import getLogger -from items import get_current_items -from persons import get_current_persons, save_persons_file - -log = getLogger(__file__) - -distribute_page = Blueprint("distribute", __name__) - - -@distribute_page.route("/distribute", methods=["GET"]) -def distribute_page_view(): - item_index = request.args.get("item_index", type=int) or 0 - - items = get_current_items(session) - persons = get_current_persons(session) - item = items.items[item_index] - - return render_template( - "distribute.html", - item=item, - persons=persons or [], - item_index=item_index or 0, - item_count=len(items.items), - ) - - -@distribute_page.route("/get_item", methods=["GET"]) -def get_item(): - item_index = request.args.get("item_index", type=int) or 0 - - items = get_current_items(session) - item = items.items[item_index] - - return jsonify({"name": item.name, "price": item.price}), 200 - - -@distribute_page.route("/distribute_item", methods=["POST"]) -def distribute_item(): - data = request.get_json() - item_index = data.get("item_index") - person_ids = data.get("person_ids", []) - - items = get_current_items(session) - persons = get_current_persons(session) - item = items.items[item_index] - - num_persons = len(person_ids) - share_per_person = item.price / num_persons - - distribution = [ - { - "person_id": person_id, - "person_name": persons[person_id].name, - "share": share_per_person, - } - for person_id in person_ids - ] - - total_distributed = share_per_person * num_persons - - return ( - jsonify( - { - "success": True, - "distribution": distribution, - "total_distributed": total_distributed, - "item_name": item.name, - "item_price": item.price, - "num_persons": num_persons, - } - ), - 200, - ) - - -@distribute_page.route("/save_distribution", methods=["POST"]) -def save_distribution(): - data = request.get_json() - item_index = data.get("item_index") - person_ids = data.get("person_ids", []) - - persons = get_current_persons(session) - - for person_id, person in enumerate(persons): - if person_id in person_ids: - person.items = sorted(set(person.items + [item_index])) - elif item_index in person.items: - person.items.remove(item_index) - - save_persons_file(persons, session) - - return jsonify({"success": True}), 200 diff --git a/src/ui/distribute.ts b/src/ui/distribute.ts deleted file mode 100644 index 5957ec3..0000000 --- a/src/ui/distribute.ts +++ /dev/null @@ -1,263 +0,0 @@ -let selectedPersons: number[] = []; -let currentItem: { name: string; price: number } | null = null; -let currentItemIndex: number = 0; -let totalItemCount: number = 0; - -const itemName = document.getElementById("item-name") as HTMLSpanElement; -const itemPrice = document.getElementById("item-price") as HTMLSpanElement; -const prevItemButton = document.getElementById( - "prev-item-button", -) as HTMLButtonElement; -const nextItemButton = document.getElementById( - "next-item-button", -) as HTMLButtonElement; -const backButton = document.getElementById("back-button") as HTMLButtonElement; -const distributeButton = document.getElementById( - "distribute-button", -) as HTMLButtonElement; -const distributionResults = document.getElementById( - "distribution-results", -) as HTMLDivElement; -const resultsList = document.getElementById("results-list") as HTMLDivElement; -const loadingOverlay = document.getElementById("loading-overlay") as HTMLElement; - -document.addEventListener("DOMContentLoaded", async () => { - prevItemButton.addEventListener("click", handlePrevItem); - nextItemButton.addEventListener("click", handleNextItem); - backButton.addEventListener("click", handleBack); - distributeButton.addEventListener("click", handleDone); - - initializeItemData(); - - setupPersonSelectionHandlers(); - - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - handleBack(); - } - }); -}); - -function setupPersonSelectionHandlers(): void { - const selectedPersonClasses = [ - "bg-green-600/30", - "border-green-400", - "border-2", - "ring-4", - "ring-green-400/40", - "shadow-lg", - ] as const; - selectedPersons = []; - - const personElements = document.querySelectorAll(".person-box"); - personElements.forEach((personBox) => { - const personId = parseInt(personBox.getAttribute("data-person-id") || "0"); - - let addPerson = () => { - console.log("Adding person", personId); - selectedPersons.push(personId); - personBox.classList.add(...selectedPersonClasses); - }; - - let removePerson = () => { - console.log("Removing person", personId); - personBox.classList.remove(...selectedPersonClasses); - }; - - const wasPersonSelected = - personBox.getAttribute("data-selected") === "true"; - if (wasPersonSelected) { - addPerson(); - } - - personBox.addEventListener("click", (e) => { - const isPersonDeselected = selectedPersons.includes(personId); - - if (isPersonDeselected) { - selectedPersons = selectedPersons.filter((id) => id !== personId); - removePerson(); - personBox.setAttribute("data-selected", "false"); - } else { - addPerson(); - personBox.setAttribute("data-selected", "true"); - } - - updateDistributeButton(); - }); - }); - - updateDistributeButton(); -} - -function initializeItemData(): void { - const itemCountElement = document.getElementById( - "item-count", - ) as HTMLMetaElement; - const itemIndexElement = document.getElementById( - "item-index", - ) as HTMLMetaElement; - const itemNameElement = document.getElementById( - "item-name", - ) as HTMLMetaElement; - const itemPriceElement = document.getElementById( - "item-price", - ) as HTMLMetaElement; - - totalItemCount = parseInt(itemCountElement.content || "0"); - currentItemIndex = parseInt(itemIndexElement.content || "0"); - - const itemName = itemNameElement.content || ""; - const itemPrice = parseFloat(itemPriceElement.content || "0"); - - currentItem = { - name: itemName, - price: itemPrice, - }; - console.log("Current item", currentItem); - - updateNavigationButtons(); -} - -async function getCurrentItem(): Promise { - const itemIndex = getItemIndexFromUrl(); - - if (itemIndex) { - try { - const response = await fetch(`/get_item?item_index=${itemIndex}`); - if (response.ok) { - const item = await response.json(); - currentItem = item; - updateItemDisplay(); - } - } catch (error) { - console.error("Error fetching item:", error); - } - } -} - -function updateItemDisplay(): void { - if (currentItem) { - itemName.textContent = currentItem.name; - itemPrice.textContent = `$${currentItem.price.toFixed(2)}`; - } -} - -function getItemSharedCount(): number { - return selectedPersons.length; -} - -function updateDistributeButton(): void { - const distributionClasses = ["opacity-50", "cursor-not-allowed"] as const; - - let isNoOneSelected = getItemSharedCount() === 0; - distributeButton.disabled = isNoOneSelected; - - if (isNoOneSelected) { - distributeButton.classList.add(...distributionClasses); - } else { - distributeButton.classList.remove(...distributionClasses); - } - - updatePersonShares(); -} - -function updatePersonShares(): void { - if (!currentItem) { - return; - } - - document.querySelectorAll(".person-share").forEach((element) => { - (element as HTMLSpanElement).textContent = "$0.00"; - }); - - const itemSharedCount = getItemSharedCount(); - if (itemSharedCount === 0) { - return; - } - - const sharePerPerson = currentItem.price / itemSharedCount; - - selectedPersons.forEach((personId) => { - const shareElement = document.querySelector( - `.person-share[data-person-id="${personId}"]`, - ) as HTMLSpanElement; - if (shareElement) { - shareElement.textContent = `$${sharePerPerson.toFixed(2)}`; - } - }); -} - -async function handlePrevItem(): Promise { - if (currentItemIndex > 0) { - await saveCurrentDistribution(); - window.location.href = `/distribute?item_index=${currentItemIndex - 1}`; - } -} - -async function handleNextItem(): Promise { - if (currentItemIndex < totalItemCount - 1) { - await saveCurrentDistribution(); - window.location.href = `/distribute?item_index=${currentItemIndex + 1}`; - } -} - -function updateNavigationButtons(): void { - const navigationClasses = ["opacity-50", "cursor-not-allowed"] as const; - - prevItemButton.disabled = currentItemIndex <= 0; - if (currentItemIndex <= 0) { - prevItemButton.classList.add(...navigationClasses); - } else { - prevItemButton.classList.remove(...navigationClasses); - } - - nextItemButton.disabled = currentItemIndex >= totalItemCount - 1; - if (currentItemIndex >= totalItemCount - 1) { - nextItemButton.classList.add(...navigationClasses); - } else { - nextItemButton.classList.remove(...navigationClasses); - } -} - -async function saveCurrentDistribution(): Promise { - if (selectedPersons.length === 0 || !currentItem) { - return; - } - - try { - const response = await fetch("/save_distribution", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - item_index: getItemIndexFromUrl(), - person_ids: selectedPersons, - item_name: currentItem.name, - item_price: currentItem.price, - }), - }); - - if (!response.ok) { - console.error("Failed to save distribution"); - } - } catch (error) { - console.error("Error saving distribution:", error); - } -} - -async function handleBack(): Promise { - await saveCurrentDistribution(); - window.location.href = "/items"; -} - -async function handleDone(): Promise { - await saveCurrentDistribution(); - loadingOverlay.classList.remove("hidden"); - window.location.href = "/extras"; -} - -function getItemIndexFromUrl(): number { - const urlParams = new URLSearchParams(window.location.search); - return parseInt(urlParams.get("item_index") || "0"); -} diff --git a/src/ui/extras.ts b/src/ui/extras.ts index 82b4f65..386ce98 100644 --- a/src/ui/extras.ts +++ b/src/ui/extras.ts @@ -173,7 +173,7 @@ async function handleExtraSave(): Promise { } async function navigateBack(): Promise { - window.location.href = "/distribute"; + window.location.href = "/items"; } async function navigateDone(): Promise { diff --git a/src/ui/items.ts b/src/ui/items.ts index ce250c2..02e99e7 100644 --- a/src/ui/items.ts +++ b/src/ui/items.ts @@ -40,6 +40,9 @@ const editTitle = document.getElementById("edit-title") as HTMLHeadingElement; const editSubtitle = document.getElementById( "edit-subtitle", ) as HTMLParagraphElement; +const loadingOverlay = document.getElementById( + "loading-overlay", +) as HTMLElement; function showItemsList(): void { itemsListView.classList.remove("hidden"); @@ -83,6 +86,14 @@ function showAddItem(): void { editItemName.focus(); } +function showLoadingOverlay(): void { + loadingOverlay.classList.remove("hidden"); +} + +function hideLoadingOverlay(): void { + loadingOverlay.classList.add("hidden"); +} + function setupItemClickHandlers(): void { const itemElements = document.querySelectorAll("[data-item-index]"); itemElements.forEach((itemElement) => { @@ -189,6 +200,8 @@ async function navigateToPersons(): Promise { } async function navigateToSplit(): Promise { + showLoadingOverlay(); + try { const response = await fetch("/prepare_split", { method: "POST", @@ -202,18 +215,22 @@ async function navigateToSplit(): Promise { console.log( `Prepared split: ${result.person_count} persons, ${result.item_count} items`, ); - window.location.href = "/distribute"; + window.location.href = "/extras"; } else { const error = await response.json(); + hideLoadingOverlay(); alert(error.error || "Failed to prepare split"); } } catch (error) { console.error("Error preparing for split:", error); + hideLoadingOverlay(); alert("Error preparing for split. Please try again."); } } document.addEventListener("DOMContentLoaded", () => { + hideLoadingOverlay(); + cancelButton.addEventListener("click", handleCancel); splitButton.addEventListener("click", handleSplit); saveButton.addEventListener("click", handleSave); diff --git a/src/ui/payments.py b/src/ui/payments.py index 96d5625..c3f5210 100644 --- a/src/ui/payments.py +++ b/src/ui/payments.py @@ -10,7 +10,8 @@ from logging import getLogger from items import get_current_items from extras import get_current_extras -from persons import get_current_persons, save_persons_file +from persons import get_current_persons +from session_data import save_persons_file from datetime import datetime log = getLogger(__file__) diff --git a/src/ui/payments.ts b/src/ui/payments.ts index 663a4d7..98df400 100644 --- a/src/ui/payments.ts +++ b/src/ui/payments.ts @@ -24,6 +24,7 @@ document.addEventListener("DOMContentLoaded", () => { initializePersonData(); setupItemClickedHandlers(); + initializeItemHighlighting(); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { @@ -79,6 +80,25 @@ function setupItemClickedHandlers(): void { }); } +function initializeItemHighlighting(): void { + const itemElements = document.querySelectorAll(".item-box"); + itemElements.forEach((itemElement) => { + const shareElement = itemElement.querySelector(".text-blue-400.font-semibold") as HTMLElement; + if (shareElement) { + const shareText = shareElement.textContent; + if (shareText) { + const shareMatch = shareText.match(/\$(\d+\.\d+)/); + if (shareMatch) { + const share = parseFloat(shareMatch[1]); + if (share > 0) { + itemElement.classList.add("ring-4", "ring-green-400/40", "border-green-400"); + } + } + } + } + }); +} + async function handleItemClick(e: Event): Promise { const itemElement = e.currentTarget as HTMLElement; const itemIndex = parseInt(itemElement.getAttribute("data-item-index") || "0"); @@ -119,16 +139,21 @@ function updateItemInfoDisplay(itemElement: HTMLElement, data: any): void { } if (data.person_subtotal !== undefined) { - const subtotalElement = document.querySelector(".text-green-400") as HTMLElement; + const subtotalElement = document.getElementById("person-subtotal") as HTMLElement; if (subtotalElement) { subtotalElement.textContent = `$${data.person_subtotal.toFixed(2)}`; } } if (data.person_total !== undefined) { - const totalElement = document.getElementById("person-total") as HTMLElement; + const totalElement = document.getElementById("person-total-bottom") as HTMLElement; if (totalElement) { totalElement.textContent = `$${data.person_total.toFixed(2)}`; } + + const personTotalElement = document.getElementById("person-total") as HTMLElement; + if (personTotalElement) { + personTotalElement.textContent = `$${data.person_total.toFixed(2)}`; + } } } diff --git a/src/ui/templates/distribute.html b/src/ui/templates/distribute.html deleted file mode 100644 index 3cf61af..0000000 --- a/src/ui/templates/distribute.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - Split - - - - -
-
-
- - - -
-
-

Split

-
-
- - -
- - -
- - -
- - -
- - -
- - -
- {{ item.name if item else 'No item selected' }} - ${{ "%.2f"|format(item.price) if item else '0.00' }} -
-
- - -
- - - -
- {% if persons %} - {% for person in persons %} -
-
- {{ person.name }} -
- $0.00 -
- {% endfor %} - {% else %} -
- - - -

No people found

-

Add people first to distribute items

-
- {% endif %} -
- - - - - -
-
- - - - - - - - \ No newline at end of file diff --git a/src/ui/templates/extras.html b/src/ui/templates/extras.html index d3bc3e9..b9cee7b 100644 --- a/src/ui/templates/extras.html +++ b/src/ui/templates/extras.html @@ -31,7 +31,7 @@

Extras

- Split + Items