feat: Working purchase order table and form.

This commit is contained in:
2025-01-09 16:23:42 -05:00
parent da5fec4a94
commit bf71b725f6
23 changed files with 544 additions and 254 deletions

View File

@@ -19,39 +19,14 @@ header {
background-color: #14141f; background-color: #14141f;
color: #ff66ff; color: #ff66ff;
padding: 10px 0; padding: 10px 0;
height: 60px;
border-bottom: 1px solid grey; border-bottom: 1px solid grey;
} }
#logo { #logo {
float: left; float: left;
font-size: 1.5em; font-size: 1.5em;
} margin-top: 10px;
nav {
float: right;
}
.nav-links {
list-style-type: none;
margin: 0;
padding: 0;
}
.nav-links li {
display: inline-block;
margin-left: 20px;
}
.nav-links li a {
color: #fff;
text-decoration: none;
padding: 10px 15px;
display: inline-block;
transition: background-color 0.3s;
}
.nav-links li a:hover {
background-color: #555;
} }
.content { .content {
@@ -116,7 +91,7 @@ input[type=submit] {
padding: 5px 20px; padding: 5px 20px;
} }
input[type=text], input[type=password], input[type=email] { input[type=text], input[type=password], input[type=email], input[type=number] {
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
border: none; border: none;
@@ -124,6 +99,21 @@ input[type=text], input[type=password], input[type=email] {
padding: 5px; padding: 5px;
} }
select {
background-color: #14141f;
border: none;
border-bottom: 2px solid #555;
border-radius: 5px;
color: inherit;
padding: 10px 20px;
width: 100%;
margin-bottom: 10px;
}
option {
padding-left: 10px;
}
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus { input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
outline: none; outline: none;
} }
@@ -164,50 +154,146 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
background-color: #555; background-color: #555;
} }
.dropbtn {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropbtn:hover, .dropbtn:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover {
background-color: #ddd;
}
.show {
display: block;
}
tr.htmx-swapping td { tr.htmx-swapping td {
opacity: 0; opacity: 0;
transition: opacity 0.5s ease-out; transition: opacity 0.5s ease-out;
} }
.sidepanel {
height: 275px;
width: 0;
position: fixed;
z-index: 1;
top: 0;
right: 0;
background-color: #111;
overflow-x: hidden;
padding-top: 60px;
transition: 0.5s;
}
.sidepanel a {
margin-left: 15px;
margin-bottom: 15px;
padding 10px 10px 10px 32px;
text-decoration: none;
font-size: 25px;
display: block;
transition: 0.3s;
color: grey;
}
.sidepanel a:hover {
color: #f1f1f1;
}
.sidepanel .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
color: grey;
}
.openbtn {
font-size: 30px;
cursor: pointer;
background-color: inherit;
color: white;
padding: 10px 15px;
border: none;
position: fixed;
top: 0;
right: 0;
}
.openbtn:hover {
background-color: #444;
}
.form-content {
transition: 0.5s;
overflow: auto;
z-index: 1;
position: fixed;
top: 100px;
left: 0;
background-color: #14141f;
width: 100%;
}
.closebtn {
color: grey;
margin-left: 50px;
text-decoration: none;
}
.form-content .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
color: grey;
}
.btn-add {
color: grey;
font-size: 1.5em;
text-decoration: none;
}
.btn-add:hover {
background-color: #444;
}
.btn {
text-decoration: none;
}
.btn:hover {
background-color: #444;
}
.danger {
color: red;
}
.vendor-branches {
width: 350px;
}
.vendor-branches ul li a {
position: relative;
top: 2px;
right: 0;
margin-left: 10px;
font-size: 1.5em;
}
.vendor-branches ul li {
transition: 0.3s ease-out;
}
.branch-row {
display: inline-block;
width: 300px;
height: 40px;
background-color: #14141f;
border-radius: 25px;
padding-left: 15px;
margin: 5px;
}
.branch-row .branch-name {
display: inline-block;
margin-top: 10px;
}
.branch-row a {
float: right;
margin-top: 5px;
margin-right: 15px;
font-size: 1.5em;
}

View File

@@ -24,3 +24,21 @@ function updateDropDownSelection(id, contentId) {
let content = document.getElementById(contentId).innerHTML; let content = document.getElementById(contentId).innerHTML;
document.getElementById(id).innerHTML = content; document.getElementById(id).innerHTML = content;
} }
function openSidepanel() {
document.getElementById("sidepanel").style.width = "250px";
}
function closeSidepanel() {
document.getElementById("sidepanel").style.width = "0";
}
// Show or hide an element by id.
function toggleContent(id) {
var el = document.getElementById(id);
if (el.style.display === "none") {
el.style.display = "block";
} else {
el.style.display = "none";
}
}

