feat: Working purchase order table and form.
This commit is contained in:
@@ -19,39 +19,14 @@ header {
|
||||
background-color: #14141f;
|
||||
color: #ff66ff;
|
||||
padding: 10px 0;
|
||||
height: 60px;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
#logo {
|
||||
float: left;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
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;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -116,7 +91,7 @@ input[type=submit] {
|
||||
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;
|
||||
color: inherit;
|
||||
border: none;
|
||||
@@ -124,6 +99,21 @@ input[type=text], input[type=password], input[type=email] {
|
||||
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 {
|
||||
outline: none;
|
||||
}
|
||||
@@ -164,50 +154,146 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
|
||||
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 {
|
||||
opacity: 0;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -24,3 +24,21 @@ function updateDropDownSelection(id, contentId) {
|
||||
let content = document.getElementById(contentId).innerHTML;
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
6
Resources/Views/btn/close-form.leaf
Normal file
6
Resources/Views/btn/close-form.leaf
Normal file
@@ -0,0 +1,6 @@
|
||||
<a href="javascript:void(0)"
|
||||
class="closebtn"
|
||||
onClick="toggleContent('form')"
|
||||
>
|
||||
×
|
||||
</a>
|
||||
6
Resources/Views/btn/toggle-form.leaf
Normal file
6
Resources/Views/btn/toggle-form.leaf
Normal file
@@ -0,0 +1,6 @@
|
||||
<a href="javascript:void(0)"
|
||||
onclick="toggleContent('form')"
|
||||
class="btn-add"
|
||||
>
|
||||
+
|
||||
</a>
|
||||
@@ -1,26 +1,12 @@
|
||||
<form class="employee-form"
|
||||
id="employee-form"
|
||||
#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
|
||||
>
|
||||
#extend("htmx-form", htmxForm):
|
||||
#export("formBody"):
|
||||
<input type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
autofocus
|
||||
required
|
||||
#if(employee.firstName): value=#(employee.firstName) #endif
|
||||
#if(context.employee.firstName): value=#(context.employee.firstName) #endif
|
||||
>
|
||||
<br>
|
||||
<input type="text"
|
||||
@@ -28,16 +14,9 @@
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
required
|
||||
#if(employee.lastName): value=#(employee.lastName) #endif
|
||||
#if(context.employee.lastName): value=#(context.employee.lastName) #endif
|
||||
>
|
||||
<br>
|
||||
<input type="submit" value=#if(employee.id): Update #else: Create #endif>
|
||||
#if(employee.id):
|
||||
<button hx-get="/employees/form"
|
||||
hx-target="#employee-form"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
#endif
|
||||
</form>
|
||||
<input type="submit" value=#if(context.employee.id): Update #else: Create #endif>
|
||||
#endexport
|
||||
#endextend
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
<div class="container">
|
||||
<h1>Employees</h1>
|
||||
<br>
|
||||
<p>Employees are who purchase orders can be generated for.</p>
|
||||
<h3>Employees are who purchase orders can be generated for.</h3>
|
||||
<br>
|
||||
</div>
|
||||
#extend("employees/form", form)
|
||||
#extend("form-container"): #export("formContent"):
|
||||
#extend("employees/form", form)
|
||||
#endexport #endextend
|
||||
#extend("employees/table")
|
||||
</div>
|
||||
#endexport
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
<table id="employee-table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
#for(employee in employees):
|
||||
<tr id="employee_#(employee.id)">
|
||||
<td>#capitalized(employee.firstName) #capitalized(employee.lastName)</td>
|
||||
<td style="width: 10%; text-align: center;">
|
||||
#if(employee.active):
|
||||
<a class="toggle"
|
||||
hx-post="/employees/#(employee.id)/toggle-active"
|
||||
hx-target="#employee-table"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<img src="images/toggle-on.svg" alt="Active">
|
||||
</a>
|
||||
#else:
|
||||
<a class="toggle"
|
||||
hx-post="/employees/#(employee.id)/toggle-active"
|
||||
hx-target="#employee-table"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<img src="images/toggle-off.svg" alt="Active">
|
||||
</a>
|
||||
#endif
|
||||
</td>
|
||||
<td style="width: 100px;">
|
||||
<a class="btn btn-delete"
|
||||
hx-delete="/employees/#(employee.id)"
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>
|
||||
<a href="javascript:void(0)"
|
||||
hx-get="employees/form"
|
||||
hx-target="#employee-form"
|
||||
hx-on::after-request="toggleContent('form')"
|
||||
class="btn-add"
|
||||
>
|
||||
+
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
#for(employee in employees):
|
||||
<tr id="employee_#(employee.id)">
|
||||
<td>#capitalized(employee.firstName) #capitalized(employee.lastName)</td>
|
||||
<td style="width: 10%; text-align: center;">
|
||||
#if(employee.active):
|
||||
<a class="toggle"
|
||||
hx-post="/employees/#(employee.id)/toggle-active"
|
||||
hx-target="#employee-table"
|
||||
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 class="btn btn-edit" hx-get="/employees/#(employee.id)"
|
||||
hx-target="#employee-form"
|
||||
>
|
||||
<img src="images/pencil.svg", alt="Edit">
|
||||
#else:
|
||||
<a class="toggle"
|
||||
hx-post="/employees/#(employee.id)/toggle-active"
|
||||
hx-target="#employee-table"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<img src="images/toggle-off.svg" alt="Active">
|
||||
</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>
|
||||
#endfor
|
||||
</tr>
|
||||
#endfor
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
4
Resources/Views/form-container.leaf
Normal file
4
Resources/Views/form-container.leaf
Normal file
@@ -0,0 +1,4 @@
|
||||
<div id="form" style="display: none;" class="form-content">
|
||||
#extend("btn/close-form")
|
||||
#import("formContent")
|
||||
</div>
|
||||
@@ -8,36 +8,6 @@
|
||||
</div>
|
||||
</header>
|
||||
<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")
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
hx-swap-oob="#(htmxSwapOob)"
|
||||
#endif
|
||||
#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
|
||||
hx-disabled-elt="find input[type='text'], find button, find input[type='submit']"
|
||||
>
|
||||
#import("formBody")
|
||||
</form>
|
||||
|
||||
1
Resources/Views/img/pencil.leaf
Normal file
1
Resources/Views/img/pencil.leaf
Normal file
@@ -0,0 +1 @@
|
||||
<img src="images/pencil.svg", alt="Edit">
|
||||
@@ -9,19 +9,6 @@
|
||||
<title>#(title)</title>
|
||||
</head>
|
||||
<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")
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a hx-post="logout" hx-target="#content" hx-trigger="click" hx-swap="outerHTML">Logout</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="sidepanel" id="sidepanel">
|
||||
<a href="javscript:void(0)" class="closebtn" onclick="closeSidepanel()">×</a>
|
||||
<a hx-get="/purchase-orders"
|
||||
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 style="padding-top: 5px;"
|
||||
hx-post="logout"
|
||||
hx-target="#content"
|
||||
hx-trigger="click"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
<button class="openbtn" onclick="openSidepanel()">☰</button>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<form hx-post="/fix-me"
|
||||
>
|
||||
<input type="number"
|
||||
#extend("htmx-form", htmxForm):
|
||||
#export("formBody"):
|
||||
<input type="text"
|
||||
id="workOrder"
|
||||
name="workOrder"
|
||||
placeholder="12345"
|
||||
placeholder="Work Order: 12345"
|
||||
>
|
||||
<br>
|
||||
<!-- TODO: Add vendor drop-down -->
|
||||
<input type="hidden"
|
||||
id="vendorBranchId"
|
||||
name="vendorBranchId"
|
||||
>
|
||||
<!-- TODO: Add employee drop-down -->
|
||||
<input type="hidden"
|
||||
id="employeeId"
|
||||
name="employeeId"
|
||||
>
|
||||
<select id="vendorBranchID" name="vendorBranchID">
|
||||
#for(branch in context.branches):
|
||||
<option value="#(branch.id)">#capitalized(branch.name) - #capitalized(branch.vendor.name)</option>
|
||||
#endfor
|
||||
</select>
|
||||
<br>
|
||||
<select id="createdForID" name="createdForID">
|
||||
#for(employee in context.employees):
|
||||
<option value="#(employee.id)">#capitalized(employee.firstName) #capitalized(employee.lastName)</option>
|
||||
#endfor
|
||||
</select>
|
||||
<br>
|
||||
<input type="text"
|
||||
id="materials"
|
||||
@@ -32,10 +33,11 @@
|
||||
>
|
||||
|
||||
<br>
|
||||
<label for="truckStock">
|
||||
<label for="truckStock">Truck Stock</label>
|
||||
<input type="checkbox"
|
||||
id="truckStock"
|
||||
name="truckStock"
|
||||
>
|
||||
|
||||
</form>
|
||||
<input type="submit" value="Create">
|
||||
#endexport
|
||||
#endextend
|
||||
|
||||
14
Resources/Views/purchaseOrders/index.leaf
Normal file
14
Resources/Views/purchaseOrders/index.leaf
Normal 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
|
||||
@@ -4,18 +4,18 @@
|
||||
<th>Work Order</th>
|
||||
<th>Vendor</th>
|
||||
<th>Materials</th>
|
||||
<th>Employee</th>
|
||||
<th>Created For</th>
|
||||
<th>Truck Stock</th>
|
||||
<th></th>
|
||||
<th>#extend("btn/toggle-form")</th>
|
||||
</tr>
|
||||
<tbody id="po-table-body">
|
||||
#for(po in purchaseOrders):
|
||||
<tr id="po_#(po.id)">
|
||||
<td>#(po.id)</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.employee.firstName) #(po.employee.lastName)</td>
|
||||
<td>#capitalized(po.createdFor.firstName) #capitalized(po.createdFor.lastName)</td>
|
||||
<td>#capitalized(po.truckStock)</td>
|
||||
<td>
|
||||
<!-- TODO: add buttons here -->
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<p>Users are people that can login and generate puchase orders for employees.</p>
|
||||
<br>
|
||||
</div>
|
||||
#extend("users/form", form)
|
||||
#extend("form-container"): #export("formContent"):
|
||||
#extend("users/form", form)
|
||||
#endexport #endextend
|
||||
#extend("users/table")
|
||||
</div>
|
||||
#endexport
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<table id="user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
<th>#extend("btn/toggle-form")</th>
|
||||
</tr>
|
||||
#for(user in users):
|
||||
<tr id="user_#(user.id)">
|
||||
<td>#(user.username)</td>
|
||||
<td>#(user.email)</td>
|
||||
<td style="width: 60px;">
|
||||
<a class="btn btn-delete"
|
||||
hx-delete="/users/#(user.id)"
|
||||
hx-target="#user-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this user?"
|
||||
>
|
||||
<img src="images/trash-can.svg" alt="Delete">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
#endfor
|
||||
</thead>
|
||||
<tbody>
|
||||
#for(user in users):
|
||||
<tr id="user_#(user.id)">
|
||||
<td>#(user.username)</td>
|
||||
<td>#(user.email)</td>
|
||||
<td style="width: 50px;">
|
||||
<a class="btn btn-delete"
|
||||
hx-delete="/users/#(user.id)"
|
||||
hx-target="#user-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this user?"
|
||||
>
|
||||
<img src="images/trash-can.svg" alt="Delete">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
#endfor
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
4
Resources/Views/vendors/index.leaf
vendored
4
Resources/Views/vendors/index.leaf
vendored
@@ -6,7 +6,9 @@
|
||||
<br>
|
||||
<p>Vendors are who purchase orders can be issued for, they consist of multiple branches / locations.</p>
|
||||
<br>
|
||||
#extend("vendors/form", form)
|
||||
#extend("form-container"): #export("formContent"):
|
||||
#extend("vendors/form", form)
|
||||
#endexport #endextend
|
||||
#extend("vendors/table")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
31
Resources/Views/vendors/table.leaf
vendored
31
Resources/Views/vendors/table.leaf
vendored
@@ -1,24 +1,39 @@
|
||||
<table id="vendor-table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Branches</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Branches</th>
|
||||
<th>#extend("btn/toggle-form")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vendor-table-body">
|
||||
#for(vendor in vendors):
|
||||
<tr id="vendor_#(vendor.id)">
|
||||
<td>#capitalized(vendor.name)</td>
|
||||
<td>
|
||||
<td class="vendor-branches">
|
||||
#if(vendor.branches):
|
||||
<ul>
|
||||
#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"
|
||||
>
|
||||
×
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
#endfor
|
||||
</ul>
|
||||
#endif
|
||||
</td>
|
||||
<!-- TODO: Add edit button -->
|
||||
<td>
|
||||
<td style="width: 50px;">
|
||||
<a class="btn btn-delete"
|
||||
hx-delete="/vendors/#(vendor.id)"
|
||||
hx-target="closest tr"
|
||||
|
||||
@@ -10,12 +10,12 @@ struct EmployeeViewController: RouteCollection {
|
||||
let employees = routes.protected.grouped("employees")
|
||||
employees.get(use: index(req:))
|
||||
employees.get("form", use: employeeForm(req:))
|
||||
employees.post(use: postEmployeeForm(req:))
|
||||
employees.post(use: create(req:))
|
||||
employees.group(":employeeID") {
|
||||
$0.get(use: editEmployee(req:))
|
||||
$0.delete(use: deleteEmployee(req:))
|
||||
$0.put(use: updateEmployee(req:))
|
||||
$0.post("toggle-active", use: toggleActiveEmployee(req:))
|
||||
$0.get(use: edit(req:))
|
||||
$0.delete(use: delete(req:))
|
||||
$0.put(use: update(req:))
|
||||
$0.post("toggle-active", use: toggleActive(req:))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,13 @@ struct EmployeeViewController: RouteCollection {
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func postEmployeeForm(req: Request) async throws -> View {
|
||||
func create(req: Request) async throws -> View {
|
||||
_ = try await api.createEmployee(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/index", EmployeesCTX(oob: true, api: api, req: req))
|
||||
}
|
||||
|
||||
@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 {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
@@ -43,14 +42,14 @@ struct EmployeeViewController: RouteCollection {
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func deleteEmployee(req: Request) async throws -> View {
|
||||
func delete(req: Request) async throws -> View {
|
||||
_ = try await api.deleteEmployee(req: req)
|
||||
let employees = try await api.getSortedEmployees(req: req)
|
||||
return try await req.view.render("employees/table", ["employees": employees])
|
||||
}
|
||||
|
||||
@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 {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
@@ -58,7 +57,7 @@ struct EmployeeViewController: RouteCollection {
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func updateEmployee(req: Request) async throws -> View {
|
||||
func update(req: Request) async throws -> View {
|
||||
_ = try await api.updateEmployee(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 {
|
||||
let employee: Employee.DTO?
|
||||
let oob: Bool
|
||||
|
||||
let htmxForm: HtmxFormCTX<Context>
|
||||
|
||||
init(employee: Employee.DTO? = nil) {
|
||||
self.employee = employee
|
||||
self.oob = employee != nil
|
||||
self.htmxForm = .init(
|
||||
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?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,136 @@ struct PurchaseOrderViewController: RouteCollection {
|
||||
private let api = ApiController()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ struct ViewController: RouteCollection {
|
||||
|
||||
private let api = ApiController()
|
||||
private let employees = EmployeeViewController()
|
||||
private let purchaseOrders = PurchaseOrderViewController()
|
||||
private let users = UserViewController()
|
||||
private let vendors = VendorViewController()
|
||||
|
||||
@@ -25,6 +26,7 @@ struct ViewController: RouteCollection {
|
||||
protected.post("logout", use: logout(req:))
|
||||
// protected.get("users", use: users(req:))
|
||||
try routes.register(collection: employees)
|
||||
try routes.register(collection: purchaseOrders)
|
||||
try routes.register(collection: users)
|
||||
try routes.register(collection: vendors)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user