Compare commits
11 Commits
f3ffdbf41b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
2811e6142b
|
|||
|
701e942710
|
|||
| a76d523541 | |||
|
027c7037a6
|
|||
|
5da433b815
|
|||
|
f6c36cb489
|
|||
|
b2108e2742
|
|||
|
981ad30adc
|
|||
|
2af978ab20
|
|||
|
a7df4f349f
|
|||
| 9478fae371 |
@@ -2,6 +2,11 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ jobs:
|
|||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=sha
|
type=sha
|
||||||
|
type=raw,value=prod
|
||||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If
|
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If
|
||||||
# the build succeeds, it pushes the image to GitHub Packages. It uses the `context` parameter to define the build's context
|
# the build succeeds, it pushes the image to GitHub Packages. It uses the `context` parameter to define the build's context
|
||||||
# as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)"
|
# as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)"
|
||||||
|
|||||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# vapor-po
|
||||||
|
|
||||||
|
The website for generating purchase orders.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Generally the app should be ran through a docker container or docker-compose file. Examples are in
|
||||||
|
the `./docker` folder.
|
||||||
|
|
||||||
|
Images get built in the `CI` environment when a tag is pushed to the repository.
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
When the application is first launched an admin user should be created in the running container.
|
||||||
|
Attach to the container using `docker exec` or `docker compose exec`, then run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./App generate-admin --username "admin" --password "super-secret --confirmPassword "super-secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then login and generate user, employees, vendors, etc.
|
||||||
|
|
||||||
|
After the setup has been completed, then you should generate a mock purchase order and set the `id`
|
||||||
|
to the value you would like new purchase orders to start from. This should be done through calling
|
||||||
|
the api, as the web interface does not allow users to enter an id value.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
These examples use `httpie`, note the port is used for local development, in a production
|
||||||
|
environment you would just use the FQDN of where the application is running.
|
||||||
|
|
||||||
|
**Login**
|
||||||
|
|
||||||
|
```
|
||||||
|
http :8080/api/v1/login username="admin" password="super-secret" \
|
||||||
|
| jq '.["token"]' \
|
||||||
|
| pbcopy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Set the token as environment variable**
|
||||||
|
|
||||||
|
```
|
||||||
|
export API_TOKEN=<clipboard contents>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get the employees to copy an id to use for the purchase order**
|
||||||
|
|
||||||
|
```
|
||||||
|
http -A bearer -a "$API_TOKEN" :8080/api/v1/employees
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get the vendor branches to copy an id to use for the purchase order**
|
||||||
|
|
||||||
|
```
|
||||||
|
http -A bearer -a "$API_TOKEN" :8080/api/v1/vendors/branches
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate first po**
|
||||||
|
|
||||||
|
```
|
||||||
|
http -A bearer -a "$API_TOKEN" :8080/api/v1/purchase-orders \
|
||||||
|
id:="60000" \
|
||||||
|
materials="Test" \
|
||||||
|
customer="Testy McTestface" \
|
||||||
|
createdForID="<employee-id>"
|
||||||
|
vendorBranchID="<vendor-branch-id>"
|
||||||
|
```
|
||||||
@@ -10,7 +10,10 @@ extension ViewController {
|
|||||||
for: route,
|
for: route,
|
||||||
isHtmxRequest: request.isHtmxRequest,
|
isHtmxRequest: request.isHtmxRequest,
|
||||||
logger: request.logger,
|
logger: request.logger,
|
||||||
authenticate: { request.session.authenticate($0) }
|
authenticate: { request.session.authenticate($0) },
|
||||||
|
currentUser: {
|
||||||
|
try request.auth.require(User.self)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return AnyHTMLResponse(value: html)
|
return AnyHTMLResponse(value: html)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public extension DatabaseClient.Employees {
|
|||||||
guard let model = try await EmployeeModel.find(id, on: database) else {
|
guard let model = try await EmployeeModel.find(id, on: database) else {
|
||||||
throw NotFoundError()
|
throw NotFoundError()
|
||||||
}
|
}
|
||||||
|
database.logger.debug("Applying updates to employee: \(updates)")
|
||||||
try model.applyUpdate(updates)
|
try model.applyUpdate(updates)
|
||||||
try await model.save(on: database)
|
try await model.save(on: database)
|
||||||
return try model.toDTO()
|
return try model.toDTO()
|
||||||
@@ -170,8 +171,10 @@ final class EmployeeModel: Model, @unchecked Sendable {
|
|||||||
if let lastName = updates.lastName {
|
if let lastName = updates.lastName {
|
||||||
self.lastName = lastName
|
self.lastName = lastName
|
||||||
}
|
}
|
||||||
if let active = updates.active {
|
// NB: When html forms are submitted with a checkbox then active is nil
|
||||||
self.active = active
|
// in the update context.
|
||||||
|
if active, updates.active == nil || updates.active == false {
|
||||||
|
active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ extension PurchaseOrder.Create {
|
|||||||
func toModel() throws -> PurchaseOrderModel {
|
func toModel() throws -> PurchaseOrderModel {
|
||||||
try validate()
|
try validate()
|
||||||
return .init(
|
return .init(
|
||||||
|
id: id,
|
||||||
|
workOrder: workOrder,
|
||||||
materials: materials,
|
materials: materials,
|
||||||
customer: customer,
|
customer: customer,
|
||||||
truckStock: truckStock ?? false,
|
truckStock: truckStock ?? false,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents an employee database model.
|
||||||
|
///
|
||||||
|
/// Employee's are who purchase orders can be generated for.
|
||||||
public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
||||||
public var id: UUID
|
public var id: UUID
|
||||||
public var active: Bool
|
public var active: Bool
|
||||||
@@ -31,6 +34,8 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public extension Employee {
|
public extension Employee {
|
||||||
|
/// Represents the required fields for generating a new employee in the
|
||||||
|
/// database.
|
||||||
struct Create: Codable, Sendable, Equatable {
|
struct Create: Codable, Sendable, Equatable {
|
||||||
public let firstName: String
|
public let firstName: String
|
||||||
public let lastName: String
|
public let lastName: String
|
||||||
@@ -47,6 +52,8 @@ public extension Employee {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the required fields for updating an existing employee in the
|
||||||
|
/// database.
|
||||||
struct Update: Codable, Sendable, Equatable {
|
struct Update: Codable, Sendable, Equatable {
|
||||||
public let firstName: String?
|
public let firstName: String?
|
||||||
public let lastName: String?
|
public let lastName: String?
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a purchase order database model.
|
||||||
|
///
|
||||||
|
/// A purchase order is generated on behalf of an `Employee` and issued to
|
||||||
|
/// a `VendorBranch`. It includes information about the customer / job it was created
|
||||||
|
/// for, the materials that were purchased, etc.
|
||||||
public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
||||||
|
|
||||||
public let id: Int
|
public let id: Int
|
||||||
@@ -41,6 +46,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
|||||||
|
|
||||||
public extension PurchaseOrder {
|
public extension PurchaseOrder {
|
||||||
|
|
||||||
|
/// Represents the required fields for generating a new purchase order in the database.
|
||||||
struct Create: Codable, Sendable, Equatable {
|
struct Create: Codable, Sendable, Equatable {
|
||||||
|
|
||||||
public let id: Int?
|
public let id: Int?
|
||||||
@@ -73,6 +79,9 @@ public extension PurchaseOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the required fields for generating a new purchase order in the database,
|
||||||
|
/// without the user information who is issuing the request, which get's parsed from the
|
||||||
|
/// currently authenticated user's session and is used to generate the full `Create` request.
|
||||||
struct CreateIntermediate: Codable, Sendable, Equatable {
|
struct CreateIntermediate: Codable, Sendable, Equatable {
|
||||||
|
|
||||||
public let id: Int?
|
public let id: Int?
|
||||||
@@ -115,10 +124,12 @@ public extension PurchaseOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the context to search or filter purchase orders based on the
|
||||||
|
/// given parameters.
|
||||||
enum SearchContext: Sendable, Equatable {
|
enum SearchContext: Sendable, Equatable {
|
||||||
case customer(String)
|
case customer(String)
|
||||||
case vendor(VendorBranch.ID)
|
|
||||||
case employee(Employee.ID)
|
case employee(Employee.ID)
|
||||||
|
case vendor(VendorBranch.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import Foundation
|
|||||||
|
|
||||||
public extension SiteRoute {
|
public extension SiteRoute {
|
||||||
|
|
||||||
|
/// Represents api routes that can be interacted with.
|
||||||
|
///
|
||||||
|
/// These routes return json information, as opposed to html like the view routes.
|
||||||
enum Api: Sendable, Equatable {
|
enum Api: Sendable, Equatable {
|
||||||
|
|
||||||
case employee(EmployeeRoute)
|
case employee(EmployeeRoute)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import CasePathsCore
|
|||||||
import Foundation
|
import Foundation
|
||||||
@preconcurrency import URLRouting
|
@preconcurrency import URLRouting
|
||||||
|
|
||||||
|
/// Represents all the routes that our server can handle.
|
||||||
public enum SiteRoute: Sendable {
|
public enum SiteRoute: Sendable {
|
||||||
case api(SiteRoute.Api)
|
case api(SiteRoute.Api)
|
||||||
case health
|
case health
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import Foundation
|
|||||||
|
|
||||||
public extension SiteRoute {
|
public extension SiteRoute {
|
||||||
// swiftlint:disable type_body_length
|
// swiftlint:disable type_body_length
|
||||||
|
|
||||||
|
/// Represents view routes that can be interacted with.
|
||||||
|
///
|
||||||
|
/// These routes return html and are used to generate the web interface.
|
||||||
enum View: Sendable, Equatable {
|
enum View: Sendable, Equatable {
|
||||||
|
|
||||||
case employee(SiteRoute.View.EmployeeRoute)
|
case employee(SiteRoute.View.EmployeeRoute)
|
||||||
@@ -131,7 +135,7 @@ public extension SiteRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum PurchaseOrderRoute: Sendable, Equatable {
|
public enum PurchaseOrderRoute: Sendable, Equatable {
|
||||||
case create(PurchaseOrder.Create)
|
case create(PurchaseOrder.CreateIntermediate)
|
||||||
case form
|
case form
|
||||||
case get(id: PurchaseOrder.ID)
|
case get(id: PurchaseOrder.ID)
|
||||||
case index
|
case index
|
||||||
@@ -151,11 +155,10 @@ public extension SiteRoute {
|
|||||||
Field("materials", .string)
|
Field("materials", .string)
|
||||||
Field("customer", .string)
|
Field("customer", .string)
|
||||||
Optionally { Field("truckStock") { Bool.parser() } }
|
Optionally { Field("truckStock") { Bool.parser() } }
|
||||||
Field("createdByID") { User.ID.parser() }
|
|
||||||
Field("createdForID") { Employee.ID.parser() }
|
Field("createdForID") { Employee.ID.parser() }
|
||||||
Field("vendorBranchID") { VendorBranch.ID.parser() }
|
Field("vendorBranchID") { VendorBranch.ID.parser() }
|
||||||
}
|
}
|
||||||
.map(.memberwise(PurchaseOrder.Create.init))
|
.map(.memberwise(PurchaseOrder.CreateIntermediate.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.form)) {
|
Route(.case(Self.form)) {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a user database model.
|
||||||
|
///
|
||||||
|
/// User's are who can login to the system and generate purchase orders, manage
|
||||||
|
/// employees, vendors, etc.
|
||||||
|
///
|
||||||
public struct User: Codable, Equatable, Identifiable, Sendable {
|
public struct User: Codable, Equatable, Identifiable, Sendable {
|
||||||
|
|
||||||
public var id: UUID
|
public var id: UUID
|
||||||
@@ -26,6 +31,7 @@ public struct User: Codable, Equatable, Identifiable, Sendable {
|
|||||||
|
|
||||||
public extension User {
|
public extension User {
|
||||||
|
|
||||||
|
/// Represents the fields needed to generate a new user in the database.
|
||||||
struct Create: Codable, Sendable, Equatable {
|
struct Create: Codable, Sendable, Equatable {
|
||||||
public let username: String
|
public let username: String
|
||||||
public let email: String
|
public let email: String
|
||||||
@@ -45,6 +51,7 @@ public extension User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the fields needed for new user to login.
|
||||||
struct Login: Codable, Sendable, Equatable {
|
struct Login: Codable, Sendable, Equatable {
|
||||||
public let username: String?
|
public let username: String?
|
||||||
public let email: String?
|
public let email: String?
|
||||||
@@ -61,6 +68,7 @@ public extension User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the fields needed to reset the password of a user.
|
||||||
struct ResetPassword: Codable, Equatable, Sendable {
|
struct ResetPassword: Codable, Equatable, Sendable {
|
||||||
public let password: String
|
public let password: String
|
||||||
public let confirmPassword: String
|
public let confirmPassword: String
|
||||||
@@ -74,6 +82,8 @@ public extension User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a user token that can be used to authenticate a user, typically
|
||||||
|
/// used for interacting with api routes remotely.
|
||||||
struct Token: Codable, Equatable, Identifiable, Sendable {
|
struct Token: Codable, Equatable, Identifiable, Sendable {
|
||||||
public let id: UUID
|
public let id: UUID
|
||||||
public let userID: User.ID
|
public let userID: User.ID
|
||||||
@@ -90,6 +100,7 @@ public extension User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the fields needed to update a user's attributes in the database.
|
||||||
struct Update: Codable, Equatable, Sendable {
|
struct Update: Codable, Equatable, Sendable {
|
||||||
public let username: String?
|
public let username: String?
|
||||||
public let email: String?
|
public let email: String?
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a vendor item in the database.
|
||||||
|
///
|
||||||
|
/// A vendor is parent item that contains one or more branches where purchase orders
|
||||||
|
/// can be issued to. It is primarily a name space to group related branches together.
|
||||||
public struct Vendor: Codable, Equatable, Identifiable, Sendable {
|
public struct Vendor: Codable, Equatable, Identifiable, Sendable {
|
||||||
public var id: UUID
|
public var id: UUID
|
||||||
public var name: String
|
public var name: String
|
||||||
@@ -25,6 +29,7 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable {
|
|||||||
|
|
||||||
public extension Vendor {
|
public extension Vendor {
|
||||||
|
|
||||||
|
/// Represents the fields required to generate a new vendor in the database.
|
||||||
struct Create: Codable, Sendable, Equatable {
|
struct Create: Codable, Sendable, Equatable {
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|
||||||
@@ -33,6 +38,7 @@ public extension Vendor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the fields required to update a vendor in the database.
|
||||||
struct Update: Codable, Sendable, Equatable {
|
struct Update: Codable, Sendable, Equatable {
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a vendor branch database model.
|
||||||
|
///
|
||||||
|
/// A vendor branch is who purchase orders can be issued to on behalf an `Employee`.
|
||||||
|
/// They are associated with a particular `Vendor`.
|
||||||
public struct VendorBranch: Codable, Equatable, Identifiable, Sendable {
|
public struct VendorBranch: Codable, Equatable, Identifiable, Sendable {
|
||||||
public var id: UUID
|
public var id: UUID
|
||||||
public var name: String
|
public var name: String
|
||||||
@@ -24,6 +28,8 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public extension VendorBranch {
|
public extension VendorBranch {
|
||||||
|
|
||||||
|
/// Represents the fields required to generate a new vendor branch in the database.
|
||||||
struct Create: Codable, Sendable, Equatable {
|
struct Create: Codable, Sendable, Equatable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let vendorID: Vendor.ID
|
public let vendorID: Vendor.ID
|
||||||
@@ -34,6 +40,10 @@ public extension VendorBranch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the details of a vendor branch, which includes the parent vendor item.
|
||||||
|
///
|
||||||
|
/// This is used in several of the views / api routes that require information about both the
|
||||||
|
/// vendor branch and it's associated parent vendor item.
|
||||||
struct Detail: Codable, Equatable, Identifiable, Sendable {
|
struct Detail: Codable, Equatable, Identifiable, Sendable {
|
||||||
public var id: UUID
|
public var id: UUID
|
||||||
public var name: String
|
public var name: String
|
||||||
@@ -56,6 +66,7 @@ public extension VendorBranch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the fields that are used to update attributes of a vendor branch in the database.
|
||||||
struct Update: Codable, Sendable, Equatable {
|
struct Update: Codable, Sendable, Equatable {
|
||||||
public let name: String?
|
public let name: String?
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public typealias AnySendableHTML = (any HTML & Sendable)
|
|||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ViewController: Sendable {
|
public struct ViewController: Sendable {
|
||||||
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
||||||
|
public typealias CurrentUserHandler = @Sendable () throws -> User
|
||||||
|
|
||||||
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||||
|
|
||||||
@@ -24,9 +25,16 @@ public struct ViewController: Sendable {
|
|||||||
for route: SiteRoute.View,
|
for route: SiteRoute.View,
|
||||||
isHtmxRequest: Bool,
|
isHtmxRequest: Bool,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
authenticate: @escaping AuthenticateHandler
|
authenticate: @escaping AuthenticateHandler,
|
||||||
|
currentUser: @escaping CurrentUserHandler
|
||||||
) async throws -> AnySendableHTML {
|
) async throws -> AnySendableHTML {
|
||||||
try await view(.init(route, isHtmxRequest: isHtmxRequest, authenticate: authenticate, logger: logger))
|
try await view(.init(
|
||||||
|
route,
|
||||||
|
isHtmxRequest: isHtmxRequest,
|
||||||
|
authenticate: authenticate,
|
||||||
|
logger: logger,
|
||||||
|
currentUser: currentUser
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Request: Sendable {
|
public struct Request: Sendable {
|
||||||
@@ -34,17 +42,20 @@ public struct ViewController: Sendable {
|
|||||||
public let isHtmxRequest: Bool
|
public let isHtmxRequest: Bool
|
||||||
public let authenticate: AuthenticateHandler
|
public let authenticate: AuthenticateHandler
|
||||||
public let logger: Logger
|
public let logger: Logger
|
||||||
|
public let currentUser: CurrentUserHandler
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
_ route: SiteRoute.View,
|
_ route: SiteRoute.View,
|
||||||
isHtmxRequest: Bool,
|
isHtmxRequest: Bool,
|
||||||
authenticate: @escaping AuthenticateHandler,
|
authenticate: @escaping AuthenticateHandler,
|
||||||
logger: Logger
|
logger: Logger,
|
||||||
|
currentUser: @escaping CurrentUserHandler
|
||||||
) {
|
) {
|
||||||
self.route = route
|
self.route = route
|
||||||
self.isHtmxRequest = isHtmxRequest
|
self.isHtmxRequest = isHtmxRequest
|
||||||
self.authenticate = authenticate
|
self.authenticate = authenticate
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
self.currentUser = currentUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ extension ViewController: DependencyKey {
|
|||||||
try await request.route.view(
|
try await request.route.view(
|
||||||
isHtmxRequest: request.isHtmxRequest,
|
isHtmxRequest: request.isHtmxRequest,
|
||||||
logger: request.logger,
|
logger: request.logger,
|
||||||
authenticate: request.authenticate
|
authenticate: request.authenticate,
|
||||||
|
currentUser: request.currentUser
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ public extension SiteRoute.View {
|
|||||||
func view(
|
func view(
|
||||||
isHtmxRequest: Bool,
|
isHtmxRequest: Bool,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
authenticate: @escaping @Sendable (User) -> Void
|
authenticate: @escaping @Sendable (User) -> Void,
|
||||||
|
currentUser: @escaping @Sendable () throws -> User
|
||||||
) async throws -> AnySendableHTML {
|
) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.users) var users
|
@Dependency(\.database.users) var users
|
||||||
switch self {
|
switch self {
|
||||||
// case .index:
|
|
||||||
// // This get's redirected to purchase-orders route in the app / site handler.
|
|
||||||
// return nil
|
|
||||||
|
|
||||||
case let .employee(route):
|
case let .employee(route):
|
||||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ public extension SiteRoute.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case let .purchaseOrder(route):
|
case let .purchaseOrder(route):
|
||||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
return try await route.view(isHtmxRequest: isHtmxRequest, currentUser: currentUser)
|
||||||
|
|
||||||
case let .resetPassword(route):
|
case let .resetPassword(route):
|
||||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||||
@@ -80,7 +77,7 @@ extension SiteRoute.View.EmployeeRoute {
|
|||||||
return try await render(mainPage, isHtmxRequest, EmployeeForm(shouldShow: true))
|
return try await render(mainPage, isHtmxRequest, EmployeeForm(shouldShow: true))
|
||||||
|
|
||||||
case let .select(context: context):
|
case let .select(context: context):
|
||||||
return try await context.toHTML(employees: employees.fetchAll())
|
return try await context.toHTML(employees: employees.fetchAll(.active))
|
||||||
|
|
||||||
case .index:
|
case .index:
|
||||||
return try await mainPage(EmployeeForm())
|
return try await mainPage(EmployeeForm())
|
||||||
@@ -116,7 +113,10 @@ extension SiteRoute.View.PurchaseOrderRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Sendable
|
@Sendable
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
func view(
|
||||||
|
isHtmxRequest: Bool,
|
||||||
|
currentUser: @escaping @Sendable () throws -> User
|
||||||
|
) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.purchaseOrders) var purchaseOrders
|
@Dependency(\.database.purchaseOrders) var purchaseOrders
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
@@ -129,7 +129,9 @@ extension SiteRoute.View.PurchaseOrderRoute {
|
|||||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||||
|
|
||||||
case let .create(purchaseOrder):
|
case let .create(purchaseOrder):
|
||||||
return try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder))
|
return try await PurchaseOrderTable.Row(
|
||||||
|
purchaseOrder: purchaseOrders.create(purchaseOrder.toCreate(createdByID: currentUser().id))
|
||||||
|
)
|
||||||
|
|
||||||
case .index:
|
case .index:
|
||||||
return try await mainPage(PurchaseOrderForm())
|
return try await mainPage(PurchaseOrderForm())
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ struct EmployeeForm: HTML {
|
|||||||
.placeholder("Last Name"), .required
|
.placeholder("Last Name"), .required
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if let employee {
|
||||||
|
div(.class("row"), .style("margin: 20px; float: left;")) {
|
||||||
|
label(.for("active"), .style("margin-right: 15px;")) { h2 { "Active" } }
|
||||||
|
if employee.active {
|
||||||
|
input(
|
||||||
|
.type(.checkbox), .id("active"), .name("active"), .checked
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
input(
|
||||||
|
.type(.checkbox), .id("active"), .name("active")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
div(.class("btn-row")) {
|
div(.class("btn-row")) {
|
||||||
button(.type(.submit), .class("btn-primary")) {
|
button(.type(.submit), .class("btn-primary")) {
|
||||||
buttonLabel
|
buttonLabel
|
||||||
@@ -59,6 +73,18 @@ struct EmployeeForm: HTML {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if employee != nil {
|
||||||
|
h3 {
|
||||||
|
i {
|
||||||
|
span(.class("primary"), .style("padding-right: 15px;")) {
|
||||||
|
"Note:"
|
||||||
|
}
|
||||||
|
span(.class("secondary")) {
|
||||||
|
"It is better to mark an employee as in-active instead of deleting them."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import SharedModels
|
||||||
|
|
||||||
|
struct PurchaseOrderFilter: HTML, Sendable {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
form(
|
||||||
|
.id("filter-form")
|
||||||
|
) {
|
||||||
|
input(.type(.text), .name("id"), .placeholder("Filter: (12345)"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ struct ViewControllerTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func employeeViews() async throws {
|
func employeeViews() async throws {
|
||||||
try await withSnapshotTesting(record: record) {
|
try await withSnapshotTesting(record: .missing) {
|
||||||
try await withDependencies {
|
try await withDependencies {
|
||||||
$0.viewController = .liveValue
|
$0.viewController = .liveValue
|
||||||
$0.database.employees = .mock
|
$0.database.employees = .mock
|
||||||
@@ -213,7 +213,8 @@ extension ViewController {
|
|||||||
for: route,
|
for: route,
|
||||||
isHtmxRequest: true,
|
isHtmxRequest: true,
|
||||||
logger: .init(label: "tests"),
|
logger: .init(label: "tests"),
|
||||||
authenticate: { _ in }
|
authenticate: { _ in },
|
||||||
|
currentUser: { .mock }
|
||||||
)
|
)
|
||||||
return html.renderFormatted()
|
return html.renderFormatted()
|
||||||
}
|
}
|
||||||
@@ -428,6 +429,17 @@ extension PurchaseOrder.Create {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PurchaseOrder.CreateIntermediate {
|
||||||
|
static var mock: Self {
|
||||||
|
.init(
|
||||||
|
materials: "bar",
|
||||||
|
customer: "Testy McTestface",
|
||||||
|
createdForID: UUID(0),
|
||||||
|
vendorBranchID: UUID(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension User.ResetPassword {
|
extension User.ResetPassword {
|
||||||
static var mock: Self {
|
static var mock: Self {
|
||||||
.init(password: "super-secret", confirmPassword: "super-secret")
|
.init(password: "super-secret", confirmPassword: "super-secret")
|
||||||
|
|||||||
@@ -8,9 +8,15 @@
|
|||||||
<div class="col-2"></div>
|
<div class="col-2"></div>
|
||||||
<input type="text" class="col-5" name="lastName" value="McTestface" placeholder="Last Name" required>
|
<input type="text" class="col-5" name="lastName" value="McTestface" placeholder="Last Name" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" style="margin: 20px; float: left;">
|
||||||
|
<label for="active" style="margin-right: 15px;">
|
||||||
|
<h2>Active</h2></label>
|
||||||
|
<input type="checkbox" id="active" name="active" checked>
|
||||||
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button type="submit" class="btn-primary">Update</button>
|
<button type="submit" class="btn-primary">Update</button>
|
||||||
<button class="danger" hx-confirm="Are you sure you want to delete this employee?" hx-delete="/api/v1/employees/00000000-0000-0000-0000-000000000000" hx-target="#employee-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s">Delete</button>
|
<button class="danger" hx-confirm="Are you sure you want to delete this employee?" hx-delete="/api/v1/employees/00000000-0000-0000-0000-000000000000" hx-target="#employee-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<h3><i><span class="primary" style="padding-right: 15px;">Note:</span><span class="secondary">It is better to mark an employee as in-active instead of deleting them.</span></i></h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,6 @@ struct PurchaseOrderViewRouteTests {
|
|||||||
materials: "some",
|
materials: "some",
|
||||||
customer: "Testy",
|
customer: "Testy",
|
||||||
truckStock: false,
|
truckStock: false,
|
||||||
createdByID: id,
|
|
||||||
createdForID: id,
|
createdForID: id,
|
||||||
vendorBranchID: id
|
vendorBranchID: id
|
||||||
))))
|
))))
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
|||||||
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
||||||
# libxml2 \
|
# libxml2 \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
|
curl \
|
||||||
&& rm -r /var/lib/apt/lists/*
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create a vapor user and group with /app as its home directory
|
# Create a vapor user and group with /app as its home directory
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: hhe-po:latest
|
image: hhe-po:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
|
dockerfile: ./docker/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
<<: *shared_environment
|
<<: *shared_environment
|
||||||
volumes:
|
volumes:
|
||||||
@@ -30,12 +31,20 @@ services:
|
|||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
labels:
|
labels:
|
||||||
- dev.orbstack.domains=po.local
|
- dev.orbstack.domains=po.local
|
||||||
|
healthcheck:
|
||||||
|
test: curl --fail -s http://0.0.0.0:8080/health || exit 1
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
|
# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
|
||||||
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||||
migrate:
|
migrate:
|
||||||
image: hhe-po:latest
|
image: hhe-po:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
|
dockerfile: ./docker/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
<<: *shared_environment
|
<<: *shared_environment
|
||||||
command: ["migrate", "--yes"]
|
command: ["migrate", "--yes"]
|
||||||
@@ -46,7 +55,8 @@ services:
|
|||||||
revert:
|
revert:
|
||||||
image: hhe-po:latest
|
image: hhe-po:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
|
dockerfile: ./docker/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
<<: *shared_environment
|
<<: *shared_environment
|
||||||
command: ["migrate", "--revert", "--yes"]
|
command: ["migrate", "--revert", "--yes"]
|
||||||
|
|||||||
Reference in New Issue
Block a user