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}
+
+ }
+ type="primary"
+ size="large"
+ style={{ height: "100%", width: "4rem" }}
+ />
+
+
+ ))
+ ) : (
+ <>
+
{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;