feat: Reorganizes files.

This commit is contained in:
2025-01-10 14:03:52 -05:00
parent 280bc31a03
commit 455287fe1c
10 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import Vapor
/// Represents a generic form context that is used to generate form templates
/// that are handled by htmx.
struct HtmxFormCTX<Context: Content>: Content {
let formClass: String?
let formId: String
let htmxPostTargetUrl: String?
let htmxPutTargetUrl: String?
let htmxTarget: String
let htmxPushUrl: Bool
let htmxResetAfterRequest: Bool
let htmxSwapOob: String?
let htmxSwap: String?
let context: Context
init(
formClass: String? = nil,
formId: String,
htmxTargetUrl: TargetUrl,
htmxTarget: String,
htmxPushUrl: Bool,
htmxResetAfterRequest: Bool = true,
htmxSwapOob: HtmxSwap? = nil,
htmxSwap: HtmxSwap? = nil,
context: Context
) {
self.formClass = formClass
self.formId = formId
self.htmxPostTargetUrl = htmxTargetUrl.postUrl
self.htmxPutTargetUrl = htmxTargetUrl.putUrl
self.htmxTarget = htmxTarget
self.htmxPushUrl = htmxPushUrl
self.htmxResetAfterRequest = htmxResetAfterRequest
self.htmxSwapOob = htmxSwapOob?.rawValue
self.htmxSwap = htmxSwap?.rawValue
self.context = context
}
enum HtmxSwap: String {
case innerHTML
case outerHTML
case afterbegin
case beforebegin
case afterend
case beforeend
case delete
case none
}
enum TargetUrl {
case put(String)
case post(String)
var putUrl: String? {
guard case let .put(url) = self else { return nil }
return url
}
var postUrl: String? {
guard case let .post(url) = self else { return nil }
return url
}
}
}
struct EmptyContent: Content {}
struct ButtonLabelContext: Content {
let buttonLabel: String
}
extension HtmxFormCTX where Context == ButtonLabelContext {
init(
formClass: String? = nil,
formId: String,
htmxTargetUrl: TargetUrl,
htmxTarget: String,
htmxPushUrl: Bool,
htmxResetAfterRequest: Bool = true,
htmxSwapOob: HtmxSwap? = nil,
htmxSwap: HtmxSwap? = nil,
buttonLabel: String
) {
self.init(
formClass: formClass,
formId: formId,
htmxTargetUrl: htmxTargetUrl,
htmxTarget: htmxTarget,
htmxPushUrl: htmxPushUrl,
htmxResetAfterRequest: htmxResetAfterRequest,
htmxSwapOob: htmxSwapOob,
htmxSwap: htmxSwapOob,
context: .init(buttonLabel: buttonLabel)
)
}
}

View File

@@ -0,0 +1,114 @@
import Fluent
import Leaf
import Vapor
struct EmployeeViewController: RouteCollection {
private let employees = EmployeeDB()
private let api = EmployeeApiController()
func boot(routes: any RoutesBuilder) throws {
let protected = routes.protected.grouped("employees")
protected.get(use: index(req:))
protected.get("form", use: employeeForm(req:))
protected.post(use: create(req:))
protected.group(":employeeID") {
$0.get(use: edit(req:))
$0.delete(use: delete(req:))
$0.put(use: update(req:))
$0.post("toggle-active", use: toggleActive(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
return try await req.view.render("employees/index", EmployeesCTX(db: employees, req: req))
}
@Sendable
func create(req: Request) async throws -> View {
try Employee.Create.validate(content: req)
let model = try req.content.decode(Employee.Create.self)
_ = try await employees.create(model, on: req.db)
// _ = try await db.createEmployee(req: req)
return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req))
}
@Sendable
func toggleActive(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 employees.fetchAll(on: req.db)
return try await req.view.render("employees/table", ["employees": employees])
}
@Sendable
func delete(req: Request) async throws -> View {
_ = try await api.delete(req: req)
let employees = try await employees.fetchAll(on: req.db)
return try await req.view.render("employees/table", ["employees": employees])
}
@Sendable
func edit(req: Request) async throws -> View {
guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else {
throw Abort(.notFound)
}
return try await req.view.render("employees/form", EmployeeFormCTX(employee: employee.toDTO()))
}
@Sendable
func update(req: Request) async throws -> View {
_ = try await api.update(req: req)
return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req))
}
@Sendable
func employeeForm(req: Request) async throws -> View {
try await req.view.render("employees/form", EmployeeFormCTX())
}
}
private struct EmployeesCTX: Content {
let oob: Bool
let employees: [Employee.DTO]
let form: EmployeeFormCTX
init(
oob: Bool = false,
employee: Employee? = nil,
db: EmployeeDB,
req: Request
) async throws {
self.oob = oob
self.employees = try await db.fetchAll(on: req.db)
self.form = .init(employee: employee.map { $0.toDTO() })
}
}
private struct EmployeeFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
init(employee: Employee.DTO? = nil) {
self.htmxForm = .init(
formClass: "employee-form",
formId: "employee-form",
htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"),
htmxTarget: "#employee-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: employee == nil ? .outerHTML : nil,
context: .init(employee: employee)
)
}
struct Context: Content {
let employee: Employee.DTO?
}
}

