From 531a385dba66c04a262d13748fdd89954b1f268d Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 17 Jan 2025 17:04:41 -0500 Subject: [PATCH] feat: Working on search for purchase orders. --- Package.resolved | 20 +--- Package.swift | 3 - Resources/Views/btn/close-form.leaf | 6 -- Resources/Views/btn/toggle-form.leaf | 6 -- Resources/Views/employees/detail.leaf | 52 ---------- Resources/Views/employees/form.leaf | 22 ----- Resources/Views/employees/index.leaf | 17 ---- Resources/Views/employees/table-row.leaf | 33 ------- Resources/Views/employees/table.leaf | 23 ----- Resources/Views/form-container.leaf | 4 - Resources/Views/home.leaf | 15 --- Resources/Views/htmx-form.leaf | 24 ----- Resources/Views/img/pencil.leaf | 1 - Resources/Views/img/spinner.leaf | 1 - Resources/Views/img/trash-can.leaf | 1 - Resources/Views/index.leaf | 14 --- Resources/Views/login.leaf | 12 --- Resources/Views/logo.leaf | 1 - Resources/Views/navbar.leaf | 37 ------- Resources/Views/purchaseOrders/detail.leaf | 56 ----------- Resources/Views/purchaseOrders/form.leaf | 43 -------- Resources/Views/purchaseOrders/index.leaf | 46 --------- Resources/Views/purchaseOrders/table-row.leaf | 20 ---- Resources/Views/purchaseOrders/table.leaf | 18 ---- Resources/Views/users/detail.leaf | 36 ------- Resources/Views/users/form.leaf | 40 -------- Resources/Views/users/index.leaf | 18 ---- Resources/Views/users/table-row.leaf | 14 --- Resources/Views/users/table.leaf | 14 --- Resources/Views/vendors/form.leaf | 35 ------- Resources/Views/vendors/index.leaf | 16 --- Resources/Views/vendors/table.leaf | 49 ---------- .../View/Contexts/HtmxFormCTX.swift | 97 ------------------- .../View/PurchaseOrderViewController.swift | 82 ++++++++++++++-- .../View/UtilsViewController.swift | 38 ++++++++ .../Extensions/RouteBuilder+protected.swift | 16 +-- .../PurchaseOrders/PurchaseOrderForm.swift | 8 +- .../PurchaseOrders/PurchaseOrderSearch.swift | 50 ++++++++++ .../PurchaseOrders/PurchaseOrderTable.swift | 4 +- Sources/App/Views/Utils/EmployeeSelect.swift | 47 +++++++++ Sources/App/Views/Utils/Navbar.swift | 2 +- Sources/App/configure.swift | 2 - Sources/App/routes.swift | 18 +++- Sources/DatabaseClient/PurchaseOrders.swift | 1 + .../DatabaseClientLive/PurchaseOrders.swift | 32 ++++++ Sources/SharedModels/PurchaseOrder.swift | 6 ++ 46 files changed, 283 insertions(+), 817 deletions(-) delete mode 100644 Resources/Views/btn/close-form.leaf delete mode 100644 Resources/Views/btn/toggle-form.leaf delete mode 100644 Resources/Views/employees/detail.leaf delete mode 100644 Resources/Views/employees/form.leaf delete mode 100644 Resources/Views/employees/index.leaf delete mode 100644 Resources/Views/employees/table-row.leaf delete mode 100644 Resources/Views/employees/table.leaf delete mode 100644 Resources/Views/form-container.leaf delete mode 100644 Resources/Views/home.leaf delete mode 100644 Resources/Views/htmx-form.leaf delete mode 100644 Resources/Views/img/pencil.leaf delete mode 100644 Resources/Views/img/spinner.leaf delete mode 100644 Resources/Views/img/trash-can.leaf delete mode 100644 Resources/Views/index.leaf delete mode 100644 Resources/Views/login.leaf delete mode 100644 Resources/Views/logo.leaf delete mode 100644 Resources/Views/navbar.leaf delete mode 100644 Resources/Views/purchaseOrders/detail.leaf delete mode 100644 Resources/Views/purchaseOrders/form.leaf delete mode 100644 Resources/Views/purchaseOrders/index.leaf delete mode 100644 Resources/Views/purchaseOrders/table-row.leaf delete mode 100644 Resources/Views/purchaseOrders/table.leaf delete mode 100644 Resources/Views/users/detail.leaf delete mode 100644 Resources/Views/users/form.leaf delete mode 100644 Resources/Views/users/index.leaf delete mode 100644 Resources/Views/users/table-row.leaf delete mode 100644 Resources/Views/users/table.leaf delete mode 100644 Resources/Views/vendors/form.leaf delete mode 100644 Resources/Views/vendors/index.leaf delete mode 100644 Resources/Views/vendors/table.leaf delete mode 100644 Sources/App/Controllers/View/Contexts/HtmxFormCTX.swift create mode 100644 Sources/App/Controllers/View/UtilsViewController.swift create mode 100644 Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift create mode 100644 Sources/App/Views/Utils/EmployeeSelect.swift diff --git a/Package.resolved b/Package.resolved index 9840710..dd38eb2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b5426b686e9548f4fc69224d4e0f13d10ce55816d5b71622f5f446b12d66140a", + "originHash" : "aefc6edf3bfecf4e8b49731482d5e8f4fd78c1188ba5d90f5b78a36ab8106df6", "pins" : [ { "identity" : "async-http-client", @@ -82,24 +82,6 @@ "version" : "4.8.0" } }, - { - "identity" : "leaf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/leaf.git", - "state" : { - "revision" : "bf48d2423c00292b5937c60166c7db99705cae47", - "version" : "4.4.1" - } - }, - { - "identity" : "leaf-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/leaf-kit.git", - "state" : { - "revision" : "d0ca4417166ef7868d28ad21bc77d36b8735a0fc", - "version" : "1.11.1" - } - }, { "identity" : "multipart-kit", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 4029233..274d6c1 100644 --- a/Package.swift +++ b/Package.swift @@ -19,8 +19,6 @@ let package = Package( .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), // 🪶 Fluent driver for SQLite. .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), - // 🍃 An expressive, performant, and extensible templating language built for Swift. - .package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"), // 🔵 Non-blocking, event-driven networking for Swift. Used for, custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), @@ -35,7 +33,6 @@ let package = Package( "DatabaseClientLive", .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), - .product(name: "Leaf", package: "leaf"), .product(name: "Vapor", package: "vapor"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), diff --git a/Resources/Views/btn/close-form.leaf b/Resources/Views/btn/close-form.leaf deleted file mode 100644 index df28ebf..0000000 --- a/Resources/Views/btn/close-form.leaf +++ /dev/null @@ -1,6 +0,0 @@ - - × - diff --git a/Resources/Views/btn/toggle-form.leaf b/Resources/Views/btn/toggle-form.leaf deleted file mode 100644 index 44be1e2..0000000 --- a/Resources/Views/btn/toggle-form.leaf +++ /dev/null @@ -1,6 +0,0 @@ - - + - diff --git a/Resources/Views/employees/detail.leaf b/Resources/Views/employees/detail.leaf deleted file mode 100644 index 04117c7..0000000 --- a/Resources/Views/employees/detail.leaf +++ /dev/null @@ -1,52 +0,0 @@ - diff --git a/Resources/Views/employees/form.leaf b/Resources/Views/employees/form.leaf deleted file mode 100644 index a3f3f61..0000000 --- a/Resources/Views/employees/form.leaf +++ /dev/null @@ -1,22 +0,0 @@ -#extend("htmx-form", htmxForm): - #export("formBody"): - -
- -
- - #endexport -#endextend diff --git a/Resources/Views/employees/index.leaf b/Resources/Views/employees/index.leaf deleted file mode 100644 index d99ea03..0000000 --- a/Resources/Views/employees/index.leaf +++ /dev/null @@ -1,17 +0,0 @@ -#extend("home"): - #export("homeContent"): -
-
-

