diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 7caf2aa..19d570a 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, types from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship import uuid @@ -7,6 +7,21 @@ import datetime from .database import Base +class ChoiceType(types.TypeDecorator): + + impl = types.String + + def __init__(self, choices, **kw): + self.choices = dict(choices) + super(ChoiceType, self).__init__(**kw) + + def process_bind_param(self, value, dialect): + return [k for k, v in self.choices.items() if v == value][0] + + def process_result_value(self, value, dialect): + return self.choices[value] + + class User(Base): __tablename__ = "users" @@ -47,8 +62,20 @@ class Queue(Base): description = Column(String, index=True) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) start_time = Column(DateTime, nullable=True) + status = Column( + ChoiceType( + { + "created": "created", + "waiting": "waiting", + "active": "active", + "finished": "finished", + } + ), + nullable=False, + ) users = relationship("QueueUser", backref="queue", lazy="dynamic") + logs = relationship("QueueLog", backref="queue", lazy="dynamic") class QueueUser(Base): @@ -66,6 +93,7 @@ class QueueLog(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) action = Column(String) + queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id")) created = Column(DateTime, default=datetime.datetime.utcnow) diff --git a/backend/app/views/auth/api.py b/backend/app/views/auth/api.py index 56bc1b9..22cd634 100644 --- a/backend/app/views/auth/api.py +++ b/backend/app/views/auth/api.py @@ -72,8 +72,10 @@ async def register( @router.get("/me") async def read_users_me( - current_user: Annotated[schemas.User, Depends(services.get_current_active_user)], -) -> schemas.User: + current_user: Annotated[ + schemas.UserInDB, Depends(services.get_current_active_user) + ], +) -> schemas.UserInDB: return current_user diff --git a/backend/app/views/queue/api.py b/backend/app/views/queue/api.py index a4bdcfc..1f8c305 100644 --- a/backend/app/views/queue/api.py +++ b/backend/app/views/queue/api.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta, timezone from typing import Annotated, Union from sqlalchemy.orm import Session +import redis from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm @@ -9,7 +10,8 @@ from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel from ...config import jwt_config -from ...dependencies import get_db +from ...dependencies import get_db, get_redis +from ...db import models from . import schemas from . import services @@ -25,8 +27,15 @@ router = APIRouter( ) +@router.get("/owned") +async def owned_queues_list( + queues: Annotated[schemas.Queue, Depends(services.get_owned_queues)], +) -> list[schemas.QueueInDb]: + return queues + + @router.get("/") -async def user_queues_list( +async def anonuser_queues_list( queues: Annotated[schemas.Queue, Depends(services.get_user_queues)], ) -> list[schemas.QueueInDb]: return queues @@ -60,3 +69,10 @@ async def listen_queue( updated_queue: Annotated[schemas.QueueDetail, Depends(services.set_queue_listener)] ) -> schemas.QueueDetail: return updated_queue + + +@router.post("/{queue_id}/action/{action}") +async def perform_queue_action( + result: Annotated[schemas.ActionResult, Depends(services.action_wrapper)] +): + return result diff --git a/backend/app/views/queue/schemas.py b/backend/app/views/queue/schemas.py index 1da71c6..8e9412e 100644 --- a/backend/app/views/queue/schemas.py +++ b/backend/app/views/queue/schemas.py @@ -41,4 +41,14 @@ class QueueInDb(Queue): class QueueDetail(Queue): id: UUID + status: str + owner_id: UUID participants: ParticipantInfo + + +class ActionResult(BaseModel): + action: str + result: str + + class Config: + from_attributes = True diff --git a/backend/app/views/queue/services.py b/backend/app/views/queue/services.py index b1471dc..6001bfd 100644 --- a/backend/app/views/queue/services.py +++ b/backend/app/views/queue/services.py @@ -14,24 +14,38 @@ from ..auth import schemas as auth_schemas from . import schemas -def get_queue_by_id(queue_id: UUID, db: Session) -> models.Queue: +def get_queue_by_id( + queue_id: UUID, db: Annotated[Session, Depends(get_db)] +) -> models.Queue: q = db.query(models.Queue).filter(models.Queue.id == queue_id).first() return q -def get_user_queues( +def get_owned_queues( current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)] ) -> list[schemas.QueueInDb]: return [schemas.QueueInDb.model_validate(q) for q in current_user.owns_queues] +def get_user_queues( + current_user: Annotated[auth_schemas.User, Depends(auth_services.get_anon_user)] +) -> list[schemas.QueueInDb]: + return [ + schemas.QueueInDb.model_validate(q.queue) + for q in current_user.parts_in_queues.filter(models.QueueUser.passed == False) + ] + + def create_queue( new_queue: schemas.Queue, current_user: auth_schemas.UserInDB, db: Session, ) -> schemas.QueueInDb: q = models.Queue( - name=new_queue.name, description=new_queue.description, owner_id=current_user.id + name=new_queue.name, + description=new_queue.description, + owner_id=current_user.id, + status="created", ) db.add(q) db.commit() @@ -48,6 +62,8 @@ def get_detailed_queue( id=q.id, name=q.name, description=q.description, + status=q.status, + owner_id=q.owner_id, participants=schemas.ParticipantInfo( total=q.users.count(), remaining=q.users.filter(models.QueueUser.passed == False).count(), @@ -102,3 +118,132 @@ async def set_queue_listener( await ps.unsubscribe() new_queue = get_detailed_queue(queue_id=queue_id, db=db) return new_queue + + +async def get_queue_owner( + queue: Annotated[models.Queue, Depends(get_queue_by_id)] +) -> auth_schemas.UserInDB: + return queue.owner if queue else None + + +async def verify_queue_owner( + queue_owner: Annotated[auth_schemas.UserInDB, Depends(get_queue_owner)], + current_user: Annotated[ + auth_schemas.UserInDB, Depends(auth_services.get_current_user) + ], +) -> bool: + return queue_owner.id == current_user.id if queue_owner and current_user else False + + +async def rebuild_queue( + queue: Annotated[models.Queue, Depends(get_queue_by_id)], + db: Annotated[Session, Depends(get_db)], +): + for i, qu in enumerate( + queue.users.filter(models.QueueUser.passed == False).order_by( + models.QueueUser.position.asc() + ) + ): + setattr(qu, "position", i) + db.commit() + + +async def kick_first( + is_owner: Annotated[bool, Depends(verify_queue_owner)], + queue: Annotated[models.Queue, Depends(get_queue_by_id)], + db: Annotated[Session, Depends(get_db)], +): + if is_owner: + first_user = ( + queue.users.filter(models.QueueUser.passed == False) + .order_by(models.QueueUser.position.asc()) + .first() + ) + if first_user: + setattr(first_user, "passed", True) + + db.commit() + await rebuild_queue(queue=queue, db=db) + return + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No first user", + ) + + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="You are not a queue owner!", + ) + + +async def start_queue( + is_owner: Annotated[bool, Depends(verify_queue_owner)], + queue: Annotated[models.Queue, Depends(get_queue_by_id)], + db: Annotated[Session, Depends(get_db)], +): + if queue and is_owner: + setattr(queue, "status", "active") + db.commit() + await rebuild_queue(queue=queue, db=db) + return + + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="You are not a queue owner!", + ) + + +async def pass_queueuser( + queue: Annotated[models.Queue, Depends(get_queue_by_id)], + anon_user: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)], + db: Annotated[Session, Depends(get_db)], +): + if anon_user: + qu = ( + db.query(models.QueueUser) + .filter( + models.QueueUser.queue_id == queue.id, + models.QueueUser.user_id == anon_user.id, + ) + .first() + ) + if qu: + setattr(qu, "passed", True) + db.commit() + await rebuild_queue(queue=queue, db=db) + return + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found!", + ) + + +async def action_wrapper( + action: str, + is_owner: Annotated[bool, Depends(verify_queue_owner)], + queue: Annotated[models.Queue, Depends(get_queue_by_id)], + anon_user: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)], + db: Annotated[Session, Depends(get_db)], + r: Annotated[redis.client.Redis, Depends(get_redis)], +) -> schemas.ActionResult: + if queue: + if action == "kick-first": + await kick_first(is_owner=is_owner, queue=queue, db=db) + await r.publish(str(queue.id), "updated") + return {"action": action, "status": "success"} + if action == "pass": + await pass_queueuser(queue=queue, anon_user=anon_user, db=db) + await r.publish(str(queue.id), "updated") + return {"action": action, "status": "success"} + if action == "start": + await start_queue(queue=queue, is_owner=is_owner, db=db) + await r.publish(str(queue.id), "updated") + return {"action": action, "status": "success"} + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Action not found!", + ) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Queue not found!", + ) diff --git a/frontend/app/src/components/HeaderComponent.tsx b/frontend/app/src/components/HeaderComponent.tsx index 45b7b36..be14bf5 100644 --- a/frontend/app/src/components/HeaderComponent.tsx +++ b/frontend/app/src/components/HeaderComponent.tsx @@ -1,4 +1,5 @@ import { + ClockCircleOutlined, DesktopOutlined, GlobalOutlined, LogoutOutlined, @@ -113,6 +114,11 @@ const HeaderComponent = () => { icon: , disabled: !user, }, + { + label: {tr("Current queues")}, + key: "parting", + icon: , + }, { label: {tr("News")}, key: "news", @@ -171,6 +177,11 @@ const HeaderComponent = () => { disabled: !user, onClick: () => setDrawerOpen(false), }, + { + label: {tr("Current queues")}, + key: "parting", + icon: , + }, { label: {tr("News")}, key: "news", diff --git a/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx b/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx index 95ed689..13c8822 100644 --- a/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx +++ b/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx @@ -31,11 +31,9 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => { const onJoinButtonClick = () => { joinQueue(props.id) .unwrap() - .then(() => - messageApi.success(tr("Successfully joined queue ") + data.name) - ) .then(() => navigate(`/queue/${props.id}`)) .then(() => refetch()) + .then(() => messageApi.success(tr("Successfully joined queue"))) .catch((e) => messageApi.error(tr(e.data.detail))); }; diff --git a/frontend/app/src/components/queue/PartingQueuesList.tsx b/frontend/app/src/components/queue/PartingQueuesList.tsx new file mode 100644 index 0000000..c31a077 --- /dev/null +++ b/frontend/app/src/components/queue/PartingQueuesList.tsx @@ -0,0 +1,60 @@ +import React, { useEffect } from "react"; +import { useGetQueuesQuery } from "../../slice/QueueApi"; +import "../styles.css"; +import { Button, Spin } from "antd"; +import { LoadingOutlined, RightOutlined } from "@ant-design/icons"; +import Title from "antd/es/typography/Title"; +import tr from "../../config/translation"; +import { Link } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { StorePrototype } from "../../config/store"; + +type Queue = { + id: string; + name: string; +}; + +const PartingQueuesList = (): JSX.Element => { + const user = useSelector((state: StorePrototype) => state.auth.user); + const { data, refetch, isLoading } = useGetQueuesQuery({}); + useEffect(() => { + user && refetch(); + }, [user]); + return ( +
+ {tr("Queues you are in")} +
+
+ } + spinning={isLoading} + > + {data?.length ? ( + data?.map((ele: Queue) => ( +
+ {ele.name} + +
+ )) + ) : ( + <> + {tr("You are not parting in any queues!")} +
+ + + +
+ + )} +
+
+ ); +}; +export default PartingQueuesList; diff --git a/frontend/app/src/components/queue/QueueCard.tsx b/frontend/app/src/components/queue/QueueCard.tsx index a86459f..58a14ba 100644 --- a/frontend/app/src/components/queue/QueueCard.tsx +++ b/frontend/app/src/components/queue/QueueCard.tsx @@ -1,26 +1,99 @@ -import React from "react"; -import { useGetQueueDetailQuery } from "../../slice/QueueApi"; +import React, { useContext } from "react"; +import { + useGetQueueDetailQuery, + useKickFirstActionMutation, + useStartQueueActionMutation, +} from "../../slice/QueueApi"; import "../styles.css"; import { Button, Spin } from "antd"; import { - ArrowUpOutlined, + FieldTimeOutlined, FileTextOutlined, + FlagOutlined, + HourglassOutlined, LoadingOutlined, + PlusCircleOutlined, + QuestionCircleOutlined, UserOutlined, } from "@ant-design/icons"; import Title from "antd/es/typography/Title"; import tr from "../../config/translation"; -import { Link, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; +import AnonUserCard from "../user/AnonUserCard"; import { useSelector } from "react-redux"; import { StorePrototype } from "../../config/store"; -import AnonUserCard from "../user/AnonUserCard"; +import { MessageContext } from "../../App"; + +const getStatusText = (status: string) => { + switch (status) { + case "created": + return ( +

+ {" "} + {tr("Created")} +

+ ); + case "waiting": + return ( +

+ {" "} + {tr("Waiting for start")} +

+ ); + case "active": + return ( +

+ {" "} + {tr("In progress")} +

+ ); + case "finished": + return ( +

+ {" "} + {tr("Finished")} +

+ ); + default: + return ( +

+ {" "} + {tr("Unknown status")} +

+ ); + } +}; const QueueCard = (): JSX.Element => { - const user = useSelector((state: StorePrototype) => state.auth.user); + const messageApi = useContext(MessageContext); + const { queueId } = useParams(); - const { data, isFetching } = useGetQueueDetailQuery(queueId, { + const { data, isFetching, refetch } = useGetQueueDetailQuery(queueId, { skip: !queueId, }); + const user = useSelector((state: StorePrototype) => state.auth.user); + const [kickFirstAction] = useKickFirstActionMutation(); + const [startQueueAction] = useStartQueueActionMutation(); + + const kickFirst = () => { + if (queueId) { + kickFirstAction(queueId) + .unwrap() + .then(() => refetch()) + .then(() => messageApi.success(tr("First user in queue kicked!"))) + .catch(() => messageApi.error(tr("Action error"))); + } + }; + + const startQueue = () => { + if (queueId) { + startQueueAction(queueId) + .unwrap() + .then(() => refetch()) + .then(() => messageApi.success(tr("Queue has started!"))) + .catch(() => messageApi.error(tr("Action error"))); + } + }; return (
@@ -42,13 +115,22 @@ const QueueCard = (): JSX.Element => { {" "} {data?.participants?.remaining} / {data?.participants?.total}

+ {data && getStatusText(data.status)} + {user && user.id && ( +
+ + {data?.status === "created" && ( + + )} +
+ )}
{tr("Queue participants")} {data?.participants.users_list.map((v) => { - return ; + return ; })}
diff --git a/frontend/app/src/components/queue/QueuesList.tsx b/frontend/app/src/components/queue/QueuesList.tsx index 160cc2c..0951098 100644 --- a/frontend/app/src/components/queue/QueuesList.tsx +++ b/frontend/app/src/components/queue/QueuesList.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { useGetQueuesQuery } from "../../slice/QueueApi"; +import { useGetOwnedQueuesQuery } from "../../slice/QueueApi"; import "../styles.css"; import { Button, Spin } from "antd"; import { @@ -21,7 +21,7 @@ type Queue = { const QueuesList = (): JSX.Element => { const user = useSelector((state: StorePrototype) => state.auth.user); - const { data, refetch, isLoading } = useGetQueuesQuery({}); + const { data, refetch, isLoading } = useGetOwnedQueuesQuery({}); useEffect(() => { user && refetch(); }, [user]); diff --git a/frontend/app/src/components/user/AnonUserCard.tsx b/frontend/app/src/components/user/AnonUserCard.tsx index 25cd5d4..1f0389d 100644 --- a/frontend/app/src/components/user/AnonUserCard.tsx +++ b/frontend/app/src/components/user/AnonUserCard.tsx @@ -1,9 +1,12 @@ -import React from "react"; -import { AnonUser } from "../../slice/AuthApi"; +import React, { useContext } from "react"; +import { QueueUser } from "../../slice/AuthApi"; import "../styles.css"; import tr from "../../config/translation"; -import Title from "antd/es/typography/Title"; -import Paragraph from "antd/es/typography/Paragraph"; +import { useSelector } from "react-redux"; +import { StorePrototype } from "../../config/store"; +import { Button } from "antd"; +import { Queue, usePassQueueActionMutation } from "../../slice/QueueApi"; +import { MessageContext } from "../../App"; const UUIDToColor = (uuid: string): string => { return ( @@ -20,24 +23,74 @@ const getProfileText = (name: string): string => { }; const AnonUserCard = (props: { - user: AnonUser; - backlight?: "self" | "active" | undefined; + queueUser: QueueUser; + queue: Queue | undefined; }): JSX.Element => { + const messageApi = useContext(MessageContext); + + const clientId = useSelector((state: StorePrototype) => state.auth.clientId); + const [passAction] = usePassQueueActionMutation(); + + const passQueue = () => { + if (props.queue) { + passAction(props.queue.id) + .unwrap() + .then(() => messageApi.success(tr("You passed"))) + .catch(() => messageApi.error(tr("Failed to pass"))); + } + }; + return (
+ #{props.queueUser.position}
- {props.user.name - ? getProfileText(props.user.name) - : props.user.id.substring(0, 2)} + {props.queueUser.user.name + ? getProfileText(props.queueUser.user.name) + : props.queueUser.id.substring(0, 2)}

- {props.user.name - ? props.user.name - : tr("Anonymous") + " #" + props.user.id.substring(0, 4)} + {props.queueUser.user.name + ? props.queueUser.user.name + : tr("Anonymous") + " #" + props.queueUser.id.substring(0, 4)}

+ {props.queueUser && clientId === props.queueUser.user.id && ( + + {tr("YOU")} + + )} + {props.queueUser && + clientId === props.queueUser.user.id && + props.queueUser.position === 0 && ( + + {tr("It is your turn!")} + + )} + {props.queueUser && + clientId === props.queueUser.user.id && + props.queueUser.position === 0 && ( + + )} +

); }; diff --git a/frontend/app/src/config/store.ts b/frontend/app/src/config/store.ts index 3af843d..ef37368 100644 --- a/frontend/app/src/config/store.ts +++ b/frontend/app/src/config/store.ts @@ -12,7 +12,7 @@ import { NewsApi } from "../slice/NewsApi"; export type AuthDataType = { token: string | null; clientId: string | null; - user: { name: string | null; username: string } | null; + user: { id: string | null; name: string | null; username: string } | null; }; const initialAuthDataState: AuthDataType = { diff --git a/frontend/app/src/config/translationMap.json b/frontend/app/src/config/translationMap.json index c0bb3dd..e91d10f 100644 --- a/frontend/app/src/config/translationMap.json +++ b/frontend/app/src/config/translationMap.json @@ -115,5 +115,56 @@ }, "Invalid captcha": { "ru": "Неверная капча" + }, + "YOU": { + "ru": "ВЫ" + }, + "Current queues": { + "ru": "Текущие очереди" + }, + "Queues you are in": { + "ru": "Очереди, в которых вы участвуете" + }, + "Queue participants": { + "ru": "Участники очереди" + }, + "Anonymous": { + "ru": "Аноним" + }, + "Join": { + "ru": "Присоединиться" + }, + "QR-code scanner in development": { + "ru": "Сканер QR-кодов в разработке" + }, + "OR": { + "ru": "ИЛИ" + }, + "Join queue by link or id": { + "ru": "Присоединиться к очереди по ссылке или айди" + }, + "You are not parting in any queues!": { + "ru": "Вы не принимаете участие ни в одной очереди!" + }, + "Created": { + "ru": "Создана" + }, + "Waiting": { + "ru": "Ожидает" + }, + "In progress": { + "ru": "В процессе" + }, + "Finished": { + "ru": "Завершена" + }, + "Kick first": { + "ru": "Кикнуть первого" + }, + "It is your turn!": { + "ru": "Сейчас ваша очередь!" + }, + "Pass": { + "ru": "Пройти" } -} \ No newline at end of file +} diff --git a/frontend/app/src/pages/AppRoutes.tsx b/frontend/app/src/pages/AppRoutes.tsx index 28e7ad5..29e1f27 100644 --- a/frontend/app/src/pages/AppRoutes.tsx +++ b/frontend/app/src/pages/AppRoutes.tsx @@ -17,6 +17,7 @@ import CreateNewsPage from "./CreateNewsPage"; import QueueCard from "../components/queue/QueueCard"; import JoinQueuePage from "./JoinQueuePage"; import ApproveQueueJoinPage from "./ApproveQueueJoinPage"; +import PartingQueuesPage from "./PartingQueuesPage"; const AppRoutes = ({ children }: { children: ReactNode }) => { store.dispatch(getLocalToken()); @@ -30,6 +31,7 @@ const AppRoutes = ({ children }: { children: ReactNode }) => { } /> } /> + } /> } /> } /> } /> diff --git a/frontend/app/src/pages/PartingQueuesPage.tsx b/frontend/app/src/pages/PartingQueuesPage.tsx new file mode 100644 index 0000000..816c1b2 --- /dev/null +++ b/frontend/app/src/pages/PartingQueuesPage.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import "./styles.css"; +import { useSelector } from "react-redux"; +import { StorePrototype } from "../config/store"; +import tr from "../config/translation"; +import Title from "antd/es/typography/Title"; +import PartingQueuesList from "../components/queue/PartingQueuesList"; + +const PartingQueuesPage = () => { + const clientId = useSelector((state: StorePrototype) => state.auth.clientId); + return clientId ? ( + + ) : ( + {tr("Whoops!")} + ); +}; + +export default PartingQueuesPage; diff --git a/frontend/app/src/slice/AuthApi.ts b/frontend/app/src/slice/AuthApi.ts index b1d5ff4..99f0182 100644 --- a/frontend/app/src/slice/AuthApi.ts +++ b/frontend/app/src/slice/AuthApi.ts @@ -3,6 +3,7 @@ import { baseUrl } from "../config/baseUrl"; import { RootState } from "../config/store"; export interface User { + id: string; username: string; name: string; } @@ -24,6 +25,13 @@ export type TokenResponse = { token_type: string; }; +export type QueueUser = { + id: string; + position: number; + passed: boolean; + user: AnonUser; +}; + export type AnonUser = { id: string; name: string; diff --git a/frontend/app/src/slice/QueueApi.ts b/frontend/app/src/slice/QueueApi.ts index 462a41f..33256be 100644 --- a/frontend/app/src/slice/QueueApi.ts +++ b/frontend/app/src/slice/QueueApi.ts @@ -1,7 +1,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { baseUrl } from "../config/baseUrl"; import { RootState } from "../config/store"; -import { AnonUser } from "./AuthApi"; +import { QueueUser } from "./AuthApi"; export type CreateQueueRequest = { name: string; @@ -18,10 +18,11 @@ export type QueueDetail = { id: string; name: string; description: string | null; + status: string; participants: { total: number; remaining: number; - users_list: [AnonUser]; + users_list: [QueueUser]; }; }; @@ -42,9 +43,12 @@ export const QueueApi = createApi({ }, }), endpoints: (builder) => ({ - getQueues: builder.query<[Queue], undefined>({ + getQueues: builder.query<[Queue], unknown>({ query: () => "/", }), + getOwnedQueues: builder.query<[Queue], unknown>({ + query: () => "/owned", + }), getQueueDetail: builder.query({ query: (queueId: string | undefined) => `/${queueId}`, }), @@ -58,12 +62,34 @@ export const QueueApi = createApi({ body: data, }), }), + passQueueAction: builder.mutation({ + query: (queueId: string) => ({ + url: `/${queueId}/action/pass`, + method: "POST", + }), + }), + kickFirstAction: builder.mutation({ + query: (queueId: string) => ({ + url: `/${queueId}/action/kick-first`, + method: "POST", + }), + }), + startQueueAction: builder.mutation({ + query: (queueId: string) => ({ + url: `/${queueId}/action/start`, + method: "POST", + }), + }), }), }); export const { useGetQueuesQuery, + useGetOwnedQueuesQuery, useGetQueueDetailQuery, useJoinQueueMutation, useCreateQueueMutation, + usePassQueueActionMutation, + useKickFirstActionMutation, + useStartQueueActionMutation, } = QueueApi;