import Dependencies import Elementary import SharedModels import Vapor import VaporElementary struct PurchaseOrderViewController: RouteCollection { @Dependency(\.database.employees) var employees @Dependency(\.database.vendorBranches) var vendorBranches @Dependency(\.database.purchaseOrders) var purchaseOrders func boot(routes: any RoutesBuilder) throws { let route = routes.protected.grouped("purchase-orders") route.get(use: index) route.get("next", use: nextPage) route.post(use: create(req:)) route.post("search", use: postSearch) route.get("search", use: getSearch) route.group("create") { $0.get(use: form) $0.get("vendor-branch-select", use: vendorBranchSelect(req:)) $0.get("employee-select", use: employeeSelect(req:)) } route.group(":id") { $0.get(use: get) } } @Sendable func index(req: Request) async throws -> HTMLResponse { let page = try? req.query.decode(IndexQuery.self) return try await req.render { try await mainPage(PurchaseOrderForm(), page: page ?? .default) } } @Sendable func nextPage(req: Request) async throws -> HTMLResponse { let query = try req.query.decode(IndexQuery.self) let page = try await purchaseOrders.fetchPage(.init(page: query.page, per: query.limit)) return await req.render { PurchaseOrderTable.Rows(page: page) } } @Sendable func form(req: Request) async throws -> HTMLResponse { guard req.isHtmxRequest else { return try await req.render { try await mainPage(PurchaseOrderForm(shouldShow: true), page: .default) } } return await req.render { PurchaseOrderForm(shouldShow: true) } } @Sendable func vendorBranchSelect(req: Request) async throws -> HTMLResponse { let branches = try await vendorBranches.fetchAllWithDetail() return await req.render { PurchaseOrderForm.VendorSelect(vendorBranches: branches) } } @Sendable func employeeSelect(req: Request) async throws -> HTMLResponse { let employees = try await self.employees.fetchAll() return await req.render { PurchaseOrderForm.EmployeeSelect(employees: employees) } } @Sendable func get(req: Request) async throws -> HTMLResponse { let purchaseOrder = try await purchaseOrders.get(req.ensureIDPathComponent(as: Int.self)) let form = PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true) guard req.isHtmxRequest else { return try await req.render { try await mainPage(form, page: .default) } } return await req.render { form } } @Sendable func create(req: Request) async throws -> HTMLResponse { let create = try req.content.decode(CreateContext.self).toIntermediate() let user = try req.auth.require(User.self) let purchaseOrder = try await purchaseOrders.create(create.toCreate(createdByID: user.id)) return await req.render { PurchaseOrderTable.Row(purchaseOrder: purchaseOrder) } } @Sendable func postSearch(req: Request) async throws -> HTMLResponse { let query = try req.content.decode([String: String].self) req.logger.info("query: \(query)") let context = try req.content.decode(SearchContext.self).toSearch() let purchaseOrders = try await purchaseOrders.search(context) req.logger.info("\(purchaseOrders)") return await req.render { PurchaseOrderTable.Rows(page: purchaseOrders) } } @Sendable func getSearch(req: Request) async throws -> HTMLResponse { let context = try req.query.decode(SearchQuery.self).toSearchContext() return await req.render { PurchaseOrderSearch(context: context) } } private func mainPage( _ html: C, page: IndexQuery ) async throws -> some SendableHTMLDocument where C: Sendable { let page = try await purchaseOrders.fetchPage(.init(page: page.page, per: page.limit)) return MainPage(displayNav: true, route: .purchaseOrders) { div(.class("container")) { html PurchaseOrderSearch() PurchaseOrderTable(page: page) } } } } struct IndexQuery: Content { let page: Int let limit: Int static var `default`: Self { .init(page: 1, limit: 25) } } private struct CreateContext: Content { let id: Int? let workOrder: String let materials: String let customer: String let truckStock: Bool? let createdForID: Employee.ID let vendorBranchID: VendorBranch.ID func toIntermediate() -> PurchaseOrder.CreateIntermediate { .init( id: id, workOrder: workOrder.isEmpty ? nil : Int(workOrder), materials: materials, customer: customer, truckStock: truckStock, createdForID: createdForID, vendorBranchID: vendorBranchID ) } } private struct SearchContext: Content { let context: String let search: String func toSearch() throws -> PurchaseOrder.SearchContext { switch context { case "employee": return .employee(search) case "customer": return .customer(search) case "vendor": return .vendor(search) default: throw Abort(.badRequest, reason: "Invalid search context.") } } } struct SearchQuery: Content { let context: String func toSearchContext() throws -> PurchaseOrderSearchContext { guard let context = PurchaseOrderSearchContext(rawValue: context) else { throw Abort(.badRequest, reason: "Invalid context.") } return context } }