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() + } + } + } }