diff --git a/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift b/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift index 22b7377..fe2dcf5 100644 --- a/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift +++ b/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift @@ -1,73 +1,73 @@ -import Dependencies -import Elementary -import Fluent -import SharedModels -import Vapor -import VaporElementary - -struct PurchaseOrderSearchViewController: RouteCollection { - @Dependency(\.database.employees) var employees - @Dependency(\.database.vendorBranches) var vendorBranches - @Dependency(\.database.purchaseOrders) var purchaseOrders - - func boot(routes: any RoutesBuilder) throws { - let route = routes.protected.grouped("purchase-orders", "search") - route.get(use: index) - route.post(use: post) - } - - @Sendable - func index(req: Request) async throws -> HTMLResponse { - let query = try? req.query.decode(FormQuery.self) - let html = PurchaseOrderSearch(context: query?.context) - if query?.table == true || !req.isHtmxRequest { - return await req.render { mainPage(search: html) } - } - return await req.render { html } - } - - @Sendable - func post(req: Request) async throws -> HTMLResponse { - let context = try req.content.decode(PurchaseOrderSearchContent.self) - let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) - return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } - } - - func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { - MainPage(displayNav: true, route: .purchaseOrders) { - div(.class("container"), .id("purchase-order-content")) { - search - PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) - } - } - } - -} - -extension PurchaseOrderSearchContent { - - func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { - switch context { - case .employee: - guard let createdForID else { - throw Abort(.badRequest, reason: "Employee id not provided") - } - return .employee(createdForID) - case .customer: - guard let search, !search.isEmpty else { - throw Abort(.badRequest, reason: "Customer search string is empty.") - } - return .customer(search) - case .vendor: - guard let vendorBranchID else { - throw Abort(.badRequest, reason: "Vendor branch id not provided.") - } - return .vendor(vendorBranchID) - } - } -} - -private struct FormQuery: Content { - let context: PurchaseOrderSearchContext - let table: Bool? -} +// import Dependencies +// import Elementary +// import Fluent +// import SharedModels +// import Vapor +// import VaporElementary +// +// struct PurchaseOrderSearchViewController: RouteCollection { +// @Dependency(\.database.employees) var employees +// @Dependency(\.database.vendorBranches) var vendorBranches +// @Dependency(\.database.purchaseOrders) var purchaseOrders +// +// func boot(routes: any RoutesBuilder) throws { +// let route = routes.protected.grouped("purchase-orders", "search") +// route.get(use: index) +// route.post(use: post) +// } +// +// @Sendable +// func index(req: Request) async throws -> HTMLResponse { +// let query = try? req.query.decode(FormQuery.self) +// let html = PurchaseOrderSearch(context: query?.context) +// if query?.table == true || !req.isHtmxRequest { +// return await req.render { mainPage(search: html) } +// } +// return await req.render { html } +// } +// +// @Sendable +// func post(req: Request) async throws -> HTMLResponse { +// let context = try req.content.decode(PurchaseOrderSearchContent.self) +// let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) +// return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } +// } +// +// func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { +// MainPage(displayNav: true, route: .purchaseOrders) { +// div(.class("container"), .id("purchase-order-content")) { +// search +// PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) +// } +// } +// } +// +// } +// +// extension PurchaseOrderSearchContent { +// +// func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { +// switch context { +// case .employee: +// guard let createdForID else { +// throw Abort(.badRequest, reason: "Employee id not provided") +// } +// return .employee(createdForID) +// case .customer: +// guard let search, !search.isEmpty else { +// throw Abort(.badRequest, reason: "Customer search string is empty.") +// } +// return .customer(search) +// case .vendor: +// guard let vendorBranchID else { +// throw Abort(.badRequest, reason: "Vendor branch id not provided.") +// } +// return .vendor(vendorBranchID) +// } +// } +// } +// +// private struct FormQuery: Content { +// let context: PurchaseOrderSearchContext +// let table: Bool? +// } diff --git a/Sources/App/Controllers/View/PurchaseOrderViewController.swift b/Sources/App/Controllers/View/PurchaseOrderViewController.swift index de93e48..c748e88 100644 --- a/Sources/App/Controllers/View/PurchaseOrderViewController.swift +++ b/Sources/App/Controllers/View/PurchaseOrderViewController.swift @@ -15,8 +15,6 @@ 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) } @@ -68,26 +66,26 @@ struct PurchaseOrderViewController: RouteCollection { return await req.render { PurchaseOrderTable.Row(purchaseOrder: purchaseOrder) } } - @Sendable - func postSearch(req: Request) async throws -> HTMLResponse { - let context = try req.content.decode(PurchaseOrderSearchContent.self) - let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) - return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } - } + // @Sendable + // func postSearch(req: Request) async throws -> HTMLResponse { + // let context = try req.content.decode(PurchaseOrderSearchContent.self) + // let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) + // return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } + // } // Show the form to generate a search query. - @Sendable - func getSearch(req: Request) async throws -> HTMLResponse { - // TODO: Need to handle updating the form. - return await req.render { - MainPage(displayNav: true, route: .purchaseOrders) { - div(.class("container"), .id("purchase-order-content")) { - PurchaseOrderSearch() - PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) - } - } - } - } + // @Sendable + // func getSearch(req: Request) async throws -> HTMLResponse { + // // TODO: Need to handle updating the form. + // return await req.render { + // MainPage(displayNav: true, route: .purchaseOrders) { + // div(.class("container"), .id("purchase-order-content")) { + // PurchaseOrderSearch() + // PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) + // } + // } + // } + // } private func mainPage( _ html: C, diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift index 6023a69..a5ee0ef 100644 --- a/Sources/App/Views/Employees/EmployeeForm.swift +++ b/Sources/App/Views/Employees/EmployeeForm.swift @@ -19,7 +19,7 @@ struct EmployeeForm: HTML { var content: some HTML { Float(shouldDisplay: shouldShow, resetURL: "/employees") { form( - employee == nil ? .hx.post(targetURL) : .hx.put(targetURL), + employee == nil ? .hx.post(route: targetURL) : .hx.put(route: targetURL), .hx.target(target), employee == nil ? .hx.swap(.beforeEnd.transition(true).swap("0.5s")) @@ -51,7 +51,7 @@ struct EmployeeForm: HTML { "Delete" } .attributes( - .hx.delete("/api/v1/employees/\(employee.id)"), + .hx.delete(route: .employee(.shared(.delete(id: employee.id)))), .hx.confirm("Are you sure you want to delete this employee?"), .hx.target("#employee_\(employee.id)"), .hx.swap(.outerHTML.transition(true).swap("1s")) @@ -74,8 +74,8 @@ struct EmployeeForm: HTML { return "Update" } - private var targetURL: String { - guard let employee else { return "/employees" } - return "/employees/\(employee.id)" + private var targetURL: SharedModels.ViewRoute { + guard let employee else { return .employee(.shared(.index)) } + return .employee(.shared(.get(id: employee.id))) } } diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift index 0390b26..687e584 100644 --- a/Sources/App/Views/Employees/EmployeeTable.swift +++ b/Sources/App/Views/Employees/EmployeeTable.swift @@ -14,7 +14,7 @@ struct EmployeeTable: HTML { Button.add() .attributes( .style("padding: 0px 10px;"), - .hx.get(route: .employees(.create)), + .hx.get(route: .employee(.shared(.index))), .hx.target(.float), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) @@ -39,7 +39,7 @@ struct EmployeeTable: HTML { Button.detail() .attributes( .style("padding-left: 15px;"), - .hx.get(route: .employees(.id(employee.id))), + .hx.get(route: .employee(.shared(.get(id: employee.id)))), .hx.target(.float), .hx.pushURL(true), .hx.swap(.outerHTML.transition(true).swap("0.5s")) diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift index d0fcab7..e53c090 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -27,7 +27,7 @@ struct PurchaseOrderForm: HTML { } } form( - .hx.post(route: .purchaseOrders()), + .hx.post(route: .purchaseOrder(.shared(.index))), .hx.target(.purchaseOrders(.table)), .hx.swap(.afterBegin), .customToggleFloatAfterRequest diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift index aaffbad..52dc73e 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift @@ -5,35 +5,37 @@ import Vapor struct PurchaseOrderSearch: HTML { - let context: PurchaseOrderSearchContext + typealias Context = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context - init(context: PurchaseOrderSearchContext? = nil) { + let context: Context + + init(context: Context? = nil) { self.context = context ?? .employee } var content: some HTML { form( .id(.search), - .hx.post(route: .purchaseOrders(.search())), + .hx.post(route: .purchaseOrder(.search(.index()))), .hx.target(.purchaseOrders()), .hx.swap(.outerHTML) ) { div(.class("btn-row")) { button( .class("btn-secondary"), .style("position: absolute; top: 80px; right: 20px;"), - .hx.get(route: .purchaseOrders()), .hx.pushURL(true), .hx.target("body") + .hx.get(route: .purchaseOrder(.shared(.index))), .hx.pushURL(true), .hx.target("body") ) { "x" } } div(.class("row")) { select( .name("context"), .class("col-3"), - .hx.get(route: .purchaseOrders(.search())), + .hx.get(route: .purchaseOrder(.search(.index()))), .hx.target(.search), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) ) { - for context in PurchaseOrderSearchContext.allCases { + for context in Context.allCases { option(.value(context.rawValue)) { context.rawValue.capitalized } .attributes(.selected, when: self.context == context) } @@ -44,7 +46,7 @@ struct PurchaseOrderSearch: HTML { } else if context == .customer { input( .type(.text), .class("col-6"), .style("margin-left: 60px; margin-top: 18px;"), - .name("search"), .placeholder("Search"), .required + .name("customerSearch"), .placeholder("Search"), .required ) } else if context == .vendor { VendorBranchSelect.purchaseOrderSearch() @@ -60,15 +62,15 @@ struct PurchaseOrderSearch: HTML { } -enum PurchaseOrderSearchContext: String, Codable, Content, CaseIterable { - case employee - case customer - case vendor -} +// enum PurchaseOrderSearchContext: String, Codable, Content, CaseIterable { +// case employee +// case customer +// case vendor +// } -struct PurchaseOrderSearchContent: Content { - let context: PurchaseOrderSearchContext - let createdForID: Employee.ID? - let search: String? - let vendorBranchID: VendorBranch.ID? -} +// struct PurchaseOrderSearchContent: Content { +// let context: PurchaseOrderSearchContext +// let createdForID: Employee.ID? +// let search: String? +// let vendorBranchID: VendorBranch.ID? +// } diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift index 39ecc70..edfd6bd 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -5,15 +5,16 @@ import SharedModels import Vapor struct PurchaseOrderTable: HTML { + typealias SearchContext = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context let page: Page let context: Context - let searchContext: PurchaseOrderSearchContext? + let searchContext: SearchContext? init( page: Page, context: Context = .default, - searchContext: PurchaseOrderSearchContext? = nil + searchContext: SearchContext? = nil ) { self.page = page self.context = context @@ -44,7 +45,7 @@ struct PurchaseOrderTable: HTML { if context != .search { Button.add() .attributes( - .hx.get(route: .purchaseOrders(.create)), .hx.target(.float), + .hx.get(route: .purchaseOrder(.shared(.index))), .hx.target(.float), .hx.swap(.outerHTML), .hx.pushURL(true) ) } @@ -59,7 +60,7 @@ struct PurchaseOrderTable: HTML { button( .id("btn-search"), .class("btn-primary"), .style("position: absolute; top: 80px; right: 20px;"), - .hx.get(route: .purchaseOrders(.search(.context(.employee, table: true)))), + .hx.get(route: .purchaseOrder(.search(.index(context: .employee, table: true)))), .hx.target(.body), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) @@ -81,7 +82,7 @@ struct PurchaseOrderTable: HTML { if page.metadata.pageCount > page.metadata.page { tr( // .hx.get("/purchase-orders/next?page=\(page.metadata.page + 1)&limit=\(page.metadata.per)"), - .hx.get(route: .purchaseOrders(.nextPage(page.metadata))), + .hx.get(route: .purchaseOrder(.shared(.page(page: page.metadata.page + 1, limit: page.metadata.per)))), .hx.trigger(.event(.revealed)), .hx.swap(.outerHTML.transition(true).swap("1s")), .hx.target(.this), @@ -110,7 +111,7 @@ struct PurchaseOrderTable: HTML { td { Button.detail() .attributes( - .hx.get("/purchase-orders/\(purchaseOrder.id)"), + .hx.get(route: .purchaseOrder(.shared(.get(id: purchaseOrder.id)))), .hx.target("#float"), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift index 3a5cfab..8d59290 100644 --- a/Sources/App/Views/Users/UserDetail.swift +++ b/Sources/App/Views/Users/UserDetail.swift @@ -12,7 +12,7 @@ struct UserDetail: HTML, Sendable { Float(shouldDisplay: user != nil, resetURL: "/users") { if let user { form( - .hx.post(route: .users(.id(user.id))), + .hx.post(route: .user(.shared(.get(id: user.id)))), .hx.swap(.outerHTML), .hx.target(.user(.row(id: user.id))), .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';") @@ -36,7 +36,7 @@ struct UserDetail: HTML, Sendable { ) { "Update" } Button.danger { "Delete" } .attributes( - .hx.delete(route: .users(.id(user.id))), + .hx.delete(route: .user(.shared(.get(id: user.id)))), .hx.trigger(.event(.click)), .hx.swap(.outerHTML), .hx.target(.user(.row(id: user.id))), diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index baacf44..42b0e62 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -99,7 +99,7 @@ struct UserForm: HTML, Sendable { } } - // TODO: Return a route container. + // TODO: Return a ViewRoute. var targetURL: String { switch self { case .create: diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift index f41d3d3..592008d 100644 --- a/Sources/App/Views/Users/UserTable.swift +++ b/Sources/App/Views/Users/UserTable.swift @@ -17,7 +17,7 @@ struct UserTable: HTML { th(.style("width: 50px;")) { Button.add() .attributes( - .hx.get(route: .users(.create)), + .hx.get(route: .user(.form)), .hx.target(.float), .hx.swap(.outerHTML) ) @@ -45,7 +45,7 @@ struct UserTable: HTML { td { user.email } td { Button.detail().attributes( - .hx.get(route: .users(.id(user.id))), + .hx.get(route: .user(.shared(.get(id: user.id)))), .hx.target(.float), .hx.swap(.outerHTML), .hx.pushURL(true) diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 8a1d23c..1ee57a6 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -15,14 +15,15 @@ struct VendorDetail: HTML { } closeButton: { Button.close(id: "float") .attributes( - .hx.get("/vendors"), + .hx.get(route: .vendor(.shared(.index(withBranches: true)))), .hx.pushURL(true), - .hx.target("body"), + .hx.target(.body), .hx.swap(.outerHTML) ) } } + // TODO: What route for here?? var branchForm: some HTML { form( .id("branch-form"), @@ -33,7 +34,7 @@ struct VendorDetail: HTML { ) { input( .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, - .hx.post("/vendors/\(vendor.id)/branches"), + .hx.post(route: .vendorBranch(.index(for: vendor.id))), .hx.trigger(.event(.keyup).changed().delay("800ms")), .hx.target("#branches"), .hx.swap(.beforeEnd) // , @@ -65,7 +66,7 @@ struct VendorDetail: HTML { span(.class("label")) { branch.name.capitalized } button( .class("btn"), - .hx.delete("/api/v1/vendors/branches/\(branch.id)"), + .hx.delete(route: .vendorBranch(.delete(id: branch.id))), .hx.target("#branch_\(branch.id)"), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) { diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift index cef1a98..4fbc50c 100644 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -41,7 +41,7 @@ struct VendorForm: HTML { func makeForm(vendor: Vendor?) -> some HTML { form( .id("vendor-form"), - vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), + vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), .hx.target("#content"), .hx.swap(.outerHTML) ) { @@ -53,7 +53,7 @@ struct VendorForm: HTML { .name("name"), .value(vendor?.name ?? ""), .placeholder("Vendor Name"), - vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), + vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), .hx.trigger(.event(.keyup).changed().delay("500ms")), .required ) @@ -61,7 +61,7 @@ struct VendorForm: HTML { button( .class("danger"), .style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"), - .hx.delete("/api/v1/vendors/\(vendor.id)"), + .hx.delete(route: .vendor(.delete(id: vendor.id))), .hx.confirm("Are you sure you want to delete this vendor?"), .hx.target("#vendor_\(vendor.id)"), .hx.swap(.outerHTML.transition(true).swap("1s")), @@ -85,8 +85,8 @@ struct VendorForm: HTML { return "Update" } - var targetURL: String { - guard let vendor else { return "/vendors" } - return "/vendors/\(vendor.id)" + var targetURL: SharedModels.ViewRoute { + guard let vendor else { return .vendor(.shared(.index(withBranches: true))) } + return .vendor(.shared(.get(id: vendor.id))) } } diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift index 77655e6..c8854c8 100644 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -15,7 +15,7 @@ struct VendorTable: HTML { Button.add() .attributes( .style("padding: 0px 10px;"), - .hx.get("/vendors/create"), + .hx.get(route: .vendor(.form)), .hx.target("#float"), .hx.swap(.outerHTML) ) @@ -41,7 +41,7 @@ struct VendorTable: HTML { Button.detail() .attributes( .style("padding-left: 15px;"), - .hx.get("/vendors/\(vendor.id)"), + .hx.get(route: .vendor(.shared(.get(id: vendor.id)))), .hx.target("#float"), .hx.pushURL(true), .hx.swap(.outerHTML) diff --git a/Sources/App/Views/ViewRoute.swift b/Sources/App/Views/ViewRoute.swift index 9425700..5b22660 100644 --- a/Sources/App/Views/ViewRoute.swift +++ b/Sources/App/Views/ViewRoute.swift @@ -4,21 +4,38 @@ import Fluent import SharedModels extension HTMLAttribute.hx { - static func get(route: RouteKey) -> HTMLAttribute { - get(route.url) + static func get(route: SharedModels.ViewRoute) -> HTMLAttribute { + get(SharedModels.ViewRoute.router.path(for: route)) } - static func post(route: RouteKey) -> HTMLAttribute { - post(route.url) + static func post(route: SharedModels.ViewRoute) -> HTMLAttribute { + post(SharedModels.ViewRoute.router.path(for: route)) } - static func put(route: RouteKey) -> HTMLAttribute { - put(route.url) + static func put(route: SharedModels.ViewRoute) -> HTMLAttribute { + put(SharedModels.ViewRoute.router.path(for: route)) } - static func delete(route: RouteKey) -> HTMLAttribute { - delete(route.url) + static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute { + delete(SharedModels.ViewRoute.router.path(for: route)) } + + // static func get(route: SharedModels.ApiRoute) -> HTMLAttribute { + // get(route: .shared(route)) + // } + // + // static func post(route: SharedModels.ApiRoute) -> HTMLAttribute { + // post(SharedModels.ApiRoute.router.path(for: route)) + // } + // + // static func put(route: SharedModels.ApiRoute) -> HTMLAttribute { + // put(SharedModels.ApiRoute.router.path(for: route)) + // } + // + // static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute { + // delete(SharedModels.ApiRoute.router.path(for: route)) + // } + } extension HTMLAttribute.hx { @@ -34,6 +51,7 @@ extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { } } +// TODO: Remove. enum RouteKey { case employees(EmployeeRoute? = nil) case purchaseOrders(PurchaseOrderRoute? = nil) @@ -73,8 +91,8 @@ enum RouteKey { enum PurchaseOrderRoute { case create case nextPage(PageMetadata) - case search(SearchQuery? = nil) - + // case search(SearchQuery? = nil) + // var path: String { switch self { case .create: @@ -82,25 +100,24 @@ enum RouteKey { case let .nextPage(currentPage): return "next?page=\(currentPage.page + 1)&limit\(currentPage.per)" - - case let .search(query): - guard let query else { return "search" } - return "search?\(query.query)" + // case let .search(query): + // guard let query else { return "search" } + // return "search?\(query.query)" } } - enum SearchQuery { - case context(PurchaseOrderSearchContext, table: Bool? = nil) - - var query: String { - switch self { - case let .context(context, table): - let query = "context=\(context.rawValue)" - guard let table else { return query } - return "\(query)&table=\(table)" - } - } - } + // enum SearchQuery { + // case context(PurchaseOrderSearchContext, table: Bool? = nil) + // + // var query: String { + // switch self { + // case let .context(context, table): + // let query = "context=\(context.rawValue)" + // guard let table else { return query } + // return "\(query)&table=\(table)" + // } + // } + // } } enum UserRoute { diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index af16ff8..7e43a9b 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -13,13 +13,13 @@ func routes(_ app: Application) throws { // try app.register(collection: ApiController()) app.mount(SiteRoute.router, use: siteHandler) - try app.register(collection: UserViewController()) - try app.register(collection: VendorViewController()) - try app.register(collection: EmployeeViewController()) - try app.register(collection: PurchaseOrderViewController()) - try app.register(collection: PurchaseOrderSearchViewController()) - try app.register(collection: UtilsViewController()) - + // try app.register(collection: UserViewController()) + // try app.register(collection: VendorViewController()) + // try app.register(collection: EmployeeViewController()) + // try app.register(collection: PurchaseOrderViewController()) + // try app.register(collection: PurchaseOrderSearchViewController()) + // try app.register(collection: UtilsViewController()) + // app.get { _ in HTMLResponse { MainPage(displayNav: false, route: .purchaseOrders) { @@ -29,38 +29,38 @@ func routes(_ app: Application) throws { } } } - - app.get("login") { req in - let context = try req.query.decode(LoginContext.self) - return await req.render { - MainPage(displayNav: false, route: .login) { - UserForm(context: .login(next: context.next)) - } - } - } - - app.post("login") { req in - @Dependency(\.database.users) var users - let loginForm = try req.content.decode(User.Login.self) - let token = try await users.login(loginForm) - let user = try await users.get(token.userID)! - req.session.authenticate(user) - 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")) - } - } - } - } + // + // app.get("login") { req in + // let context = try req.query.decode(LoginContext.self) + // return await req.render { + // MainPage(displayNav: false, route: .login) { + // UserForm(context: .login(next: context.next)) + // } + // } + // } + // + // app.post("login") { req in + // @Dependency(\.database.users) var users + // let loginForm = try req.content.decode(User.Login.self) + // let token = try await users.login(loginForm) + // let user = try await users.get(token.userID)! + // req.session.authenticate(user) + // 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")) + // } + // } + // } + // } } private struct LoginContext: Content { @@ -73,7 +73,17 @@ func siteHandler( ) async throws -> any AsyncResponseEncodable { switch route { case let .api(route): - switch route { + return try await route.handle(request: request) + case .health: + return HTTPStatus.ok + case let .view(route): + return try await route.handle(request: request) + } +} + +extension ApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + switch self { case let .employee(route): return try await route.handle(request: request) case let .purchaseOrder(route): @@ -85,10 +95,12 @@ func siteHandler( case let .vendorBranch(route): return try await route.handle(request: request) } - case .health: - return HTTPStatus.ok - case let .view(route): - switch route { + } +} + +extension SharedModels.ViewRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + switch self { case let .employee(route): return try await route.handle(request: request) @@ -100,11 +112,11 @@ func siteHandler( } } - case .purchaseOrder: - fatalError() + case let .purchaseOrder(route): + return try await route.handle(request: request) - case .select: - fatalError() + case let .select(route): + return try await route.handle(request: request) case let .user(route): return try await route.handle(request: request) @@ -288,6 +300,105 @@ extension SharedModels.ViewRoute.EmployeeRoute { } +extension SharedModels.ViewRoute.PurchaseOrderRoute { + private func mainPage( + _ html: C, + page: Int, + limit: Int + ) async throws -> some SendableHTMLDocument where C: Sendable { + @Dependency(\.database.purchaseOrders) var purchaseOrders + let page = try await purchaseOrders.fetchPage(.init(page: page, per: limit)) + return MainPage(displayNav: true, route: .purchaseOrders) { + div(.class("container"), .id("purchase-order-content")) { + html + PurchaseOrderTable(page: page) + } + } + } + + private func mainPage( + _ html: C + ) async throws -> some SendableHTMLDocument where C: Sendable { + try await mainPage(html, page: 1, limit: 25) + } + + func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.purchaseOrders) var purchaseOrders + switch self { + case .form: + return try await request.render(mainPage: mainPage) { + PurchaseOrderForm(shouldShow: true) + } + + case let .search(route): + return try await route.handle(request: request) + + case let .shared(route): + switch route { + case let .create(purchaseOrder): + return try await request.render { + try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder)) + } + + case let .delete(id: id): + try await purchaseOrders.delete(id) + return HTTPStatus.ok + + case .index: + return try await request.render { + try await mainPage(PurchaseOrderForm()) + } + + case let .get(id: id): + guard let purchaseOrder = try await purchaseOrders.get(id) else { + throw Abort(.badRequest, reason: "Purchase order not found.") + } + return try await request.render(mainPage: mainPage) { + PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true) + } + + case let .page(page: page, limit: limit): + return try await request.render { + try await PurchaseOrderTable.Rows( + page: purchaseOrders.fetchPage(.init(page: page, per: limit)) + ) + } + } + } + } + +} + +extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { + + func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { + MainPage(displayNav: true, route: .purchaseOrders) { + div(.class("container"), .id("purchase-order-content")) { + search + PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) + } + } + } + + func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + switch self { + case let .index(context: context, table: table): + let html = PurchaseOrderSearch(context: context) + if table == true || !request.isHtmxRequest { + return await request.render { mainPage(search: html) } + } + return await request.render { html } + + case let .search(context): + let results = try await database.purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) + return await request.render { + PurchaseOrderTable(page: results, context: .search) + } + } + } +} + extension SharedModels.ViewRoute.UserRoute { private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { @@ -381,11 +492,6 @@ extension SharedModels.ViewRoute.VendorRoute { VendorForm(.float(shouldShow: true)) } - case let .createBranch(branch): - return try await request.render { - try await VendorDetail.BranchRow(branch: database.vendorBranches.create(branch)) - } - case let .shared(route): switch route { case let .create(vendor): @@ -423,5 +529,96 @@ extension SharedModels.ViewRoute.VendorRoute { } } } - +} + +extension SharedModels.ViewRoute.VendorBranchRoute { + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + + switch self { + case let .shared(route): + switch route { + case let .create(branch): + return try await request.render { + try await VendorDetail.BranchRow(branch: database.vendorBranches.create(branch)) + } + + case let .delete(id: id): + try await database.vendorBranches.delete(id) + return HTTPStatus.ok + + // FIX: + case let .get(id: id): + fatalError() + + case let .index(for: vendorID): + fatalError() + + case let .update(id: id, updates: updates): + fatalError() + } + } + } +} + +extension SharedModels.ViewRoute.PurchaseOrderRoute.Search.Request { + + func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { + switch context { + case .employee: + guard let createdForID else { + throw Abort(.badRequest, reason: "Employee id not provided") + } + return .employee(createdForID) + case .customer: + guard let customerSearch, !customerSearch.isEmpty else { + throw Abort(.badRequest, reason: "Customer search string is empty.") + } + return .customer(customerSearch) + case .vendor: + guard let vendorBranchID else { + throw Abort(.badRequest, reason: "Vendor branch id not provided.") + } + return .vendor(vendorBranchID) + } + } +} + +extension SharedModels.ViewRoute.SelectRoute { + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + + switch self { + case let .employee(context: context): + return try await request.render { + try await context.toHTML(employees: database.employees.fetchAll()) + } + case let .vendorBranches(context: context): + return try await request.render { + try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail()) + } + } + } +} + +extension SharedModels.ViewRoute.SelectRoute.Context { + func toHTML(employees: [Employee]) -> EmployeeSelect { + switch self { + case .purchaseOrderForm: + return .purchaseOrderForm(employees: employees) + case .purchaseOrderSearch: + return .purchaseOrderSearch(employees: employees) + } + } + + func toHTML(branches: [VendorBranch.Detail]) -> VendorBranchSelect { + switch self { + case .purchaseOrderForm: + return .purchaseOrderForm(branches: branches) + case .purchaseOrderSearch: + return .purchaseOrderSearch(branches: branches) + } + } } diff --git a/Sources/SharedModels/Routes/ApiRoute.swift b/Sources/SharedModels/Routes/ApiRoute.swift new file mode 100644 index 0000000..bf81fd3 --- /dev/null +++ b/Sources/SharedModels/Routes/ApiRoute.swift @@ -0,0 +1,115 @@ +import CasePathsCore +import Foundation +@preconcurrency import URLRouting + +// TODO: Switch shared to be on API routes not view routes?? + +public enum ApiRoute: Sendable { + + case employee(EmployeeApiRoute) + case purchaseOrder(PurchaseOrderApiRoute) + case user(UserApiRoute) + case vendor(VendorApiRoute) + case vendorBranch(VendorBranchApiRoute) + + static let rootPath = Path { "api"; "v1" } + + public static let router = OneOf { + Route(.case(Self.employee)) { + rootPath + EmployeeApiRoute.router + } + Route(.case(Self.purchaseOrder)) { + rootPath + PurchaseOrderApiRoute.router + } + Route(.case(Self.user)) { + rootPath + UserApiRoute.router + } + Route(.case(Self.vendor)) { + rootPath + VendorApiRoute.router + } + Route(.case(Self.vendorBranch)) { + rootPath + VendorBranchApiRoute.router + } + } + + public enum EmployeeApiRoute: Sendable { + case shared(SharedEmployeeRoute) + + public static let router = Route(.case(Self.shared)) { + SharedEmployeeRoute.router + } + } + + public enum PurchaseOrderApiRoute: Sendable { + case shared(SharedPurchaseOrderRoute) + + public static let router = Route(.case(Self.shared)) { + SharedPurchaseOrderRoute.router + } + } + + // TODO: Add logout. + public enum UserApiRoute: Sendable { + case shared(SharedUserRoute) + + public static let router = Route(.case(Self.shared)) { + SharedUserRoute.router + } + } + + public enum VendorApiRoute: Sendable { + case index(withBranches: Bool?) + case shared(SharedVendorRoute) + + static let rootPath = "vendors" + + public static let router = OneOf { + Route(.case(Self.index(withBranches:))) { + Path { rootPath } + Method.get + Query { + Field("branches", default: nil) { + Optionally { Bool.parser() } + } + } + } + Route(.case(Self.shared)) { + SharedVendorRoute.router + } + } + } + + public enum VendorBranchApiRoute: Sendable { + case get(id: VendorBranch.ID) + case index(for: Vendor.ID? = nil) + case shared(SharedVendorBranchRoute) + case update(id: VendorBranch.ID, updates: VendorBranch.Update) + + public static let router = OneOf { + Route(.case(Self.get(id:))) { + Path { "vendors"; "branches"; VendorBranch.ID.parser() } + Method.get + } + Route(.case(Self.index(for:))) { + Path { "vendors"; "branches" } + Method.get + Query { + Field("vendorID", default: nil) { Optionally { VendorBranch.ID.parser() } } + } + } + Route(.case(Self.shared)) { + SharedVendorBranchRoute.router + } + Route(.case(Self.update(id:updates:))) { + Path { "vendors"; "branches"; VendorBranch.ID.parser() } + Method.put + Body(.json(VendorBranch.Update.self)) + } + } + } +} diff --git a/Sources/SharedModels/Routes/SharedRoutes.swift b/Sources/SharedModels/Routes/SharedRoutes.swift new file mode 100644 index 0000000..7b85f60 --- /dev/null +++ b/Sources/SharedModels/Routes/SharedRoutes.swift @@ -0,0 +1,165 @@ +import CasePathsCore +import Foundation +@preconcurrency import URLRouting + +public enum SharedEmployeeRoute: Sendable { + case create(Employee.Create) + case delete(id: Employee.ID) + case get(id: Employee.ID) + case index + case update(id: Employee.ID, updates: Employee.Update) + + static let rootPath = "employees" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(Employee.Create.self)) + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.delete(id:))) { + Path { rootPath; UUID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; UUID.parser() } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; UUID.parser() } + Method.put + Body(.json(Employee.Update.self)) + } + } +} + +public enum SharedPurchaseOrderRoute: Sendable { + case create(PurchaseOrder.Create) + case delete(id: PurchaseOrder.ID) + case get(id: PurchaseOrder.ID) + case index + case page(page: Int, limit: Int) + + static let rootPath = "purchase-orders" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(PurchaseOrder.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Digits() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Digits() } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.page(page:limit:))) { + Path { rootPath; "next" } + Method.get + Query { + Field("page", default: 1) { Digits() } + Field("limit", default: 25) { Digits() } + } + } + } +} + +public enum SharedUserRoute: Sendable { + case create(User.Create) + case delete(id: User.ID) + case get(id: User.ID) + case index + case login(User.Login) + case update(id: User.ID, updates: User.Update) + + static let rootPath = "users" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(User.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; User.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; User.ID.parser() } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.login)) { + Path { rootPath } + Method.post + Body(.json(User.Login.self)) + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; User.ID.parser() } + // TODO: Use put or patch. + Method.post + Body(.json(User.Update.self)) + } + } +} + +public enum SharedVendorRoute: Sendable { + case create(Vendor.Create) + case delete(id: Vendor.ID) + case get(id: Vendor.ID) + case update(id: Vendor.ID, updates: Vendor.Update) + + static let rootPath = "vendors" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(Vendor.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; Vendor.ID.parser() } + Method.put + Body(.json(Vendor.Update.self)) + } + } +} + +public enum SharedVendorBranchRoute: Sendable { + case create(VendorBranch.Create) + case delete(id: VendorBranch.ID) + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { "vendors"; "branches" } + Method.post + Body(.json(VendorBranch.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { "vendors"; "branches"; VendorBranch.ID.parser() } + Method.delete + } + } +} diff --git a/Sources/SharedModels/Routes/SiteRoute.swift b/Sources/SharedModels/Routes/SiteRoute.swift new file mode 100644 index 0000000..dad7b20 --- /dev/null +++ b/Sources/SharedModels/Routes/SiteRoute.swift @@ -0,0 +1,18 @@ +import CasePathsCore +import Foundation +@preconcurrency import URLRouting + +public enum SiteRoute: Sendable { + case api(ApiRoute) + case health + case view(ViewRoute) + + public static let router = OneOf { + Route(.case(Self.view)) { ViewRoute.router } + Route(.case(Self.health)) { + Path { "health" } + Method.get + } + Route(.case(Self.api)) { ApiRoute.router } + } +} diff --git a/Sources/SharedModels/Routes/ViewRoute.swift b/Sources/SharedModels/Routes/ViewRoute.swift new file mode 100644 index 0000000..4747a91 --- /dev/null +++ b/Sources/SharedModels/Routes/ViewRoute.swift @@ -0,0 +1,218 @@ +import CasePathsCore +import Foundation +@preconcurrency import URLRouting + +public enum ViewRoute: Sendable { + + case employee(EmployeeRoute) + case login + case purchaseOrder(PurchaseOrderRoute) + case select(SelectRoute) + case user(UserRoute) + case vendor(VendorRoute) + case vendorBranch(VendorBranchRoute) + + public static let router = OneOf { + Route(.case(Self.employee)) { EmployeeRoute.router } + Route(.case(Self.login)) { + Path { "login" } + Method.get + } + Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router } + Route(.case(Self.select)) { SelectRoute.router } + Route(.case(Self.user)) { UserRoute.router } + Route(.case(Self.vendor)) { VendorRoute.router } + Route(.case(Self.vendorBranch)) { VendorBranchRoute.router } + } + + public enum EmployeeRoute: Sendable { + case form + case shared(SharedEmployeeRoute) + + public static func delete(id: Employee.ID) -> Self { + .shared(.delete(id: id)) + } + + public static func get(id: Employee.ID) -> Self { + .shared(.get(id: id)) + } + + public static var index: Self { .shared(.index) } + + static let rootPath = "employees" + + public static let router = OneOf { + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.shared)) { + SharedEmployeeRoute.router + } + } + } + + public enum PurchaseOrderRoute: Sendable { + case form + case search(Search) + case shared(SharedPurchaseOrderRoute) + + static let rootPath = "purchase-orders" + + public static let router = OneOf { + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.search)) { + Search.router + } + Route(.case(Self.shared)) { + SharedPurchaseOrderRoute.router + } + } + + public enum Search: Sendable { + case index(context: Context? = nil, table: Bool? = nil) + case search(Request) + + static let rootPath = Path { "purchase-orders"; "search" } + + static let router = OneOf { + Route(.case(Search.index(context:table:))) { + rootPath + Method.get + Query { + Field("context", default: .employee) { Optionally { Search.Context.parser() } } + Field("table", default: nil) { Optionally { Bool.parser() } } + } + } + Route(.case(Search.search)) { + rootPath + Method.post + Body(.json(Search.Request.self)) + } + } + + public enum Context: String, Codable, CaseIterable, Sendable { + case employee + case customer + case vendor + } + + public struct Request: Codable, Sendable { + public let context: Context + public let createdForID: Employee.ID? + public let customerSearch: String? + public let vendorBranchID: VendorBranch.ID? + + public init( + context: Context, + createdForID: Employee.ID? = nil, + customerSearch: String? = nil, + vendorBranchID: VendorBranch.ID? = nil + ) { + self.context = context + self.createdForID = createdForID + self.customerSearch = customerSearch + self.vendorBranchID = vendorBranchID + } + } + + } + } + + // TODO: Move into respective view routes. + public enum SelectRoute: Sendable { + case employee(context: Context) + case vendorBranches(context: Context) + + public enum Context: String, Codable, Sendable, CaseIterable { + case purchaseOrderForm + case purchaseOrderSearch + } + + static let rootPath = "select" + + public static let router = OneOf { + Route(.case(Self.employee(context:))) { + Path { rootPath; "employee" } + Method.get + Query { + Field("context") { Context.parser() } + } + } + Route(.case(Self.vendorBranches(context:))) { + Path { rootPath; "vendor-branches" } + Method.get + Query { + Field("context") { Context.parser() } + } + } + } + } + + public enum UserRoute: Sendable { + case form + case shared(SharedUserRoute) + + static let rootPath = "users" + + public static func delete(id: User.ID) -> Self { + .shared(.delete(id: id)) + } + + public static var index: Self { .shared(.index) } + + public static let router = OneOf { + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.shared)) { + SharedUserRoute.router + } + } + } + + public enum VendorRoute: Sendable { + case form + case shared(SharedVendorRoute) + + static let rootPath = "vendors" + + public static let router = OneOf { + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.shared)) { + SharedVendorRoute.router + } + } + } + + // TODO: Add Select + public enum VendorBranchRoute: Sendable { + + case index(vendorID: Vendor.ID) + case shared(SharedVendorBranchRoute) + + public static func delete(id: VendorBranch.ID) -> Self { + .shared(.delete(id: id)) + } + + public static let router = OneOf { + Route(.case(Self.index(vendorID:))) { + Path { "vendors"; "branches" } + Method.get + Query { + Field("vendorID") { Vendor.ID.parser() } + } + } + Route(.case(Self.shared)) { + SharedVendorBranchRoute.router + } + } + } +} diff --git a/Sources/SharedModels/SiteRoute.swift b/Sources/SharedModels/SiteRoute.swift deleted file mode 100644 index d7acb11..0000000 --- a/Sources/SharedModels/SiteRoute.swift +++ /dev/null @@ -1,422 +0,0 @@ -import CasePathsCore -import Foundation -@preconcurrency import URLRouting - -public enum SiteRoute: Sendable { - case api(ApiRoute) - case health - case view(ViewRoute) - - public static let router = OneOf { - Route(.case(Self.view)) { ViewRoute.router } - Route(.case(Self.health)) { - Path { "health" } - Method.get - } - Route(.case(Self.api)) { ApiRoute.router } - } -} - -public enum ApiRoute: Sendable { - - case employee(EmployeeApiRoute) - case purchaseOrder(PurchaseOrderApiRoute) - case user(UserApiRoute) - case vendor(VendorApiRoute) - case vendorBranch(VendorBranchApiRoute) - - static let rootPath = Path { "api"; "v1" } - - public static let router = OneOf { - Route(.case(Self.employee)) { - rootPath - EmployeeApiRoute.router - } - Route(.case(Self.purchaseOrder)) { - rootPath - PurchaseOrderApiRoute.router - } - Route(.case(Self.user)) { - rootPath - UserApiRoute.router - } - Route(.case(Self.vendor)) { - rootPath - VendorApiRoute.router - } - Route(.case(Self.vendorBranch)) { - rootPath - VendorBranchApiRoute.router - } - } - - public enum EmployeeApiRoute: Sendable { - case create(Employee.Create) - case delete(id: Employee.ID) - case get(id: Employee.ID) - case index - case update(id: Employee.ID, updates: Employee.Update) - - static let rootPath = "employees" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(Employee.Create.self)) - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.delete(id:))) { - Path { rootPath; UUID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; UUID.parser() } - Method.get - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; UUID.parser() } - Method.put - Body(.json(Employee.Update.self)) - } - } - } - - public enum PurchaseOrderApiRoute: Sendable { - case create(PurchaseOrder.Create) - case delete(id: PurchaseOrder.ID) - case get(id: PurchaseOrder.ID) - case index - case page(page: Int, limit: Int) - - static let rootPath = "purchase-orders" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(PurchaseOrder.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; Digits() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; Digits() } - Method.get - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.page(page:limit:))) { - Path { rootPath; "next" } - Method.get - Query { - Field("page", default: 1) { Digits() } - Field("limit", default: 25) { Digits() } - } - } - } - } - - // TODO: Add logout. - public enum UserApiRoute: Sendable { - case create(User.Create) - case delete(id: User.ID) - case get(id: User.ID) - case index - case login(User.Login) - case update(id: User.ID, updates: User.Update) - - static let rootPath = "users" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(User.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; User.ID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; User.ID.parser() } - Method.get - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.login)) { - Path { rootPath } - Method.post - Body(.json(User.Login.self)) - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; User.ID.parser() } - // TODO: Use put or patch. - Method.post - Body(.json(User.Update.self)) - } - } - } - - public enum VendorApiRoute: Sendable { - case create(Vendor.Create) - case delete(id: Vendor.ID) - case get(id: Vendor.ID) - case index(withBranches: Bool?) - case update(id: Vendor.ID, updates: Vendor.Update) - - static let rootPath = "vendors" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(Vendor.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; Vendor.ID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; Vendor.ID.parser() } - Method.get - } - Route(.case(Self.index(withBranches:))) { - Path { rootPath } - Method.get - Query { - Field("branches", default: nil) { - Optionally { Bool.parser() } - } - } - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; Vendor.ID.parser() } - Method.put - Body(.json(Vendor.Update.self)) - } - } - } - - public enum VendorBranchApiRoute: Sendable { - case create(VendorBranch.Create) - case delete(id: VendorBranch.ID) - case get(id: VendorBranch.ID) - case index(for: Vendor.ID? = nil) - case update(id: VendorBranch.ID, updates: VendorBranch.Update) - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { "vendors"; "branches" } - Method.post - Body(.json(VendorBranch.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { "vendors"; "branches"; VendorBranch.ID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { "vendors"; "branches"; VendorBranch.ID.parser() } - Method.get - } - Route(.case(Self.index(for:))) { - Path { "vendors"; "branches" } - Method.get - Query { - Field("vendorID", default: nil) { Optionally { VendorBranch.ID.parser() } } - } - } - Route(.case(Self.update(id:updates:))) { - Path { "vendors"; "branches"; VendorBranch.ID.parser() } - Method.put - Body(.json(VendorBranch.Update.self)) - } - } - } -} - -public enum ViewRoute: Sendable { - - case employee(EmployeeRoute) - case login - case purchaseOrder(PurchaseOrderRoute) - case select(SelectRoute) - case user(UserRoute) - case vendor(VendorRoute) - - public static let router = OneOf { - Route(.case(Self.employee)) { EmployeeRoute.router } - Route(.case(Self.login)) { - Path { "login" } - Method.get - } - Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router } - Route(.case(Self.select)) { SelectRoute.router } - Route(.case(Self.user)) { UserRoute.router } - Route(.case(Self.vendor)) { VendorRoute.router } - } - - public enum EmployeeRoute: Sendable { - case form - case shared(ApiRoute.EmployeeApiRoute) - - static let rootPath = "employees" - - public static let router = OneOf { - Route(.case(Self.form)) { - Path { rootPath; "create" } - Method.get - } - Route(.case(Self.shared)) { - ApiRoute.EmployeeApiRoute.router - } - } - } - - public enum PurchaseOrderRoute: Sendable { - case form - case search(Search) - case shared(ApiRoute.PurchaseOrderApiRoute) - - static let rootPath = "purchase-orders" - - public static let router = OneOf { - Route(.case(Self.form)) { - Path { rootPath; "create" } - Method.get - } - Route(.case(Self.search)) { - Search.router - } - Route(.case(Self.shared)) { - ApiRoute.PurchaseOrderApiRoute.router - } - } - - public enum Search: Sendable { - case index(context: Context, table: Bool?) - case search(Request) - - static let rootPath = Path { "purchase-orders"; "search" } - - static let router = OneOf { - Route(.case(Search.index(context:table:))) { - rootPath - Method.get - Query { - Field("context", default: .employee) { Search.Context.parser() } - Field("table", default: nil) { Optionally { Bool.parser() } } - } - } - Route(.case(Search.search)) { - rootPath - Method.post - Body(.json(Search.Request.self)) - } - } - - public enum Context: String, Codable, CaseIterable, Sendable { - case employee - case customer - case vendor - } - - public struct Request: Codable, Sendable { - public let context: Context - public let createdForID: Employee.ID? - public let customerSearch: String? - public let vendorBranchID: VendorBranch.ID? - - public init( - context: Context, - createdForID: Employee.ID? = nil, - customerSearch: String? = nil, - vendorBranchID: VendorBranch.ID? = nil - ) { - self.context = context - self.createdForID = createdForID - self.customerSearch = customerSearch - self.vendorBranchID = vendorBranchID - } - } - - } - } - - public enum SelectRoute: Sendable { - case employee(context: Context) - case vendorBranches(context: Context) - - public enum Context: String, Codable, Sendable, CaseIterable { - case purchaseOrderForm - case purchaseOrderSearch - } - - static let rootPath = "select" - - public static let router = OneOf { - Route(.case(Self.employee(context:))) { - Path { rootPath; "employee" } - Method.get - Query { - Field("context") { Context.parser() } - } - } - Route(.case(Self.vendorBranches(context:))) { - Path { rootPath; "vendor-branches" } - Method.get - Query { - Field("context") { Context.parser() } - } - } - } - } - - public enum UserRoute: Sendable { - case form - case shared(ApiRoute.UserApiRoute) - - static let rootPath = "users" - - public static let router = OneOf { - Route(.case(Self.form)) { - Path { rootPath; "create" } - Method.get - } - Route(.case(Self.shared)) { - ApiRoute.UserApiRoute.router - } - } - } - - public enum VendorRoute: Sendable { - case createBranch(VendorBranch.Create) - case form - case shared(ApiRoute.VendorApiRoute) - - static let rootPath = "vendors" - - public static let router = OneOf { - Route(.case(Self.createBranch)) { - Path { rootPath; "branches" } - Method.post - Body(.json(VendorBranch.Create.self)) - } - Route(.case(Self.form)) { - Path { rootPath; "create" } - Method.get - } - Route(.case(Self.shared)) { - ApiRoute.VendorApiRoute.router - } - } - } -}