View File

@@ -0,0 +1,6 @@
<a href="javascript:void(0)"
class="closebtn"
onClick="toggleContent('form')"
>
&times;
</a>

View File

@@ -0,0 +1,6 @@
<a href="javascript:void(0)"
onclick="toggleContent('form')"
class="btn-add"
>
&plus;
</a>

View File

@@ -1,26 +1,12 @@
<form class="employee-form" #extend("htmx-form", htmxForm):
id="employee-form" #export("formBody"):
#if(employee.id):
hx-put="/employees/#(employee.id)"
#else:
hx-post="/employees"
#endif
#if(employee.id):
hx-target="#home-content"
#else:
hx-target="#employee-table"
#endif
#if(oob):
hx-swap-oob="outerHTML"
#endif
>
<input type="text" <input type="text"
id="firstName" id="firstName"
name="firstName" name="firstName"
placeholder="First Name" placeholder="First Name"
autofocus autofocus
required required
#if(employee.firstName): value=#(employee.firstName) #endif #if(context.employee.firstName): value=#(context.employee.firstName) #endif
> >
<br> <br>
<input type="text" <input type="text"
@@ -28,16 +14,9 @@
name="lastName" name="lastName"
placeholder="Last Name" placeholder="Last Name"
required required
#if(employee.lastName): value=#(employee.lastName) #endif #if(context.employee.lastName): value=#(context.employee.lastName) #endif
> >
<br> <br>
<input type="submit" value=#if(employee.id): Update #else: Create #endif> <input type="submit" value=#if(context.employee.id): Update #else: Create #endif>
#if(employee.id): #endexport
<button hx-get="/employees/form" #endextend
hx-target="#employee-form"
hx-swap="outerHTML"
>
Reset
</button>
#endif
</form>

View File

@@ -4,10 +4,12 @@
<div class="container"> <div class="container">
<h1>Employees</h1> <h1>Employees</h1>
<br> <br>
<p>Employees are who purchase orders can be generated for.</p> <h3>Employees are who purchase orders can be generated for.</h3>
<br> <br>
</div> </div>
#extend("employees/form", form) #extend("form-container"): #export("formContent"):
#extend("employees/form", form)
#endexport #endextend
#extend("employees/table") #extend("employees/table")
</div> </div>
#endexport #endexport

View File

@@ -1,47 +1,62 @@
<table id="employee-table"> <table id="employee-table">
<tr> <thead>
<th>Name</th> <tr>
<th>Active</th> <th>Name</th>
<th></th> <th>Active</th>
</tr> <th>
#for(employee in employees): <a href="javascript:void(0)"
<tr id="employee_#(employee.id)"> hx-get="employees/form"
<td>#capitalized(employee.firstName) #capitalized(employee.lastName)</td> hx-target="#employee-form"
<td style="width: 10%; text-align: center;"> hx-on::after-request="toggleContent('form')"
#if(employee.active): class="btn-add"
<a class="toggle" >
hx-post="/employees/#(employee.id)/toggle-active" &plus;
hx-target="#employee-table" </a>
hx-swap="outerHTML" </th>
> </tr>
<img src="images/toggle-on.svg" alt="Active"> </thead>
</a> <tbody>
#else: #for(employee in employees):
<a class="toggle" <tr id="employee_#(employee.id)">
hx-post="/employees/#(employee.id)/toggle-active" <td>#capitalized(employee.firstName) #capitalized(employee.lastName)</td>
hx-target="#employee-table" <td style="width: 10%; text-align: center;">
hx-swap="outerHTML" #if(employee.active):
> <a class="toggle"
<img src="images/toggle-off.svg" alt="Active"> hx-post="/employees/#(employee.id)/toggle-active"
</a>
#endif
</td>
<td style="width: 100px;">
<a class="btn btn-delete"
hx-delete="/employees/#(employee.id)"
hx-target="#employee-table" hx-target="#employee-table"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this employee?"
> >
<img src="images/trash-can.svg" alt="Delete"> <img src="images/toggle-on.svg" alt="Active">
</a> </a>
<a class="btn btn-edit" hx-get="/employees/#(employee.id)" #else:
hx-target="#employee-form" <a class="toggle"
> hx-post="/employees/#(employee.id)/toggle-active"
<img src="images/pencil.svg", alt="Edit"> hx-target="#employee-table"
hx-swap="outerHTML"
>
<img src="images/toggle-off.svg" alt="Active">
</a> </a>
</td> #endif
</td>
<td style="width: 100px;">
<a class="btn btn-delete"
href="javascript:void(0)"
hx-delete="/employees/#(employee.id)"
hx-target="#employee-table"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this employee?"
>
#extend("img/trash-can")
</a>
<a class="btn btn-edit" hx-get="/employees/#(employee.id)"
hx-target="#employee-form"
hx-on::after-request=" if(event.detail.successful) toggleContent('form')"
>
#extend("img/pencil")
</a>
</td>
</tr> </tr>
#endfor #endfor
</tbody>
</table> </table>

