diff --git a/Makefile b/Makefile index dbbf0a2..0addab2 100644 --- a/Makefile +++ b/Makefile @@ -41,4 +41,4 @@ deploy-meilisearch-prod: deploy-search-prod: cd search && fly deploy --config fly.prod.toml -deploy-all-prod: deploy-frontend-prod deploy-backend-prod deploy-pocketbase-prod deploy-meilisearch-prod deploy-search-prod \ No newline at end of file +deploy-all-prod: deploy-frontend-prod deploy-backend-prod deploy-pocketbase-prod deploy-meilisearch-prod deploy-search-prod diff --git a/db/pb_migrations/1739303000_updated_users_add_roles.js b/db/pb_migrations/1739303000_updated_users_add_roles.js new file mode 100644 index 0000000..cc86b31 --- /dev/null +++ b/db/pb_migrations/1739303000_updated_users_add_roles.js @@ -0,0 +1,30 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_"); + + // add roles field + collection.fields.addAt(0, new Field({ + "system": false, + "id": "roles", + "name": "roles", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "maxSelect": 3, + "values": [ + "admin", + "delivery", + "vendor" + ] + })); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_"); + + // remove roles field + collection.fields.removeById("roles"); + + return app.save(collection); +}); \ No newline at end of file diff --git a/db/pb_migrations/1739303001_updated_users_rules.js b/db/pb_migrations/1739303001_updated_users_rules.js new file mode 100644 index 0000000..f41f795 --- /dev/null +++ b/db/pb_migrations/1739303001_updated_users_rules.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_"); + + // Update view rule to allow admin users to view all user data + collection.viewRule = '@request.auth.roles ?= "admin" || @request.auth.id = id'; + + // Update list rule to allow admin users to list all users + collection.listRule = '@request.auth.roles ?= "admin" || @request.auth.id = id'; + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_"); + + // Revert to default rules + collection.viewRule = '@request.auth.id = id'; + collection.listRule = '@request.auth.id = id'; + + return app.save(collection); +}); \ No newline at end of file diff --git a/db/pb_migrations/1739303002_updated_orders_rules.js b/db/pb_migrations/1739303002_updated_orders_rules.js new file mode 100644 index 0000000..4698cfd --- /dev/null +++ b/db/pb_migrations/1739303002_updated_orders_rules.js @@ -0,0 +1,30 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("orders"); + + // Update view rule to allow admin users to view all orders and their expanded data + collection.viewRule = '@request.auth.roles ?= "admin" || user.id = @request.auth.id'; + + // Update list rule to allow admin users to list all orders + collection.listRule = '@request.auth.roles ?= "admin" || user.id = @request.auth.id'; + + // Update expand rule to allow admin users to expand all relations + collection.options = { + ...collection.options, + expandRule: '@request.auth.roles ?= "admin" || user.id = @request.auth.id' + }; + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("orders"); + + // Revert to default rules + collection.viewRule = 'user.id = @request.auth.id'; + collection.listRule = 'user.id = @request.auth.id'; + collection.options = { + ...collection.options, + expandRule: 'user.id = @request.auth.id' + }; + + return app.save(collection); +}); \ No newline at end of file diff --git a/db/pb_migrations/1739303004_created_store_roles.js b/db/pb_migrations/1739303004_created_store_roles.js new file mode 100644 index 0000000..2129ea1 --- /dev/null +++ b/db/pb_migrations/1739303004_created_store_roles.js @@ -0,0 +1,78 @@ +/// +migrate((app) => { + const collection = new Collection({ + "id": "store_roles", + "name": "store_roles", + "type": "base", + "system": false, + "fields": [ + { + "id": "user_relation", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "system": false, + "cascadeDelete": true, + "collectionId": "_pb_users_auth_", + "maxSelect": 1, + "minSelect": 1 + }, + { + "id": "store_relation", + "name": "store", + "type": "relation", + "required": true, + "presentable": false, + "system": false, + "cascadeDelete": true, + "collectionId": "pbc_3800236418", + "maxSelect": 1, + "minSelect": 1 + }, + { + "id": "store_role", + "name": "role", + "type": "select", + "required": true, + "presentable": false, + "system": false, + "values": ["admin", "staff"], + "maxSelect": 1 + }, + { + "id": "created", + "name": "created", + "type": "autodate", + "required": false, + "presentable": false, + "system": false, + "hidden": false, + "onCreate": true, + "onUpdate": false + }, + { + "id": "updated", + "name": "updated", + "type": "autodate", + "required": false, + "presentable": false, + "system": false, + "hidden": false, + "onCreate": true, + "onUpdate": true + } + ], + "indexes": ["CREATE UNIQUE INDEX idx_unique_user_store ON store_roles (user, store)"], + "listRule": "@request.auth.id != ''", + "viewRule": "@request.auth.id != ''", + "createRule": "@request.auth.roles.admin = true", + "updateRule": "@request.auth.roles.admin = true", + "deleteRule": "@request.auth.roles.admin = true" + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("store_roles"); + return app.delete(collection); +}); \ No newline at end of file diff --git a/db/pb_migrations/1739826655_updated_orders.js b/db/pb_migrations/1739826655_updated_orders.js new file mode 100644 index 0000000..bf83abd --- /dev/null +++ b/db/pb_migrations/1739826655_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ \"admin\" || user.id = @request.auth.id", + "viewRule": "@request.auth.roles ?~ \"admin\" || user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?= \"admin\" || user.id = @request.auth.id", + "viewRule": "@request.auth.roles ?= \"admin\" || user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739830958_updated_users.js b/db/pb_migrations/1739830958_updated_users.js new file mode 100644 index 0000000..c72c8cd --- /dev/null +++ b/db/pb_migrations/1739830958_updated_users.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ \"admin\" || @request.auth.id = id", + "viewRule": "@request.auth.roles ?~ \"admin\" || @request.auth.id = id" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("_pb_users_auth_") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?= \"admin\" || @request.auth.id = id", + "viewRule": "@request.auth.roles ?= \"admin\" || @request.auth.id = id" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739857597_updated_order_items.js b/db/pb_migrations/1739857597_updated_order_items.js new file mode 100644 index 0000000..a9eb975 --- /dev/null +++ b/db/pb_migrations/1739857597_updated_order_items.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "deleteRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "listRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "updateRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "viewRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "", + "deleteRule": null, + "listRule": "", + "updateRule": null, + "viewRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739857689_updated_order_items.js b/db/pb_migrations/1739857689_updated_order_items.js new file mode 100644 index 0000000..8e46dc9 --- /dev/null +++ b/db/pb_migrations/1739857689_updated_order_items.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "deleteRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "listRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "updateRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "viewRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "deleteRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "listRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "updateRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'", + "viewRule": "@request.auth.roles ?~ 'admin' || @collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin'" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739857745_updated_store_items.js b/db/pb_migrations/1739857745_updated_store_items.js new file mode 100644 index 0000000..71eb9fe --- /dev/null +++ b/db/pb_migrations/1739857745_updated_store_items.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "updateRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "updateRule": null + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739857799_updated_order_items.js b/db/pb_migrations/1739857799_updated_order_items.js new file mode 100644 index 0000000..51025b4 --- /dev/null +++ b/db/pb_migrations/1739857799_updated_order_items.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin'", + "deleteRule": "@request.auth.roles ?~ 'admin'", + "listRule": "@request.auth.roles ?~ 'admin'", + "updateRule": "@request.auth.roles ?~ 'admin'", + "viewRule": "@request.auth.roles ?~ 'admin'" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "deleteRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "listRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "updateRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "viewRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739857921_updated_order_items.js b/db/pb_migrations/1739857921_updated_order_items.js new file mode 100644 index 0000000..2620d5b --- /dev/null +++ b/db/pb_migrations/1739857921_updated_order_items.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id", + "deleteRule": null, + "listRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id", + "updateRule": null, + "viewRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin'", + "deleteRule": "@request.auth.roles ?~ 'admin'", + "listRule": "@request.auth.roles ?~ 'admin'", + "updateRule": "@request.auth.roles ?~ 'admin'", + "viewRule": "@request.auth.roles ?~ 'admin'" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739858051_updated_store_items.js b/db/pb_migrations/1739858051_updated_store_items.js new file mode 100644 index 0000000..a22e7b1 --- /dev/null +++ b/db/pb_migrations/1739858051_updated_store_items.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "deleteRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": null, + "deleteRule": null + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739923684_updated_orders.js b/db/pb_migrations/1739923684_updated_orders.js new file mode 100644 index 0000000..563bbb8 --- /dev/null +++ b/db/pb_migrations/1739923684_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ \"admin\" || user.id = @request.auth.id", + "viewRule": "@request.auth.roles ?~ \"admin\" || user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739923701_updated_orders.js b/db/pb_migrations/1739923701_updated_orders.js new file mode 100644 index 0000000..612abba --- /dev/null +++ b/db/pb_migrations/1739923701_updated_orders.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "viewRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739925919_updated_orders.js b/db/pb_migrations/1739925919_updated_orders.js new file mode 100644 index 0000000..b7e55d5 --- /dev/null +++ b/db/pb_migrations/1739925919_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739925939_updated_orders.js b/db/pb_migrations/1739925939_updated_orders.js new file mode 100644 index 0000000..f8dd81c --- /dev/null +++ b/db/pb_migrations/1739925939_updated_orders.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739925955_updated_orders.js b/db/pb_migrations/1739925955_updated_orders.js new file mode 100644 index 0000000..bdfa4c5 --- /dev/null +++ b/db/pb_migrations/1739925955_updated_orders.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739926005_updated_orders.js b/db/pb_migrations/1739926005_updated_orders.js new file mode 100644 index 0000000..5ead215 --- /dev/null +++ b/db/pb_migrations/1739926005_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role ?= 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role ?= 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739926077_updated_orders.js b/db/pb_migrations/1739926077_updated_orders.js new file mode 100644 index 0000000..519f884 --- /dev/null +++ b/db/pb_migrations/1739926077_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role ?= 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role ?= 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739926527_updated_orders.js b/db/pb_migrations/1739926527_updated_orders.js new file mode 100644 index 0000000..6bb4b2a --- /dev/null +++ b/db/pb_migrations/1739926527_updated_orders.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.items.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739927119_updated_order_items.js b/db/pb_migrations/1739927119_updated_order_items.js new file mode 100644 index 0000000..f6806c4 --- /dev/null +++ b/db/pb_migrations/1739927119_updated_order_items.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id", + "listRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id", + "listRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id", + "viewRule": "@request.auth.role ?= 'admin' || @request.body.order.user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739929352_updated_store_items.js b/db/pb_migrations/1739929352_updated_store_items.js new file mode 100644 index 0000000..53c5250 --- /dev/null +++ b/db/pb_migrations/1739929352_updated_store_items.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "deleteRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "updateRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "deleteRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')", + "updateRule": "@request.auth.roles ?~ 'admin' || (@collection.store_roles.user.id = @request.auth.id && @collection.store_roles.store.id = @request.body.store_item.store.id && @collection.store_roles.role = 'admin')" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739929423_updated_store_items.js b/db/pb_migrations/1739929423_updated_store_items.js new file mode 100644 index 0000000..fee9dbb --- /dev/null +++ b/db/pb_migrations/1739929423_updated_store_items.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store.id &&\n @collection.store_roles.role = 'admin'\n)", + "deleteRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store.id &&\n @collection.store_roles.role = 'admin'\n)", + "updateRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1842453536") + + // update collection data + unmarshal({ + "createRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "deleteRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "updateRule": "@request.auth.roles ?~ 'admin' ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = @request.body.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739946005_updated_orders.js b/db/pb_migrations/1739946005_updated_orders.js new file mode 100644 index 0000000..3997753 --- /dev/null +++ b/db/pb_migrations/1739946005_updated_orders.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' ||\nuser.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "updateRule": "@request.auth.roles ?~ 'admin' ||\nuser.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' ||\nuser.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "updateRule": "", + "viewRule": "@request.auth.roles ?~ 'admin' || (\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = order_items_via_order.store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739946102_updated_order_items.js b/db/pb_migrations/1739946102_updated_order_items.js new file mode 100644 index 0000000..23304c9 --- /dev/null +++ b/db/pb_migrations/1739946102_updated_order_items.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "", + "listRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id", + "listRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.role ?= 'admin' || \n@request.body.order.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739946117_updated_order_items.js b/db/pb_migrations/1739946117_updated_order_items.js new file mode 100644 index 0000000..e552536 --- /dev/null +++ b/db/pb_migrations/1739946117_updated_order_items.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739946138_updated_order_items.js b/db/pb_migrations/1739946138_updated_order_items.js new file mode 100644 index 0000000..8fb3aa8 --- /dev/null +++ b/db/pb_migrations/1739946138_updated_order_items.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "order.user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "createRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739946154_updated_orders.js b/db/pb_migrations/1739946154_updated_orders.js new file mode 100644 index 0000000..6669a1e --- /dev/null +++ b/db/pb_migrations/1739946154_updated_orders.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "createRule": "user.id = @request.auth.id" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3527180448") + + // update collection data + unmarshal({ + "createRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739949362_updated_order_items.js b/db/pb_migrations/1739949362_updated_order_items.js new file mode 100644 index 0000000..a224c65 --- /dev/null +++ b/db/pb_migrations/1739949362_updated_order_items.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "listRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.role ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/db/pb_migrations/1739949416_updated_order_items.js b/db/pb_migrations/1739949416_updated_order_items.js new file mode 100644 index 0000000..431f0e9 --- /dev/null +++ b/db/pb_migrations/1739949416_updated_order_items.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?~ 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?~ 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2456927940") + + // update collection data + unmarshal({ + "listRule": "@request.auth.roles ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)", + "viewRule": "@request.auth.roles ?= 'admin' || \norder.user.id = @request.auth.id ||\n(\n @collection.store_roles.user.id = @request.auth.id &&\n @collection.store_roles.store.id = store_item.store.id &&\n @collection.store_roles.role = 'admin'\n)" + }, collection) + + return app.save(collection) +}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3fe5599..27bbea1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,16 +12,20 @@ "@heroicons/react": "^2.2.0", "@stripe/react-stripe-js": "^3.1.1", "@stripe/stripe-js": "^5.6.0", + "leaflet": "^1.9.4", "next": "15.1.5", "pocketbase": "^0.25.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.1", - "react-icons": "^5.4.0" + "react-icons": "^5.4.0", + "react-leaflet": "^5.0.0", + "recharts": "^2.15.1" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/aspect-ratio": "^0.4.2", + "@types/leaflet": "^1.9.16", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -45,6 +49,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -1030,6 +1046,17 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@react-stately/utils": { "version": "3.10.5", "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", @@ -1140,6 +1167,69 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1147,6 +1237,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1161,6 +1258,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.16.tgz", + "integrity": "sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "20.17.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz", @@ -2052,6 +2159,127 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2131,6 +2359,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2211,6 +2445,16 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2894,6 +3138,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2901,6 +3151,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3444,6 +3703,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4018,6 +4286,12 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4068,6 +4342,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4869,6 +5149,51 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4892,6 +5217,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4915,6 +5278,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5740,6 +6109,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5940,6 +6315,28 @@ "dev": true, "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2118c43..7f09039 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,16 +13,20 @@ "@heroicons/react": "^2.2.0", "@stripe/react-stripe-js": "^3.1.1", "@stripe/stripe-js": "^5.6.0", + "leaflet": "^1.9.4", "next": "15.1.5", "pocketbase": "^0.25.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.1", - "react-icons": "^5.4.0" + "react-icons": "^5.4.0", + "react-leaflet": "^5.0.0", + "recharts": "^2.15.1" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/aspect-ratio": "^0.4.2", + "@types/leaflet": "^1.9.16", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/frontend/public/marker-icon-2x.png b/frontend/public/marker-icon-2x.png new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/frontend/public/marker-icon-2x.png @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/frontend/public/marker-icon.png b/frontend/public/marker-icon.png new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/frontend/public/marker-icon.png @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/frontend/public/marker-shadow.png b/frontend/public/marker-shadow.png new file mode 100644 index 0000000..84c5808 Binary files /dev/null and b/frontend/public/marker-shadow.png differ diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..a3747bf --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,374 @@ +import { config } from '@/config'; + +// Types +export interface User { + id: string; + email: string; + name: string; + token: string; + roles: string[]; +} + +export interface UserResponse { + user: { + id: string; + email: string; + first_name: string; + last_name: string; + roles: string[]; + }; + token: string; +} + +export interface Profile { + first_name: string; + last_name: string; + phone_number: string; + street_1: string; + street_2: string; + city: string; + state: string; + zip: string; +} + +export interface Order { + id: string; + created: string; + status: string; + payment_status: string; + delivery_fee: number; + total_amount: number; + tax_amount: number; + delivery_address?: { + street_address: string[]; + city: string; + state: string; + zip_code: string; + country?: string; + customer_name?: string; + customer_phone?: string; + }; + customer_phone?: string; + stores: { + store: { + id: string; + name: string; + }; + items: { + id: string; + name: string; + quantity: number; + price: number; + }[]; + }[]; +} + +export interface Store { + id: string; + name: string; + street_1: string; + street_2?: string; + city: string; + state: string; + zip_code: string; + instagram?: string; + facebook?: string; + twitter?: string; + items?: StoreItem[]; +} + +export interface StoreItem { + id: string; + name: string; + description: string; + price: number; + quantity: number; + imageUrl?: string; +} + +export interface SavedCard { + id: string; + last4: string; + brand: string; + exp_month: number; + exp_year: number; + isDefault: boolean; +} + +export interface OrderCreateData { + token: string; + user_id: string; + store_id: string; + payment_method_id: string; + items: { + store_item_id: string; + quantity: number; + price: number; + }[]; + subtotal_amount: number; + tax_amount: number; + delivery_fee: number; + total_amount: number; + delivery_address: { + street_address: string[]; + city: string; + state: string; + zip_code: string; + country: string; + }; +} + +// Helper function to handle API responses +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `API Error: ${response.status}`); + } + return response.json(); +} + +// Auth API +export const authApi = { + login: async (email: string, password: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + return handleResponse(response); + }, + + signup: async (email: string, password: string, first_name: string, last_name: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/auth/signup`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + password, + passwordConfirm: password, + first_name, + last_name + }), + }); + return handleResponse(response); + }, + + getProfile: async (token: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/auth/profile`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + updateProfile: async (token: string, profile: Partial): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/auth/profile`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(profile), + }); + return handleResponse(response); + }, +}; + +// Orders API +export const ordersApi = { + getUserOrders: async (token: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/user/orders`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + getAdminOrders: async (token: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/orders`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + getOrder: async (token: string, orderId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/orders/${orderId}`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + createOrder: async (token: string, orderData: OrderCreateData): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/orders`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(orderData), + }); + return handleResponse(response); + }, + + getOrderById: async (token: string, orderId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/orders/${orderId}`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + updateOrderStatus: async (token: string, orderId: string, status: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/orders/${orderId}/status`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ status }), + }); + return handleResponse(response); + }, +}; + +// Stores API +export const storesApi = { + getAllStores: async (): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores`); + return handleResponse(response); + }, + + getStore: async (storeId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}`); + return handleResponse(response); + }, + + getStoreItems: async (storeId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/items`); + return handleResponse(response); + }, + + getStoreOrders: async (token: string, storeId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/orders`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + getStoreRoles: async (token: string, storeId: string): Promise<{ roles: string[] }> => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/roles`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + createStoreItem: async (token: string, storeId: string, itemData: Partial): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/items`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(itemData), + }); + return handleResponse(response); + }, + + updateStoreItem: async (token: string, storeId: string, itemId: string, itemData: Partial): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/items/${itemId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(itemData), + }); + return handleResponse(response); + }, + + deleteStoreItem: async (token: string, storeId: string, itemId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/stores/${storeId}/items/${itemId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + getStoreWithItems: async (storeId: string): Promise => { + const [store, items] = await Promise.all([ + storesApi.getStore(storeId), + storesApi.getStoreItems(storeId) + ]); + return { + ...store, + items: items.map(item => ({ + ...item, + imageUrl: `https://picsum.photos/seed/${item.id}/400/300` + })) + }; + }, + + getAllStoresWithItems: async (): Promise<(Store & { items: StoreItem[] })[]> => { + const stores = await storesApi.getAllStores(); + const storesWithItems = await Promise.all( + stores.map(async (store) => { + const items = await storesApi.getStoreItems(store.id); + return { + ...store, + items: items.map(item => ({ + ...item, + imageUrl: `https://picsum.photos/seed/${item.id}/400/300` + })) + }; + }) + ); + return storesWithItems; + }, +}; + +// Payment API +export const paymentApi = { + getCards: async (token: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/payment/cards`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (response.status === 404) return []; + return handleResponse(response); + }, + + createSetupIntent: async (token: string): Promise<{ clientSecret: string }> => { + const response = await fetch(`${config.apiUrl}/api/v0/payment/setup-intent`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, + + attachCard: async (token: string, paymentMethodId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/payment/cards`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ payment_method_id: paymentMethodId }), + }); + return handleResponse(response); + }, + + deleteCard: async (token: string, cardId: string): Promise => { + const response = await fetch(`${config.apiUrl}/api/v0/payment/cards/${cardId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` }, + }); + return handleResponse(response); + }, +}; + +// Search API +export const searchApi = { + searchProducts: async (query: string): Promise => { + const response = await fetch( + `${config.searchUrl}/api/search?query=${encodeURIComponent(query)}&index=products` + ); + const { hits } = await handleResponse<{ hits: any[] }>(response); + return hits; + }, +}; \ No newline at end of file diff --git a/frontend/src/app/contexts/auth.tsx b/frontend/src/app/contexts/auth.tsx index 59c74b8..4e15cfa 100644 --- a/frontend/src/app/contexts/auth.tsx +++ b/frontend/src/app/contexts/auth.tsx @@ -1,14 +1,7 @@ 'use client'; import React, { createContext, useContext, useState, useEffect } from 'react'; -import { config } from '@/config'; - -interface User { - id: string; - email: string; - name: string; - token: string; -} +import { authApi, User } from '@/api'; interface AuthContextType { user: User | null; @@ -31,24 +24,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }, []); const login = async (email: string, password: string) => { - const response = await fetch(`${config.apiUrl}/api/v0/auth/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - throw new Error('Login failed'); - } - - const data = await response.json(); + const data = await authApi.login(email, password); const userData = { id: data.user.id, email: data.user.email, name: `${data.user.first_name} ${data.user.last_name}`.trim(), token: data.token, + roles: data.user.roles || [], }; setUser(userData); @@ -58,32 +40,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const signup = async (email: string, password: string, name: string) => { // Split name into first and last name const [first_name, ...lastNameParts] = name.trim().split(' '); - const last_name = lastNameParts.join(' ') || ''; // Join remaining parts or empty string + const last_name = lastNameParts.join(' ') || ''; - const response = await fetch(`${config.apiUrl}/api/v0/auth/signup`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email, - password, - passwordConfirm: password, - first_name, - last_name - }), - }); - - if (!response.ok) { - throw new Error('Signup failed'); - } - - const data = await response.json(); + const data = await authApi.signup(email, password, first_name, last_name); const userData = { id: data.user.id, email: data.user.email, name: `${data.user.first_name} ${data.user.last_name}`.trim(), token: data.token, + roles: data.user.roles || [], }; setUser(userData); @@ -108,4 +73,9 @@ export function useAuth() { throw new Error('useAuth must be used within an AuthProvider'); } return context; +} + +export function useIsAdmin() { + const { user } = useAuth(); + return user?.roles?.includes('admin') || false; } \ No newline at end of file diff --git a/frontend/src/app/hooks/useStoreRoles.ts b/frontend/src/app/hooks/useStoreRoles.ts new file mode 100644 index 0000000..9cb3f4e --- /dev/null +++ b/frontend/src/app/hooks/useStoreRoles.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '../contexts/auth'; +import { storesApi } from '@/api'; + +export function useStoreRoles(storeId: string) { + const { user } = useAuth(); + const [roles, setRoles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRoles = async () => { + if (!user?.token) { + return; + } + + try { + const data = await storesApi.getStoreRoles(user.token, storeId); + setRoles(data.roles); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch store roles'); + setRoles([]); + } finally { + setLoading(false); + } + }; + + fetchRoles(); + }, [user, storeId]); + + const isAdmin = roles.includes('admin'); + const isStaff = roles.includes('staff'); + + console.log('roles', roles); + + return { + roles, + isAdmin, + isStaff, + loading, + error + }; +} \ No newline at end of file diff --git a/frontend/src/app/orders/[id]/map.module.css b/frontend/src/app/orders/[id]/map.module.css new file mode 100644 index 0000000..e1021f2 --- /dev/null +++ b/frontend/src/app/orders/[id]/map.module.css @@ -0,0 +1,57 @@ +/* Fix for Leaflet default marker icons */ +.leafletContainer { + z-index: 0; +} + +.leafletContainer :global(.leaflet-div-icon) { + background: transparent; + border: none; +} + +/* Custom marker styling */ +.marker { + width: 25px; + height: 25px; + border-radius: 50%; + background: white; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #2A9D8F; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.marker svg { + width: 14px; + height: 14px; + color: #2A9D8F; +} + +/* Popup styling */ +.leafletContainer :global(.leaflet-popup-content-wrapper) { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.leafletContainer :global(.leaflet-popup-tip) { + background: rgba(255, 255, 255, 0.95); +} + +.leafletContainer :global(.leaflet-popup-content) { + margin: 8px 12px; + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + color: #2D3748; +} + +/* Ensure popups appear above the order panel */ +.leafletContainer :global(.leaflet-popup) { + z-index: 1000; +} + +/* Ensure controls appear above the order panel */ +.leafletContainer :global(.leaflet-control) { + z-index: 1000; +} \ No newline at end of file diff --git a/frontend/src/app/orders/[id]/page.tsx b/frontend/src/app/orders/[id]/page.tsx new file mode 100644 index 0000000..3295f83 --- /dev/null +++ b/frontend/src/app/orders/[id]/page.tsx @@ -0,0 +1,349 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useAuth } from '@/app/contexts/auth'; +import { formatCurrency } from '@/utils/currency'; +import { MapPinIcon, BuildingStorefrontIcon, XMarkIcon, ClockIcon } from '@heroicons/react/24/outline'; +import { useRouter, useParams } from 'next/navigation'; +import dynamic from 'next/dynamic'; +import 'leaflet/dist/leaflet.css'; +import type { LatLngExpression, LatLngTuple } from 'leaflet'; +import styles from './map.module.css'; +import ReactDOMServer from 'react-dom/server'; +import L from 'leaflet'; +import { ordersApi, Order } from '@/api'; + +// Dynamically import the map components to avoid SSR issues +const MapContainer = dynamic( + () => import('react-leaflet').then((mod) => mod.MapContainer), + { ssr: false } +); +const TileLayer = dynamic( + () => import('react-leaflet').then((mod) => mod.TileLayer), + { ssr: false } +); +const Marker = dynamic( + () => import('react-leaflet').then((mod) => mod.Marker), + { ssr: false } +); +const Popup = dynamic( + () => import('react-leaflet').then((mod) => mod.Popup), + { ssr: false } +); +const Polyline = dynamic( + () => import('react-leaflet').then((mod) => mod.Polyline), + { ssr: false } +); + +// Mock coordinates for demo (we'll replace these with real geocoding later) +const MOCK_COORDINATES: { store: LatLngTuple; delivery: LatLngTuple } = { + store: [40.7594, -73.9229], + delivery: [40.7614, -73.9265] +}; + +// Custom marker icon components +const StoreMarker = () => ( +
+ +
+); + +const DeliveryMarker = () => ( +
+ +
+); + +const formatDateTime = (isoString: string) => { + const utcDate = new Date(isoString + 'Z'); + return new Intl.DateTimeFormat('en-US', { + dateStyle: 'medium', + timeStyle: 'short', + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }).format(utcDate); +}; + +export default function OrderViewPage() { + const { user } = useAuth(); + const [order, setOrder] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); + const params = useParams(); + const orderId = params?.id as string; + + // Create a ref for the map instance + const [mapReady, setMapReady] = useState(false); + + useEffect(() => { + const fetchOrder = async () => { + if (!user?.token || !orderId) return; + + try { + const orders = await ordersApi.getAdminOrders(user.token); + const order = orders.find(o => o.id === orderId); + + if (!order) { + throw new Error('Order not found'); + } + + setOrder(order); + } catch (error) { + console.error('Error fetching order:', error); + } finally { + setLoading(false); + } + }; + + if (user) { + fetchOrder(); + } + }, [user, orderId]); + + useEffect(() => { + // This effect ensures Leaflet is only loaded client-side + setMapReady(true); + }, []); + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending': + return 'bg-yellow-100 text-yellow-800' + case 'confirmed': + return 'bg-blue-100 text-blue-800' + case 'picked_up': + return 'bg-purple-100 text-purple-800' + case 'delivered': + return 'bg-green-100 text-green-800' + case 'cancelled': + return 'bg-red-100 text-red-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + const getStatusLabel = (status: string) => { + const labels: { [key: string]: string } = { + pending: 'Delivery Pending', + confirmed: 'Confirmed', + picked_up: 'Picked Up', + delivered: 'Delivered', + cancelled: 'Cancelled' + } + return labels[status] || status.charAt(0).toUpperCase() + status.slice(1) + } + + const getPaymentStatusColor = (status: string) => { + switch (status) { + case 'pending': + return 'bg-yellow-100 text-yellow-800' + case 'processing': + return 'bg-blue-100 text-blue-800' + case 'succeeded': + return 'bg-green-100 text-green-800' + case 'failed': + return 'bg-red-100 text-red-800' + case 'refunded': + return 'bg-gray-100 text-gray-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + const getPaymentStatusLabel = (status: string) => { + const labels: { [key: string]: string } = { + pending: 'Payment Pending', + processing: 'Processing Payment', + succeeded: 'Payment Successful', + failed: 'Payment Failed', + refunded: 'Payment Refunded' + } + return labels[status] || status.charAt(0).toUpperCase() + status.slice(1) + } + + if (loading || !order) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Map Background */} +
+ {mapReady && ( + + + ) + })} + > + +
Store Location
+
{order.stores[0]?.store.name}
+
+
+ ) + })} + > + +
Delivery Location
+ {order.delivery_address && ( +
+ {order.delivery_address.street_address.filter(Boolean).join(', ')} +
+ )} +
+
+ +
+ )} +
+ + {/* Floating Order Panel */} +
+ {/* Back Button */} + + + {/* Main Order Card */} +
+ {/* Header Section */} +
+
+