Employees

-
-

Employees are who purchase orders can be generated for.

-
-
- #extend("employees/detail") - #extend("form-container"): #export("formContent"): - #extend("employees/form", form) - #endexport #endextend - #extend("employees/table") -
- #endexport -#endextend diff --git a/Resources/Views/employees/table-row.leaf b/Resources/Views/employees/table-row.leaf deleted file mode 100644 index 9b1219a..0000000 --- a/Resources/Views/employees/table-row.leaf +++ /dev/null @@ -1,33 +0,0 @@ - - #capitalized(firstName) #capitalized(lastName) - - #if(active): - - Active - - #else: - - Active - - #endif - - - - - diff --git a/Resources/Views/employees/table.leaf b/Resources/Views/employees/table.leaf deleted file mode 100644 index 14e014b..0000000 --- a/Resources/Views/employees/table.leaf +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - #for(employee in employees): - #extend("employees/table-row", employee) - #endfor - -
NameActive - - + - -
diff --git a/Resources/Views/form-container.leaf b/Resources/Views/form-container.leaf deleted file mode 100644 index 2a96da0..0000000 --- a/Resources/Views/form-container.leaf +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/Resources/Views/home.leaf b/Resources/Views/home.leaf deleted file mode 100644 index fd089e0..0000000 --- a/Resources/Views/home.leaf +++ /dev/null @@ -1,15 +0,0 @@ -#extend("index"): - #export("content"): -
-
-
- #extend("logo") - #extend("navbar") -
-
-
- #import("homeContent") -
-
- #endexport -#endextend diff --git a/Resources/Views/htmx-form.leaf b/Resources/Views/htmx-form.leaf deleted file mode 100644 index b9dbcd3..0000000 --- a/Resources/Views/htmx-form.leaf +++ /dev/null @@ -1,24 +0,0 @@ -
- #import("formBody") -
diff --git a/Resources/Views/img/pencil.leaf b/Resources/Views/img/pencil.leaf deleted file mode 100644 index 6276d6e..0000000 --- a/Resources/Views/img/pencil.leaf +++ /dev/null @@ -1 +0,0 @@ -Edit diff --git a/Resources/Views/img/spinner.leaf b/Resources/Views/img/spinner.leaf deleted file mode 100644 index 68dd7d1..0000000 --- a/Resources/Views/img/spinner.leaf +++ /dev/null @@ -1 +0,0 @@ -Spinner diff --git a/Resources/Views/img/trash-can.leaf b/Resources/Views/img/trash-can.leaf deleted file mode 100644 index f41997e..0000000 --- a/Resources/Views/img/trash-can.leaf +++ /dev/null @@ -1 +0,0 @@ -Delete diff --git a/Resources/Views/index.leaf b/Resources/Views/index.leaf deleted file mode 100644 index ce7efa7..0000000 --- a/Resources/Views/index.leaf +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - #(title) - - - #import("content") - - diff --git a/Resources/Views/login.leaf b/Resources/Views/login.leaf deleted file mode 100644 index 6f02767..0000000 --- a/Resources/Views/login.leaf +++ /dev/null @@ -1,12 +0,0 @@ -#extend("index"): -#export("content"): -
-
-
- #extend("logo") -
-
- #extend("users/form") -
-#endexport -#endextend diff --git a/Resources/Views/logo.leaf b/Resources/Views/logo.leaf deleted file mode 100644 index ed9262e..0000000 --- a/Resources/Views/logo.leaf +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Resources/Views/navbar.leaf b/Resources/Views/navbar.leaf deleted file mode 100644 index 975a90a..0000000 --- a/Resources/Views/navbar.leaf +++ /dev/null @@ -1,37 +0,0 @@ -
- × - - Purchase Orders - - - Users - - - Employees - - - Vendors - -
- - Logout - -
- diff --git a/Resources/Views/purchaseOrders/detail.leaf b/Resources/Views/purchaseOrders/detail.leaf deleted file mode 100644 index e3bd857..0000000 --- a/Resources/Views/purchaseOrders/detail.leaf +++ /dev/null @@ -1,56 +0,0 @@ -
- #if(purchaseOrderDetail): - - × - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Purchase Order:

#(purchaseOrderDetail.id)

Work Order:

#(purchaseOrderDetail.workOrder)

Customer:

#(purchaseOrderDetail.customer)

Vendor:

#capitalized(purchaseOrderDetail.vendorBranch.vendor.name) - #capitalized(purchaseOrderDetail.vendorBranch.name)

Materials:

#(purchaseOrderDetail.materials)

Created For:

#capitalized(purchaseOrderDetail.createdFor.firstName) #capitalized(purchaseOrderDetail.createdFor.lastName)

Truck Stock:

#capitalized(purchaseOrderDetail.truckStock)

Created By:

#(purchaseOrderDetail.createdBy.username)

Date:

#date(purchaseOrderDetail.createdAt, "MM-dd-yyyy")

Updated:

#date(purchaseOrderDetail.updatedAt, "MM-dd-yyyy")

- #endif -
diff --git a/Resources/Views/purchaseOrders/form.leaf b/Resources/Views/purchaseOrders/form.leaf deleted file mode 100644 index c5c60ea..0000000 --- a/Resources/Views/purchaseOrders/form.leaf +++ /dev/null @@ -1,43 +0,0 @@ -#extend("htmx-form", htmxForm): - #export("formBody"): - -
- -
- -
- -
- - -
- - - - #endexport -#endextend diff --git a/Resources/Views/purchaseOrders/index.leaf b/Resources/Views/purchaseOrders/index.leaf deleted file mode 100644 index c32f2b6..0000000 --- a/Resources/Views/purchaseOrders/index.leaf +++ /dev/null @@ -1,46 +0,0 @@ -#extend("home"): - #export("homeContent"): -
-
-

Purchase Orders