View File

@@ -0,0 +1,144 @@
import Fluent
import Vapor
struct PurchaseOrderViewController: RouteCollection {
private let employeesApi = EmployeeApiController()
private let branches = VendorBranchDB()
private let api = ApiController()
private let api2 = PurchaseOrderDB()
func boot(routes: any RoutesBuilder) throws {
let pos = routes.protected.grouped("purchase-orders")
pos.get(use: index(req:))
pos.post(use: create(req:))
}
// TODO: Use pageinated version.
@Sendable
func index(req: Request) async throws -> View {
let purchaseOrders = try await api2.fetchAll(on: req.db)
let branches = try await self.branches.getBranches(req: req)
let employees = try await employeesApi.index(req: req)
req.logger.debug("Branches: \(branches)")
return try await req.view.render(
"purchaseOrders/index",
PurchaseOrderCTX(
purchaseOrders: purchaseOrders,
form: .create(branches: branches, employees: employees)
)
)
}
@Sendable
func create(req: Request) async throws -> View {
try PurchaseOrder.FormCreate.validate(content: req)
let createdById = try req.auth.require(User.self).requireID()
let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate()
let purchaseOrder = try await api2.create(create, createdById: createdById, on: req.db)
return try await req.view.render("purchaseOrders/table-row", purchaseOrder)
}
}
private struct PurchaseOrderCTX: Content {
let purchaseOrders: [PurchaseOrder.DTO]
let form: PurchaseOrderFormCTX?
}
private struct PurchaseOrderFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
struct Context: Content {
let branches: [VendorBranch.FormDTO]
let employees: [Employee.DTO]
}
static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self {
.init(htmxForm: .init(
formClass: "po-form",
formId: "po-form",
htmxTargetUrl: .post("/purchase-orders"),
htmxTarget: "#po-table-body",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: .afterbegin,
context: .init(branches: branches, employees: employees)
))
}
}
extension VendorBranch {
struct FormDTO: Content {
let id: UUID
let name: String
let vendor: Vendor.DTO
}
func toFormDTO() throws -> VendorBranch.FormDTO {
try .init(
id: requireID(),
name: name,
vendor: vendor.toDTO()
)
}
}
private extension PurchaseOrder {
struct FormCreate: Content {
let id: Int?
let workOrder: String?
let materials: String
let customer: String
let truckStock: Bool?
let createdForID: Employee.IDValue
let vendorBranchID: VendorBranch.IDValue
// TODO: Remove.
func toModel(createdByID: User.IDValue) -> PurchaseOrder {
.init(
id: id,
workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
materials: materials,
customer: customer,
truckStock: truckStock ?? false,
createdByID: createdByID,
createdForID: createdForID,
vendorBranchID: vendorBranchID,
createdAt: nil,
updatedAt: nil
)
}
func toCreate() -> PurchaseOrder.Create {
.init(
id: id,
workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
materials: materials,
customer: customer,
truckStock: truckStock,
createdForID: createdForID,
vendorBranchID: vendorBranchID
)
}
}
}
private extension VendorBranchDB {
func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] {
try await VendorBranch.query(on: req.db)
.with(\.$vendor)
.all()
.map { try $0.toFormDTO() }
}
}
extension PurchaseOrder.FormCreate: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("materials", as: String.self, is: !.empty)
validations.add("customer", as: String.self, is: !.empty)
}
}

View File

