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? } }