Edit and save order pools and orders

This commit is contained in:
Johnny Gear 2026-01-29 15:30:51 -06:00
parent 1f94855504
commit ca3cbb6e54
8 changed files with 398 additions and 92 deletions

View file

@ -25,6 +25,9 @@ def user_get(username):
def orders_pool_list(user_id):
return OrdersPool.select().where(OrdersPool.user_id == user_id)
def orders_pool(user_id, set_id):
return OrdersPool.get(OrdersPool.user_id == user_id, OrdersPool.id == set_id)
def domsubusers_add(sub, dom):
return DomSubUsers.create(
sub=sub,

View file

@ -1,6 +1,7 @@
from flask import Blueprint, jsonify, abort
from flask import Blueprint, jsonify, abort, request
from flask_login import current_user
from db.queries import user_get, domsubusers_list, orders_pool_list
from db.models import database, Order
from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool
api = Blueprint('api', __name__)
@ -14,19 +15,70 @@ def subs():
]
)
@api.route('/subs/<username>/orders')
def sub_orders(username):
@api.route('/subs/<username>/sets')
def sub_order_sets(username):
try:
sub = user_get(username)
except:
abort(500)
abort(403)
return
if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]:
abort(500)
abort(403)
return
return jsonify([
{
'id': op.id,
'name': op.name,
}
for op
in orders_pool_list(sub.id)
])
@api.route('/subs/<username>/sets/<set_id>', methods = ['GET', 'POST'])
def sub_order_set(username, set_id):
try:
sub = user_get(username)
except:
abort(403)
return
if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]:
abort(403)
return
op = orders_pool(sub.id, set_id)
if request.method == 'POST':
with database.atomic() as transaction:
try:
op.name = request.json['name']
op.save()
for updated_order in request.json['orders']:
if isinstance(updated_order['id'], int):
order_to_update = op.orders.where(Order.id == updated_order['id']).get()
if '_delete' in updated_order:
order_to_update.delete_instance(recursive=True)
else:
order_to_update.name = updated_order['name']
order_to_update.weight = updated_order['weight']
order_to_update.repeat = updated_order['repeat']
order_to_update.save()
else:
Order.create(
pool=op,
name=updated_order['name'],
weight=updated_order['weight'],
repeat=updated_order['repeat']
)
except:
transaction.rollback()
return jsonify(
{
'id': op.id,
'name': op.name,
@ -34,9 +86,20 @@ def sub_orders(username):
'id': order.id,
'name': order.name,
'weight': order.weight,
'repeat': order.repeat
'repeat': order.repeat,
'add_ons': [
{
'id': 1,
'name': 'Leg Shackles',
'probability': 0.9
},
{
'id': 2,
'name': 'Leather Collar',
'probability': 0.5
}
]
} for order in op.orders]
}
for op
in orders_pool_list(sub.id)
])
)

View file

