Skip to content

Commit 52ca750

Browse files
dphoriacursoragentswc.pubactions-user
authored
feature/share-on-list (#9)
* Refactor distribute page to support per-person item distribution Co-authored-by: swc.pub <swc.pub@pm.me> * Refactor item share calculation using map and list comprehension Co-authored-by: swc.pub <swc.pub@pm.me> * feature/actions (#6) * feature/pull-request-actions (#3) * feat: Add GitHub Actions for pull request checks - Create feature/pull-request-actions branch - Add comprehensive CI workflow with tests, linting, and security checks - Add pull request workflow for basic quality checks - Add security workflow for vulnerability scanning - Update pyproject.toml with additional dev and security dependencies - Update README with development workflow documentation - Configure PDM for dependency management - Set up ruff, black, pytest, bandit, and safety checks * chore: Update lock file with security dependencies * refactor: Consolidate GitHub Actions into single comprehensive workflow - Remove redundant pull-request.yml and security.yml files - Consolidate all checks into single ci.yml workflow - Add conditional job execution for optimized performance - Quick checks for PRs, full suite for main branch - Update README to reflect simplified approach * simplify: Remove lint and security jobs from GitHub Actions - Remove lint job (ruff format, isort checks) - Remove security job (bandit, safety scans) - Keep only quick-checks and test jobs - Update README to reflect simplified workflow - Streamline local development instructions * feat: Make test job run only on manual triggers - Add conditional 'if: github.event_name == workflow_dispatch' to test job - Test job now only runs when manually triggered - Pull requests only run quick checks (Ruff + Black) - Update README to reflect new behavior * revert: Restore README.md to original state - Remove all GitHub Actions documentation - Remove development workflow instructions - Restore original README content * feat: Add pytest to quick-checks job - Add pytest step to quick-checks job for pull requests - Exclude test_receipt_analysis_with_chat test that calls OpenAI API - Use -k 'not test_receipt_analysis_with_chat' to skip the API test - All other tests (15/16) run successfully in quick checks * fix: Add ruff configuration to resolve CI failures - Add comprehensive ruff configuration to pyproject.toml - Use modern lint section syntax to avoid deprecation warnings - Ignore import sorting issues (I001) to focus on code quality - Ignore other style issues that don't affect functionality - Ensure consistent behavior between local and CI environments * fix: Simplify ruff configuration to resolve CI issues - Reduce ruff rules to only essential error checking (E, W, F) - Remove complex rules that might cause version compatibility issues - Keep only basic pycodestyle errors, warnings, and pyflakes - Ignore only line length issues (E501) which are handled by black * fix: Use pdm sync instead of pdm install in GitHub Actions - Change pdm install to pdm sync to ensure all dependencies are installed - pdm sync installs all groups including dev dependencies (ruff, black, pytest) - pdm install only installs main dependencies by default - This should resolve the 'ruff command not found' error in CI * fix: Add dev group to lockfile and update GitHub Actions - Add dev group to pdm.lock so dev dependencies can be installed - Update GitHub Actions to explicitly install dev group with --group dev - Add pdm list command for debugging to see what's installed - This should resolve the missing ruff and other dev tools in CI * test binary data --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * feat: add manual lint-format-write GitHub Action workflow (#5) Co-authored-by: Cursor Agent <cursoragent@cursor.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * bugfix/action-commit (#7) * fix: add explicit permissions to lint-format workflow to resolve authentication issues * test fix --------- Co-authored-by: GitHub Action <action@github.com> * Simplify person items rendering and filtering logic Co-authored-by: swc.pub <swc.pub@pm.me> * Add method to get individual person's shares across all items Co-authored-by: swc.pub <swc.pub@pm.me> * Modify get_person_shares to return item-share tuples instead of just shares Co-authored-by: swc.pub <swc.pub@pm.me> * Refactor get_person_shares to use lazy evaluation with map Co-authored-by: swc.pub <swc.pub@pm.me> * Add distribute_item endpoint for splitting bill item among people Co-authored-by: swc.pub <swc.pub@pm.me> * Remove total_distributed calculation from item distribution response Co-authored-by: swc.pub <swc.pub@pm.me> * Add Calculator initialization with empty extras in distribute_item Co-authored-by: swc.pub <swc.pub@pm.me> * Refactor item distribution to use Calculator for precise share calculation Co-authored-by: swc.pub <swc.pub@pm.me> * Remove distribute_item function from payments page Co-authored-by: swc.pub <swc.pub@pm.me> * Add route to share/unshare payment items between persons Co-authored-by: swc.pub <swc.pub@pm.me> * Simplify share_item logic by removing redundant index check Co-authored-by: swc.pub <swc.pub@pm.me> * Add interactive item sharing with dynamic UI updates Co-authored-by: swc.pub <swc.pub@pm.me> * Remove unnecessary comments in payments UI code Co-authored-by: swc.pub <swc.pub@pm.me> * Simplify item sharing logic by removing redundant sharing flag Co-authored-by: swc.pub <swc.pub@pm.me> * Add person subtotal and total to share item response Co-authored-by: swc.pub <swc.pub@pm.me> * Refactor payments view to use new calculator method for person shares Co-authored-by: swc.pub <swc.pub@pm.me> * Add item management methods to Person class and simplify share_item logic Co-authored-by: swc.pub <swc.pub@pm.me> * Enhance docstrings for Person methods with detailed parameter descriptions Co-authored-by: swc.pub <swc.pub@pm.me> * Refactor item management methods to prevent duplicates and handle errors Co-authored-by: swc.pub <swc.pub@pm.me> * Add test for Person.update_item() method with toggle functionality Co-authored-by: swc.pub <swc.pub@pm.me> * Simplify test_person_update_item by removing redundant comments Co-authored-by: swc.pub <swc.pub@pm.me> * Revert distribute files to master branch state * Checkpoint before follow-up message Co-authored-by: swc.pub <swc.pub@pm.me> * Fix TypeScript compilation errors by renaming duplicate functions in payments.ts * Fix missing import: add save_persons_file to payments.py imports * Fix missing import: add Iterable to calculator.py imports * style: apply automatic formatting and linting fixes --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: swc.pub <swc.pub@pm.me> Co-authored-by: GitHub Action <action@github.com>
1 parent ee70dcd commit 52ca750

File tree

7 files changed

+225
-22
lines changed

7 files changed

+225
-22
lines changed

.github/workflows/lint-format-write.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ on:
1111

1212
jobs:
1313
lint-format-write:
14-
# Only run on manual trigger, not on main/master branches
1514
if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master'
1615
runs-on: ubuntu-latest
16+
permissions:
17+
contents: write
18+
pull-requests: write
1719

1820
steps:
1921
- name: Checkout code
2022
uses: actions/checkout@v4
2123
with:
22-
# Need to fetch all history for git operations
2324
fetch-depth: 0
24-
# Need write permissions to push commits
2525
token: ${{ secrets.GITHUB_TOKEN }}
2626

2727
- name: Set up Python
@@ -66,4 +66,10 @@ jobs:
6666
run: |
6767
git add -A
6868
git commit -m "${{ github.event.inputs.commit_message }}"
69-
git push origin ${{ github.ref }}
69+
git push origin ${{ github.ref }} || {
70+
echo "Failed to push changes. This might be due to:"
71+
echo "1. Insufficient permissions on the token"
72+
echo "2. Branch protection rules"
73+
echo "3. Repository settings"
74+
exit 1
75+
}

src/bill/calculator.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import csv
22
from io import StringIO
3+
from typing import Iterable
34
from bill.person import Person
45
from bill.receipts import Items, Item
56

@@ -124,6 +125,24 @@ def get_person_total(self, person: Person) -> float:
124125
)
125126
return person_subtotal + sum(person_extras)
126127

128+
def get_person_shares(self, person: Person) -> "Iterable[tuple[Item, float]]":
129+
"""
130+
Lazily compute a person's shares across all items, paired with items.
131+
132+
Parameters
133+
----------
134+
person: Person
135+
The person to calculate shares for
136+
137+
Returns
138+
-------
139+
Iterable[tuple[Item, float]]
140+
A lazy iterable yielding (Item, share) tuples for each item in self.items.items.
141+
"""
142+
return map(
143+
lambda item: (item, self.get_person_share(item, person)), self.items.items
144+
)
145+
127146
def get_shares_csv(self):
128147
"""
129148
Get a CSV string of shares for each person.

src/bill/person.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,59 @@
55
class Person(BaseModel):
66
name: str
77
items: List[int]
8+
9+
def insert_item(self, item_index: int) -> None:
10+
"""
11+
Add an item to the person's items list if not already present.
12+
13+
Parameters
14+
----------
15+
item_index : int
16+
The index of the item to add to the person's items list.
17+
18+
Returns
19+
-------
20+
None
21+
The items list is modified in place.
22+
"""
23+
items_set = set(self.items)
24+
items_set.add(item_index)
25+
self.items = sorted(list(items_set))
26+
27+
def remove_item(self, item_index: int) -> None:
28+
"""
29+
Remove an item from the person's items list if present.
30+
31+
Parameters
32+
----------
33+
item_index : int
34+
The index of the item to remove from the person's items list.
35+
36+
Returns
37+
-------
38+
None
39+
The items list is modified in place.
40+
"""
41+
try:
42+
self.items.remove(item_index)
43+
except ValueError:
44+
pass
45+
46+
def update_item(self, item_index: int) -> None:
47+
"""
48+
Toggle an item in the person's items list - add if not present, remove if present.
49+
50+
Parameters
51+
----------
52+
item_index : int
53+
The index of the item to toggle in the person's items list.
54+
55+
Returns
56+
-------
57+
None
58+
The items list is modified in place.
59+
"""
60+
if item_index not in self.items:
61+
self.insert_item(item_index)
62+
else:
63+
self.remove_item(item_index)

src/ui/payments.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
Blueprint,
55
request,
66
Response,
7+
jsonify,
78
)
89
from bill.calculator import Calculator
910
from logging import getLogger
1011
from items import get_current_items
1112
from extras import get_current_extras
12-
from persons import get_current_persons
13+
from persons import get_current_persons, save_persons_file
1314
from datetime import datetime
1415

1516
log = getLogger(__file__)
@@ -36,10 +37,9 @@ def payments_page_view():
3637
{
3738
"name": item.name,
3839
"price": item.price,
39-
"share": calculator.get_person_share(item, person),
40+
"share": share,
4041
}
41-
for item_index, item in enumerate(items.items)
42-
if item_index in person.items
42+
for item, share in calculator.get_person_shares(person)
4343
]
4444

4545
person_extras = [
@@ -88,3 +88,38 @@ def download_csv():
8888
mimetype="text/csv",
8989
headers={"Content-Disposition": f"attachment; filename={filename}"},
9090
)
91+
92+
93+
@payments_page.route("/share_item", methods=["POST"])
94+
def share_item():
95+
data = request.get_json()
96+
item_index = data.get("item_index")
97+
person_index = data.get("person_index")
98+
99+
persons = get_current_persons(session)
100+
person = persons[person_index]
101+
person.update_item(item_index)
102+
save_persons_file(persons, session)
103+
104+
items = get_current_items(session)
105+
extras = get_current_extras(session)
106+
calculator = Calculator(persons=persons, items=items, extras=extras)
107+
item = items.items[item_index]
108+
share = calculator.get_person_share(item, person)
109+
110+
person_subtotal = calculator.get_person_subtotal(person)
111+
person_total = calculator.get_person_total(person)
112+
113+
return (
114+
jsonify(
115+
{
116+
"success": True,
117+
"share": share,
118+
"item_name": item.name,
119+
"item_price": item.price,
120+
"person_subtotal": person_subtotal,
121+
"person_total": person_total,
122+
}
123+
),
124+
200,
125+
)

src/ui/payments.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", () => {
2323
downloadButton.addEventListener("click", handleDownload);
2424

2525
initializePersonData();
26+
setupItemClickedHandlers();
2627

2728
document.addEventListener("keydown", (e) => {
2829
if (e.key === "Escape") {
@@ -70,3 +71,64 @@ function handleExtras(): void {
7071
function handleDownload(): void {
7172
window.location.href = "/payments/download";
7273
}
74+
75+
function setupItemClickedHandlers(): void {
76+
const itemElements = document.querySelectorAll(".item-box");
77+
itemElements.forEach((itemElement) => {
78+
itemElement.addEventListener("click", handleItemClick);
79+
});
80+
}
81+
82+
async function handleItemClick(e: Event): Promise<void> {
83+
const itemElement = e.currentTarget as HTMLElement;
84+
const itemIndex = parseInt(itemElement.getAttribute("data-item-index") || "0");
85+
86+
try {
87+
const response = await fetch("/share_item", {
88+
method: "POST",
89+
headers: {
90+
"Content-Type": "application/json",
91+
},
92+
body: JSON.stringify({
93+
item_index: itemIndex,
94+
person_index: currentPersonIndex,
95+
}),
96+
});
97+
98+
if (response.ok) {
99+
const result = await response.json();
100+
updateItemInfoDisplay(itemElement, result);
101+
}
102+
} catch (error) {
103+
console.error("Error sharing item:", error);
104+
}
105+
}
106+
107+
function updateItemInfoDisplay(itemElement: HTMLElement, data: any): void {
108+
const shareElement = itemElement.querySelector(".text-blue-400.font-semibold") as HTMLElement;
109+
const priceElement = itemElement.querySelector(".text-xs.text-slate-400") as HTMLElement;
110+
111+
if (data.share !== undefined) {
112+
shareElement.textContent = `$${data.share.toFixed(2)}`;
113+
}
114+
115+
if (data.share > 0) {
116+
itemElement.classList.add("ring-4", "ring-green-400/40", "border-green-400");
117+
} else {
118+
itemElement.classList.remove("ring-4", "ring-green-400/40", "border-green-400");
119+
}
120+
121+
if (data.person_subtotal !== undefined) {
122+
const subtotalElement = document.querySelector(".text-green-400") as HTMLElement;
123+
if (subtotalElement) {
124+
subtotalElement.textContent = `$${data.person_subtotal.toFixed(2)}`;
125+
}
126+
}
127+
128+
if (data.person_total !== undefined) {
129+
const totalElement = document.getElementById("person-total") as HTMLElement;
130+
if (totalElement) {
131+
totalElement.textContent = `$${data.person_total.toFixed(2)}`;
132+
}
133+
}
134+
}

src/ui/templates/payments.html

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,15 @@ <h1 class="text-2xl font-bold">Bill</h1>
6767
<h3 class="text-lg font-semibold text-slate-200 mb-4">Items</h3>
6868

6969
<div class="space-y-3 mb-6">
70-
{% if person_items %}
71-
{% for item in person_items %}
72-
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg border border-slate-700">
73-
<span class="font-medium text-slate-100">{{ item.name }}</span>
74-
<div class="text-right">
75-
<div class="text-blue-400 font-semibold">${{ "%.2f"|format(item.share) }}</div>
76-
<div class="text-xs text-slate-400">/ ${{ "%.2f"|format(item.price) }}</div>
77-
</div>
78-
</div>
79-
{% endfor %}
80-
{% else %}
81-
<div class="text-center text-slate-400 py-4">
82-
<p>No items</p>
70+
{% for item in person_items %}
71+
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800/70 transition-colors item-box" data-item-index="{{ loop.index0 }}">
72+
<span class="font-medium text-slate-100">{{ item.name }}</span>
73+
<div class="text-right">
74+
<div class="text-blue-400 font-semibold">${{ "%.2f"|format(item.share) }}</div>
75+
<div class="text-xs text-slate-400">/ ${{ "%.2f"|format(item.price) }}</div>
8376
</div>
84-
{% endif %}
77+
</div>
78+
{% endfor %}
8579
</div>
8680

8781
<!-- Items Subtotal -->

tests/test_calculator.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,34 @@ def test_csv(calculator, sample_persons):
194194

195195
total = subtotal + service_charge + tax
196196
assert f"Total,{total:.2f},99.72,118.62,200.09,{total:.2f}" in csv_output
197+
198+
199+
def test_person_update_item():
200+
"""
201+
Test Person.update_item() method to ensure it correctly toggles items.
202+
Tests both insert_item() and remove_item() scenarios.
203+
"""
204+
person = Person(name="Test", items=[1, 3, 5])
205+
206+
person.update_item(7)
207+
assert person.items == [
208+
1,
209+
3,
210+
5,
211+
7,
212+
], "Item 7 should be added and list should be sorted"
213+
214+
person.update_item(3)
215+
assert person.items == [1, 3, 5, 7], "Item 3 should remain unchanged"
216+
217+
person.update_item(5)
218+
assert person.items == [1, 3, 7], "Item 5 should be removed"
219+
220+
person.update_item(9)
221+
assert person.items == [1, 3, 7], "Item 9 should not affect the list"
222+
223+
person.update_item(5)
224+
assert person.items == [1, 3, 5, 7], "Item 5 should be added back"
225+
226+
person.update_item(5)
227+
assert person.items == [1, 3, 7], "Item 5 should be removed again"

0 commit comments

Comments
 (0)