';
- currentTour.ideas.forEach(idea => {
- // Format time only (HH:mm) in Norwegian:
- let timeCell = "";
+ currentTour.ideas.forEach(idea => {
+ // Format time only (HH:mm) in Norwegian:
+ let timeCell = "";
if (idea.start_time && idea.end_time) {
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
hour: '2-digit', minute: '2-digit', hour12: false
@@ -913,18 +927,23 @@
timeCell = `
β
`;
}
- html += `
`
+ let rowStyle = idea.decided ? ' style="background: rgba(0,255,0,0.1)"' : '';
+ html += `
`
+ `
${idea.name} ${linkify(idea.description)}
`
+ timeCell;
allVoters.forEach(voter => {
html += `
${idea.voters.includes(voter) ? 'β' : ''}
`;
});
html += `
${idea.voters.length}
`;
- });
+ });
- html += '
';
- container.innerHTML = html;
- }
+ html += '';
+ container.innerHTML = html;
+
+ document.querySelectorAll('#addIdeaForm input, #addIdeaForm textarea, #addIdeaForm button').forEach(el => {
+ el.disabled = !!decidedIdea;
+ });
+ }
// Switch view
function showView(view) {
@@ -942,7 +961,11 @@
// Utility functions
function sortIdeasByVotes() {
if (!currentTour) return;
- currentTour.ideas.sort((a, b) => b.voters.length - a.voters.length);
+ currentTour.ideas.sort((a, b) => {
+ if (a.decided) return -1;
+ if (b.decided) return 1;
+ return b.voters.length - a.voters.length;
+ });
}
function generateUUID() {
@@ -967,13 +990,30 @@
const voteSubmitBtn = document.getElementById('voteSubmitBtn');
let _pendingVoteIdeaId = null;
- function voteForIdea(ideaId) {
- _pendingVoteIdeaId = ideaId;
- voterNameInput.value = '';
- voteSubmitBtn.disabled = true;
- voteModal.classList.remove('hidden');
- setTimeout(() => voterNameInput.focus(), 100);
- }
+ function voteForIdea(ideaId) {
+ _pendingVoteIdeaId = ideaId;
+ voterNameInput.value = '';
+ voteSubmitBtn.disabled = true;
+ voteModal.classList.remove('hidden');
+ setTimeout(() => voterNameInput.focus(), 100);
+ }
+
+ async function decideIdea(ideaId) {
+ if (!confirm('Mark this idea as decided?')) return;
+ showLoader();
+ try {
+ const resp = await fetch(`${API_URL}/tours/${currentTour.id}/ideas/${ideaId}/decide`, { method: 'POST' });
+ if (!resp.ok) throw new Error('Failed');
+ const decidedIdea = await resp.json();
+ currentTour.ideas = currentTour.ideas.map(i => i.id === decidedIdea.id ? decidedIdea : i);
+ renderIdeas();
+ } catch (err) {
+ console.error('Decide error', err);
+ alert('Failed to decide idea');
+ } finally {
+ hideLoader();
+ }
+ }
voterNameInput.addEventListener('input', () => {
voteSubmitBtn.disabled = voterNameInput.value.trim().length === 0;
diff --git a/server.py b/server.py
index 6ef2181..b079267 100644
--- a/server.py
+++ b/server.py
@@ -20,6 +20,7 @@ class Idea(BaseModel):
end_time: Optional[datetime] = None
description: str
voters: list[str] = Field(default_factory=list)
+ decided: bool = False
class Tour(BaseModel):
id: str
@@ -127,6 +128,9 @@ def add_idea(tour_id: str, idea: IdeaCreate):
with open(filepath) as f:
data = json.load(f)
+ if any(i.get("decided") for i in data["ideas"]):
+ raise HTTPException(status_code=403, detail="Idea already decided")
+
idea_id = str(uuid.uuid4())
new_idea = Idea(
id=idea_id,
@@ -135,6 +139,7 @@ def add_idea(tour_id: str, idea: IdeaCreate):
voters=[],
start_time=idea.start_time,
end_time=idea.end_time,
+ decided=False,
)
data["ideas"].append(new_idea.model_dump())
@@ -159,6 +164,8 @@ def vote_idea(tour_id: str, idea_id: str, vote: Vote):
with open(filepath) as f:
data = json.load(f)
+ if any(i.get("decided") for i in data["ideas"]):
+ raise HTTPException(status_code=403, detail="Idea already decided")
for idea in data["ideas"]:
if idea["id"] == idea_id:
if vote.voterName not in idea["voters"]:
@@ -170,6 +177,32 @@ def vote_idea(tour_id: str, idea_id: str, vote: Vote):
raise HTTPException(status_code=404, detail="Idea not found")
+# βββ ENDPOINT: DECIDE IDEA ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+@app.post("/tour/v1/tours/{tour_id}/ideas/{idea_id}/decide", response_model=Idea)
+def decide_idea(tour_id: str, idea_id: str):
+ """Mark an idea as decided. Only one idea can be decided per tour."""
+ filepath = get_tour_filepath(tour_id)
+ if not os.path.exists(filepath):
+ raise HTTPException(status_code=404, detail="Tour not found")
+
+ lock = FileLock(filepath + ".lock")
+ with lock:
+ with open(filepath) as f:
+ data = json.load(f)
+
+ if any(i.get("decided") for i in data["ideas"]):
+ raise HTTPException(status_code=403, detail="Idea already decided")
+
+ for idea in data["ideas"]:
+ if idea["id"] == idea_id:
+ idea["decided"] = True
+ with open(filepath, "w") as f:
+ json.dump(data, f, default=str)
+ return Idea(**idea)
+
+ raise HTTPException(status_code=404, detail="Idea not found")
+
# βββ MAIN: RUN UVIDORN WITH COMMAND-LINE DATA DIR ββββββββββββββββββββββββββββββ
if __name__ == "__main__":