From 08a0a8e1a35d66dc0918a4f19027605718e77b40 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 7 Jan 2025 14:05:40 -0500 Subject: [PATCH] feat: Adds employee form and table view, begins user form and table view. --- Public/css/main.css | 74 +++++++++++ Public/images/toggle-off.svg | 7 ++ Public/images/toggle-on.svg | 7 ++ Public/images/trash-can.svg | 20 +++ Resources/Views/employee-table.leaf | 42 +++++++ Resources/Views/employees.leaf | 22 ++++ Resources/Views/home.leaf | 35 ++++++ Resources/Views/logged-in.leaf | 17 --- Resources/Views/login.leaf | 2 +- Resources/Views/logo.leaf | 1 + Resources/Views/navbar.leaf | 5 + Resources/Views/users.leaf | 19 +++ Sources/App/Controllers/ApiController.swift | 6 + Sources/App/Controllers/ViewController.swift | 126 +++++++++++++++++++ Sources/App/routes.swift | 76 +---------- 15 files changed, 366 insertions(+), 93 deletions(-) create mode 100644 Public/images/toggle-off.svg create mode 100644 Public/images/toggle-on.svg create mode 100644 Public/images/trash-can.svg create mode 100644 Resources/Views/employee-table.leaf create mode 100644 Resources/Views/employees.leaf create mode 100644 Resources/Views/home.leaf delete mode 100644 Resources/Views/logged-in.leaf create mode 100644 Resources/Views/logo.leaf create mode 100644 Resources/Views/navbar.leaf create mode 100644 Resources/Views/users.leaf create mode 100644 Sources/App/Controllers/ViewController.swift diff --git a/Public/css/main.css b/Public/css/main.css index 144b7d0..dcdc4fd 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -58,6 +58,19 @@ nav { padding: 50px 0; } +form { + padding: 50px 0; + text-align: center; +} + +form label { + padding: 15px; +} + +form input { + margin-bottom: 15px; +} + .login-form { padding: 50px 0; text-align: center; @@ -70,3 +83,64 @@ nav { .login-form input { margin-bottom: 15px; } + +table { + width: 100%; +} + +table, th, td { + border: 1px solid grey; + border-collapse: collapse; +} + +td, th { + padding: 5px 15px; +} + +.employee-form { + margin: 20px auto; + width: 50%; + background-color: #aeb6bf; + padding: 20px; + border-radius: 25px; +} + +.employee-form label { + color: #555; + font-weight: bold; +} + +input[type=submit] { + padding: 5px 20px; +} + +input[type=text], input[type=password] { + padding: 5px; +} + +.btn-delete { + display: inline-block; + color: red; + text-align: center; + cursor: pointer; +} + +.btn-delete img { + width: 20px; + height: 20px; +} + +.btn-delete img:hover { + background-color: #555; +} + +.toggle img { + background-color: inherit; + width: 60px; + height: 30px; + cursor: pointer; +} + +.toggle img:hover { + background-color: #555; +} diff --git a/Public/images/toggle-off.svg b/Public/images/toggle-off.svg new file mode 100644 index 0000000..b8aadce --- /dev/null +++ b/Public/images/toggle-off.svg @@ -0,0 +1,7 @@ + + + + + + toggle-on Created with Sketch Beta. + \ No newline at end of file diff --git a/Public/images/toggle-on.svg b/Public/images/toggle-on.svg new file mode 100644 index 0000000..c42bd8f --- /dev/null +++ b/Public/images/toggle-on.svg @@ -0,0 +1,7 @@ + + + + + + toggle-off Created with Sketch Beta. + \ No newline at end of file diff --git a/Public/images/trash-can.svg b/Public/images/trash-can.svg new file mode 100644 index 0000000..174bf65 --- /dev/null +++ b/Public/images/trash-can.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/Resources/Views/employee-table.leaf b/Resources/Views/employee-table.leaf new file mode 100644 index 0000000..9a497ea --- /dev/null +++ b/Resources/Views/employee-table.leaf @@ -0,0 +1,42 @@ + + + + + + + #for(employee in employees): + + + + + + + #endfor +
NameActive
#capitalized(employee.firstName) #capitalized(employee.lastName) + #if(employee.active): + + Active + + #else: + + Active + + #endif + + + Delete + +
diff --git a/Resources/Views/employees.leaf b/Resources/Views/employees.leaf new file mode 100644 index 0000000..219ac0e --- /dev/null +++ b/Resources/Views/employees.leaf @@ -0,0 +1,22 @@ +
+
+

Employees

+
+

Employees are who purchase orders can be generated for.

+
+
+
+ + +
+ + +
+ +
+ #extend("employee-table") +
diff --git a/Resources/Views/home.leaf b/Resources/Views/home.leaf new file mode 100644 index 0000000..d3fb644 --- /dev/null +++ b/Resources/Views/home.leaf @@ -0,0 +1,35 @@ +
+
+
+ #extend("logo") + #extend("navbar") +
+
+
+
+ +
+
+

We're in!

+
+
+
diff --git a/Resources/Views/logged-in.leaf b/Resources/Views/logged-in.leaf deleted file mode 100644 index 92a3e94..0000000 --- a/Resources/Views/logged-in.leaf +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
- - -
-
-
-
-

We're in!

-
-
-
diff --git a/Resources/Views/login.leaf b/Resources/Views/login.leaf index 40103af..6a528f8 100644 --- a/Resources/Views/login.leaf +++ b/Resources/Views/login.leaf @@ -1,7 +1,7 @@
- + #extend("logo")
diff --git a/Resources/Views/logo.leaf b/Resources/Views/logo.leaf new file mode 100644 index 0000000..ed9262e --- /dev/null +++ b/Resources/Views/logo.leaf @@ -0,0 +1 @@ + diff --git a/Resources/Views/navbar.leaf b/Resources/Views/navbar.leaf new file mode 100644 index 0000000..784727c --- /dev/null +++ b/Resources/Views/navbar.leaf @@ -0,0 +1,5 @@ + diff --git a/Resources/Views/users.leaf b/Resources/Views/users.leaf new file mode 100644 index 0000000..6ed475f --- /dev/null +++ b/Resources/Views/users.leaf @@ -0,0 +1,19 @@ +
+
+

Users

+

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

+
+
+ + + + + + #for(user in users): + + + + + #endfor +
UsernameEmail
#(user.username)#(user.email)
+
diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index 1caf7b3..f4fd13b 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -30,6 +30,7 @@ struct ApiController: RouteCollection { purchaseOrders.get(use: purchaseOrdersIndex(req:)) purchaseOrders.post(use: createPurchaseOrder(req:)) + users.get(use: usersIndex(req:)) users.post(use: createUser(req:)) users.group("login") { $0.get(use: self.login(req:)) @@ -168,6 +169,11 @@ struct ApiController: RouteCollection { return token } + @Sendable + func usersIndex(req: Request) async throws -> [User.DTO] { + try await User.query(on: req.db).all().map { $0.toDTO() } + } + // MARK: - Vendors @Sendable diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift new file mode 100644 index 0000000..41bfac4 --- /dev/null +++ b/Sources/App/Controllers/ViewController.swift @@ -0,0 +1,126 @@ +import Fluent +import Leaf +import Vapor + +struct ViewController: RouteCollection { + + private let api = ApiController() + + func boot(routes: any RoutesBuilder) throws { + let protected = routes.grouped(User.credentialsAuthenticator(), User.redirectMiddleware(path: "login")) + let login = routes.grouped("login") + let employees = protected.grouped("employees") + + // MARK: - Non-protected routes. + + routes.get(use: index(req:)) + login.get(use: getLogin(req:)) + login.post(use: postLogin(req:)) + routes.post("logout", use: logout(req:)) + + // MARK: Protected routes. + + 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:)) + } + } + + @Sendable + func index(req: Request) async throws -> View { + try await req.view.render("index") + } + + @Sendable + func getLogin(req: Request) async throws -> View { + try await req.view.render("login") + } + + @Sendable + func postLogin(req: Request) async throws -> View { + let content = try req.content.decode(UserForm.self) + guard let user = try await User.query(on: req.db) + .filter(\.$username == content.username) + .first() + else { + throw Abort(.badRequest, reason: "User not found.") + } + + guard try user.verify(password: content.password) else { + throw Abort(.unauthorized, reason: "Invalid password.") + } + req.auth.login(user) + + req.logger.debug("User logged in: \(user.toDTO())") + return try await req.view.render("home") + } + + @Sendable + func logout(req: Request) async throws -> View { + req.auth.logout(User.self) + return try await req.view.render("login") + } + + @Sendable + func home(req: Request) async throws -> View { + try await req.view.render("home") + } + + @Sendable + func users(req: Request) async throws -> View { + let users = try await api.usersIndex(req: req) + 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/routes.swift b/Sources/App/routes.swift index 48146e1..4d2e87f 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -2,80 +2,6 @@ import Fluent import Vapor func routes(_ app: Application) throws { - let redirectMiddleware = User.redirectMiddleware(path: "login") - // let protected = app.grouped(redirectMiddleware) - let credentialsProtected = app.grouped(User.credentialsAuthenticator(), redirectMiddleware) - - app.get { req async throws in - try await req.view.render( - "index", - Index(showNavLinks: false) - ) - } - - app.get("login") { req async throws -> View in - req.logger.info("login") - return try await req.view.render("login") - } - - app.post("logout") { req async throws -> View in - req.auth.logout(User.self) - return try await req.view.render("login") - } - - app.post("login") { req async throws -> View in - let content = try req.content.decode(UserForm.self) - guard let user = try await User.query(on: req.db) - .filter(\.$username == content.username) - .first() - else { - throw Abort(.badRequest, reason: "User not found.") - } - - guard try user.verify(password: content.password) else { - throw Abort(.unauthorized, reason: "Invalid password.") - } - req.auth.login(user) - - req.logger.debug("User: \(user.toDTO())") - return try await req.view.render("logged-in") - } - - credentialsProtected.get("home") { req async throws in - req.logger.info("home") - return try await req.view.render("logged-in") - } - - // TODO: Remove. - credentialsProtected.get("logged-in") { _ in - "Hello, logged-in!" - } - - // app.get("index") { req async throws -> View in - // - // } - - app.get("hello") { _ async -> String in - "Hello, world!" - } - try app.register(collection: ApiController()) -} - -struct Index: Content { - let title: String - let showNavLinks: Bool - - init( - title: String = "HHE - Purchase Orders", - showNavLinks: Bool - ) { - self.title = title - self.showNavLinks = showNavLinks - } -} - -struct UserForm: Content { - let username: String - let password: String + try app.register(collection: ViewController()) }