From 6eb723a7cf524d4075260eda3b88f6221a7488f7 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 7 Jan 2025 17:07:37 -0500 Subject: [PATCH] feat: Moves employee views to their own controller, updates css, and employee table view. --- Public/css/main.css | 17 ++- Public/images/pencil.svg | 7 ++ Resources/Views/employee-form.leaf | 45 +++++++ Resources/Views/employee-table.leaf | 9 +- Resources/Views/employees.leaf | 16 +-- Resources/Views/user-table.leaf | 12 ++ Resources/Views/users.leaf | 14 +-- .../Controllers/EmployeeViewController.swift | 111 ++++++++++++++++++ Sources/App/Controllers/ViewController.swift | 53 +-------- .../Extensions/RouteBuilder+protected.swift | 10 ++ Sources/App/Models/Employee.swift | 2 + 11 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 Public/images/pencil.svg create mode 100644 Resources/Views/employee-form.leaf create mode 100644 Resources/Views/user-table.leaf create mode 100644 Sources/App/Controllers/EmployeeViewController.swift create mode 100644 Sources/App/Extensions/RouteBuilder+protected.swift diff --git a/Public/css/main.css b/Public/css/main.css index dcdc4fd..eb0eee4 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -118,16 +118,25 @@ input[type=text], input[type=password] { padding: 5px; } -.btn-delete { - display: inline-block; - color: red; - text-align: center; +.btn { cursor: pointer; } +.btn-edit img { + position: fixed; + right: 30px; + width: 30px; + height: 30px; +} + +.btn img:hover { + background-color: #555; +} + .btn-delete img { width: 20px; height: 20px; + margin-top: 5px; } .btn-delete img:hover { diff --git a/Public/images/pencil.svg b/Public/images/pencil.svg new file mode 100644 index 0000000..2d1dc2b --- /dev/null +++ b/Public/images/pencil.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Resources/Views/employee-form.leaf b/Resources/Views/employee-form.leaf new file mode 100644 index 0000000..94e40ae --- /dev/null +++ b/Resources/Views/employee-form.leaf @@ -0,0 +1,45 @@ +
+ + +
+ + +
+ + #if(employee.id): + + #endif +
diff --git a/Resources/Views/employee-table.leaf b/Resources/Views/employee-table.leaf index 9a497ea..a84de0b 100644 --- a/Resources/Views/employee-table.leaf +++ b/Resources/Views/employee-table.leaf @@ -26,8 +26,8 @@ #endif - - + Delete + + Edit + diff --git a/Resources/Views/employees.leaf b/Resources/Views/employees.leaf index 219ac0e..3f7952c 100644 --- a/Resources/Views/employees.leaf +++ b/Resources/Views/employees.leaf @@ -1,22 +1,10 @@ -
+

Employees


Employees are who purchase orders can be generated for.


-
- - -
- - -
- -
+ #extend("employee-form", form) #extend("employee-table")
diff --git a/Resources/Views/user-table.leaf b/Resources/Views/user-table.leaf new file mode 100644 index 0000000..9861469 --- /dev/null +++ b/Resources/Views/user-table.leaf @@ -0,0 +1,12 @@ + + + + + + #for(user in users): + + + + + #endfor +
UsernameEmail
#(user.username)#(user.email)
diff --git a/Resources/Views/users.leaf b/Resources/Views/users.leaf index 6ed475f..d11ba43 100644 --- a/Resources/Views/users.leaf +++ b/Resources/Views/users.leaf @@ -1,19 +1,9 @@

Users

+

Users are people that can login and generate puchase orders for employees.


- - - - - - #for(user in users): - - - - - #endfor -
UsernameEmail
#(user.username)#(user.email)
+ #extend("user-table")
diff --git a/Sources/App/Controllers/EmployeeViewController.swift b/Sources/App/Controllers/EmployeeViewController.swift new file mode 100644 index 0000000..345d564 --- /dev/null +++ b/Sources/App/Controllers/EmployeeViewController.swift @@ -0,0 +1,111 @@ +import Fluent +import Leaf +import Vapor + +struct EmployeeViewController: RouteCollection { + + private let api = ApiController() + + func boot(routes: any RoutesBuilder) throws { + let employees = routes.protected.grouped("employees") + + // MARK: Protected routes. + + employees.get(use: employees(req:)) + employees.get("form", use: employeeForm(req:)) + employees.post(use: postEmployeeForm(req:)) + employees.group(":employeeID") { + $0.get(use: editEmployee(req:)) + $0.delete(use: deleteEmployee(req:)) + $0.put(use: updateEmployee(req:)) + $0.post("toggle-active", use: toggleActiveEmployee(req:)) + } + } + + @Sendable + func employees(req: Request) async throws -> View { + return try await req.view.render("employees", EmployeesCTX(api: api, req: req)) + } + + @Sendable + func postEmployeeForm(req: Request) async throws -> View { + _ = try await api.createEmployee(req: req) + let employees = try await api.getSortedEmployees(req: req) + return try await req.view.render("employee-table", ["employees": employees]) + } + + @Sendable + func toggleActiveEmployee(req: Request) async throws -> View { + guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { + throw Abort(.notFound) + } + employee.active.toggle() + try await employee.save(on: req.db) + let employees = try await api.getSortedEmployees(req: req) + return try await req.view.render("employee-table", ["employees": employees]) + } + + @Sendable + func deleteEmployee(req: Request) async throws -> View { + _ = try await api.deleteEmployee(req: req) + let employees = try await api.getSortedEmployees(req: req) + return try await req.view.render("employee-table", ["employees": employees]) + } + + @Sendable + func editEmployee(req: Request) async throws -> View { + guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { + throw Abort(.notFound) + } + return try await req.view.render("employee-form", EmployeeFormCTX(employee: employee.toDTO())) + } + + @Sendable + func updateEmployee(req: Request) async throws -> View { + _ = try await api.updateEmployee(req: req) + return try await req.view.render("employees", EmployeesCTX(oob: true, api: api, req: req)) + } + + @Sendable + func employeeForm(req: Request) async throws -> View { + try await req.view.render("employee-form", EmployeeFormCTX()) + } + +} + +private struct EmployeesCTX: Content { + let oob: Bool + let employees: [Employee.DTO] + let form: EmployeeFormCTX + + init( + oob: Bool = false, + employee: Employee? = nil, + api: ApiController, + req: Request + ) async throws { + self.oob = oob + self.employees = try await api.getSortedEmployees(req: req) + self.form = .init(employee: employee.map { $0.toDTO() }) + } +} + +private struct EmployeeFormCTX: Content { + let employee: Employee.DTO? + let oob: Bool + + init(employee: Employee.DTO? = nil) { + self.employee = employee + self.oob = employee != nil + } +} + +private extension ApiController { + + func getSortedEmployees(req: Request) async throws -> [Employee.DTO] { + var employees = try await employeesIndex(req: req) + employees.sort { ($0.active ?? false) && !($1.active ?? false) } + employees.sort { ($0.lastName ?? "") < ($1.lastName ?? "") } + return employees + } +} diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index 41bfac4..eea11db 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -7,9 +7,8 @@ struct ViewController: RouteCollection { private let api = ApiController() func boot(routes: any RoutesBuilder) throws { - let protected = routes.grouped(User.credentialsAuthenticator(), User.redirectMiddleware(path: "login")) + let protected = routes.protected let login = routes.grouped("login") - let employees = protected.grouped("employees") // MARK: - Non-protected routes. @@ -23,12 +22,7 @@ struct ViewController: RouteCollection { protected.get("home", use: home(req:)) protected.get("users", use: users(req:)) - employees.get(use: employees(req:)) - employees.post(use: postEmployeeForm(req:)) - employees.group(":employeeID") { - $0.delete(use: deleteEmployee(req:)) - $0.post("toggle-active", use: toggleActiveEmployee(req:)) - } + try routes.register(collection: EmployeeViewController()) } @Sendable @@ -66,6 +60,8 @@ struct ViewController: RouteCollection { return try await req.view.render("login") } + // TODO: Add route parameters for active route / tab. + @Sendable func home(req: Request) async throws -> View { try await req.view.render("home") @@ -77,50 +73,9 @@ struct ViewController: RouteCollection { return try await req.view.render("users", ["users": users]) } - @Sendable - func employees(req: Request) async throws -> View { - let employees = try await api.getSortedEmployees(req: req) - return try await req.view.render("employees", ["employees": employees]) - } - - @Sendable - func postEmployeeForm(req: Request) async throws -> View { - _ = try await api.createEmployee(req: req) - let employees = try await api.getSortedEmployees(req: req) - return try await req.view.render("employee-table", ["employees": employees]) - } - - @Sendable - func toggleActiveEmployee(req: Request) async throws -> View { - guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { - throw Abort(.notFound) - } - employee.active.toggle() - try await employee.save(on: req.db) - let employees = try await api.getSortedEmployees(req: req) - return try await req.view.render("employee-table", ["employees": employees]) - } - - @Sendable - func deleteEmployee(req: Request) async throws -> View { - _ = try await api.deleteEmployee(req: req) - let employees = try await api.getSortedEmployees(req: req) - return try await req.view.render("employee-table", ["employees": employees]) - } - } private struct UserForm: Content { let username: String let password: String } - -private extension ApiController { - - func getSortedEmployees(req: Request) async throws -> [Employee.DTO] { - var employees = try await employeesIndex(req: req) - employees.sort { ($0.active ?? false) && !($1.active ?? false) } - employees.sort { ($0.lastName ?? "") < ($1.lastName ?? "") } - return employees - } -} diff --git a/Sources/App/Extensions/RouteBuilder+protected.swift b/Sources/App/Extensions/RouteBuilder+protected.swift new file mode 100644 index 0000000..4ed49c0 --- /dev/null +++ b/Sources/App/Extensions/RouteBuilder+protected.swift @@ -0,0 +1,10 @@ +import Vapor + +extension RoutesBuilder { + + // Used to ensure views are protected, redirects users to the login page if they're + // not authenticated. + var protected: any RoutesBuilder { + grouped(User.credentialsAuthenticator(), User.redirectMiddleware(path: "login")) + } +} diff --git a/Sources/App/Models/Employee.swift b/Sources/App/Models/Employee.swift index b1065e6..fee9e2e 100644 --- a/Sources/App/Models/Employee.swift +++ b/Sources/App/Models/Employee.swift @@ -2,6 +2,8 @@ import Fluent import struct Foundation.UUID import Vapor +// TODO: Add soft-delete?? + /// The employee database model. /// /// An employee is someone that PO's can be generated for. They can be either a field