+ Order #{order.id.slice(-6)} +

+
+ + {getPaymentStatusLabel(order.payment_status)} + + + {getStatusLabel(order.status)} + +
+
+
+ + +
+
+ +
+ {/* Order Items Section */} +
+

Order Details

+
+ {order.stores.map((store) => ( +
+
+ +

{store.store.name}

+
+
+ {store.items.map((item) => ( +
+
+ + {item.quantity} + + {item.name} +
+ {formatCurrency(item.price * item.quantity)} +
+ ))} +
+
+ ))} +
+
+ + {/* Delivery Address Section */} +
+

Delivery Address

+
+ +
+ {order.delivery_address ? ( + <> +

{order.delivery_address.street_address.filter(Boolean).join(', ')}

+

{order.delivery_address.city}, {order.delivery_address.state} {order.delivery_address.zip_code}

+ + ) : ( +

No address available

+ )} +
+
+
+ + {/* Price Breakdown Section */} +
+
+
+ Subtotal + {formatCurrency(order.stores.reduce((acc, store) => + acc + store.items.reduce((itemAcc, item) => itemAcc + (item.price * item.quantity), 0), 0 + ))} +
+
+ Tax + {formatCurrency(order.tax_amount || 0)} +
+
+ Delivery Fee + {formatCurrency(order.delivery_fee)} +
+
+
+ Total + {formatCurrency(order.total_amount)} +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/orders/page.tsx b/frontend/src/app/orders/page.tsx index c861881..73fc68e 100644 --- a/frontend/src/app/orders/page.tsx +++ b/frontend/src/app/orders/page.tsx @@ -4,6 +4,10 @@ import { useState, useEffect } from 'react'; import { useAuth } from '@/app/contexts/auth'; import { toast } from 'react-hot-toast'; import { config } from '@/config'; +import { CurrencyDollarIcon, ShoppingBagIcon, ClockIcon, ChartBarIcon } from '@heroicons/react/24/outline'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { useRouter } from 'next/navigation'; +import { ordersApi, Order } from '@/api'; interface OrderItem { id: string; @@ -16,29 +20,10 @@ interface OrderItem { }; } -interface Order { - id: string; - created: string; - status: string; - payment_status: string; - delivery_fee: number; - total_amount: number; - customer_name: string; - customer_phone?: string; - delivery_address: { - street_address: string[]; - city: string; - state: string; - zip_code: string; - country: string; - } | null; - stores: Array<{ - store: { - id: string; - name: string; - }; - items: OrderItem[]; - }>; +interface DailyCount { + date: string; + timestamp: number; + [storeId: string]: string | number; // Allow string indexes for store IDs } const statusColors = { @@ -50,7 +35,7 @@ const statusColors = { }; const statusLabels = { - pending: 'Pending', + pending: 'Delivery Pending', confirmed: 'Confirmed', picked_up: 'Picked Up', delivered: 'Delivered', @@ -85,66 +70,180 @@ const formatDateTime = (isoString: string) => { }).format(utcDate); }; +function calculateMetrics(orders: Order[]) { + const totalOrders = orders.length; + const totalRevenue = orders.reduce((sum, order) => sum + order.total_amount, 0); + const pendingOrders = orders.filter(order => order.status === 'pending').length; + const avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0; + + return { + totalOrders, + totalRevenue, + pendingOrders, + avgOrderValue + }; +} + +function calculateDailyOrderCounts(orders: Order[]) { + // Get date range for last 30 days + const today = new Date(); + const thirtyDaysAgo = new Date(today); + thirtyDaysAgo.setDate(today.getDate() - 29); // 30 days including today + + // Create array of last 30 days + const days = Array.from({ length: 30 }, (_, i) => { + const date = new Date(thirtyDaysAgo); + date.setDate(thirtyDaysAgo.getDate() + i); + return date; + }); + + // Get unique stores from all orders + const storeIds = new Set(); + const storeNames = new Map(); + orders.forEach(order => { + order.stores.forEach(store => { + storeIds.add(store.store.id); + storeNames.set(store.store.id, store.store.name); + }); + }); + + // Initialize counts for each day with store-specific counts + const dailyCounts: DailyCount[] = days.map(date => { + const baseCount = { + date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + timestamp: date.getTime() + }; + + // Add a count property for each store, initialized to 0 + const storeCounts: { [key: string]: number } = {}; + storeIds.forEach(storeId => { + storeCounts[storeId] = 0; + }); + + return { + ...baseCount, + ...storeCounts + }; + }); + + // Count orders for each day and store + orders.forEach(order => { + const orderDate = new Date(order.created); + const orderDateStr = orderDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + const dayData = dailyCounts.find(d => d.date === orderDateStr); + + if (dayData) { + order.stores.forEach(store => { + const currentCount = dayData[store.store.id] as number; + dayData[store.store.id] = currentCount + 1; + }); + } + }); + + // Convert storeIds to array for consistent ordering + const storeIdsArray = Array.from(storeIds); + + return { + data: dailyCounts, + stores: storeIdsArray.map(id => ({ + id, + name: storeNames.get(id) || 'Unknown Store' + })) + }; +} + +// Add color generation function +function getStoreColor(index: number, opacity: number = 0.9) { + const hue = (166 + index * 30) % 360; + return `hsla(${hue}, 85%, 35%, ${opacity})`; +} + export default function OrdersDashboard() { const { user } = useAuth(); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [selectedStatuses, setSelectedStatuses] = useState>(new Set(Object.keys(statusLabels))); + const [searchTerm, setSearchTerm] = useState(''); + const router = useRouter(); + + const metrics = calculateMetrics(orders); + + // Filter orders based on selected statuses and search term + const filteredOrders = orders.filter(order => { + const matchesStatus = selectedStatuses.has(order.status); + const searchLower = searchTerm.toLowerCase(); + const matchesSearch = !searchTerm || + order.id.toLowerCase().includes(searchLower) || + order.delivery_address?.customer_name.toLowerCase().includes(searchLower); + return matchesStatus && matchesSearch; + }); + + // Toggle status filter + const toggleStatus = (status: string) => { + setSelectedStatuses(prev => { + const newSet = new Set(prev); + if (newSet.has(status)) { + newSet.delete(status); + } else { + newSet.add(status); + } + return newSet; + }); + }; + + // Select/deselect all statuses + const toggleAll = (select: boolean) => { + setSelectedStatuses(new Set(select ? Object.keys(statusLabels) : [])); + }; + + // Check for global admin access + useEffect(() => { + if (!loading && user) { + const isGlobalAdmin = user.roles?.includes('admin'); + if (!isGlobalAdmin) { + router.push('/'); + toast.error("You don't have permission to access this page"); + } + } + }, [loading, user, router]); useEffect(() => { const fetchOrders = async () => { try { - const response = await fetch(`${config.apiUrl}/api/v0/orders`, { - headers: { - 'Authorization': `Bearer ${user?.token}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to fetch orders'); + // Only fetch if user is a global admin + if (!user?.roles?.includes('admin')) { + return; } - const data = await response.json(); - setOrders(data); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch orders'); + const orders = await ordersApi.getAdminOrders(user.token); + setOrders(orders); + } catch (error) { + console.error('Error fetching orders:', error); + setError(error instanceof Error ? error.message : 'Failed to fetch orders'); } finally { setLoading(false); } }; - if (user) { + if (user?.token) { fetchOrders(); } }, [user]); const updateOrderStatus = async (orderId: string, newStatus: string) => { - try { - const response = await fetch(`${config.apiUrl}/api/v0/orders/${orderId}/status`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${user?.token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ status: newStatus }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || 'Failed to update order status'); - } + if (!user?.token) return; - setOrders(currentOrders => - currentOrders.map(order => - order.id === orderId - ? { ...order, status: newStatus } - : order + try { + const updatedOrder = await ordersApi.updateOrderStatus(user.token, orderId, newStatus); + setOrders(prevOrders => + prevOrders.map(order => + order.id === orderId ? updatedOrder : order ) ); - - toast.success('Order status updated'); - } catch (err) { - console.error('Status update error:', err); + toast.success('Order status updated successfully'); + } catch (error) { + console.error('Error updating order status:', error); toast.error('Failed to update order status'); } }; @@ -182,8 +281,181 @@ export default function OrdersDashboard() { return (
-

Orders Dashboard

+ {/* Header Section */} +
+
+
+

Localmart Dashboard

+
+ + {/* Metrics Grid */} +
+ {/* Pending Orders */} +
+
+
+ +
+

Pending Orders

+
+

{metrics.pendingOrders}

+
+ + {/* Total Orders */} +
+
+
+ +
+

Total Orders

+
+

{metrics.totalOrders}

+
+ + {/* Average Order Value */} +
+
+
+ +
+

Avg. Order Value

+
+

${metrics.avgOrderValue.toFixed(2)}

+
+ + {/* Total Revenue */} +
+
+
+ +
+

Total Revenue

+
+

${metrics.totalRevenue.toFixed(2)}

+
+
+
+
+ + {/* Orders Chart */} +
+
+

Daily Orders by Store (Last 30 Days)

+
+ + + + + + + {calculateDailyOrderCounts(orders).stores.map((store, index) => ( + + ))} + + +
+
+
+ + {/* Filters Section */} +
+
+
+ {/* Status Filter */} +
+
+

Filter by Status

+
+ + +
+
+
+ {Object.entries(statusLabels).map(([value, label]) => ( + + ))} +
+
+ + {/* Search */} +
+

Search Orders

+ setSearchTerm(e.target.value)} + className="w-full px-4 py-2 rounded-lg border border-[#2A9D8F]/20 bg-white focus:ring-2 focus:ring-[#2A9D8F] focus:border-transparent" + /> +
+
+
+
+ {/* Results Summary */} +
+ Showing {filteredOrders.length} {filteredOrders.length === 1 ? 'order' : 'orders'} + {selectedStatuses.size < Object.keys(statusLabels).length && + ` with status${selectedStatuses.size === 1 ? '' : 'es'}: ${Array.from(selectedStatuses).map(s => statusLabels[s as keyof typeof statusLabels]).join(', ')}`} + {searchTerm && ` matching "${searchTerm}"`} +
+ + {/* Orders Table */}
@@ -218,78 +490,88 @@ export default function OrdersDashboard() { - {orders.map((order) => ( - - - - - - - + + + + + + + + - - - - - ))} + + + + + ); + })}
- #{order.id.slice(-6)} - - {formatDateTime(order.created)} - - {order.customer_name} - - {order.customer_phone || '-'} - - {order.delivery_address ? ( - <> -
- {order.delivery_address.street_address.filter(Boolean).join(', ')} -
-
- {order.delivery_address.city}, {order.delivery_address.state} {order.delivery_address.zip_code} -
- - ) : ( - No address available - )} -
-
- - {statusLabels[order.status as keyof typeof statusLabels]} + {filteredOrders.map((order) => { + // Get the first store's index for coloring + const storeIndex = calculateDailyOrderCounts(orders).stores + .findIndex(s => s.id === order.stores[0]?.store.id); + + return ( +
+ #{order.id.slice(-6)} + + {formatDateTime(order.created)} + + {order.delivery_address?.customer_name || '-'} + + {order.delivery_address?.customer_phone || '-'} + + {order.delivery_address ? ( + <> +
+ {order.delivery_address.street_address.filter(Boolean).join(', ')} +
+
+ {order.delivery_address.city}, {order.delivery_address.state} {order.delivery_address.zip_code} +
+ + ) : ( + No address available + )} +
+
+ + {statusLabels[order.status as keyof typeof statusLabels]} + + +
+
+ + {paymentStatusLabels[order.payment_status as keyof typeof paymentStatusLabels]} - +
+ {order.stores.map((store) => ( +
+
{store.store.name}
+ {store.items.map((item) => ( +
+ {item.quantity}x {item.name} (${item.price.toFixed(2)}) +
+ ))} +
))} - -
-
- - {paymentStatusLabels[order.payment_status as keyof typeof paymentStatusLabels]} - - -
- {order.stores.map((store) => ( -
-
{store.store.name}
- {store.items.map((item) => ( -
- {item.quantity}x {item.name} (${item.price.toFixed(2)}) -
- ))} -
- ))} -
-
- ${order.total_amount.toFixed(2)} -
+ ${order.total_amount.toFixed(2)} +
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 4c44d1e..9e08d7f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,38 +2,26 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { BuildingStorefrontIcon } from '@heroicons/react/24/outline'; +import { BuildingStorefrontIcon, PlusIcon } from '@heroicons/react/24/outline'; import { SearchProvider } from '@/app/contexts/search'; import { Search } from '@/components/Search'; import { useFeatureFlag } from '@/app/contexts/featureFlags'; - -interface Store { - id: string; - name: string; - street_1: string; - street_2?: string; - city: string; - state: string; - zip_code: string; -} +import { useCart } from '@/app/contexts/cart'; +import { toast } from 'react-hot-toast'; +import { storesApi, Store, StoreItem } from '@/api'; export default function Page() { const [stores, setStores] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const productSearchEnabled = useFeatureFlag('product_search'); + const { addItem, items: cartItems } = useCart(); useEffect(() => { const fetchStores = async () => { try { - // Use environment variable with fallback for client-side fetching - const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; - const response = await fetch(`${baseUrl}/api/v0/stores`); - if (!response.ok) { - throw new Error('Failed to fetch stores'); - } - const data = await response.json(); - setStores(data); + const storesWithItems = await storesApi.getAllStoresWithItems(); + setStores(storesWithItems); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch stores'); } finally { @@ -44,12 +32,36 @@ export default function Page() { fetchStores(); }, []); + const handleAddToCart = (storeId: string, item: StoreItem) => { + const hasItemsFromDifferentStore = cartItems.length > 0 && cartItems[0].store !== storeId; + + if (hasItemsFromDifferentStore) { + toast.error('You can only add items from one store at a time'); + return; + } + + addItem({ + ...item, + store: storeId + }); + toast.success('Added to cart'); + }; + if (loading) { return (
-
+
+ {/* Hero section skeleton */} +
+ {/* Featured stores skeleton */}
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ {/* Items grid skeleton */} +
{[1, 2, 3, 4, 5, 6].map((i) => (
))} @@ -76,51 +88,153 @@ export default function Page() { return (
{/* Hero Section */} -
-
-
-
- -

localmart

+
+ {/* Decorative Background Pattern */} +
+ + {/* Gradient Overlay */} +
+ +
+
+ {/* Region Select */} +
+ + +
+ + {/* Main Hero Content */} +
+
+

localmart

+ +
+

+ Shop local. Same-day delivery. +
+ From your favorite neighborhood stores. +

+ + {productSearchEnabled && } +
-

- Shop from your favorite local stores with same-day delivery -

- - { productSearchEnabled && } -
- {/* Stores Grid */} -
-
- {stores.map((store) => ( - -
-
- {store.name} -
-
-

{store.name}

-

- {store.street_1} - {store.street_2 && `, ${store.street_2}`} -
- {store.city}, {store.state} {store.zip_code} -

+ {/* Main Content Container */} +
+ {/* Local Stores Section - Commented Out +
+

Local Stores

+
+ {stores.map((store) => ( + +
+
+ {store.name} +
+
+

+ {store.name} +

+

+ {store.street_1} + {store.street_2 && `, ${store.street_2}`} +
+ {store.city}, {store.state} {store.zip_code} +

+

+ View store + +

+
+ + ))} +
+
+ */} + + {/* Popular Items Sections */} +
+ {stores.map((store, storeIndex) => store.items && store.items.length > 0 && ( +
+
+ +

{store.name}

+ + + See all + + +
+
+ {store.items.slice(0, 3).map((item) => ( +
+ +
+ {item.name} +
+ +
+ +

+ {item.name} +

+ +
+

${item.price.toFixed(2)}

+ +
+
+
+ ))}
- +
))}
diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 4f1bc07..cdebf3d 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -6,30 +6,11 @@ import { toast } from 'react-hot-toast'; import { config } from '@/config'; import { loadStripe } from '@stripe/stripe-js'; import { Elements, useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; +import { authApi, paymentApi, Profile, SavedCard } from '@/api'; // Initialize Stripe const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!); -interface UserProfile { - first_name: string; - last_name: string; - phone_number: string; - street_1: string; - street_2: string; - city: string; - state: string; - zip: string; -} - -interface SavedCard { - id: string; - last4: string; - brand: string; - exp_month: number; - exp_year: number; - isDefault: boolean; -} - // Card form component function CardForm({ onSuccess }: { onSuccess: () => void }) { const stripe = useStripe(); @@ -44,18 +25,7 @@ function CardForm({ onSuccess }: { onSuccess: () => void }) { setLoading(true); try { // Get setup intent from backend - const setupResponse = await fetch(`${config.apiUrl}/api/v0/payment/setup-intent`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${user.token}`, - } - }); - - if (!setupResponse.ok) { - throw new Error('Failed to create setup intent'); - } - - const { clientSecret } = await setupResponse.json(); + const { clientSecret } = await paymentApi.createSetupIntent(user.token); // Confirm card setup const result = await stripe.confirmCardSetup(clientSecret, { @@ -71,22 +41,13 @@ function CardForm({ onSuccess }: { onSuccess: () => void }) { throw new Error(result.error.message); } - // Notify backend of successful setup - const attachResponse = await fetch(`${config.apiUrl}/api/v0/payment/cards`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${user.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - payment_method_id: result.setupIntent!.payment_method, - }), - }); - - if (!attachResponse.ok) { - throw new Error('Failed to save card'); + if (!result.setupIntent?.payment_method || typeof result.setupIntent.payment_method !== 'string') { + throw new Error('Invalid payment method'); } + // Notify backend of successful setup + await paymentApi.attachCard(user.token, result.setupIntent.payment_method); + toast.success('Card added successfully'); onSuccess(); } catch (error) { @@ -128,11 +89,7 @@ function CardForm({ onSuccess }: { onSuccess: () => void }) { export default function ProfilePage() { const { user } = useAuth(); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [showCardForm, setShowCardForm] = useState(false); - const [savedCards, setSavedCards] = useState([]); - const [profile, setProfile] = useState({ + const [profile, setProfile] = useState({ first_name: '', last_name: '', phone_number: '', @@ -142,6 +99,10 @@ export default function ProfilePage() { state: '', zip: '' }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showAddCard, setShowAddCard] = useState(false); + const [savedCards, setSavedCards] = useState([]); const formatPhoneNumber = (value: string) => { // Remove all non-digits @@ -167,27 +128,8 @@ export default function ProfilePage() { if (!user?.token) return; try { - const response = await fetch(`${config.apiUrl}/api/v0/auth/profile`, { - headers: { - 'Authorization': `Bearer ${user.token}` - } - }); - - if (!response.ok) { - throw new Error('Failed to fetch profile'); - } - - const data = await response.json(); - setProfile({ - first_name: data.first_name || '', - last_name: data.last_name || '', - phone_number: data.phone_number || '', - street_1: data.street_1 || '', - street_2: data.street_2 || '', - city: data.city || '', - state: data.state || '', - zip: data.zip || '' - }); + const data = await authApi.getProfile(user.token); + setProfile(data); } catch (error) { console.error('Error fetching profile:', error); toast.error('Failed to load profile'); @@ -203,30 +145,10 @@ export default function ProfilePage() { if (!user?.token) return; try { - const response = await fetch(`${config.apiUrl}/api/v0/payment/cards`, { - headers: { - 'Authorization': `Bearer ${user.token}` - } - }); - - if (response.status === 404) { - // No cards is a valid state - setSavedCards([]); - return; - } - - if (!response.ok) { - console.error('Error fetching cards:', response.status, response.statusText); - const errorData = await response.json().catch(() => ({})); - console.error('Error details:', errorData); - throw new Error('Failed to fetch cards'); - } - - const data = await response.json(); - setSavedCards(Array.isArray(data) ? data : []); + const cards = await paymentApi.getCards(user.token); + setSavedCards(cards); } catch (error) { console.error('Error fetching cards:', error); - // Don't show error toast for no cards setSavedCards([]); } }; @@ -241,19 +163,7 @@ export default function ProfilePage() { setSaving(true); try { - const response = await fetch(`${config.apiUrl}/api/v0/auth/profile`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${user.token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(profile) - }); - - if (!response.ok) { - throw new Error('Failed to update profile'); - } - + await authApi.updateProfile(user.token, profile); toast.success('Profile updated successfully'); } catch (error) { console.error('Error updating profile:', error); @@ -267,17 +177,7 @@ export default function ProfilePage() { if (!user?.token) return; try { - const response = await fetch(`${config.apiUrl}/api/v0/payment/cards/${cardId}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${user.token}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to remove card'); - } - + await paymentApi.deleteCard(user.token, cardId); toast.success('Card removed successfully'); setSavedCards(cards => cards.filter(card => card.id !== cardId)); } catch (error) { @@ -505,18 +405,18 @@ export default function ProfilePage() {
{/* Add Card Section */} - {showCardForm ? ( + {showAddCard ? ( { - setShowCardForm(false); + setShowAddCard(false); fetchCards(); }} /> ) : ( + +
+
+
+ {Object.entries(statusLabels).map(([value, label]) => ( + + ))} +
+
+ + {/* Search */} +
+

Search Orders

+ setSearchTerm(e.target.value)} + className="w-full px-4 py-2 rounded-lg border border-[#2A9D8F]/20 bg-white focus:ring-2 focus:ring-[#2A9D8F] focus:border-transparent" + /> +
+
+
+
+ + {/* Results Summary */} +
+ Showing {filteredOrders.length} {filteredOrders.length === 1 ? 'order' : 'orders'} + {selectedStatuses.size < Object.keys(statusLabels).length && + ` with status${selectedStatuses.size === 1 ? '' : 'es'}: ${Array.from(selectedStatuses).map(s => statusLabels[s as keyof typeof statusLabels]).join(', ')}`} + {searchTerm && ` matching "${searchTerm}"`} +
+ + {/* Table */} +
+ + + + + + + + + + + + + + + + {filteredOrders.map((order) => ( + + + + + + + + + + + + ))} + +
+ Order ID + + Date & Time + + Customer + + Phone + + Delivery Address + + Status + + Payment + + Items + + Total +
+ #{order.id.slice(-6)} + + {formatDateTime(order.created)} + + {order.delivery_address?.customer_name || '-'} + + {order.delivery_address?.customer_phone || '-'} + + {order.delivery_address ? ( + <> +
+ {order.delivery_address.street_address.filter(Boolean).join(', ')} +
+
+ {order.delivery_address.city}, {order.delivery_address.state} {order.delivery_address.zip_code} +
+ + ) : ( + No address available + )} +
+ + {statusLabels[order.status as keyof typeof statusLabels]} + + + + {paymentStatusLabels[order.payment_status as keyof typeof paymentStatusLabels]} + + +
+ {order.stores.map((store) => ( +
+ {store.items.map((item) => ( +
+ {item.quantity}x {item.name} (${item.price.toFixed(2)}) +
+ ))} +
+ ))} +
+
+ ${order.total_amount.toFixed(2)} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/store/[id]/inventory/page.tsx b/frontend/src/app/store/[id]/inventory/page.tsx new file mode 100644 index 0000000..969f625 --- /dev/null +++ b/frontend/src/app/store/[id]/inventory/page.tsx @@ -0,0 +1,320 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useAuth } from '@/app/contexts/auth'; +import { useStoreRoles } from '@/app/hooks/useStoreRoles'; +import { toast } from 'react-hot-toast'; +import { useRouter } from 'next/navigation'; +import { use } from 'react'; +import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { storesApi, StoreItem } from '@/api'; + +interface EditItemModalProps { + item?: StoreItem; + isOpen: boolean; + onClose: () => void; + onSave: (item: Partial) => void; +} + +function EditItemModal({ item, isOpen, onClose, onSave }: EditItemModalProps) { + const [name, setName] = useState(item?.name || ''); + const [price, setPrice] = useState(item?.price?.toString() || ''); + const [description, setDescription] = useState(item?.description || ''); + + useEffect(() => { + if (isOpen) { + setName(item?.name || ''); + setPrice(item?.price?.toString() || ''); + setDescription(item?.description || ''); + } + }, [isOpen, item]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ + name, + price: parseFloat(price), + description + }); + }; + + if (!isOpen) return null; + + return ( +
+
+

+ {item ? 'Edit Item' : 'Add New Item'} +

+
+
+ + setName(e.target.value)} + className="mt-1 block w-full rounded-md border border-[#2A9D8F]/20 px-3 py-2 focus:border-[#2A9D8F] focus:ring focus:ring-[#2A9D8F]/50" + required + /> +
+
+ + setPrice(e.target.value)} + step="0.01" + min="0" + className="mt-1 block w-full rounded-md border border-[#2A9D8F]/20 px-3 py-2 focus:border-[#2A9D8F] focus:ring focus:ring-[#2A9D8F]/50" + required + /> +
+
+ +