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 @@
+
+
+
\ 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 @@
+
+
+
\ 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 @@
+
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 @@
-
-
HHE - Purchase Orders
+ #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 @@
+
HHE - Purchase Orders
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.
+
+
+
+
+ | Username |
+ Email |
+
+ #for(user in users):
+
+ | #(user.username) |
+ #(user.email) |
+
+ #endfor
+
+
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())
}