View File

@@ -0,0 +1,4 @@
<div id="form" style="display: none;" class="form-content">
#extend("btn/close-form")
#import("formContent")
</div>

View File

@@ -8,36 +8,6 @@
</div> </div>
</header> </header>
<section class="content"> <section class="content">
<div class="container">
<nav>
<ul class="nav-links">
<li>
<a hx-get="/users"
hx-target="body"
hx-push-url="true"
>
Users
</a>
</li>
<li>
<a hx-get="/employees"
hx-target="body"
hx-push-url="true"
>
Employees
</a>
</li>
<li>
<a hx-get="/vendors"
hx-target="body"
hx-push-url="true"
>
Vendors
</a>
</li>
</ul>
</nav>
</div>
#import("homeContent") #import("homeContent")
</section> </section>
</div> </div>

View File

@@ -16,8 +16,9 @@
hx-swap-oob="#(htmxSwapOob)" hx-swap-oob="#(htmxSwapOob)"
#endif #endif
#if(htmxResetAfterRequest): #if(htmxResetAfterRequest):
hx-on::after-request=" if(event.detail.successful) this.reset()" hx-on::after-request=" if(event.detail.successful) this.reset(); toggleContent('form');"
#endif #endif
hx-disabled-elt="find input[type='text'], find button, find input[type='submit']"
> >
#import("formBody") #import("formBody")
</form> </form>

View File

@@ -0,0 +1 @@
<img src="images/pencil.svg", alt="Edit">

View File

@@ -9,19 +9,6 @@
<title>#(title)</title> <title>#(title)</title>
</head> </head>
<body> <body>
<!-- <div class="dropdown"> -->
<!-- <button id="test-dropdown" -->
<!-- onClick="showDropdownContent('myDropdown')" -->
<!-- class="dropbtn" -->
<!-- > -->
<!-- Dropdown -->
<!-- </button> -->
<!-- <div id="myDropdown" class="dropdown-content"> -->
<!-- <a href="#" id="home" onClick="updateDropDownSelection('test-dropdown', 'home')">Home</a> -->
<!-- <a href="#" id="about" onClick="updateDropDownSelection('test-dropdown', 'about')">About</a> -->
<!-- <a href="#" id="contact" onClick="updateDropDownSelection('test-dropdown', 'contact')">Contact</a> -->
<!-- </div> -->
<!-- </div> -->
#import("content") #import("content")
</body> </body>
</html> </html>

View File

@@ -1,5 +1,37 @@
<nav> <div class="sidepanel" id="sidepanel">
<ul class="nav-links"> <a href="javscript:void(0)" class="closebtn" onclick="closeSidepanel()">&times;</a>
<li><a hx-post="logout" hx-target="#content" hx-trigger="click" hx-swap="outerHTML">Logout</a></li> <a hx-get="/purchase-orders"
</ul> hx-target="body"
</nav> 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 style="padding-top: 5px;"
hx-post="logout"
hx-target="#content"
hx-trigger="click"
hx-swap="outerHTML"
>
Logout
</a>
</div>
<button class="openbtn" onclick="openSidepanel()">&#9776;</button>

View File

