groups functional & bugfixes
This commit is contained in:
@ -76,6 +76,7 @@ class Queue(Base):
|
|||||||
|
|
||||||
users = relationship("QueueUser", backref="queue", lazy="dynamic")
|
users = relationship("QueueUser", backref="queue", lazy="dynamic")
|
||||||
logs = relationship("QueueLog", backref="queue", lazy="dynamic")
|
logs = relationship("QueueLog", backref="queue", lazy="dynamic")
|
||||||
|
groups = relationship("QueueGroup", backref="queue", lazy="dynamic")
|
||||||
|
|
||||||
|
|
||||||
class QueueUser(Base):
|
class QueueUser(Base):
|
||||||
@ -86,6 +87,7 @@ class QueueUser(Base):
|
|||||||
queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id"))
|
queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id"))
|
||||||
position = Column(Integer)
|
position = Column(Integer)
|
||||||
passed = Column(Boolean, default=False)
|
passed = Column(Boolean, default=False)
|
||||||
|
group_id = Column(UUID(as_uuid=True), ForeignKey("queuegroup.id"), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class QueueLog(Base):
|
class QueueLog(Base):
|
||||||
@ -97,6 +99,17 @@ class QueueLog(Base):
|
|||||||
created = Column(DateTime, default=datetime.datetime.utcnow)
|
created = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class QueueGroup(Base):
|
||||||
|
__tablename__ = "queuegroup"
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
name = Column(String)
|
||||||
|
priority = Column(Integer)
|
||||||
|
queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id"))
|
||||||
|
|
||||||
|
users = relationship("QueueUser", backref="group", lazy="dynamic")
|
||||||
|
|
||||||
|
|
||||||
class Captcha(Base):
|
class Captcha(Base):
|
||||||
__tablename__ = "captcha"
|
__tablename__ = "captcha"
|
||||||
|
|
||||||
|
|||||||
@ -96,17 +96,21 @@ def get_current_user(
|
|||||||
|
|
||||||
|
|
||||||
def get_current_user_or_none(
|
def get_current_user_or_none(
|
||||||
token: Annotated[str, Depends(oauth2_scheme)],
|
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
|
authorization: Annotated[Union[str, None], Header()] = None,
|
||||||
) -> Union[schemas.UserInDB, None]:
|
) -> Union[schemas.UserInDB, None]:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
if authorization:
|
||||||
token, jwt_config.SECRET_KEY, algorithms=[jwt_config.ALGORITHM]
|
token = authorization.split()[1]
|
||||||
)
|
payload = jwt.decode(
|
||||||
username: str = payload.get("sub")
|
token, jwt_config.SECRET_KEY, algorithms=[jwt_config.ALGORITHM]
|
||||||
if username is None:
|
)
|
||||||
raise credentials_exception
|
username: str = payload.get("sub")
|
||||||
token_data = schemas.TokenData(username=username)
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
token_data = schemas.TokenData(username=username)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
except JWTError:
|
except JWTError:
|
||||||
return None
|
return None
|
||||||
user = get_user_by_username(db, username=token_data.username)
|
user = get_user_by_username(db, username=token_data.username)
|
||||||
|
|||||||
@ -4,10 +4,23 @@ from uuid import UUID
|
|||||||
from ..auth import schemas as auth_schemas
|
from ..auth import schemas as auth_schemas
|
||||||
|
|
||||||
|
|
||||||
|
class QueueGroup(BaseModel):
|
||||||
|
name: str
|
||||||
|
priority: int
|
||||||
|
|
||||||
|
|
||||||
|
class QueueGroupDetail(QueueGroup):
|
||||||
|
id: UUID
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class QueueUser(BaseModel):
|
class QueueUser(BaseModel):
|
||||||
id: UUID
|
id: UUID
|
||||||
position: int
|
position: int
|
||||||
passed: bool
|
passed: bool
|
||||||
|
group_id: UUID | None = None
|
||||||
user: auth_schemas.AnonUser
|
user: auth_schemas.AnonUser
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -23,6 +36,7 @@ class ParticipantInfo(BaseModel):
|
|||||||
class Queue(BaseModel):
|
class Queue(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: Union[str, None] = None
|
description: Union[str, None] = None
|
||||||
|
groups: List[QueueGroup] | None = None
|
||||||
|
|
||||||
|
|
||||||
class QueueInList(Queue):
|
class QueueInList(Queue):
|
||||||
@ -32,8 +46,10 @@ class QueueInList(Queue):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class QueueInDb(Queue):
|
class QueueInDb(BaseModel):
|
||||||
id: UUID
|
id: UUID
|
||||||
|
name: str
|
||||||
|
description: Union[str, None] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@ -44,6 +60,7 @@ class QueueDetail(Queue):
|
|||||||
status: str
|
status: str
|
||||||
owner_id: UUID
|
owner_id: UUID
|
||||||
participants: ParticipantInfo
|
participants: ParticipantInfo
|
||||||
|
groups: List[QueueGroupDetail] | None
|
||||||
|
|
||||||
|
|
||||||
class ActionResult(BaseModel):
|
class ActionResult(BaseModel):
|
||||||
@ -52,3 +69,7 @@ class ActionResult(BaseModel):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class JoinRequest(BaseModel):
|
||||||
|
group_id: UUID | None = None
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
from sqlalchemy import func
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
import redis
|
import redis
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -49,6 +50,14 @@ def create_queue(
|
|||||||
)
|
)
|
||||||
db.add(q)
|
db.add(q)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
if new_queue.groups:
|
||||||
|
db.add_all(
|
||||||
|
instances=[
|
||||||
|
models.QueueGroup(name=qg.name, priority=qg.priority, queue_id=q.id)
|
||||||
|
for qg in new_queue.groups
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
return schemas.QueueInDb.model_validate(q)
|
return schemas.QueueInDb.model_validate(q)
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +73,7 @@ def get_detailed_queue(
|
|||||||
description=q.description,
|
description=q.description,
|
||||||
status=q.status,
|
status=q.status,
|
||||||
owner_id=q.owner_id,
|
owner_id=q.owner_id,
|
||||||
|
groups=q.groups.order_by(models.QueueGroup.priority.asc()),
|
||||||
participants=schemas.ParticipantInfo(
|
participants=schemas.ParticipantInfo(
|
||||||
total=q.users.count(),
|
total=q.users.count(),
|
||||||
remaining=q.users.filter(models.QueueUser.passed == False).count(),
|
remaining=q.users.filter(models.QueueUser.passed == False).count(),
|
||||||
@ -80,6 +90,7 @@ def get_detailed_queue(
|
|||||||
|
|
||||||
async def join_queue(
|
async def join_queue(
|
||||||
queue_id: UUID,
|
queue_id: UUID,
|
||||||
|
join_request: schemas.JoinRequest | None,
|
||||||
client: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)],
|
client: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)],
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
r: Annotated[redis.client.Redis, Depends(get_redis)],
|
r: Annotated[redis.client.Redis, Depends(get_redis)],
|
||||||
@ -90,10 +101,18 @@ async def join_queue(
|
|||||||
last_qu = q.users.order_by(models.QueueUser.position.desc()).first()
|
last_qu = q.users.order_by(models.QueueUser.position.desc()).first()
|
||||||
position = last_qu.position + 1 if last_qu else 0
|
position = last_qu.position + 1 if last_qu else 0
|
||||||
new_qu = models.QueueUser(
|
new_qu = models.QueueUser(
|
||||||
user_id=client.id, queue_id=q.id, position=position
|
user_id=client.id,
|
||||||
|
queue_id=q.id,
|
||||||
|
position=position,
|
||||||
|
group_id=(
|
||||||
|
join_request.group_id
|
||||||
|
if join_request and join_request.group_id
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
db.add(new_qu)
|
db.add(new_qu)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
await rebuild_queue(queue=q, db=db)
|
||||||
await r.publish(str(queue_id), "updated")
|
await r.publish(str(queue_id), "updated")
|
||||||
return new_qu
|
return new_qu
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -129,7 +148,7 @@ async def get_queue_owner(
|
|||||||
async def verify_queue_owner(
|
async def verify_queue_owner(
|
||||||
queue_owner: Annotated[auth_schemas.UserInDB, Depends(get_queue_owner)],
|
queue_owner: Annotated[auth_schemas.UserInDB, Depends(get_queue_owner)],
|
||||||
current_user: Annotated[
|
current_user: Annotated[
|
||||||
auth_schemas.UserInDB, Depends(auth_services.get_current_user)
|
auth_schemas.UserInDB, Depends(auth_services.get_current_user_or_none)
|
||||||
],
|
],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return queue_owner.id == current_user.id if queue_owner and current_user else False
|
return queue_owner.id == current_user.id if queue_owner and current_user else False
|
||||||
@ -139,11 +158,32 @@ async def rebuild_queue(
|
|||||||
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
|
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
):
|
):
|
||||||
for i, qu in enumerate(
|
query = (
|
||||||
queue.users.filter(models.QueueUser.passed == False).order_by(
|
db.query(models.QueueUser)
|
||||||
models.QueueUser.position.asc()
|
.join(
|
||||||
|
models.QueueGroup,
|
||||||
|
models.QueueUser.group_id == models.QueueGroup.id,
|
||||||
|
isouter=True,
|
||||||
)
|
)
|
||||||
):
|
.filter(models.QueueUser.passed == False, models.QueueUser.queue_id == queue.id)
|
||||||
|
.order_by(
|
||||||
|
func.coalesce(models.QueueGroup.priority, 0).asc(),
|
||||||
|
models.QueueUser.position.asc(),
|
||||||
|
)
|
||||||
|
.options(joinedload(models.QueueUser.group))
|
||||||
|
)
|
||||||
|
queueusers = query.all()
|
||||||
|
first_qu_found_and_queue_in_process = False
|
||||||
|
if queue.status == "active":
|
||||||
|
for i, qu in enumerate(queueusers):
|
||||||
|
if qu.position == 0:
|
||||||
|
first_qu_found_and_queue_in_process = True
|
||||||
|
del queueusers[i]
|
||||||
|
break
|
||||||
|
for i, qu in enumerate(queueusers):
|
||||||
|
if first_qu_found_and_queue_in_process:
|
||||||
|
setattr(qu, "position", i + 1)
|
||||||
|
continue
|
||||||
setattr(qu, "position", i)
|
setattr(qu, "position", i)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import "../styles.css";
|
import "../styles.css";
|
||||||
import { Button, Input, Spin } from "antd";
|
import { Button, Input, Select, Spin } from "antd";
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
@ -30,9 +30,10 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
|||||||
const [joinQueue, { isLoading }] = useJoinQueueMutation();
|
const [joinQueue, { isLoading }] = useJoinQueueMutation();
|
||||||
const [patchAnon] = usePatchAnonMutation();
|
const [patchAnon] = usePatchAnonMutation();
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState<string>();
|
||||||
|
|
||||||
const onJoinButtonClick = () => {
|
const onJoinButtonClick = () => {
|
||||||
joinQueue(props.id)
|
joinQueue({ queueId: props.id, data: { group_id: selectedGroup } })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => navigate(`/queue/${props.id}`))
|
.then(() => navigate(`/queue/${props.id}`))
|
||||||
.then(() => refetch())
|
.then(() => refetch())
|
||||||
@ -63,20 +64,20 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="queue-info">
|
<div className="queue-info">
|
||||||
<Title level={3} style={{ textAlign: "left" }}>
|
|
||||||
{data?.name}
|
|
||||||
</Title>
|
|
||||||
<p>
|
|
||||||
<FileTextOutlined />
|
|
||||||
{" "}
|
|
||||||
{data?.description}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<UserOutlined />
|
|
||||||
{" "}
|
|
||||||
{data?.participants?.remaining} / {data?.participants?.total}
|
|
||||||
</p>
|
|
||||||
<Spin spinning={isLoading}>
|
<Spin spinning={isLoading}>
|
||||||
|
<Title level={3} style={{ textAlign: "left" }}>
|
||||||
|
{data?.name}
|
||||||
|
</Title>
|
||||||
|
<p>
|
||||||
|
<FileTextOutlined />
|
||||||
|
{" "}
|
||||||
|
{data?.description}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<UserOutlined />
|
||||||
|
{" "}
|
||||||
|
{data?.participants?.remaining} / {data?.participants?.total}
|
||||||
|
</p>
|
||||||
<Title level={4}>{tr("Update your name")}</Title>
|
<Title level={4}>{tr("Update your name")}</Title>
|
||||||
<div style={{ display: "flex", flexFlow: "row", width: "30vw" }}>
|
<div style={{ display: "flex", flexFlow: "row", width: "30vw" }}>
|
||||||
<Input
|
<Input
|
||||||
@ -88,6 +89,22 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
|||||||
{tr("Update")}
|
{tr("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{data?.groups.length !== undefined && data?.groups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title level={4}>{tr("Select group in queue")}</Title>
|
||||||
|
<Select
|
||||||
|
options={data?.groups.map((obj) => ({
|
||||||
|
value: obj.id,
|
||||||
|
label: obj.name,
|
||||||
|
}))}
|
||||||
|
placeholder={tr("Select group...")}
|
||||||
|
value={selectedGroup}
|
||||||
|
style={{ width: "30vw", marginTop: "1rem" }}
|
||||||
|
onChange={setSelectedGroup}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={{ width: "100%", marginTop: "2rem" }}
|
style={{ width: "100%", marginTop: "2rem" }}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext, useReducer, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
CreateQueue,
|
||||||
CreateQueueRequest,
|
CreateQueueRequest,
|
||||||
Queue,
|
Queue,
|
||||||
useCreateQueueMutation,
|
useCreateQueueMutation,
|
||||||
} from "../../slice/QueueApi";
|
} from "../../slice/QueueApi";
|
||||||
import "../styles.css";
|
import "../styles.css";
|
||||||
import { Button, Form, Input, Spin } from "antd";
|
import { Button, Form, Input, List, Spin, Tooltip, Typography } from "antd";
|
||||||
import Title from "antd/es/typography/Title";
|
import Title from "antd/es/typography/Title";
|
||||||
import tr from "../../config/translation";
|
import tr from "../../config/translation";
|
||||||
import { MessageContext } from "../../App";
|
import { MessageContext } from "../../App";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
QuestionCircleOutlined,
|
||||||
|
UpOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
const CreateQueueCard = (): JSX.Element => {
|
const CreateQueueCard = (): JSX.Element => {
|
||||||
const messageApi = useContext(MessageContext);
|
const messageApi = useContext(MessageContext);
|
||||||
@ -19,14 +26,95 @@ const CreateQueueCard = (): JSX.Element => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [createQueue, { isLoading }] = useCreateQueueMutation();
|
const [createQueue, { isLoading }] = useCreateQueueMutation();
|
||||||
|
|
||||||
const submit = (formData: CreateQueueRequest) => {
|
const [newGroupValue, setNewGroupValue] = useState("");
|
||||||
createQueue(formData)
|
const [groupsList, setGroupsList] = useState<[{ name: string; id: string }]>([
|
||||||
|
{ name: tr("Default"), id: "default" },
|
||||||
|
]);
|
||||||
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const pushGroupUp = (element: { name: string; id: string }) => {
|
||||||
|
const index = groupsList.indexOf(element);
|
||||||
|
if (index > 0) {
|
||||||
|
const newArr = groupsList;
|
||||||
|
const temp = newArr[index - 1];
|
||||||
|
newArr[index - 1] = element;
|
||||||
|
newArr[index] = temp;
|
||||||
|
setGroupsList(newArr);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushGroupDown = (element: { name: string; id: string }) => {
|
||||||
|
const index = groupsList.indexOf(element);
|
||||||
|
if (index < groupsList.length - 1) {
|
||||||
|
const newArr = groupsList;
|
||||||
|
const temp = newArr[index + 1];
|
||||||
|
newArr[index + 1] = element;
|
||||||
|
newArr[index] = temp;
|
||||||
|
setGroupsList(newArr);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGroup = (element: { name: string; id: string }) => {
|
||||||
|
if (groupsList.includes(element)) {
|
||||||
|
const newArr = groupsList;
|
||||||
|
newArr.splice(groupsList.indexOf(element), 1);
|
||||||
|
setGroupsList(newArr);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrderedGroup = { name: string; priority: number };
|
||||||
|
|
||||||
|
const getOrderedGroups = () => {
|
||||||
|
const divId = groupsList.findIndex((ele) => ele.id === "default");
|
||||||
|
if (divId || divId === 0) {
|
||||||
|
const arr1: OrderedGroup[] = [];
|
||||||
|
groupsList
|
||||||
|
.slice(0, divId)
|
||||||
|
.reverse()
|
||||||
|
.forEach((ele, i) => arr1.push({ name: ele.name, priority: -(i + 1) }));
|
||||||
|
const arr2: OrderedGroup[] = [];
|
||||||
|
groupsList
|
||||||
|
.slice(divId + 1)
|
||||||
|
.forEach((ele, i) => arr2.push({ name: ele.name, priority: i + 1 }));
|
||||||
|
return [...arr1, ...arr2];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
getOrderedGroups();
|
||||||
|
|
||||||
|
const submit = (formData: CreateQueue) => {
|
||||||
|
createQueue({ ...formData, groups: getOrderedGroups() })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then((data: Queue) => navigate(`/queue/${data.id}`))
|
.then((data: Queue) => navigate(`/queue/${data.id}`))
|
||||||
.then(() => messageApi.success(tr("Queue created")))
|
.then(() => messageApi.success(tr("Queue created")))
|
||||||
.catch(() => messageApi.error(tr("Failed to create queue")));
|
.catch(() => messageApi.error(tr("Failed to create queue")));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addGroupToList = () => {
|
||||||
|
if (newGroupValue) {
|
||||||
|
const tmp = groupsList;
|
||||||
|
if (tmp) {
|
||||||
|
tmp.push({
|
||||||
|
name: newGroupValue,
|
||||||
|
id: tmp.length.toString(),
|
||||||
|
});
|
||||||
|
setGroupsList(tmp);
|
||||||
|
} else {
|
||||||
|
setGroupsList((v) => [
|
||||||
|
{
|
||||||
|
name: newGroupValue,
|
||||||
|
id: v.length.toString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setNewGroupValue("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<Spin spinning={isLoading}>
|
<Spin spinning={isLoading}>
|
||||||
@ -55,6 +143,77 @@ const CreateQueueCard = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<List
|
||||||
|
dataSource={groupsList}
|
||||||
|
header={
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<Title level={4}>
|
||||||
|
{tr("Queue groups") + " " + tr("(optional)")}
|
||||||
|
</Title>
|
||||||
|
<Tooltip
|
||||||
|
title={tr(
|
||||||
|
"Queue's groups function allows you to define groups inside this particular queue. Groups have name and priority. Every participant selects theirs group for this queue, and their position is calculated by group's priority. Groups higher in the list will have higher priority."
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: "flex", gap: "1rem" }}>
|
||||||
|
<Input
|
||||||
|
placeholder={tr("Group's name")}
|
||||||
|
value={newGroupValue}
|
||||||
|
onChange={(e) => setNewGroupValue(e.target.value)}
|
||||||
|
onPressEnter={addGroupToList}
|
||||||
|
/>
|
||||||
|
<Button onClick={addGroupToList}>{tr("Add group")}</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
renderItem={(item, index) => (
|
||||||
|
<List.Item
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignContent: "space-between",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignContent: "space-between",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text>#{index + 1}</Typography.Text>
|
||||||
|
<Typography.Text>{item.name}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
{item.id !== "default" && (
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => pushGroupUp(item)}
|
||||||
|
icon={<UpOutlined />}
|
||||||
|
disabled={groupsList.indexOf(item) === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<DownOutlined />}
|
||||||
|
onClick={() => pushGroupDown(item)}
|
||||||
|
disabled={
|
||||||
|
groupsList.indexOf(item) === groupsList.length - 1
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => deleteGroup(item)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
></List>
|
||||||
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
icon={<PlusCircleOutlined />}
|
icon={<PlusCircleOutlined />}
|
||||||
|
|||||||
@ -182,7 +182,14 @@ const QueueCard = (): JSX.Element => {
|
|||||||
{tr("Queue participants")}
|
{tr("Queue participants")}
|
||||||
</Title>
|
</Title>
|
||||||
{data?.participants.users_list.map((v) => {
|
{data?.participants.users_list.map((v) => {
|
||||||
return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
|
return (
|
||||||
|
<AnonUserCard
|
||||||
|
key={v.id}
|
||||||
|
queueUser={v}
|
||||||
|
queue={data}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@ -122,4 +122,5 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 2px solid #00d8a4;
|
border: 2px solid #00d8a4;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const getProfileText = (name: string): string => {
|
|||||||
const AnonUserCard = (props: {
|
const AnonUserCard = (props: {
|
||||||
queueUser: QueueUser;
|
queueUser: QueueUser;
|
||||||
queue: QueueDetail | undefined;
|
queue: QueueDetail | undefined;
|
||||||
|
refetch: () => void;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const messageApi = useContext(MessageContext);
|
const messageApi = useContext(MessageContext);
|
||||||
|
|
||||||
@ -35,47 +36,49 @@ const AnonUserCard = (props: {
|
|||||||
if (props.queue) {
|
if (props.queue) {
|
||||||
passAction(props.queue.id)
|
passAction(props.queue.id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => messageApi.success(tr("You passed")))
|
.then(props.refetch)
|
||||||
.catch(() => messageApi.error(tr("Failed to pass")));
|
.then(() => messageApi.success(tr("You left the queue")))
|
||||||
|
.catch(() => messageApi.error(tr("Failed to left")));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="anon-card">
|
<div className="anon-card">
|
||||||
<span style={{ marginRight: "1rem" }}>#{props.queueUser.position}</span>
|
|
||||||
<div
|
<div
|
||||||
className="anon-circle"
|
style={{
|
||||||
style={{ background: UUIDToColor(props.queueUser.user.id) }}
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
alignItems: "center",
|
||||||
|
flexFlow: "row wrap",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{props.queueUser.user.name
|
<span style={{ marginRight: "1rem" }}>#{props.queueUser.position}</span>
|
||||||
? getProfileText(props.queueUser.user.name)
|
<div
|
||||||
: props.queueUser.id.substring(0, 2)}
|
className="anon-circle"
|
||||||
</div>
|
style={{ background: UUIDToColor(props.queueUser.user.id) }}
|
||||||
<p color="white" style={{ marginLeft: "10px" }}>
|
|
||||||
{props.queueUser.user.name
|
|
||||||
? props.queueUser.user.name
|
|
||||||
: tr("Anonymous") + " #" + props.queueUser.id.substring(0, 4)}
|
|
||||||
</p>
|
|
||||||
{props.queueUser && clientId === props.queueUser.user.id && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
background: "#00d8a4",
|
|
||||||
marginLeft: "1rem",
|
|
||||||
marginRight: "1rem",
|
|
||||||
marginBottom: "0",
|
|
||||||
borderRadius: "5px",
|
|
||||||
padding: "2px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tr("YOU")}
|
{props.queueUser.user.name
|
||||||
</span>
|
? getProfileText(props.queueUser.user.name)
|
||||||
)}
|
: props.queueUser.id.substring(0, 2)}
|
||||||
{props.queueUser &&
|
</div>
|
||||||
clientId === props.queueUser.user.id &&
|
<p color="white" style={{ marginLeft: "10px" }}>
|
||||||
props.queueUser.position === 0 &&
|
{props.queueUser.user.name
|
||||||
props.queue?.status === "active" && (
|
? props.queueUser.user.name
|
||||||
|
: tr("Anonymous") + " #" + props.queueUser.id.substring(0, 4)}
|
||||||
|
</p>
|
||||||
|
{props.queueUser.group_id && (
|
||||||
|
<p color="white" style={{ marginLeft: "10px" }}>
|
||||||
|
{tr("Group") +
|
||||||
|
": " +
|
||||||
|
props.queue?.groups.find(
|
||||||
|
(ele) => ele.id === props.queueUser.group_id
|
||||||
|
)?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{props.queueUser && clientId === props.queueUser.user.id && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
background: "#00d8a4",
|
||||||
marginLeft: "1rem",
|
marginLeft: "1rem",
|
||||||
marginRight: "1rem",
|
marginRight: "1rem",
|
||||||
marginBottom: "0",
|
marginBottom: "0",
|
||||||
@ -83,16 +86,43 @@ const AnonUserCard = (props: {
|
|||||||
padding: "2px",
|
padding: "2px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tr("It is your turn!")}
|
{tr("YOU")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{props.queueUser &&
|
{props.queueUser &&
|
||||||
clientId === props.queueUser.user.id &&
|
clientId === props.queueUser.user.id &&
|
||||||
(props.queueUser.position === 0 && props.queue?.status === "active" ? (
|
props.queueUser.position === 0 &&
|
||||||
<Button onClick={() => passQueue()}>{tr("Pass")}</Button>
|
props.queue?.status === "active" && (
|
||||||
) : (
|
<span
|
||||||
<Button onClick={() => passQueue()}>{tr("Leave")}</Button>
|
style={{
|
||||||
))}
|
marginLeft: "1rem",
|
||||||
|
marginRight: "1rem",
|
||||||
|
marginBottom: "0",
|
||||||
|
borderRadius: "5px",
|
||||||
|
padding: "2px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tr("It is your turn!")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
alignItems: "center",
|
||||||
|
flexFlow: "row wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.queueUser &&
|
||||||
|
clientId === props.queueUser.user.id &&
|
||||||
|
(props.queueUser.position === 0 &&
|
||||||
|
props.queue?.status === "active" ? (
|
||||||
|
<Button onClick={() => passQueue()}>{tr("Pass")}</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => passQueue()}>{tr("Leave")}</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export const darkTheme: ThemeConfig = {
|
|||||||
activeBorderColor: "#001529",
|
activeBorderColor: "#001529",
|
||||||
colorTextPlaceholder: "grey",
|
colorTextPlaceholder: "grey",
|
||||||
},
|
},
|
||||||
|
Select: {
|
||||||
|
colorTextPlaceholder: "grey",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export type QueueUser = {
|
|||||||
position: number;
|
position: number;
|
||||||
passed: boolean;
|
passed: boolean;
|
||||||
user: AnonUser;
|
user: AnonUser;
|
||||||
|
group_id: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AnonUser = {
|
export type AnonUser = {
|
||||||
|
|||||||
@ -3,9 +3,26 @@ import { baseUrl } from "../config/baseUrl";
|
|||||||
import { RootState } from "../config/store";
|
import { RootState } from "../config/store";
|
||||||
import { QueueUser } from "./AuthApi";
|
import { QueueUser } from "./AuthApi";
|
||||||
|
|
||||||
|
type OrderedGroup = {
|
||||||
|
name: string;
|
||||||
|
priority: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseGroup = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
priority: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateQueueRequest = {
|
export type CreateQueueRequest = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
groups: OrderedGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateQueue = {
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Queue = {
|
export type Queue = {
|
||||||
@ -20,6 +37,7 @@ export type QueueDetail = {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
owner_id: string;
|
owner_id: string;
|
||||||
|
groups: ResponseGroup[];
|
||||||
participants: {
|
participants: {
|
||||||
total: number;
|
total: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
@ -27,6 +45,10 @@ export type QueueDetail = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JoinRequest = {
|
||||||
|
group_id: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const QueueApi = createApi({
|
export const QueueApi = createApi({
|
||||||
reducerPath: "QueueApi",
|
reducerPath: "QueueApi",
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: fetchBaseQuery({
|
||||||
@ -54,7 +76,11 @@ export const QueueApi = createApi({
|
|||||||
query: (queueId: string | undefined) => `/${queueId}`,
|
query: (queueId: string | undefined) => `/${queueId}`,
|
||||||
}),
|
}),
|
||||||
joinQueue: builder.mutation({
|
joinQueue: builder.mutation({
|
||||||
query: (queueId: string) => ({ url: `/${queueId}/join`, method: "POST" }),
|
query: (args: { queueId: string; data: JoinRequest }) => ({
|
||||||
|
url: `/${args.queueId}/join`,
|
||||||
|
method: "POST",
|
||||||
|
body: args.data,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
createQueue: builder.mutation({
|
createQueue: builder.mutation({
|
||||||
query: (data: CreateQueueRequest) => ({
|
query: (data: CreateQueueRequest) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user