feat: Refactoring routes to use shared / base routes.

This commit is contained in:
2025-01-21 16:00:16 -05:00
parent 20e58114c0
commit 66074d66f4
15 changed files with 445 additions and 227 deletions

View File

@@ -16,22 +16,22 @@ extension ApiRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handle(request: Request) async throws -> any AsyncResponseEncodable {
switch self { switch self {
case let .employee(route): case let .employee(route):
return try await route.handle(request: request) return try await route.handleApiRequest(request: request)
case let .purchaseOrder(route): case let .purchaseOrder(route):
return try await route.handle(request: request) return try await route.handleApiRequest(request: request)
case let .user(route): case let .user(route):
return try await route.handle(request: request) return try await route.handleApiRequest(request: request)
case let .vendor(route): case let .vendor(route):
return try await route.handle(request: request) return try await route.handleApiRequest(request: request)
case let .vendorBranch(route): case let .vendorBranch(route):
return try await route.handle(request: request) return try await route.handleApiRequest(request: request)
} }
} }
} }
extension ApiRoute.EmployeeApiRoute { extension BaseRoute.EmployeeRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handleApiRequest(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case let .create(employee): case let .create(employee):
@@ -52,9 +52,9 @@ extension ApiRoute.EmployeeApiRoute {
} }
} }
extension ApiRoute.PurchaseOrderApiRoute { extension BaseRoute.PurchaseOrderRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handleApiRequest(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database.purchaseOrders) var purchaseOrders @Dependency(\.database.purchaseOrders) var purchaseOrders
switch self { switch self {
case .index: case .index:
@@ -76,9 +76,9 @@ extension ApiRoute.PurchaseOrderApiRoute {
} }
// TODO: Add Login. // TODO: Add Login.
extension ApiRoute.UserApiRoute { extension BaseRoute.UserRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handleApiRequest(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database.users) var users @Dependency(\.database.users) var users
switch self { switch self {
case let .create(user): case let .create(user):
@@ -101,8 +101,8 @@ extension ApiRoute.UserApiRoute {
} }
} }
extension ApiRoute.VendorApiRoute { extension BaseRoute.VendorRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handleApiRequest(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database.vendors) var vendors @Dependency(\.database.vendors) var vendors
switch self { switch self {
case let .create(vendor): case let .create(vendor):
@@ -126,8 +126,8 @@ extension ApiRoute.VendorApiRoute {
} }
} }
extension ApiRoute.VendorBranchApiRoute { extension BaseRoute.VendorBranchRoute {
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handleApiRequest(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database.vendorBranches) var vendorBranches @Dependency(\.database.vendorBranches) var vendorBranches
switch self { switch self {
case let .create(branch): case let .create(branch):

View File

@@ -12,6 +12,7 @@ private let viewProtectedMiddleware: [any Middleware] = [
} }
] ]
// TODO: Return `any HTML` instead to make testing the rendered documents easier.
extension SharedModels.ViewRoute { extension SharedModels.ViewRoute {
var middleware: [any Middleware]? { var middleware: [any Middleware]? {
@@ -348,6 +349,31 @@ extension SharedModels.ViewRoute.VendorBranchRoute {
var middleware: [any Middleware]? { viewProtectedMiddleware } var middleware: [any Middleware]? { viewProtectedMiddleware }
// func html() async throws -> any HTML {
// @Dependency(\.database) var database
//
// switch self {
// case let .index(for: vendorID):
// guard let vendorID else {
// throw Abort(.badRequest, reason: "Vendor id not supplied")
// }
// return try await VendorBranchList(
// vendorID: vendorID,
// branches: database.vendorBranches.fetchAll(.for(vendorID: vendorID))
// )
//
// case let .select(context: context):
// return try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail())
//
// case let .create(branch):
// return try await VendorBranchList.Row(branch: database.vendorBranches.create(branch))
//
// case let .delete(id: id):
// try await database.vendorBranches.delete(id)
// return HTTPStatus.ok
// }
// }
func handle(request: Request) async throws -> any AsyncResponseEncodable { func handle(request: Request) async throws -> any AsyncResponseEncodable {
@Dependency(\.database) var database @Dependency(\.database) var database

View File

@@ -4,20 +4,6 @@ import Vapor
import VaporElementary import VaporElementary
extension Request { extension Request {
func ensureValidContent<T>(_ decoding: T.Type) throws -> T where T: Content, T: Validatable {
try T.validate(content: self)
return try content.decode(T.self)
}
func ensureIDPathComponent<T: LosslessStringConvertible>(
as decoding: T.Type = UUID.self,
key: String = "id"
) throws -> T {
guard let id = parameters.get(key, as: T.self) else {
throw Abort(.badRequest, reason: "Id not supplied.")
}
return id
}
var isHtmxRequest: Bool { var isHtmxRequest: Bool {
headers.contains(name: "hx-request") headers.contains(name: "hx-request")

View File

@@ -4,222 +4,34 @@ import Foundation
public enum ApiRoute: Sendable, Equatable { public enum ApiRoute: Sendable, Equatable {
case employee(EmployeeApiRoute) case employee(BaseRoute.EmployeeRoute)
case purchaseOrder(PurchaseOrderApiRoute) case purchaseOrder(BaseRoute.PurchaseOrderRoute)
case user(UserApiRoute) case user(BaseRoute.UserRoute)
case vendor(VendorApiRoute) case vendor(BaseRoute.VendorRoute)
case vendorBranch(VendorBranchApiRoute) case vendorBranch(BaseRoute.VendorBranchRoute)
static let rootPath = Path { "api"; "v1" } static let rootPath = Path { "api"; "v1" }
public static let router = OneOf { public static let router = OneOf {
Route(.case(Self.employee)) { Route(.case(Self.employee)) {
rootPath rootPath
EmployeeApiRoute.router BaseRoute.EmployeeRoute.router
} }
Route(.case(Self.purchaseOrder)) { Route(.case(Self.purchaseOrder)) {
rootPath rootPath
PurchaseOrderApiRoute.router BaseRoute.PurchaseOrderRoute.router
} }
Route(.case(Self.user)) { Route(.case(Self.user)) {
rootPath rootPath
UserApiRoute.router BaseRoute.UserRoute.router
} }
Route(.case(Self.vendor)) { Route(.case(Self.vendor)) {
rootPath rootPath
VendorApiRoute.router BaseRoute.VendorRoute.router
} }
Route(.case(Self.vendorBranch)) { Route(.case(Self.vendorBranch)) {
rootPath rootPath
VendorBranchApiRoute.router BaseRoute.VendorBranchRoute.router
}
}
public enum EmployeeApiRoute: Sendable, Equatable {
case create(Employee.Create)
case delete(id: Employee.ID)
case get(id: Employee.ID)
case index
case update(id: Employee.ID, updates: Employee.Update)
static let rootPath = "employees"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
Body(.json(Employee.Create.self))
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.delete(id:))) {
Path { rootPath; UUID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; UUID.parser() }
Method.get
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; UUID.parser() }
Method.put
Body(.json(Employee.Update.self))
}
}
}
public enum PurchaseOrderApiRoute: Sendable, Equatable {
case create(PurchaseOrder.Create)
case delete(id: PurchaseOrder.ID)
case get(id: PurchaseOrder.ID)
case index
case page(page: Int, limit: Int)
static let rootPath = "purchase-orders"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
Body(.json(PurchaseOrder.Create.self))
}
Route(.case(Self.delete(id:))) {
Path { rootPath; Digits() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; Digits() }
Method.get
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.page(page:limit:))) {
Path { rootPath; "next" }
Method.get
Query {
Field("page", default: 1) { Digits() }
Field("limit", default: 25) { Digits() }
}
}
}
}
// TODO: Add login / logout.
public enum UserApiRoute: Sendable, Equatable {
case create(User.Create)
case delete(id: User.ID)
case get(id: User.ID)
case index
case update(id: User.ID, updates: User.Update)
static let rootPath = "users"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
Body(.json(User.Create.self))
}
Route(.case(Self.delete(id:))) {
Path { rootPath; User.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; User.ID.parser() }
Method.get
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; User.ID.parser() }
Method.patch
Body(.json(User.Update.self))
}
}
}
public enum VendorApiRoute: Sendable, Equatable {
case index(withBranches: Bool? = nil)
case create(Vendor.Create)
case delete(id: Vendor.ID)
case get(id: Vendor.ID)
case update(id: Vendor.ID, updates: Vendor.Update)
static let rootPath = "vendors"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
Body(.json(Vendor.Create.self))
}
Route(.case(Self.delete(id:))) {
Path { rootPath; Vendor.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; Vendor.ID.parser() }
Method.get
}
Route(.case(Self.index(withBranches:))) {
Path { rootPath }
Method.get
Query {
Optionally {
Field("branches", default: nil) {
Bool.parser()
}
}
}
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; Vendor.ID.parser() }
Method.put
Body(.json(Vendor.Update.self))
}
}
}
public enum VendorBranchApiRoute: Sendable, Equatable {
case create(VendorBranch.Create)
case delete(id: VendorBranch.ID)
case get(id: VendorBranch.ID)
case index(for: Vendor.ID? = nil)
case update(id: VendorBranch.ID, updates: VendorBranch.Update)
public static let router = OneOf {
Route(.case(Self.create)) {
Path { "vendors"; "branches" }
Method.post
Body(.json(VendorBranch.Create.self))
}
Route(.case(Self.delete(id:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.get
}
Route(.case(Self.index(for:))) {
Path { "vendors"; "branches" }
Method.get
Query {
Optionally { Field("vendorID", default: nil) { VendorBranch.ID.parser() } }
}
}
Route(.case(Self.update(id:updates:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.put
Body(.json(VendorBranch.Update.self))
}
} }
} }
} }

View File

@@ -0,0 +1,294 @@
import CasePathsCore
import Foundation
@preconcurrency import URLRouting
public enum BaseRoute {}
public extension BaseRoute {
enum EmployeeRoute: Sendable, Equatable {
case create(Employee.Create)
case delete(id: Employee.ID)
case get(id: Employee.ID)
case index
case update(id: Employee.ID, updates: Employee.Update)
static let rootPath = "employees"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
OneOf {
Body(.json(Employee.Create.self))
Body {
FormData {
Field("firstName", .string)
Field("lastName", .string)
Optionally { Field("active") { Bool.parser() } }
}
.map(.memberwise(Employee.Create.init))
}
}
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.delete(id:))) {
Path { rootPath; UUID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; UUID.parser() }
Method.get
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; UUID.parser() }
Method.put
OneOf {
Body(.json(Employee.Update.self))
Body {
FormData {
Optionally { Field("firstName") { CharacterSet.alphanumerics.map(.string) } }
Optionally { Field("lastName") { CharacterSet.alphanumerics.map(.string) } }
Optionally { Field("active") { Bool.parser() } }
}
.map(.memberwise(Employee.Update.init))
}
}
}
}
}
}
public extension BaseRoute {
enum PurchaseOrderRoute: Sendable, Equatable {
case create(PurchaseOrder.Create)
case delete(id: PurchaseOrder.ID)
case get(id: PurchaseOrder.ID)
case index
case page(page: Int, limit: Int)
static let rootPath = "purchase-orders"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
OneOf {
Body(.json(PurchaseOrder.Create.self))
Body {
FormData {
Optionally { Field("id") { PurchaseOrder.ID.parser() } }
Optionally { Field("workOrder") { Int.parser() } }
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))
}
}
}
Route(.case(Self.delete(id:))) {
Path { rootPath; Digits() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; Digits() }
Method.get
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.page(page:limit:))) {
Path { rootPath; "next" }
Method.get
Query {
Field("page", default: 1) { Digits() }
Field("limit", default: 25) { Digits() }
}
}
}
}
}
public extension BaseRoute {
enum UserRoute: Sendable, Equatable {
case create(User.Create)
case delete(id: User.ID)
case get(id: User.ID)
case index
case update(id: User.ID, updates: User.Update)
static let rootPath = "users"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
OneOf {
Body(.json(User.Create.self))
Body {
FormData {
Field("username", .string)
Field("email", .string)
Field("password", .string)
Field("confirmPassword", .string)
}
.map(.memberwise(User.Create.init))
}
}
}
Route(.case(Self.delete(id:))) {
Path { rootPath; User.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; User.ID.parser() }
Method.get
}
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; User.ID.parser() }
Method.patch
OneOf {
Body(.json(User.Update.self))
Body {
FormData {
Optionally { Field("username") {
CharacterSet.alphanumerics.map(.string)
}
}
Optionally { Field("email", .string) }
}
.map(.memberwise(User.Update.init))
}
}
}
}
}
}
public extension BaseRoute {
enum VendorRoute: Sendable, Equatable {
case index(withBranches: Bool? = nil)
case create(Vendor.Create)
case delete(id: Vendor.ID)
case get(id: Vendor.ID)
case update(id: Vendor.ID, updates: Vendor.Update)
static let rootPath = "vendors"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
OneOf {
Body(.json(Vendor.Create.self))
Body {
FormData {
Field("name", .string)
}
.map(.memberwise(Vendor.Create.init))
}
}
}
Route(.case(Self.delete(id:))) {
Path { rootPath; Vendor.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { rootPath; Vendor.ID.parser() }
Method.get
}
Route(.case(Self.index(withBranches:))) {
Path { rootPath }
Method.get
Query {
Optionally {
Field("branches", default: nil) {
Bool.parser()
}
}
}
}
Route(.case(Self.update(id:updates:))) {
Path { rootPath; Vendor.ID.parser() }
Method.put
OneOf {
Body(.json(Vendor.Update.self))
Body {
FormData {
Field("name", .string)
}
.map(.memberwise(Vendor.Update.init))
}
}
}
}
}
}
public extension BaseRoute {
enum VendorBranchRoute: Sendable, Equatable {
case create(VendorBranch.Create)
case delete(id: VendorBranch.ID)
case get(id: VendorBranch.ID)
case index(for: Vendor.ID? = nil)
case update(id: VendorBranch.ID, updates: VendorBranch.Update)
public static let router = OneOf {
Route(.case(Self.create)) {
Path { "vendors"; "branches" }
Method.post
OneOf {
Body(.json(VendorBranch.Create.self))
Body {
FormData {
Field("name", .string)
Field("vendorID") { Vendor.ID.parser() }
}
.map(.memberwise(VendorBranch.Create.init))
}
}
}
Route(.case(Self.delete(id:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.delete
}
Route(.case(Self.get(id:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.get
}
Route(.case(Self.index(for:))) {
Path { "vendors"; "branches" }
Method.get
Query {
Optionally { Field("vendorID", default: nil) { VendorBranch.ID.parser() } }
}
}
Route(.case(Self.update(id:updates:))) {
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
Method.put
OneOf {
Body(.json(VendorBranch.Update.self))
Body {
FormData {
Field("name", .string)
}
.map(.memberwise(VendorBranch.Update.init))
}
}
}
}
}
}

View File

@@ -107,6 +107,66 @@ final class ViewSnapshotTests: XCTestCase {
} }
} }
} }
func testVendorViews() async throws {
try await withDependencies {
$0.database.vendors = .mock
} operation: {
@Dependency(\.database) var database
try await configure(app, makeDatabaseClient: { _ in database })
try app.test(.GET, router.path(for: .vendor(.form))) { res in
assertSnapshot(of: res.body.string, as: .html)
}
try app.test(.POST, router.path(for: .vendor(.index))) { req in
req.body = ByteBuffer(string: "name=Test")
} afterResponse: { res in
assertSnapshot(of: res.body.string, as: .html)
}
try app.test(.GET, router.path(for: .vendor(.index))) { res in
assertSnapshot(of: res.body.string, as: .html)
}
try app.test(.GET, router.path(for: .vendor(.get(id: UUID(0))))) { res in
assertSnapshot(of: res.body.string, as: .html)
}
try app.test(.PUT, router.path(for: .vendor(.update(id: UUID(0), updates: .mock)))) { req in
req.body = .init(string: "name=Test")
} afterResponse: { res in
assertSnapshot(of: res.body.string, as: .html)
}
}
}
func testVendorBranchViews() async throws {
try await withDependencies {
$0.database.vendorBranches = .mock
} operation: {
@Dependency(\.database) var database
try await configure(app, makeDatabaseClient: { _ in database })
try app.test(.GET, router.path(for: .vendorBranch(.index(for: UUID(0))))) { res in
assertSnapshot(of: res.body.string, as: .html)
}
for context in SharedModels.ViewRoute.SelectContext.allCases {
try app.test(.GET, router.path(for: .vendorBranch(.select(context: context)))) { res in
assertSnapshot(of: res.body.string, as: .html)
}
}
try app.test(.POST, router.path(for: .vendorBranch(.create(.mock)))) { req in
req.body = .init(string: "name=Test&vendorID=\(UUID(0))")
} afterResponse: { res in
assertSnapshot(of: res.body.string, as: .html)
}
}
}
} }
extension DatabaseClient.Employees { extension DatabaseClient.Employees {
@@ -137,6 +197,31 @@ extension DatabaseClient.Users {
} }
} }
extension DatabaseClient.Vendors {
static var mock: Self {
.init(
create: { _ in Vendor.mock },
delete: { _ in },
fetchAll: { _ in [Vendor.mock] },
get: { _, _ in Vendor.mock },
update: { _, _, _ in Vendor.mock }
)
}
}
extension DatabaseClient.VendorBranches {
static var mock: Self {
.init(
create: { _ in VendorBranch.mock },
delete: { _ in },
fetchAll: { _ in [VendorBranch.mock] },
fetchAllWithDetail: { [VendorBranch.Detail.mock] },
get: { _ in VendorBranch.mock },
update: { _, _ in VendorBranch.mock }
)
}
}
extension Date { extension Date {
static var mock: Self { static var mock: Self {
Date(timeIntervalSince1970: 1_234_567_890) Date(timeIntervalSince1970: 1_234_567_890)
@@ -214,6 +299,12 @@ extension Vendor.Create {
} }
} }
extension Vendor.Update {
static var mock: Self {
.init(name: "Test")
}
}
extension VendorBranch { extension VendorBranch {
static var mock: Self { static var mock: Self {
.init(id: UUID(1), name: "Mock", vendorID: UUID(0), createdAt: .mock, updatedAt: .mock) .init(id: UUID(1), name: "Mock", vendorID: UUID(0), createdAt: .mock, updatedAt: .mock)

View File

@@ -0,0 +1 @@
<ul id="branch-list"><li id="branch-00000000-0000-0000-0000-000000000001" class="branch-row"><span class="label">Mock</span><button class="btn" hx-delete="/vendors/branches/00000000-0000-0000-0000-000000000001" hx-target="#branch-00000000-0000-0000-0000-000000000001" hx-swap="outerHTML transition:true swap:0.5s"><img src="/images/trash-can.svg" width="30" height="30" style="margin-top: 5px;"></button></li></ul>

View File

@@ -0,0 +1 @@
<select name="vendorBranchID" class="col-4"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>

View File

@@ -0,0 +1 @@
<select name="vendorBranchID" class="col-4" style="margin-left: 15px;"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>

View File

@@ -0,0 +1 @@
<li id="branch-00000000-0000-0000-0000-000000000001" class="branch-row"><span class="label">Mock</span><button class="btn" hx-delete="/vendors/branches/00000000-0000-0000-0000-000000000001" hx-target="#branch-00000000-0000-0000-0000-000000000001" hx-swap="outerHTML transition:true swap:0.5s"><img src="/images/trash-can.svg" width="30" height="30" style="margin-top: 5px;"></button></li>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&amp;limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');">x</button></div><form id="vendor-form" hx-post="/vendors" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="" placeholder="Vendor Name" hx-post="/vendors" hx-trigger="keyup changed delay:500ms" required><button type="submit" class="btn-primary" style="float: right">Create</button></div></form></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML"></button></td></tr></tbody></table></div></body></html>

View File

@@ -0,0 +1 @@
<div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML"></button></td></tr></tbody></table></div>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&amp;limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="" style="display: hidden;"></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML"></button></td></tr></tbody></table></div></body></html>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&amp;limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML"></button></td></tr></tbody></table></div></body></html>

View File

@@ -0,0 +1 @@
<div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div>