@@ -1,21 +1,22 @@
<form hx-post="/fix-me" #extend("htmx-form", htmxForm):
> #export("formBody"):
<input type="number" <input type="text"
id="workOrder" id="workOrder"
name="workOrder" name="workOrder"
placeholder="12345" placeholder="Work Order: 12345"
> >
<br> <br>
<!-- TODO: Add vendor drop-down --> <select id="vendorBranchID" name="vendorBranchID">
<input type="hidden" #for(branch in context.branches):
id="vendorBranchId" <option value="#(branch.id)">#capitalized(branch.name) - #capitalized(branch.vendor.name)</option>
name="vendorBranchId" #endfor
> </select>
<!-- TODO: Add employee drop-down --> <br>
<input type="hidden" <select id="createdForID" name="createdForID">
id="employeeId" #for(employee in context.employees):
name="employeeId" <option value="#(employee.id)">#capitalized(employee.firstName) #capitalized(employee.lastName)</option>
> #endfor
</select>
<br> <br>
<input type="text" <input type="text"
id="materials" id="materials"
@@ -32,10 +33,11 @@
> >
<br> <br>
<label for="truckStock"> <label for="truckStock">Truck Stock</label>
<input type="checkbox" <input type="checkbox"
id="truckStock" id="truckStock"
name="truckStock" name="truckStock"
> >
<input type="submit" value="Create">
</form> #endexport
#endextend

View File

@@ -0,0 +1,14 @@
#extend("home"):
#export("homeContent"):
<div id="home-content" class="container" #if(oob): hx-swap-oob="outerHTML" #endif>
<div class="container">
<h1>Purchase Orders</h1>
<br>
</div>
#extend("form-container"): #export("formContent"):
#extend("purchaseOrders/form", form)
#endexport #endextend
#extend("purchaseOrders/table")
</div>
#endexport
#endextend

View File

@@ -4,18 +4,18 @@
<th>Work Order</th> <th>Work Order</th>
<th>Vendor</th> <th>Vendor</th>
<th>Materials</th> <th>Materials</th>
<th>Employee</th> <th>Created For</th>
<th>Truck Stock</th> <th>Truck Stock</th>
<th></th> <th>#extend("btn/toggle-form")</th>
</tr> </tr>
<tbody id="po-table-body"> <tbody id="po-table-body">
#for(po in purchaseOrders): #for(po in purchaseOrders):
<tr id="po_#(po.id)"> <tr id="po_#(po.id)">
<td>#(po.id)</td> <td>#(po.id)</td>
<td>#(po.workOrder)</td> <td>#(po.workOrder)</td>
<td>#(po.vendorBranch.vendor.name) - #(po.vendorBranch.name)</td> <td>#capitalized(po.vendorBranch.vendor.name) - #capitalized(po.vendorBranch.name)</td>
<td>#(po.materials)</td> <td>#(po.materials)</td>
<td>#(po.employee.firstName) #(po.employee.lastName)</td> <td>#capitalized(po.createdFor.firstName) #capitalized(po.createdFor.lastName)</td>
<td>#capitalized(po.truckStock)</td> <td>#capitalized(po.truckStock)</td>
<td> <td>
<!-- TODO: add buttons here --> <!-- TODO: add buttons here -->

View File

@@ -7,7 +7,9 @@
<p>Users are people that can login and generate puchase orders for employees.</p> <p>Users are people that can login and generate puchase orders for employees.</p>
<br> <br>
</div> </div>
#extend("users/form", form) #extend("form-container"): #export("formContent"):
#extend("users/form", form)
#endexport #endextend
#extend("users/table") #extend("users/table")
</div> </div>
#endexport #endexport

View File

@@ -1,23 +1,27 @@
<table id="user-table"> <table id="user-table">
<thead>
<tr> <tr>
<th>Username</th> <th>Username</th>
<th>Email</th> <th>Email</th>
<th></th> <th>#extend("btn/toggle-form")</th>
</tr> </tr>
#for(user in users): </thead>
<tr id="user_#(user.id)"> <tbody>
<td>#(user.username)</td> #for(user in users):
<td>#(user.email)</td> <tr id="user_#(user.id)">
<td style="width: 60px;"> <td>#(user.username)</td>
<a class="btn btn-delete" <td>#(user.email)</td>
hx-delete="/users/#(user.id)" <td style="width: 50px;">
hx-target="#user-table" <a class="btn btn-delete"
hx-swap="outerHTML" hx-delete="/users/#(user.id)"
hx-confirm="Are you sure you want to delete this user?" hx-target="#user-table"
> hx-swap="outerHTML"
<img src="images/trash-can.svg" alt="Delete"> hx-confirm="Are you sure you want to delete this user?"
</a> >
</td> <img src="images/trash-can.svg" alt="Delete">
</tr> </a>
#endfor </td>
</tr>
#endfor
</tbody>
</table> </table>

View File