-
-
- #extend("form-container"): #export("formContent"): - #extend("purchaseOrders/form", form) - #endexport #endextend -
-
-
- #if(hasPrevious): - - #endif - #if(hasNext): - - #endif -
- #extend("purchaseOrders/table") -
- #endexport -#endextend diff --git a/Resources/Views/purchaseOrders/table-row.leaf b/Resources/Views/purchaseOrders/table-row.leaf deleted file mode 100644 index 8286a2d..0000000 --- a/Resources/Views/purchaseOrders/table-row.leaf +++ /dev/null @@ -1,20 +0,0 @@ - - #(id) - #(workOrder) - #(customer) - #capitalized(vendorBranch.vendor.name) - #capitalized(vendorBranch.name) - #(materials) - #capitalized(createdFor.firstName) #capitalized(createdFor.lastName) - #(createdBy.username) - #capitalized(truckStock) - - - - diff --git a/Resources/Views/purchaseOrders/table.leaf b/Resources/Views/purchaseOrders/table.leaf deleted file mode 100644 index 118206c..0000000 --- a/Resources/Views/purchaseOrders/table.leaf +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - #for(po in purchaseOrders): - #extend("purchaseOrders/table-row", po) - #endfor - -
POWork OrderCustomerVendorMaterialsCreated ForCreated ByTruck Stock#extend("btn/toggle-form")
diff --git a/Resources/Views/users/detail.leaf b/Resources/Views/users/detail.leaf deleted file mode 100644 index c13d52c..0000000 --- a/Resources/Views/users/detail.leaf +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/Resources/Views/users/form.leaf b/Resources/Views/users/form.leaf deleted file mode 100644 index 15f4c87..0000000 --- a/Resources/Views/users/form.leaf +++ /dev/null @@ -1,40 +0,0 @@ -#extend("htmx-form", htmxForm): - #export("formBody"): - -
- #if(context.showEmailInput): - -
- #endif - -
- #if(context.showConfirmPassword): - -
- #endif - - #endexport -#endextend diff --git a/Resources/Views/users/index.leaf b/Resources/Views/users/index.leaf deleted file mode 100644 index 87bb9c8..0000000 --- a/Resources/Views/users/index.leaf +++ /dev/null @@ -1,18 +0,0 @@ -#extend("home"): - #export("homeContent"): -
-
-

Users

-
-

Users are people that can login and generate puchase orders for employees.

-
-
- #extend("users/detail") - #extend("form-container"): #export("formContent"): - #extend("users/form", form) - #endexport #endextend - - #extend("users/table") -
- #endexport -#endextend diff --git a/Resources/Views/users/table-row.leaf b/Resources/Views/users/table-row.leaf deleted file mode 100644 index 1baf71b..0000000 --- a/Resources/Views/users/table-row.leaf +++ /dev/null @@ -1,14 +0,0 @@ - - #(username) - #(email) - - - - diff --git a/Resources/Views/users/table.leaf b/Resources/Views/users/table.leaf deleted file mode 100644 index 6b69087..0000000 --- a/Resources/Views/users/table.leaf +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - #for(user in users): - #extend("users/table-row", user) - #endfor - -
UsernameEmail#extend("btn/toggle-form")
diff --git a/Resources/Views/vendors/form.leaf b/Resources/Views/vendors/form.leaf deleted file mode 100644 index c34a1d1..0000000 --- a/Resources/Views/vendors/form.leaf +++ /dev/null @@ -1,35 +0,0 @@ -#extend("htmx-form", htmxForm): - #export("formBody"): - - - -
- -
- -
-

Mutliple branches can be specified separated by a comma.

- #endexport -#endextend diff --git a/Resources/Views/vendors/index.leaf b/Resources/Views/vendors/index.leaf deleted file mode 100644 index 0afa9be..0000000 --- a/Resources/Views/vendors/index.leaf +++ /dev/null @@ -1,16 +0,0 @@ -#extend("home"): - #export("homeContent"): -
-
-

Vendors

-
-

Vendors are who purchase orders can be issued for, they consist of multiple branches / locations.

-
- #extend("form-container"): #export("formContent"): - #extend("vendors/form", form) - #endexport #endextend - #extend("vendors/table") -
-
- #endexport -#endextend diff --git a/Resources/Views/vendors/table.leaf b/Resources/Views/vendors/table.leaf deleted file mode 100644 index 87f802c..0000000 --- a/Resources/Views/vendors/table.leaf +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - #for(vendor in vendors): - - - - - - - #endfor - -
NameBranches#extend("btn/toggle-form")
#capitalized(vendor.name) - #if(vendor.branches): -
    - #for(branch in vendor.branches): -
  • -
    -
    #capitalized(branch.name)
    - - × - -
    -
  • - #endfor -
