feat: Initial purchase order views, login seems to be broken though.

This commit is contained in:
2025-01-17 12:58:32 -05:00
parent e1d07008a1
commit be0b5a6033
18 changed files with 534 additions and 288 deletions

View File

@@ -26,9 +26,11 @@ h1 { font-size: 2.5em; }
overflow: hidden; overflow: hidden;
} }
header { .header {
position: sticky;
background-color: var(--dark-bg); background-color: var(--dark-bg);
color: var(--primary); color: var(--primary);
top: 0;
padding: 10px 0; padding: 10px 0;
height: 60px; height: 60px;
border-bottom: 1px solid grey; border-bottom: 1px solid grey;
@@ -97,6 +99,10 @@ form {
text-align: center; text-align: center;
} }
form .label {
text-align: right;
}
#user-form input { #user-form input {
width: 100%; width: 100%;
margin: 20px; margin: 20px;
@@ -142,6 +148,7 @@ select {
padding: 10px 20px; padding: 10px 20px;
width: 100%; width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 1.2em;
} }
option { option {
@@ -291,11 +298,12 @@ a.toggle, a img.toggle {
.float { .float {
z-index: 1; z-index: 1;
position: absolute; position: absolute;
top: 60px; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
background-color: #14141f; background-color: #14141f;
padding: 20px; padding: 20px;
display: flex;
} }
.float .closebtn { .float .closebtn {

View File

@@ -28,8 +28,8 @@ struct PurchaseOrderApiController: RouteCollection {
@Sendable @Sendable
func create(req: Request) async throws -> PurchaseOrder { func create(req: Request) async throws -> PurchaseOrder {
try await purchaseOrders.create( try await purchaseOrders.create(
req.content.decode(PurchaseOrder.Create.self), req.content.decode(PurchaseOrder.CreateIntermediate.self)
req.auth.require(User.self).id .toCreate(createdByID: req.auth.require(User.self).id)
) )
} }

View File

@@ -1,196 +1,103 @@
// import Dependencies import Dependencies
// import Fluent import Elementary
// import Vapor import SharedModels
// import Vapor
// struct PurchaseOrderViewController: RouteCollection { import VaporElementary
// @Dependency(\.employees) var employees
// @Dependency(\.purchaseOrders) var purchaseOrders struct PurchaseOrderViewController: RouteCollection {
// @Dependency(\.vendorBranches) var vendorBranches @Dependency(\.database.employees) var employees
// @Dependency(\.database.vendorBranches) var vendorBranches
// func boot(routes: any RoutesBuilder) throws { @Dependency(\.database.purchaseOrders) var purchaseOrders
// let pos = routes.protected.grouped("purchase-orders")
// func boot(routes: any RoutesBuilder) throws {
// pos.get(use: index(req:)) let route = routes.protected.grouped("purchase-orders")
// pos.group("details", "close") { route.get(use: index)
// $0.get(use: detailClose(req:)) route.get("next", use: nextPage)
// } route.post(use: create(req:))
// pos.post(use: create(req:)) route.group("create") {
// pos.group(":id") { $0.get(use: form)
// $0.get(use: detail(req:)) $0.get("vendor-branch-select", use: vendorBranchSelect(req:))
// } $0.get("employee-select", use: employeeSelect(req:))
// } }
// route.group(":id") {
// @Sendable $0.get(use: get)
// 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) @Sendable
// ) func index(req: Request) async throws -> HTMLResponse {
// let branches = try await vendorBranches.getBranches(req: req) let page = try? req.query.decode(IndexQuery.self)
// let employees = try await employees.fetchAll() return try await req.render { try await mainPage(PurchaseOrderForm(), page: page ?? .default) }
// req.logger.debug("Branches: \(branches)") }
// return try await req.view.render(
// "purchaseOrders/index", @Sendable
// PurchaseOrderCTX( func nextPage(req: Request) async throws -> HTMLResponse {
// page: purchaseOrdersPage, let query = try req.query.decode(IndexQuery.self)
// form: .create(branches: branches, employees: employees) let page = try await purchaseOrders.fetchPage(.init(page: query.page, per: query.limit))
// ) return await req.render { PurchaseOrderTable.Rows(page: page) }
// ) }
// }
// @Sendable
// @Sendable func form(req: Request) async throws -> HTMLResponse {
// func detail(req: Request) async throws -> View { // guard req.isHtmxRequest else {
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { // return try await req.render {
// throw Abort(.badRequest, reason: "Id not supplied.") // try await mainPage(PurchaseOrderForm(shouldShow: true), page: .default)
// } // }
// let purchaseOrder = try await purchaseOrders.get(id) // }
// return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder]) return await req.render { PurchaseOrderForm(shouldShow: true) }
// } }
//
// @Sendable @Sendable
// func detailClose(req: Request) async throws -> View { func vendorBranchSelect(req: Request) async throws -> HTMLResponse {
// return try await req.view.render("purchaseOrders/detail") let branches = try await vendorBranches.fetchAllWithDetail()
// } return await req.render { PurchaseOrderForm.VendorSelect(vendorBranches: branches) }
// }
// @Sendable
// func create(req: Request) async throws -> View { @Sendable
// try PurchaseOrder.FormCreate.validate(content: req) func employeeSelect(req: Request) async throws -> HTMLResponse {
// let createdById = try req.auth.require(User.self).requireID() let employees = try await self.employees.fetchAll()
// let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate() return await req.render { PurchaseOrderForm.EmployeeSelect(employees: employees) }
// let purchaseOrder = try await purchaseOrders.create(create, createdById) }
// return try await req.view.render("purchaseOrders/table-row", purchaseOrder)
// } @Sendable
// } func get(req: Request) async throws -> HTMLResponse {
// let purchaseOrder = try await purchaseOrders.get(req.ensureIDPathComponent(as: Int.self))
// private struct PurchaseOrderIndex: Content { let form = PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true)
// let page: Int? guard req.isHtmxRequest else {
// let limit: Int? return try await req.render {
// } try await mainPage(form, page: .default)
// }
// private struct PurchaseOrderCTX: Content { }
// let purchaseOrderDetail: PurchaseOrder.DTO? return await req.render { form }
// let purchaseOrders: [PurchaseOrder.DTO] }
// let page: Int
// let limit: Int @Sendable
// let hasNext: Bool func create(req: Request) async throws -> HTMLResponse {
// let hasPrevious: Bool let create = try req.content.decode(PurchaseOrder.CreateIntermediate.self)
// let form: PurchaseOrderFormCTX? let user = try req.auth.require(User.self)
// let purchaseOrder = try await purchaseOrders.create(create.toCreate(createdByID: user.id))
// init( return await req.render { PurchaseOrderTable.Row(purchaseOrder: purchaseOrder) }
// detail: PurchaseOrder.DTO? = nil, }
// page: Page<PurchaseOrder.DTO>,
// form: PurchaseOrderFormCTX? private func mainPage<C: HTML>(
// ) { _ html: C,
// self.purchaseOrderDetail = detail page: IndexQuery
// self.purchaseOrders = page.items ) async throws -> some SendableHTMLDocument where C: Sendable {
// self.page = page.metadata.page let page = try await purchaseOrders.fetchPage(.init(page: page.page, per: page.limit))
// self.limit = page.metadata.per return MainPage(displayNav: true, route: .purchaseOrders) {
// self.hasNext = page.metadata.hasNext div(.class("container")) {
// self.hasPrevious = page.metadata.page > 1 html
// self.form = form PurchaseOrderTable(page: page)
// } }
// } }
// }
// private extension PageMetadata { }
// var hasNext: Bool {
// total > (page * per) struct IndexQuery: Content {
// } let page: Int
// } let limit: Int
//
// private struct PurchaseOrderFormCTX: Content { static var `default`: Self {
// .init(page: 1, limit: 25)
// 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

@@ -9,15 +9,14 @@ extension RoutesBuilder {
// Used to ensure views are protected, redirects users to the login page if they're // Used to ensure views are protected, redirects users to the login page if they're
// not authenticated. // not authenticated.
var protected: any RoutesBuilder { var protected: any RoutesBuilder {
return self // return self
// return grouped( return grouped(
// UserPasswordAuthenticator(), UserPasswordAuthenticator(),
// UserTokenAuthenticator(), UserSessionAuthenticator(),
// UserSessionAuthenticator(), User.redirectMiddleware { req in
// User.redirectMiddleware { req in "login?next=\(req.url)"
// "login?next=\(req.url)" }
// } )
// )
} }
func apiUnprotected(route: PathComponent) -> any RoutesBuilder { func apiUnprotected(route: PathComponent) -> any RoutesBuilder {

View File

@@ -21,6 +21,15 @@
var vendors: [Vendor] = [] var vendors: [Vendor] = []
var vendorBranches: [VendorBranch] = [] 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() { for user in User.Create.generateMocks() {
let created = try await database.users.create(user) let created = try await database.users.create(user)
users.append(created) users.append(created)
@@ -41,8 +50,13 @@
vendorBranches.append(created) vendorBranches.append(created)
} }
for purchaseOrder in PurchaseOrder.Create.generateMocks(employees: employees, vendorBranches: vendorBranches) { for purchaseOrder in PurchaseOrder.CreateIntermediate.generateMocks(
_ = try await database.purchaseOrders.create(purchaseOrder, users.randomElement()!.id) employees: employees,
vendorBranches: vendorBranches
) {
_ = try await database.purchaseOrders.create(
purchaseOrder.toCreate(createdByID: users.randomElement()!.id)
)
} }
} }
} }

View File

@@ -28,7 +28,7 @@ struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
} }
var body: some HTML { var body: some HTML {
header { header(.class("header")) {
Logo() Logo()
if displayNav { if displayNav {
Navbar() Navbar()

View 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 }
}
}
}
}
}

View 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)"
}
}

View 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))
}
}

