From d4a8444700648be88ebdb723ddfcd84caf0d2105 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 16 Jan 2025 08:02:13 -0500 Subject: [PATCH] feat working on vendor views. --- .../Controllers/Api/VendorApiController.swift | 2 +- .../View/VendorViewController.swift | 194 ++++++++---------- Sources/App/Views/Vendors/VendorDetail.swift | 61 +++++- Sources/App/Views/Vendors/VendorForm.swift | 51 ++++- Sources/App/Views/Vendors/VendorTable.swift | 27 ++- Sources/App/routes.swift | 3 +- Sources/DatabaseClient/Vendors.swift | 22 +- Sources/DatabaseClientLive/Vendors.swift | 11 +- 8 files changed, 239 insertions(+), 132 deletions(-) diff --git a/Sources/App/Controllers/Api/VendorApiController.swift b/Sources/App/Controllers/Api/VendorApiController.swift index 5395290..fc5aad9 100644 --- a/Sources/App/Controllers/Api/VendorApiController.swift +++ b/Sources/App/Controllers/Api/VendorApiController.swift @@ -31,7 +31,7 @@ struct VendorApiController: RouteCollection { @Sendable func update(req: Request) async throws -> Vendor { - return try await vendors.update(req.ensureIDPathComponent(), req.content.decode(Vendor.Update.self)) + return try await vendors.update(req.ensureIDPathComponent(), with: req.content.decode(Vendor.Update.self)) } @Sendable diff --git a/Sources/App/Controllers/View/VendorViewController.swift b/Sources/App/Controllers/View/VendorViewController.swift index dbb6c77..aba103f 100644 --- a/Sources/App/Controllers/View/VendorViewController.swift +++ b/Sources/App/Controllers/View/VendorViewController.swift @@ -1,108 +1,86 @@ -// 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 -// -// 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? -// } +import DatabaseClient +import Dependencies +import Elementary +import SharedModels +import Vapor +import VaporElementary + +struct VendorViewController: RouteCollection { + + @Dependency(\.database.vendors) var vendors + @Dependency(\.database.vendorBranches) var vendorBranches + + func boot(routes: any RoutesBuilder) throws { + let route = routes.grouped("vendors") + route.get(use: index) + route.post(use: create) + route.get("create", use: form) + route.group(":id") { + $0.get(use: detail) + $0.put(use: update) + $0.post("branches", use: createBranch(req:)) + } + } + + @Sendable + func index(req: Request) async throws -> HTMLResponse { + try await req.render { + try await mainPage(VendorForm()) + } + } + + @Sendable + func create(req: Request) async throws -> HTMLResponse { + let vendor = try await vendors.create(req.content.decode(Vendor.Create.self)) + return await req.render { VendorDetail(vendor: vendor) } + } + + @Sendable + func form(req: Request) async throws -> HTMLResponse { + await req.render { VendorForm(.float(shouldShow: true)) } + } + + @Sendable + func detail(req: Request) async throws -> HTMLResponse { + guard let vendor = try await vendors.get(req.ensureIDPathComponent(), .withBranches) else { + throw Abort(.badRequest, reason: "Vendor does not exist.") + } + let html = VendorDetail(vendor: vendor) + guard req.isHtmxRequest else { + return try await req.render { try await mainPage(html) } + } + return await req.render { html } + } + + @Sendable + func update(req: Request) async throws -> HTMLResponse { + let vendor = try await vendors.update( + req.ensureIDPathComponent(), + with: req.content.decode(Vendor.Update.self), + returnWithBranches: true + ) + return await req.render { VendorDetail(vendor: vendor) } + } + + @Sendable + func createBranch(req: Request) async throws -> HTMLResponse { + let vendorID = try req.ensureIDPathComponent() + let create = try req.content.decode(CreateBranch.self) + let branch = try await vendorBranches.create(.init(name: create.name, vendorID: vendorID)) + return await req.render { VendorDetail.BranchTable.Row(branch: branch) } + } + + private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + let vendors = try await vendors.fetchAll(.withBranches) + return MainPage(displayNav: true, route: .vendors) { + div(.class("container")) { + html + VendorTable(vendors: vendors) + } + } + } +} + +struct CreateBranch: Content { + let name: String +} diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 519a36d..1aae8ed 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -3,12 +3,65 @@ import ElementaryHTMX import SharedModels struct VendorDetail: HTML { - let vendor: Vendor? + + let vendor: Vendor var content: some HTML { - div(.class("container")) { - VendorForm(vendor: vendor) - // TODO: Branch table + form. + Float(shouldDisplay: true) { + VendorForm(.formOnly(vendor)) + BranchTable(branches: vendor.branches ?? []) + div(.class("row btn-row")) { + button(.class("btn-done")) { "Done" } + button(.class("danger")) { "Delete" } + } + } + } + + struct BranchTable: HTML { + let branches: [VendorBranch] + + var content: some HTML { + div { + form( + .id("branch-form"), + .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") + ) { + div(.class("row"), .style("margin: 0;")) { + input(.type(.text), .class("col-10"), .placeholder("Branch Name"), .required) + button( + .type(.submit), + .class("btn-secondary"), + .style("float: right; padding: 10px 50px;"), + .hx.target("#branch-table"), + .hx.swap(.beforeEnd) + ) { "+" } + } + } + table { + thead { + tr { + th(.class("label col-11")) { "Branch Location" } + th(.class("col-1")) {} + } + } + tbody(.id("branch-table")) { + for branch in branches { + Row(branch: branch) + } + } + } + } + } + + struct Row: HTML { + let branch: VendorBranch + + var content: some HTML { + tr { + td(.class("col-11")) { branch.name } + td(.class("col-1")) { Button.danger { "Delete" } } + } + } } } diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift index a89fbd0..db97b65 100644 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -3,31 +3,70 @@ import ElementaryHTMX import SharedModels struct VendorForm: HTML { - let vendor: Vendor? - var content: some HTML { + let context: Context + var vendor: Vendor? { context.vendor } + + init( + _ context: Context + ) { + self.context = context + } + + init() { self.init(.float(nil)) } + + enum Context { + case float(Vendor? = nil, shouldShow: Bool = false) + case formOnly(Vendor) + + var vendor: Vendor? { + switch self { + case let .float(vendor, _): return vendor + case let .formOnly(vendor): return vendor + } + } + } + + var content: some HTML { + switch context { + case let .float(vendor, shouldDisplay): + Float(shouldDisplay: shouldDisplay) { + makeForm(vendor: vendor) + } + case let .formOnly(vendor): + makeForm(vendor: vendor) + } + } + + func makeForm(vendor: Vendor?) -> some HTML { form( .id("vendor-form"), vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), - .hx.target("this"), + .hx.target("#float"), .hx.swap(.outerHTML) ) { div(.class("row")) { input( + .type(.text), + .class("col-9"), .id("vendor-name"), .name("name"), .value(vendor?.name ?? ""), .placeholder("Vendor Name"), .required ) - button(.type(.submit), .class("btn-primary")) { buttonLabel } + button( + .type(.submit), + .class("col-1 btn-primary"), + .style("float: right") + ) { buttonLabel } } } } private var buttonLabel: String { - guard vendor != nil else { return "Update" } - return "Create" + guard vendor != nil else { return "Create" } + return "Update" } var targetURL: String { diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift index 11dc800..77655e6 100644 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -8,9 +8,19 @@ struct VendorTable: HTML { var content: some HTML { table { thead { - th { "Name" } - th {} - th { Button.add() } + tr { + th { "Name" } + th { "Branches" } + th(.style("width: 100px;")) { + Button.add() + .attributes( + .style("padding: 0px 10px;"), + .hx.get("/vendors/create"), + .hx.target("#float"), + .hx.swap(.outerHTML) + ) + } + } } tbody(.id("vendor-table")) { for vendor in vendors { @@ -27,7 +37,16 @@ struct VendorTable: HTML { tr(.id("vendor_\(vendor.id)")) { td { vendor.name.capitalized } td { "(\(vendor.branches?.count ?? 0)) Branches" } - td {} + td { + Button.detail() + .attributes( + .style("padding-left: 15px;"), + .hx.get("/vendors/\(vendor.id)"), + .hx.target("#float"), + .hx.pushURL(true), + .hx.swap(.outerHTML) + ) + } } } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index c421f17..1a9f61f 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -8,9 +8,10 @@ import VaporElementary func routes(_ app: Application) throws { try app.register(collection: ApiController()) try app.register(collection: UserViewController()) + try app.register(collection: VendorViewController()) // try app.register(collection: ViewController()) - app.get("test") { _ in + app.get { _ in HTMLResponse { MainPage(displayNav: false, route: .purchaseOrders) { div(.class("container")) { diff --git a/Sources/DatabaseClient/Vendors.swift b/Sources/DatabaseClient/Vendors.swift index 0ff9d4f..a40804c 100644 --- a/Sources/DatabaseClient/Vendors.swift +++ b/Sources/DatabaseClient/Vendors.swift @@ -9,25 +9,35 @@ public extension DatabaseClient { public var create: @Sendable (Vendor.Create) async throws -> Vendor public var delete: @Sendable (Vendor.ID) async throws -> Void public var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor] - public var get: @Sendable (Vendor.ID, GetRequest) async throws -> Vendor? - public var update: @Sendable (Vendor.ID, Vendor.Update) async throws -> Vendor + public var get: @Sendable (Vendor.ID, GetRequest?) async throws -> Vendor? + public var update: @Sendable (Vendor.ID, Vendor.Update, GetRequest?) async throws -> Vendor - public enum FetchRequest { + public enum FetchRequest: Sendable { case all case withBranches } - public enum GetRequest { - case all + public enum GetRequest: Sendable { case withBranches } + @Sendable public func fetchAll() async throws -> [Vendor] { try await fetchAll(.all) } + @Sendable public func get(_ id: Vendor.ID) async throws -> Vendor? { - try await get(id, .all) + try await get(id, nil) + } + + @Sendable + public func update( + _ id: Vendor.ID, + with updates: Vendor.Update, + returnWithBranches: Bool = false + ) async throws -> Vendor { + try await update(id, updates, returnWithBranches ? GetRequest.withBranches : nil) } } } diff --git a/Sources/DatabaseClientLive/Vendors.swift b/Sources/DatabaseClientLive/Vendors.swift index 6334827..1854b1f 100644 --- a/Sources/DatabaseClientLive/Vendors.swift +++ b/Sources/DatabaseClientLive/Vendors.swift @@ -37,13 +37,20 @@ public extension DatabaseClient.Vendors { query = query.with(\.$branches) } return try await query.first().map { try $0.toDTO(includeBranches: withBranches) } - } update: { id, updates in + } update: { id, updates, withBranches in guard let model = try await VendorModel.find(id, on: db) else { throw NotFoundError() } try model.applyUpdates(updates) try await model.save(on: db) - return try model.toDTO() + if withBranches != .withBranches { + return try model.toDTO() + } + return try await VendorModel.query(on: db) + .filter(\.$id == id) + .with(\.$branches) + .first()! + .toDTO() } } }