product editor

This commit is contained in:
SimonFJ20 2025-03-19 15:15:00 +01:00
parent f36505a38e
commit e494277978
11 changed files with 549 additions and 14 deletions

View File

@ -3,8 +3,107 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="product_editor.js" type="module" defer></script> <script src="product_editor.js" type="module" defer></script>
<style>
body {
text-align: center;
margin: auto;
max-width: 1600px;
}
#product-list {
border-spacing: 0;
border: 1px solid black;
}
#product-list th {
text-align: left;
padding: 5px 10px;
background-color: #eee;
border-right: 1px solid lightgray;
border-bottom: 1px solid black;
}
#product-list th:last-child {
border-right: 0;
}
#product-list td {
border-top: 1px solid black;
border-right: 1px solid lightgray;
padding: 5px 10px;
}
#product-list td:last-child {
border-right: 0;
}
#product-list tr:nth-child(2n) td {
background-color: #eee;
}
fieldset {
background-color: #F9F9F9;
text-align: left;
margin-top: 1rem;
display: inline-block;
}
legend {
font-weight: bold;
font-size: 1.6rem;
}
#editor table {
margin-top: 0.5rem;
}
#editor td {
padding: 0.25rem;
vertical-align: top;
}
textarea {
font-family: inherit;
resize: vertical;
}
#editor input, #editor textarea {
width: 250px;
}
</style>
</head> </head>
<body> <body>
<h1>Product editor</h1> <h2>Products</h2>
<table id="product-list">
</table>
<fieldset>
<legend>Editor</legend>
<form id="editor">
<center>
<button id="load">Load</button>
<button id="save">Save</button>
<button id="new">New</button>
</center>
<table>
<tr>
<td><label for="product-id">id: </label></td>
<td>
<input id="product-id" type="text">
</td>
</tr>
<tr>
<td><label for="product-name">name: </label></td>
<td><input id="product-name" type="text"></td>
</tr>
<tr>
<td><label for="product-price">price (dkk): </label></td>
<td><input id="product-price" type="number" step="0.01"></td>
</tr>
<tr>
<td><label for="product-description">description: </label></td>
<td><textarea id="product-description"></textarea></td>
</tr>
<tr>
<td><label for="product-coord">coord_id: </label></td>
<td><input id="product-coord" type="text"></td>
</tr>
<tr>
<td><label for="product-barcode">barcode: </label></td>
<td><input id="product-barcode" type="text"></td>
</tr>
</table>
</form>
</fieldset>
</body> </body>
</html> </html>

View File

@ -1,3 +1,135 @@
console.log("hello world") const productList = document.querySelector("#product-list");
const editor = {
form: document.querySelector("#editor"),
loadButton: document.querySelector("#editor #load"),
saveButton: document.querySelector("#editor #save"),
newButton: document.querySelector("#editor #new"),
idInput: document.querySelector("#editor #product-id"),
nameInput: document.querySelector("#editor #product-name"),
priceInput: document.querySelector("#editor #product-price"),
descriptionTextarea: document.querySelector("#editor #product-description"),
coordInput: document.querySelector("#editor #product-coord"),
barcodeInput: document.querySelector("#editor #product-barcode"),
};
let products = [];
let selectedProductId = null;
function selectProduct(product) {
selectedProductId = product.id;
editor.idInput.value = product.id.toString();
editor.nameInput.value = product.name;
editor.priceInput.value = product.price_dkk_cent / 100;
editor.descriptionTextarea.value = product.description;
editor.coordInput.value = product.coord_id.toString();
editor.barcodeInput.value = product.barcode.toString();
}
async function loadProduct() {
selectedProductId = parseInt(editor.idInput.value);
const product = products.find(product => product.id === selectedProductId);
if (!product){
alert(`no product with id ${selectedProductId}`);
return;
}
selectProduct(product);
}
function productFromForm() {
return {
id: parseInt(editor.idInput.value),
name: editor.nameInput.value,
description: editor.descriptionTextarea.value,
price_dkk_cent: Math.floor(parseFloat(editor.priceInput.value) * 100),
coord_id: parseInt(editor.coordInput.value),
barcode: editor.barcodeInput.value,
}
}
async function saveProduct() {
const product = productFromForm();
await fetch("/api/products/update", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(product),
}).then(res => res.json());
await updateProductList();
}
async function newProduct() {
const product = productFromForm();
await fetch("/api/products/create", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(product),
}).then(res => res.json());
await updateProductList();
}
async function updateProductList() {
const res = await fetch("/api/products/all")
.then(res => res.json());
products = res.products;
productList.innerHTML = `
<tr>
<th>id</th>
<th>name</th>
<th>price_dkk_cent</th>
<th>description</th>
<th>coord_id</th>
<th>barcode</th>
<th> </th>
</tr>
`;
productList.innerHTML += products
.map(product => `
<tr>
<td><code>${product.id}</code></td>
<td><strong>${product.name}</strong></td>
<td>${product.price_dkk_cent / 100} dkk</td>
<td>${product.description}</td>
<td><code>${product.coord_id}</code></td>
<td><code>${product.barcode}</code></td>
<td><button id="product-${product.id}-edit">Edit</button></td>
</tr>
`)
.join("");
for (const product of products) {
document
.querySelector(`#product-${product.id}-edit`)
.addEventListener("click", () => {
selectProduct(product);
});
}
}
updateProductList();
editor.form
.addEventListener("submit", (e) => {
e.preventDefault();
});
editor.loadButton
.addEventListener("click", (e) => {
loadProduct();
});
editor.saveButton
.addEventListener("click", (e) => {
saveProduct();
});
editor.newButton
.addEventListener("click", (e) => {
newProduct();
});