@ -15,6 +15,8 @@
},
"dependencies": {
"@mantine/core": "^8.3.12",
"@mantine/form": "^8.3.13",
"@tabler/icons-react": "^3.36.1",
"postcss": "^8.5.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",

View file

@ -0,0 +1,226 @@
import {
Container,
Paper,
Input,
Slider,
TextInput,
Button,
Divider,
Flex,
ActionIcon,
} from "@mantine/core";
import { randomId } from "@mantine/hooks";
import { useForm } from "@mantine/form";
import React from "react";
import { Params, useLoaderData, useParams, useNavigate } from "react-router";
import { IconMinus, IconPlus } from "@tabler/icons-react";
export const subOrderSetLoader = async ({
params: { username, set_id },
}: {
params: Params<string>;
}) =>
fetch(`/api/subs/${username}/sets/${set_id}`).then((response) =>
response.json()
);
type FormOrderSetOrderAddOn = Omit<OrderSetOrderAddOn, "id"> & {
id: number | string;
_delete?: boolean;
};
type FormOrderSetOrder = Omit<OrderSetOrder, "id" | "add_ons"> & {
id: number | string;
_delete?: boolean;
add_ons: FormOrderSetOrderAddOn[];
};
type FormOrderSet = Omit<OrderSet, "orders"> & {
orders: FormOrderSetOrder[];
};
export const SubOrderSet: React.FC = () => {
const navigate = useNavigate();
const { username, set_id } = useParams();
const { id, name, orders } = useLoaderData<OrderSet>();
const form = useForm<FormOrderSet>({
mode: "uncontrolled",
initialValues: { id, name, orders },
validate: {
name: (value) => (value.length < 1 ? "Please enter a name" : null),
orders: {
name: (value) => (value.length < 1 ? "Please enter a name" : null),
add_ons: {
name: (value) => (value.length < 1 ? "Please enter a name" : null),
},
},
},
});
const handleSubmit = React.useCallback(
form.onSubmit((values) => {
fetch(`/api/subs/${username}/sets/${set_id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
}).then((response) => {
if (response.ok) {
navigate(`/dashboard/subs/${username}`);
} else {
console.error(response.statusText);
}
});
}),
[form]
);
const handleNewOrder = React.useCallback(() => {
form.insertListItem("orders", {
id: `new_${randomId()}`,
name: "",
weight: 10,
repeat: 0.8,
add_ons: [],
});
}, [form]);
const handleRemoveOrder = React.useCallback(
(idx: number) => {
const orderId = form.getValues().orders[idx].id.toString();
if (orderId.indexOf("new_") === 0) {
form.removeListItem("orders", idx);
} else {
form.setFieldValue(`orders.${idx}._delete`, true);
}
},
[form]
);
const handleNewAddOn = React.useCallback(
(idx: number) => {
form.insertListItem(`orders.${idx}.add_ons`, {
id: `new_${randomId()}`,
name: "",
probability: 0.5,
});
},
[form]
);
const handleRemoveAddOn = React.useCallback(
(idx: number, add_on_idx: number) => {
const addOnId = form
.getValues()
.orders[idx].add_ons[add_on_idx].id.toString();
if (addOnId.indexOf("new_") === 0) {
form.removeListItem(`orders.${idx}.add_ons`, add_on_idx);
} else {
form.setFieldValue(`orders.${idx}.add_ons.${add_on_idx}._delete`, true);
}
},
[form]
);
return (
<form onSubmit={handleSubmit}>
<Container pb="xl">
<h1>{name}</h1>
<>
<TextInput {...form.getInputProps("name")} label="Name" />
{form.getValues().orders.map(({ id: order_id, _delete }, idx) =>
_delete ? null : (
<Paper key={order_id} bg="gray.2">
<Flex gap="xl" justify="space-between">
<TextInput
{...form.getInputProps(`orders.${idx}.name`)}
label="Name"
flex={1}
/>
<ActionIcon
color="red.8"
onClick={() => handleRemoveOrder(idx)}
>
<IconMinus />
</ActionIcon>
</Flex>
<Input.Wrapper
label="Weight"
description="Chance this order will be selected"
>
<Slider
{...form.getInputProps(`orders.${idx}.weight`)}
domain={[0, 100]}
/>
</Input.Wrapper>
<Input.Wrapper
label="Repeat"
description="Percent chance that this order will repeat the next day"
>
<Slider
{...form.getInputProps(`orders.${idx}.repeat`)}
domain={[0, 1]}
step={0.05}
/>
</Input.Wrapper>
<h4>Add-On</h4>
{form
.getValues()
.orders[idx].add_ons.map(
({ id: add_on_id, _delete: _add_on_delete }, add_on_idx) =>
_add_on_delete ? null : (
<Paper key={add_on_id} p="xs">
<Flex gap="xl" justify="space-between">
<TextInput
{...form.getInputProps(
`orders.${idx}.add_ons.${add_on_idx}.name`
)}
label="Name"
/>
<ActionIcon
color="red.8"
onClick={() => handleRemoveAddOn(idx, add_on_idx)}
>
<IconMinus />
</ActionIcon>
</Flex>
<Input.Wrapper label="Probability">
<Slider
{...form.getInputProps(
`orders.${idx}.add_ons.${add_on_idx}.probability`
)}
name="Probability"
domain={[0, 1]}
step={0.05}
/>
</Input.Wrapper>
</Paper>
)
)}
<Button onClick={() => handleNewAddOn(idx)}>
<IconPlus
style={{ width: "60%", height: "60%" }}
stroke={2}
/>
New Add On
</Button>
</Paper>
)
)}
<Button onClick={handleNewOrder}>
<IconPlus style={{ width: "60%", height: "60%" }} stroke={2} />
New Order
</Button>
<Divider my="md" />
<Flex justify="flex-end">
<Button type="submit">Save</Button>
</Flex>
</>
</Container>
</form>
);
};

View file

@ -0,0 +1,42 @@
import {
Container,
Accordion,
TextInput,
Divider,
Input,
Slider,
} from "@mantine/core";
import React from "react";
import { Link, Params, useLoaderData, useParams } from "react-router";
export const subOrderSetsLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/subs/${username}/sets`).then((response) => response.json());
export const SubOrderSets: React.FC = () => {
const { username: sub_username } = useParams();
const orders = useLoaderData<
{
id: number;
name: string;
orders: Pick<OrderSet, "id" | "name">[];
}[]
>();
return (
<Container>
<h1>Orders for {sub_username}</h1>
<Accordion>
{orders
? orders.map(({ id, name }) => (
<Link key={id} to={`/dashboard/subs/${sub_username}/${id}`}>
{name}
</Link>
))
: null}
</Accordion>
</Container>
);
};

View file

@ -1,76 +0,0 @@
import {
Container,
Accordion,
TextInput,
Divider,
Input,
Slider,
} from "@mantine/core";
import React from "react";
import { Params, useLoaderData, useParams } from "react-router";
export const subOrdersLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/subs/${username}/orders`).then((response) => response.json());
export const SubOrders: React.FC = () => {
const { username: sub_username } = useParams();
const orders = useLoaderData<
{
id: number;
name: string;
orders: {
id: number;
name: string;
weight: number;
repeat: number;
}[];
}[]
>();
return (
<Container>
<h1>Orders for {sub_username}</h1>
<Accordion>
{orders
? orders.map(({ id, name, orders }) => (
<Accordion.Item key={id} value={`${id}`}>
<Accordion.Control>{name}</Accordion.Control>
<Accordion.Panel>
<>
<TextInput label="Name" value={name} />
{orders.map(
({ id: order_id, name: order_name, weight, repeat }) => (
<React.Fragment key={order_id}>
<Divider my="md" />
<TextInput label="Name" value={order_name} />
<Input.Wrapper
label="Weight"
description="Chance this order will be selected"
>
<Slider domain={[0, 100]} value={weight} />
</Input.Wrapper>
<Input.Wrapper
label="Repeat"
description="Percent chance that this order will repeat the next day"
>
<Slider
domain={[0, 1]}
step={0.05}
value={repeat}
/>
</Input.Wrapper>
</React.Fragment>
)
)}
</>
</Accordion.Panel>
</Accordion.Item>
))
: null}
</Accordion>
</Container>
);
};

19
flask/vite/src/index.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
type OrderSetOrderAddOn = {
id: number;
name: string;
probability: number;
}
type OrderSetOrder = {
id: number;
name: string;
weight: number;
repeat: number;
add_ons: OrderSetOrderAddOn[];
}
type OrderSet = {
id: number;
name: string;
orders: OrderSetOrder[];
}

View file

@ -2,21 +2,43 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { createTheme, MantineProvider, Input } from "@mantine/core";
import {
createTheme,
MantineProvider,
Input,
Paper,
Slider,
} from "@mantine/core";
import "@mantine/core/styles.css";
import { SubsList, subsListLoader } from "./SubsList";
import { SubOrders, subOrdersLoader } from "./SubOrders";
import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
import { SubOrderSet, subOrderSetLoader } from "./SubOrderSet";
const theme = createTheme({
components: {
InputWrapper: Input.Wrapper.extend({
defaultProps: {
inputWrapperOrder: ["label", "input", "description", "error"],
my: "md",
mb: "xs",
},
}),
Paper: Paper.extend({
defaultProps: {
p: "sm",
shadow: "xs",
mb: "lg",
},
}),
Slider: Slider.extend({
vars: (theme) =>
({
root: {
"--slider-track-bg": theme.colors.gray[4],
},
} as any),
}),
},
});
@ -27,9 +49,14 @@ const router = createBrowserRouter([
loader: subsListLoader,
},
{
path: "dashboard/subs/:username",
Component: SubOrders,
loader: subOrdersLoader,
path: "dashboard/subs/:username/",
Component: SubOrderSets,
loader: subOrderSetsLoader,
},
{
path: "dashboard/subs/:username/:set_id",
Component: SubOrderSet,
loader: subOrderSetLoader,
},
]);