View File

@@ -2,6 +2,7 @@ import DatabaseClientLive
import Dependencies import Dependencies
import Elementary import Elementary
import Fluent import Fluent
import SharedModels
import Vapor import Vapor
import VaporElementary import VaporElementary
@@ -10,7 +11,7 @@ func routes(_ app: Application) throws {
try app.register(collection: UserViewController()) try app.register(collection: UserViewController())
try app.register(collection: VendorViewController()) try app.register(collection: VendorViewController())
try app.register(collection: EmployeeViewController()) try app.register(collection: EmployeeViewController())
// try app.register(collection: ViewController()) try app.register(collection: PurchaseOrderViewController())
app.get { _ in app.get { _ in
HTMLResponse { HTMLResponse {
@@ -22,20 +23,37 @@ func routes(_ app: Application) throws {
} }
} }
app.get("login") { _ in app.get("login") { req in
HTMLResponse { let context = try req.query.decode(LoginContext.self)
return await req.render {
MainPage(displayNav: false, route: .login) { MainPage(displayNav: false, route: .login) {
UserForm(context: .login(next: nil)) UserForm(context: .login(next: context.next))
} }
} }
} }
// app.get("users") { _ in app.post("login") { req in
// HTMLResponse { @Dependency(\.database.users) var users
// // UserIndex() let loginForm = try req.content.decode(User.Login.self)
// MainPage(displayNav: false, route: .users) { let token = try await users.login(loginForm)
// UserTable() 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?
} }

