diff --git a/Public/css/main.css b/Public/css/main.css index 683f6d7..0135a53 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -59,7 +59,8 @@ nav { } form { - padding: 50px 0; + padding: 80px; + width: 100%; text-align: center; } @@ -69,6 +70,7 @@ form label { form input { margin-bottom: 15px; + width: 100%; } .login-form { @@ -204,3 +206,8 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus { .show { display: block; } + +tr.htmx-swapping td { + opacity: 0; + transition: opacity 0.5s ease-out; +} diff --git a/Resources/Views/home.leaf b/Resources/Views/home.leaf index 5842b6c..420c9f9 100644 --- a/Resources/Views/home.leaf +++ b/Resources/Views/home.leaf @@ -31,12 +31,21 @@ Employees +
  • + + Vendors + +
  • -
    -

    We're in!

    -
    + #import("homeContent") + + + #endexport diff --git a/Resources/Views/htmx-form.leaf b/Resources/Views/htmx-form.leaf index 638b986..6e6551e 100644 --- a/Resources/Views/htmx-form.leaf +++ b/Resources/Views/htmx-form.leaf @@ -4,7 +4,7 @@ #endif #if(htmxPostTargetUrl): hx-post="#(htmxPostTargetUrl)" - #else: + #elseif(htmxPutTargetUrl): hx-put="#(htmxPutTargetUrl)" #endif hx-target="#(htmxTarget)" diff --git a/Resources/Views/vendor-form.leaf b/Resources/Views/vendor-form.leaf index 67b2fcf..c34a1d1 100644 --- a/Resources/Views/vendor-form.leaf +++ b/Resources/Views/vendor-form.leaf @@ -1,9 +1,11 @@ -
    +#extend("htmx-form", htmxForm): + #export("formBody"): - -
    +
    + +
    + +
    +

    Mutliple branches can be specified separated by a comma.

    + #endexport +#endextend diff --git a/Resources/Views/vendor-table.leaf b/Resources/Views/vendor-table.leaf new file mode 100644 index 0000000..22f8f85 --- /dev/null +++ b/Resources/Views/vendor-table.leaf @@ -0,0 +1,34 @@ + + + + + + + + #for(vendor in vendors): + + + + + + + #endfor + +
    NameBranches
    #capitalized(vendor.name) + #if(vendor.branches): +
      + #for(branch in vendor.branches): +
    • #capitalized(branch.name)
    • + #endfor +
    + #endif +
    + + Delete + +
    diff --git a/Resources/Views/vendors.leaf b/Resources/Views/vendors.leaf new file mode 100644 index 0000000..017904e --- /dev/null +++ b/Resources/Views/vendors.leaf @@ -0,0 +1,14 @@ +#extend("home"): + #export("homeContent"): +
    +
    +

    Vendors

    +
    +

    Vendors are who purchase orders can be issued for, they consist of multiple branches / locations.

    +
    + #extend("vendor-form", form) + #extend("vendor-table") +
    +
    + #endexport +#endextend diff --git a/Sources/App/Contexts/HtmxFormCTX.swift b/Sources/App/Contexts/HtmxFormCTX.swift index 48fa931..5bedf7d 100644 --- a/Sources/App/Contexts/HtmxFormCTX.swift +++ b/Sources/App/Contexts/HtmxFormCTX.swift @@ -2,7 +2,7 @@ import Vapor /// Represents a generic form context that is used to generate form templates /// that are handled by htmx. -struct HtmxFormCTX: Content { +struct HtmxFormCTX: Content { let formClass: String? let formId: String let htmxPostTargetUrl: String? @@ -12,7 +12,7 @@ struct HtmxFormCTX: Content { let htmxResetAfterRequest: Bool let htmxSwapOob: String? let htmxSwap: String? - let context: C + let context: Context init( formClass: String? = nil, @@ -23,7 +23,7 @@ struct HtmxFormCTX: Content { htmxResetAfterRequest: Bool = true, htmxSwapOob: HtmxSwap? = nil, htmxSwap: HtmxSwap? = nil, - context: C + context: Context ) { self.formClass = formClass self.formId = formId @@ -65,3 +65,33 @@ struct HtmxFormCTX: Content { } 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) + ) + } +} diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index 05d9c69..4feca1f 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -31,7 +31,7 @@ struct ApiController: RouteCollection { purchaseOrders.post(use: createPurchaseOrder(req:)) users.get(use: usersIndex(req:)) - users.post(use: createUser(req:)) + api.post("users", use: createUser(req:)) users.group("login") { $0.get(use: self.login(req:)) } @@ -83,7 +83,7 @@ struct ApiController: RouteCollection { throw Abort(.notFound) } try await employee.delete(on: req.db) - return .noContent + return .ok } @Sendable @@ -150,6 +150,13 @@ struct ApiController: RouteCollection { @Sendable func createUser(req: Request) async throws -> User.DTO { + let count = try await User.query(on: req.db).count() + if count > 0 { + guard req.auth.get(User.self) != nil else { + throw Abort(.unauthorized) + } + } + try User.Create.validate(content: req) let create = try req.content.decode(User.Create.self) guard create.password == create.confirmPassword else { @@ -184,7 +191,7 @@ struct ApiController: RouteCollection { } try await user.delete(on: req.db) - return .noContent + return .ok } // MARK: - Vendors @@ -198,7 +205,10 @@ struct ApiController: RouteCollection { dbQuery = dbQuery.with(\.$branches) } - return try await dbQuery.all().map { $0.toDTO(includeBranches: params.branches) } + return try await dbQuery + .sort(\.$name, .ascending) + .all() + .map { $0.toDTO(includeBranches: params.branches) } } @Sendable @@ -228,7 +238,7 @@ struct ApiController: RouteCollection { } try await vendor.delete(on: req.db) - return .noContent + return .ok } // MARK: - VendorBranch @@ -258,7 +268,7 @@ struct ApiController: RouteCollection { throw Abort(.notFound) } try await branch.delete(on: req.db) - return .noContent + return .ok } @Sendable diff --git a/Sources/App/Controllers/VendorViewController.swift b/Sources/App/Controllers/VendorViewController.swift index e5255d8..346828d 100644 --- a/Sources/App/Controllers/VendorViewController.swift +++ b/Sources/App/Controllers/VendorViewController.swift @@ -5,16 +5,105 @@ struct VendorViewController: RouteCollection { private let api = ApiController() func boot(routes: any RoutesBuilder) throws { - // Do something. + 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", makeCtx(req: req)) + } + + @Sendable + func create(req: Request) async throws -> View { + let ctx = try req.content.decode(CreateVendorCTX.self) + req.logger.info("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 + ) + } + } + + // _ = try await api.createVendor(req: req) + return try await req.view.render("vendor-table", makeCtx(req: req)) + } + + @Sendable + func delete(req: Request) async throws -> HTTPStatus { + try await api.deleteVendor(req: req) + } + + @Sendable + func update(req: Request) async throws -> View { + _ = try await api.updateVendor(req: req) + return try await req.view.render("vendor-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 vendor: Vendor? - let buttonLabel: String + let htmxForm: HtmxFormCTX - init(vendor: Vendor? = nil, buttonLabel: String = "Create") { - self.vendor = vendor - self.buttonLabel = buttonLabel + 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? +} diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index 70cbed1..09e9350 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -7,6 +7,7 @@ struct ViewController: RouteCollection { private let api = ApiController() private let employees = EmployeeViewController() private let users = UserViewController() + private let vendors = VendorViewController() func boot(routes: any RoutesBuilder) throws { let protected = routes.protected @@ -25,6 +26,7 @@ struct ViewController: RouteCollection { // protected.get("users", use: users(req:)) try routes.register(collection: employees) try routes.register(collection: users) + try routes.register(collection: vendors) } @Sendable @@ -99,6 +101,7 @@ private struct UserForm: Content { enum HomeRoute: String, Content { case employees case users + case vendors } struct HomeCTX: Content { diff --git a/Sources/App/Models/Vendor.swift b/Sources/App/Models/Vendor.swift index da5f7b7..f609dff 100644 --- a/Sources/App/Models/Vendor.swift +++ b/Sources/App/Models/Vendor.swift @@ -79,7 +79,6 @@ extension Vendor { func revert(on database: Database) async throws { try await database.schema(Vendor.schema).delete() } - } struct Update: Content { diff --git a/Sources/App/Models/VendorBranch.swift b/Sources/App/Models/VendorBranch.swift index a2ad237..b93de1f 100644 --- a/Sources/App/Models/VendorBranch.swift +++ b/Sources/App/Models/VendorBranch.swift @@ -73,7 +73,8 @@ extension VendorBranch { try await database.schema(VendorBranch.schema) .id() .field("name", .string, .required) - .field("vendor_id", .uuid, .required, .references("vendor", "id")) + .field("vendor_id", .uuid, .required) + .foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade) .create() }