diff --git a/.github/workflows/lint-format-write.yml b/.github/workflows/lint-format-write.yml index 7912838..850a78d 100644 --- a/.github/workflows/lint-format-write.yml +++ b/.github/workflows/lint-format-write.yml @@ -11,17 +11,17 @@ on: jobs: lint-format-write: - # Only run on manual trigger, not on main/master branches if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 with: - # Need to fetch all history for git operations fetch-depth: 0 - # Need write permissions to push commits token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python @@ -66,4 +66,10 @@ jobs: run: | git add -A git commit -m "${{ github.event.inputs.commit_message }}" - git push origin ${{ github.ref }} \ No newline at end of file + git push origin ${{ github.ref }} || { + echo "Failed to push changes. This might be due to:" + echo "1. Insufficient permissions on the token" + echo "2. Branch protection rules" + echo "3. Repository settings" + exit 1 + } diff --git a/src/ui/distribute.py b/src/ui/distribute.py index c972e28..aaa4fcd 100644 --- a/src/ui/distribute.py +++ b/src/ui/distribute.py @@ -16,18 +16,35 @@ @distribute_page.route("/distribute", methods=["GET"]) def distribute_page_view(): - item_index = request.args.get("item_index", type=int) or 0 + person_index = request.args.get("person_index", type=int) or 0 items = get_current_items(session) persons = get_current_persons(session) - item = items.items[item_index] + person = ( + persons[person_index] if persons and 0 <= person_index < len(persons) else None + ) + + # Calculate per-item share for this person using map and list comprehension + def share_for_item(args): + idx, item = args + count = sum(1 for p in persons if idx in p.items) + share = ( + item.price / count if person and idx in person.items and count > 0 else 0.0 + ) + return { + "name": item.name, + "price": item.price, + "share": share, + } + + item_shares = list(map(share_for_item, enumerate(items.items))) return render_template( "distribute.html", - item=item, - persons=persons or [], - item_index=item_index or 0, - item_count=len(items.items), + items=item_shares, + person=person, + person_index=person_index, + person_count=len(persons), ) @@ -80,20 +97,34 @@ def distribute_item(): ) +@distribute_page.route("/get_persons", methods=["GET"]) +def get_persons_api(): + persons = get_current_persons(session) + return jsonify([p.model_dump() for p in persons]), 200 + + +@distribute_page.route("/get_items", methods=["GET"]) +def get_items_api(): + items = get_current_items(session) + return jsonify([{"name": i.name, "price": i.price} for i in items.items]), 200 + + @distribute_page.route("/save_distribution", methods=["POST"]) def save_distribution(): data = request.get_json() + person_index = data.get("person_index") item_index = data.get("item_index") - person_ids = data.get("person_ids", []) + add = data.get("add", True) 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) - + if 0 <= person_index < len(persons): + person = persons[person_index] + if add: + if item_index not in person.items: + person.items.append(item_index) + person.items.sort() + else: + if 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 index 5957ec3..b0e1c6c 100644 --- a/src/ui/distribute.ts +++ b/src/ui/distribute.ts @@ -3,6 +3,12 @@ let currentItem: { name: string; price: number } | null = null; let currentItemIndex: number = 0; let totalItemCount: number = 0; +// Person-based distribution logic +let currentPersonIndex: number = 0; +let totalPersonCount: number = 0; +let persons: any[] = []; +let items: any[] = []; + const itemName = document.getElementById("item-name") as HTMLSpanElement; const itemPrice = document.getElementById("item-price") as HTMLSpanElement; const prevItemButton = document.getElementById( @@ -21,6 +27,19 @@ const distributionResults = document.getElementById( const resultsList = document.getElementById("results-list") as HTMLDivElement; const loadingOverlay = document.getElementById("loading-overlay") as HTMLElement; +const prevPersonButton = document.getElementById("prev-person-button") as HTMLButtonElement; +const nextPersonButton = document.getElementById("next-person-button") as HTMLButtonElement; +const personName = document.getElementById("person-name") as HTMLSpanElement; + +const itemNameElement = document.getElementById( + "item-name", +) as HTMLMetaElement; +const itemPriceElement = document.getElementById( + "item-price", +) as HTMLMetaElement; + +const personElements = document.querySelectorAll(".person-box"); + document.addEventListener("DOMContentLoaded", async () => { prevItemButton.addEventListener("click", handlePrevItem); nextItemButton.addEventListener("click", handleNextItem); @@ -28,6 +47,18 @@ document.addEventListener("DOMContentLoaded", async () => { distributeButton.addEventListener("click", handleDone); initializeItemData(); + initializePersonData(); + await fetchDistributeData(); + renderItemsForPerson(); + updateNavigationButtons(); + + prevPersonButton.addEventListener("click", handlePrevPerson); + nextPersonButton.addEventListener("click", handleNextPerson); + + // Attach click handlers to item boxes + document.querySelectorAll(".item-box").forEach((itemBox) => { + itemBox.addEventListener("click", handleItemBoxClick); + }); setupPersonSelectionHandlers(); @@ -49,7 +80,6 @@ function setupPersonSelectionHandlers(): void { ] as const; selectedPersons = []; - const personElements = document.querySelectorAll(".person-box"); personElements.forEach((personBox) => { const personId = parseInt(personBox.getAttribute("data-person-id") || "0"); @@ -96,16 +126,6 @@ function initializeItemData(): void { 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"); @@ -261,3 +281,97 @@ function getItemIndexFromUrl(): number { const urlParams = new URLSearchParams(window.location.search); return parseInt(urlParams.get("item_index") || "0"); } + +// Helper to fetch initial data from meta tags or window context +function initializePersonData(): void { + const personCountElement = document.getElementById("person-count") as HTMLMetaElement; + const personIndexElement = document.getElementById("person-index") as HTMLMetaElement; + totalPersonCount = parseInt(personCountElement.content || "0"); + currentPersonIndex = parseInt(personIndexElement.content || "0"); +} + +// Fetch all persons and items from backend +async function fetchDistributeData() { + const [personsRes, itemsRes] = await Promise.all([ + fetch("/get_persons"), + fetch("/get_items"), + ]); + persons = await personsRes.json(); + items = await itemsRes.json(); +} + +// Render all items for the current person +function renderItemsForPerson() { + const person = persons[currentPersonIndex]; + personName.textContent = person.name; + const itemsList = document.querySelectorAll(".item-box"); + itemsList.forEach((itemBox, idx) => { + const itemIndex = parseInt((itemBox as HTMLElement).getAttribute("data-item-index") || "0"); + if (person.items.includes(itemIndex)) { + itemBox.classList.add("ring-4", "ring-green-400/40", "border-green-400"); + } else { + itemBox.classList.remove("ring-4", "ring-green-400/40", "border-green-400"); + } + // Update amount for this item for this person + const amountDiv = itemBox.querySelector(".text-blue-400.font-semibold") as HTMLElement; + const share = getPersonItemShare(itemIndex, person); + amountDiv.textContent = `$${share.toFixed(2)}`; + }); +} + +// Calculate share for a person for an item +function getPersonItemShare(itemIndex: number, person: any): number { + const item = items[itemIndex]; + // Count how many persons have this item + const count = persons.filter(p => p.items.includes(itemIndex)).length; + if (person.items.includes(itemIndex) && count > 0) { + return item.price / count; + } + return 0; +} + +// Toggle item for current person +async function handleItemBoxClick(e: Event) { + const itemBox = e.currentTarget as HTMLElement; + const itemIndex = parseInt(itemBox.getAttribute("data-item-index") || "0"); + const person = persons[currentPersonIndex]; + const hasItem = person.items.includes(itemIndex); + // Update backend + await fetch("/save_distribution", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + person_index: currentPersonIndex, + item_index: itemIndex, + add: !hasItem + }) + }); + // Update local state + if (hasItem) { + person.items = person.items.filter((idx: number) => idx !== itemIndex); + } else { + person.items.push(itemIndex); + } + renderItemsForPerson(); +} + +// Navigation +function updateNavigationButtons() { + prevPersonButton.disabled = currentPersonIndex <= 0; + nextPersonButton.disabled = currentPersonIndex >= totalPersonCount - 1; +} + +async function handlePrevPerson() { + if (currentPersonIndex > 0) { + currentPersonIndex--; + renderItemsForPerson(); + updateNavigationButtons(); + } +} +async function handleNextPerson() { + if (currentPersonIndex < totalPersonCount - 1) { + currentPersonIndex++; + renderItemsForPerson(); + updateNavigationButtons(); + } +} diff --git a/src/ui/templates/distribute.html b/src/ui/templates/distribute.html index 3cf61af..16812f6 100644 --- a/src/ui/templates/distribute.html +++ b/src/ui/templates/distribute.html @@ -4,11 +4,9 @@
- - - - -No people found
-Add people first to distribute items
+No items