View File

@ -36,6 +36,9 @@ void route_post_set_number(HttpCtx* ctx);
void route_get_not_found(HttpCtx* ctx); void route_get_not_found(HttpCtx* ctx);
void route_get_products_all(HttpCtx* ctx); void route_get_products_all(HttpCtx* ctx);
void route_post_products_create(HttpCtx* ctx);
void route_post_products_update(HttpCtx* ctx);
void route_get_product_editor_html(HttpCtx* ctx); void route_get_product_editor_html(HttpCtx* ctx);
void route_get_product_editor_js(HttpCtx* ctx); void route_get_product_editor_js(HttpCtx* ctx);

View File

@ -36,6 +36,83 @@ void route_get_products_all(HttpCtx* ctx)
string_destroy(&res); string_destroy(&res);
} }
void route_post_products_create(HttpCtx* ctx)
{
Cx* cx = http_ctx_user_ctx(ctx);
const char* body_str = http_ctx_req_body(ctx);
JsonValue* body_json = json_parse(body_str, strlen(body_str));
if (!body_json) {
RESPOND_BAD_REQUEST(ctx, "bad request");
return;
}
ProductsCreateReq req;
int parse_result = products_create_req_from_json(&req, body_json);
json_free(body_json);
if (parse_result != 0) {
RESPOND_BAD_REQUEST(ctx, "bad request");
return;
}
Product product = {
.id = 0,
.name = str_dup(req.name),
.price_dkk_cent = req.price_dkk_cent,
.description = str_dup(req.description),
.coord_id = req.coord_id,
.barcode = str_dup(req.barcode),
};
products_create_req_destroy(&req);
DbRes db_res = db_product_insert(cx->db, &product);
if (db_res != DbRes_Ok) {
RESPOND_SERVER_ERROR(ctx);
goto l0_return;
}
RESPOND_JSON(ctx, 200, "{\"ok\":true}");
l0_return:
product_destroy(&product);
}
void route_post_products_update(HttpCtx* ctx)
{
Cx* cx = http_ctx_user_ctx(ctx);
const char* body_str = http_ctx_req_body(ctx);
printf("body_str = '%s'\n", body_str);
JsonValue* body_json = json_parse(body_str, strlen(body_str));
printf("body_json = %p\n", (void*)body_json);
if (!body_json) {
RESPOND_BAD_REQUEST(ctx, "bad request");
return;
}
Product product;
int parse_result = product_from_json(&product, body_json);
printf("parse_result = %d\n", parse_result);
json_free(body_json);
if (parse_result != 0) {
RESPOND_BAD_REQUEST(ctx, "bad request");
return;
}
DbRes db_res = db_product_update(cx->db, &product);
if (db_res != DbRes_Ok) {
RESPOND_SERVER_ERROR(ctx);
goto l0_return;
}
RESPOND_JSON(ctx, 200, "{\"ok\":true}");
l0_return:
product_destroy(&product);
}
static inline int read_and_send_file(HttpCtx* ctx, static inline int read_and_send_file(HttpCtx* ctx,
const char* filepath, const char* filepath,
size_t max_file_size, size_t max_file_size,

View File

@ -30,6 +30,12 @@ DbRes db_user_with_email_exists(Db* db, bool* exists, const char* email);
/// `user` is an out parameter. /// `user` is an out parameter.
DbRes db_user_with_email(Db* db, User* user, const char* email); DbRes db_user_with_email(Db* db, User* user, const char* email);
/// `product.id` are ignored.
DbRes db_product_insert(Db* db, const Product* product);
/// Uses `user.id` to find model.
DbRes db_product_update(Db* db, const Product* product);
/// `product` is an out parameter. /// `product` is an out parameter.
DbRes db_product_with_id(Db* db, Product* product, int64_t id); DbRes db_product_with_id(Db* db, Product* product, int64_t id);

View File

@ -285,7 +285,97 @@ l0_return:
return res; return res;
} }
/// `product` is an out parameter. DbRes db_product_insert(Db* db, const Product* product)
{
static_assert(sizeof(Product) == 48, "model has changed");
sqlite3* connection;
CONNECT;
DbRes res;
sqlite3_stmt* stmt;
int prepare_res = sqlite3_prepare_v2(connection,
"INSERT INTO products"
" (name, price_dkk_cent, description, coord, barcode)"
" VALUES (?, ?, ?, ?, ?)",
-1,
&stmt,
NULL);
if (prepare_res != SQLITE_OK) {
REPORT_SQLITE3_ERROR();
res = DbRes_Error;
goto l0_return;
}
sqlite3_bind_text(stmt, 1, product->name, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, product->price_dkk_cent);
sqlite3_bind_text(stmt, 3, product->description, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, product->coord_id);
sqlite3_bind_text(stmt, 5, product->barcode, -1, SQLITE_STATIC);
int step_res = sqlite3_step(stmt);
if (step_res != SQLITE_DONE) {
REPORT_SQLITE3_ERROR();
res = DbRes_Error;
goto l0_return;
}
res = DbRes_Ok;
l0_return:
if (stmt)
sqlite3_finalize(stmt);
DISCONNECT;
return res;
}
DbRes db_product_update(Db* db, const Product* product)
{
static_assert(sizeof(Product) == 48, "model has changed");
sqlite3* connection;
CONNECT;
DbRes res;
sqlite3_stmt* stmt;
int prepare_res = sqlite3_prepare_v2(connection,
"UPDATE products SET"
" name = ?,"
" price_dkk_cent = ?,"
" description = ?,"
" coord = ?,"
" barcode = ?"
" WHERE id = ?",
-1,
&stmt,
NULL);
if (prepare_res != SQLITE_OK) {
REPORT_SQLITE3_ERROR();
res = DbRes_Error;
goto l0_return;
}
sqlite3_bind_text(stmt, 1, product->name, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, product->price_dkk_cent);
sqlite3_bind_text(stmt, 3, product->description, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, product->coord_id);
sqlite3_bind_text(stmt, 5, product->barcode, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 6, product->id);
int step_res = sqlite3_step(stmt);
if (step_res != SQLITE_DONE) {
fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection));
res = DbRes_Error;
goto l0_return;
}
res = DbRes_Ok;
l0_return:
if (stmt)
sqlite3_finalize(stmt);
DISCONNECT;
return res;
}
DbRes db_product_with_id(Db* db, Product* product, int64_t id) DbRes db_product_with_id(Db* db, Product* product, int64_t id)
{ {
static_assert(sizeof(Product) == 48, "model has changed"); static_assert(sizeof(Product) == 48, "model has changed");

View File

@ -148,6 +148,23 @@ void json_parser_destroy(JsonParser* p)
(void)p; (void)p;
} }
static inline void free_unused_arr(Arr* arr)
{
for (size_t i = 0; i < arr->size; ++i) {
json_free(arr->data[i]);
}
arr_destroy(arr);
}
static inline void free_unused_obj(Obj* obj)
{
for (size_t i = 0; i < obj->size; ++i) {
free(obj->data[i].key);
json_free(obj->data[i].val);
}
obj_destroy(obj);
}
JsonValue* json_parser_parse(JsonParser* p) JsonValue* json_parser_parse(JsonParser* p)
{ {
switch (p->curr_tok) { switch (p->curr_tok) {
@ -188,23 +205,29 @@ JsonValue* json_parser_parse(JsonParser* p)
arr_construct(&arr); arr_construct(&arr);
{ {
JsonValue* value = json_parser_parse(p); JsonValue* value = json_parser_parse(p);
if (!value) if (!value) {
free_unused_arr(&arr);
return NULL; return NULL;
}
arr_push(&arr, value); arr_push(&arr, value);
} }
while (p->curr_tok != TOK_EOF && p->curr_tok != ']') { while (p->curr_tok != TOK_EOF && p->curr_tok != ']') {
if (p->curr_tok != ',') { if (p->curr_tok != ',') {
fprintf(stderr, "error: json: expected ',' in array\n"); fprintf(stderr, "error: json: expected ',' in array\n");
free_unused_arr(&arr);
return NULL; return NULL;
} }
lex(p); lex(p);
JsonValue* value = json_parser_parse(p); JsonValue* value = json_parser_parse(p);
if (!value) if (!value) {
free_unused_arr(&arr);
return NULL; return NULL;
}
arr_push(&arr, value); arr_push(&arr, value);
} }
if (p->curr_tok != ']') { if (p->curr_tok != ']') {
fprintf(stderr, "error: json: expected ']' after array\n"); fprintf(stderr, "error: json: expected ']' after array\n");
free_unused_arr(&arr);
return NULL; return NULL;
} }
lex(p); lex(p);
@ -218,52 +241,62 @@ JsonValue* json_parser_parse(JsonParser* p)
{ {
if (p->curr_tok != '"') { if (p->curr_tok != '"') {
fprintf(stderr, "error: json: expected '\"' in kv\n"); fprintf(stderr, "error: json: expected '\"' in kv\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
char* key = p->curr_val; char* key = p->curr_val;
lex(p); lex(p);
if (p->curr_tok != ':') { if (p->curr_tok != ':') {
fprintf(stderr, "error: json: expected ':' in kv\n"); fprintf(stderr, "error: json: expected ':' in kv\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
lex(p); lex(p);
JsonValue* value = json_parser_parse(p); JsonValue* value = json_parser_parse(p);
if (!value) if (!value) {
free_unused_obj(&obj);
return NULL; return NULL;
}
obj_push(&obj, (KV) { key, value }); obj_push(&obj, (KV) { key, value });
} }
while (p->curr_tok != TOK_EOF && p->curr_tok != '}') { while (p->curr_tok != TOK_EOF && p->curr_tok != '}') {
if (p->curr_tok != ',') { if (p->curr_tok != ',') {
fprintf(stderr, "error: json: expected ',' in object\n"); fprintf(stderr, "error: json: expected ',' in object\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
lex(p); lex(p);
if (p->curr_tok != '"') { if (p->curr_tok != '"') {
fprintf(stderr, "error: json: expected '\"' in kv\n"); fprintf(stderr, "error: json: expected '\"' in kv\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
char* key = p->curr_val; char* key = p->curr_val;
lex(p); lex(p);
if (p->curr_tok != ':') { if (p->curr_tok != ':') {
fprintf(stderr, "error: json: expected ':' in kv\n"); fprintf(stderr, "error: json: expected ':' in kv\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
lex(p); lex(p);
JsonValue* value = json_parser_parse(p); JsonValue* value = json_parser_parse(p);
if (!value) if (!value) {
free_unused_obj(&obj);
return NULL; return NULL;
}
obj_push(&obj, (KV) { key, value }); obj_push(&obj, (KV) { key, value });
} }
if (p->curr_tok != '}') { if (p->curr_tok != '}') {
fprintf(stderr, "error: json: expected '}' after object\n"); fprintf(stderr, "error: json: expected '}' after object\n");
free_unused_obj(&obj);
return NULL; return NULL;
} }
lex(p); lex(p);
return alloc((JsonValue) { .type = JsonType_Object, .obj_val = obj }); return alloc((JsonValue) { .type = JsonType_Object, .obj_val = obj });
} }
fprintf(stderr, "error: json: unexpeted tok\n"); fprintf(stderr, "error: json: unexpeted token\n");
return NULL; return NULL;
} }
@ -300,6 +333,7 @@ static inline void lex(JsonParser* p)
case '0': case '0':
lstep(p); lstep(p);
p->curr_tok = TOK_NUMBER; p->curr_tok = TOK_NUMBER;
p->curr_val = str_dup("0");
return; return;
} }
if ((p->ch >= '1' && p->ch <= '9') || p->ch == '.') { if ((p->ch >= '1' && p->ch <= '9') || p->ch == '.') {
@ -315,14 +349,36 @@ static inline void lex(JsonParser* p)
string_push(&value, p->ch); string_push(&value, p->ch);
lstep(p); lstep(p);
} }
// should be interned
char* copy = string_copy(&value); char* copy = string_copy(&value);
string_destroy(&value); string_destroy(&value);
p->curr_tok = TOK_NUMBER; p->curr_tok = TOK_NUMBER;
p->curr_val = copy; p->curr_val = copy;
return; return;
} }
if ((p->ch >= 'a' && p->ch <= 'z') || (p->ch >= 'A' && p->ch <= 'Z')) {
String value;
string_construct(&value);
while (p->i < p->text_len
&& ((p->ch >= 'a' && p->ch <= 'z')
|| (p->ch >= 'A' && p->ch <= 'Z'))) {
string_push(&value, p->ch);
lstep(p);
}
if (strcmp(value.data, "null")) {
p->curr_tok = TOK_NULL;
} else if (strcmp(value.data, "false")) {
p->curr_tok = TOK_FALSE;
} else if (strcmp(value.data, "true")) {
p->curr_tok = TOK_TRUE;
} else {
fprintf(
stderr, "error: json: illegal keyword \"%s\"\n", value.data);
p->curr_tok = TOK_ERROR;
}
string_destroy(&value);
return;
}
if (p->ch == '"') { if (p->ch == '"') {
lstep(p); lstep(p);
@ -364,10 +420,9 @@ static inline void lex(JsonParser* p)
} }
lstep(p); lstep(p);
// should be interned
char* copy = string_copy(&value); char* copy = string_copy(&value);
string_destroy(&value); string_destroy(&value);
p->curr_tok = '"'; p->curr_tok = '"';
p->curr_val = copy; p->curr_val = copy;
return; return;

View File

@ -38,11 +38,16 @@ int main(void)
http_server_set_user_ctx(server, &cx); http_server_set_user_ctx(server, &cx);
http_server_get(server, "/api/products/all", route_get_products_all); http_server_get(server, "/api/products/all", route_get_products_all);
http_server_post(
server, "/api/products/create", route_post_products_create);
http_server_post(
server, "/api/products/update", route_post_products_update);
http_server_get( http_server_get(
server, "/product_editor/index.html", route_get_product_editor_html); server, "/product_editor/index.html", route_get_product_editor_html);
http_server_get(server, http_server_get(server,
"/product_editor/product_editor.js", "/product_editor/product_editor.js",
route_get_product_editor_html); route_get_product_editor_js);
http_server_post(server, "/api/carts/purchase", route_post_carts_purchase); http_server_post(server, "/api/carts/purchase", route_post_carts_purchase);

View File

@ -104,6 +104,15 @@ void receipts_one_res_destroy(ReceiptsOneRes* m)
receipts_one_res_product_vec_destroy(&m->products); receipts_one_res_product_vec_destroy(&m->products);
} }
void products_create_req_destroy(ProductsCreateReq* m)
{
static_assert(sizeof(ProductsCreateReq) == 40, "model has changed");
free(m->name);
free(m->description);
free(m->barcode);
}
char* user_to_json_string(const User* m) char* user_to_json_string(const User* m)
{ {
static_assert(sizeof(User) == 40, "model has changed"); static_assert(sizeof(User) == 40, "model has changed");
@ -363,6 +372,31 @@ char* receipts_one_res_to_json_string(const ReceiptsOneRes* m)
return result; return result;
} }
char* products_create_req_to_json_string(const ProductsCreateReq* m)
{
static_assert(sizeof(ProductsCreateReq) == 40, "model has changed");
String string;
string_construct(&string);
string_pushf(&string,
"{"
"\"name\":\"%s\","
"\"description\":\"%s\","
"\"price_dkk_cent\":%ld,"
"\"coord_id\":%ld,"
"\"barcode\":\"%s\""
"}",
m->name,
m->description,
m->price_dkk_cent,
m->coord_id,
m->barcode);
char* result = string_copy(&string);
string_destroy(&string);
return result;
}
typedef struct { typedef struct {
const char* key; const char* key;
JsonType type; JsonType type;
@ -456,7 +490,7 @@ int product_from_json(Product* m, const JsonValue* json)
.description = GET_STR("description"), .description = GET_STR("description"),
.price_dkk_cent = GET_INT("price_dkk_cent"), .price_dkk_cent = GET_INT("price_dkk_cent"),
.coord_id = GET_INT("coord_id"), .coord_id = GET_INT("coord_id"),
.barcode = GET_STR("y"), .barcode = GET_STR("barcode"),
}; };
return 0; return 0;
} }
@ -583,6 +617,29 @@ int receipts_one_res_from_json(ReceiptsOneRes* m, const JsonValue* json)
PANIC("not implemented"); PANIC("not implemented");
} }
int products_create_req_from_json(ProductsCreateReq* m, const JsonValue* json)
{
static_assert(sizeof(ProductsCreateReq) == 40, "model has changed");
ObjField fields[] = {
{ "name", JsonType_String },
{ "description", JsonType_String },
{ "price_dkk_cent", JsonType_Number },
{ "coord_id", JsonType_Number },
{ "barcode", JsonType_String },
};
if (!OBJ_CONFORMS(json, fields))
return -1;
*m = (ProductsCreateReq) {
.name = GET_STR("name"),
.description = GET_STR("description"),
.price_dkk_cent = GET_INT("price_dkk_cent"),
.coord_id = GET_INT("coord_id"),
.barcode = GET_STR("barcode"),
};
return 0;
}
DEFINE_VEC_IMPL(ProductPrice, ProductPriceVec, product_price_vec, ) DEFINE_VEC_IMPL(ProductPrice, ProductPriceVec, product_price_vec, )
DEFINE_VEC_IMPL(ReceiptProduct, ReceiptProductVec, receipt_product_vec, ) DEFINE_VEC_IMPL(ReceiptProduct, ReceiptProductVec, receipt_product_vec, )
DEFINE_VEC_IMPL(Receipt, ReceiptVec, receipt_vec, ) DEFINE_VEC_IMPL(Receipt, ReceiptVec, receipt_vec, )

View File

@ -121,3 +121,13 @@ typedef struct {
} ReceiptsOneRes; } ReceiptsOneRes;
void receipts_one_res_destroy(ReceiptsOneRes* model); void receipts_one_res_destroy(ReceiptsOneRes* model);
typedef struct {
char* name;
char* description;
int64_t price_dkk_cent;
int64_t coord_id;
char* barcode;
} ProductsCreateReq;
void products_create_req_destroy(ProductsCreateReq* model);

View File

@ -17,3 +17,4 @@ DEFINE_MODEL_JSON(SessionsLoginReq, sessions_login_req)
DEFINE_MODEL_JSON(CartsPurchaseReq, carts_purchase_req) DEFINE_MODEL_JSON(CartsPurchaseReq, carts_purchase_req)
DEFINE_MODEL_JSON(ReceiptsOneResProduct, receipts_one_res_product) DEFINE_MODEL_JSON(ReceiptsOneResProduct, receipts_one_res_product)
DEFINE_MODEL_JSON(ReceiptsOneRes, receipts_one_res) DEFINE_MODEL_JSON(ReceiptsOneRes, receipts_one_res)
DEFINE_MODEL_JSON(ProductsCreateReq, products_create_req)