From aa60f697587d2edcbdd617203b2a9cf876feb2ab Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 24 Jan 2025 09:50:55 -0500 Subject: [PATCH] feat: Integrates view controller produced views, without working middleware protected routes. Need to get middleware working --- Sources/App/Controllers/ViewController.swift | 399 ------------------ Sources/App/Dependencies/DateFormatter.swift | 56 +-- .../App/Extensions/Request+extensions.swift | 36 +- .../App/Views/Employees/EmployeeForm.swift | 81 ---- .../App/Views/Employees/EmployeeTable.swift | 51 --- Sources/App/Views/HTMXExtensions.swift | 204 --------- Sources/App/Views/Main.swift | 140 ------ .../PurchaseOrders/PurchaseOrderForm.swift | 130 ------ .../PurchaseOrders/PurchaseOrderSearch.swift | 63 --- .../PurchaseOrders/PurchaseOrderTable.swift | 137 ------ Sources/App/Views/Users/UserDetail.swift | 76 ---- Sources/App/Views/Users/UserForm.swift | 119 ------ Sources/App/Views/Users/UserTable.swift | 57 --- Sources/App/Views/Utils/Buttons.swift | 53 --- Sources/App/Views/Utils/Float.swift | 81 ---- Sources/App/Views/Utils/Img.swift | 18 - Sources/App/Views/Utils/Navbar.swift | 31 -- Sources/App/Views/Utils/Select.swift | 89 ---- .../VendorBranches/VendorBranchForm.swift | 34 -- .../VendorBranches/VendorBranchList.swift | 46 -- Sources/App/Views/Vendors/VendorDetail.swift | 25 -- Sources/App/Views/Vendors/VendorForm.swift | 92 ---- Sources/App/Views/Vendors/VendorTable.swift | 53 --- Sources/App/configure.swift | 4 +- Sources/ViewControllerLive/Routes+view.swift | 5 +- 25 files changed, 53 insertions(+), 2027 deletions(-) delete mode 100644 Sources/App/Views/Employees/EmployeeForm.swift delete mode 100644 Sources/App/Views/Employees/EmployeeTable.swift delete mode 100644 Sources/App/Views/HTMXExtensions.swift delete mode 100644 Sources/App/Views/Main.swift delete mode 100644 Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift delete mode 100644 Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift delete mode 100644 Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift delete mode 100644 Sources/App/Views/Users/UserDetail.swift delete mode 100644 Sources/App/Views/Users/UserForm.swift delete mode 100644 Sources/App/Views/Users/UserTable.swift delete mode 100644 Sources/App/Views/Utils/Buttons.swift delete mode 100644 Sources/App/Views/Utils/Float.swift delete mode 100644 Sources/App/Views/Utils/Img.swift delete mode 100644 Sources/App/Views/Utils/Navbar.swift delete mode 100644 Sources/App/Views/Utils/Select.swift delete mode 100644 Sources/App/Views/VendorBranches/VendorBranchForm.swift delete mode 100644 Sources/App/Views/VendorBranches/VendorBranchList.swift delete mode 100644 Sources/App/Views/Vendors/VendorDetail.swift delete mode 100644 Sources/App/Views/Vendors/VendorForm.swift delete mode 100644 Sources/App/Views/Vendors/VendorTable.swift diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index b0c01a4..02ed93d 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -12,7 +12,6 @@ private let viewProtectedMiddleware: [any Middleware] = [ } ] -// TODO: Return `any HTML` instead to make testing the rendered documents easier. extension SharedModels.ViewRoute { var middleware: [any Middleware]? { @@ -26,425 +25,27 @@ extension SharedModels.ViewRoute { case let .vendorBranch(route): return route.middleware } } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.users) var users - switch self { - case .index: - return request.redirect(to: Self.router.path(for: .purchaseOrder(.index))) - - case let .employee(route): - return try await route.handle(request: request) - - case let .login(route): - switch route { - case let .index(next: next): - return await request.render { - MainPage(displayNav: false, route: .login) { - UserForm(context: .login(next: next)) - } - } - case let .post(login): - let token = try await users.login(.init(username: login.username, password: login.password)) - let user = try await users.get(token.userID)! - request.session.authenticate(user) - request.logger.info("Logged in next: \(login.next ?? "N/A")") - return await request.render { - MainPage.loggedIn(next: login.next) - } - } - - case let .purchaseOrder(route): - return try await route.handle(request: request) - - case let .user(route): - return try await route.handle(request: request) - - case let .vendor(route): - return try await route.handle(request: request) - - case let .vendorBranch(route): - return try await route.handle(request: request) - } - } } extension SharedModels.ViewRoute.EmployeeRoute { - private func mainPage( - _ html: C - ) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database) var database - let employees = try await database.employees.fetchAll() - return MainPage(displayNav: true, route: .employees) { - div(.class("container")) { - html - EmployeeTable(employees: employees) - } - } - } - var middleware: [any Middleware]? { viewProtectedMiddleware } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.employees) var employees - - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - EmployeeForm(shouldShow: true) - } - - case let .select(context: context): - return try await request.render { - try await context.toHTML(employees: employees.fetchAll()) - } - - case .index: - return try await request.render { try await mainPage(EmployeeForm()) } - - case let .get(id: id): - guard let employee = try await employees.get(id) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - return try await request.render(mainPage: mainPage) { - EmployeeForm(employee: employee) - } - - case let .create(employee): - return try await request.render { - try await EmployeeTable.Row(employee: employees.create(employee)) - } - - case let .delete(id: id): - try await employees.delete(id) - return HTTPStatus.ok - - case let .update(id: id, updates: updates): - return try await request.render { - try await EmployeeTable.Row(employee: employees.update(id, updates)) - } - } - } - } 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) - } - var middleware: [any Middleware]? { viewProtectedMiddleware } - - 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 .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)), - context: .search - ) - } - } - } - - 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 .request(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 { - @Dependency(\.database) var database - let users = try await database.users.fetchAll() - return MainPage(displayNav: true, route: .users) { - div(.class("container")) { - html - UserTable(users: users) - } - } - } - var middleware: [any Middleware]? { viewProtectedMiddleware } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.users) var users - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - UserForm(context: .create) - } - - case let .create(user): - return try await request.render { - try await UserTable.Row(user: users.create(user)) - } - - case let .delete(id: id): - try await users.delete(id) - return HTTPStatus.ok - - case .index: - return try await request.render { - try await mainPage(UserDetail(user: nil)) - } - - case let .get(id: id): - guard let user = try await users.get(id) else { - throw Abort(.badRequest, reason: "User not found.") - } - return try await request.render(mainPage: mainPage) { - UserDetail(user: user) - } - - case let .update(id: id, updates: updates): - return try await request.render { - try await UserTable.Row(user: users.update(id, updates)) - } - } - } - } extension SharedModels.ViewRoute.VendorRoute { - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database) var database - let vendors = try await database.vendors.fetchAll(.withBranches) - return MainPage(displayNav: true, route: .vendors) { - div(.class("container"), .id("content")) { - html - VendorTable(vendors: vendors) - } - } - } - var middleware: [any Middleware]? { viewProtectedMiddleware } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - VendorForm(.float(shouldShow: true)) - } - - case .index: - return try await request.render { - try await mainPage(VendorForm()) - } - - case let .create(vendor): - let vendor = try await database.vendors.create(vendor) - return await request.render { - div(.class("container"), .id("content")) { - VendorDetail(vendor: vendor) - try await VendorTable(vendors: database.vendors.fetchAll(.withBranches)) - } - } - - case let .delete(id: id): - try await database.vendors.delete(id) - return HTTPStatus.ok - - case let .get(id: id): - guard let vendor = try await database.vendors.get(id, .withBranches) else { - throw Abort(.badRequest, reason: "Vendor not found.") - } - return try await request.render(mainPage: mainPage) { - VendorDetail(vendor: vendor) - } - - case let .update(id: id, updates: updates): - return try await request.render { - try await VendorDetail( - vendor: database.vendors.update(id, with: updates, returnWithBranches: true) - ) - } - } - } } extension SharedModels.ViewRoute.VendorBranchRoute { - var middleware: [any Middleware]? { viewProtectedMiddleware } - - // func html() async throws -> any HTML { - // @Dependency(\.database) var database - // - // switch self { - // case let .index(for: vendorID): - // guard let vendorID else { - // throw Abort(.badRequest, reason: "Vendor id not supplied") - // } - // return try await VendorBranchList( - // vendorID: vendorID, - // branches: database.vendorBranches.fetchAll(.for(vendorID: vendorID)) - // ) - // - // case let .select(context: context): - // return try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail()) - // - // case let .create(branch): - // return try await VendorBranchList.Row(branch: database.vendorBranches.create(branch)) - // - // case let .delete(id: id): - // try await database.vendorBranches.delete(id) - // return HTTPStatus.ok - // } - // } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - - switch self { - case let .index(for: vendorID): - guard let vendorID else { - throw Abort(.badRequest, reason: "Vendor id not supplied") - } - return try await request.render { - try await VendorBranchList( - vendorID: vendorID, - branches: database.vendorBranches.fetchAll(.for(vendorID: vendorID)) - ) - } - - case let .select(context: context): - return try await request.render { - try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail()) - } - - case let .create(branch): - return try await request.render { - try await VendorBranchList.Row(branch: database.vendorBranches.create(branch)) - } - - case let .delete(id: id): - try await database.vendorBranches.delete(id) - return HTTPStatus.ok - } - } -} - -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.SelectContext { - 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/App/Dependencies/DateFormatter.swift b/Sources/App/Dependencies/DateFormatter.swift index bb96c8e..dc7848a 100644 --- a/Sources/App/Dependencies/DateFormatter.swift +++ b/Sources/App/Dependencies/DateFormatter.swift @@ -1,28 +1,28 @@ -import Dependencies -import Foundation - -public extension DependencyValues { - var dateFormatter: DateFormatter { - get { self[DateFormatter.self] } - set { self[DateFormatter.self] = newValue } - } -} - -#if hasFeature(RetroactiveAttribute) - extension DateFormatter: @retroactive DependencyKey { - - public static var liveValue: DateFormatter { - let formatter = DateFormatter() - formatter.dateStyle = .short - return formatter - } - } -#else - extension DateFormatter: DependencyKey { - public static var liveValue: DateFormatter { - let formatter = DateFormatter() - formatter.dateStyle = .short - return formatter - } - } -#endif +// import Dependencies +// import Foundation +// +// public extension DependencyValues { +// var dateFormatter: DateFormatter { +// get { self[DateFormatter.self] } +// set { self[DateFormatter.self] = newValue } +// } +// } +// +// #if hasFeature(RetroactiveAttribute) +// extension DateFormatter: @retroactive DependencyKey { +// +// public static var liveValue: DateFormatter { +// let formatter = DateFormatter() +// formatter.dateStyle = .short +// return formatter +// } +// } +// #else +// extension DateFormatter: DependencyKey { +// public static var liveValue: DateFormatter { +// let formatter = DateFormatter() +// formatter.dateStyle = .short +// return formatter +// } +// } +// #endif diff --git a/Sources/App/Extensions/Request+extensions.swift b/Sources/App/Extensions/Request+extensions.swift index 261ba1e..be68c3a 100644 --- a/Sources/App/Extensions/Request+extensions.swift +++ b/Sources/App/Extensions/Request+extensions.swift @@ -9,22 +9,22 @@ extension Request { headers.contains(name: "hx-request") } - func render( - @HTMLBuilder html: () async throws -> C - ) async rethrows -> HTMLResponse where C: Sendable { - let html = try await html() - return HTMLResponse { html } - } - - // Render the html if we're an htmx request, otherwise render the main page. - func render( - mainPage: (C) async throws -> D, - @HTMLBuilder html: () async throws -> C - ) async rethrows -> HTMLResponse where C: Sendable { - let html = try await html() - guard isHtmxRequest else { - return try await render { try await mainPage(html) } - } - return HTMLResponse { html } - } + // func render( + // @HTMLBuilder html: () async throws -> C + // ) async rethrows -> HTMLResponse where C: Sendable { + // let html = try await html() + // return HTMLResponse { html } + // } + // + // // Render the html if we're an htmx request, otherwise render the main page. + // func render( + // mainPage: (C) async throws -> D, + // @HTMLBuilder html: () async throws -> C + // ) async rethrows -> HTMLResponse where C: Sendable { + // let html = try await html() + // guard isHtmxRequest else { + // return try await render { try await mainPage(html) } + // } + // return HTMLResponse { html } + // } } diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift deleted file mode 100644 index 167948f..0000000 --- a/Sources/App/Views/Employees/EmployeeForm.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct EmployeeForm: HTML { - let employee: Employee? - let shouldShow: Bool - - init(employee: Employee? = nil, shouldShow: Bool = false) { - self.employee = employee - self.shouldShow = shouldShow - } - - init(employee: Employee) { - self.employee = employee - self.shouldShow = true - } - - var content: some HTML { - Float(shouldDisplay: shouldShow, resetURL: .employee(.index)) { - form( - employee == nil ? .hx.post(route: targetURL) : .hx.put(route: targetURL), - .hx.target(target), - employee == nil - ? .hx.swap(.beforeEnd.transition(true).swap("0.5s")) - : .hx.swap(.outerHTML.transition(true).swap("0.5s")), - .hx.on( - .afterRequest, - .ifSuccessful(.toggleContent(.float), .setWindowLocation(to: .employee(.index))) - ) - ) { - div(.class("row")) { - input( - .type(.text), .class("col-5"), - .name("firstName"), .value(employee?.firstName ?? ""), - .placeholder("First Name"), .required - ) - div(.class("col-2")) {} - input( - .type(.text), .class("col-5"), - .name("lastName"), .value(employee?.lastName ?? ""), - .placeholder("Last Name"), .required - ) - } - div(.class("btn-row")) { - button(.type(.submit), .class("btn-primary")) { - buttonLabel - } - if let employee { - Button.danger { - "Delete" - } - .attributes( - .hx.confirm("Are you sure you want to delete this employee?"), - .hx.delete(route: .employee(.delete(id: employee.id))), - .hx.target(.id(.employee(.row(id: employee.id)))), - .hx.swap(.outerHTML.transition(true).swap("1s")) - ) - } - } - } - } - } - - private var target: HXTarget { - guard let employee else { - return .id(.employee(.table)) - } - return .id(.employee(.row(id: employee.id))) - } - - private var buttonLabel: String { - guard employee != nil else { return "Create" } - return "Update" - } - - private var targetURL: SharedModels.ViewRoute { - guard let employee else { return .employee(.index) } - return .employee(.get(id: employee.id)) - } -} diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift deleted file mode 100644 index 4e6ae33..0000000 --- a/Sources/App/Views/Employees/EmployeeTable.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct EmployeeTable: HTML { - let employees: [Employee] - - var content: some HTML { - table { - thead { - tr { - th { "Name" } - th(.style("width: 100px;")) { - Button.add() - .attributes( - .style("padding: 0px 10px;"), - .hx.get(route: .employee(.form)), - .hx.target(.id(.float)), - .hx.swap(.outerHTML.transition(true).swap("0.5s")) - ) - } - } - } - tbody(.id(.employee(.table))) { - for employee in employees { - Row(employee: employee) - } - } - } - } - - struct Row: HTML { - let employee: Employee - - var content: some HTML { - tr(.id(.employee(.row(id: employee.id)))) { - td { employee.fullName } - td { - Button.detail() - .attributes( - .style("padding-left: 15px;"), - .hx.get(route: .employee(.get(id: employee.id))), - .hx.target(.id(.float)), - .hx.pushURL(true), - .hx.swap(.outerHTML.transition(true).swap("0.5s")) - ) - } - } - } - } -} diff --git a/Sources/App/Views/HTMXExtensions.swift b/Sources/App/Views/HTMXExtensions.swift deleted file mode 100644 index efb1041..0000000 --- a/Sources/App/Views/HTMXExtensions.swift +++ /dev/null @@ -1,204 +0,0 @@ -import Elementary -import ElementaryHTMX -import Fluent -import SharedModels - -extension HTMLAttribute.hx { - static func get(route: SharedModels.ViewRoute) -> HTMLAttribute { - get(SharedModels.ViewRoute.router.path(for: route)) - } - - static func post(route: SharedModels.ViewRoute) -> HTMLAttribute { - post(SharedModels.ViewRoute.router.path(for: route)) - } - - static func put(route: SharedModels.ViewRoute) -> HTMLAttribute { - put(SharedModels.ViewRoute.router.path(for: route)) - } - - static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute { - delete(SharedModels.ViewRoute.router.path(for: route)) - } -} - -extension HTMLAttribute.hx { - static func target(_ target: HXTarget) -> HTMLAttribute { - Self.target(target.selector) - } -} - -extension HTMLAttribute.hx where Tag: HTMLTrait.Attributes.Global { - - static func on(_ event: HXOnEvent, value: String) -> HTMLAttribute { - HTMLAttribute.custom(name: "hx-on::\(event.rawValue)", value: value) - } - - static func on(_ event: HXOnEvent, _ value: HXOnValue) -> HTMLAttribute { - on(event, value: value.value) - } - - static func on(_ event: HXOnEvent, _ values: HXOnValue...) -> HTMLAttribute { - on(event, value: values.value) - } -} - -enum HXOnEvent: String { - case afterRequest = "after-request" -} - -indirect enum HXOnValue { - case ifSuccessful([Self]) - case resetForm - case setWindowLocation(String) - case toggleContent(id: String) - - static func toggleContent(_ toggle: Toggle) -> Self { - toggleContent(id: toggle.rawValue) - } - - static func setWindowLocation(to route: ViewRoute) -> Self { - setWindowLocation(ViewRoute.router.path(for: route)) - } - - static func ifSuccessful(_ values: Self...) -> Self { - .ifSuccessful(values) - } - - fileprivate var value: String { - switch self { - case .resetForm: - return "this.reset();" - case let .toggleContent(toggle): - return "toggleContent('\(toggle)');" - case let .setWindowLocation(string): - return "window.location.href='\(string)';" - case let .ifSuccessful(values): - return "if(event.detail.successful) \(values.value)" - } - } - - enum Toggle: String { - case float - } -} - -extension Array where Element == HXOnValue { - - var value: String { - return map(\.value).joined(separator: " ") - } -} - -extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { - static func id(_ key: IDKey) -> Self { - id(key.description) - } -} - -enum IDKey: CustomStringConvertible { - case branch(Branch) - case employee(Employee) - case float - case purchaseOrder(PurchaseOrder? = nil) - case user(User) - case vendor(Vendor) - - var description: String { - switch self { - case let .branch(key): return "branch-\(key)" - case let .employee(key): return "employee-\(key)" - case .float: return "float" - case let .purchaseOrder(key): - guard let key else { return "purchase-order" } - return "purchase-order-\(key)" - case let .user(key): return "user-\(key)" - case let .vendor(key): return "vendor-\(key)" - } - } - - enum Branch: CustomStringConvertible { - case list - case form - case row(id: VendorBranch.ID) - - var description: String { - switch self { - case .list: return "list" - case .form: return "form" - case let .row(id): return id.uuidString - } - } - } - - enum Employee: CustomStringConvertible { - case table - case row(id: SharedModels.Employee.ID) - - var description: String { - switch self { - case .table: return "table" - case let .row(id): return "\(id)" - } - } - } - - enum PurchaseOrder: CustomStringConvertible { - case content - case row(id: SharedModels.PurchaseOrder.ID) - case search - case table - - var description: String { - switch self { - case .content: return "content" - case let .row(id): return "\(id)" - case .search: return "search" - case .table: return "table" - } - } - } - - enum User: CustomStringConvertible { - case form - case row(id: SharedModels.User.ID) - case table - - var description: String { - switch self { - case .form: return "form" - case .table: return "table" - case let .row(id): return "\(id)" - } - } - } - - enum Vendor: CustomStringConvertible { - case form - case row(id: SharedModels.Vendor.ID) - - var description: String { - switch self { - case .form: return "form" - case let .row(id): return "\(id)" - } - } - - } -} - -enum HXTarget: CustomStringConvertible { - case body - case id(IDKey) - case this - - var selector: String { - switch self { - case .body: return "body" - case let .id(key): return "#\(key)" - case .this: return "this" - } - } - - var description: String { selector } - -} diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift deleted file mode 100644 index 93fe7bd..0000000 --- a/Sources/App/Views/Main.swift +++ /dev/null @@ -1,140 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct MainPage: SendableHTMLDocument where Inner: Sendable { - - var title: String { "Purchase Orders" } - var lang: String { "en" } - - let inner: Inner - let displayNav: Bool - let routeHeader: RouteHeaderView - - init( - displayNav: Bool = false, - route: RouteHeaderView.ViewRoute, - _ inner: () -> Inner - ) { - self.displayNav = displayNav - self.routeHeader = .init(route: route) - self.inner = inner() - } - - var head: some HTML { - meta(.charset(.utf8)) - script(.src("https://unpkg.com/htmx.org@2.0.4")) {} - script(.src("/js/main.js")) {} - link(.rel(.stylesheet), .href("/css/main.css")) - link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon")) - } - - var body: some HTML { - header(.class("header")) { - Logo() - if displayNav { - Navbar() - } - } - routeHeader - inner - } -} - -extension MainPage where Inner == LoggedIn { - static func loggedIn(next: String?) -> Self { - MainPage(displayNav: true, route: .purchaseOrders) { - LoggedIn(next: next) - } - } -} - -struct LoggedIn: HTML { - let next: String? - var content: some HTML { - div( - .hx.get(nextRoute ?? ViewRoute.router.path(for: .purchaseOrder(.index))), - .hx.pushURL(true), - .hx.target(.body), - .hx.trigger(.event(.revealed)), - .hx.indicator(".hx-indicator") - ) { - Img.spinner().attributes(.class("hx-indicator")) - } - } - - // HACK: to get search route to work after login. - var nextRoute: String? { - if let next, next.contains("search") { - return ViewRoute.router.path(for: .purchaseOrder(.search(.index(context: .employee, table: true)))) - } - return next - } -} - -struct RouteHeaderView: HTML { - - let title: String - let description: String - - init(title: String, description: String) { - self.title = title - self.description = description - } - - init(route: ViewRoute) { - self.init(title: route.title, description: route.description) - } - - var content: some HTML { - div(.class("container"), .style("padding: 20px 20px;")) { - h1 { title } - br() - p(.class("secondary")) { i { description } } - br() - } - } - - enum ViewRoute: String { - - case employees - case login - case purchaseOrders - case users - case vendors - - var title: String { - switch self { - case .purchaseOrders: - return "Purchase Orders" - default: - return rawValue.capitalized - } - } - - var description: String { - switch self { - case .employees: - return "Employees are who purchase orders can be issued to." - case .purchaseOrders, .login: - return "" - case .users: - return "Users are who can login and issue purchase orders for employees." - case .vendors: - return "Vendors are where purchase orders can be issued to." - } - } - - } -} - -struct Logo: HTML, Sendable { - - var content: some HTML { - div(.id("logo")) { - "HHE - Purchase Orders" - } - } -} - -protocol SendableHTMLDocument: HTMLDocument, Sendable {} diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift deleted file mode 100644 index 83b8ec5..0000000 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Dependencies -import Elementary -import ElementaryHTMX -import SharedModels - -struct PurchaseOrderForm: HTML { - - @Dependency(\.dateFormatter) var dateFormatter - - let purchaseOrder: PurchaseOrder? - let shouldShow: Bool - - init(purchaseOrder: PurchaseOrder? = nil, shouldShow: Bool = false) { - self.purchaseOrder = purchaseOrder - self.shouldShow = shouldShow - } - - var content: some HTML { - Float(shouldDisplay: shouldShow, resetURL: .purchaseOrder(.index)) { - if shouldShow { - if purchaseOrder != nil { - p { - span(.class("label"), .style("margin-right: 15px;")) { "Note:" } - span { i(.style("font-size: 1em;")) { - "Vendor and Employee can not be changed once a purchase order has been created." - } } - } - } - form( - .hx.post(route: .purchaseOrder(.index)), - .hx.target(.id(.purchaseOrder(.table))), - .hx.swap(.afterBegin), - .hx.on(.afterRequest, .ifSuccessful(.toggleContent(.float))) - ) { - div(.class("row")) { - label( - .for("customer"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Customer:" } - input( - .type(.text), .class("col-3"), - .name("customer"), .placeholder("Customer"), - .value(purchaseOrder?.customer ?? ""), - .required, .autofocus - ) - label( - .for("workOrder"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Work Order:" } - input( - .type(.text), .class("col-4"), - .name("workOrder"), .placeholder("Work Order: (12345)"), - .value("\(purchaseOrder?.workOrder != nil ? String(purchaseOrder!.workOrder!) : "")") - ) - } - div(.class("row")) { - label( - .for("materials"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Materials:" } - input( - .type(.text), .class("col-3"), - .name("materials"), .placeholder("Materials"), - .value(purchaseOrder?.materials ?? ""), - .required - ) - label( - .for("vendorBranchID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Vendor:" } - if purchaseOrder == nil { - VendorBranchSelect.purchaseOrderForm() - } else { - input( - .type(.text), .class("col-4"), - .name("vendorBranchID"), - .value("\(purchaseOrder!.vendorBranch.vendor.name) - \(purchaseOrder!.vendorBranch.name)"), - .disabled - ) - } - } - div(.class("row")) { - label( - .for("createdForID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Employee:" } - if purchaseOrder == nil { - EmployeeSelect.purchaseOrderForm() - } else { - input( - .type(.text), .class("col-3"), - .value(purchaseOrder!.createdFor.fullName), - .disabled - ) - } - label( - .for("truckStock"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;") - ) { "Truck Stock:" } - if purchaseOrder?.truckStock == true { - input( - .type(.checkbox), .class("col-2"), .name("truckStock"), .style("margin-top: 20px;"), .checked - ) - } else { - input( - .type(.checkbox), .class("col-2"), .name("truckStock"), .style("margin-top: 20px;") - ) - } - } - if let purchaseOrder, let createdAt = purchaseOrder.createdAt { - div(.class("row")) { - label(.class("label col-2")) { "Created:" } - h3(.class("col-2")) { dateFormatter.string(from: createdAt) } - if let updatedAt = purchaseOrder.updatedAt { - div(.class("col-1")) {} - label(.class("label col-2")) { "Updated:" } - h3(.class("col-2")) { dateFormatter.string(from: updatedAt) } - } - } - } - div(.class("btn-row")) { - button(.class("btn-primary"), .type(.submit)) { buttonLabel } - if purchaseOrder != nil { - Button.danger { "Delete" } - } - } - } - } - } - } - - private var buttonLabel: String { - guard purchaseOrder != nil else { return "Create" } - return "Update" - } -} diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift deleted file mode 100644 index 78c6f4d..0000000 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels -import Vapor - -struct PurchaseOrderSearch: HTML { - - typealias Context = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context - - let context: Context - - init(context: Context? = nil) { - self.context = context ?? .employee - } - - var content: some HTML { - form( - .id(.purchaseOrder(.search)), - .hx.post(route: .purchaseOrder(.search(.index()))), - .hx.target(.id(.purchaseOrder())), - .hx.swap(.outerHTML) - ) { - div(.class("btn-row")) { - button( - .class("btn-secondary"), .style("position: absolute; top: 80px; right: 20px;"), - .hx.get(route: .purchaseOrder(.index)), .hx.pushURL(true), .hx.target("body") - ) - { "x" } - } - div(.class("row")) { - select( - .name("context"), .class("col-3"), - .hx.get(route: .purchaseOrder(.search(.index()))), - .hx.target(.id(.purchaseOrder(.search))), - .hx.swap(.outerHTML.transition(true).swap("0.5s")), - .hx.pushURL(true) - ) { - for context in Context.allCases { - option(.value(context.rawValue)) { context.rawValue.capitalized } - .attributes(.selected, when: self.context == context) - } - } - - if context == .employee { - EmployeeSelect.purchaseOrderSearch() - } else if context == .customer { - input( - .type(.text), .class("col-6"), .style("margin-left: 60px; margin-top: 18px;"), - .name("customerSearch"), .placeholder("Search"), .required - ) - } else if context == .vendor { - VendorBranchSelect.purchaseOrderSearch() - } - } - - div(.class("btn-row")) { - button(.type(.submit), .class("btn-primary")) - { "Search" } - } - } - } - -} diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift deleted file mode 100644 index f143b21..0000000 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ /dev/null @@ -1,137 +0,0 @@ -import Elementary -import ElementaryHTMX -import Fluent -import SharedModels -import Vapor - -struct PurchaseOrderTable: HTML { - typealias SearchContext = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context - - let page: Page - let context: Context - let searchContext: SearchContext? - - init( - page: Page, - context: Context = .default, - searchContext: SearchContext? = nil - ) { - self.page = page - self.context = context - self.searchContext = searchContext - } - - var content: some HTML { - table(.id(.purchaseOrder())) { - thead { - buttonRow - tableHeader - } - tbody(.id(.purchaseOrder(.table))) { - Rows(page: page) - } - } - } - - private var tableHeader: some HTML { - tr { - th { "PO" } - th { "Work Order" } - th { "Customer" } - th { "Vendor" } - th { "Materials" } - th { "Created For" } - th { - if context != .search { - Button.add() - .attributes( - .hx.get(route: .purchaseOrder(.form)), .hx.target(.id(.float)), - .hx.swap(.outerHTML), .hx.pushURL(true) - ) - } - } - } - } - - private var buttonRow: some HTML { - tr { - div(.class("btn-row")) { - if context != .search { - button( - .id("btn-search"), - .class("btn-primary"), .style("position: absolute; top: 80px; right: 20px;"), - .hx.get(route: .purchaseOrder(.search(.index(context: .employee, table: true)))), - .hx.target(.body), - .hx.swap(.outerHTML.transition(true).swap("0.5s")), - .hx.pushURL(true) - ) - { Img.search() } - } - } - } - } - - // Produces only the rows for the given page - struct Rows: HTML { - let page: Page - - var content: some HTML { - for purchaseOrder in page.items { - Row(purchaseOrder: purchaseOrder) - } - // We set page to 0 when we're on search, but have not completed the search - // form yet, so don't add the infinite scroll row / trigger otherwise it will - // load the first page, which is not what we want, but we need the empty table - // to be available once the search form is completed. - if page.metadata.page > 0, page.metadata.pageCount > page.metadata.page { - tr( - .hx.get(route: .purchaseOrder(.page(page: page.metadata.page + 1, limit: page.metadata.per))), - .hx.trigger(.event(.revealed)), - .hx.swap(.outerHTML.transition(true).swap("1s")), - .hx.target(.this), - .hx.indicator("next .htmx-indicator") - ) { - img(.src("/images/spinner.svg"), .class("htmx-indicator"), .width(60), .height(60)) - } - } - } - } - - // A single row. - struct Row: HTML { - let purchaseOrder: PurchaseOrder - - var content: some HTML { - tr( - .id(.purchaseOrder(.row(id: purchaseOrder.id))) - ) { - td { "\(purchaseOrder.id)" } - td { purchaseOrder.workOrder != nil ? String(purchaseOrder.workOrder!) : "" } - td { purchaseOrder.customer } - td { purchaseOrder.vendorBranch.displayName } - td { purchaseOrder.materials } - td { purchaseOrder.createdFor.fullName } - td { - Button.detail() - .attributes( - .hx.get(route: .purchaseOrder(.get(id: purchaseOrder.id))), - .hx.target(.id(.float)), - .hx.swap(.outerHTML.transition(true).swap("0.5s")), - .hx.pushURL(true) - ) - } - } - } - } - - enum Context: String { - case `default` - case search - } -} - -private extension VendorBranch.Detail { - var displayName: String { - "\(vendor.name.capitalized) - \(name.capitalized)" - } -} diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift deleted file mode 100644 index f9d429b..0000000 --- a/Sources/App/Views/Users/UserDetail.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Dependencies -import Elementary -import Foundation -import SharedModels - -struct UserDetail: HTML, Sendable { - @Dependency(\.dateFormatter) var dateFormatter - - let user: User? - - var content: some HTML { - Float(shouldDisplay: user != nil, resetURL: .user(.index)) { - if let user { - form( - .hx.post(route: .user(.get(id: user.id))), - .hx.swap(.outerHTML), - .hx.target(.id(.user(.row(id: user.id)))), - .hx.on(.afterRequest, .toggleContent(.float)) - ) { - div(.class("row")) { - makeLabel(for: "username", value: "Username:") - input(.class("col-4"), .type(.text), .id("username"), .name("username"), .value(user.username), .required) - makeLabel(for: "email", value: "Email:") - input(.class("col-4"), .type(.email), .id("email"), .name("email"), .value(user.email), .required) - } - div(.class("row")) { - span(.class("label col-2")) { "Created:" } - span(.class("date col-4")) { dateFormatter.formattedDate(user.createdAt) } - span(.class("label col-2")) { "Updated:" } - span(.class("date col-4")) { dateFormatter.formattedDate(user.updatedAt) } - } - div(.class("btn-row user-buttons")) { - button( - .type(.submit), - .class("btn-secondary") - ) { "Update" } - Button.danger { "Delete" } - .attributes( - .hx.delete(route: .user(.get(id: user.id))), - .hx.trigger(.event(.click)), - .hx.swap(.outerHTML), - .hx.target(.id(.user(.row(id: user.id)))), - .hx.confirm("Are you sure you want to delete this user?"), - .hx.on( - .afterRequest, - .toggleContent(.float), .setWindowLocation(to: .user(.index)) - ) - ) - } - } - } - } - } - - func makeLabel( - for name: String, - value: String - ) -> some HTML { - label(.for(name), .class("col-2")) { span(.class("label")) { value } } - } - - func row(_ label: String, _ value: String) -> some HTML { - tr { - td(.class("label")) { h3 { label } } - td { h3 { value } } - } - } -} - -extension DateFormatter { - - func formattedDate(_ date: Date?) -> String { - guard let date else { return "" } - return string(from: date) - } -} diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift deleted file mode 100644 index 4ce7c95..0000000 --- a/Sources/App/Views/Users/UserForm.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -// Form used to login or create a new user. -struct UserForm: HTML, Sendable { - let context: Context - - var content: some HTML { - if context == .create { - Float(shouldDisplay: true) { - makeForm() - } - } else { - makeForm() - } - } - - private func makeForm() -> some HTML { - form( - .id(.user(.form)), - .class("user-form"), - .hx.post(context.targetURL), - .hx.pushURL(context.pushURL), - .hx.target(context.target), - .hx.swap(context == .create ? .afterBegin.transition(true).swap("0.5s") : .outerHTML), - .hx.on( - .afterRequest, - .ifSuccessful(.resetForm, .toggleContent(.float)) - ) - ) { - if case let .login(next) = context, let next { - input(.type(.hidden), .name("next"), .value(next)) - } - div(.class("row")) { - input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required) - } - if context.showEmailInput { - div(.class("row")) { - input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required) - } - } - div(.class("row")) { - input(.type(.password), .id("password"), .name("password"), .placeholder("Password"), .required) - } - if context.showConfirmPassword { - div(.class("row")) { - input( - .type(.password), .id("confirmPassword"), .name("confirmPassword"), - .placeholder("Confirm Password"), - .required - ) - } - } - div(.class("row")) { - button(.type(.submit), .class("btn-primary")) { context.buttonLabel } - } - } - } - - enum Context: Equatable { - case create - case login(next: String?) - - var showConfirmPassword: Bool { - switch self { - case .create: return true - case .login: return false - } - } - - var showEmailInput: Bool { - switch self { - case .create: return true - case .login: return false - } - } - - var pushURL: Bool { - switch self { - case .create: return false - case .login: return true - } - } - - var buttonLabel: String { - switch self { - case .create: - return "Create" - case .login: - return "Login" - } - } - - var target: HXTarget { - switch self { - case .create: - return .id(.user(.table)) - case .login: - return .body - } - } - - // TODO: Return a ViewRoute. - var targetURL: String { - switch self { - case .create: - return "/users" - case .login: - return "/login" - // let path = "/login" - // if let next { - // return "\(path)?next=\(next)" - // } - // return path - } - } - } -} diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift deleted file mode 100644 index 54bb936..0000000 --- a/Sources/App/Views/Users/UserTable.swift +++ /dev/null @@ -1,57 +0,0 @@ -import DatabaseClient -import Dependencies -import Elementary -import ElementaryHTMX -import SharedModels - -struct UserTable: HTML { - - let users: [User] - - var content: some HTML { - table { - thead { - tr { - th { "Username" } - th { "Email" } - th(.style("width: 50px;")) { - Button.add() - .attributes( - .hx.get(route: .user(.form)), - .hx.target(.id(.float)), - .hx.swap(.outerHTML) - ) - } - } - } - tbody(.id(.user(.table))) { - for user in users { - Row(user: user) - } - } - } - } - - struct Row: HTML { - let user: User - - init(user: User) { - self.user = user - } - - var content: some HTML { - tr(.id(.user(.row(id: user.id)))) { - td { user.username } - td { user.email } - td { - Button.detail().attributes( - .hx.get(route: .user(.get(id: user.id))), - .hx.target(.id(.float)), - .hx.swap(.outerHTML), - .hx.pushURL(true) - ) - } - } - } - } -} diff --git a/Sources/App/Views/Utils/Buttons.swift b/Sources/App/Views/Utils/Buttons.swift deleted file mode 100644 index 98333f8..0000000 --- a/Sources/App/Views/Utils/Buttons.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Elementary -import SharedModels -import URLRouting - -struct ToggleFormButton: HTML { - var content: some HTML { - a(.href("javascript:void(0)"), .on(.click, "toggleContent('form')"), .class("btn-add")) { - "+" - } - } -} - -enum Button { - - static func add() -> some HTML { - button(.class("btn btn-add")) { "+" } - } - - static func danger(@HTMLBuilder body: () -> C) -> some HTML { - button(.class("danger")) { body() } - } - - static func close(id: String, resetURL: String? = nil) -> some HTML { - button(.class("btn-close"), .on(.click, makeOnClick(id, resetURL))) { - "x" - } - } - - static func close(id: IDKey, resetURL route: ViewRoute? = nil) -> some HTML { - close( - id: id.description, - resetURL: route != nil ? ViewRoute.router.path(for: route!) : nil - ) - } - - static func update() -> some HTML { - button(.class("btn-update")) { "Update" } - } - - static func detail() -> some HTML { - button(.class("btn-detail")) { - "〉" - } - } - - private static func makeOnClick(_ id: String, _ resetURL: String?) -> String { - let output = "toggleContent('\(id)');" - if let resetURL { - return "\(output) window.location.href='\(resetURL)';" - } - return output - } -} diff --git a/Sources/App/Views/Utils/Float.swift b/Sources/App/Views/Utils/Float.swift deleted file mode 100644 index cf459d7..0000000 --- a/Sources/App/Views/Utils/Float.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Elementary -import SharedModels - -struct Float: HTML { - - let id: String - let shouldDisplay: Bool - let body: C? - let closeButton: B? - - init(id: String = "float") { - self.id = id - self.shouldDisplay = false - self.body = nil - self.closeButton = nil - } - - init( - id: String = "float", - shouldDisplay: Bool, - @HTMLBuilder body: () -> C, - @HTMLBuilder closeButton: () -> B - ) { - self.id = id - self.shouldDisplay = shouldDisplay - self.body = body() - self.closeButton = closeButton() - } - - private var classString: String { - shouldDisplay ? "float" : "" - } - - private var display: String { - shouldDisplay ? "block" : "hidden" - } - - var content: some HTML { - div(.id(id), .class(classString), .style("display: \(display);")) { - if let body, shouldDisplay { - if let closeButton { - div(.class("btn-row")) { - closeButton - } - } - body - } - } - } -} - -struct DefaultCloseButton: HTML { - let id: String - let resetURL: String? - - var content: some HTML { - Button.close(id: id, resetURL: resetURL) - } -} - -extension Float where B == DefaultCloseButton { - init( - id: String = "float", - shouldDisplay: Bool, - resetURL route: ViewRoute? = nil, - @HTMLBuilder body: () -> C - ) { - self.init( - id: id, - shouldDisplay: shouldDisplay, - body: body, - closeButton: { DefaultCloseButton( - id: id, - resetURL: route != nil ? ViewRoute.router.path(for: route!) : nil - ) } - ) - } - -} - -extension Float: Sendable where C: Sendable, B: Sendable {} diff --git a/Sources/App/Views/Utils/Img.swift b/Sources/App/Views/Utils/Img.swift deleted file mode 100644 index b048088..0000000 --- a/Sources/App/Views/Utils/Img.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Elementary - -enum Img { - @Sendable - static func spinner(width: Int = 30, height: Int = 30) -> some HTML { - img(.src("/images/spinner.svg"), .width(width), .height(height)) - } - - @Sendable - static func search(width: Int = 30, height: Int = 30) -> some HTML { - img(.src("/images/search.svg"), .width(width), .height(height)) - } - - @Sendable - static func trashCan(width: Int = 30, height: Int = 30) -> some HTML { - img(.src("/images/trash-can.svg"), .width(width), .height(height)) - } -} diff --git a/Sources/App/Views/Utils/Navbar.swift b/Sources/App/Views/Utils/Navbar.swift deleted file mode 100644 index 8fce606..0000000 --- a/Sources/App/Views/Utils/Navbar.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Elementary -import ElementaryHTMX - -struct Navbar: HTML, Sendable { - var content: some HTML { - div(.class("sidepanel"), .id("sidepanel")) { - a(.href("javascript:void(0)"), .class("closebtn"), .on(.click, "closeSidepanel()")) { - "x" - } - a(.hx.get("/purchase-orders?page=1&limit=50"), .hx.target("body"), .hx.pushURL(true)) { - "Purchase Orders" - } - a(.hx.get("/users"), .hx.target("body"), .hx.pushURL(true)) { - "Users" - } - a(.hx.get("/employees"), .hx.target("body"), .hx.pushURL(true)) { - "Employees" - } - a(.hx.get("/vendors"), .hx.target("body"), .hx.pushURL(true)) { - "Vendors" - } - div(.style("border-bottom: 1px solid grey; margin-bottom: 5px;")) {} - a(.hx.post("/logout"), .hx.target("#content"), .hx.swap(.outerHTML), .hx.trigger(.event(.click))) { - "Logout" - } - } - button(.class("openbtn"), .on(.click, "openSidepanel()")) { - img(.src("/images/menu.svg"), .style("width: 30px;, height: 30px;")) - } - } -} diff --git a/Sources/App/Views/Utils/Select.swift b/Sources/App/Views/Utils/Select.swift deleted file mode 100644 index 0e9bb90..0000000 --- a/Sources/App/Views/Utils/Select.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels -import Vapor - -struct EmployeeSelect: HTML { - - let employees: [Employee]? - let context: ViewRoute.SelectContext - - var content: some HTML { - if let employees { - select(.name("createdForID"), .class(context.classString)) { - for employee in employees { - option(.value(employee.id.uuidString)) { employee.fullName } - } - } - .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch) - } else { - div( - .hx.get(route: .employee(.select(context: context))), - .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(employees: employees, context: .purchaseOrderForm) - } - - static func purchaseOrderSearch(employees: [Employee]? = nil) -> Self { - .init(employees: employees, context: .purchaseOrderSearch) - } - -} - -struct VendorBranchSelect: HTML { - let branches: [VendorBranch.Detail]? - let context: ViewRoute.SelectContext - - var content: some HTML { - if let branches { - select(.name("vendorBranchID"), .class("col-4")) { - for branch in branches { - option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" } - } - } - .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch) - } else { - div( - .hx.get(route: .vendorBranch(.select(context: context))), - .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(branches: [VendorBranch.Detail]? = nil) -> Self { - .init(branches: branches, context: .purchaseOrderForm) - } - - static func purchaseOrderSearch(branches: [VendorBranch.Detail]? = nil) -> Self { - .init(branches: branches, context: .purchaseOrderSearch) - } -} - -// enum SelectContext: String, Codable, Content { -// case purchaseOrderForm -// case purchaseOrderSearch - -extension ViewRoute.SelectContext { - var classString: String { - switch self { - case .purchaseOrderForm: return "col-3" - case .purchaseOrderSearch: return "col-6" - } - } -} diff --git a/Sources/App/Views/VendorBranches/VendorBranchForm.swift b/Sources/App/Views/VendorBranches/VendorBranchForm.swift deleted file mode 100644 index 9d8b3fd..0000000 --- a/Sources/App/Views/VendorBranches/VendorBranchForm.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct VendorBranchForm: HTML { - let vendorID: Vendor.ID - - var content: some HTML { - form( - .id(.branch(.form)), - .hx.post(route: .vendorBranch(.index())), - .hx.target(.id(.branch(.list))), - .hx.swap(.beforeEnd), - .hx.on(.afterRequest, .ifSuccessful(.resetForm)) - ) { - input(.type(.hidden), .name("vendorID"), .value(vendorID.uuidString)) - input( - .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, - // .hx.post(route: .vendorBranch(.index())), - .hx.trigger(.event(.keyup).changed().delay("800ms")) // , - // .hx.target(.id(.branch(.list))), - // .hx.swap(.beforeEnd), - // .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") - ) - button( - .type(.submit), - .class("btn-secondary"), - .style("float: right; padding: 10px 50px;"), - .hx.target(.id(.branch(.list))), - .hx.swap(.beforeEnd) - ) { "+" } - } - } -} diff --git a/Sources/App/Views/VendorBranches/VendorBranchList.swift b/Sources/App/Views/VendorBranches/VendorBranchList.swift deleted file mode 100644 index bfc8c8e..0000000 --- a/Sources/App/Views/VendorBranches/VendorBranchList.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct VendorBranchList: HTML { - let vendorID: Vendor.ID - let branches: [VendorBranch]? - - var content: some HTML { - if let branches { - ul(.id(.branch(.list))) { - for branch in branches { - Row(branch: branch) - } - } - } else { - div( - .hx.get(route: .vendorBranch(.index(for: vendorID))), - .hx.target(.this), - .hx.indicator(".hx-indicator"), - .hx.trigger(.event(.revealed)) - ) { - Img.spinner().attributes(.class("hx-indicator")) - } - } - } - - struct Row: HTML { - let branch: VendorBranch - - var content: some HTML { - li(.id(.branch(.row(id: branch.id))), .class("branch-row")) { - span(.class("label")) { branch.name.capitalized } - button( - .class("btn"), - .hx.delete(route: .vendorBranch(.delete(id: branch.id))), - .hx.target(.id(.branch(.row(id: branch.id)))), - .hx.swap(.outerHTML.transition(true).swap("0.5s")) - ) { - Img.trashCan().attributes(.style("margin-top: 5px;")) - } - } - } - } - -} diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift deleted file mode 100644 index e459950..0000000 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct VendorDetail: HTML { - - let vendor: Vendor - - var content: some HTML { - Float(shouldDisplay: true) { - VendorForm(.formOnly(vendor)) - h2(.style("margin-left: 20px; font-size: 1.5em;"), .class("label")) { "Branches" } - VendorBranchForm(vendorID: vendor.id) - VendorBranchList(vendorID: vendor.id, branches: nil) - } closeButton: { - Button.close(id: "float") - .attributes( - .hx.get(route: .vendor(.index)), - .hx.pushURL(true), - .hx.target(.body), - .hx.swap(.outerHTML) - ) - } - } -} diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift deleted file mode 100644 index f140200..0000000 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct VendorForm: HTML { - - let context: Context - var vendor: Vendor? { context.vendor } - - init( - _ context: Context - ) { - self.context = context - } - - init() { self.init(.float(nil)) } - - enum Context { - case float(Vendor? = nil, shouldShow: Bool = false) - case formOnly(Vendor) - - var vendor: Vendor? { - switch self { - case let .float(vendor, _): return vendor - case let .formOnly(vendor): return vendor - } - } - } - - var content: some HTML { - switch context { - case let .float(vendor, shouldDisplay): - Float(shouldDisplay: shouldDisplay) { - makeForm(vendor: vendor) - } - case let .formOnly(vendor): - makeForm(vendor: vendor) - } - } - - func makeForm(vendor: Vendor?) -> some HTML { - form( - .id(.vendor(.form)), - vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), - .hx.target("#content"), - .hx.swap(.outerHTML) - ) { - div(.class("row")) { - input( - .type(.text), - .class("col-9"), - .id("vendor-name"), - .name("name"), - .value(vendor?.name ?? ""), - .placeholder("Vendor Name"), - vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), - .hx.trigger(.event(.keyup).changed().delay("500ms")), - .required - ) - if let vendor { - button( - .class("danger"), - .style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"), - .hx.delete(route: .vendor(.delete(id: vendor.id))), - .hx.confirm("Are you sure you want to delete this vendor?"), - .hx.target(.id(.vendor(.row(id: vendor.id)))), - .hx.swap(.outerHTML.transition(true).swap("1s")), - .custom( - name: "hx-on::after-request", - value: "if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';" - ) - ) { "Delete" } - } - button( - .type(.submit), - .class("btn-primary"), - .style("float: right") - ) { buttonLabel } - } - } - } - - private var buttonLabel: String { - guard vendor != nil else { return "Create" } - return "Update" - } - - var targetURL: SharedModels.ViewRoute { - guard let vendor else { return .vendor(.index) } - return .vendor(.get(id: vendor.id)) - } -} diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift deleted file mode 100644 index 3858a01..0000000 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Elementary -import ElementaryHTMX -import SharedModels - -struct VendorTable: HTML { - let vendors: [Vendor] - - var content: some HTML { - table { - thead { - tr { - th { "Name" } - th { "Branches" } - th(.style("width: 100px;")) { - Button.add() - .attributes( - .style("padding: 0px 10px;"), - .hx.get(route: .vendor(.form)), - .hx.target(.id(.float)), - .hx.swap(.outerHTML) - ) - } - } - } - tbody(.id("vendor-table")) { - for vendor in vendors { - Row(vendor: vendor) - } - } - } - } - - struct Row: HTML { - let vendor: Vendor - - var content: some HTML { - tr(.id(.vendor(.row(id: vendor.id)))) { - td { vendor.name.capitalized } - td { "(\(vendor.branches?.count ?? 0)) Branches" } - td { - Button.detail() - .attributes( - .style("padding-left: 15px;"), - .hx.get(route: .vendor(.get(id: vendor.id))), - .hx.target(.id(.float)), - .hx.pushURL(true), - .hx.swap(.outerHTML) - ) - } - } - } - } -} diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 674d7f6..0fb71fa 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -79,6 +79,7 @@ extension SiteRoute { case .health: return nil case let .view(route): + // return nil return route.middleware } } @@ -95,7 +96,8 @@ func siteHandler( case .health: return HTTPStatus.ok case let .view(route): - return try await route.handle(request: request) + return try await route.respond(request: request) + // return try await route.handle(request: request) } } diff --git a/Sources/ViewControllerLive/Routes+view.swift b/Sources/ViewControllerLive/Routes+view.swift index 358bdc0..7854562 100644 --- a/Sources/ViewControllerLive/Routes+view.swift +++ b/Sources/ViewControllerLive/Routes+view.swift @@ -8,7 +8,10 @@ public typealias AnySendableHTML = (any HTML & Sendable) public extension SharedModels.ViewRoute { - func view(isHtmxRequest: Bool, authenticate: @escaping @Sendable (User) -> Void) async throws -> AnySendableHTML? { + func view( + isHtmxRequest: Bool, + authenticate: @escaping @Sendable (User) -> Void + ) async throws -> AnySendableHTML? { @Dependency(\.database.users) var users switch self { case .index: