diff --git a/Public/images/search.svg b/Public/images/search.svg
new file mode 100644
index 0000000..1170aac
--- /dev/null
+++ b/Public/images/search.svg
@@ -0,0 +1 @@
+
diff --git a/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift b/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift
new file mode 100644
index 0000000..e1a0475
--- /dev/null
+++ b/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift
@@ -0,0 +1,84 @@
+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.get("form", use: form)
+ 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)
+ guard req.isHtmxRequest else {
+ 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) }
+ }
+
+ //
+ // @Sendable
+ // func form(req: Request) async throws -> HTMLResponse {
+ // let query = try req.query.decode(FormQuery.self)
+ // let html = PurchaseOrderSearch(context: query.context)
+ // guard req.isHtmxRequest else {
+ // return await req.render { mainPage(search: html) }
+ // }
+ // return await req.render { PurchaseOrderSearch(context: query.context) }
+ // }
+
+ 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
+}
diff --git a/Sources/App/Controllers/View/PurchaseOrderViewController.swift b/Sources/App/Controllers/View/PurchaseOrderViewController.swift
index 3ca02a4..de93e48 100644
--- a/Sources/App/Controllers/View/PurchaseOrderViewController.swift
+++ b/Sources/App/Controllers/View/PurchaseOrderViewController.swift
@@ -1,5 +1,6 @@
import Dependencies
import Elementary
+import Fluent
import SharedModels
import Vapor
import VaporElementary
@@ -14,12 +15,10 @@ 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.post("search", use: postSearch)
+ // route.get("search", use: getSearch)
route.group("create") {
$0.get(use: form)
- $0.get("vendor-branch-select", use: vendorBranchSelect(req:))
- $0.get("employee-select", use: employeeSelect(req:))
}
route.group(":id") {
$0.get(use: get)
@@ -49,18 +48,6 @@ struct PurchaseOrderViewController: RouteCollection {
return await req.render { PurchaseOrderForm(shouldShow: true) }
}
- @Sendable
- func vendorBranchSelect(req: Request) async throws -> HTMLResponse {
- let branches = try await vendorBranches.fetchAllWithDetail()
- return await req.render { PurchaseOrderForm.VendorSelect(vendorBranches: branches) }
- }
-
- @Sendable
- func employeeSelect(req: Request) async throws -> HTMLResponse {
- let employees = try await self.employees.fetchAll()
- return await req.render { PurchaseOrderForm.EmployeeSelect(employees: employees) }
- }
-
@Sendable
func get(req: Request) async throws -> HTMLResponse {
let purchaseOrder = try await purchaseOrders.get(req.ensureIDPathComponent(as: Int.self))
@@ -83,18 +70,23 @@ struct PurchaseOrderViewController: RouteCollection {
@Sendable
func postSearch(req: Request) async throws -> HTMLResponse {
- let query = try req.content.decode([String: String].self)
- req.logger.info("query: \(query)")
- let context = try req.content.decode(SearchContext.self).toSearch()
- let purchaseOrders = try await purchaseOrders.search(context)
- req.logger.info("\(purchaseOrders)")
- return await req.render { PurchaseOrderTable.Rows(page: purchaseOrders) }
+ 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 {
- let context = try req.query.decode(SearchQuery.self).toSearchContext()
- return await req.render { PurchaseOrderSearch(context: context) }
+ // 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(
@@ -103,9 +95,8 @@ struct PurchaseOrderViewController: RouteCollection {
) async throws -> some SendableHTMLDocument where C: Sendable {
let page = try await purchaseOrders.fetchPage(.init(page: page.page, per: page.limit))
return MainPage(displayNav: true, route: .purchaseOrders) {
- div(.class("container")) {
+ div(.class("container"), .id("purchase-order-content")) {
html
- PurchaseOrderSearch()
PurchaseOrderTable(page: page)
}
}
@@ -142,32 +133,3 @@ private struct CreateContext: Content {
)
}
}
-
-private struct SearchContext: Content {
- let context: String
- let search: String
-
- func toSearch() throws -> PurchaseOrder.SearchContext {
- switch context {
- case "employee":
- return .employee(search)
- case "customer":
- return .customer(search)
- case "vendor":
- return .vendor(search)
- default:
- throw Abort(.badRequest, reason: "Invalid search context.")
- }
- }
-}
-
-struct SearchQuery: Content {
- let context: String
-
- func toSearchContext() throws -> PurchaseOrderSearchContext {
- guard let context = PurchaseOrderSearchContext(rawValue: context) else {
- throw Abort(.badRequest, reason: "Invalid context.")
- }
- return context
- }
-}
diff --git a/Sources/App/Controllers/View/UtilsViewController.swift b/Sources/App/Controllers/View/UtilsViewController.swift
index 573ac1a..1e07824 100644
--- a/Sources/App/Controllers/View/UtilsViewController.swift
+++ b/Sources/App/Controllers/View/UtilsViewController.swift
@@ -11,28 +11,44 @@ struct UtilsViewController: RouteCollection {
let route = routes.protected
route.group("select") {
$0.get("employee", use: employeeSelect(req:))
+ $0.get("vendor-branches", use: vendorBranchSelect(req:))
}
}
@Sendable
func employeeSelect(req: Request) async throws -> HTMLResponse {
- let context = try req.query.decode(EmployeeSelectContext.self)
+ let context = try req.query.decode(SelectQueryContext.self)
let employees = try await database.employees.fetchAll()
return await req.render { context.toHTML(employees: employees) }
}
+
+ @Sendable
+ func vendorBranchSelect(req: Request) async throws -> HTMLResponse {
+ let context = try req.query.decode(SelectQueryContext.self)
+ let branches = try await database.vendorBranches.fetchAllWithDetail()
+ return await req.render { context.toHTML(branches: branches) }
+ }
}
-private struct EmployeeSelectContext: Content {
+private struct SelectQueryContext: Content {
- let context: EmployeeSelect.Context
+ let context: SelectContext
func toHTML(employees: [Employee]) -> EmployeeSelect {
switch context {
- case .form:
+ case .purchaseOrderForm:
return .purchaseOrderForm(employees: employees)
- case .search:
+ case .purchaseOrderSearch:
return .purchaseOrderSearch(employees: employees)
}
}
+ func toHTML(branches: [VendorBranch.Detail]) -> VendorBranchSelect {
+ switch context {
+ case .purchaseOrderForm:
+ return .purchaseOrderForm(branches: branches)
+ case .purchaseOrderSearch:
+ return .purchaseOrderSearch(branches: branches)
+ }
+ }
}
diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift
index a727710..6023a69 100644
--- a/Sources/App/Views/Employees/EmployeeForm.swift
+++ b/Sources/App/Views/Employees/EmployeeForm.swift
@@ -20,7 +20,7 @@ struct EmployeeForm: HTML {
Float(shouldDisplay: shouldShow, resetURL: "/employees") {
form(
employee == nil ? .hx.post(targetURL) : .hx.put(targetURL),
- employee == nil ? .hx.target("#employee-table") : .hx.target("#employee_\(employee!.id)"),
+ .hx.target(target),
employee == nil
? .hx.swap(.beforeEnd.transition(true).swap("0.5s"))
: .hx.swap(.outerHTML.transition(true).swap("0.5s")),
@@ -62,6 +62,13 @@ struct EmployeeForm: HTML {
}
}
+ private var target: HXTarget {
+ guard let employee else {
+ return .employee(.table)
+ }
+ return .employee(.row(id: employee.id))
+ }
+
private var buttonLabel: String {
guard employee != nil else { return "Create" }
return "Update"
diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift
index 9db1e46..0390b26 100644
--- a/Sources/App/Views/Employees/EmployeeTable.swift
+++ b/Sources/App/Views/Employees/EmployeeTable.swift
@@ -14,14 +14,14 @@ struct EmployeeTable: HTML {
Button.add()
.attributes(
.style("padding: 0px 10px;"),
- .hx.get("/employees/create"),
- .hx.target("#float"),
+ .hx.get(route: .employees(.create)),
+ .hx.target(.float),
.hx.swap(.outerHTML.transition(true).swap("0.5s"))
)
}
}
}
- tbody(.id("employee-table")) {
+ tbody(.id(.employee(.table))) {
for employee in employees {
Row(employee: employee)
}
@@ -33,14 +33,14 @@ struct EmployeeTable: HTML {
let employee: Employee
var content: some HTML {
- tr(.id("employee_\(employee.id)")) {
- td { "\(employee.firstName.capitalized) \(employee.lastName.capitalized)" }
+ tr(.id(.employee(.row(id: employee.id)))) {
+ td { employee.fullName }
td {
Button.detail()
.attributes(
.style("padding-left: 15px;"),
- .hx.get("/employees/\(employee.id)"),
- .hx.target("#float"),
+ .hx.get(route: .employees(.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 31f99d7..61d7d20 100644
--- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift
+++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift
@@ -24,13 +24,10 @@ struct PurchaseOrderForm: HTML {
}
}
form(
- .hx.post("/purchase-orders"),
- .hx.target("#purchase-order-table"),
+ .hx.post(route: .purchaseOrders()),
+ .hx.target(.purchaseOrders(.table)),
.hx.swap(.afterBegin),
- .custom(
- name: "hx-on::after-request",
- value: "if(event.detail.successful) toggleContent('float')"
- )
+ .customToggleFloatAfterRequest
) {
div(.class("row")) {
label(
@@ -65,16 +62,7 @@ struct PurchaseOrderForm: HTML {
.for("vendorBranchID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
) { "Vendor:" }
if purchaseOrder == nil {
- div(
- .class("col-4"),
- .hx.get("/purchase-orders/create/vendor-branch-select"),
- .hx.target("this"),
- .hx.swap(.outerHTML.transition(true).swap("0.5s")),
- .hx.trigger(.event(.revealed)),
- .hx.indicator(".hx-indicator")
- ) {
- Img.spinner().attributes(.class("hx-indicator"), .style("float: left;"))
- }
+ VendorBranchSelect.purchaseOrderForm()
} else {
input(
.type(.text), .class("col-4"),
@@ -89,16 +77,7 @@ struct PurchaseOrderForm: HTML {
.for("createdForID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
) { "Employee:" }
if purchaseOrder == nil {
- div(
- .class("col-3"),
- .hx.get("/purchase-orders/create/employee-select"),
- .hx.target("this"),
- .hx.swap(.outerHTML.transition(true).swap("0.5s")),
- .hx.trigger(.event(.revealed)),
- .hx.indicator(".hx-indicator")
- ) {
- Img.spinner().attributes(.class("hx-indicator"), .style("float: left;"))
- }
+ EmployeeSelect.purchaseOrderForm()
} else {
input(
.type(.text), .class("col-3"),
@@ -134,28 +113,4 @@ struct PurchaseOrderForm: HTML {
guard purchaseOrder != nil else { return "Create" }
return "Update"
}
-
- struct VendorSelect: HTML {
- let vendorBranches: [VendorBranch.Detail]
-
- var content: some HTML {
- select(.name("vendorBranchID"), .class("col-4")) {
- for branch in vendorBranches {
- option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" }
- }
- }
- }
- }
-
- struct EmployeeSelect: HTML {
- let employees: [Employee]
-
- var content: some HTML {
- select(.name("createdForID"), .class("col-3")) {
- for employee in employees {
- option(.value(employee.id.uuidString)) { employee.fullName }
- }
- }
- }
- }
}
diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift
index 971e9ee..aaffbad 100644
--- a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift
+++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift
@@ -5,46 +5,70 @@ import Vapor
struct PurchaseOrderSearch: HTML {
- let context: PurchaseOrderSearchContext?
+ let context: PurchaseOrderSearchContext
init(context: PurchaseOrderSearchContext? = nil) {
- self.context = context
+ self.context = context ?? .employee
}
var content: some HTML {
form(
- .id("search"),
- .hx.post("/purchase-orders/search"),
- .hx.target("#purchase-order-table"),
- .hx.swap(.outerHTML.transition(true).swap("1s"))
+ .id(.search),
+ .hx.post(route: .purchaseOrders(.search())),
+ .hx.target(.purchaseOrders()),
+ .hx.swap(.outerHTML)
) {
- select(
- .name("context"), .class("col-3"),
- .hx.get("/purchase-orders/search"),
- .hx.target("#search"),
- .hx.swap(.outerHTML)
- ) {
- option(.value("employee")) { "Employee" }
- .attributes(.selected, when: context == .employee || context == nil)
+ 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")
+ )
+ { "x" }
+ }
+ div(.class("row")) {
+ select(
+ .name("context"), .class("col-3"),
+ .hx.get(route: .purchaseOrders(.search())),
+ .hx.target(.search),
+ .hx.swap(.outerHTML.transition(true).swap("0.5s")),
+ .hx.pushURL(true)
+ ) {
+ for context in PurchaseOrderSearchContext.allCases {
+ option(.value(context.rawValue)) { context.rawValue.capitalized }
+ .attributes(.selected, when: self.context == context)
+ }
+ }
- option(.value("customer")) { "Customer" }
- .attributes(.selected, when: context == .customer)
+ if context == .employee {
+ EmployeeSelect.purchaseOrderSearch()
+ } else if context == .customer {
+ input(
+ .type(.text), .class("col-6"), .style("margin-left: 60px; margin-top: 18px;"),
+ .name("search"), .placeholder("Search"), .required
+ )
+ } else if context == .vendor {
+ VendorBranchSelect.purchaseOrderSearch()
+ }
}
- if context == .employee || context == nil {
- EmployeeSelect.purchaseOrderSearch()
- } else if context == .customer {
- input(.type(.text), .name("search"), .placeholder("Search"), .required)
+ div(.class("btn-row")) {
+ button(.type(.submit), .class("btn-primary"))
+ { "Search" }
}
-
- button(.type(.submit), .class("btn-primary")) { "Search" }
- // Img.spinner().attributes(.class("hx-indicator"))
}
}
}
-enum PurchaseOrderSearchContext: String, Codable, Content {
+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?
}
diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift
index 3b69628..c8b59ba 100644
--- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift
+++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift
@@ -7,30 +7,66 @@ import Vapor
struct PurchaseOrderTable: HTML {
let page: Page
+ let context: Context
+ let searchContext: PurchaseOrderSearchContext?
+
+ init(
+ page: Page,
+ context: Context = .default,
+ searchContext: PurchaseOrderSearchContext? = nil
+ ) {
+ self.page = page
+ self.context = context
+ self.searchContext = searchContext
+ }
var content: some HTML {
- table {
- thead {
- tr {
- th { "PO" }
- th { "Work Order" }
- th { "Customer" }
- th { "Vendor" }
- th { "Materials" }
- th { "Created For" }
- th {
- Button.add()
- .attributes(
- .hx.get("/purchase-orders/create"),
- .hx.target("#float"),
- .hx.swap(.outerHTML),
- .hx.pushURL(true)
- )
- }
+ table(.id(.purchaseOrders())) {
+ if page.items.count > 0 {
+ thead {
+ buttonRow
+ tableHeader
+ }
+ tbody(.id(.purchaseOrders(.table))) {
+ Rows(page: page)
}
}
- tbody(.id("purchase-order-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: .purchaseOrders(.create)), .hx.target(.float),
+ .hx.swap(.outerHTML), .hx.pushURL(true)
+ )
+ }
+ }
+ }
+ }
+
+ private var buttonRow: some HTML {
+ tr {
+ div(.class("btn-row")) {
+ if context != .search {
+ button(
+ .class("btn-primary"), .style("position: absolute; top: 80px; right: 20px;"),
+ .hx.get(route: .purchaseOrders(.search(.context(.employee)))),
+ .hx.target(.body),
+ .hx.swap(.outerHTML.transition(true).swap("0.5s")),
+ .hx.pushURL(true)
+ )
+ { Img.search() }
+ }
}
}
}
@@ -45,10 +81,11 @@ 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("/purchase-orders/next?page=\(page.metadata.page + 1)&limit=\(page.metadata.per)"),
+ .hx.get(route: .purchaseOrders(.nextPage(page.metadata))),
.hx.trigger(.event(.revealed)),
.hx.swap(.outerHTML.transition(true).swap("1s")),
- .hx.target("this"),
+ .hx.target(.this),
.hx.indicator("next .htmx-indicator")
) {
img(.src("/images/spinner.svg"), .class("htmx-indicator"), .width(60), .height(60))
@@ -63,7 +100,7 @@ struct PurchaseOrderTable: HTML {
var content: some HTML {
tr(
- .id("purchase_order_\(purchaseOrder.id)")
+ .id(.purchaseOrders(.row(id: purchaseOrder.id)))
) {
td { "\(purchaseOrder.id)" }
td { purchaseOrder.workOrder != nil ? String(purchaseOrder.workOrder!) : "" }
@@ -83,6 +120,11 @@ struct PurchaseOrderTable: HTML {
}
}
}
+
+ enum Context: String {
+ case `default`
+ case search
+ }
}
private extension VendorBranch.Detail {
diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift
index 60e1f29..3a5cfab 100644
--- a/Sources/App/Views/Users/UserDetail.swift
+++ b/Sources/App/Views/Users/UserDetail.swift
@@ -12,9 +12,9 @@ struct UserDetail: HTML, Sendable {
Float(shouldDisplay: user != nil, resetURL: "/users") {
if let user {
form(
- .hx.post("/users/\(user.id)"),
+ .hx.post(route: .users(.id(user.id))),
.hx.swap(.outerHTML),
- .hx.target("#user_\(user.id)"),
+ .hx.target(.user(.row(id: user.id))),
.custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';")
) {
div(.class("row")) {
@@ -36,10 +36,10 @@ struct UserDetail: HTML, Sendable {
) { "Update" }
Button.danger { "Delete" }
.attributes(
- .hx.delete("/users/\(user.id)"),
+ .hx.delete(route: .users(.id(user.id))),
.hx.trigger(.event(.click)),
.hx.swap(.outerHTML),
- .hx.target("#user_\(user.id)"),
+ .hx.target(.user(.row(id: user.id))),
.hx.confirm("Are you sure you want to delete this user?"),
.custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';")
)
diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift
index a637f8e..baacf44 100644
--- a/Sources/App/Views/Users/UserForm.swift
+++ b/Sources/App/Views/Users/UserForm.swift
@@ -17,7 +17,7 @@ struct UserForm: HTML, Sendable {
private func makeForm() -> some HTML {
form(
- .id("user-form"),
+ .id(.user(.form)),
.class("user-form"),
.hx.post(context.targetURL),
.hx.pushURL(context.pushURL),
@@ -99,6 +99,7 @@ struct UserForm: HTML, Sendable {
}
}
+ // TODO: Return a route container.
var targetURL: String {
switch self {
case .create:
diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift
index 46bb6d7..f41d3d3 100644
--- a/Sources/App/Views/Users/UserTable.swift
+++ b/Sources/App/Views/Users/UserTable.swift
@@ -9,7 +9,7 @@ struct UserTable: HTML {
let users: [User]
var content: some HTML {
- table(.id("user-table")) {
+ table(.id(.user(.table()))) {
thead {
tr {
th { "Username" }
@@ -17,14 +17,14 @@ struct UserTable: HTML {
th(.style("width: 50px;")) {
Button.add()
.attributes(
- .hx.get("/users/create"),
- .hx.target("#float"),
+ .hx.get(route: .users(.create)),
+ .hx.target(.float),
.hx.swap(.outerHTML)
)
}
}
}
- tbody(.id("user-table-body")) {
+ tbody(.id(.user(.table(.body)))) {
for user in users {
Row(user: user)
}
@@ -40,13 +40,13 @@ struct UserTable: HTML {
}
var content: some HTML {
- tr(.id("user_\(user.id)")) {
+ tr(.id(.user(.row(id: user.id)))) {
td { user.username }
td { user.email }
td {
Button.detail().attributes(
- .hx.get("/users/\(user.id.uuidString)"),
- .hx.target("#float"),
+ .hx.get(route: .users(.id(user.id))),
+ .hx.target(.float),
.hx.swap(.outerHTML),
.hx.pushURL(true)
)
diff --git a/Sources/App/Views/Utils/AttributeExtensions.swift b/Sources/App/Views/Utils/AttributeExtensions.swift
new file mode 100644
index 0000000..83991e3
--- /dev/null
+++ b/Sources/App/Views/Utils/AttributeExtensions.swift
@@ -0,0 +1,11 @@
+import Elementary
+
+extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global {
+
+ static var customToggleFloatAfterRequest: Self {
+ .custom(
+ name: "hx-on::after-request",
+ value: "if(event.detail.successful) toggleContent('float')"
+ )
+ }
+}
diff --git a/Sources/App/Views/Utils/EmployeeSelect.swift b/Sources/App/Views/Utils/EmployeeSelect.swift
deleted file mode 100644
index 1906397..0000000
--- a/Sources/App/Views/Utils/EmployeeSelect.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-import Elementary
-import ElementaryHTMX
-import SharedModels
-import Vapor
-
-struct EmployeeSelect: HTML {
-
- let classString: String
- let name: String
- let employees: [Employee]?
- let context: Context
-
- var content: some HTML {
- if let employees {
- select(.name(name), .class(classString)) {
- for employee in employees {
- option(.value(employee.id.uuidString)) { employee.fullName }
- }
- }
- .attributes(.style("margin-left: 15px;"), when: context == .search)
- } else {
- div(
- .hx.get("/select/employee?context=\(context.rawValue)"),
- .hx.target("this"),
- .hx.swap(.outerHTML.transition(true).swap("0.5s")),
- .hx.indicator("next .hx-indicator"),
- .hx.trigger(.event(.revealed)),
- .style("display: inline;")
- ) {
- Img.spinner().attributes(.class("hx-indicator"))
- }
- }
- }
-
- static func purchaseOrderForm(employees: [Employee]? = nil) -> Self {
- .init(classString: "col-3", name: "createdForID", employees: employees, context: .form)
- }
-
- static func purchaseOrderSearch(employees: [Employee]? = nil) -> Self {
- .init(classString: "col-3", name: "employeeID", employees: employees, context: .search)
- }
-
- enum Context: String, Codable, Content {
- case form
- case search
- }
-}
diff --git a/Sources/App/Views/Utils/Img.swift b/Sources/App/Views/Utils/Img.swift
index 64331a9..4044bd6 100644
--- a/Sources/App/Views/Utils/Img.swift
+++ b/Sources/App/Views/Utils/Img.swift
@@ -4,4 +4,8 @@ enum Img {
static func spinner(width: Int = 30, height: Int = 30) -> some HTML {
img(.src("/images/spinner.svg"), .width(width), .height(height))
}
+
+ static func search(width: Int = 30, height: Int = 30) -> some HTML {
+ img(.src("/images/search.svg"), .width(width), .height(height))
+ }
}
diff --git a/Sources/App/Views/Utils/Select.swift b/Sources/App/Views/Utils/Select.swift
new file mode 100644
index 0000000..b7b1f2c
--- /dev/null
+++ b/Sources/App/Views/Utils/Select.swift
@@ -0,0 +1,88 @@
+import Elementary
+import ElementaryHTMX
+import SharedModels
+import Vapor
+
+struct EmployeeSelect: HTML {
+
+ let employees: [Employee]?
+ let context: 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("/select/employee?context=\(context.rawValue)"),
+ .hx.target("this"),
+ .hx.swap(.outerHTML.transition(true).swap("0.5s")),
+ .hx.indicator("next .hx-indicator"),
+ .hx.trigger(.event(.revealed)),
+ .style("display: inline;")
+ ) {
+ Img.spinner().attributes(.class("hx-indicator"))
+ }
+ }
+ }
+
+ static func purchaseOrderForm(employees: [Employee]? = nil) -> Self {
+ .init(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: SelectContext
+
+ var content: some HTML {
+ if let branches {
+ select(.name("vendorBranchID"), .class(context.classString)) {
+ 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("/select/vendor-branches?context=\(context.rawValue)"),
+ .hx.target("this"),
+ .hx.swap(.outerHTML.transition(true).swap("0.5s")),
+ .hx.indicator("next .hx-indicator"),
+ .hx.trigger(.event(.revealed)),
+ .style("display: inline;")
+ ) {
+ Img.spinner().attributes(.class("hx-indicator"))
+ }
+ }
+ }
+
+ static func purchaseOrderForm(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
+
+ var classString: String {
+ switch self {
+ case .purchaseOrderForm: return "col-3"
+ case .purchaseOrderSearch: return "col-6"
+ }
+ }
+}
diff --git a/Sources/App/Views/ViewRoute.swift b/Sources/App/Views/ViewRoute.swift
index ec16346..0a45c79 100644
--- a/Sources/App/Views/ViewRoute.swift
+++ b/Sources/App/Views/ViewRoute.swift
@@ -1,3 +1,203 @@
+import Elementary
+import ElementaryHTMX
+import Fluent
+import SharedModels
+
+enum RouteContainer {
+ case employees(EmployeeRoute? = nil)
+ case purchaseOrders(PurchaseOrderRoute? = nil)
+ case users(UserRoute? = nil)
+
+ var url: String {
+ switch self {
+ case let .employees(employees):
+ let path = "/employees"
+ guard let employees else { return path }
+ return "\(path)/\(employees.path)"
+
+ case let .purchaseOrders(route):
+ let path = "/purchase-orders"
+ guard let route else { return path }
+ return "\(path)/\(route.path)"
+
+ case let .users(route):
+ let path = "/users"
+ guard let route else { return path }
+ return "\(path)/\(route.path)"
+ }
+ }
+
+ enum EmployeeRoute {
+ case create
+ case id(Employee.ID)
+
+ var path: String {
+ switch self {
+ case .create: return "create"
+ case let .id(id): return id.uuidString
+ }
+ }
+ }
+
+ enum PurchaseOrderRoute {
+ case create
+ case nextPage(PageMetadata)
+ case search(SearchQuery? = nil)
+
+ var path: String {
+ switch self {
+ case .create:
+ return "create"
+
+ 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)"
+ }
+ }
+
+ enum SearchQuery {
+ case context(PurchaseOrderSearchContext)
+
+ var query: String {
+ switch self {
+ case let .context(context):
+ return "context=\(context.rawValue)"
+ }
+ }
+ }
+ }
+
+ enum UserRoute {
+ case create
+ case id(User.ID)
+
+ var path: String {
+ switch self {
+ case .create: return "create"
+ case let .id(id): return id.uuidString
+ }
+ }
+ }
+
+}
+
+extension HTMLAttribute.hx {
+ static func get(route: RouteContainer) -> HTMLAttribute {
+ get(route.url)
+ }
+
+ static func post(route: RouteContainer) -> HTMLAttribute {
+ post(route.url)
+ }
+
+ static func put(route: RouteContainer) -> HTMLAttribute {
+ put(route.url)
+ }
+
+ static func delete(route: RouteContainer) -> HTMLAttribute {
+ delete(route.url)
+ }
+
+}
+
+enum RouteKey: String {
+ case purchaseOrders = "purchase-orders"
+}
+
+enum HXTarget {
+ case body
+ case employee(EmployeeKey)
+ case float
+ case purchaseOrders(PurchaseOrdersKey? = nil)
+ case search
+ case this
+ case user(UserKey)
+
+ var selector: String {
+ switch self {
+ case .body: return "body"
+ case .this: return "this"
+ default:
+ return "#\(id)"
+ }
+ }
+
+ var id: String {
+ switch self {
+ case let .employee(key): return key.key
+ case .float: return "float"
+ case let .purchaseOrders(key):
+ guard let key else { return "purchase-orders" }
+ return key.key
+ case .search: return "search"
+ case let .user(key): return key.key
+ case .this, .body:
+ fatalError("'\(selector)' can not be used as an id.")
+ }
+ }
+
+ enum PurchaseOrdersKey {
+ case table
+ case row(id: PurchaseOrder.ID)
+
+ var key: String {
+ switch self {
+ case .table: return "purchase-orders-table"
+ case let .row(id): return "purchase_order_\(id)"
+ }
+ }
+ }
+
+ enum EmployeeKey {
+ case table
+ case row(id: Employee.ID)
+
+ var key: String {
+ switch self {
+ case .table: return "employee-table"
+ case let .row(id): return "employee_\(id.uuidString)"
+ }
+ }
+ }
+
+ enum UserKey {
+ case form
+ case row(id: User.ID)
+ case table(Table? = nil)
+
+ var key: String {
+ switch self {
+ case .form: return "user-form"
+ case let .row(id): return "user_\(id)"
+ case let .table(table):
+ let key = "user-table"
+ guard let table else { return key }
+ return "\(key)-\(table.rawValue)"
+ }
+ }
+
+ enum Table: String {
+ case body
+ }
+ }
+}
+
+extension HTMLAttribute.hx {
+
+ static func target(_ target: HXTarget) -> HTMLAttribute {
+ Self.target(target.selector)
+ }
+}
+
+extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global {
+ static func id(_ target: HXTarget) -> Self {
+ id(target.id)
+ }
+}
+
enum ViewRoute: String {
case employees
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index 3fa097c..3eb2029 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -13,6 +13,7 @@ func routes(_ app: Application) throws {
try app.register(collection: VendorViewController())
try app.register(collection: EmployeeViewController())
try app.register(collection: PurchaseOrderViewController())
+ try app.register(collection: PurchaseOrderSearchViewController())
try app.register(collection: UtilsViewController())
app.get { _ in
@@ -56,18 +57,6 @@ func routes(_ app: Application) throws {
}
}
}
-
- let protected = app.grouped(UserPasswordAuthenticator(), UserSessionAuthenticator())
-
- protected.get("me") { req in
- let user = try req.auth.require(User.self)
-
- return HTMLResponse {
- MainPage(displayNav: false, route: .purchaseOrders) {
- h1 { "You are logged in as: \(user.username)" }
- }
- }
- }
}
private struct LoginContext: Content {
diff --git a/Sources/DatabaseClient/PurchaseOrders.swift b/Sources/DatabaseClient/PurchaseOrders.swift
index 7bce8b1..c18cbb6 100644
--- a/Sources/DatabaseClient/PurchaseOrders.swift
+++ b/Sources/DatabaseClient/PurchaseOrders.swift
@@ -13,7 +13,7 @@ public extension DatabaseClient {
public var get: @Sendable (PurchaseOrder.ID) async throws -> PurchaseOrder?
// var update: @Sendable (PurchaseOrder.ID, PurchaseOrder.Update) async throws -> PurchaseOrder
public var delete: @Sendable (PurchaseOrder.ID) async throws -> Void
- public var search: @Sendable (PurchaseOrder.SearchContext) async throws -> Page
+ public var search: @Sendable (PurchaseOrder.SearchContext, PageRequest) async throws -> Page
}
}
diff --git a/Sources/DatabaseClientLive/PurchaseOrders.swift b/Sources/DatabaseClientLive/PurchaseOrders.swift
index 13da795..32495f1 100644
--- a/Sources/DatabaseClientLive/PurchaseOrders.swift
+++ b/Sources/DatabaseClientLive/PurchaseOrders.swift
@@ -29,31 +29,24 @@ public extension DatabaseClient.PurchaseOrders {
throw NotFoundError()
}
try await model.delete(on: database)
- } search: { search in
+ } search: { search, page in
let query = PurchaseOrderModel.allQuery(on: database)
switch search {
- case let .employee(employee):
- guard let employee = try await EmployeeModel.query(on: database).group(.or, { group in
- group.filter(\.$firstName ~~ employee).filter(\.$lastName ~~ employee)
- }).first()
- else { return Page.empty }
-
- return try await query.filter(\.$createdFor.$id == employee.id!)
- .paginate(.init(page: 1, per: 25))
+ case let .employee(id):
+ return try await query.filter(\.$createdFor.$id == id)
+ .paginate(page)
.map { try $0.toDTO() }
case let .customer(search):
return try await query.filter(\.$customer ~~ search)
- .paginate(.init(page: 1, per: 25))
+ .paginate(page)
.map { try $0.toDTO() }
- case let .vendor(search):
- guard let vendor = try await VendorModel.query(on: database).filter(\.$name ~~ search).first() else {
- return .empty
- }
- // TODO: how to search for this??
- return .init(items: [], metadata: .init(page: 1, per: 1, total: 0))
+ case let .vendor(id):
+ return try await query.filter(\.$vendorBranch.$id == id)
+ .paginate(page)
+ .map { try $0.toDTO() }
}
}
}
diff --git a/Sources/SharedModels/PurchaseOrder.swift b/Sources/SharedModels/PurchaseOrder.swift
index 7782974..bac00aa 100644
--- a/Sources/SharedModels/PurchaseOrder.swift
+++ b/Sources/SharedModels/PurchaseOrder.swift
@@ -117,8 +117,8 @@ public extension PurchaseOrder {
enum SearchContext: Sendable {
case customer(String)
- case vendor(String)
- case employee(String)
+ case vendor(VendorBranch.ID)
+ case employee(Employee.ID)
}
}