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:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -47,6 +47,7 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
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
|
||||
# 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)"
|
||||
|
||||
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,
|
||||
isHtmxRequest: request.isHtmxRequest,
|
||||
logger: request.logger,
|
||||
authenticate: { request.session.authenticate($0) }
|
||||
authenticate: { request.session.authenticate($0) },
|
||||
currentUser: {
|
||||
try request.auth.require(User.self)
|
||||
}
|
||||
)
|
||||
return AnyHTMLResponse(value: html)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public extension DatabaseClient.Employees {
|
||||
guard let model = try await EmployeeModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
database.logger.debug("Applying updates to employee: \(updates)")
|
||||
try model.applyUpdate(updates)
|
||||
try await model.save(on: database)
|
||||
return try model.toDTO()
|
||||
@@ -170,8 +171,10 @@ final class EmployeeModel: Model, @unchecked Sendable {
|
||||
if let lastName = updates.lastName {
|
||||
self.lastName = lastName
|
||||
}
|
||||
if let active = updates.active {
|
||||
self.active = active
|
||||
// NB: When html forms are submitted with a checkbox then active is nil
|
||||
// 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 {
|
||||
try validate()
|
||||
return .init(
|
||||
id: id,
|
||||
workOrder: workOrder,
|
||||
materials: materials,
|
||||
customer: customer,
|
||||
truckStock: truckStock ?? false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Dependencies
|
||||
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 var id: UUID
|
||||
public var active: Bool
|
||||
@@ -31,6 +34,8 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
public extension Employee {
|
||||
/// Represents the required fields for generating a new employee in the
|
||||
/// database.
|
||||
struct Create: Codable, Sendable, Equatable {
|
||||
public let firstName: 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 {
|
||||
public let firstName: String?
|
||||
public let lastName: String?
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import Dependencies
|
||||
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 let id: Int
|
||||
@@ -41,6 +46,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public extension PurchaseOrder {
|
||||
|
||||
/// Represents the required fields for generating a new purchase order in the database.
|
||||
struct Create: Codable, Sendable, Equatable {
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
case customer(String)
|
||||
case vendor(VendorBranch.ID)
|
||||
case employee(Employee.ID)
|
||||
case vendor(VendorBranch.ID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import Foundation
|
||||
|
||||
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 {
|
||||
|
||||
case employee(EmployeeRoute)
|
||||
|
||||
@@ -2,6 +2,7 @@ import CasePathsCore
|
||||
import Foundation
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
/// Represents all the routes that our server can handle.
|
||||
public enum SiteRoute: Sendable {
|
||||
case api(SiteRoute.Api)
|
||||
case health
|
||||
|
||||
@@ -4,6 +4,10 @@ import Foundation
|
||||
|
||||
public extension SiteRoute {
|
||||
// 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 {
|
||||
|
||||
case employee(SiteRoute.View.EmployeeRoute)
|
||||
@@ -131,7 +135,7 @@ public extension SiteRoute {
|
||||
}
|
||||
|
||||
public enum PurchaseOrderRoute: Sendable, Equatable {
|
||||
case create(PurchaseOrder.Create)
|
||||
case create(PurchaseOrder.CreateIntermediate)
|
||||
case form
|
||||
case get(id: PurchaseOrder.ID)
|
||||
case index
|
||||
@@ -151,11 +155,10 @@ public extension SiteRoute {
|
||||
Field("materials", .string)
|
||||
Field("customer", .string)
|
||||
Optionally { Field("truckStock") { Bool.parser() } }
|
||||
Field("createdByID") { User.ID.parser() }
|
||||
Field("createdForID") { Employee.ID.parser() }
|
||||
Field("vendorBranchID") { VendorBranch.ID.parser() }
|
||||
}
|
||||
.map(.memberwise(PurchaseOrder.Create.init))
|
||||
.map(.memberwise(PurchaseOrder.CreateIntermediate.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.form)) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import Dependencies
|
||||
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 var id: UUID
|
||||
@@ -26,6 +31,7 @@ public struct User: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public extension User {
|
||||
|
||||
/// Represents the fields needed to generate a new user in the database.
|
||||
struct Create: Codable, Sendable, Equatable {
|
||||
public let username: 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 {
|
||||
public let username: 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 {
|
||||
public let password: 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 {
|
||||
public let id: UUID
|
||||
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 {
|
||||
public let username: String?
|
||||
public let email: String?
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Dependencies
|
||||
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 var id: UUID
|
||||
public var name: String
|
||||
@@ -25,6 +29,7 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public extension Vendor {
|
||||
|
||||
/// Represents the fields required to generate a new vendor in the database.
|
||||
struct Create: Codable, Sendable, Equatable {
|
||||
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 {
|
||||
public let name: String
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Dependencies
|
||||
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 var id: UUID
|
||||
public var name: String
|
||||
@@ -24,6 +28,8 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
public extension VendorBranch {
|
||||
|
||||
/// Represents the fields required to generate a new vendor branch in the database.
|
||||
struct Create: Codable, Sendable, Equatable {
|
||||
public let name: String
|
||||
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 {
|
||||
public var id: UUID
|
||||
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 {
|
||||
public let name: String?
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public typealias AnySendableHTML = (any HTML & Sendable)
|
||||
@DependencyClient
|
||||
public struct ViewController: Sendable {
|
||||
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
||||
public typealias CurrentUserHandler = @Sendable () throws -> User
|
||||
|
||||
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||
|
||||
@@ -24,9 +25,16 @@ public struct ViewController: Sendable {
|
||||
for route: SiteRoute.View,
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger,
|
||||
authenticate: @escaping AuthenticateHandler
|
||||
authenticate: @escaping AuthenticateHandler,
|
||||
currentUser: @escaping CurrentUserHandler
|
||||
) 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 {
|
||||
@@ -34,17 +42,20 @@ public struct ViewController: Sendable {
|
||||
public let isHtmxRequest: Bool
|
||||
public let authenticate: AuthenticateHandler
|
||||
public let logger: Logger
|
||||
public let currentUser: CurrentUserHandler
|
||||
|
||||
public init(
|
||||
_ route: SiteRoute.View,
|
||||
isHtmxRequest: Bool,
|
||||
authenticate: @escaping AuthenticateHandler,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
currentUser: @escaping CurrentUserHandler
|
||||
) {
|
||||
self.route = route
|
||||
self.isHtmxRequest = isHtmxRequest
|
||||
self.authenticate = authenticate
|
||||
self.logger = logger
|
||||
self.currentUser = currentUser
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ extension ViewController: DependencyKey {
|
||||
try await request.route.view(
|
||||
isHtmxRequest: request.isHtmxRequest,
|
||||
logger: request.logger,
|
||||
authenticate: request.authenticate
|
||||
authenticate: request.authenticate,
|
||||
currentUser: request.currentUser
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,14 +12,11 @@ public extension SiteRoute.View {
|
||||
func view(
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger,
|
||||
authenticate: @escaping @Sendable (User) -> Void
|
||||
authenticate: @escaping @Sendable (User) -> Void,
|
||||
currentUser: @escaping @Sendable () throws -> User
|
||||
) async throws -> AnySendableHTML {
|
||||
@Dependency(\.database.users) var users
|
||||
switch self {
|
||||
// case .index:
|
||||
// // This get's redirected to purchase-orders route in the app / site handler.
|
||||
// return nil
|
||||
|
||||
case let .employee(route):
|
||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||
|
||||
@@ -38,7 +35,7 @@ public extension SiteRoute.View {
|
||||
}
|
||||
|
||||
case let .purchaseOrder(route):
|
||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||
return try await route.view(isHtmxRequest: isHtmxRequest, currentUser: currentUser)
|
||||
|
||||
case let .resetPassword(route):
|
||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||
@@ -80,7 +77,7 @@ extension SiteRoute.View.EmployeeRoute {
|
||||
return try await render(mainPage, isHtmxRequest, EmployeeForm(shouldShow: true))
|
||||
|
||||
case let .select(context: context):
|
||||
return try await context.toHTML(employees: employees.fetchAll())
|
||||
return try await context.toHTML(employees: employees.fetchAll(.active))
|
||||
|
||||
case .index:
|
||||
return try await mainPage(EmployeeForm())
|
||||
@@ -116,7 +113,10 @@ extension SiteRoute.View.PurchaseOrderRoute {
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
switch self {
|
||||
@@ -129,7 +129,9 @@ extension SiteRoute.View.PurchaseOrderRoute {
|
||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||
|
||||
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:
|
||||
return try await mainPage(PurchaseOrderForm())
|
||||
|
||||
@@ -42,6 +42,20 @@ struct EmployeeForm: HTML {
|
||||
.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")) {
|
||||
button(.type(.submit), .class("btn-primary")) {
|
||||
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
|
||||
func employeeViews() async throws {
|
||||
try await withSnapshotTesting(record: record) {
|
||||
try await withSnapshotTesting(record: .missing) {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.database.employees = .mock
|
||||
@@ -213,7 +213,8 @@ extension ViewController {
|
||||
for: route,
|
||||
isHtmxRequest: true,
|
||||
logger: .init(label: "tests"),
|
||||
authenticate: { _ in }
|
||||
authenticate: { _ in },
|
||||
currentUser: { .mock }
|
||||
)
|
||||
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 {
|
||||
static var mock: Self {
|
||||
.init(password: "super-secret", confirmPassword: "super-secret")
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
<div class="col-2"></div>
|
||||
<input type="text" class="col-5" name="lastName" value="McTestface" placeholder="Last Name" required>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
@@ -23,7 +23,6 @@ struct PurchaseOrderViewRouteTests {
|
||||
materials: "some",
|
||||
customer: "Testy",
|
||||
truckStock: false,
|
||||
createdByID: id,
|
||||
createdForID: 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`.
|
||||
# libxml2 \
|
||||
sqlite3 \
|
||||
curl \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
# Create a vapor user and group with /app as its home directory
|
||||
|
||||
@@ -21,7 +21,8 @@ services:
|
||||
app:
|
||||
image: hhe-po:latest
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: ./docker/Dockerfile
|
||||
environment:
|
||||
<<: *shared_environment
|
||||
volumes:
|
||||
@@ -30,12 +31,20 @@ services:
|
||||
- '8080:8080'
|
||||
labels:
|
||||
- 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.
|
||||
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||
migrate:
|
||||
image: hhe-po:latest
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: ./docker/Dockerfile
|
||||
environment:
|
||||
<<: *shared_environment
|
||||
command: ["migrate", "--yes"]
|
||||
@@ -46,7 +55,8 @@ services:
|
||||
revert:
|
||||
image: hhe-po:latest
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: ./docker/Dockerfile
|
||||
environment:
|
||||
<<: *shared_environment
|
||||
command: ["migrate", "--revert", "--yes"]
|
||||
|
||||
Reference in New Issue
Block a user