diff --git a/Package.resolved b/Package.resolved
index 1e46b16..9840710 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "5e46aec5c52d22d116c033b012d49986e5f900c102b88ac80e054d2e6e64d458",
+ "originHash" : "b5426b686e9548f4fc69224d4e0f13d10ce55816d5b71622f5f446b12d66140a",
"pins" : [
{
"identity" : "async-http-client",
@@ -37,6 +37,24 @@
"version" : "4.15.1"
}
},
+ {
+ "identity" : "elementary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/sliemeobn/elementary.git",
+ "state" : {
+ "revision" : "9830c5e1b6740d367b28d416797ff4e6b007e541",
+ "version" : "0.4.4"
+ }
+ },
+ {
+ "identity" : "elementary-htmx",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/sliemeobn/elementary-htmx.git",
+ "state" : {
+ "revision" : "6e8430d24a6dc2de9d16270a6af985eb37a190ad",
+ "version" : "0.4.0"
+ }
+ },
{
"identity" : "fluent",
"kind" : "remoteSourceControl",
@@ -325,6 +343,15 @@
"version" : "4.111.0"
}
},
+ {
+ "identity" : "vapor-elementary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor-community/vapor-elementary.git",
+ "state" : {
+ "revision" : "a14bc5d995f06fa2c398fa7a6bb6f98f1fd54446",
+ "version" : "0.2.1"
+ }
+ },
{
"identity" : "websocket-kit",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index 75abb46..b8b5cb4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -23,7 +23,10 @@ let package = Package(
.package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
- .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3")
+ .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"),
+ .package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"),
+ .package(url: "https://github.com/sliemeobn/elementary-htmx.git", from: "0.4.0"),
+ .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0")
],
targets: [
.executableTarget(
@@ -37,7 +40,10 @@ let package = Package(
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "Dependencies", package: "swift-dependencies"),
- .product(name: "DependenciesMacros", package: "swift-dependencies")
+ .product(name: "DependenciesMacros", package: "swift-dependencies"),
+ .product(name: "Elementary", package: "elementary"),
+ .product(name: "VaporElementary", package: "vapor-elementary"),
+ .product(name: "ElementaryHTMX", package: "elementary-htmx")
],
swiftSettings: swiftSettings
diff --git a/Public/images/menu.svg b/Public/images/menu.svg
new file mode 100644
index 0000000..3ab00e0
--- /dev/null
+++ b/Public/images/menu.svg
@@ -0,0 +1 @@
+
diff --git a/Sources/App/DB/EmployeeDB.swift b/Sources/App/DB/EmployeeDB.swift
deleted file mode 100644
index a44a7d5..0000000
--- a/Sources/App/DB/EmployeeDB.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-// import Dependencies
-// import DependenciesMacros
-// import Fluent
-// import Vapor
-//
-// extension DependencyValues {
-// // An intermediate layer between our api and view controllers that interacts with the
-// // database model.
-// var employees: EmployeeDB {
-// get { self[EmployeeDB.self] }
-// set { self[EmployeeDB.self] = newValue }
-// }
-// }
-//
-// @DependencyClient
-// struct EmployeeDB: Sendable {
-// var create: @Sendable (Employee.Create) async throws -> Employee.DTO
-// var fetchAll: @Sendable (FetchRequest) async throws -> [Employee.DTO]
-// var get: @Sendable (Employee.IDValue) async throws -> Employee.DTO?
-// var update: @Sendable (Employee.IDValue, Employee.Update) async throws -> Employee.DTO
-// var delete: @Sendable (Employee.IDValue) async throws -> Void
-// var toggleActive: @Sendable (Employee.IDValue) async throws -> Employee.DTO
-//
-// enum FetchRequest {
-// case active
-// case `default`
-// }
-//
-// func fetchAll() async throws -> [Employee.DTO] {
-// try await fetchAll(.default)
-// }
-//
-// func get(_ id: String?) async throws -> Employee.DTO? {
-// guard let idString = id, let id = UUID(uuidString: idString) else {
-// throw Abort(.badRequest, reason: "Employee id not valid.")
-// }
-// return try await get(id)
-// }
-// }
-//
-// extension EmployeeDB: TestDependencyKey {
-// static let testValue: EmployeeDB = Self()
-//
-// static func live(database: any Database) -> Self {
-// .init(
-// create: { model in
-// let model = model.toModel()
-// try await model.save(on: database)
-// return model.toDTO()
-// },
-// fetchAll: { request in
-// var query = Employee.query(on: database)
-// .sort(\.$lastName)
-//
-// if request == .active {
-// query = query.filter(\.$active == true)
-// }
-//
-// return try await query.all().map { $0.toDTO() }
-// },
-// get: { id in
-// try await Employee.find(id, on: database).map { $0.toDTO() }
-// },
-// update: { id, updates in
-// guard let employee = try await Employee.find(id, on: database) else {
-// throw Abort(.badRequest, reason: "Employee id not found.")
-// }
-// employee.applyUpdates(updates)
-// try await employee.save(on: database)
-// return employee.toDTO()
-// },
-// delete: { id in
-// guard let employee = try await Employee.find(id, on: database) else {
-// throw Abort(.badRequest, reason: "Employee id not found.")
-// }
-// try await employee.delete(on: database)
-// },
-// toggleActive: { id in
-// guard let employee = try await Employee.find(id, on: database) else {
-// throw Abort(.notFound)
-// }
-// employee.active.toggle()
-// try await employee.save(on: database)
-// return employee.toDTO()
-// }
-// )
-// }
-// }
diff --git a/Sources/App/DB/PurchaseOrderDB.swift b/Sources/App/DB/PurchaseOrderDB.swift
deleted file mode 100644
index 07d0d80..0000000
--- a/Sources/App/DB/PurchaseOrderDB.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-// import Dependencies
-// import DependenciesMacros
-// import Fluent
-// import Vapor
-//
-// extension DependencyValues {
-// // An intermediate between our api and view controllers that interacts with the
-// // database.
-// var purchaseOrders: PurchaseOrdersDB {
-// get { self[PurchaseOrdersDB.self] }
-// set { self[PurchaseOrdersDB.self] = newValue }
-// }
-// }
-//
-// @DependencyClient
-// struct PurchaseOrdersDB: Sendable {
-// var create: @Sendable (PurchaseOrder.Create, User.IDValue) async throws -> PurchaseOrder.DTO
-// var fetchAll: @Sendable () async throws -> [PurchaseOrder.DTO]
-// var fetchPage: @Sendable (PageRequest) async throws -> Page
-// var get: @Sendable (PurchaseOrder.IDValue) async throws -> PurchaseOrder.DTO?
-// // var update: @Sendable (PurchaseOrder.IDValue, PurchaseOrder.Update) async throws -> PurchaseOrder.DTO
-// var delete: @Sendable (PurchaseOrder.IDValue) async throws -> Void
-// }
-//
-// extension PurchaseOrdersDB: TestDependencyKey {
-// static let testValue: PurchaseOrdersDB = Self()
-//
-// static func live(database db: any Database) -> Self {
-// .init(
-// create: { model, createdById in
-// guard let employee = try await Employee.find(model.createdForID, on: db) else {
-// throw Abort(.notFound, reason: "Employee not found.")
-// }
-//
-// guard employee.active else {
-// throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees")
-// }
-//
-// let purchaseOrder = model.toModel(createdByID: createdById)
-// try await purchaseOrder.save(on: db)
-// guard let loaded = try await PurchaseOrder.get(purchaseOrder.requireID(), on: db) else {
-// return purchaseOrder.toDTO()
-// }
-// return loaded
-//
-// },
-// fetchAll: {
-// try await PurchaseOrder.allQuery(on: db)
-// .sort(\.$id, .descending)
-// .all().map { $0.toDTO() }
-// },
-// fetchPage: { request in
-// try await PurchaseOrder.allQuery(on: db)
-// .sort(\.$id, .descending)
-// .paginate(request)
-// .map { $0.toDTO() }
-// },
-// get: { id in
-// try await PurchaseOrder.get(id, on: db)
-// },
-// delete: { id in
-// guard let purchaseOrder = try await PurchaseOrder.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// try await purchaseOrder.delete(on: db)
-// }
-// )
-// }
-//
-// }
-//
-// private extension PurchaseOrder {
-// static func allQuery(on db: any Database) -> QueryBuilder {
-// PurchaseOrder.query(on: db)
-// .with(\.$createdBy)
-// .with(\.$createdFor)
-// .with(\.$vendorBranch) { branch in
-// branch.with(\.$vendor)
-// }
-// }
-//
-// static func get(_ id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? {
-// try await PurchaseOrder.allQuery(on: db)
-// .filter(\.$id == id)
-// .first()?.toDTO()
-// }
-// }
diff --git a/Sources/App/DB/UserDB.swift b/Sources/App/DB/UserDB.swift
deleted file mode 100644
index 114e09d..0000000
--- a/Sources/App/DB/UserDB.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-// import Dependencies
-// import DependenciesMacros
-// import Fluent
-// import Vapor
-//
-// extension DependencyValues {
-// var users: UserDB {
-// get { self[UserDB.self] }
-// set { self[UserDB.self] = newValue }
-// }
-// }
-//
-// @DependencyClient
-// struct UserDB: Sendable {
-// var create: @Sendable (User.Create) async throws -> User.DTO
-// var delete: @Sendable (User.IDValue) async throws -> Void
-// var fetchAll: @Sendable () async throws -> [User.DTO]
-// var get: @Sendable (User.IDValue) async throws -> User.DTO?
-// var login: @Sendable (User) async throws -> UserToken
-// }
-//
-// extension UserDB: TestDependencyKey {
-// static let testValue: UserDB = Self()
-//
-// static func live(database db: any Database) -> Self {
-// self.init(
-// create: { model in
-// guard model.password == model.confirmPassword else {
-// throw Abort(.badRequest, reason: "Passwords did not match.")
-// }
-// let user = try User(
-// username: model.username,
-// email: model.email,
-// passwordHash: Bcrypt.hash(model.password)
-// )
-// try await user.save(on: db)
-// return user.toDTO()
-//
-// },
-// delete: { id in
-// guard let user = try await User.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// try await user.delete(on: db)
-//
-// },
-// fetchAll: {
-// try await User.query(on: db).all().map { $0.toDTO() }
-// },
-// get: { id in
-// try await User.find(id, on: db).map { $0.toDTO() }
-// },
-// login: { user in
-// let token = try user.generateToken()
-// try await token.save(on: db)
-// return token
-// }
-// )
-// }
-// }
diff --git a/Sources/App/DB/VendorBranchDB.swift b/Sources/App/DB/VendorBranchDB.swift
deleted file mode 100644
index 96b553e..0000000
--- a/Sources/App/DB/VendorBranchDB.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-// import Dependencies
-// import DependenciesMacros
-// import Fluent
-// import Vapor
-//
-// public extension DependencyValues {
-// var vendorBranches: VendorBranchDB {
-// get { self[VendorBranchDB.self] }
-// set { self[VendorBranchDB.self] = newValue }
-// }
-// }
-//
-// @DependencyClient
-// public struct VendorBranchDB: Sendable {
-// var create: @Sendable (VendorBranch.Create, Vendor.IDValue) async throws -> VendorBranch.DTO
-// var delete: @Sendable (VendorBranch.IDValue) async throws -> Void
-// var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch.DTO]
-// var get: @Sendable (VendorBranch.IDValue) async throws -> VendorBranch.DTO?
-// var update: @Sendable (VendorBranch.IDValue, VendorBranch.Update) async throws -> VendorBranch.DTO
-//
-// enum FetchRequest: Equatable {
-// case `default`
-// case `for`(vendorID: Vendor.IDValue)
-// case withVendor
-// }
-//
-// func fetchAll() async throws -> [VendorBranch.DTO] {
-// try await fetchAll(.default)
-// }
-// }
-//
-// extension VendorBranchDB: TestDependencyKey {
-// public static let testValue: VendorBranchDB = Self()
-//
-// static func live(database db: any Database) -> Self {
-// .init(
-// create: { model, vendorID in
-// let branch = model.toModel()
-// guard let vendor = try await Vendor.find(vendorID, on: db) else {
-// throw Abort(.badRequest, reason: "Vendor does not exist.")
-// }
-// try await vendor.$branches.create(branch, on: db)
-// return branch.toDTO()
-// },
-// delete: { id in
-// guard let branch = try await VendorBranch.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// try await branch.delete(on: db)
-// },
-// fetchAll: { request in
-// var query = VendorBranch.query(on: db)
-// switch request {
-// case .withVendor:
-// query = query.with(\.$vendor)
-//
-// case let .for(vendorID: vendorID):
-// let branches = try await Vendor.query(on: db)
-// .filter(\.$id == vendorID)
-// .with(\.$branches)
-// .first()?
-// .branches
-// .map { $0.toDTO() }
-//
-// guard let branches else { throw Abort(.badGateway, reason: "Vendor id not found.") }
-// return branches
-//
-// case .default:
-// break
-// }
-// return try await query.all().map { $0.toDTO() }
-//
-// },
-// get: { id in
-// try await VendorBranch.find(id, on: db).map { $0.toDTO() }
-// },
-// update: { id, updates in
-// guard let branch = try await VendorBranch.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// branch.applyUpdates(updates)
-// try await branch.save(on: db)
-// return branch.toDTO()
-// }
-// )
-// }
-// }
diff --git a/Sources/App/DB/VendorDB.swift b/Sources/App/DB/VendorDB.swift
deleted file mode 100644
index 5247765..0000000
--- a/Sources/App/DB/VendorDB.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-// import Dependencies
-// import DependenciesMacros
-// import Fluent
-// import Vapor
-//
-// public extension DependencyValues {
-// var vendors: VendorDB {
-// get { self[VendorDB.self] }
-// set { self[VendorDB.self] = newValue }
-// }
-// }
-//
-// @DependencyClient
-// public struct VendorDB: Sendable {
-// var create: @Sendable (Vendor.Create) async throws -> Vendor.DTO
-// var delete: @Sendable (Vendor.IDValue) async throws -> Void
-// var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor.DTO]
-// var get: @Sendable (Vendor.IDValue, GetRequest) async throws -> Vendor.DTO?
-// var update: @Sendable (Vendor.IDValue, Vendor.Update) async throws -> Vendor.DTO
-//
-// enum FetchRequest {
-// case `default`
-// case withBranches
-// }
-//
-// enum GetRequest {
-// case `default`
-// case withBranches
-// }
-//
-// func fetchAll() async throws -> [Vendor.DTO] {
-// try await fetchAll(.default)
-// }
-//
-// func get(_ id: Vendor.IDValue) async throws -> Vendor.DTO? {
-// try await get(id, .default)
-// }
-// }
-//
-// extension VendorDB: TestDependencyKey {
-// public static let testValue: VendorDB = Self()
-//
-// static func live(database db: any Database) -> Self {
-// .init(
-// create: { model in
-// let model = model.toModel()
-// try await model.save(on: db)
-// return model.toDTO()
-//
-// },
-// delete: { id in
-// guard let vendor = try await Vendor.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// try await vendor.delete(on: db)
-//
-// },
-// fetchAll: { request in
-// var query = Vendor.query(on: db).sort(\.$name, .ascending)
-// let withBranches = request == .withBranches
-// if withBranches {
-// query = query.with(\.$branches)
-// }
-// return try await query.all().map { $0.toDTO(includeBranches: withBranches) }
-//
-// },
-// get: { id, request in
-// var query = Vendor.query(on: db).filter(\.$id == id)
-// let withBranches = request == .withBranches
-// if withBranches {
-// query = query.with(\.$branches)
-// }
-// return try await query.first().map { $0.toDTO(includeBranches: withBranches) }
-//
-// },
-// update: { id, updates in
-// guard let vendor = try await Vendor.find(id, on: db) else {
-// throw Abort(.notFound)
-// }
-// vendor.applyUpdates(updates)
-// return vendor.toDTO()
-// }
-// )
-// }
-// }
diff --git a/Sources/App/DependenciesMiddleware.swift b/Sources/App/DependenciesMiddleware.swift
new file mode 100644
index 0000000..59fcb5e
--- /dev/null
+++ b/Sources/App/DependenciesMiddleware.swift
@@ -0,0 +1,23 @@
+import DatabaseClientLive
+import Dependencies
+import Vapor
+
+struct DependenciesMiddleware: AsyncMiddleware {
+
+ private let values: DependencyValues.Continuation
+
+ init() {
+ self.values = withEscapedDependencies { $0 }
+ }
+
+ func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
+ try await values.yield {
+ try await withDependencies {
+ $0.database = .live(database: request.db)
+ } operation: {
+ try await next.respond(to: request)
+ }
+ }
+ }
+
+}
diff --git a/Sources/App/Models/Employee.swift b/Sources/App/Models/Employee.swift
deleted file mode 100644
index b9f6a12..0000000
--- a/Sources/App/Models/Employee.swift
+++ /dev/null
@@ -1,165 +0,0 @@
-// 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
-// /// employee / technician, an office employee, or an administrator.
-// ///
-// /// # NOTE: Only `User` types can login and generate po's for employees.
-// ///
-// final class Employee: Model, @unchecked Sendable {
-//
-// static let schema = "employee"
-//
-// @ID(key: .id)
-// var id: UUID?
-//
-// @Field(key: "first_name")
-// var firstName: String
-//
-// @Field(key: "last_name")
-// var lastName: String
-//
-// @Field(key: "is_active")
-// var active: Bool
-//
-// @Timestamp(key: "created_at", on: .create)
-// var createdAt: Date?
-//
-// @Timestamp(key: "updated_at", on: .update)
-// var updatedAt: Date?
-//
-// init() {}
-//
-// init(
-// id: UUID? = nil,
-// firstName: String,
-// lastName: String,
-// active: Bool,
-// createdAt: Date? = nil,
-// updatedAt: Date? = nil
-// ) {
-// self.id = id
-// self.firstName = firstName
-// self.lastName = lastName
-// self.active = active
-// self.createdAt = createdAt
-// self.updatedAt = updatedAt
-// }
-//
-// func toDTO() -> DTO {
-// .init(
-// id: id,
-// firstName: $firstName.value,
-// lastName: $lastName.value,
-// active: $active.value,
-// createdAt: createdAt,
-// updatedAt: updatedAt
-// )
-// }
-//
-// func applyUpdates(_ updates: Update) {
-// if let firstName = updates.firstName {
-// self.firstName = firstName
-// }
-// if let lastName = updates.lastName {
-// self.lastName = lastName
-// }
-// if let active = updates.active {
-// self.active = active
-// }
-// }
-// }
-//
-// // MARK: - Helpers
-//
-// extension Employee {
-//
-// struct Create: Content {
-// let firstName: String
-// let lastName: String
-// let active: Bool?
-//
-// func toModel() -> Employee {
-// .init(
-// firstName: firstName,
-// lastName: lastName,
-// active: active ?? true
-// )
-// }
-// }
-//
-// struct DTO: Content {
-//
-// var id: UUID?
-// var firstName: String?
-// var lastName: String?
-// var active: Bool?
-// var createdAt: Date?
-// var updatedAt: Date?
-//
-// func toModel() -> Employee {
-// let model = Employee()
-//
-// model.id = id
-// if let firstName {
-// model.firstName = firstName
-// }
-// if let lastName {
-// model.lastName = lastName
-// }
-// if let active {
-// model.active = active
-// }
-// return model
-// }
-// }
-//
-// struct Migrate: AsyncMigration {
-//
-// let name = "CreateEmployee"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(Employee.schema)
-// .id()
-// .field("first_name", .string, .required)
-// .field("last_name", .string, .required)
-// .field("is_active", .bool, .required, .sql(.default(true)))
-// .field("created_at", .datetime)
-// .field("updated_at", .datetime)
-// .unique(on: "first_name", "last_name")
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(Employee.schema).delete()
-// }
-//
-// }
-//
-// struct Update: Content {
-// var firstName: String?
-// var lastName: String?
-// var active: Bool?
-// }
-// }
-//
-// // MARK: - Validations
-//
-// extension Employee.Create: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("firstName", as: String.self, is: !.empty)
-// validations.add("lastName", as: String.self, is: !.empty)
-// }
-// }
-//
-// extension Employee.Update: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("firstName", as: String?.self, is: .nil || !.empty, required: false)
-// validations.add("lastName", as: String?.self, is: .nil || !.empty, required: false)
-// }
-// }
diff --git a/Sources/App/Models/PurchaseOrder.swift b/Sources/App/Models/PurchaseOrder.swift
deleted file mode 100644
index f66146c..0000000
--- a/Sources/App/Models/PurchaseOrder.swift
+++ /dev/null
@@ -1,156 +0,0 @@
-// import Fluent
-// import Vapor
-//
-// /// The purchase order database model.
-// ///
-// /// # NOTE: An initial purchase order should be created with an `id` higher than our current PO
-// /// so that subsequent PO's are generated with higher values than our current system produces.
-// /// once the first one is set, the rest will auto-increment from there.
-// final class PurchaseOrder: Model, Content, @unchecked Sendable {
-// static let schema = "purchase_order"
-//
-// @ID(custom: "id", generatedBy: .database)
-// var id: Int?
-//
-// @Field(key: "work_order")
-// var workOrder: Int?
-//
-// @Field(key: "materials")
-// var materials: String
-//
-// @Field(key: "customer")
-// var customer: String
-//
-// @Field(key: "truck_stock")
-// var truckStock: Bool
-//
-// @Parent(key: "created_by_id")
-// var createdBy: User
-//
-// @Parent(key: "created_for_id")
-// var createdFor: Employee
-//
-// @Parent(key: "vendor_branch_id")
-// var vendorBranch: VendorBranch
-//
-// @Timestamp(key: "created_at", on: .create)
-// var createdAt: Date?
-//
-// @Timestamp(key: "updated_at", on: .update)
-// var updatedAt: Date?
-//
-// init() {}
-//
-// init(
-// id: Int? = nil,
-// workOrder: Int? = nil,
-// materials: String,
-// customer: String,
-// truckStock: Bool,
-// createdByID: User.IDValue,
-// createdForID: Employee.IDValue,
-// vendorBranchID: VendorBranch.IDValue,
-// createdAt: Date? = nil,
-// updatedAt: Date? = nil
-// ) {
-// self.id = id
-// self.workOrder = workOrder
-// self.materials = materials
-// self.customer = customer
-// self.truckStock = truckStock
-// $createdBy.id = createdByID
-// $createdFor.id = createdForID
-// $vendorBranch.id = vendorBranchID
-// self.createdAt = createdAt
-// self.updatedAt = updatedAt
-// }
-//
-// func toDTO() -> DTO {
-// .init(
-// id: id,
-// workOrder: workOrder,
-// materials: materials,
-// customer: customer,
-// truckStock: truckStock,
-// createdBy: $createdBy.value?.toDTO(),
-// createdFor: $createdFor.value?.toDTO(),
-// vendorBranch: $vendorBranch.value,
-// createdAt: createdAt,
-// updatedAt: updatedAt
-// )
-// }
-//
-// }
-//
-// extension PurchaseOrder {
-//
-// struct Create: Content {
-// let id: Int?
-// let workOrder: Int?
-// let materials: String
-// let customer: String
-// let truckStock: Bool?
-// let createdForID: Employee.IDValue
-// let vendorBranchID: VendorBranch.IDValue
-//
-// func toModel(createdByID: User.IDValue) -> PurchaseOrder {
-// .init(
-// id: id,
-// workOrder: workOrder,
-// materials: materials,
-// customer: customer,
-// truckStock: truckStock ?? false,
-// createdByID: createdByID,
-// createdForID: createdForID,
-// vendorBranchID: vendorBranchID,
-// createdAt: nil,
-// updatedAt: nil
-// )
-// }
-// }
-//
-// struct DTO: Content {
-// let id: Int?
-// let workOrder: Int?
-// let materials: String
-// let customer: String
-// let truckStock: Bool
-// let createdBy: User.DTO?
-// let createdFor: Employee.DTO?
-// let vendorBranch: VendorBranch?
-// let createdAt: Date?
-// let updatedAt: Date?
-// }
-//
-// struct Migrate: AsyncMigration {
-//
-// let name = "CreatePurchaseOrder"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(PurchaseOrder.schema)
-// .field("id", .int, .identifier(auto: true))
-// .field("work_order", .int)
-// .field("customer", .string, .required)
-// .field("materials", .string, .required)
-// .field("truck_stock", .bool, .required)
-// .field("created_by_id", .uuid, .required, .references(User.schema, "id"))
-// .field("created_for_id", .uuid, .required, .references(Employee.schema, "id"))
-// .field("vendor_branch_id", .uuid, .required, .references(VendorBranch.schema, "id"))
-// .field("created_at", .datetime)
-// .field("updated_at", .datetime)
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(PurchaseOrder.schema).delete()
-// }
-// }
-// }
-//
-// extension PurchaseOrder.Create: Validatable {
-//
-// static func validations(_ validations: inout Validations) {
-// validations.add("materials", as: String.self, is: !.empty)
-// validations.add("customer", as: String.self, is: !.empty)
-// }
-// }
diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift
deleted file mode 100644
index 874d234..0000000
--- a/Sources/App/Models/User.swift
+++ /dev/null
@@ -1,121 +0,0 @@
-// import Fluent
-// import Vapor
-//
-// /// The user database model.
-// ///
-// /// A user is someone who is able to login and generate PO's for employees. Generally a user should also
-// /// have an employee profile, but not all employees are users. Users are generally restricted to office workers
-// /// and administrators.
-// ///
-// ///
-// final class User: Model, @unchecked Sendable {
-// static let schema = "user"
-//
-// @ID(key: .id)
-// var id: UUID?
-//
-// @Field(key: "username")
-// var username: String
-//
-// @Field(key: "email")
-// var email: String
-//
-// @Field(key: "password_hash")
-// var passwordHash: String
-//
-// @Timestamp(key: "created_at", on: .create)
-// var createdAt: Date?
-//
-// @Timestamp(key: "updated_at", on: .update)
-// var updatedAt: Date?
-//
-// init() {}
-//
-// init(
-// id: UUID? = nil,
-// username: String,
-// email: String,
-// passwordHash: String
-// ) {
-// self.id = id
-// self.username = username
-// self.email = email
-// self.passwordHash = passwordHash
-// }
-//
-// func toDTO() -> DTO {
-// .init(
-// id: id,
-// username: $username.value,
-// email: $email.value,
-// createdAt: createdAt,
-// updatedAt: updatedAt
-// )
-// }
-//
-// func generateToken() throws -> UserToken {
-// try .init(
-// value: [UInt8].random(count: 16).base64,
-// userID: requireID()
-// )
-// }
-//
-// }
-//
-// extension User {
-//
-// struct Create: Content {
-// var username: String
-// var email: String
-// var password: String
-// var confirmPassword: String
-// }
-//
-// struct DTO: Content {
-// let id: UUID?
-// let username: String?
-// let email: String?
-// let createdAt: Date?
-// let updatedAt: Date?
-// }
-//
-// struct Migrate: AsyncMigration {
-// let name = "CreateUser"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(User.schema)
-// .id()
-// .field("username", .string, .required)
-// .field("email", .string, .required)
-// .field("password_hash", .string, .required)
-// .field("created_at", .datetime)
-// .field("updated_at", .datetime)
-// .unique(on: "email", "username")
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(User.schema).delete()
-// }
-// }
-// }
-//
-// extension User: ModelAuthenticatable {
-// static let usernameKey = \User.$username
-// static let passwordHashKey = \User.$passwordHash
-//
-// func verify(password: String) throws -> Bool {
-// try Bcrypt.verify(password, created: passwordHash)
-// }
-// }
-//
-// extension User: ModelSessionAuthenticatable {}
-// extension User: ModelCredentialsAuthenticatable {}
-//
-// extension User.Create: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("username", as: String.self, is: !.empty)
-// validations.add("email", as: String.self, is: .email)
-// validations.add("password", as: String.self, is: .count(8...))
-// }
-// }
diff --git a/Sources/App/Models/UserToken.swift b/Sources/App/Models/UserToken.swift
deleted file mode 100644
index 5db2f47..0000000
--- a/Sources/App/Models/UserToken.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-// import Fluent
-// import Vapor
-//
-// final class UserToken: Model, Content, @unchecked Sendable {
-//
-// static let schema = "user_token"
-//
-// @ID(key: .id)
-// var id: UUID?
-//
-// @Field(key: "value")
-// var value: String
-//
-// @Parent(key: "user_id")
-// var user: User
-//
-// init() {}
-//
-// init(id: UUID? = nil, value: String, userID: User.IDValue) {
-// self.id = id
-// self.value = value
-// $user.id = userID
-// }
-// }
-//
-// extension UserToken {
-//
-// struct Migrate: AsyncMigration {
-// let name = "CreateUserToken"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(UserToken.schema)
-// .id()
-// .field("value", .string, .required)
-// .field("user_id", .uuid, .required, .references(User.schema, "id"))
-// .unique(on: "value")
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(UserToken.schema).delete()
-// }
-// }
-// }
-//
-// extension UserToken: ModelTokenAuthenticatable {
-// static let valueKey = \UserToken.$value
-// static let userKey = \UserToken.$user
-//
-// var isValid: Bool { true }
-// }
diff --git a/Sources/App/Models/Vendor.swift b/Sources/App/Models/Vendor.swift
deleted file mode 100644
index 5e0a5bc..0000000
--- a/Sources/App/Models/Vendor.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-// import Fluent
-// import struct Foundation.UUID
-// import Vapor
-//
-// // The primary database model.
-// final class Vendor: Model, @unchecked Sendable {
-//
-// static let schema = "vendor"
-//
-// @ID(key: .id)
-// var id: UUID?
-//
-// @Field(key: "name")
-// var name: String
-//
-// @Timestamp(key: "created_at", on: .create)
-// var createdAt: Date?
-//
-// @Timestamp(key: "updated_at", on: .update)
-// var updatedAt: Date?
-//
-// @Children(for: \.$vendor)
-// var branches: [VendorBranch]
-//
-// init() {}
-//
-// init(id: UUID? = nil, name: String) {
-// self.id = id
-// self.name = name
-// }
-//
-// func toDTO(includeBranches: Bool? = nil) -> DTO {
-// .init(
-// id: id,
-// name: $name.value,
-// branches: ($branches.value != nil && $branches.value!.count > 0)
-// ? $branches.value!.map { $0.toDTO() }
-// : (includeBranches == true) ? [] : nil,
-// createdAt: createdAt,
-// updatedAt: updatedAt
-// )
-// }
-//
-// func applyUpdates(_ updates: Update) {
-// name = updates.name
-// }
-// }
-//
-// // MARK: - Helpers.
-//
-// extension Vendor {
-// struct Create: Content {
-// var name: String
-//
-// func toModel() -> Vendor {
-// .init(name: name)
-// }
-// }
-//
-// struct DTO: Content {
-//
-// var id: UUID?
-// var name: String?
-// var branches: [VendorBranch.DTO]?
-// let createdAt: Date?
-// let updatedAt: Date?
-//
-// func toModel() -> Vendor {
-// let model = Vendor()
-// model.id = id
-// if let name {
-// model.name = name
-// }
-// return model
-// }
-// }
-//
-// struct Migrate: AsyncMigration {
-// let name = "CreateVendor"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(Vendor.schema)
-// .id()
-// .field("name", .string, .required)
-// .field("created_at", .datetime)
-// .field("updated_at", .datetime)
-// .unique(on: "name")
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(Vendor.schema).delete()
-// }
-// }
-//
-// struct Update: Content {
-// var name: String
-// }
-// }
-//
-// // MARK: - Validations
-//
-// extension Vendor.Create: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("name", as: String.self, is: !.empty)
-// }
-// }
-//
-// extension Vendor.Update: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("name", as: String.self, is: !.empty)
-// }
-// }
diff --git a/Sources/App/Models/VendorBranch.swift b/Sources/App/Models/VendorBranch.swift
deleted file mode 100644
index f7d7448..0000000
--- a/Sources/App/Models/VendorBranch.swift
+++ /dev/null
@@ -1,119 +0,0 @@
-// import Fluent
-// import struct Foundation.UUID
-// import Vapor
-//
-// final class VendorBranch: Model, @unchecked Sendable {
-//
-// static let schema = "vendor_branch"
-//
-// @ID(key: .id)
-// var id: UUID?
-//
-// @Field(key: "name")
-// var name: String
-//
-// @Timestamp(key: "created_at", on: .create)
-// var createdAt: Date?
-//
-// @Timestamp(key: "updated_at", on: .update)
-// var updatedAt: Date?
-//
-// @Parent(key: "vendor_id")
-// var vendor: Vendor
-//
-// init() {}
-//
-// init(id: UUID? = nil, name: String, vendorId: Vendor.IDValue) {
-// self.id = id
-// self.name = name
-// $vendor.id = vendorId
-// }
-//
-// func toDTO() -> DTO {
-// .init(
-// id: id,
-// name: $name.value,
-// vendorId: $vendor.id,
-// createdAt: createdAt,
-// updatedAt: updatedAt
-// )
-// }
-//
-// func applyUpdates(_ updates: Update) {
-// name = updates.name
-// }
-//
-// }
-//
-// // MARK: - Helpers
-//
-// extension VendorBranch {
-// struct Create: Content {
-// var name: String
-//
-// func toModel() -> VendorBranch {
-// let model = VendorBranch()
-// model.name = name
-// return model
-// }
-// }
-//
-// struct DTO: Content {
-// var id: UUID?
-// var name: String?
-// var vendorId: Vendor.IDValue?
-// let createdAt: Date?
-// let updatedAt: Date?
-//
-// func toModel() -> VendorBranch {
-// let model = VendorBranch()
-//
-// model.id = id
-// if let name {
-// model.name = name
-// }
-// if let vendorId {
-// model.$vendor.id = vendorId
-// }
-// return model
-// }
-//
-// }
-//
-// struct Migrate: AsyncMigration {
-// let name = "CreateVendorBranch"
-//
-// func prepare(on database: any Database) async throws {
-// try await database.schema(VendorBranch.schema)
-// .id()
-// .field("name", .string, .required)
-// .field("vendor_id", .uuid, .required)
-// .field("created_at", .datetime)
-// .field("updated_at", .datetime)
-// .foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade)
-// .create()
-// }
-//
-// func revert(on database: any Database) async throws {
-// try await database.schema(VendorBranch.schema).delete()
-// }
-// }
-//
-// struct Update: Content {
-// var name: String
-// }
-// }
-//
-// // MARK: - Validations
-//
-// extension VendorBranch.Create: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("name", as: String.self, is: !.empty)
-// }
-// }
-//
-// extension VendorBranch.Update: Validatable {
-// static func validations(_ validations: inout Validations) {
-// validations.add("name", as: String.self, is: !.empty)
-// }
-// }
diff --git a/Sources/App/Views/Buttons.swift b/Sources/App/Views/Buttons.swift
new file mode 100644
index 0000000..74e3aa5
--- /dev/null
+++ b/Sources/App/Views/Buttons.swift
@@ -0,0 +1,9 @@
+import Elementary
+
+struct ToggleFormButton: HTML {
+ var content: some HTML {
+ a(.href("javascript:void(0)"), .on(.click, "toggleContent('form')"), .class("btn-add")) {
+ "+"
+ }
+ }
+}
diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift
new file mode 100644
index 0000000..7fd94a6
--- /dev/null
+++ b/Sources/App/Views/Main.swift
@@ -0,0 +1,43 @@
+import Elementary
+import ElementaryHTMX
+
+struct MainPage: HTMLDocument {
+
+ var title: String { "Purchase Orders" }
+
+ let inner: Inner
+ let displayNav: Bool
+
+ init(displayNav: Bool = false, _ inner: () -> Inner) {
+ self.displayNav = displayNav
+ self.inner = inner()
+ }
+
+ var head: some HTML {
+ meta(.charset(.utf8))
+ script(.src("https://unpkg.com/htmx.org@2.0.4")) {}
+ script(.src("/js/main.js")) {}
+ link(.rel(.stylesheet), .href("/css/main.css"))
+ }
+
+ var body: some HTML {
+ header {
+ Logo()
+ if displayNav {
+ Navbar()
+ }
+ }
+ inner
+ }
+}
+
+extension MainPage: Sendable where Inner: Sendable {}
+
+struct Logo: HTML, Sendable {
+
+ var content: some HTML {
+ div(.id("logo")) {
+ "HHE - Purchase Orders"
+ }
+ }
+}
diff --git a/Sources/App/Views/Navbar.swift b/Sources/App/Views/Navbar.swift
new file mode 100644
index 0000000..f6e9bd7
--- /dev/null
+++ b/Sources/App/Views/Navbar.swift
@@ -0,0 +1,31 @@
+import Elementary
+import ElementaryHTMX
+
+struct Navbar: HTML {
+ var content: some HTML {
+ div(.class("sidepanel"), .id("sidepanel")) {
+ a(.href("javascript:void(0)"), .class("closebtn"), .on(.click, "closeSidepanel()")) {
+ "x"
+ }
+ a(.hx.get("/purchase-orders?page=1&limit=50"), .hx.target("body"), .hx.pushURL(true)) {
+ "Purchae Orders"
+ }
+ a(.hx.get("/users"), .hx.target("body"), .hx.pushURL(true)) {
+ "Users"
+ }
+ a(.hx.get("/employees"), .hx.target("body"), .hx.pushURL(true)) {
+ "Employees"
+ }
+ a(.hx.get("/vendors"), .hx.target("body"), .hx.pushURL(true)) {
+ "Vendors"
+ }
+ div(.style("border-bottom: 1px solid grey; margin-bottom: 5px;")) {}
+ a(.hx.post("/logout"), .hx.target("#content"), .hx.swap(.outerHTML), .hx.trigger(.event(.click))) {
+ "Logout"
+ }
+ }
+ button(.class("openbtn"), .on(.click, "openSidepanel()")) {
+ img(.src("/images/menu.svg"), .style("width: 30px;, height: 30px;"))
+ }
+ }
+}
diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift
new file mode 100644
index 0000000..ac31506
--- /dev/null
+++ b/Sources/App/Views/Users/UserForm.swift
@@ -0,0 +1,78 @@
+import Elementary
+import ElementaryHTMX
+
+struct UserForm: HTML, Sendable {
+ let context: Context
+
+ var content: some HTML {
+ form(
+ .id("user-form"),
+ .class("user-form"),
+ .hx.post(context.targetURL),
+ .hx.pushURL(context.pushURL),
+ .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset(); toggleContent('form');")
+ ) {
+ input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required)
+ br()
+ if context.showEmailInput {
+ input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required)
+ br()
+ }
+ input(.type(.password), .id("password"), .name("password"), .placeholder("Password"), .required)
+ br()
+ if context.showConfirmPassword {
+ input(.type(.password), .id("confirmPassword"), .name("confirmPassword"), .required)
+ br()
+ }
+ input(.type(.submit), .value(context.buttonLabel))
+ }
+ }
+
+ enum Context {
+ case create
+ case login(next: String?)
+
+ var showConfirmPassword: Bool {
+ switch self {
+ case .create: return true
+ case .login: return false
+ }
+ }
+
+ var showEmailInput: Bool {
+ switch self {
+ case .create: return true
+ case .login: return false
+ }
+ }
+
+ var pushURL: Bool {
+ switch self {
+ case .create: return false
+ case .login: return true
+ }
+ }
+
+ var buttonLabel: String {
+ switch self {
+ case .create:
+ return "Create"
+ case .login:
+ return "Login"
+ }
+ }
+
+ var targetURL: String {
+ switch self {
+ case .create:
+ return "/users"
+ case let .login(next: next):
+ let path = "/login"
+ if let next {
+ return "\(path)?next=\(next)"
+ }
+ return path
+ }
+ }
+ }
+}
diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift
new file mode 100644
index 0000000..3e2bc86
--- /dev/null
+++ b/Sources/App/Views/Users/UserTable.swift
@@ -0,0 +1,40 @@
+import DatabaseClient
+import Dependencies
+import Elementary
+import ElementaryHTMX
+import SharedModels
+
+struct UserTable: HTML {
+
+ @Dependency(\.database.users.fetchAll) var fetchAll
+
+ var content: some HTML {
+ table(.id("user-table")) {
+ thead {
+ tr {
+ th { "Username" }
+ th { "Email" }
+ th { ToggleFormButton() }
+ }
+ }
+ tbody {
+ let users = try await fetchAll()
+ for user in users {
+ Row(user: user)
+ }
+ }
+ }
+ }
+
+ struct Row: HTML {
+ let user: User
+
+ var content: some HTML {
+ tr {
+ td { user.username }
+ td { user.email }
+ td { "Fix me." }
+ }
+ }
+ }
+}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 562106f..12f69a1 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -22,6 +22,7 @@ public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware)
+ app.middleware.use(DependenciesMiddleware())
#if DEBUG
app.lifecycle.use(BrowserSyncHandler())
@@ -42,7 +43,6 @@ public func configure(_ app: Application) async throws {
try withDependencies {
$0.database = databaseClient
} operation: {
- // register routes
try routes(app)
}
diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift
index 6b67cec..e2f097a 100644
--- a/Sources/App/entrypoint.swift
+++ b/Sources/App/entrypoint.swift
@@ -1,3 +1,5 @@
+import DatabaseClientLive
+import Dependencies
import Logging
import NIOCore
import NIOPosix
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index 55f9179..c8112ab 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -1,7 +1,37 @@
+import DatabaseClientLive
+import Dependencies
+import Elementary
import Fluent
import Vapor
+import VaporElementary
func routes(_ app: Application) throws {
try app.register(collection: ApiController())
// try app.register(collection: ViewController())
+
+ app.get("test") { _ in
+ HTMLResponse {
+ MainPage(displayNav: false) {
+ div(.class("container")) {
+ h1 { "iT WORKS" }
+ }
+ }
+ }
+ }
+
+ app.get("login") { _ in
+ HTMLResponse {
+ MainPage(displayNav: false) {
+ UserForm(context: .login(next: nil))
+ }
+ }
+ }
+
+ app.get("users") { _ in
+ HTMLResponse {
+ MainPage(displayNav: false) {
+ UserTable()
+ }
+ }
+ }
}