feat: Working purchase order table and form.
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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"
|
#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>
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
+
|
||||||
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>
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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>
|
<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>
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
<nav>
|
<div class="sidepanel" id="sidepanel">
|
||||||
<ul class="nav-links">
|
<a href="javscript:void(0)" class="closebtn" onclick="closeSidepanel()">×</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()">☰</button>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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>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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
4
Resources/Views/vendors/index.leaf
vendored
4
Resources/Views/vendors/index.leaf
vendored
@@ -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>
|
||||||
|
|||||||
31
Resources/Views/vendors/table.leaf
vendored
31
Resources/Views/vendors/table.leaf
vendored
@@ -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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user