@@ -0,0 +1,96 @@
import Fluent
import Vapor
struct UserViewController: RouteCollection {
private let api = UserApiController()
func boot(routes: any RoutesBuilder) throws {
let users = routes.protected.grouped("users")
users.get(use: index(req:))
users.post(use: create(req:))
users.group(":userID") {
$0.delete(use: delete(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
try await req.view.render(
"users/index",
UsersCTX(users: api.getSortedUsers(req: req))
)
}
@Sendable
func create(req: Request) async throws -> View {
_ = try await api.create(req: req)
return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)])
}
@Sendable
func delete(req: Request) async throws -> View {
_ = try await api.delete(req: req)
return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)])
}
}
struct UserFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
struct Context: Content {
let showConfirmPassword: Bool
let showEmailInput: Bool
let buttonLabel: String
}
static func signIn(next: String?) -> Self {
.init(
htmxForm: .init(
formClass: "user-form",
formId: "user-form",
htmxTargetUrl: .post("/login\(next != nil ? "?next=\(next!)" : "")"),
htmxTarget: "body",
htmxPushUrl: true,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: nil,
context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In")
)
)
}
static func create() -> Self {
.init(
htmxForm: .init(
formClass: "user-form",
formId: "user-form",
htmxTargetUrl: .post("/users"),
htmxTarget: "#user-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: nil,
context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create")
)
)
}
}
private struct UsersCTX: Content {
let users: [User.DTO]
let form: UserFormCTX
init(users: [User.DTO], form: UserFormCTX? = nil) {
self.users = users
self.form = form ?? .create()
}
}
private extension UserApiController {
func getSortedUsers(req: Request) async throws -> [User.DTO] {
try await index(req: req)
.sorted { ($0.username ?? "") < ($1.username ?? "") }
}
}

View File

@@ -0,0 +1,108 @@
import Fluent
import Vapor
struct VendorViewController: RouteCollection {
private let api = VendorApiController()
func boot(routes: any RoutesBuilder) throws {
let vendors = routes.protected.grouped("vendors")
vendors.get(use: index(req:))
vendors.post(use: create(req:))
vendors.group(":vendorID") {
$0.delete(use: delete(req:))
$0.put(use: update(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
return try await req.view.render("vendors/index", makeCtx(req: req))
}
@Sendable
func create(req: Request) async throws -> View {
let ctx = try req.content.decode(CreateVendorCTX.self)
req.logger.debug("CTX: \(ctx)")
let vendor = Vendor.Create(name: ctx.name).toModel()
try await vendor.save(on: req.db)
if let branchString = ctx.branches {
let branches = branchString.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
for branch in branches {
try await vendor.$branches.create(
VendorBranch(name: String(branch), vendorId: vendor.requireID()),
on: req.db
)
}
}
return try await req.view.render("vendors/table", makeCtx(req: req))
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
try await api.delete(req: req)
}
@Sendable
func update(req: Request) async throws -> View {
_ = try await api.update(req: req)
return try await req.view.render("vendors/table", makeCtx(req: req, oob: true))
}
private func makeCtx(req: Request, vendor: Vendor? = nil, oob: Bool = false) async throws -> VendorsCTX {
let vendors = try await Vendor.query(on: req.db)
.with(\.$branches)
.sort(\.$name, .ascending)
.all()
.map { $0.toDTO() }
return .init(
vendors: vendors,
form: .init(vendor: vendor, oob: oob)
)
}
}
struct VendorFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
init(vendor: Vendor? = nil, oob: Bool = false) {
self.htmxForm = .init(
formClass: "vendor-form",
formId: "vendor-form",
htmxTargetUrl: vendor == nil ? .post("/vendors") : .put("/vendors"),
htmxTarget: "#vendor-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: oob ? .outerHTML : nil,
htmxSwap: oob ? nil : .outerHTML,
context: .init(vendor: vendor)
)
}
struct Context: Content {
let vendor: Vendor?
let branches: String?
let buttonLabel: String
init(vendor: Vendor? = nil) {
self.vendor = vendor
self.branches = vendor?.branches.map(\.name).joined(separator: ", ")
self.buttonLabel = vendor == nil ? "Create" : "Update"
}
}
}
private struct VendorsCTX: Content {
let vendors: [Vendor.DTO]
let form: VendorFormCTX
}
private struct CreateVendorCTX: Content {
let name: String
let branches: String?
}