- #endif -
- - #extend("img/trash-can") - -
diff --git a/Sources/App/Controllers/View/Contexts/HtmxFormCTX.swift b/Sources/App/Controllers/View/Contexts/HtmxFormCTX.swift deleted file mode 100644 index 5bedf7d..0000000 --- a/Sources/App/Controllers/View/Contexts/HtmxFormCTX.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Vapor - -/// Represents a generic form context that is used to generate form templates -/// that are handled by htmx. -struct HtmxFormCTX: Content { - let formClass: String? - let formId: String - let htmxPostTargetUrl: String? - let htmxPutTargetUrl: String? - let htmxTarget: String - let htmxPushUrl: Bool - let htmxResetAfterRequest: Bool - let htmxSwapOob: String? - let htmxSwap: String? - let context: Context - - init( - formClass: String? = nil, - formId: String, - htmxTargetUrl: TargetUrl, - htmxTarget: String, - htmxPushUrl: Bool, - htmxResetAfterRequest: Bool = true, - htmxSwapOob: HtmxSwap? = nil, - htmxSwap: HtmxSwap? = nil, - context: Context - ) { - self.formClass = formClass - self.formId = formId - self.htmxPostTargetUrl = htmxTargetUrl.postUrl - self.htmxPutTargetUrl = htmxTargetUrl.putUrl - self.htmxTarget = htmxTarget - self.htmxPushUrl = htmxPushUrl - self.htmxResetAfterRequest = htmxResetAfterRequest - self.htmxSwapOob = htmxSwapOob?.rawValue - self.htmxSwap = htmxSwap?.rawValue - self.context = context - } - - enum HtmxSwap: String { - case innerHTML - case outerHTML - case afterbegin - case beforebegin - case afterend - case beforeend - case delete - case none - } - - enum TargetUrl { - case put(String) - case post(String) - - var putUrl: String? { - guard case let .put(url) = self else { return nil } - return url - } - - var postUrl: String? { - guard case let .post(url) = self else { return nil } - return url - } - } -} - -struct EmptyContent: Content {} - -struct ButtonLabelContext: Content { - let buttonLabel: String -} - -extension HtmxFormCTX where Context == ButtonLabelContext { - init( - formClass: String? = nil, - formId: String, - htmxTargetUrl: TargetUrl, - htmxTarget: String, - htmxPushUrl: Bool, - htmxResetAfterRequest: Bool = true, - htmxSwapOob: HtmxSwap? = nil, - htmxSwap: HtmxSwap? = nil, - buttonLabel: String - ) { - self.init( - formClass: formClass, - formId: formId, - htmxTargetUrl: htmxTargetUrl, - htmxTarget: htmxTarget, - htmxPushUrl: htmxPushUrl, - htmxResetAfterRequest: htmxResetAfterRequest, - htmxSwapOob: htmxSwapOob, - htmxSwap: htmxSwapOob, - context: .init(buttonLabel: buttonLabel) - ) - } -} diff --git a/Sources/App/Controllers/View/PurchaseOrderViewController.swift b/Sources/App/Controllers/View/PurchaseOrderViewController.swift index da7aa0a..3ca02a4 100644 --- a/Sources/App/Controllers/View/PurchaseOrderViewController.swift +++ b/Sources/App/Controllers/View/PurchaseOrderViewController.swift @@ -14,6 +14,8 @@ struct PurchaseOrderViewController: RouteCollection { route.get(use: index) route.get("next", use: nextPage) route.post(use: create(req:)) + route.post("search", use: postSearch) + route.get("search", use: getSearch) route.group("create") { $0.get(use: form) $0.get("vendor-branch-select", use: vendorBranchSelect(req:)) @@ -39,11 +41,11 @@ struct PurchaseOrderViewController: RouteCollection { @Sendable func form(req: Request) async throws -> HTMLResponse { - // guard req.isHtmxRequest else { - // return try await req.render { - // try await mainPage(PurchaseOrderForm(shouldShow: true), page: .default) - // } - // } + guard req.isHtmxRequest else { + return try await req.render { + try await mainPage(PurchaseOrderForm(shouldShow: true), page: .default) + } + } return await req.render { PurchaseOrderForm(shouldShow: true) } } @@ -73,12 +75,28 @@ struct PurchaseOrderViewController: RouteCollection { @Sendable func create(req: Request) async throws -> HTMLResponse { - let create = try req.content.decode(PurchaseOrder.CreateIntermediate.self) + let create = try req.content.decode(CreateContext.self).toIntermediate() let user = try req.auth.require(User.self) let purchaseOrder = try await purchaseOrders.create(create.toCreate(createdByID: user.id)) return await req.render { PurchaseOrderTable.Row(purchaseOrder: purchaseOrder) } } + @Sendable + func postSearch(req: Request) async throws -> HTMLResponse { + let query = try req.content.decode([String: String].self) + req.logger.info("query: \(query)") + let context = try req.content.decode(SearchContext.self).toSearch() + let purchaseOrders = try await purchaseOrders.search(context) + req.logger.info("\(purchaseOrders)") + return await req.render { PurchaseOrderTable.Rows(page: purchaseOrders) } + } + + @Sendable + func getSearch(req: Request) async throws -> HTMLResponse { + let context = try req.query.decode(SearchQuery.self).toSearchContext() + return await req.render { PurchaseOrderSearch(context: context) } + } + private func mainPage( _ html: C, page: IndexQuery @@ -87,6 +105,7 @@ struct PurchaseOrderViewController: RouteCollection { return MainPage(displayNav: true, route: .purchaseOrders) { div(.class("container")) { html + PurchaseOrderSearch() PurchaseOrderTable(page: page) } } @@ -101,3 +120,54 @@ struct IndexQuery: Content { .init(page: 1, limit: 25) } } + +private struct CreateContext: Content { + let id: Int? + let workOrder: String + let materials: String + let customer: String + let truckStock: Bool? + let createdForID: Employee.ID + let vendorBranchID: VendorBranch.ID + + func toIntermediate() -> PurchaseOrder.CreateIntermediate { + .init( + id: id, + workOrder: workOrder.isEmpty ? nil : Int(workOrder), + materials: materials, + customer: customer, + truckStock: truckStock, + createdForID: createdForID, + vendorBranchID: vendorBranchID + ) + } +} + +private struct SearchContext: Content { + let context: String + let search: String + + func toSearch() throws -> PurchaseOrder.SearchContext { + switch context { + case "employee": + return .employee(search) + case "customer": + return .customer(search) + case "vendor": + return .vendor(search) + default: + throw Abort(.badRequest, reason: "Invalid search context.") + } + } +} + +struct SearchQuery: Content { + let context: String + + func toSearchContext() throws -> PurchaseOrderSearchContext { + guard let context = PurchaseOrderSearchContext(rawValue: context) else { + throw Abort(.badRequest, reason: "Invalid context.") + } + return context + } +} diff --git a/Sources/App/Controllers/View/UtilsViewController.swift b/Sources/App/Controllers/View/UtilsViewController.swift new file mode 100644 index 0000000..573ac1a --- /dev/null +++ b/Sources/App/Controllers/View/UtilsViewController.swift @@ -0,0 +1,38 @@ +import DatabaseClient +import Dependencies +import SharedModels +import Vapor +import VaporElementary + +struct UtilsViewController: RouteCollection { + @Dependency(\.database) var database + + func boot(routes: any RoutesBuilder) throws { + let route = routes.protected + route.group("select") { + $0.get("employee", use: employeeSelect(req:)) + } + } + + @Sendable + func employeeSelect(req: Request) async throws -> HTMLResponse { + let context = try req.query.decode(EmployeeSelectContext.self) + let employees = try await database.employees.fetchAll() + return await req.render { context.toHTML(employees: employees) } + } +} + +private struct EmployeeSelectContext: Content { + + let context: EmployeeSelect.Context + + func toHTML(employees: [Employee]) -> EmployeeSelect { + switch context { + case .form: + return .purchaseOrderForm(employees: employees) + case .search: + return .purchaseOrderSearch(employees: employees) + } + } + +} diff --git a/Sources/App/Extensions/RouteBuilder+protected.swift b/Sources/App/Extensions/RouteBuilder+protected.swift index 84efa26..ed8acfa 100644 --- a/Sources/App/Extensions/RouteBuilder+protected.swift +++ b/Sources/App/Extensions/RouteBuilder+protected.swift @@ -9,14 +9,14 @@ extension RoutesBuilder { // Used to ensure views are protected, redirects users to the login page if they're // not authenticated. var protected: any RoutesBuilder { - // return self - return grouped( - UserPasswordAuthenticator(), - UserSessionAuthenticator(), - User.redirectMiddleware { req in - "login?next=\(req.url)" - } - ) + return self + // return grouped( + // UserPasswordAuthenticator(), + // UserSessionAuthenticator(), + // User.redirectMiddleware { req in + // "/login?next=\(req.url)" + // } + // ) } func apiUnprotected(route: PathComponent) -> any RoutesBuilder { diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift index b7b2c12..31f99d7 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -25,11 +25,11 @@ struct PurchaseOrderForm: HTML { } form( .hx.post("/purchase-orders"), - .hx.target("purchase-order-table"), - .hx.swap(.afterBegin.transition(true).swap("1s")), + .hx.target("#purchase-order-table"), + .hx.swap(.afterBegin), .custom( name: "hx-on::after-request", - value: "if (event.detail.successful) toggleContent('float'); window.location.href='/purchase-orders';" + value: "if(event.detail.successful) toggleContent('float')" ) ) { div(.class("row")) { @@ -139,7 +139,7 @@ struct PurchaseOrderForm: HTML { let vendorBranches: [VendorBranch.Detail] var content: some HTML { - select(.name("vendorBranchID"), .class("col-3")) { + select(.name("vendorBranchID"), .class("col-4")) { for branch in vendorBranches { option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" } } diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift new file mode 100644 index 0000000..971e9ee --- /dev/null +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift @@ -0,0 +1,50 @@ +import Elementary +import ElementaryHTMX +import SharedModels +import Vapor + +struct PurchaseOrderSearch: HTML { + + let context: PurchaseOrderSearchContext? + + init(context: PurchaseOrderSearchContext? = nil) { + self.context = context + } + + var content: some HTML { + form( + .id("search"), + .hx.post("/purchase-orders/search"), + .hx.target("#purchase-order-table"), + .hx.swap(.outerHTML.transition(true).swap("1s")) + ) { + select( + .name("context"), .class("col-3"), + .hx.get("/purchase-orders/search"), + .hx.target("#search"), + .hx.swap(.outerHTML) + ) { + option(.value("employee")) { "Employee" } + .attributes(.selected, when: context == .employee || context == nil) + + option(.value("customer")) { "Customer" } + .attributes(.selected, when: context == .customer) + } + + if context == .employee || context == nil { + EmployeeSelect.purchaseOrderSearch() + } else if context == .customer { + input(.type(.text), .name("search"), .placeholder("Search"), .required) + } + + button(.type(.submit), .class("btn-primary")) { "Search" } + // Img.spinner().attributes(.class("hx-indicator")) + } + } + +} + +enum PurchaseOrderSearchContext: String, Codable, Content { + case employee + case customer +} diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift index 65819b1..3b69628 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -23,7 +23,7 @@ struct PurchaseOrderTable: HTML { .attributes( .hx.get("/purchase-orders/create"), .hx.target("#float"), - .hx.swap(.outerHTML.transition(true).swap("1s")), + .hx.swap(.outerHTML), .hx.pushURL(true) ) } @@ -49,7 +49,7 @@ struct PurchaseOrderTable: HTML { .hx.trigger(.event(.revealed)), .hx.swap(.outerHTML.transition(true).swap("1s")), .hx.target("this"), - .hx.indicator(".htmx-indicator") + .hx.indicator("next .htmx-indicator") ) { img(.src("/images/spinner.svg"), .class("htmx-indicator"), .width(60), .height(60)) } diff --git a/Sources/App/Views/Utils/EmployeeSelect.swift b/Sources/App/Views/Utils/EmployeeSelect.swift new file mode 100644 index 0000000..1906397 --- /dev/null +++ b/Sources/App/Views/Utils/EmployeeSelect.swift @@ -0,0 +1,47 @@ +import Elementary +import ElementaryHTMX +import SharedModels +import Vapor + +struct EmployeeSelect: HTML { + + let classString: String + let name: String + let employees: [Employee]? + let context: Context + + var content: some HTML { + if let employees { + select(.name(name), .class(classString)) { + for employee in employees { + option(.value(employee.id.uuidString)) { employee.fullName } + } + } + .attributes(.style("margin-left: 15px;"), when: context == .search) + } else { + div( + .hx.get("/select/employee?context=\(context.rawValue)"), + .hx.target("this"), + .hx.swap(.outerHTML.transition(true).swap("0.5s")), + .hx.indicator("next .hx-indicator"), + .hx.trigger(.event(.revealed)), + .style("display: inline;") + ) { + Img.spinner().attributes(.class("hx-indicator")) + } + } + } + + static func purchaseOrderForm(employees: [Employee]? = nil) -> Self { + .init(classString: "col-3", name: "createdForID", employees: employees, context: .form) + } + + static func purchaseOrderSearch(employees: [Employee]? = nil) -> Self { + .init(classString: "col-3", name: "employeeID", employees: employees, context: .search) + } + + enum Context: String, Codable, Content { + case form + case search + } +} diff --git a/Sources/App/Views/Utils/Navbar.swift b/Sources/App/Views/Utils/Navbar.swift index d3b0281..8fce606 100644 --- a/Sources/App/Views/Utils/Navbar.swift +++ b/Sources/App/Views/Utils/Navbar.swift @@ -8,7 +8,7 @@ struct Navbar: HTML, Sendable { "x" } a(.hx.get("/purchase-orders?page=1&limit=50"), .hx.target("body"), .hx.pushURL(true)) { - "Purchae Orders" + "Purchase Orders" } a(.hx.get("/users"), .hx.target("body"), .hx.pushURL(true)) { "Users" diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index fb1003b..a991bea 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -39,8 +39,6 @@ public func configure(_ app: Application) async throws { let databaseClient = DatabaseClient.live(database: app.db) try await app.migrations.add(databaseClient.migrations()) - app.views.use(.leaf) - try withDependencies { $0.database = databaseClient } operation: { diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index df6107b..3fa097c 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,6 +1,7 @@ import DatabaseClientLive import Dependencies import Elementary +import ElementaryHTMX import Fluent import SharedModels import Vapor @@ -12,6 +13,7 @@ func routes(_ app: Application) throws { try app.register(collection: VendorViewController()) try app.register(collection: EmployeeViewController()) try app.register(collection: PurchaseOrderViewController()) + try app.register(collection: UtilsViewController()) app.get { _ in HTMLResponse { @@ -38,7 +40,21 @@ func routes(_ app: Application) throws { let token = try await users.login(loginForm) let user = try await users.get(token.userID)! req.session.authenticate(user) - return try await PurchaseOrderViewController().index(req: req) + let context = try req.query.decode(LoginContext.self) + + return await req.render { + MainPage(displayNav: true, route: .purchaseOrders) { + div( + .hx.get(context.next ?? "/purchase-orders"), + .hx.pushURL(true), + .hx.target("body"), + .hx.trigger(.event(.revealed)), + .hx.indicator(".hx-indicator") + ) { + Img.spinner().attributes(.class("hx-indicator")) + } + } + } } let protected = app.grouped(UserPasswordAuthenticator(), UserSessionAuthenticator()) diff --git a/Sources/DatabaseClient/PurchaseOrders.swift b/Sources/DatabaseClient/PurchaseOrders.swift index 1fcae86..7bce8b1 100644 --- a/Sources/DatabaseClient/PurchaseOrders.swift +++ b/Sources/DatabaseClient/PurchaseOrders.swift @@ -13,6 +13,7 @@ public extension DatabaseClient { public var get: @Sendable (PurchaseOrder.ID) async throws -> PurchaseOrder? // var update: @Sendable (PurchaseOrder.ID, PurchaseOrder.Update) async throws -> PurchaseOrder public var delete: @Sendable (PurchaseOrder.ID) async throws -> Void + public var search: @Sendable (PurchaseOrder.SearchContext) async throws -> Page } } diff --git a/Sources/DatabaseClientLive/PurchaseOrders.swift b/Sources/DatabaseClientLive/PurchaseOrders.swift index 028fae4..13da795 100644 --- a/Sources/DatabaseClientLive/PurchaseOrders.swift +++ b/Sources/DatabaseClientLive/PurchaseOrders.swift @@ -29,10 +29,42 @@ public extension DatabaseClient.PurchaseOrders { throw NotFoundError() } try await model.delete(on: database) + } search: { search in + let query = PurchaseOrderModel.allQuery(on: database) + + switch search { + case let .employee(employee): + guard let employee = try await EmployeeModel.query(on: database).group(.or, { group in + group.filter(\.$firstName ~~ employee).filter(\.$lastName ~~ employee) + }).first() + else { return Page.empty } + + return try await query.filter(\.$createdFor.$id == employee.id!) + .paginate(.init(page: 1, per: 25)) + .map { try $0.toDTO() } + + case let .customer(search): + return try await query.filter(\.$customer ~~ search) + .paginate(.init(page: 1, per: 25)) + .map { try $0.toDTO() } + + case let .vendor(search): + guard let vendor = try await VendorModel.query(on: database).filter(\.$name ~~ search).first() else { + return .empty + } + // TODO: how to search for this?? + return .init(items: [], metadata: .init(page: 1, per: 1, total: 0)) + } } } } +private extension Page where T == PurchaseOrder { + static var empty: Self { + .init(items: [], metadata: .init(page: 1, per: 1, total: 0)) + } +} + extension PurchaseOrder { struct Migrate: AsyncMigration { diff --git a/Sources/SharedModels/PurchaseOrder.swift b/Sources/SharedModels/PurchaseOrder.swift index 642e5b6..7782974 100644 --- a/Sources/SharedModels/PurchaseOrder.swift +++ b/Sources/SharedModels/PurchaseOrder.swift @@ -115,6 +115,12 @@ public extension PurchaseOrder { } } + enum SearchContext: Sendable { + case customer(String) + case vendor(String) + case employee(String) + } + } #if DEBUG