@@ -6,7 +6,9 @@
<br> <br>
<p>Vendors are who purchase orders can be issued for, they consist of multiple branches / locations.</p> <p>Vendors are who purchase orders can be issued for, they consist of multiple branches / locations.</p>
<br> <br>
#extend("vendors/form", form) #extend("form-container"): #export("formContent"):
#extend("vendors/form", form)
#endexport #endextend
#extend("vendors/table") #extend("vendors/table")
</div> </div>
</div> </div>

View File

@@ -1,24 +1,39 @@
<table id="vendor-table"> <table id="vendor-table">
<tr> <thead>
<th>Name</th> <tr>
<th>Branches</th> <th>Name</th>
<th></th> <th>Branches</th>
</tr> <th>#extend("btn/toggle-form")</th>
</tr>
</thead>
<tbody id="vendor-table-body"> <tbody id="vendor-table-body">
#for(vendor in vendors): #for(vendor in vendors):
<tr id="vendor_#(vendor.id)"> <tr id="vendor_#(vendor.id)">
<td>#capitalized(vendor.name)</td> <td>#capitalized(vendor.name)</td>
<td> <td class="vendor-branches">
#if(vendor.branches): #if(vendor.branches):
<ul> <ul>
#for(branch in vendor.branches): #for(branch in vendor.branches):
<li>#capitalized(branch.name)</li> <li style="list-style-type: none; margin-left: 10px;">
<div class="branch-row">
<div class="branch-name">#capitalized(branch.name)</div>
<a href="javascript:void(0)"
class="btn danger"
hx-delete="/api/v1/vendors/#(vendor.id)/branches/#(branch.id)"
hx-confirm="Are you sure you want to delete this branch?"
hx-target="closest li"
hx-swap="outerHTML swap:0.3s"
>
&times;
</a>
</div>
</li>
#endfor #endfor
</ul> </ul>
#endif #endif
</td> </td>
<!-- TODO: Add edit button --> <!-- TODO: Add edit button -->
<td> <td style="width: 50px;">
<a class="btn btn-delete" <a class="btn btn-delete"
hx-delete="/vendors/#(vendor.id)" hx-delete="/vendors/#(vendor.id)"
hx-target="closest tr" hx-target="closest tr"

View File

@@ -10,12 +10,12 @@ struct EmployeeViewController: RouteCollection {
let employees = routes.protected.grouped("employees") let employees = routes.protected.grouped("employees")
employees.get(use: index(req:)) employees.get(use: index(req:))
employees.get("form", use: employeeForm(req:)) employees.get("form", use: employeeForm(req:))
employees.post(use: postEmployeeForm(req:)) employees.post(use: create(req:))
employees.group(":employeeID") { employees.group(":employeeID") {
$0.get(use: editEmployee(req:)) $0.get(use: edit(req:))
$0.delete(use: deleteEmployee(req:)) $0.delete(use: delete(req:))
$0.put(use: updateEmployee(req:)) $0.put(use: update(req:))
$0.post("toggle-active", use: toggleActiveEmployee(req:)) $0.post("toggle-active", use: toggleActive(req:))
} }
} }
@@ -25,14 +25,13 @@ struct EmployeeViewController: RouteCollection {
} }
@Sendable @Sendable
func postEmployeeForm(req: Request) async throws -> View { func create(req: Request) async throws -> View {
_ = try await api.createEmployee(req: req) _ = try await api.createEmployee(req: req)
let employees = try await api.getSortedEmployees(req: req) return try await req.view.render("employees/index", EmployeesCTX(oob: true, api: api, req: req))
return try await req.view.render("employees/table", ["employees": employees])
} }
@Sendable @Sendable
func toggleActiveEmployee(req: Request) async throws -> View { func toggleActive(req: Request) async throws -> View {
guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else {
throw Abort(.notFound) throw Abort(.notFound)
} }
@@ -43,14 +42,14 @@ struct EmployeeViewController: RouteCollection {
} }
@Sendable @Sendable
func deleteEmployee(req: Request) async throws -> View { func delete(req: Request) async throws -> View {
_ = try await api.deleteEmployee(req: req) _ = try await api.deleteEmployee(req: req)
let employees = try await api.getSortedEmployees(req: req) let employees = try await api.getSortedEmployees(req: req)
return try await req.view.render("employees/table", ["employees": employees]) return try await req.view.render("employees/table", ["employees": employees])
} }
@Sendable @Sendable
func editEmployee(req: Request) async throws -> View { func edit(req: Request) async throws -> View {
guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else {
throw Abort(.notFound) throw Abort(.notFound)
} }
@@ -58,7 +57,7 @@ struct EmployeeViewController: RouteCollection {
} }
@Sendable @Sendable
func updateEmployee(req: Request) async throws -> View { func update(req: Request) async throws -> View {
_ = try await api.updateEmployee(req: req) _ = try await api.updateEmployee(req: req)
return try await req.view.render("employees/index", EmployeesCTX(oob: true, api: api, req: req)) return try await req.view.render("employees/index", EmployeesCTX(oob: true, api: api, req: req))
} }
@@ -88,12 +87,25 @@ private struct EmployeesCTX: Content {
} }
private struct EmployeeFormCTX: Content { private struct EmployeeFormCTX: Content {
let employee: Employee.DTO?
let oob: Bool let htmxForm: HtmxFormCTX<Context>
init(employee: Employee.DTO? = nil) { init(employee: Employee.DTO? = nil) {
self.employee = employee self.htmxForm = .init(
self.oob = employee != nil formClass: "employee-form",
formId: "employee-form",
htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"),
htmxTarget: "#employee-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: employee == nil ? .outerHTML : nil,
context: .init(employee: employee)
)
}
struct Context: Content {
let employee: Employee.DTO?
} }
} }

