diff --git a/Sources/App/Controllers/View/EmployeeViewController.swift b/Sources/App/Controllers/View/EmployeeViewController.swift index ff37628..dbe52cb 100644 --- a/Sources/App/Controllers/View/EmployeeViewController.swift +++ b/Sources/App/Controllers/View/EmployeeViewController.swift @@ -1,158 +1,65 @@ -// import Dependencies -// import Fluent -// import Leaf -// import Vapor -// -// struct EmployeeViewController: RouteCollection { -// -// @Dependency(\.employees) var employees -// -// func boot(routes: any RoutesBuilder) throws { -// let protected = routes.protected.grouped("employees") -// protected.get(use: index(req:)) -// protected.get("form", use: employeeForm(req:)) -// protected.post(use: create(req:)) -// protected.group(":employeeID") { -// $0.get(use: get(req:)) -// $0.get("edit", use: edit(req:)) -// $0.delete(use: delete(req:)) -// $0.put(use: update(req:)) -// $0.patch("toggle-active", use: toggleActive(req:)) -// } -// } -// -// @Sendable -// func index(req: Request) async throws -> View { -// return try await renderIndex(req) -// } -// -// @Sendable -// private func renderIndex( -// _ req: Request, -// _ employee: Employee.DTO? = nil, -// _ form: EmployeeFormCTX? = nil -// ) async throws -> View { -// return try await req.view.render( -// "employees/index", -// EmployeesCTX(employee: employee, employees: employees.fetchAll(), form: form ?? .init()) -// ) -// } -// -// @Sendable -// func create(req: Request) async throws -> View { -// try Employee.Create.validate(content: req) -// let employee = try await employees.create(req.content.decode(Employee.Create.self)) -// return try await req.view.render("employees/table-row", employee) -// } -// -// @Sendable -// func get(req: Request) async throws -> View { -// let employee = try await employees.get(req.ensureIDPathComponent(key: "employeeID")) -// // Check if we've rendered the page yet. -// guard req.isHtmxRequest else { -// return try await renderIndex(req, employee) -// } -// return try await req.view.render("employees/detail", ["employee": employee]) -// } -// -// @Sendable -// func toggleActive(req: Request) async throws -> View { -// guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self) else { -// throw Abort(.badRequest, reason: "Employee id not supplied.") -// } -// let employee = try await employees.toggleActive(id) -// return try await req.view.render("employees/table-row", employee) -// } -// -// // TODO: I think we can just return a response and remove the table-row, here. -// @Sendable -// func delete(req: Request) async throws -> View { -// let id = try req.requireEmployeeID() -// _ = try await employees.delete(id) -// let employees = try await employees.fetchAll() -// return try await req.view.render("employees/table", ["employees": employees]) -// } -// -// @Sendable -// func edit(req: Request) async throws -> View { -// guard let employee = try await employees.get(req.parameters.get("employeeID")) else { -// throw Abort(.notFound) -// } -// return try await req.view.render("employees/detail", EmployeeDetailCTX(editing: true, employee: employee)) -// } -// -// @Sendable -// func update(req: Request) async throws -> View { -// let id = try req.requireEmployeeID() -// try Employee.Update.validate(content: req) -// let updates = try req.content.decode(Employee.Update.self) -// req.logger.info("Employee updates: \(updates)") -// let employee = try await employees.update(id, updates) -// req.logger.info("Done updating employee: \(employee)") -// return try await req.view.render("employees/table-row", employee) -// } -// -// @Sendable -// func employeeForm(req: Request) async throws -> View { -// try await req.view.render("employees/form", EmployeeFormCTX()) -// } -// -// } -// -// private extension Request { -// func requireEmployeeID() throws -> Employee.IDValue { -// guard let id = parameters.get("employeeID", as: Employee.IDValue.self) else { -// throw Abort(.badRequest, reason: "Employee id not supplied") -// } -// return id -// } -// } -// -// private struct EmployeeDetailCTX: Content { -// let editing: Bool -// let employee: Employee.DTO? -// -// init(editing: Bool = false, employee: Employee.DTO? = nil) { -// self.editing = editing -// self.employee = employee -// } -// } -// -// private struct EmployeesCTX: Content { -// let employee: Employee.DTO? -// let employees: [Employee.DTO] -// let form: EmployeeFormCTX -// -// init( -// employee: Employee.DTO? = nil, -// employees: [Employee.DTO], -// form: EmployeeFormCTX? = nil -// ) { -// self.employee = employee -// self.employees = employees -// self.form = form ?? .init() -// } -// } -// -// private struct EmployeeFormCTX: Content { -// -// let htmxForm: HtmxFormCTX -// -// init(employee: Employee.DTO? = nil) { -// self.htmxForm = .init( -// formClass: "employee-form", -// formId: "employee-form", -// htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"), -// htmxTarget: "#employee-table", -// htmxPushUrl: false, -// htmxResetAfterRequest: true, -// htmxSwapOob: nil, -// htmxSwap: employee == nil ? .outerHTML : nil, -// context: .init(employee: employee) -// ) -// } -// -// struct Context: Content { -// let employee: Employee.DTO? -// } -// } +import DatabaseClient +import Dependencies +import Elementary +import SharedModels +import Vapor +import VaporElementary + +struct EmployeeViewController: RouteCollection { + + @Dependency(\.database.employees) var employees + + func boot(routes: any RoutesBuilder) throws { + let route = routes.protected.grouped("employees") + route.get(use: index) + route.get("create", use: form) + route.post(use: create) + route.group(":id") { + $0.get(use: get) + $0.put(use: update) + } + } + + @Sendable + func index(req: Request) async throws -> HTMLResponse { + try await req.render { try await mainPage(EmployeeForm()) } + } + + @Sendable + func form(req: Request) async throws -> HTMLResponse { + await req.render { EmployeeForm(shouldShow: true) } + } + + @Sendable + func create(req: Request) async throws -> HTMLResponse { + let employee = try await employees.create(req.content.decode(Employee.Create.self)) + return await req.render { EmployeeTable.Row(employee: employee) } + } + + @Sendable + func get(req: Request) async throws -> HTMLResponse { + guard let employee = try await employees.get(req.ensureIDPathComponent()) else { + throw Abort(.badRequest, reason: "Employee not found.") + } + guard req.isHtmxRequest else { + return try await req.render { try await mainPage(EmployeeForm(employee: employee)) } + } + return await req.render { EmployeeForm(employee: employee) } + } + + @Sendable + func update(req: Request) async throws -> HTMLResponse { + let employee = try await employees.update(req.ensureIDPathComponent(), req.content.decode(Employee.Update.self)) + return await req.render { EmployeeTable.Row(employee: employee) } + } + + private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + let employees = try await self.employees.fetchAll() + return MainPage(displayNav: true, route: .employees) { + div(.class("container")) { + html + EmployeeTable(employees: employees) + } + } + } +} diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift new file mode 100644 index 0000000..525a22f --- /dev/null +++ b/Sources/App/Views/Employees/EmployeeForm.swift @@ -0,0 +1,63 @@ +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: "/employees") { + form( + employee == nil ? .hx.post(targetURL) : .hx.put(targetURL), + employee == nil ? .hx.target("#employee-table") : .hx.target("#employee_\(employee!.id)"), + employee == nil + ? .hx.swap(.beforeEnd.transition(true).swap("0.5s")) + : .hx.swap(.outerHTML.transition(true).swap("0.5s")), + .custom( + name: "hx-on::after-request", + value: "if (event.detail.successful) toggleContent('float'); window.location.href='/employees';" + ) + ) { + 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 + } + } + } + } + } + + private var buttonLabel: String { + guard employee != nil else { return "Create" } + return "Update" + } + + private var targetURL: String { + guard let employee else { return "/employees" } + return "/employees/\(employee.id)" + } +} diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift new file mode 100644 index 0000000..9db1e46 --- /dev/null +++ b/Sources/App/Views/Employees/EmployeeTable.swift @@ -0,0 +1,51 @@ +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("/employees/create"), + .hx.target("#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_\(employee.id)")) { + td { "\(employee.firstName.capitalized) \(employee.lastName.capitalized)" } + td { + Button.detail() + .attributes( + .style("padding-left: 15px;"), + .hx.get("/employees/\(employee.id)"), + .hx.target("#float"), + .hx.pushURL(true), + .hx.swap(.outerHTML.transition(true).swap("0.5s")) + ) + } + } + } + } +} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 1a9f61f..9474696 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -9,6 +9,7 @@ func routes(_ app: Application) throws { try app.register(collection: ApiController()) try app.register(collection: UserViewController()) try app.register(collection: VendorViewController()) + try app.register(collection: EmployeeViewController()) // try app.register(collection: ViewController()) app.get { _ in