From e4942779789969f882bc54cc45f02e27d63f86cd Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Wed, 19 Mar 2025 15:15:00 +0100 Subject: [PATCH] product editor --- backend/public/product_editor.html | 101 ++++++++++++++++++- backend/public/product_editor.js | 134 +++++++++++++++++++++++++- backend/src/controllers/controllers.h | 3 + backend/src/controllers/products.c | 77 +++++++++++++++ backend/src/db/db.h | 6 ++ backend/src/db/db_sqlite.c | 92 +++++++++++++++++- backend/src/json/json.c | 73 ++++++++++++-- backend/src/main.c | 7 +- backend/src/models/models.c | 59 +++++++++++- backend/src/models/models.h | 10 ++ backend/src/models/models_json.h | 1 + 11 files changed, 549 insertions(+), 14 deletions(-) diff --git a/backend/public/product_editor.html b/backend/public/product_editor.html index 8c847e0..3db5a9e 100644 --- a/backend/public/product_editor.html +++ b/backend/public/product_editor.html @@ -3,8 +3,107 @@ + -

Product editor

+

Products

+ +
+
+ Editor + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
diff --git a/backend/public/product_editor.js b/backend/public/product_editor.js index d5c6ee4..b130570 100644 --- a/backend/public/product_editor.js +++ b/backend/public/product_editor.js @@ -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 = ` + + id + name + price_dkk_cent + description + coord_id + barcode + + + `; + productList.innerHTML += products + .map(product => ` + + ${product.id} + ${product.name} + ${product.price_dkk_cent / 100} dkk + ${product.description} + ${product.coord_id} + ${product.barcode} + + + `) + .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(); + }); diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index dc1b025..bbcd296 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -36,6 +36,9 @@ void route_post_set_number(HttpCtx* ctx); void route_get_not_found(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_js(HttpCtx* ctx); diff --git a/backend/src/controllers/products.c b/backend/src/controllers/products.c index 7a0e548..cbb356a 100644 --- a/backend/src/controllers/products.c +++ b/backend/src/controllers/products.c @@ -36,6 +36,83 @@ void route_get_products_all(HttpCtx* ctx) 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, const char* filepath, size_t max_file_size, diff --git a/backend/src/db/db.h b/backend/src/db/db.h index 78cd9a2..dbb25a3 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -30,6 +30,12 @@ DbRes db_user_with_email_exists(Db* db, bool* exists, const char* email); /// `user` is an out parameter. 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. DbRes db_product_with_id(Db* db, Product* product, int64_t id); diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index 48e3152..547fa97 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -285,7 +285,97 @@ l0_return: 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) { static_assert(sizeof(Product) == 48, "model has changed"); diff --git a/backend/src/json/json.c b/backend/src/json/json.c index b25e84e..484644f 100644 --- a/backend/src/json/json.c +++ b/backend/src/json/json.c @@ -148,6 +148,23 @@ void json_parser_destroy(JsonParser* 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) { switch (p->curr_tok) { @@ -188,23 +205,29 @@ JsonValue* json_parser_parse(JsonParser* p) arr_construct(&arr); { JsonValue* value = json_parser_parse(p); - if (!value) + if (!value) { + free_unused_arr(&arr); return NULL; + } arr_push(&arr, value); } while (p->curr_tok != TOK_EOF && p->curr_tok != ']') { if (p->curr_tok != ',') { fprintf(stderr, "error: json: expected ',' in array\n"); + free_unused_arr(&arr); return NULL; } lex(p); JsonValue* value = json_parser_parse(p); - if (!value) + if (!value) { + free_unused_arr(&arr); return NULL; + } arr_push(&arr, value); } if (p->curr_tok != ']') { fprintf(stderr, "error: json: expected ']' after array\n"); + free_unused_arr(&arr); return NULL; } lex(p); @@ -218,52 +241,62 @@ JsonValue* json_parser_parse(JsonParser* p) { if (p->curr_tok != '"') { fprintf(stderr, "error: json: expected '\"' in kv\n"); + free_unused_obj(&obj); return NULL; } char* key = p->curr_val; lex(p); if (p->curr_tok != ':') { fprintf(stderr, "error: json: expected ':' in kv\n"); + free_unused_obj(&obj); return NULL; } lex(p); JsonValue* value = json_parser_parse(p); - if (!value) + if (!value) { + free_unused_obj(&obj); return NULL; + } obj_push(&obj, (KV) { key, value }); } while (p->curr_tok != TOK_EOF && p->curr_tok != '}') { if (p->curr_tok != ',') { fprintf(stderr, "error: json: expected ',' in object\n"); + free_unused_obj(&obj); return NULL; } lex(p); if (p->curr_tok != '"') { fprintf(stderr, "error: json: expected '\"' in kv\n"); + free_unused_obj(&obj); return NULL; } char* key = p->curr_val; lex(p); if (p->curr_tok != ':') { fprintf(stderr, "error: json: expected ':' in kv\n"); + free_unused_obj(&obj); return NULL; } lex(p); JsonValue* value = json_parser_parse(p); - if (!value) + if (!value) { + free_unused_obj(&obj); return NULL; + } obj_push(&obj, (KV) { key, value }); } if (p->curr_tok != '}') { fprintf(stderr, "error: json: expected '}' after object\n"); + free_unused_obj(&obj); return NULL; } lex(p); 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; } @@ -300,6 +333,7 @@ static inline void lex(JsonParser* p) case '0': lstep(p); p->curr_tok = TOK_NUMBER; + p->curr_val = str_dup("0"); return; } if ((p->ch >= '1' && p->ch <= '9') || p->ch == '.') { @@ -315,14 +349,36 @@ static inline void lex(JsonParser* p) string_push(&value, p->ch); lstep(p); } - // should be interned char* copy = string_copy(&value); - string_destroy(&value); + p->curr_tok = TOK_NUMBER; p->curr_val = copy; 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 == '"') { lstep(p); @@ -364,10 +420,9 @@ static inline void lex(JsonParser* p) } lstep(p); - // should be interned char* copy = string_copy(&value); - string_destroy(&value); + p->curr_tok = '"'; p->curr_val = copy; return; diff --git a/backend/src/main.c b/backend/src/main.c index 10ad31e..cc4a240 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -38,11 +38,16 @@ int main(void) http_server_set_user_ctx(server, &cx); 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( server, "/product_editor/index.html", route_get_product_editor_html); http_server_get(server, "/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); diff --git a/backend/src/models/models.c b/backend/src/models/models.c index 89211a5..969222a 100644 --- a/backend/src/models/models.c +++ b/backend/src/models/models.c @@ -104,6 +104,15 @@ void receipts_one_res_destroy(ReceiptsOneRes* m) 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) { static_assert(sizeof(User) == 40, "model has changed"); @@ -363,6 +372,31 @@ char* receipts_one_res_to_json_string(const ReceiptsOneRes* m) 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 { const char* key; JsonType type; @@ -456,7 +490,7 @@ int product_from_json(Product* m, const JsonValue* json) .description = GET_STR("description"), .price_dkk_cent = GET_INT("price_dkk_cent"), .coord_id = GET_INT("coord_id"), - .barcode = GET_STR("y"), + .barcode = GET_STR("barcode"), }; return 0; } @@ -583,6 +617,29 @@ int receipts_one_res_from_json(ReceiptsOneRes* m, const JsonValue* json) 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(ReceiptProduct, ReceiptProductVec, receipt_product_vec, ) DEFINE_VEC_IMPL(Receipt, ReceiptVec, receipt_vec, ) diff --git a/backend/src/models/models.h b/backend/src/models/models.h index 1ed0119..dbcd51d 100644 --- a/backend/src/models/models.h +++ b/backend/src/models/models.h @@ -121,3 +121,13 @@ typedef struct { } ReceiptsOneRes; 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); diff --git a/backend/src/models/models_json.h b/backend/src/models/models_json.h index 73fc2cf..ba8b530 100644 --- a/backend/src/models/models_json.h +++ b/backend/src/models/models_json.h @@ -17,3 +17,4 @@ DEFINE_MODEL_JSON(SessionsLoginReq, sessions_login_req) DEFINE_MODEL_JSON(CartsPurchaseReq, carts_purchase_req) DEFINE_MODEL_JSON(ReceiptsOneResProduct, receipts_one_res_product) DEFINE_MODEL_JSON(ReceiptsOneRes, receipts_one_res) +DEFINE_MODEL_JSON(ProductsCreateReq, products_create_req)