From dba58fcb60393253427363a7cff11d9cd3dece30 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:09:51 -0300 Subject: [PATCH 1/7] fix:update hardcoded strings into enums Also removed unnecessary data from schemas --- backend/app/session/engine.py | 12 ++++++------ backend/app/session/enums.py | 14 ++++++++++++++ backend/app/session/models.py | 4 ++-- backend/app/session/schemas.py | 23 +++++++---------------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/backend/app/session/engine.py b/backend/app/session/engine.py index 3981f36..a15234d 100644 --- a/backend/app/session/engine.py +++ b/backend/app/session/engine.py @@ -451,7 +451,7 @@ def handle_open_informal_voting( ) state.voting = VotingContext( - target_type="INFORMAL", + target_type=enums.VotingType.INFORMAL, title=event.payload.title, return_state=state.current_state, voting_registry={}, @@ -476,7 +476,7 @@ def handle_close_informal_voting( if ( state.current_state != States.VOTING_EXECUTION - or state.voting.target_type != "INFORMAL" + or state.voting.target_type != enums.VotingType.INFORMAL ): raise InvalidProceduralMove("Can't close voting") @@ -500,7 +500,7 @@ def handle_close_procedural_voting( if ( state.current_state != States.VOTING_EXECUTION - or state.voting.target_type != "PROCEDURAL" + or state.voting.target_type != enums.VotingType.PROCEDURAL ): raise InvalidProceduralMove("Can't close voting") @@ -634,13 +634,13 @@ def handle_resolve_motion( if motion is None: raise InvalidProceduralMove("Motion not found") - if payload.action == "ACCEPT": + if payload.action: state.voting = VotingContext( - target_type="PROCEDURAL", + target_type=enums.VotingType.PROCEDURAL, motion_in_vote=motion, return_state=state.current_state, voting_registry={}, - majority="QUALIFIED", # TODO: change depending on type of motion + majority=enums.MajorityTypes.QUALIFIED, # TODO: change depending on type of motion veto_power=True, ) diff --git a/backend/app/session/enums.py b/backend/app/session/enums.py index 0d94d81..7e0f8d4 100644 --- a/backend/app/session/enums.py +++ b/backend/app/session/enums.py @@ -96,8 +96,22 @@ class RollCallChoice(StrEnum): PRESENT_AND_VOTING = "Present and Voting" ABSENT = "Absent" +class MajorityTypes(StrEnum): + SIMPLE = "Maioria Simples" + QUALIFIED = "Maioria Qualificada" + ABSOLUTE = "Consenso" class SessionRole(StrEnum): CHAIR = "CHAIR" DELEGATE = "DELEGATE" # further roles are put here + +class VotingChoice(StrEnum): + FAVOUR = "Favour" + AGAINST = "Against" + ABSTAIN = "Abstain" + +class VotingType(StrEnum): + INFORMAL = "Informal" + PROCEDURAL = "Procedural" + SUBSTANTIVE = "Substantive" diff --git a/backend/app/session/models.py b/backend/app/session/models.py index 4130e64..1440e1a 100644 --- a/backend/app/session/models.py +++ b/backend/app/session/models.py @@ -61,10 +61,10 @@ class VotingContext(BaseModel): motion_in_vote: MotionContext | None = None title: str | None = None return_state: enums.States - voting_registry: dict[int, Literal["FAVOUR", "AGAINST", "ABSTAIN"]] = {} + voting_registry: dict[int, enums.VotingChoice] = {} # additional fields - majority: Literal["SIMPLE", "QUALIFIED", "ABSOLUTE"] + majority: enums.MajorityTypes veto_power: bool diff --git a/backend/app/session/schemas.py b/backend/app/session/schemas.py index 9e7cfe5..4c97e89 100644 --- a/backend/app/session/schemas.py +++ b/backend/app/session/schemas.py @@ -35,16 +35,7 @@ class DelegateQuestionPayload(BaseModel): class DelegateVotingPayload(BaseModel): - # other types of voting must be put in here - type: Literal[ - "FORMAL", - "INFORMAL", - ] # perhaps not needed - motion_id: int | None = ( - None # perhaps not needed, unless we pass the voting context to UI to validate? - ) - title: str | None = None # perhaps not needed - vote: Literal["FAVOUR", "AGAINST", "ABSTAIN"] + vote: enums.VotingChoice # TODO: should be better implemented @@ -108,13 +99,13 @@ class ChairToggleTimerPayload(BaseModel): class ChairOpenInformalVotingPayload(BaseModel): # For informal Votings title: str | None = None - majority: Literal["SIMPLE", "QUALIFIED", "ABSOLUTE"] + majority: enums.MajorityTypes veto_power: bool class ChairResolveMotionPayload(BaseModel): motion_id: int # or motion_id if possible - action: Literal["ACCEPT", "DENY"] + action: bool class ChairForceSpeakerPayload(BaseModel): @@ -148,10 +139,10 @@ class MarkAgendaItemPayload(BaseModel): class DeleteAgendaItemPayload(BaseModel): index: str # Agenda Item Id - +#Removed: Unnecessary # These two normally don't need to have an id -class ChairCloseInformalVotingPayload(BaseModel): - voting_id: int | None = None +# class ChairCloseInformalVotingPayload(BaseModel): +# voting_id: int | None = None class EmptyPayload(BaseModel): ... @@ -214,7 +205,7 @@ class SetPhaseEvent(BaseModel): class CloseInformalVotingEvent(BaseModel): type: Literal[enums.ChairEvents.CLOSE_INFORMAL_VOTING] - payload: ChairCloseInformalVotingPayload + payload: EmptyPayload class CloseProceduralVotingEvent(BaseModel): From 21e8721a64b9614ede1778de515c6fdd8ff8c8e0 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:15:23 -0300 Subject: [PATCH 2/7] fix: Added type safety/consistency to ./access views --- backend/app/access/enums.py | 5 +++++ backend/app/access/models.py | 4 ++-- backend/app/access/schemas.py | 6 ++++++ backend/app/access/views.py | 8 +++++--- 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 backend/app/access/enums.py create mode 100644 backend/app/access/schemas.py diff --git a/backend/app/access/enums.py b/backend/app/access/enums.py new file mode 100644 index 0000000..a88e8c9 --- /dev/null +++ b/backend/app/access/enums.py @@ -0,0 +1,5 @@ +from enum import StrEnum + +class SessionRoles(StrEnum): + CHAIR = "chair" + DELEGATION = "delegation" \ No newline at end of file diff --git a/backend/app/access/models.py b/backend/app/access/models.py index 2a677c5..a088422 100644 --- a/backend/app/access/models.py +++ b/backend/app/access/models.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Literal from uuid import UUID - +from . import enums @dataclass(frozen=True) class CommitteeAssignment: @@ -9,5 +9,5 @@ class CommitteeAssignment: user_id: UUID committee_id: int # TODO: remove this to map out to committees/conferences - role: Literal["chair", "delegate"] + role: enums.SessionRoles representation_id: int | None diff --git a/backend/app/access/schemas.py b/backend/app/access/schemas.py new file mode 100644 index 0000000..e586898 --- /dev/null +++ b/backend/app/access/schemas.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +from . import enums + +class SessionRepresentation(BaseModel): + role: enums.SessionRoles + representation_id: int | None \ No newline at end of file diff --git a/backend/app/access/views.py b/backend/app/access/views.py index 7da3c4b..7983f3f 100644 --- a/backend/app/access/views.py +++ b/backend/app/access/views.py @@ -8,16 +8,18 @@ from app.core.database import get_db_session from .service import AccessDenied, resolve_session_assignment +from .schemas import SessionRepresentation router = APIRouter() -@router.get("/sessions/{session_id}/me") + +@router.get("/sessions/{session_id}/me", response_model=SessionRepresentation) async def get_my_session_access( session_id: int, db_session: Annotated[AsyncSession, Depends(get_db_session)], current_user: Annotated[AuthUser, Depends(get_current_user)], -): +)->SessionRepresentation: """Return the authenticated user's actor context for a session.""" try: assignment = await resolve_session_assignment( @@ -32,4 +34,4 @@ async def get_my_session_access( return { "role": assignment.role, "representation_id": assignment.representation_id, - } + } From 5bbe5d87c9fd8e753f7988275632ad3f4efa5d85 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:15:43 -0300 Subject: [PATCH 3/7] fix:minor docker fix --- docker-compose.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51a5fd6..4fbd687 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,10 @@ services: watch: - action: restart path: ./backend/app/session/schemas.py + - action: restart + path: ./backend/app/session/models.py + - action: restart + path: ./backend/app/session/enums.py backend: build: @@ -50,12 +54,6 @@ services: retries: 3 start_period: 50s start_interval: 1s - develop: - watch: - - action: restart - path: ./backend/app/session/schemas.py - - action: restart - path: ./backend/app/session/models.py volumes: packages: From edb307dfc2ddeba7e6d1499f016dd40a10d7f72d Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:16:03 -0300 Subject: [PATCH 4/7] fix:minor enum fix --- backend/app/session/models.py | 2 +- backend/app/tests/session/test_engine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/session/models.py b/backend/app/session/models.py index 1440e1a..22f05b6 100644 --- a/backend/app/session/models.py +++ b/backend/app/session/models.py @@ -57,7 +57,7 @@ class QuestionContext(BaseModel): class VotingContext(BaseModel): - target_type: Literal["PROCEDURAL", "SUBSTANTIVE", "INFORMAL"] + target_type: enums.VotingType motion_in_vote: MotionContext | None = None title: str | None = None return_state: enums.States diff --git a/backend/app/tests/session/test_engine.py b/backend/app/tests/session/test_engine.py index ed4003a..f2b2388 100644 --- a/backend/app/tests/session/test_engine.py +++ b/backend/app/tests/session/test_engine.py @@ -119,7 +119,7 @@ def open_informal_voting_event() -> sch.OpenInformalVotingEvent: def close_informal_voting_event() -> sch.CloseInformalVotingEvent: return sch.CloseInformalVotingEvent( type=enums.ChairEvents.CLOSE_INFORMAL_VOTING, - payload=sch.ChairCloseInformalVotingPayload(), + payload=sch.EmptyPayload(), ) From 3f47308270c99cfaeef877e959e50629dbf30622 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:16:50 -0300 Subject: [PATCH 5/7] feat:working button --- .../session/bottom-bar-buttons/VoteButton.tsx | 23 +++++++++++----- .../src/components/session/voting-popup.tsx | 27 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx b/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx index 91358dc..2721088 100644 --- a/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx +++ b/frontend/src/components/session/bottom-bar-buttons/VoteButton.tsx @@ -10,9 +10,17 @@ import { import { Vote } from "lucide-react" import { States } from "@/schemas/types.gen" import { useCommitteeStore } from "@/store/useCommitteeStore" +import { sendMessage } from "@/context/SessionContext" +import { MajorityTypes, ChairEvents, type OpenInformalVotingEvent, type CloseInformalVotingEvent } from "@/schemas/types.gen" +import { useRef } from "react" export default function VoteButton() { const currentState = useCommitteeStore((state) => state.current_state) + const title = useRef(null) + const majority = useRef(null) + const veto = useRef(null) + + return ( @@ -32,19 +40,19 @@ export default function VoteButton() {
- +

O que vai ser votado

- + + +
- +

@@ -52,7 +60,8 @@ export default function VoteButton() {

- + +
diff --git a/frontend/src/components/session/voting-popup.tsx b/frontend/src/components/session/voting-popup.tsx index 8399533..a8038b2 100644 --- a/frontend/src/components/session/voting-popup.tsx +++ b/frontend/src/components/session/voting-popup.tsx @@ -15,21 +15,31 @@ import { FieldLabel, } from "@/components/ui/field" import { useState } from "react" +import { useCommitteeStore } from "@/store/useCommitteeStore" +import { sendMessage } from "@/context/SessionContext" +import { DelegateEvents, VotingChoice, type CastVoteEvent } from "@/schemas/types.gen" type VoteType = "rollCall1" | "rollCall2" | "procedural" | "informal" -const voteType: VoteType = "procedural" -const voteTitle = "Batata no Coffee Breaks" const canAbstain = false +const voteType : VoteType = "procedural" + export default function VotingPopup() { - const [voteWithRights, setVoteWithRights] = useState(false) + + //TODO: Implement rollcall voting const isRollCall1 = voteType === "rollCall1" const isRollCall2 = voteType === "rollCall2" const isRollCall = isRollCall1 || isRollCall2 + const [voteWithRights, setVoteWithRights] = useState(false) + //TEMPORARY^^^^ + + const voting = useCommitteeStore((state) => state.voting ?? null) + const voteTitle = voting?.title + const [voted, setVoted] = useState(false) return ( - + Votação @@ -66,8 +76,12 @@ export default function VotingPopup() {
- + )} - +
From 3249da1b68e4307b0c618edadf6819f1b53fed49 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:17:18 -0300 Subject: [PATCH 6/7] refactor: Moved backend connection logic to Session Context --- frontend/src/components/session/Agenda.tsx | 2 +- .../bottom-bar-buttons/MotionsButton.tsx | 8 +- .../bottom-bar-buttons/SessionButton.tsx | 2 +- .../src/components/session/bottom-bar.tsx | 6 +- .../src/components/session/delegation-map.tsx | 2 +- .../src/components/session/manual-quorum.tsx | 2 +- .../components/session/moderated-debate.tsx | 6 +- .../src/components/session/motions-list.tsx | 6 +- .../src/components/session/speaker-list.tsx | 8 +- frontend/src/components/session/timer.tsx | 8 +- frontend/src/context/SessionContext.tsx | 97 +++++++++++++++ frontend/src/pages/Session.tsx | 77 +----------- frontend/src/schemas/types.gen.ts | 115 +++++++++++------- 13 files changed, 210 insertions(+), 129 deletions(-) create mode 100644 frontend/src/context/SessionContext.tsx diff --git a/frontend/src/components/session/Agenda.tsx b/frontend/src/components/session/Agenda.tsx index 4273251..dec0ae3 100644 --- a/frontend/src/components/session/Agenda.tsx +++ b/frontend/src/components/session/Agenda.tsx @@ -1,4 +1,4 @@ -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import type {SetAgendaItemEvent, MarkAgendaItemEvent, DeleteAgendaItemEvent} from "@/schemas/types.gen" import {ChairEvents } from "@/schemas/types.gen" import { Button } from "@/components/ui/button" diff --git a/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx b/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx index a072e75..b1661ce 100644 --- a/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx +++ b/frontend/src/components/session/bottom-bar-buttons/MotionsButton.tsx @@ -40,8 +40,8 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { Label } from "@/components/ui/label" import { useCommitteeStore } from "@/store/useCommitteeStore" import { States } from "@/schemas/types.gen" - -const isChair = true // Replace with actual logic to determine if the user is the chair +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" const motions = [ "Moção para Adiamento de Sessão", @@ -89,6 +89,10 @@ function QuestionsMotionsList(type: MotionKind) { } export default function TestButton() { + + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR + const currentState = useCommitteeStore((state) => state.current_state) const [motionKind, setMotionKind] = useState("moção") const [selectedMotion, setSelectedMotion] = useState("") diff --git a/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx b/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx index 9cb4098..448661c 100644 --- a/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx +++ b/frontend/src/components/session/bottom-bar-buttons/SessionButton.tsx @@ -20,7 +20,7 @@ import { } from "@/components/ui/select" import ManualQuorum from "@/components/session/manual-quorum" import { RollCallChoice, ChairEvents, States, type CloseRollCallEvent, type CloseSessionEvent, type OpenSessionEvent } from "@/schemas/types.gen" -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import { useCommitteeStore } from "@/store/useCommitteeStore" diff --git a/frontend/src/components/session/bottom-bar.tsx b/frontend/src/components/session/bottom-bar.tsx index 634bdf7..ce5503c 100644 --- a/frontend/src/components/session/bottom-bar.tsx +++ b/frontend/src/components/session/bottom-bar.tsx @@ -6,12 +6,14 @@ import SessionButton from "./bottom-bar-buttons/SessionButton" import ExitButton from "./bottom-bar-buttons/ExitButton" import BRBButton from "./bottom-bar-buttons/BRB" import IncidentHelp from "./bottom-bar-buttons/IncidentHelp" - -const isChair = true // Replace with actual logic to determine if the user is the chair +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" export default function BottomBar() { + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR return ( <> diff --git a/frontend/src/components/session/delegation-map.tsx b/frontend/src/components/session/delegation-map.tsx index e4edcdc..7d1e901 100644 --- a/frontend/src/components/session/delegation-map.tsx +++ b/frontend/src/components/session/delegation-map.tsx @@ -15,7 +15,7 @@ import { } from "@/components/ui/context-menu" import { useCommitteeStore } from "@/store/useCommitteeStore" import { CircleFlag } from 'react-circle-flags' -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import { type ChairInsertQueueEvent, type MarkRollCallEvent, type SpeakerEvent , ChairEvents, RollCallChoice } from "@/schemas/types.gen" import { Tooltip, diff --git a/frontend/src/components/session/manual-quorum.tsx b/frontend/src/components/session/manual-quorum.tsx index 79a7bea..0fa3589 100644 --- a/frontend/src/components/session/manual-quorum.tsx +++ b/frontend/src/components/session/manual-quorum.tsx @@ -12,7 +12,7 @@ import { import { useCommitteeStore } from "@/store/useCommitteeStore" import { ScrollArea } from "@/components/ui/scroll-area" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import { Item, ItemContent, diff --git a/frontend/src/components/session/moderated-debate.tsx b/frontend/src/components/session/moderated-debate.tsx index cba4fbe..b5b5b73 100644 --- a/frontend/src/components/session/moderated-debate.tsx +++ b/frontend/src/components/session/moderated-debate.tsx @@ -14,11 +14,15 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" -const isChair = true // Replace with actual logic to determine if the user is the chair //TODO determine if queue is open, if not obscure the button and show a message that the queue is closed export default function ModeratedDebate() { + + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR const gslQueue = useCommitteeStore((state) => state.gsl_queue ?? []) const currentSpeaker = useCommitteeStore((state) => state.current_speaker) const delegationsById = useCommitteeStore((state) => state.delegations) diff --git a/frontend/src/components/session/motions-list.tsx b/frontend/src/components/session/motions-list.tsx index 093d5dc..b81608c 100644 --- a/frontend/src/components/session/motions-list.tsx +++ b/frontend/src/components/session/motions-list.tsx @@ -12,6 +12,8 @@ import { } from "@/components/ui/item" import { Badge } from "@/components/ui/badge" import Flags from "@/components/ui/flags" +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" export type Motion = { id: string @@ -26,9 +28,11 @@ type MotionsListProps = { motions: Motion[] } -const isChair = true// Replace with actual logic to determine if the user is the chair export default function MotionsList({ motions }: MotionsListProps) { + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR + const toMinutes = (time: string): number => { const [hours, minutes] = time.split(":").map(Number) if (Number.isNaN(hours) || Number.isNaN(minutes)) { diff --git a/frontend/src/components/session/speaker-list.tsx b/frontend/src/components/session/speaker-list.tsx index 8ed929b..33f4eac 100644 --- a/frontend/src/components/session/speaker-list.tsx +++ b/frontend/src/components/session/speaker-list.tsx @@ -15,14 +15,18 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import { type SpeakerEvent, ChairEvents } from "@/schemas/types.gen" +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" -const isChair = true // Replace with actual logic to determine if the user is the chair const isAlredyInQueue = true // Replace with actual logic to determine if the user is already in the queue //TODO determine if queue is open, if not obscure the button and show a message that the queue is closed export default function SpeakerList() { + + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR const gslQueue = useCommitteeStore((state) => state.gsl_queue ?? []) const currentSpeaker = useCommitteeStore((state) => state.current_speaker) const delegationsById = useCommitteeStore((state) => state.delegations) diff --git a/frontend/src/components/session/timer.tsx b/frontend/src/components/session/timer.tsx index 33fb4ae..a09c441 100644 --- a/frontend/src/components/session/timer.tsx +++ b/frontend/src/components/session/timer.tsx @@ -3,14 +3,16 @@ import { Separator } from "@/components/ui/separator" import { Flag, Pause, Plus, Play } from "lucide-react" import Flags from "@/components/ui/flags" import { useCommitteeStore } from "@/store/useCommitteeStore" -import { sendMessage } from "@/pages/Session" +import { sendMessage } from "@/context/SessionContext" import { type IncreaseTimerEvent, type ToggleTimerEvent, ChairEvents } from "@/schemas/types.gen" import { useEffect, useState } from "react" - -const isChair = true // Replace with actual logic to determine if the user is the chair +import { useSession } from "@/context/SessionContext" +import { SessionRoles } from "@/schemas/types.gen" export default function Timer() { + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR const delegations = useCommitteeStore((state) => state.delegations); const currentSpeaker = useCommitteeStore((state) => state.current_speaker); const speaker = currentSpeaker !== null && currentSpeaker !== undefined ? delegations[currentSpeaker] : { diff --git a/frontend/src/context/SessionContext.tsx b/frontend/src/context/SessionContext.tsx new file mode 100644 index 0000000..b0a932e --- /dev/null +++ b/frontend/src/context/SessionContext.tsx @@ -0,0 +1,97 @@ +import { useContext, type ReactNode } from 'react'; +import { createContext, useEffect, useState } from 'react'; +import { useAuth } from './AuthContext'; +import { useParams } from 'react-router-dom'; +import { UpdateStore } from '@/store/useCommitteeStore'; +import {type SessionRepresentation,type BodyDummyCommitteesDummyGet as Types} from '@/schemas/types.gen'; + + +interface SessionContextType{ + role: string + representation_id: number | null +} + +const SessionContext = createContext({ + role: "", + representation_id: null, +}); + +let socket : WebSocket|null = null + +export function SessionProvider({ children }: { children: ReactNode }) +{ + const { token } = useAuth() + const { sessionId } = useParams<{ sessionId: string }>(); + const parsedSessionId = Number(sessionId); + + const [, setStatus] = useState("Connecting..."); + + useEffect(() => { + if (!token || !Number.isInteger(parsedSessionId) || parsedSessionId < 1) { + return; + } + + const ws = new WebSocket( + `${import.meta.env.VITE_WS_URL}/ws/${parsedSessionId}`, + ); + socket = ws + + ws.onopen = () => { + ws?.send(JSON.stringify({ access_token: token })); + setStatus("Connected"); + } + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log(data); + UpdateStore(data); + }; + + ws.onclose = () => setStatus("Disconnected"); + + return () => { + ws.close(); + if (socket === ws) socket = null; + } + }, [parsedSessionId, token]); + + const [role, setRole] = useState("") + const [representation_id, setRepresentation_id] = useState(null) + + useEffect(() => { + if(!socket || !socket.readyState) return + fetch(`${import.meta.env.VITE_API_URL}/access/sessions/${parsedSessionId}/me`, + { + method:"GET", + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + } + ).then((response)=> + { + if(!response.ok) throw new Error("Error when getting role") + return response.json() + }).then((data : SessionRepresentation) => {setRole(data.role); setRepresentation_id(data.representation_id);}) + }, [parsedSessionId, token, socket, socket?.readyState]) + + const value ={ + role: role, + representation_id: representation_id + } + + return {children}; +} + +export function sendMessage(data: Types["types"]) { + if (socket && socket.readyState === WebSocket.OPEN) + { + socket.send(JSON.stringify(data)); + } + else + { + console.error("WebSocket is not connected."); + } +} + +export const useSession = () => useContext(SessionContext) \ No newline at end of file diff --git a/frontend/src/pages/Session.tsx b/frontend/src/pages/Session.tsx index a60bb7b..456e219 100644 --- a/frontend/src/pages/Session.tsx +++ b/frontend/src/pages/Session.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState} from 'react'; import { Navigate, useParams } from 'react-router-dom'; import { useAuth } from '@/context/AuthContext'; -import { UpdateStore, useCommitteeStore } from '../store/useCommitteeStore.ts' +import { useCommitteeStore } from '../store/useCommitteeStore.ts' +import { SessionProvider } from '@/context/SessionContext.tsx'; import MotionsList from "@/components/session/motions-list" import SpeakerList from "@/components/session/speaker-list" //import ModeratedDebate from "@/components/session/moderated-debate" @@ -10,30 +10,9 @@ import BottomBar from "@/components/session/bottom-bar" import TopBar from '@/components/session/top-bar'; import DelegationMap from '@/components/session/delegation-map'; import VotingPopup from '@/components/session/voting-popup.tsx'; -import {type BodyDummyCommitteesDummyGet as Types} from '@/schemas/types.gen'; - -let socket : WebSocket | null = null; - -/* -Use this function to send events to the backend, -any data with one of the Event types in schemas/types.gen.ts should work -*/ -export function sendMessage(data: Types["types"]) { - if (socket && socket.readyState === WebSocket.OPEN) - { - socket.send(JSON.stringify(data)); - } - else - { - console.error("WebSocket is not connected."); - } -} - export default function SessionPage() { - const { loading, token } = useAuth() - const motions = [ { id: "motion-1", @@ -61,67 +40,21 @@ export default function SessionPage() { }, ] + const {loading, token } = useAuth() // id that matches the name given in the Route path, at App.tsx const { sessionId } = useParams<{ sessionId: string }>(); const parsedSessionId = Number(sessionId); - //const {start_time} = useCommitteeStore(); const all = useCommitteeStore(); - const [, setStatus] = useState("Connecting..."); - //const [, setUptime] = useState(0); - - useEffect(() => { - if (!token || !Number.isInteger(parsedSessionId) || parsedSessionId < 1) { - return; - } - - const ws = new WebSocket( - `${import.meta.env.VITE_WS_URL}/ws/${parsedSessionId}`, - ); - socket = ws; - - ws.onopen = () => { - ws?.send(JSON.stringify({ access_token: token })); - setStatus("Connected"); - } - - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - console.log(data); - UpdateStore(data); - }; - - ws.onclose = () => setStatus("Disconnected"); - - return () => { - ws.close(); - if (socket === ws) socket = null; - } - }, [parsedSessionId, token]); - if (loading) return

Loading session…

; if (!token) return ; if (!Number.isInteger(parsedSessionId) || parsedSessionId < 1) { return

Invalid session ID.

; } - // Useeffect for local uptime timer - /*useEffect(() => { - - if (!start_time) return; - - // calculate timer - const interval = setInterval(() => { - const start = new Date(start_time).getTime(); - const now = new Date().getTime(); - setUptime(Math.floor((now - start) / 1000)); - }, 1000); - - - return () => clearInterval(interval); - }, [start_time]);*/ console.log(all); return ( +
{/*

{status} @@ -150,6 +83,6 @@ export default function SessionPage() {

- +
); } diff --git a/frontend/src/schemas/types.gen.ts b/frontend/src/schemas/types.gen.ts index a945d29..690c896 100644 --- a/frontend/src/schemas/types.gen.ts +++ b/frontend/src/schemas/types.gen.ts @@ -65,16 +65,6 @@ export type CastVoteEvent = { payload: DelegateVotingPayload; }; -/** - * ChairCloseInformalVotingPayload - */ -export type ChairCloseInformalVotingPayload = { - /** - * Voting Id - */ - voting_id?: number | null; -}; - /** * ChairEvents */ @@ -157,10 +147,7 @@ export type ChairOpenInformalVotingPayload = { * Title */ title?: string | null; - /** - * Majority - */ - majority: 'SIMPLE' | 'QUALIFIED' | 'ABSOLUTE'; + majority: MajorityTypes; /** * Veto Power */ @@ -178,7 +165,7 @@ export type ChairResolveMotionPayload = { /** * Action */ - action: 'ACCEPT' | 'DENY'; + action: boolean; }; /** @@ -237,7 +224,7 @@ export type CloseInformalVotingEvent = { * Type */ type: 'CloseInformalVotingEvent'; - payload: ChairCloseInformalVotingPayload; + payload: EmptyPayload; }; /** @@ -381,22 +368,7 @@ export type DelegateQuestionPayload = { * DelegateVotingPayload */ export type DelegateVotingPayload = { - /** - * Type - */ - type: 'FORMAL' | 'INFORMAL'; - /** - * Motion Id - */ - motion_id?: number | null; - /** - * Title - */ - title?: string | null; - /** - * Vote - */ - vote: 'FAVOUR' | 'AGAINST' | 'ABSTAIN'; + vote: VotingChoice; }; /** @@ -502,6 +474,20 @@ export type LeaveQueueEvent = { }; }; +/** + * MajorityTypes + */ +export const MajorityTypes = { + MAIORIA_SIMPLES: 'Maioria Simples', + MAIORIA_QUALIFICADA: 'Maioria Qualificada', + CONSENSO: 'Consenso' +} as const; + +/** + * MajorityTypes + */ +export type MajorityTypes = typeof MajorityTypes[keyof typeof MajorityTypes]; + /** * MarkAgendaItemEvent */ @@ -834,6 +820,27 @@ export type SessionLiveState = { roll_call: RollCallContext; }; +/** + * SessionRepresentation + */ +export type SessionRepresentation = { + role: SessionRoles; + /** + * Representation Id + */ + representation_id: number | null; +}; + +/** + * SessionRoles + */ +export const SessionRoles = { CHAIR: 'chair', DELEGATION: 'delegation' } as const; + +/** + * SessionRoles + */ +export type SessionRoles = typeof SessionRoles[keyof typeof SessionRoles]; + /** * SetAgendaEvent */ @@ -976,14 +983,25 @@ export type ValidationError = { }; }; +/** + * VotingChoice + */ +export const VotingChoice = { + FAVOUR: 'Favour', + AGAINST: 'Against', + ABSTAIN: 'Abstain' +} as const; + +/** + * VotingChoice + */ +export type VotingChoice = typeof VotingChoice[keyof typeof VotingChoice]; + /** * VotingContext */ export type VotingContext = { - /** - * Target Type - */ - target_type: 'PROCEDURAL' | 'SUBSTANTIVE' | 'INFORMAL'; + target_type: VotingType; motion_in_vote?: MotionContext | null; /** * Title @@ -994,18 +1012,29 @@ export type VotingContext = { * Voting Registry */ voting_registry?: { - [key: string]: 'FAVOUR' | 'AGAINST' | 'ABSTAIN'; + [key: string]: VotingChoice; }; - /** - * Majority - */ - majority: 'SIMPLE' | 'QUALIFIED' | 'ABSOLUTE'; + majority: MajorityTypes; /** * Veto Power */ veto_power: boolean; }; +/** + * VotingType + */ +export const VotingType = { + INFORMAL: 'Informal', + PROCEDURAL: 'Procedural', + SUBSTANTIVE: 'Substantive' +} as const; + +/** + * VotingType + */ +export type VotingType = typeof VotingType[keyof typeof VotingType]; + export type DummyCommitteesDummyGetData = { body: BodyDummyCommitteesDummyGet; path?: never; @@ -1121,5 +1150,7 @@ export type GetMySessionAccessAccessSessionsSessionIdMeGetResponses = { /** * Successful Response */ - 200: unknown; + 200: SessionRepresentation; }; + +export type GetMySessionAccessAccessSessionsSessionIdMeGetResponse = GetMySessionAccessAccessSessionsSessionIdMeGetResponses[keyof GetMySessionAccessAccessSessionsSessionIdMeGetResponses]; From 94d0aad5b0d2ec25a4523cbc7bf950fad9027a67 Mon Sep 17 00:00:00 2001 From: Extroias <91105220+Extroias@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:24:03 -0300 Subject: [PATCH 7/7] fix: minor missed fixes --- backend/app/access/enums.py | 2 +- frontend/src/components/session/Agenda.tsx | 8 ++++---- frontend/src/context/SessionContext.tsx | 3 +-- frontend/src/schemas/types.gen.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/app/access/enums.py b/backend/app/access/enums.py index a88e8c9..883d870 100644 --- a/backend/app/access/enums.py +++ b/backend/app/access/enums.py @@ -2,4 +2,4 @@ class SessionRoles(StrEnum): CHAIR = "chair" - DELEGATION = "delegation" \ No newline at end of file + DELEGATION = "delegate" \ No newline at end of file diff --git a/frontend/src/components/session/Agenda.tsx b/frontend/src/components/session/Agenda.tsx index dec0ae3..a5ca987 100644 --- a/frontend/src/components/session/Agenda.tsx +++ b/frontend/src/components/session/Agenda.tsx @@ -1,6 +1,6 @@ -import { sendMessage } from "@/context/SessionContext" +import { sendMessage, useSession } from "@/context/SessionContext" import type {SetAgendaItemEvent, MarkAgendaItemEvent, DeleteAgendaItemEvent} from "@/schemas/types.gen" -import {ChairEvents } from "@/schemas/types.gen" +import {ChairEvents, SessionRoles } from "@/schemas/types.gen" import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" @@ -31,8 +31,8 @@ import { useRef } from "react" export default function Agenda() { - const isChair = true // Replace with actual logic to determine if the user is the chair - + const {role} = useSession() + const isChair = role===SessionRoles.CHAIR const agendaTopics = useCommitteeStore((state) => state.agenda_topics) const numinput = useRef(null) diff --git a/frontend/src/context/SessionContext.tsx b/frontend/src/context/SessionContext.tsx index b0a932e..8027598 100644 --- a/frontend/src/context/SessionContext.tsx +++ b/frontend/src/context/SessionContext.tsx @@ -59,7 +59,6 @@ export function SessionProvider({ children }: { children: ReactNode }) const [representation_id, setRepresentation_id] = useState(null) useEffect(() => { - if(!socket || !socket.readyState) return fetch(`${import.meta.env.VITE_API_URL}/access/sessions/${parsedSessionId}/me`, { method:"GET", @@ -73,7 +72,7 @@ export function SessionProvider({ children }: { children: ReactNode }) if(!response.ok) throw new Error("Error when getting role") return response.json() }).then((data : SessionRepresentation) => {setRole(data.role); setRepresentation_id(data.representation_id);}) - }, [parsedSessionId, token, socket, socket?.readyState]) + }, [parsedSessionId, token]) const value ={ role: role, diff --git a/frontend/src/schemas/types.gen.ts b/frontend/src/schemas/types.gen.ts index 690c896..c7849ba 100644 --- a/frontend/src/schemas/types.gen.ts +++ b/frontend/src/schemas/types.gen.ts @@ -834,7 +834,7 @@ export type SessionRepresentation = { /** * SessionRoles */ -export const SessionRoles = { CHAIR: 'chair', DELEGATION: 'delegation' } as const; +export const SessionRoles = { CHAIR: 'chair', DELEGATE: 'delegate' } as const; /** * SessionRoles