diff --git a/bun.lock b/bun.lock index 95f54a59..d4637aee 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "munify-chase", "dependencies": { + "@tanstack/table-core": "^8.21.3", "drizzle-kit": "1.0.0-beta.1-fd5d1e8", "drizzle-orm": "1.0.0-beta.1-fd5d1e8", "pg": "^8.16.3", @@ -425,6 +426,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw=="], diff --git a/messages/de.json b/messages/de.json index 06b97cfe..24661fe9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10,8 +10,10 @@ "addCommittee": "Gremium hinzufügen", "addCountriesCount": "{count} Länder hinzufügen", "addCountry": "Land hinzufügen", + "addMeToList": "Auf die Liste setzen", "addMember": "Mitglied hinzufügen", "addNonStateActor": "NA hinzufügen", + "addRepresentation": "Delegation hinzufügen", "addUnActor": "UN-Akteur hinzufügen", "admin": "Admin", "adoptionAnnouncement": "BREAKING: Verabschiedung einer Resolution zum Thema \"{agendaItem}\" im Gremium {committeeName}", @@ -19,7 +21,11 @@ "agendaItemTitle": "Titel des Tagesordnungspunkts", "agendaItems": "Tagesordnungspunkte", "allRightsReservedby": "Alle Rechte vorbehalten von", + "allowSelfAddToSpeakersList": "Selbst auf Redeliste setzen", + "allowSelfAddToSpeakersListDescription": "Delegierten und nichtstaatlichen Akteuren erlauben, sich selbst auf Redelisten zu setzen.", "announceAdoption": "Verabschiedung verkünden", + "assignedCount": "{count} zugewiesen", + "assignment": "Zuweisung", "back": "Zurück", "baseFontSize": "Basis-Schriftgröße", "baseFontSizeDescription": "Hier kann die Basis-Schriftgröße für die Präsentationsansicht festgelegt werden", @@ -50,6 +56,7 @@ "committeeOverview": "Gremienübersicht", "committeeStatus": "Gremienstatus", "committeeStatusExpired": "{status} abgelaufen!", + "committees": "Gremien", "con": "Dagegen", "conferenceCreated": "Konferenz erstellt!", "conferenceCreationError": "Konferenz konnte nicht erstellt werden", @@ -58,6 +65,8 @@ "conferenceMembers": "Konferenzmitglieder", "conferenceTitle": "Konferenztitel", "configuration": "Konfiguration", + "confirmDeleteCommittee": "Möchtest du dieses Gremium wirklich löschen? Alle zugehörigen Daten gehen verloren.", + "confirmDeleteRepresentation": "Möchtest du diese Delegation wirklich entfernen? Zugehörige Gremienmitgliedschaften werden entfernt.", "confirmRemoveMember": "Möchtest du dieses Mitglied wirklich entfernen?", "countries": "Länder", "countriesRecognized": "{count} Länder erkannt", @@ -71,10 +80,12 @@ "dateCannotBeInPast": "Das Datum darf nicht in der Vergangenheit liegen!", "delegate": "Delegierte*r", "delegations": "Delegationen", + "deleteRepresentation": "Delegation entfernen", "displayRegionalGroups": "Regionalgruppenanzeige", "download": "Download", "downloadPresenceData": "Anwesenheitsdaten", "edit": "Bearbeiten", + "editUser": "Benutzer bearbeiten", "email": "E-Mail", "enterAlpha2Code": "Bitte Alpha2Code eingeben", "enterCountryCodes": "Ländercodes eingeben (Alpha-2 oder Alpha-3)", @@ -85,10 +96,13 @@ "fileParseError": "Fehler beim Parsen der Datei", "formalDebate": "Formale Debatte", "forward": "Weiter", + "general": "Allgemein", "gotoSettings": "Zu den Einstellungen", "h1": "Überschrift 1", "h2": "Überschrift 2", "h3": "Überschrift 3", + "hasModeratedCaucus": "Moderierte informelle Sitzung", + "hasModeratedCaucusDescription": "Moderierte informelle Sitzung als Gremien-Status aktivieren.", "home": "Home", "homeAboutText": "CHASE (CHAirSoftwarE) ist eine Webanwendung zur Verwaltung und Durchführung von Debatten in Model United Nations Konferenzen. Sie ist für Vorsitzende und Delegierte gleichermaßen konzipiert. CHASE ermöglicht es Vorsitzenden, Debatten einfach zu verwalten, während Delegierte der Debatte folgen und mit anderen Delegierten auf intuitive und strukturierte Weise zusammenarbeiten können. CHASE ist freie und open source Software.", "homeAboutTitle": "Über CHASE", @@ -137,10 +151,12 @@ "layoutSelect": "Layout auswählen", "link": "Link", "listClosed": "Liste geschlossen", + "listClosedCannotAdd": "Die Liste ist geschlossen", "listEmpty": "Keine Rede", "login": "Anmelden", "logout": "Abmelden", "loose_slow_reindeer_build": "Gremienmitglieder", + "majorities": "Mehrheiten", "majoritySettings": "Mehrheitseinstellungen", "majoritySettingsDescriptions": "Die Einstellung der Mehrheitsverhältnisse dient der richtigen Darstellung, ob eine Mehrheit erreicht wurde.", "maroon_bland_ray_renew": "Gremien-Abkürzung", @@ -152,10 +168,12 @@ "minutesFromNow": "Relative Zeit: Springe X Minuten in die Zukunft", "missionControl": "Mission Control", "moderatedInformalCaucus": "Moderierte informelle Sitzung", + "name": "Name", "nextSpeaker": "Nächste Rede", "nextSpeakerDescription": "Möchtest du wirklich die nächste Rede aufrufen? Eventuelle Fragen- und Kurzbemerkungen werden verfallen.", "noAgendaItemSelected": "Kein Tagesordnungspunkt aktiv", "noAgendaItemSelectedDescription": "Um mit Redelisten arbeiten zu können muss zunächst ein Tagesordnungspunkt ausgewählt werden", + "noAssignmentNeeded": "Keine Mitgliedszuweisung für diese Rolle nötig.", "noCommentList": "Keine Liste für Fragen und Kurzbemerkungen", "noCurrentSpeaker": "Keine Rede", "noData": "Keine Daten", @@ -165,16 +183,21 @@ "nonStateActors": "Nichtstaatliche Akteure", "notAuthorized": "Du bist nicht berechtigt, auf diese Seite zuzugreifen", "notPresent": "Nicht anwesend", + "notPresentCannotAdd": "Du musst als anwesend markiert sein", "nothingChanged": "Nichts verändert", "numberedList": "Nummerierte Liste", "off": "Aus", "on": "An", + "onListPosition": "Du bist #{position} auf der Liste", "openPresentation": "Präsentationsansicht öffnen", + "paperSupportThresholdTooltip": "Benötigte Unterstützerstaaten für das Einreichen eines Änderungsantrags", "parsedCountries": "Hinzuzufügende Länder:", + "participantView": "Teilnehmeransicht", "pause": "Pause", "presence": "Anwesenheit", "present": "Anwesend", "presentationMode": "Präsentationsansicht", + "pressWebsite": "Presse-Website", "pro": "Dafür", "publish": "Veröffentlichen", "publishChanges": "Änderungen Veröffentlichen", @@ -185,6 +208,7 @@ "regionalGroup_latinAmericaCaribbean": "Lateinamerika und Karibik", "regionalGroup_westernEuropeOthers": "Westeuropa und Andere", "regionalGroups": "Regionalgruppen", + "removeFromList": "Von der Liste entfernen", "removeMember": "Entfernen", "role": "Rolle", "rollCall": "Anwesenheitsfeststellung", @@ -195,7 +219,10 @@ "rollCollSuccess": "Anwesenheitsfeststellung abgeschlossen", "save": "Speichern", "searchCommitteeMembers": "Gremienmitglieder durchsuchen", + "searchUsers": "Benutzer suchen...", "selectAgendaItem": "Tagesordnungspunkt auswählen...", + "selectCommitteeMember": "Gremienmitglied auswählen...", + "selectConferenceMember": "Konferenzmitglied auswählen...", "selected": "Ausgewählt", "seoDescription": "MUNify CHASE ist das kostenlose Open-Source-Debattenmanagement-Tool für Model United Nations Konferenzen. Redelisten, Abstimmungen und Resolutionen digital verwalten.", "seoTitle": "MUNify CHASE – Debattenmanagement für Model United Nations", @@ -207,6 +234,7 @@ "short_sleek_snake_hint": "Gremium", "showOfHandsVoting": "Abstimmung per Handzeichen", "simpleMajority": "Einfach", + "simpleMajorityTooltip": "Benötigte Stimmen für die einfache Mehrheit", "speaker": "Redner*in", "speakersList": "Redeliste", "speakersListNamePlaceholder": "Neuer Name...", @@ -241,10 +269,13 @@ "toastUpdateError": "{targetName} konnte nicht aktualisiert werden", "toastUpdateLoading": "{targetName} wird aktualisiert...", "toastUpdateSuccess": "{targetName} aktualisiert", + "totalCountriesPresent": "Anzahl anwesender Staaten", "twoThirdsMajority": "Zwei-Drittel", + "twoThirdsMajorityTooltip": "Benötigte Stimmen für 2/3-Mehrheit", "typeOfVoting": "Abstimmungsart", "unActor": "UN-Akteur", "unActors": "UN-Akteure", + "unassigned": "Nicht zugewiesen", "underline": "Unterstrichen", "undo": "Rückgängig", "unknown": "unbekannt", @@ -257,10 +288,13 @@ "upload": "Hochladen", "url": "URL", "userAlreadyExists": "Benutzer existiert bereits in dieser Konferenz: {email}", + "users": "Benutzer", "version": "Version", "voteTitel": "Name der Abstimmung", "voteTitleDescription": "Der Titel der Abstimmung wird allen Teilnehmenden angezeigt und dient der Identifizierung. Wird es leer gelassen, wird als Fallback \"Abstimmung\" verwendet.", "voting": "Abstimmung", + "waitingForAssignment": "Warte auf Zuweisung", + "waitingForAssignmentDescription": "Du wurdest noch keinem Gremium zugewiesen. Bitte warte, bis ein Admin dich zuweist.", "whiteboard": "Whiteboard", "whiteboardIsEmpty": "Das Whiteboard ist momentan leer...", "whiteboardPlaceholder": "Beginne hier zu schreiben...", @@ -269,5 +303,6 @@ "withoutAbstentions": "Keine Enthaltungen", "yes": "Ja", "you": "Du", - "youCannotEditYourself": "Du kannst deine eigene Rolle nicht bearbeiten" + "youCannotEditYourself": "Du kannst deine eigene Rolle nicht bearbeiten", + "youreUp": "Du bist dran!" } diff --git a/messages/en.json b/messages/en.json index f37f213a..c624bc15 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10,8 +10,10 @@ "addCommittee": "Add Committee", "addCountriesCount": "Add {count} countries", "addCountry": "Add Country", + "addMeToList": "Add me to list", "addMember": "Add Member", "addNonStateActor": "Add NGO", + "addRepresentation": "Add Delegation", "addUnActor": "Add UN Actor", "admin": "Admin", "adoptionAnnouncement": "BREAKING: Resolution on \"{agendaItem}\" adopted in the committee {committeeName}", @@ -19,7 +21,11 @@ "agendaItemTitle": "Agenda Item Title", "agendaItems": "Agenda Items", "allRightsReservedby": "All rights reserved by", + "allowSelfAddToSpeakersList": "Self-add to Speakers List", + "allowSelfAddToSpeakersListDescription": "Allow delegates and non-state actors to add themselves to speakers lists.", "announceAdoption": "Announce Adoption", + "assignedCount": "{count} assigned", + "assignment": "Assignment", "back": "Back", "baseFontSize": "Base Font Size", "baseFontSizeDescription": "Here you can set the base font size for the presentation view.", @@ -50,6 +56,7 @@ "committeeOverview": "Committee Overview", "committeeStatus": "Committee Status", "committeeStatusExpired": "{status} expired!", + "committees": "Committees", "con": "Against", "conferenceCreated": "Conference created!", "conferenceCreationError": "Could not create conference", @@ -58,6 +65,8 @@ "conferenceMembers": "Conference Members", "conferenceTitle": "Conference Title", "configuration": "Configuration", + "confirmDeleteCommittee": "Are you sure you want to delete this committee? All associated data will be lost.", + "confirmDeleteRepresentation": "Are you sure you want to remove this delegation? Associated committee memberships will be removed.", "confirmRemoveMember": "Are you sure you want to remove this member?", "countries": "Countries", "countriesRecognized": "{count} countries recognized", @@ -71,10 +80,12 @@ "dateCannotBeInPast": "The date must not be in the past!", "delegate": "Delegate", "delegations": "Delegations", + "deleteRepresentation": "Remove Delegation", "displayRegionalGroups": "Display Regional Blocs", "download": "Download", "downloadPresenceData": "Presence Data", "edit": "Edit", + "editUser": "Edit User", "email": "Email", "enterAlpha2Code": "Please enter Alpha2Code", "enterCountryCodes": "Enter country codes (Alpha-2 or Alpha-3)", @@ -85,10 +96,13 @@ "fileParseError": "Error parsing file", "formalDebate": "Formal debate", "forward": "Next", + "general": "General", "gotoSettings": "Go to settings", "h1": "Heading 1", "h2": "Heading 2", "h3": "Heading 3", + "hasModeratedCaucus": "Moderated Caucus", + "hasModeratedCaucusDescription": "Enable moderated informal caucus as a committee status option.", "home": "Home", "homeAboutText": "CHASE (CHAirSoftwarE) is a web application for managing and conducting debates at Model United Nations conferences. It is designed for both chairs and delegates. CHASE allows chairs to easily manage debates, while delegates can follow the discussion and collaborate with others in an intuitive and structured way. CHASE is free and open-source software.", "homeAboutTitle": "About CHASE", @@ -137,10 +151,12 @@ "layoutSelect": "Select Layout", "link": "Hyperlink", "listClosed": "List closed", + "listClosedCannotAdd": "The list is closed", "listEmpty": "No speech", "login": "Register", "logout": "Log out", "loose_slow_reindeer_build": "Committee Members", + "majorities": "Majorities", "majoritySettings": "Majority Settings", "majoritySettingsDescriptions": "Majority settings help visualize whether a motion has passed.", "maroon_bland_ray_renew": "Committee abbreviation", @@ -152,10 +168,12 @@ "minutesFromNow": "Relative time: Jump X minutes into the future", "missionControl": "Mission Control", "moderatedInformalCaucus": "Moderated informal caucus", + "name": "Name", "nextSpeaker": "Next Speech", "nextSpeakerDescription": "Do you really want to call the next speech? All remaining Points of Information will be discarded.", "noAgendaItemSelected": "No agenda item active", "noAgendaItemSelectedDescription": "To work with speakers' lists, you must first select an agenda item.", + "noAssignmentNeeded": "No member assignment needed for this role.", "noCommentList": "No Point of Information List", "noCurrentSpeaker": "No speech", "noData": "No data", @@ -165,16 +183,21 @@ "nonStateActors": "Non-state Actors", "notAuthorized": "You are not authorized to access this page", "notPresent": "Not present", + "notPresentCannotAdd": "You must be marked as present to add yourself", "nothingChanged": "Nothing changed", "numberedList": "Numbered list", "off": "Off", "on": "On", + "onListPosition": "You are #{position} on the list", "openPresentation": "Open Presentation View", + "paperSupportThresholdTooltip": "Supporting states required to submit an amendment", "parsedCountries": "Countries to add:", + "participantView": "Participant View", "pause": "Pause", "presence": "Presence", "present": "Present", "presentationMode": "Presentation View", + "pressWebsite": "Press Website", "pro": "In Favor", "publish": "Publish", "publishChanges": "Publish changes", @@ -185,6 +208,7 @@ "regionalGroup_latinAmericaCaribbean": "Latin America and the Caribbean", "regionalGroup_westernEuropeOthers": "Western Europe and Others", "regionalGroups": "Regional Groups", + "removeFromList": "Remove from list", "removeMember": "Remove", "role": "Role", "rollCall": "Roll Call", @@ -195,7 +219,10 @@ "rollCollSuccess": "Roll call complete", "save": "Save", "searchCommitteeMembers": "Search committee members", + "searchUsers": "Search users...", "selectAgendaItem": "Select agenda item...", + "selectCommitteeMember": "Select committee member...", + "selectConferenceMember": "Select conference member...", "selected": "Selected", "seoDescription": "MUNify CHASE is the free, open-source debate management tool for Model United Nations conferences. Manage speakers lists, voting, and resolutions digitally.", "seoTitle": "MUNify CHASE – Debate Management for Model United Nations", @@ -207,6 +234,7 @@ "short_sleek_snake_hint": "Committee", "showOfHandsVoting": "Vote by Show of Hands", "simpleMajority": "Simple", + "simpleMajorityTooltip": "Needed notes for simple majority", "speaker": "Speaker", "speakersList": "General Speakers' List", "speakersListNamePlaceholder": "New name...", @@ -241,10 +269,13 @@ "toastUpdateError": "Could not update {targetName}", "toastUpdateLoading": "Updating {targetName}...", "toastUpdateSuccess": "{targetName} updated", + "totalCountriesPresent": "Count of Present Countries", "twoThirdsMajority": "Two-thirds", + "twoThirdsMajorityTooltip": "Needed votes for two-thrids majority", "typeOfVoting": "Type of Vote", "unActor": "UN Actor", "unActors": "UN Actors", + "unassigned": "Unassigned", "underline": "Underlined", "undo": "Undo", "unknown": "unknown", @@ -257,10 +288,13 @@ "upload": "Upload", "url": "URL", "userAlreadyExists": "User already exists in this conference: {email}", + "users": "Users", "version": "Version", "voteTitel": "Vote Title", "voteTitleDescription": "The vote title will be visible to all participants and is used for identification. If left empty, \"Vote\" will be used as fallback.", "voting": "Voting", + "waitingForAssignment": "Waiting for Assignment", + "waitingForAssignmentDescription": "You have not been assigned to a committee yet. Please wait for an admin to assign you.", "whiteboard": "Whiteboard", "whiteboardIsEmpty": "The whiteboard is currently empty...", "whiteboardPlaceholder": "Start writing here...", @@ -269,5 +303,6 @@ "withoutAbstentions": "No Abstentions", "yes": "Yes", "you": "You", - "youCannotEditYourself": "You cannot edit your own role" + "youCannotEditYourself": "You cannot edit your own role", + "youreUp": "You're up!" } diff --git a/package.json b/package.json index bcbcbd66..69b3569d 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ }, "type": "module", "dependencies": { + "@tanstack/table-core": "^8.21.3", "drizzle-kit": "1.0.0-beta.1-fd5d1e8", "drizzle-orm": "1.0.0-beta.1-fd5d1e8", "pg": "^8.16.3" diff --git a/schema.graphql b/schema.graphql index 9113a95e..8041d965 100644 --- a/schema.graphql +++ b/schema.graphql @@ -49,6 +49,7 @@ type Committee { } type CommitteeMember { + committee(where: CommitteeWhereInputArgument): Committee! committeeId: ID! createdAt: DateTime! id: ID! @@ -57,10 +58,11 @@ type CommitteeMember { representation(where: RepresentationWhereInputArgument): Representation! representationId: ID! updatedAt: DateTime - user(where: ConferenceUserWhereInputArgument): ConferenceUser + users(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! } input CommitteeMemberWhereInputArgument { + committee: CommitteeWhereInputArgument committeeId: ID createdAt: DateTime id: ID @@ -69,7 +71,7 @@ input CommitteeMemberWhereInputArgument { representation: RepresentationWhereInputArgument representationId: ID updatedAt: DateTime - user: ConferenceUserWhereInputArgument + users: ConferenceUserWhereInputArgument } enum CommitteeStatusEnum { @@ -132,7 +134,7 @@ type ConferenceMember { representationId: ID! speakerOnList(limit: Int, offset: Int, where: SpeakerOnListWhereInputArgument): [SpeakerOnList!]! updatedAt: DateTime - user(where: ConferenceUserWhereInputArgument): ConferenceUser + users(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! } input ConferenceMemberWhereInputArgument { @@ -144,13 +146,15 @@ input ConferenceMemberWhereInputArgument { representationId: ID speakerOnList: SpeakerOnListWhereInputArgument updatedAt: DateTime - user: ConferenceUserWhereInputArgument + users: ConferenceUserWhereInputArgument } type ConferenceUser { + committeeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember committeeMemberId: ID conference(where: ConferenceWhereInputArgument): Conference! conferenceId: ID! + conferenceMember(where: ConferenceMemberWhereInputArgument): ConferenceMember conferenceMemberId: ID conferenceUserType: ConferenceUserTypeEnum! createdAt: DateTime! @@ -169,9 +173,11 @@ enum ConferenceUserTypeEnum { } input ConferenceUserWhereInputArgument { + committeeMember: CommitteeMemberWhereInputArgument committeeMemberId: ID conference: ConferenceWhereInputArgument conferenceId: ID + conferenceMember: ConferenceMemberWhereInputArgument conferenceMemberId: ID conferenceUserType: ConferenceUserTypeEnum createdAt: DateTime @@ -266,14 +272,25 @@ type Mutation { addSpeakerOnList(committeeMemberId: ID, conferenceMemberId: ID, position: Int, speakersListId: ID!): SpeakerOnList clearSpeakersList(id: ID!): SpeakersList createAgendaItem(committeeId: ID!, title: String!): AgendaItem + createCommittee(abbreviation: String!, conferenceId: ID!, name: String!): Committee + createCommitteeMember(committeeId: ID!, representationId: ID!): CommitteeMember + createConferenceMember(conferenceId: ID!, representationId: ID!): ConferenceMember createConferenceUser(conferenceId: ID!, conferenceUserType: ConferenceUserTypeEnum!, userEmail: String!): ConferenceUser + createRepresentation(alpha2Code: String, alpha3Code: String, conferenceId: ID!, faIcon: String, name: String, type: RepresentationTypeEnum!): Representation + deleteCommittee(id: ID!): Boolean + deleteCommitteeMember(id: ID!): Boolean + deleteConferenceMember(id: ID!): Boolean deleteConferenceUser(id: ID!): Boolean + deleteRepresentation(id: ID!): Boolean importDelegatorConference(data: ImportData!): Conference moveSpeakerToPosition(id: ID!, position: Int!): SpeakerOnList removeSpeakerOnList(speakerOnListId: ID!): SpeakersList + selfAddToSpeakersList(speakersListId: ID!): SpeakerOnList + selfRemoveFromSpeakersList(speakersListId: ID!): SpeakersList setPresenceForCommitteeMembers(ids: [ID!]!, present: Boolean!): [CommitteeMember!] - updateCommittee(activeAgendaItemId: ID, id: ID!, lastResolutionAdoptionDate: DateTime, showWhiteboard: Boolean, stateOfDebate: String, status: CommitteeStatusEnum, statusHeadline: String, statusUntil: DateTime, whiteboardContent: String): Committee - updateConferenceUser(conferenceUserType: ConferenceUserTypeEnum!, id: ID!): ConferenceUser + updateCommittee(abbreviation: String, activeAgendaItemId: ID, allowDelegationsToAddThemselvesToSpeakersList: Boolean, id: ID!, lastResolutionAdoptionDate: DateTime, name: String, showWhiteboard: Boolean, stateOfDebate: String, status: CommitteeStatusEnum, statusHeadline: String, statusUntil: DateTime, whiteboardContent: String): Committee + updateConference(hasModeratedCaucus: Boolean, id: ID!, pressWebsite: String, title: String): Conference + updateConferenceUser(committeeMemberId: ID, conferenceMemberId: ID, conferenceUserType: ConferenceUserTypeEnum!, id: ID!): ConferenceUser updateSpeakerOnList(id: ID!, overwriteName: String): SpeakerOnList updateSpeakersList(id: ID!, isClosed: Boolean, speakingTime: Int, startTimestamp: DateTime, stopTimer: Boolean = false, timeLeft: Int): SpeakersList } diff --git a/src/api/db/relations.ts b/src/api/db/relations.ts index 3bd526b4..26d7e829 100644 --- a/src/api/db/relations.ts +++ b/src/api/db/relations.ts @@ -50,10 +50,14 @@ export const relations = defineRelations(schema, (r) => ({ to: r.representation.id, optional: false }), - user: r.one.conferenceUser({ + committee: r.one.committee({ + from: r.committeeMember.committeeId, + to: r.committee.id, + optional: false + }), + users: r.many.conferenceUser({ from: r.committeeMember.id, - to: r.conferenceUser.committeeMemberId, - optional: true + to: r.conferenceUser.committeeMemberId }), presenceChangedTimestamps: r.many.presenceChangedTimestamp({ from: r.committeeMember.id, @@ -69,6 +73,16 @@ export const relations = defineRelations(schema, (r) => ({ from: r.conferenceUser.conferenceId, to: r.conference.id, optional: false + }), + committeeMember: r.one.committeeMember({ + from: r.conferenceUser.committeeMemberId, + to: r.committeeMember.id, + optional: true + }), + conferenceMember: r.one.conferenceMember({ + from: r.conferenceUser.conferenceMemberId, + to: r.conferenceMember.id, + optional: true }) }, representation: { @@ -99,10 +113,9 @@ export const relations = defineRelations(schema, (r) => ({ from: r.conferenceMember.id, to: r.speakerOnList.conferenceMemberId }), - user: r.one.conferenceUser({ + users: r.many.conferenceUser({ from: r.conferenceMember.id, - to: r.conferenceUser.conferenceMemberId, - optional: true + to: r.conferenceUser.conferenceMemberId }) }, agendaItem: { diff --git a/src/api/handlers/agendaItem.ts b/src/api/handlers/agendaItem.ts index 47588e6c..faac3d7d 100644 --- a/src/api/handlers/agendaItem.ts +++ b/src/api/handlers/agendaItem.ts @@ -43,6 +43,11 @@ abilityBuilder.agendaItem.allow(['read']).when(({ mustBeLoggedIn }) => { } }); +abilityBuilder.agendaItem.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + schemaBuilder.mutationFields((t) => { return { createAgendaItem: t.drizzleField({ diff --git a/src/api/handlers/committee.ts b/src/api/handlers/committee.ts index d58002b4..86497e83 100644 --- a/src/api/handlers/committee.ts +++ b/src/api/handlers/committee.ts @@ -9,9 +9,11 @@ import { arg as rumbleArg } from '$api/rumble'; import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; -import { assertFirstEntryExists } from '@m1212e/rumble'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; import { and, count, eq, type InferSelectModel } from 'drizzle-orm'; import { calculateMajority } from '$lib/utils/majorities'; +import { assertConferenceAdmin } from './conferenceUser'; +import { GraphQLError } from 'graphql'; const statusEnum = enum_({ tsName: 'committeeStatus' @@ -24,6 +26,11 @@ abilityBuilder.committee.allow(['read', 'update']).when(({ mustBeLoggedIn }) => } }); +abilityBuilder.committee.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + const getTotalPresentCount = async ( parent: InferSelectModel & { members: (InferSelectModel & { @@ -104,13 +111,72 @@ query({ schemaBuilder.mutationFields((t) => { return { + createCommittee: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + name: t.arg.string({ required: true }), + abbreviation: t.arg.string({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.committee) + .values({ + conferenceId: args.conferenceId, + name: args.name, + abbreviation: args.abbreviation + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.committee + .findFirst( + query( + ctx.abilities.committee.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteCommittee: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const committee = await db.query.committee.findFirst({ + where: { id: args.id } + }); + + if (!committee) { + throw new GraphQLError('Committee not found'); + } + + await assertConferenceAdmin(ctx, committee.conferenceId); + + await db.delete(schema.committee).where(eq(schema.committee.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }), + updateCommittee: t.drizzleField({ type: ref, args: { id: t.arg.id({ required: true }), - //TODO do we want to allow updates to these defaults? - // e.g. abbreviation and name probably are pretty static... - // name: t.arg.string(), + name: t.arg.string(), + abbreviation: t.arg.string(), whiteboardContent: t.arg.string(), showWhiteboard: t.arg.boolean(), status: t.arg({ @@ -124,12 +190,15 @@ schemaBuilder.mutationFields((t) => { activeAgendaItemId: t.arg.id(), lastResolutionAdoptionDate: t.arg({ type: 'DateTime' - }) + }), + allowDelegationsToAddThemselvesToSpeakersList: t.arg.boolean() }, resolve: async (query, root, args, ctx, info) => { await db .update(schema.committee) .set({ + name: args.name ?? undefined, + abbreviation: args.abbreviation ?? undefined, whiteboardContent: args.whiteboardContent ?? undefined, showWhiteboard: args.showWhiteboard ?? undefined, status: args.status ?? undefined, @@ -137,7 +206,9 @@ schemaBuilder.mutationFields((t) => { statusUntil: args.statusUntil ?? undefined, stateOfDebate: args.stateOfDebate ?? undefined, activeAgendaItemId: args.activeAgendaItemId ?? undefined, - lastResolutionAdoptionDate: args.lastResolutionAdoptionDate ?? undefined + lastResolutionAdoptionDate: args.lastResolutionAdoptionDate ?? undefined, + allowDelegationsToAddThemselvesToSpeakersList: + args.allowDelegationsToAddThemselvesToSpeakersList ?? undefined }) .where( and( diff --git a/src/api/handlers/committeeMember.ts b/src/api/handlers/committeeMember.ts index aaf92f6b..8a864968 100644 --- a/src/api/handlers/committeeMember.ts +++ b/src/api/handlers/committeeMember.ts @@ -1,8 +1,11 @@ import { db, schema } from '$api/db/db'; import { abilityBuilder, schemaBuilder } from '$api/rumble'; -import { and, inArray } from 'drizzle-orm'; +import { and, eq, inArray } from 'drizzle-orm'; import { basics } from './basics'; import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('committeeMember'); @@ -13,8 +16,80 @@ abilityBuilder.committeeMember.allow(['read', 'update']).when(({ mustBeLoggedIn } }); +abilityBuilder.committeeMember.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + schemaBuilder.mutationFields((t) => { return { + createCommitteeMember: t.drizzleField({ + type: ref, + args: { + committeeId: t.arg.id({ required: true }), + representationId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const committee = await db.query.committee.findFirst({ + where: { id: args.committeeId } + }); + + if (!committee) { + throw new GraphQLError('Committee not found'); + } + + await assertConferenceAdmin(ctx, committee.conferenceId); + + const result = await db + .insert(schema.committeeMember) + .values({ + committeeId: args.committeeId, + representationId: args.representationId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.committeeMember + .findFirst( + query( + ctx.abilities.committeeMember.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteCommitteeMember: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const committeeMember = await db.query.committeeMember.findFirst({ + where: { id: args.id }, + with: { committee: true } + }); + + if (!committeeMember) { + throw new GraphQLError('Committee member not found'); + } + + await assertConferenceAdmin(ctx, committeeMember.committee.conferenceId); + + await db.delete(schema.committeeMember).where(eq(schema.committeeMember.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }), + setPresenceForCommitteeMembers: t.drizzleField({ type: [ref], args: { diff --git a/src/api/handlers/conference.ts b/src/api/handlers/conference.ts index ba543472..40d8d646 100644 --- a/src/api/handlers/conference.ts +++ b/src/api/handlers/conference.ts @@ -1,26 +1,30 @@ -import { db } from '$api/db/db'; +import { db, schema } from '$api/db/db'; import { abilityBuilder, object, query, + schemaBuilder, pubsub as rumblePubsub, arg as rumbleArg } from '$api/rumble'; import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; import { ConferenceMemberRef, ConferenceMemberWhereInput } from './conferenceMember'; +import { assertConferenceAdmin } from './conferenceUser'; +import { eq } from 'drizzle-orm'; +import { assertFindFirstExists } from '@m1212e/rumble'; -abilityBuilder.conference.allow('read').when(({ mustBeLoggedIn }) => { +abilityBuilder.conference.allow(['read', 'update']).when(({ mustBeLoggedIn }) => { const user = mustBeLoggedIn(); if (user?.email && isWhitelistedEmail(user.email)) { return 'allow'; } }); -// .when(({ user }) => { -// if (user) { -// return {}; -// } -// }); + +abilityBuilder.conference.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); const ref = object({ table: 'conference', @@ -62,10 +66,48 @@ const ref = object({ }) }); -const pubsub = rumblePubsub({ table: 'committee' }); -const arg = rumbleArg({ table: 'committee' }); +const pubsub = rumblePubsub({ table: 'conference' }); +const arg = rumbleArg({ table: 'conference' }); query({ table: 'conference' }); +schemaBuilder.mutationFields((t) => ({ + updateConference: t.drizzleField({ + type: ref, + args: { + id: t.arg.id({ required: true }), + title: t.arg.string(), + pressWebsite: t.arg.string(), + hasModeratedCaucus: t.arg.boolean() + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.id); + + await db + .update(schema.conference) + .set({ + title: args.title ?? undefined, + pressWebsite: args.pressWebsite ?? undefined, + hasModeratedCaucus: args.hasModeratedCaucus ?? undefined + }) + .where(eq(schema.conference.id, args.id)); + + pubsub.updated(args.id); + + return db.query.conference + .findFirst( + query( + ctx.abilities.conference.filter('read', { + inject: { + where: { id: args.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }) +})); + export const ConferenceRef = ref; diff --git a/src/api/handlers/conferenceMember.ts b/src/api/handlers/conferenceMember.ts index dec11436..b92bde5c 100644 --- a/src/api/handlers/conferenceMember.ts +++ b/src/api/handlers/conferenceMember.ts @@ -1,6 +1,11 @@ -import { abilityBuilder } from '$api/rumble'; +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder } from '$api/rumble'; import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; import { basics } from './basics'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { eq } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('conferenceMember'); @@ -11,5 +16,70 @@ abilityBuilder.conferenceMember.allow('read').when(({ mustBeLoggedIn }) => { } }); +abilityBuilder.conferenceMember.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + createConferenceMember: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + representationId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.conferenceMember) + .values({ + conferenceId: args.conferenceId, + representationId: args.representationId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.conferenceMember + .findFirst( + query( + ctx.abilities.conferenceMember.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteConferenceMember: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const conferenceMember = await db.query.conferenceMember.findFirst({ + where: { id: args.id } + }); + + if (!conferenceMember) { + throw new GraphQLError('Conference member not found'); + } + + await assertConferenceAdmin(ctx, conferenceMember.conferenceId); + + await db.delete(schema.conferenceMember).where(eq(schema.conferenceMember.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }) +})); + export const ConferenceMemberWhereInput = arg; export const ConferenceMemberRef = ref; diff --git a/src/api/handlers/conferenceUser.ts b/src/api/handlers/conferenceUser.ts index aef10446..da332cc2 100644 --- a/src/api/handlers/conferenceUser.ts +++ b/src/api/handlers/conferenceUser.ts @@ -17,26 +17,16 @@ abilityBuilder.conferenceUser.allow('read').when(({ mustBeLoggedIn }) => { } }); -// abilityBuilder.conferenceUser.allow('read').when(({ user }) => { -// if (user) { -// return { -// where: eq(schema.conferenceUser.id, user.sub) -// }; -// } -// }); - -// abilityBuilder.conferenceUser.allow('read').when(({ user }) => { -// // TODO -// if (user) { -// return {}; -// } -// }); +abilityBuilder.conferenceUser.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); /** * Helper to check if the current user is an ADMIN for a specific conference * (either OIDC admin or conference ADMIN role) */ -async function assertConferenceAdmin( +export async function assertConferenceAdmin( ctx: { hasRole: (role: string) => boolean; mustBeLoggedIn: () => { email?: string | null } }, conferenceId: string ) { @@ -170,7 +160,9 @@ schemaBuilder.mutationFields((t) => ({ conferenceUserType: t.arg({ type: enum_({ tsName: 'conferenceUserType' }), required: true - }) + }), + committeeMemberId: t.arg({ type: 'ID' }), + conferenceMemberId: t.arg({ type: 'ID' }) }, resolve: async (query, root, args, ctx, info) => { // First get the conference user to find the conferenceId @@ -212,9 +204,52 @@ schemaBuilder.mutationFields((t) => ({ } } + // Validate committeeMemberId belongs to a committee in the same conference + if (args.committeeMemberId) { + const committeeMember = await db.query.committeeMember.findFirst({ + where: { id: args.committeeMemberId }, + with: { committee: true } + }); + if ( + !committeeMember || + committeeMember.committee.conferenceId !== conferenceUser.conferenceId + ) { + throw new GraphQLError('Committee member does not belong to this conference'); + } + } + + // Validate conferenceMemberId belongs to the same conference + if (args.conferenceMemberId) { + const conferenceMember = await db.query.conferenceMember.findFirst({ + where: { id: args.conferenceMemberId } + }); + if (!conferenceMember || conferenceMember.conferenceId !== conferenceUser.conferenceId) { + throw new GraphQLError('Conference member does not belong to this conference'); + } + } + + // Build the update set + const updateSet: Record = { + conferenceUserType: args.conferenceUserType + }; + + // Auto-clear: when role changes away from DELEGATE, clear committeeMemberId + if (args.conferenceUserType !== 'DELEGATE') { + updateSet.committeeMemberId = null; + } else if (args.committeeMemberId !== undefined) { + updateSet.committeeMemberId = args.committeeMemberId; + } + + // Auto-clear: when role changes away from NON_STATE_ACTOR, clear conferenceMemberId + if (args.conferenceUserType !== 'NON_STATE_ACTOR') { + updateSet.conferenceMemberId = null; + } else if (args.conferenceMemberId !== undefined) { + updateSet.conferenceMemberId = args.conferenceMemberId; + } + await db .update(schema.conferenceUser) - .set({ conferenceUserType: args.conferenceUserType }) + .set(updateSet) .where(eq(schema.conferenceUser.id, args.id)); pubsub.updated(args.id); diff --git a/src/api/handlers/representation.ts b/src/api/handlers/representation.ts index ee2ea2f4..4c74f8a7 100644 --- a/src/api/handlers/representation.ts +++ b/src/api/handlers/representation.ts @@ -1,12 +1,120 @@ -import { abilityBuilder } from '$api/rumble'; +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder } from '$api/rumble'; import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; import { basics } from './basics'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { eq } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('representation'); +const representationTypeEnum = enum_({ + tsName: 'representationType' +}); + abilityBuilder.representation.allow(['read', 'update']).when(({ mustBeLoggedIn }) => { const user = mustBeLoggedIn(); if (user?.email && isWhitelistedEmail(user.email)) { return 'allow'; } }); + +abilityBuilder.representation.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + createRepresentation: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + type: t.arg({ type: representationTypeEnum, required: true }), + name: t.arg.string(), + alpha2Code: t.arg.string(), + alpha3Code: t.arg.string(), + faIcon: t.arg.string() + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.representation) + .values({ + conferenceId: args.conferenceId, + type: args.type, + name: args.name ?? undefined, + alpha2Code: args.alpha2Code ?? undefined, + alpha3Code: args.alpha3Code ?? undefined, + faIcon: args.faIcon ?? undefined + }) + .returning() + .then(assertFirstEntryExists); + + // For DELEGATION type, auto-create committee members for all committees + if (args.type === 'DELEGATION') { + const committees = await db.query.committee.findMany({ + where: { conferenceId: args.conferenceId } + }); + + if (committees.length > 0) { + await db.insert(schema.committeeMember).values( + committees.map((c) => ({ + committeeId: c.id, + representationId: result.id + })) + ); + } + } + + pubsub.updated(result.id); + + return db.query.representation + .findFirst( + query( + ctx.abilities.representation.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteRepresentation: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const representation = await db.query.representation.findFirst({ + where: { id: args.id } + }); + + if (!representation) { + throw new GraphQLError('Representation not found'); + } + + await assertConferenceAdmin(ctx, representation.conferenceId); + + // Delete associated committee members first (FK may not cascade) + await db + .delete(schema.committeeMember) + .where(eq(schema.committeeMember.representationId, args.id)); + + // Delete associated conference members + await db + .delete(schema.conferenceMember) + .where(eq(schema.conferenceMember.representationId, args.id)); + + await db.delete(schema.representation).where(eq(schema.representation.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }) +})); diff --git a/src/api/handlers/speakerOnList.ts b/src/api/handlers/speakerOnList.ts index da8e2f8b..52a04e48 100644 --- a/src/api/handlers/speakerOnList.ts +++ b/src/api/handlers/speakerOnList.ts @@ -22,6 +22,11 @@ abilityBuilder.speakerOnList.allow(['read', 'update', 'delete']).when(({ mustBeL } }); +abilityBuilder.speakerOnList.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + schemaBuilder.mutationFields((t) => { return { updateSpeakerOnList: t.drizzleField({ @@ -194,6 +199,233 @@ schemaBuilder.mutationFields((t) => { .then(assertFindFirstExists); } }), + selfAddToSpeakersList: t.drizzleField({ + type: ref, + args: { + speakersListId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + if (!user.email) { + throw new GraphQLError('User email is required'); + } + + const createdId = await db.transaction(async (tx) => { + // Find the user's conferenceUser record + const conferenceUser = await tx.query.conferenceUser.findFirst({ + where: { userEmail: user.email! }, + with: { + committeeMember: { + with: { committee: true } + }, + conferenceMember: true + } + }); + + if (!conferenceUser) { + throw new GraphQLError('Conference user not found'); + } + + if ( + conferenceUser.conferenceUserType !== 'DELEGATE' && + conferenceUser.conferenceUserType !== 'NON_STATE_ACTOR' + ) { + throw new GraphQLError( + 'Only delegates and non-state actors can self-add to speakers lists' + ); + } + + // Get the speakers list and traverse to committee to check the flag + const speakersList = await tx.query.speakersList.findFirst({ + where: { id: args.speakersListId }, + with: { + agendaItem: { + with: { + committee: true + } + } + } + }); + + if (!speakersList) { + throw new GraphQLError('Speakers list not found'); + } + + if (speakersList.isClosed) { + throw new GraphQLError('Speakers list is closed'); + } + + const committee = (speakersList as any).agendaItem?.committee; + if (!committee) { + throw new GraphQLError('Committee not found for this speakers list'); + } + if (!committee.allowDelegationsToAddThemselvesToSpeakersList) { + throw new GraphQLError( + 'Self-adding to speakers list is not enabled for this committee' + ); + } + + let committeeMemberId: string | null = null; + let conferenceMemberId: string | null = null; + + const confUser = conferenceUser as any; + if (conferenceUser.conferenceUserType === 'DELEGATE') { + if (!confUser.committeeMember) { + throw new GraphQLError('Delegate is not assigned to a committee'); + } + if (!confUser.committeeMember.present) { + throw new GraphQLError('Delegate must be marked as present to add to speakers list'); + } + if (confUser.committeeMember.committeeId !== committee.id) { + throw new GraphQLError('Delegate is not a member of this committee'); + } + committeeMemberId = confUser.committeeMember.id; + } else { + // NON_STATE_ACTOR + if (!confUser.conferenceMember) { + throw new GraphQLError('Non-state actor is not assigned a conference member'); + } + conferenceMemberId = confUser.conferenceMember.id; + } + + // Check not already on list + const existing = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + ...(committeeMemberId ? { committeeMemberId } : {}), + ...(conferenceMemberId ? { conferenceMemberId } : {}) + } + }); + + if (existing) { + throw new GraphQLError('Already on speakers list'); + } + + // Append at end + const position = ( + await tx + .select({ count: count() }) + .from(table) + .where(eq(table.speakersListId, args.speakersListId)) + .then(assertFirstEntryExists) + ).count; + + const created = await tx + .insert(table) + .values({ + committeeMemberId, + conferenceMemberId, + speakersListId: args.speakersListId, + position + }) + .returning({ id: table.id }) + .then(assertFirstEntryExists); + + return created.id; + }); + + pubsub.created(); + + return db.query.speakerOnList + .findFirst( + query( + ctx.abilities.speakerOnList.filter('read', { + inject: { where: { id: createdId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + selfRemoveFromSpeakersList: t.drizzleField({ + type: SpeakersListRef, + args: { + speakersListId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + if (!user.email) { + throw new GraphQLError('User email is required'); + } + + const removed = await db.transaction(async (tx) => { + const conferenceUser = await tx.query.conferenceUser.findFirst({ + where: { userEmail: user.email! }, + with: { + committeeMember: true, + conferenceMember: true + } + }); + + if (!conferenceUser) { + throw new GraphQLError('Conference user not found'); + } + + // Find own speaker entry on this list + let speakerOnList; + if (conferenceUser.committeeMemberId) { + speakerOnList = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + committeeMemberId: conferenceUser.committeeMemberId + } + }); + } + if (!speakerOnList && conferenceUser.conferenceMemberId) { + speakerOnList = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + conferenceMemberId: conferenceUser.conferenceMemberId + } + }); + } + + if (!speakerOnList) { + throw new GraphQLError('You are not on this speakers list'); + } + + const deleted = await tx + .delete(table) + .where(eq(table.id, speakerOnList.id)) + .returning() + .then(assertFirstEntryExists); + + // Shift positions down + const aboutToBeShiftedDown = await tx.query.speakerOnList.findMany({ + where: { + speakersListId: deleted.speakersListId, + position: { + gt: deleted.position + } + }, + orderBy: { position: 'asc' } + }); + + for (const speaker of aboutToBeShiftedDown) { + await tx + .update(table) + .set({ + position: sql`${table.position} - 1` + }) + .where(eq(table.id, speaker.id)); + } + + return deleted; + }); + + pubsub.removed(removed.id); + + return db.query.speakersList + .findFirst( + query( + ctx.abilities.speakersList.filter('read', { + inject: { where: { id: removed.speakersListId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), moveSpeakerToPosition: t.drizzleField({ type: ref, args: { diff --git a/src/api/handlers/speakersList.ts b/src/api/handlers/speakersList.ts index 55040f2b..1d83a0b3 100644 --- a/src/api/handlers/speakersList.ts +++ b/src/api/handlers/speakersList.ts @@ -56,6 +56,11 @@ abilityBuilder.speakersList.allow(['read', 'update', 'delete']).when(({ mustBeLo } }); +abilityBuilder.speakersList.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + schemaBuilder.mutationFields((t) => { return { updateSpeakersList: t.drizzleField({ diff --git a/src/api/rumble.ts b/src/api/rumble.ts index 39505122..dc9f75d1 100644 --- a/src/api/rumble.ts +++ b/src/api/rumble.ts @@ -13,5 +13,5 @@ export const { abilityBuilder, schemaBuilder, arg, object, query, pubsub, create rumble({ db, context, - defaultLimit: 300 + defaultLimit: 1000 }); diff --git a/src/lib/components/CommitteeGrid.svelte b/src/lib/components/CommitteeGrid.svelte index 46ce6cf3..b916c587 100644 --- a/src/lib/components/CommitteeGrid.svelte +++ b/src/lib/components/CommitteeGrid.svelte @@ -2,14 +2,19 @@ import * as m from '$lib/paraglide/messages.js'; import IconInfoBox from './IconInfoBox.svelte'; import { getCommitteeStatusIcon, getCommitteeStatusText } from '$lib/utils/committeeStatus'; - import { type CommitteeOverviewQuery$result, type MissionControlQuery$result } from '$houdini'; + import { + type CommitteeOverviewQuery$result, + type MissionControlQuery$result, + type ParticipantConferenceQuery$result + } from '$houdini'; import AdoptionConfetti from './AdoptionConfetti.svelte'; interface Props { conference: | MissionControlQuery$result['findFirstConference'] - | CommitteeOverviewQuery$result['findFirstConference']; - environment?: 'SPECTATOR' | 'TEAM'; + | CommitteeOverviewQuery$result['findFirstConference'] + | ParticipantConferenceQuery$result['findFirstConference']; + environment?: 'SPECTATOR' | 'TEAM' | 'PARTICIPANT'; } let { conference, environment = 'SPECTATOR' }: Props = $props(); @@ -17,6 +22,8 @@ const getHref = (committeeId: string) => { if (environment === 'TEAM') { return `/app/${conference.id}/${committeeId}/setup`; + } else if (environment === 'PARTICIPANT') { + return `/app/${conference.id}/participant/${committeeId}`; } else { return `/app/${conference.id}/${committeeId}`; } diff --git a/src/lib/components/Majorities.svelte b/src/lib/components/Majorities.svelte index 8cd3f95d..7990bc69 100644 --- a/src/lib/components/Majorities.svelte +++ b/src/lib/components/Majorities.svelte @@ -1,4 +1,6 @@ @@ -81,7 +87,7 @@ {#each conferenceData as c} {@const conf = c.conference} diff --git a/src/routes/app/(launcher)/+page.ts b/src/routes/app/(launcher)/+page.ts index cad4c342..b435452e 100644 --- a/src/routes/app/(launcher)/+page.ts +++ b/src/routes/app/(launcher)/+page.ts @@ -6,6 +6,10 @@ export const _houdini_load = graphql(` findManyConferenceUser(where: { user: { id: $userId } }) { id conferenceUserType + committeeMemberId + committeeMember { + committeeId + } conference { id title diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/+layout.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/+layout.ts index d92f1fae..e9a406e2 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/+layout.ts +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/+layout.ts @@ -17,6 +17,7 @@ export const _houdini_load = graphql(` paperSupportThreshold whiteboardContent lastResolutionAdoptionDate + allowDelegationsToAddThemselvesToSpeakersList activeAgendaItem { id title diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts index e01275b9..e32557cb 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts @@ -16,6 +16,7 @@ export const CommitteeSubscription = graphql(` paperSupportThreshold whiteboardContent lastResolutionAdoptionDate + allowDelegationsToAddThemselvesToSpeakersList activeAgendaItem { id title diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte index 1e9fae41..807238a2 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte @@ -15,6 +15,7 @@ import StateOfDebate from '$lib/components/committee/StateOfDebateChanger.svelte'; import AgendaItemChanger from '$lib/components/committee/AgendaItemChanger.svelte'; import PresentationSettings from './PresentationSettings.svelte'; + import Tabs from '$lib/components/Tabs.svelte'; import { CommitteeSubscription } from '../committeeSubscription'; import { ScrollArea } from 'bits-ui'; import StatusWidget from '../StatusWidget.svelte'; @@ -42,6 +43,26 @@ } } `); + + const UpdateSelfAddMutation = graphql(` + mutation UpdateSelfAdd( + $committeeId: ID! + $allowDelegationsToAddThemselvesToSpeakersList: Boolean! + ) { + updateCommittee( + id: $committeeId + allowDelegationsToAddThemselvesToSpeakersList: $allowDelegationsToAddThemselvesToSpeakersList + ) { + id + allowDelegationsToAddThemselvesToSpeakersList + } + } + `); + + const selfAddTabs = [ + { id: true, label: m.on(), faIcon: 'fa-check' }, + { id: false, label: m.off(), faIcon: 'fa-xmark' } + ]; {#if committee} @@ -100,6 +121,19 @@ + +

{m.allowSelfAddToSpeakersListDescription()}

+ { + UpdateSelfAddMutation.mutate({ + committeeId: committee.id, + allowDelegationsToAddThemselvesToSpeakersList: tab + }); + }} + /> +
+ + + + + - -
- {m.addMember()} -
- -
-
- - -
- -
-
-
-
+ {#if activeTab === 'general'} + + {:else if activeTab === 'users'} + + {:else if activeTab === 'committees'} + + {:else if activeTab === 'delegations'} + + {:else if activeTab === 'nsa'} + + {/if} {:else}
diff --git a/src/routes/app/[conferenceId]/mission-control/config/+page.ts b/src/routes/app/[conferenceId]/mission-control/config/+page.ts index 40963e3d..fa7eefc7 100644 --- a/src/routes/app/[conferenceId]/mission-control/config/+page.ts +++ b/src/routes/app/[conferenceId]/mission-control/config/+page.ts @@ -7,10 +7,75 @@ export const _houdini_load = graphql(` findFirstConference(where: { id: $conferenceId }) { id title + pressWebsite + hasModeratedCaucus users { id userEmail conferenceUserType + user { + givenName + familyName + } + committeeMember { + id + representation { + id + name + alpha2Code + alpha3Code + faIcon + } + committee { + id + name + abbreviation + } + } + conferenceMember { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + } + committees { + id + name + abbreviation + members { + id + representation { + id + name + alpha2Code + alpha3Code + faIcon + type + } + } + } + members { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + representations { + id + name + alpha2Code + alpha3Code + type + faIcon } } currentUserRole: findManyConferenceUser( diff --git a/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte new file mode 100644 index 00000000..ee6c5718 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte @@ -0,0 +1,233 @@ + + + +
+ + + + + + + + + + + {#if committees.length === 0} + + + + {:else} + {#each committees as committee (committee.id)} + + {#if editingId === committee.id} + + + + + {:else} + + + + + {/if} + + {/each} + {/if} + +
{m.committeeAbbreviation()}{m.committeeName()}{m.committeeMembers()}
{m.noData()}
+ + + + {committee.members.length} +
+ + +
+
{committee.abbreviation}{committee.name}{committee.members.length} +
+ + +
+
+
+ + +
+ {m.addCommittee()} +
+
+ {m.committeeAbbreviation()} + +
+
+ {m.committeeName()} + +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte new file mode 100644 index 00000000..c19c0a78 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte @@ -0,0 +1,188 @@ + + + +
+ + + + + + + + + + + + {#if delegations.length === 0} + + + + {:else} + {#each delegations as delegation (delegation.id)} + + + + + + + + {/each} + {/if} + +
{m.name()}Alpha-3{m.committees()}
{m.noData()}
+ + + {delegation.name || getTranslatedCountryNameFromAlpha3Code(delegation.alpha3Code)} + + {delegation.alpha3Code?.toUpperCase() ?? '—'} + + {getCommitteesForDelegation(delegation.id) || '—'} + +
+ + +
+
+
+ +
+ +
+
+ + + + diff --git a/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte b/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte new file mode 100644 index 00000000..361a1060 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte @@ -0,0 +1,180 @@ + + + + {#if user} +

{m.editUser()}

+

{user.userEmail}

+ +
+ + +
+ + {#if selectedRole === 'DELEGATE'} +
+ + +
+ {:else if selectedRole === 'NON_STATE_ACTOR'} +
+ + +
+ {:else} +

{m.noAssignmentNeeded()}

+ {/if} + + + {/if} +
diff --git a/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte b/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte new file mode 100644 index 00000000..5625a60b --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte @@ -0,0 +1,174 @@ + + + + {#if delegation} +

{m.edit()}

+ +
+ + + {delegation.name || getTranslatedCountryNameFromAlpha3Code(delegation.alpha3Code)} + +
+ +
+ + {#if committees.length === 0} +

{m.noData()}

+ {:else} +
+ {#each committees as committee (committee.id)} + + {/each} +
+ {/if} +
+ + + {/if} +
diff --git a/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte new file mode 100644 index 00000000..6f640dd5 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte @@ -0,0 +1,123 @@ + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte new file mode 100644 index 00000000..312dd5ad --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte @@ -0,0 +1,186 @@ + + + +
+ + + + + + + + + + + {#if nsaActors.length === 0} + + + + {:else} + {#each nsaActors as actor (actor.id)} + + + + + + + {/each} + {/if} + +
{m.icon()}{m.name()}{m.role()}
{m.noData()}
+ + {actor.name ?? '—'} + + {typeLabel[actor.type]?.() ?? actor.type} + + + +
+
+ + +
+ + {m.addNonStateActor()} + +
+
+ {m.name()} + +
+
+ {m.role()} + +
+
+ {m.icon()} + +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte new file mode 100644 index 00000000..f2cf42fb --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte @@ -0,0 +1,619 @@ + + + + +
+ +
+ + +
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {/each} + + {/each} + + + {#if table.getRowModel().rows.length === 0} + + + + {:else} + {#each table.getRowModel().rows as row (row.id)} + {@const user = row.original} + {@const isSelf = isCurrentUser(user.userEmail)} + + + + + + + + + + + + + + + {/each} + {/if} + +
+ {#if !header.isPlaceholder} +
+ {#if header.id === 'userEmail'} + {m.email()} + {:else if header.id === 'name'} + {m.name()} + {:else if header.id === 'conferenceUserType'} + {m.role()} + {:else if header.id === 'committee'} + {m.committee()} + {:else if header.id === 'assignment'} + {m.assignment()} + {/if} + {#if header.column.getIsSorted() === 'asc'} + + {:else if header.column.getIsSorted() === 'desc'} + + {:else if header.column.getCanSort()} + + {/if} +
+ {/if} +
+ {m.noMembers()} +
+ {user.userEmail} + {#if isSelf} + {m.you()} + {/if} + + {getUserDisplayName(user) || '—'} + + + {roleLabel[user.conferenceUserType]?.() ?? user.conferenceUserType} + + + {#if user.committeeMember?.committee} + {user.committeeMember.committee.abbreviation} + {:else} + + {/if} + + {#if getAssignmentRepresentation(user)} +
+ + {getAssignmentText(user)} +
+ {:else if isAssignableRole(user.conferenceUserType)} + {m.unassigned()} + {:else} + + {/if} +
+
+ + +
+
+
+ + + {#if table.getPageCount() > 1} +
+ + {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + + 1}–{Math.min( + (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length + )} / {table.getFilteredRowModel().rows.length} + +
+ + {#each getVisiblePages(table.getState().pagination.pageIndex, table.getPageCount()) as item, i (item === 'ellipsis' ? `ellipsis-${i}` : item)} + {#if item === 'ellipsis'} + + {:else} + + {/if} + {/each} + +
+
+ {/if} + + +
+ {m.addMember()} +
+ +
+
+ + +
+ +
+
+
+
+ + diff --git a/src/routes/app/[conferenceId]/participant/+layout.svelte b/src/routes/app/[conferenceId]/participant/+layout.svelte new file mode 100644 index 00000000..ae9c9d03 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/src/routes/app/[conferenceId]/participant/+layout.ts b/src/routes/app/[conferenceId]/participant/+layout.ts new file mode 100644 index 00000000..0b4f846c --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+layout.ts @@ -0,0 +1,47 @@ +import { graphql } from '$houdini'; +import type { ParticipantIdentityQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantIdentityQuery($conferenceId: ID!, $userId: ID!) { + findManyConferenceUser(where: { conference: { id: $conferenceId }, user: { id: $userId } }) { + id + conferenceUserType + committeeMemberId + conferenceMemberId + committeeMember { + id + present + committeeId + representation { + id + name + alpha2Code + alpha3Code + type + faIcon + } + } + conferenceMember { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + } + } +`); + +export const _ParticipantIdentityQueryVariables: ParticipantIdentityQueryVariables = async ( + event +) => { + const { user } = await event.parent(); + + return { + conferenceId: event.params.conferenceId, + userId: user.sub + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/+page.svelte b/src/routes/app/[conferenceId]/participant/+page.svelte new file mode 100644 index 00000000..10335f5d --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+page.svelte @@ -0,0 +1,69 @@ + + + + {m.participantView()} - MUNify CHASE + + +{#if role === 'DELEGATE' && !myCommitteeId} + +
+ +

{m.waitingForAssignment()}

+

+ {m.waitingForAssignmentDescription()} +

+ + + {m.back()} + +
+{:else if conference} + + + +
+ +
+ + +{/if} diff --git a/src/routes/app/[conferenceId]/participant/+page.ts b/src/routes/app/[conferenceId]/participant/+page.ts new file mode 100644 index 00000000..62fe7ea9 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+page.ts @@ -0,0 +1,32 @@ +import { graphql } from '$houdini'; +import type { ParticipantConferenceQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantConferenceQuery($conferenceId: ID!) { + findFirstConference(where: { id: $conferenceId }) { + id + title + committees { + id + name + abbreviation + lastResolutionAdoptionDate + activeAgendaItem { + id + title + } + status + statusHeadline + statusUntil + } + } + } +`); + +export const _ParticipantConferenceQueryVariables: ParticipantConferenceQueryVariables = ( + event +) => { + return { + conferenceId: event.params.conferenceId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte b/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte new file mode 100644 index 00000000..dfe8f635 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte @@ -0,0 +1,31 @@ + + +
+
+ {#if representation} + + {:else} + + {/if} + {displayName} +
+
diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte new file mode 100644 index 00000000..8025da28 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte @@ -0,0 +1,221 @@ + + + + {committee?.name ?? m.committee()} - MUNify CHASE + + +{#if committee} + + + + +
+ +
+ +
+ + +
+
+ + + {#if committee.statusHeadline} +
{committee.statusHeadline}
+ {/if} +
+
+
+
+

{m.majorities()}

+ +
+
+ + + {#if isParticipant || role === 'SPECTATOR'} + {#each [{ list: speakersList, label: m.speakersList(), myPosition: myPositionOnSpeakers }, { list: commentList, label: m.commentList(), myPosition: myPositionOnComments }] as { list, label, myPosition }} + {#if list} +
+
+

{label}

+ + + + + + + {#if canSelfAdd} + {#if myPosition !== null} + +
+ {#if myPosition === 0} + + {m.youreUp()} + + {:else} + + {m.onListPosition({ position: String(myPosition) })} + + {/if} + +
+ {:else if list.isClosed} +
+ + {m.listClosedCannotAdd()} +
+ {:else if role === 'DELEGATE' && !myPresent} +
+ + {m.notPresentCannotAdd()} +
+ {:else} + + {/if} + {/if} +
+
+ {/if} + {/each} + {/if} + + + {#if committee.showWhiteboard && committee.whiteboardContent} +
+
+

{m.whiteboard()}

+ +
+
+ {/if} +
+{/if} diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts new file mode 100644 index 00000000..c7d6d16e --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts @@ -0,0 +1,81 @@ +import { graphql } from '$houdini'; +import type { ParticipantCommitteeQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantCommitteeQuery($committeeId: ID!) { + findFirstCommittee(where: { id: $committeeId }) { + id + abbreviation + name + status + statusHeadline + statusUntil + showWhiteboard + whiteboardContent + allowDelegationsToAddThemselvesToSpeakersList + totalPresent + simpleMajority + twoThirdsMajority + paperSupportThreshold + activeAgendaItem { + id + title + speakersList { + id + type + isClosed + speakingTime + startTimestamp + timeLeft + speakers { + id + position + overwriteName + committeeMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + present + } + conferenceMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + } + } + } + } + members { + id + present + representation { + id + type + name + alpha2Code + faIcon + } + } + } + } +`); + +export const _ParticipantCommitteeQueryVariables: ParticipantCommitteeQueryVariables = (event) => { + return { + committeeId: event.params.committeeId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts new file mode 100644 index 00000000..395d0eb3 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts @@ -0,0 +1,74 @@ +import { graphql } from '$houdini'; + +export const ParticipantCommitteeSubscription = graphql(` + subscription ParticipantCommitteeSubscription($id: ID!) { + findFirstCommittee(where: { id: $id }) { + id + abbreviation + name + status + statusHeadline + statusUntil + showWhiteboard + whiteboardContent + allowDelegationsToAddThemselvesToSpeakersList + totalPresent + simpleMajority + twoThirdsMajority + paperSupportThreshold + activeAgendaItem { + id + title + speakersList { + id + type + isClosed + speakingTime + startTimestamp + timeLeft + speakers { + id + position + overwriteName + committeeMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + present + } + conferenceMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + } + } + } + } + members { + id + present + representation { + id + type + name + alpha2Code + faIcon + } + } + } + } +`);