feat: Initial purchase order views, login seems to be broken though.
This commit is contained in:
@@ -26,9 +26,11 @@ h1 { font-size: 2.5em; }
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
.header {
|
||||
position: sticky;
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--primary);
|
||||
top: 0;
|
||||
padding: 10px 0;
|
||||
height: 60px;
|
||||
border-bottom: 1px solid grey;
|
||||
@@ -97,6 +99,10 @@ form {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form .label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#user-form input {
|
||||
width: 100%;
|
||||
margin: 20px;
|
||||
@@ -142,6 +148,7 @@ select {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
option {
|
||||
@@ -291,11 +298,12 @@ a.toggle, a img.toggle {
|
||||
.float {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #14141f;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.float .closebtn {
|
||||
|
||||
@@ -28,8 +28,8 @@ struct PurchaseOrderApiController: RouteCollection {
|
||||
@Sendable
|
||||
func create(req: Request) async throws -> PurchaseOrder {
|
||||
try await purchaseOrders.create(
|
||||
req.content.decode(PurchaseOrder.Create.self),
|
||||
req.auth.require(User.self).id
|
||||
req.content.decode(PurchaseOrder.CreateIntermediate.self)
|
||||
.toCreate(createdByID: req.auth.require(User.self).id)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,196 +1,103 @@
|
||||
// import Dependencies
|
||||
// import Fluent
|
||||
// import Vapor
|
||||
//
|
||||
// struct PurchaseOrderViewController: RouteCollection {
|
||||
// @Dependency(\.employees) var employees
|
||||
// @Dependency(\.purchaseOrders) var purchaseOrders
|
||||
// @Dependency(\.vendorBranches) var vendorBranches
|
||||
//
|
||||
// func boot(routes: any RoutesBuilder) throws {
|
||||
// let pos = routes.protected.grouped("purchase-orders")
|
||||
//
|
||||
// pos.get(use: index(req:))
|
||||
// pos.group("details", "close") {
|
||||
// $0.get(use: detailClose(req:))
|
||||
// }
|
||||
// pos.post(use: create(req:))
|
||||
// pos.group(":id") {
|
||||
// $0.get(use: detail(req:))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Sendable
|
||||
// func index(req: Request) async throws -> View {
|
||||
// let params = try? req.query.decode(PurchaseOrderIndex.self)
|
||||
// let purchaseOrdersPage = try await purchaseOrders.fetchPage(
|
||||
// .init(page: params?.page ?? 1, per: params?.limit ?? 50)
|
||||
// )
|
||||
// let branches = try await vendorBranches.getBranches(req: req)
|
||||
// let employees = try await employees.fetchAll()
|
||||
// req.logger.debug("Branches: \(branches)")
|
||||
// return try await req.view.render(
|
||||
// "purchaseOrders/index",
|
||||
// PurchaseOrderCTX(
|
||||
// page: purchaseOrdersPage,
|
||||
// form: .create(branches: branches, employees: employees)
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// @Sendable
|
||||
// func detail(req: Request) async throws -> View {
|
||||
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
|
||||
// throw Abort(.badRequest, reason: "Id not supplied.")
|
||||
// }
|
||||
// let purchaseOrder = try await purchaseOrders.get(id)
|
||||
// return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder])
|
||||
// }
|
||||
//
|
||||
// @Sendable
|
||||
// func detailClose(req: Request) async throws -> View {
|
||||
// return try await req.view.render("purchaseOrders/detail")
|
||||
// }
|
||||
//
|
||||
// @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 purchaseOrders.create(create, createdById)
|
||||
// return try await req.view.render("purchaseOrders/table-row", purchaseOrder)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private struct PurchaseOrderIndex: Content {
|
||||
// let page: Int?
|
||||
// let limit: Int?
|
||||
// }
|
||||
//
|
||||
// private struct PurchaseOrderCTX: Content {
|
||||
// let purchaseOrderDetail: PurchaseOrder.DTO?
|
||||
// let purchaseOrders: [PurchaseOrder.DTO]
|
||||
// let page: Int
|
||||
// let limit: Int
|
||||
// let hasNext: Bool
|
||||
// let hasPrevious: Bool
|
||||
// let form: PurchaseOrderFormCTX?
|
||||
//
|
||||
// init(
|
||||
// detail: PurchaseOrder.DTO? = nil,
|
||||
// page: Page<PurchaseOrder.DTO>,
|
||||
// form: PurchaseOrderFormCTX?
|
||||
// ) {
|
||||
// self.purchaseOrderDetail = detail
|
||||
// self.purchaseOrders = page.items
|
||||
// self.page = page.metadata.page
|
||||
// self.limit = page.metadata.per
|
||||
// self.hasNext = page.metadata.hasNext
|
||||
// self.hasPrevious = page.metadata.page > 1
|
||||
// self.form = form
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private extension PageMetadata {
|
||||
// var hasNext: Bool {
|
||||
// total > (page * per)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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)
|
||||
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.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(PurchaseOrder.CreateIntermediate.self)
|
||||
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) }
|
||||
}
|
||||
|
||||
private func mainPage<C: HTML>(
|
||||
_ 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
|
||||
PurchaseOrderTable(page: page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IndexQuery: Content {
|
||||
let page: Int
|
||||
let limit: Int
|
||||
|
||||
static var `default`: Self {
|
||||
.init(page: 1, limit: 25)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,14 @@ extension RoutesBuilder {
|
||||
// Used to ensure views are protected, redirects users to the login page if they're
|
||||
// not authenticated.
|
||||
var protected: any RoutesBuilder {
|
||||
return self
|
||||
// return grouped(
|
||||
// UserPasswordAuthenticator(),
|
||||
// UserTokenAuthenticator(),
|
||||
// UserSessionAuthenticator(),
|
||||
// User.redirectMiddleware { req in
|
||||
// "login?next=\(req.url)"
|
||||
// }
|
||||
// )
|
||||
// return self
|
||||
return grouped(
|
||||
UserPasswordAuthenticator(),
|
||||
UserSessionAuthenticator(),
|
||||
User.redirectMiddleware { req in
|
||||
"login?next=\(req.url)"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func apiUnprotected(route: PathComponent) -> any RoutesBuilder {
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
var vendors: [Vendor] = []
|
||||
var vendorBranches: [VendorBranch] = []
|
||||
|
||||
let adminUser = User.Create(
|
||||
username: Environment.get("ADMIN_USERNAME") ?? "admin",
|
||||
email: Environment.get("ADMIN_EMAIL") ?? "admin@development.com",
|
||||
password: Environment.get("ADMIN_PASSWORD") ?? "super-secret",
|
||||
confirmPassword: Environment.get("ADMIN_PASSWORD") ?? "super-secret"
|
||||
)
|
||||
|
||||
_ = try await database.users.create(adminUser)
|
||||
|
||||
for user in User.Create.generateMocks() {
|
||||
let created = try await database.users.create(user)
|
||||
users.append(created)
|
||||
@@ -41,8 +50,13 @@
|
||||
vendorBranches.append(created)
|
||||
}
|
||||
|
||||
for purchaseOrder in PurchaseOrder.Create.generateMocks(employees: employees, vendorBranches: vendorBranches) {
|
||||
_ = try await database.purchaseOrders.create(purchaseOrder, users.randomElement()!.id)
|
||||
for purchaseOrder in PurchaseOrder.CreateIntermediate.generateMocks(
|
||||
employees: employees,
|
||||
vendorBranches: vendorBranches
|
||||
) {
|
||||
_ = try await database.purchaseOrders.create(
|
||||
purchaseOrder.toCreate(createdByID: users.randomElement()!.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
header {
|
||||
header(.class("header")) {
|
||||
Logo()
|
||||
if displayNav {
|
||||
Navbar()
|
||||
|
||||
161
Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift
Normal file
161
Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import SharedModels
|
||||
|
||||
struct PurchaseOrderForm: HTML {
|
||||
|
||||
let purchaseOrder: PurchaseOrder?
|
||||
let shouldShow: Bool
|
||||
|
||||
init(purchaseOrder: PurchaseOrder? = nil, shouldShow: Bool = false) {
|
||||
self.purchaseOrder = purchaseOrder
|
||||
self.shouldShow = shouldShow
|
||||
}
|
||||
|
||||
var content: some HTML {
|
||||
Float(shouldDisplay: shouldShow, resetURL: "/purchase-orders") {
|
||||
if shouldShow {
|
||||
if purchaseOrder != nil {
|
||||
p {
|
||||
span(.class("label"), .style("margin-right: 15px;")) { "Note:" }
|
||||
span { i(.style("font-size: 1em;")) {
|
||||
"Vendor and Employee can not be changed once a purchase order has been created."
|
||||
} }
|
||||
}
|
||||
}
|
||||
form(
|
||||
.hx.post("/purchase-orders"),
|
||||
.hx.target("purchase-order-table"),
|
||||
.hx.swap(.afterBegin.transition(true).swap("1s")),
|
||||
.custom(
|
||||
name: "hx-on::after-request",
|
||||
value: "if (event.detail.successful) toggleContent('float'); window.location.href='/purchase-orders';"
|
||||
)
|
||||
) {
|
||||
div(.class("row")) {
|
||||
label(
|
||||
.for("customer"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Customer:" }
|
||||
input(
|
||||
.type(.text), .class("col-3"),
|
||||
.name("customer"), .placeholder("Customer"),
|
||||
.value(purchaseOrder?.customer ?? ""),
|
||||
.required, .autofocus
|
||||
)
|
||||
label(
|
||||
.for("workOrder"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Work Order:" }
|
||||
input(
|
||||
.type(.text), .class("col-4"),
|
||||
.name("workOrder"), .placeholder("Work Order: (12345)"),
|
||||
.value("\(purchaseOrder?.workOrder != nil ? String(purchaseOrder!.workOrder!) : "")")
|
||||
)
|
||||
}
|
||||
div(.class("row")) {
|
||||
label(
|
||||
.for("materials"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Materials:" }
|
||||
input(
|
||||
.type(.text), .class("col-3"),
|
||||
.name("materials"), .placeholder("Materials"),
|
||||
.value(purchaseOrder?.materials ?? ""),
|
||||
.required
|
||||
)
|
||||
label(
|
||||
.for("vendorBranchID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Vendor:" }
|
||||
if purchaseOrder == nil {
|
||||
div(
|
||||
.class("col-4"),
|
||||
.hx.get("/purchase-orders/create/vendor-branch-select"),
|
||||
.hx.target("this"),
|
||||
.hx.swap(.outerHTML.transition(true).swap("0.5s")),
|
||||
.hx.trigger(.event(.revealed)),
|
||||
.hx.indicator(".hx-indicator")
|
||||
) {
|
||||
Img.spinner().attributes(.class("hx-indicator"), .style("float: left;"))
|
||||
}
|
||||
} else {
|
||||
input(
|
||||
.type(.text), .class("col-4"),
|
||||
.name("vendorBranchID"),
|
||||
.value("\(purchaseOrder!.vendorBranch.vendor.name) - \(purchaseOrder!.vendorBranch.name)"),
|
||||
.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
div(.class("row")) {
|
||||
label(
|
||||
.for("createdForID"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Employee:" }
|
||||
if purchaseOrder == nil {
|
||||
div(
|
||||
.class("col-3"),
|
||||
.hx.get("/purchase-orders/create/employee-select"),
|
||||
.hx.target("this"),
|
||||
.hx.swap(.outerHTML.transition(true).swap("0.5s")),
|
||||
.hx.trigger(.event(.revealed)),
|
||||
.hx.indicator(".hx-indicator")
|
||||
) {
|
||||
Img.spinner().attributes(.class("hx-indicator"), .style("float: left;"))
|
||||
}
|
||||
} else {
|
||||
input(
|
||||
.type(.text), .class("col-3"),
|
||||
.value(purchaseOrder!.createdFor.fullName),
|
||||
.disabled
|
||||
)
|
||||
}
|
||||
label(
|
||||
.for("truckStock"), .class("label col-2"), .style("margin-right: 15px; margin-bottom: 5px;")
|
||||
) { "Truck Stock:" }
|
||||
if purchaseOrder?.truckStock == true {
|
||||
input(
|
||||
.type(.checkbox), .class("col-2"), .name("truckStock"), .style("margin-top: 20px;"), .checked
|
||||
)
|
||||
} else {
|
||||
input(
|
||||
.type(.checkbox), .class("col-2"), .name("truckStock"), .style("margin-top: 20px;")
|
||||
)
|
||||
}
|
||||
}
|
||||
div(.class("btn-row")) {
|
||||
button(.class("btn-primary"), .type(.submit)) { buttonLabel }
|
||||
if purchaseOrder != nil {
|
||||
Button.danger { "Delete" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var buttonLabel: String {
|
||||
guard purchaseOrder != nil else { return "Create" }
|
||||
return "Update"
|
||||
}
|
||||
|
||||
struct VendorSelect: HTML {
|
||||
let vendorBranches: [VendorBranch.Detail]
|
||||
|
||||
var content: some HTML<HTMLTag.select> {
|
||||
select(.name("vendorBranchID"), .class("col-3")) {
|
||||
for branch in vendorBranches {
|
||||
option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmployeeSelect: HTML {
|
||||
let employees: [Employee]
|
||||
|
||||
var content: some HTML<HTMLTag.select> {
|
||||
select(.name("createdForID"), .class("col-3")) {
|
||||
for employee in employees {
|
||||
option(.value(employee.id.uuidString)) { employee.fullName }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift
Normal file
92
Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Fluent
|
||||
import SharedModels
|
||||
import Vapor
|
||||
|
||||
struct PurchaseOrderTable: HTML {
|
||||
|
||||
let page: Page<PurchaseOrder>
|
||||
|
||||
var content: some HTML {
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { "PO" }
|
||||
th { "Work Order" }
|
||||
th { "Customer" }
|
||||
th { "Vendor" }
|
||||
th { "Materials" }
|
||||
th { "Created For" }
|
||||
th {
|
||||
Button.add()
|
||||
.attributes(
|
||||
.hx.get("/purchase-orders/create"),
|
||||
.hx.target("#float"),
|
||||
.hx.swap(.outerHTML.transition(true).swap("1s")),
|
||||
.hx.pushURL(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody(.id("purchase-order-table")) {
|
||||
Rows(page: page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces only the rows for the given page
|
||||
struct Rows: HTML {
|
||||
let page: Page<PurchaseOrder>
|
||||
|
||||
var content: some HTML {
|
||||
for purchaseOrder in page.items {
|
||||
Row(purchaseOrder: purchaseOrder)
|
||||
}
|
||||
if page.metadata.pageCount > page.metadata.page {
|
||||
tr(
|
||||
.hx.get("/purchase-orders/next?page=\(page.metadata.page + 1)&limit=\(page.metadata.per)"),
|
||||
.hx.trigger(.event(.revealed)),
|
||||
.hx.swap(.outerHTML.transition(true).swap("1s")),
|
||||
.hx.target("this"),
|
||||
.hx.indicator(".htmx-indicator")
|
||||
) {
|
||||
img(.src("/images/spinner.svg"), .class("htmx-indicator"), .width(60), .height(60))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A single row.
|
||||
struct Row: HTML {
|
||||
let purchaseOrder: PurchaseOrder
|
||||
|
||||
var content: some HTML<HTMLTag.tr> {
|
||||
tr(
|
||||
.id("purchase_order_\(purchaseOrder.id)")
|
||||
) {
|
||||
td { "\(purchaseOrder.id)" }
|
||||
td { purchaseOrder.workOrder != nil ? String(purchaseOrder.workOrder!) : "" }
|
||||
td { purchaseOrder.customer }
|
||||
td { purchaseOrder.vendorBranch.displayName }
|
||||
td { purchaseOrder.materials }
|
||||
td { purchaseOrder.createdFor.fullName }
|
||||
td {
|
||||
Button.detail()
|
||||
.attributes(
|
||||
.hx.get("/purchase-orders/\(purchaseOrder.id)"),
|
||||
.hx.target("#float"),
|
||||
.hx.swap(.outerHTML.transition(true).swap("0.5s")),
|
||||
.hx.pushURL(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VendorBranch.Detail {
|
||||
var displayName: String {
|
||||
"\(vendor.name.capitalized) - \(name.capitalized)"
|
||||
}
|
||||
}
|
||||
7
Sources/App/Views/Utils/Img.swift
Normal file
7
Sources/App/Views/Utils/Img.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Elementary
|
||||
|
||||
enum Img {
|
||||
static func spinner(width: Int = 30, height: Int = 30) -> some HTML<HTMLTag.img> {
|
||||
img(.src("/images/spinner.svg"), .width(width), .height(height))
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import DatabaseClientLive
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import Fluent
|
||||
import SharedModels
|
||||
import Vapor
|
||||
import VaporElementary
|
||||
|
||||
@@ -10,7 +11,7 @@ func routes(_ app: Application) throws {
|
||||
try app.register(collection: UserViewController())
|
||||
try app.register(collection: VendorViewController())
|
||||
try app.register(collection: EmployeeViewController())
|
||||
// try app.register(collection: ViewController())
|
||||
try app.register(collection: PurchaseOrderViewController())
|
||||
|
||||
app.get { _ in
|
||||
HTMLResponse {
|
||||
@@ -22,20 +23,37 @@ func routes(_ app: Application) throws {
|
||||
}
|
||||
}
|
||||
|
||||
app.get("login") { _ in
|
||||
HTMLResponse {
|
||||
app.get("login") { req in
|
||||
let context = try req.query.decode(LoginContext.self)
|
||||
return await req.render {
|
||||
MainPage(displayNav: false, route: .login) {
|
||||
UserForm(context: .login(next: nil))
|
||||
UserForm(context: .login(next: context.next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// app.get("users") { _ in
|
||||
// HTMLResponse {
|
||||
// // UserIndex()
|
||||
// MainPage(displayNav: false, route: .users) {
|
||||
// UserTable()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
app.post("login") { req in
|
||||
@Dependency(\.database.users) var users
|
||||
let loginForm = try req.content.decode(User.Login.self)
|
||||
let token = try await users.login(loginForm)
|
||||
let user = try await users.get(token.userID)!
|
||||
req.session.authenticate(user)
|
||||
return try await PurchaseOrderViewController().index(req: req)
|
||||
}
|
||||
|
||||
let protected = app.grouped(UserPasswordAuthenticator(), UserSessionAuthenticator())
|
||||
|
||||
protected.get("me") { req in
|
||||
let user = try req.auth.require(User.self)
|
||||
|
||||
return HTMLResponse {
|
||||
MainPage(displayNav: false, route: .purchaseOrders) {
|
||||
h1 { "You are logged in as: \(user.username)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct LoginContext: Content {
|
||||
let next: String?
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Vapor
|
||||
public extension DatabaseClient {
|
||||
@DependencyClient
|
||||
struct PurchaseOrders: Sendable {
|
||||
public var create: @Sendable (PurchaseOrder.Create, User.ID) async throws -> PurchaseOrder
|
||||
public var create: @Sendable (PurchaseOrder.Create) async throws -> PurchaseOrder
|
||||
public var fetchAll: @Sendable () async throws -> [PurchaseOrder]
|
||||
public var fetchPage: @Sendable (PageRequest) async throws -> Page<PurchaseOrder>
|
||||
public var get: @Sendable (PurchaseOrder.ID) async throws -> PurchaseOrder?
|
||||
|
||||
@@ -9,6 +9,7 @@ public extension DatabaseClient {
|
||||
public var create: @Sendable (VendorBranch.Create) async throws -> VendorBranch
|
||||
public var delete: @Sendable (VendorBranch.ID) async throws -> Void
|
||||
public var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch]
|
||||
public var fetchAllWithDetail: @Sendable () async throws -> [VendorBranch.Detail]
|
||||
public var get: @Sendable (VendorBranch.ID) async throws -> VendorBranch?
|
||||
public var update: @Sendable (VendorBranch.ID, VendorBranch.Update) async throws -> VendorBranch
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import SharedModels
|
||||
public extension DatabaseClient.PurchaseOrders {
|
||||
|
||||
static func live(database: any Database) -> Self {
|
||||
.init { create, createdById in
|
||||
let model = try create.toModel(createdByID: createdById)
|
||||
.init { create in
|
||||
let model = try create.toModel()
|
||||
try await model.save(on: database)
|
||||
let fetched = try await PurchaseOrderModel.allQuery(on: database).filter(\.$id == model.id!).first()!
|
||||
return try fetched.toDTO()
|
||||
@@ -62,7 +62,7 @@ extension PurchaseOrder {
|
||||
|
||||
extension PurchaseOrder.Create {
|
||||
|
||||
func toModel(createdByID: User.ID) throws -> PurchaseOrderModel {
|
||||
func toModel() throws -> PurchaseOrderModel {
|
||||
try validate()
|
||||
return .init(
|
||||
materials: materials,
|
||||
@@ -157,7 +157,7 @@ final class PurchaseOrderModel: Model, Codable, @unchecked Sendable {
|
||||
truckStock: truckStock,
|
||||
createdBy: createdBy.toDTO(),
|
||||
createdFor: createdFor.toDTO(),
|
||||
vendorBranch: vendorBranch.toDTO(),
|
||||
vendorBranch: vendorBranch.toDetail(),
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt
|
||||
)
|
||||
|
||||
@@ -34,6 +34,11 @@ public extension DatabaseClient.VendorBranches {
|
||||
}
|
||||
|
||||
return try await query.all().map { try $0.toDTO() }
|
||||
} fetchAllWithDetail: {
|
||||
try await VendorBranchModel.query(on: db)
|
||||
.with(\.$vendor)
|
||||
.all()
|
||||
.map { try $0.toDetail() }
|
||||
} get: { id in
|
||||
try await VendorBranchModel.find(id, on: db).map { try $0.toDTO() }
|
||||
} update: { id, updates in
|
||||
@@ -147,6 +152,16 @@ final class VendorBranchModel: Model, @unchecked Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
func toDetail() throws -> VendorBranch.Detail {
|
||||
try .init(
|
||||
id: requireID(),
|
||||
name: name,
|
||||
vendor: vendor.toDTO(),
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
func applyUpdates(_ updates: VendorBranch.Update) throws {
|
||||
try updates.validate()
|
||||
if let name = updates.name {
|
||||
|
||||
@@ -24,6 +24,10 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
||||
self.lastName = lastName
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
public var fullName: String {
|
||||
"\(firstName.capitalized) \(lastName.capitalized)"
|
||||
}
|
||||
}
|
||||
|
||||
public extension Employee {
|
||||
|
||||
@@ -10,7 +10,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
||||
public var truckStock: Bool
|
||||
public var createdBy: User
|
||||
public var createdFor: Employee
|
||||
public var vendorBranch: VendorBranch
|
||||
public var vendorBranch: VendorBranch.Detail
|
||||
public var createdAt: Date?
|
||||
public var updatedAt: Date?
|
||||
|
||||
@@ -22,7 +22,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
||||
truckStock: Bool,
|
||||
createdBy: User,
|
||||
createdFor: Employee,
|
||||
vendorBranch: VendorBranch,
|
||||
vendorBranch: VendorBranch.Detail,
|
||||
createdAt: Date?,
|
||||
updatedAt: Date?
|
||||
) {
|
||||
@@ -41,9 +41,40 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public extension PurchaseOrder {
|
||||
|
||||
// TODO: Add created by id.
|
||||
struct Create: Codable, Sendable {
|
||||
|
||||
public let id: Int?
|
||||
public let workOrder: Int?
|
||||
public let materials: String
|
||||
public let customer: String
|
||||
public let truckStock: Bool?
|
||||
public let createdByID: User.ID
|
||||
public let createdForID: Employee.ID
|
||||
public let vendorBranchID: VendorBranch.ID
|
||||
|
||||
public init(
|
||||
id: Int? = nil,
|
||||
workOrder: Int? = nil,
|
||||
materials: String,
|
||||
customer: String,
|
||||
truckStock: Bool? = nil,
|
||||
createdByID: User.ID,
|
||||
createdForID: Employee.ID,
|
||||
vendorBranchID: VendorBranch.ID
|
||||
) {
|
||||
self.id = id
|
||||
self.workOrder = workOrder
|
||||
self.materials = materials
|
||||
self.customer = customer
|
||||
self.truckStock = truckStock
|
||||
self.createdByID = createdByID
|
||||
self.createdForID = createdForID
|
||||
self.vendorBranchID = vendorBranchID
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateIntermediate: Codable, Sendable {
|
||||
|
||||
public let id: Int?
|
||||
public let workOrder: Int?
|
||||
public let materials: String
|
||||
@@ -69,44 +100,26 @@ public extension PurchaseOrder {
|
||||
self.createdForID = createdForID
|
||||
self.vendorBranchID = vendorBranchID
|
||||
}
|
||||
|
||||
public func toCreate(createdByID userID: User.ID) -> PurchaseOrder.Create {
|
||||
.init(
|
||||
id: id,
|
||||
workOrder: workOrder,
|
||||
materials: materials,
|
||||
customer: customer,
|
||||
truckStock: truckStock,
|
||||
createdByID: userID,
|
||||
createdForID: createdForID,
|
||||
vendorBranchID: vendorBranchID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// public extension PurchaseOrder {
|
||||
|
||||
// static func generateMocks(
|
||||
// count: Int = 50,
|
||||
// employees: [Employee],
|
||||
// users: [User],
|
||||
// vendorBranches: [VendorBranch]
|
||||
// ) -> [Self] {
|
||||
// @Dependency(\.date.now) var now
|
||||
// @Dependency(\.uuid) var uuid
|
||||
//
|
||||
// var output = [Self]()
|
||||
//
|
||||
// for id in 0 ... count {
|
||||
// output.append(.init(
|
||||
// id: id,
|
||||
// workOrder: Int.random(in: 0 ... 100),
|
||||
// materials: "Some thing",
|
||||
// customer: "\(RandomNames.firstNames.randomElement()!) \(RandomNames.lastNames.randomElement()!)",
|
||||
// truckStock: Bool.random(),
|
||||
// createdBy: users.randomElement()!,
|
||||
// createdFor: employees.randomElement()!,
|
||||
// vendorBranch: vendorBranches.randomElement()!,
|
||||
// createdAt: now,
|
||||
// updatedAt: now
|
||||
// ))
|
||||
// }
|
||||
//
|
||||
// return output
|
||||
// }
|
||||
// }
|
||||
|
||||
public extension PurchaseOrder.Create {
|
||||
public extension PurchaseOrder.CreateIntermediate {
|
||||
static func generateMocks(
|
||||
count: Int = 50,
|
||||
employees: [Employee],
|
||||
|
||||
@@ -34,6 +34,28 @@ public extension VendorBranch {
|
||||
}
|
||||
}
|
||||
|
||||
struct Detail: Codable, Equatable, Identifiable, Sendable {
|
||||
public var id: UUID
|
||||
public var name: String
|
||||
public var vendor: Vendor
|
||||
public var createdAt: Date?
|
||||
public var updatedAt: Date?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
name: String,
|
||||
vendor: Vendor,
|
||||
createdAt: Date? = nil,
|
||||
updatedAt: Date? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.vendor = vendor
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
struct Update: Codable, Sendable {
|
||||
public let name: String?
|
||||
|
||||
@@ -44,30 +66,6 @@ public extension VendorBranch {
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// public extension VendorBranch {
|
||||
//
|
||||
// static func generateMocks(countPerVendor: Int = 3, vendors: [Vendor]) -> [Self] {
|
||||
// @Dependency(\.date.now) var now
|
||||
// @Dependency(\.uuid) var uuid
|
||||
//
|
||||
// var output = [Self]()
|
||||
//
|
||||
// for vendor in vendors {
|
||||
// for _ in 0 ... countPerVendor {
|
||||
// output.append(.init(
|
||||
// id: uuid(),
|
||||
// name: RandomNames.cityNames.randomElement()!,
|
||||
// vendorID: vendor.id,
|
||||
// createdAt: now,
|
||||
// updatedAt: now
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return output
|
||||
// }
|
||||
// }
|
||||
|
||||
public extension VendorBranch.Create {
|
||||
|
||||
static func generateMocks(countPerVendor: Int = 3, vendors: [Vendor]) -> [Self] {
|
||||
|
||||
Reference in New Issue
Block a user