View File

@@ -7,7 +7,7 @@ import Vapor
public extension DatabaseClient { public extension DatabaseClient {
@DependencyClient @DependencyClient
struct PurchaseOrders: Sendable { 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 fetchAll: @Sendable () async throws -> [PurchaseOrder]
public var fetchPage: @Sendable (PageRequest) async throws -> Page<PurchaseOrder> public var fetchPage: @Sendable (PageRequest) async throws -> Page<PurchaseOrder>
public var get: @Sendable (PurchaseOrder.ID) async throws -> PurchaseOrder? public var get: @Sendable (PurchaseOrder.ID) async throws -> PurchaseOrder?

View File

@@ -9,6 +9,7 @@ public extension DatabaseClient {
public var create: @Sendable (VendorBranch.Create) async throws -> VendorBranch public var create: @Sendable (VendorBranch.Create) async throws -> VendorBranch
public var delete: @Sendable (VendorBranch.ID) async throws -> Void public var delete: @Sendable (VendorBranch.ID) async throws -> Void
public var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch] 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 get: @Sendable (VendorBranch.ID) async throws -> VendorBranch?
public var update: @Sendable (VendorBranch.ID, VendorBranch.Update) async throws -> VendorBranch public var update: @Sendable (VendorBranch.ID, VendorBranch.Update) async throws -> VendorBranch

View File

@@ -6,8 +6,8 @@ import SharedModels
public extension DatabaseClient.PurchaseOrders { public extension DatabaseClient.PurchaseOrders {
static func live(database: any Database) -> Self { static func live(database: any Database) -> Self {
.init { create, createdById in .init { create in
let model = try create.toModel(createdByID: createdById) let model = try create.toModel()
try await model.save(on: database) try await model.save(on: database)
let fetched = try await PurchaseOrderModel.allQuery(on: database).filter(\.$id == model.id!).first()! let fetched = try await PurchaseOrderModel.allQuery(on: database).filter(\.$id == model.id!).first()!
return try fetched.toDTO() return try fetched.toDTO()
@@ -62,7 +62,7 @@ extension PurchaseOrder {
extension PurchaseOrder.Create { extension PurchaseOrder.Create {
func toModel(createdByID: User.ID) throws -> PurchaseOrderModel { func toModel() throws -> PurchaseOrderModel {
try validate() try validate()
return .init( return .init(
materials: materials, materials: materials,
@@ -157,7 +157,7 @@ final class PurchaseOrderModel: Model, Codable, @unchecked Sendable {
truckStock: truckStock, truckStock: truckStock,
createdBy: createdBy.toDTO(), createdBy: createdBy.toDTO(),
createdFor: createdFor.toDTO(), createdFor: createdFor.toDTO(),
vendorBranch: vendorBranch.toDTO(), vendorBranch: vendorBranch.toDetail(),
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt updatedAt: updatedAt
) )

View File

@@ -34,6 +34,11 @@ public extension DatabaseClient.VendorBranches {
} }
return try await query.all().map { try $0.toDTO() } 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 } get: { id in
try await VendorBranchModel.find(id, on: db).map { try $0.toDTO() } try await VendorBranchModel.find(id, on: db).map { try $0.toDTO() }
} update: { id, updates in } 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 { func applyUpdates(_ updates: VendorBranch.Update) throws {
try updates.validate() try updates.validate()
if let name = updates.name { if let name = updates.name {

View File

@@ -24,6 +24,10 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
self.lastName = lastName self.lastName = lastName
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
public var fullName: String {
"\(firstName.capitalized) \(lastName.capitalized)"
}
} }
public extension Employee { public extension Employee {

View File

@@ -10,7 +10,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
public var truckStock: Bool public var truckStock: Bool
public var createdBy: User public var createdBy: User
public var createdFor: Employee public var createdFor: Employee
public var vendorBranch: VendorBranch public var vendorBranch: VendorBranch.Detail
public var createdAt: Date? public var createdAt: Date?
public var updatedAt: Date? public var updatedAt: Date?
@@ -22,7 +22,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
truckStock: Bool, truckStock: Bool,
createdBy: User, createdBy: User,
createdFor: Employee, createdFor: Employee,
vendorBranch: VendorBranch, vendorBranch: VendorBranch.Detail,
createdAt: Date?, createdAt: Date?,
updatedAt: Date? updatedAt: Date?
) { ) {
@@ -41,9 +41,40 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
public extension PurchaseOrder { public extension PurchaseOrder {
// TODO: Add created by id.
struct Create: Codable, Sendable { 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 id: Int?
public let workOrder: Int? public let workOrder: Int?
public let materials: String public let materials: String
@@ -69,44 +100,26 @@ public extension PurchaseOrder {
self.createdForID = createdForID self.createdForID = createdForID
self.vendorBranchID = vendorBranchID 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 #if DEBUG
// public extension PurchaseOrder {
// static func generateMocks( public extension PurchaseOrder.CreateIntermediate {
// 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 {
static func generateMocks( static func generateMocks(
count: Int = 50, count: Int = 50,
employees: [Employee], employees: [Employee],

View File

@@ -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 { struct Update: Codable, Sendable {
public let name: String? public let name: String?
@@ -44,30 +66,6 @@ public extension VendorBranch {
} }
#if DEBUG #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 { public extension VendorBranch.Create {
static func generateMocks(countPerVendor: Int = 3, vendors: [Vendor]) -> [Self] { static func generateMocks(countPerVendor: Int = 3, vendors: [Vendor]) -> [Self] {

9
justfile Normal file
View File

@@ -0,0 +1,9 @@
seed:
swift run App seed
rm-seed:
rm -rf seed.sqlite
run:
./swift-dev