View File

@@ -5,6 +5,136 @@ struct PurchaseOrderViewController: RouteCollection {
private let api = ApiController() private let api = ApiController()
func boot(routes: any RoutesBuilder) throws { func boot(routes: any RoutesBuilder) throws {
// Do something. let pos = routes.protected.grouped("purchase-orders")
pos.get(use: index(req:))
pos.post(use: create(req:))
}
@Sendable
func index(req: Request) async throws -> View {
let purchaseOrders = try await api.purchaseOrdersIndex(req: req)
let branches = try await api.getBranches(req: req)
let employees = try await api.employeesIndex(req: req)
req.logger.info("Branches: \(branches)")
return try await req.view.render(
"purchaseOrders/index",
PurchaseOrderCTX(
purchaseOrders: purchaseOrders,
form: .create(branches: branches, employees: employees)
)
)
}
@Sendable
func create(req: Request) async throws -> View {
try PurchaseOrder.FormCreate.validate(content: req)
let createdById = try req.auth.require(User.self).requireID()
let create = try req.content.decode(PurchaseOrder.FormCreate.self)
guard let employee = try await Employee.find(create.createdForID, on: req.db) else {
throw Abort(.notFound, reason: "Employee not found.")
}
guard employee.active else {
throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees")
}
let purchaseOrder = create.toModel(createdByID: createdById)
try await purchaseOrder.save(on: req.db)
let purchaseOrders = try await api.purchaseOrdersIndex(req: req)
return try await req.view.render("purchaseOrders/table", ["purchaseOrders": purchaseOrders])
}
}
private struct PurchaseOrderCTX: Content {
let purchaseOrders: [PurchaseOrder.DTO]
let form: PurchaseOrderFormCTX?
}
private struct PurchaseOrderFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
struct Context: Content {
let branches: [VendorBranch.FormDTO]
let employees: [Employee.DTO]
}
static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self {
.init(htmxForm: .init(
formClass: "po-form",
formId: "po-form",
htmxTargetUrl: .post("/purchase-orders"),
htmxTarget: "#po-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: .outerHTML,
context: .init(branches: branches, employees: employees)
))
}
}
extension VendorBranch {
struct FormDTO: Content {
let id: UUID
let name: String
let vendor: Vendor.DTO
}
func toFormDTO() throws -> 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
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
)
}
}
}
private extension ApiController {
func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] {
try await VendorBranch.query(on: req.db)
.with(\.$vendor)
// .sort(Vendor.self, \.$name)
.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

@@ -6,6 +6,7 @@ struct ViewController: RouteCollection {
private let api = ApiController() private let api = ApiController()
private let employees = EmployeeViewController() private let employees = EmployeeViewController()
private let purchaseOrders = PurchaseOrderViewController()
private let users = UserViewController() private let users = UserViewController()
private let vendors = VendorViewController() private let vendors = VendorViewController()
@@ -25,6 +26,7 @@ struct ViewController: RouteCollection {
protected.post("logout", use: logout(req:)) protected.post("logout", use: logout(req:))
// protected.get("users", use: users(req:)) // protected.get("users", use: users(req:))
try routes.register(collection: employees) try routes.register(collection: employees)
try routes.register(collection: purchaseOrders)
try routes.register(collection: users) try routes.register(collection: users)
try routes.register(collection: vendors) try routes.register(collection: vendors)
} }