All the sources so far
Here is source\app.d:
import vibe.vibe;
import empcontrol;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
settings.sessionStore = new MemorySessionStore;
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeController);
auto listener = listenHTTP(settings, router);
scope (exit) listener.stopListening();
runApplication();
}
Here is source\empcontrol.d:
module empcontrol;
import vibe.vibe;
import empmodel;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private string realm = "The Lorem Ipsum Company";
private SessionVar!(User, "user") m_user;
this()
{
empModel = new EmployeeModel;
}
void index(string _error = null)
{
string error = _error;
m_user = User.init;
terminateSession;
render!("index.dt", error);
}
@auth
void getAddEmployee(string _authUser, string _error = null)
{
string error = _error;
render!("empadd.dt", departments, paygrades, provinces, error);
}
@errorDisplay!getAddEmployee
void postAddEmployee(Employee e)
{
import std.file;
import std.path;
import std.algorithm;
import vibe.http.auth.digest_auth;
auto pic = "picture" in request.files;
if(pic !is null)
{
string photopath = "none yet";
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath), true);
}
e.photo = photopath;
}
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("all_employees");
}
@auth
void getAllEmployees(string _authUser, string _error = null)
{
string error = _error;
Employee[] emps = empModel.getEmployees;
render!("emplistall.dt", emps, error);
}
@auth
void getEditEmployee(string _authUser, int id, string _error = null)
{
string error = _error;
Employee e = empModel.getEmployee(id);
render!("empedit.dt", e, departments, paygrades, provinces, error);
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
import std.file;
import std.path;
import std.algorithm;
import vibe.http.auth.digest_auth;
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath), true);
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("all_employees");
}
@auth
void getDeleteEmployee(string _authUser, int id, string _error = null)
{
string error = _error;
Employee e = empModel.getEmployee(id);
render!("empdelete.dt", e, error);
}
@errorDisplay!getDeleteEmployee
void postDeleteEmployee(int id)
{
empModel.deleteEmployee(id);
redirect("all_employees");
}
@auth
@errorDisplay!getAllEmployees
void postFindEmployee(string _authUser, string fname, string lname)
{
import std.uni; //so we can use the toUpper() function
string first = toUpper(fname);
string last = toUpper(lname);
Employee e = empModel.findEmployee(first, last);
enforce(e != Employee.init, fname ~ " " ~ lname ~ " not found.");
render!("employee.dt", e);
}
@errorDisplay!index
void postLogin(string email, string password)
{
import vibe.http.auth.digest_auth;
auto scrambled = createDigestPassword(realm, email, password);
bool isAdmin = empModel.isAdmin(email, scrambled);
enforce(isAdmin, "Email and password combination not found.");
User user = m_user;
user.loggedIn = true;
user.email = email;
m_user = user;
redirect("all_employees");
}
private enum auth = before!ensureAuth("_authUser");
private string ensureAuth(HTTPServerRequest req, HTTPServerResponse res)
{
if(!m_user.loggedIn) redirect("/");
return m_user.email;
}
mixin PrivateAccessProxy;
}
And here is source\empmodel.d:
module empmodel;
import mysql;
import std.conv;
import std.array;
struct Employee
{
int id; //row id or record id
string empid; //employee number
string deprt; //department
string paygd; //salary grade
string email; //email address
string pword; //password
string fname; //first name
string lname; //last name
string phone; //phone number
string photo; //ID photo
string street; //street address
string city; //city name
string province; //province name
string postcode; //postal code
}
struct Admin
{
string email; //email address
string pword; //passsword
}
string[] departments =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provinces =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
string[] paygrades =
[
"A100", "A200", "A300", "A400",
"B100", "B200", "B300", "B400",
"C100", "C200", "C300", "C400",
"D100", "D200", "D300", "D400",
"E100", "E200", "E300", "E400",
"F100", "F200", "F300", "F400"
];
class EmployeeModel
{
Connection conn;
this()
{
string url = "host=localhost;port=3306;user=owner;pwd=qwerty;db=empdb";
conn = new Connection(url);
scope(exit) conn.close;
}
ulong addEmployee(Employee e)
{
string sql =
"insert into employees
(
empid,
deprt, paygd, email, pword,
fname, lname, phone, photo,
street, city, province, postcode
)
values(?,?,?,?,?,?,?,?,?,?,?,?,?)";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs
(
to!string(e.empid),
to!string(e.deprt),
to!string(e.paygd),
to!string(e.email),
to!string(e.pword),
to!string(e.fname),
to!string(e.lname),
to!string(e.phone),
to!string(e.photo),
to!string(e.street),
to!string(e.city),
to!string(e.province),
to!string(e.postcode)
);
return conn.exec(pstmt);
}
Employee[] getEmployees()
{
Employee[] emps;
string sql = "select * from employees";
Row[] rows = conn.query(sql).array;
if(rows.length == 0) return emps;
return prepareEmployees(rows);
}
Employee[] prepareEmployees(Row[] rows)
{
Employee[] emps;
foreach(row; rows)
{
Employee e = prepareEmployee(row);
emps ~= e;
}
return emps;
}
Employee prepareEmployee(Row row)
{
Employee e;
e.id = to!int(to!string(row[0]));
e.empid = to!string(row[1]);
e.deprt = to!string(row[2]);
e.paygd = to!string(row[3]);
e.email = to!string(row[4]);
e.pword = to!string(row[5]);
e.fname = to!string(row[6]);
e.lname = to!string(row[7]);
e.phone = to!string(row[8]);
e.photo = to!string(row[9]);
e.street = to!string(row[10]);
e.city = to!string(row[11]);
e.province = to!string(row[12]);
e.postcode = to!string(row[13]);
return e;
}
Employee getEmployee(int id)
{
string sql = "select * from employees where id=?";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs(id);
Employee e;
Row[] rows = conn.query(pstmt).array;
if(rows.length == 0) return e;
return prepareEmployee(rows[0]);
}
ulong editEmployee(Employee e)
{
string sql = "update employees set empid=?,
deprt=?, paygd=?, email=?, pword=?,
fname=?, lname=?, phone=?, photo=?,
street=?, city=?, province=?, postcode=?
where id=?";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs
(
to!string(e.empid),
to!string(e.deprt),
to!string(e.paygd),
to!string(e.email),
to!string(e.pword),
to!string(e.fname),
to!string(e.lname),
to!string(e.phone),
to!string(e.photo),
to!string(e.street),
to!string(e.city),
to!string(e.province),
to!string(e.postcode),
to!int(to!string(e.id))
);
return conn.exec(pstmt);
}
void deleteEmployee(int id)
{
string sql = "delete from employees where id=?";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs(id);
conn.exec(pstmt);
}
Employee findEmployee(string first, string last)
{
Employee e;
string sql = "select * from employees where upper(fname)=? and upper(lname)=?";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs(first, last);
Row[] rows = conn.query(pstmt).array;
if(rows.length == 0) return e;
return prepareEmployee(rows[0]);
}
bool isAdmin(string email, string password)
{
string sql = "select * from admins where email=? and pword=?";
Prepared pstmt = conn.prepare(sql);
pstmt.setArgs(email, password);
Row[] rows = conn.query(pstmt).array;
if(rows.length == 0) return false;
return true;
}
}
Here is views\cssformgrid.dt:
:css
.form-grid-wrapper
{
width: 700px;
height: auto;
margin: 20px auto;
padding: 1px 20px 20px 0;
border-radius: 20px;
background-color: #eef;
}
.form-grid
{
display: grid;
grid-template-columns: 1fr 3fr;
gap: 5px;
}
.center-align
{
text-align: center;
}
.form-grid-label
{
width: 100%;
font-size: 16px;
text-align: right;
padding: 2px;
}
.form-grid-input
{
width: 100%;
font-size: 14px;
padding: 0;
}
.form-grid-field
{
width: 100%;
font-size: 16px;
border-bottom: 1px solid black;
padding: 2px;
}
.form-grid-button
{
width: 100%;
font-size: 14px;
height: 30px;
margin-top: 10px;
}
Here is views\csstable.dt:
:css
.table-wrapper
{
margin: 20px auto;
border-radius: 20px;
}
table
{
margin: 0 auto;
padding: 20px 0;
border-collapse: collapse;
background-color: #eef;
}
table td, table th
{
border: 1px solid black;
margin: 0;
padding: 0 5px;
}
.no-border
{
border: 0;
}
Here is views\empadd.dt:
extends layout
block maincontent
include cssformgrid.dt
-if(error)
div.error-div
span.error-message #{error}
div.form-grid-wrapper
h2.center-align New employee details
form.form-grid(method="post", action="add_employee", enctype="multipart/form-data")
label.form-grid-label Employee number
input.form-grid-input(type="empid", name="e_empid", placeholder="employee number", required)
label.form-grid-label Department
select#deprt.form-grid-input(name="e_deprt")
-foreach(dep; departments)
option(value="#{dep}") #{dep}
label.form-grid-label Salary grade
select#deprt.form-grid-input(name="e_paygd")
-foreach(pay; paygrades)
option(value="#{pay}") #{pay}
label.form-grid-label Email address
input.form-grid-input(type="email", name="e_email", placeholder="email@company.com", required)
label.form-grid-label Password
input.form-grid-input(type="password", name="e_pword", placeholder="password", required)
label.form-grid-label First name
input.form-grid-input(type="text", name="e_fname", placeholder="First name", required)
label.form-grid-label Last name
input.form-grid-input(type="text", name="e_lname", placeholder="Last name", required)
label.form-grid-label Phone
input.form-grid-input(type="text", name="e_phone", placeholder="Phone number")
label.form-grid-label Street address (no city)
input.form-grid-input(type="text", name="e_street", placeholder="Street address", required)
label.form-grid-label City
input.form-grid-input(type="text", name="e_city", placeholder="City", required)
label.form-grid-label Province
select#province.form-grid-input(name="e_province")
-foreach(prov; provinces)
option(value="#{prov[0]}") #{prov[1]}
label.form-grid-label Postal code
input.form-grid-input(type="text", name="e_postcode", placeholder="A1A 1A1")
label.form-grid-label ID Picture
input.form-grid-input(type="file", name="picture")
input(type="hidden", name="e_photo")
input(type="hidden", name="e_id", value="1")
div
div
input.form-grid-button(type="reset", value="Clear the form")
input.form-grid-button(type="submit", value="Submit")
Here is views\empdelete.dt:
extends layout
block maincontent
include cssformgrid.dt
-if(error)
div.error-div
span.error-message #{error}
div.form-grid-wrapper
h2.center-align Are you sure you want to delete this record?
form.form-grid(method="post", action="delete_employee")
span.form-grid-label Employee number:
span.form-grid-field #{e.empid}
span.form-grid-label Department:
span.form-grid-field #{e.deprt}
span.form-grid-label Salary grade:
span.form-grid-field #{e.paygd}
span.form-grid-label Email address:
span.form-grid-field #{e.email}
span.form-grid-label First name:
span.form-grid-field #{e.fname}
span.form-grid-label Last name:
span.form-grid-field #{e.lname}
span.form-grid-label Phone:
span.form-grid-field #{e.phone}
span.form-grid-label Street address:
span.form-grid-field #{e.street}
span.form-grid-label City:
span.form-grid-field #{e.city}
span.form-grid-label Province:
span.form-grid-field #{e.province}
span.form-grid-label Postal code:
span.form-grid-field #{e.postcode}
span.form-grid-label ID Picture:
img(src="#{e.photo}", height="80px")
input(type="hidden", name="id", value="#{e.id}")
div
div
a(href="all_employees")
button.form-grid-button(type="button") Cancel
input.form-grid-button(type="submit", value="Delete")
Here is views\empedit.dt:
extends layout
block maincontent
include cssformgrid.dt
-if(error)
div.error-div
span.error-message #{error}
div.form-grid-wrapper
h2.center-align Edit employee details
form.form-grid(method="post", action="edit_employee", enctype="multipart/form-data")
label.form-grid-label Employee number
input.form-grid-input(type="empid", name="e_empid", value="#{e.empid}")
label.form-grid-label Department
select#deprt.form-grid-input(name="e_deprt", value="#{e.deprt}")
-foreach(dep; departments)
-if(dep == e.deprt)
option(value="#{dep}", selected) #{dep}
-else
option(value="#{dep}") #{dep}
label.form-grid-label Salary grade
select#paygd.form-grid-input(name="e_paygd", value="#{e.paygd}")
-foreach(pay; paygrades)
-if(pay == e.paygd)
option(value="#{pay}", selected) #{pay}
-else
option(value="#{pay}") #{pay}
label.form-grid-label Email address
input.form-grid-input(type="email", name="e_email", value="#{e.email}")
label.form-grid-label Password
input.form-grid-input(type="password", name="e_pword", value="#{e.pword}")
label.form-grid-label First name
input.form-grid-input(type="text", name="e_fname", value="#{e.fname}")
label.form-grid-label Last name
input.form-grid-input(type="text", name="e_lname", value="#{e.lname}")
label.form-grid-label Phone
input.form-grid-input(type="text", name="e_phone", value="#{e.phone}")
label.form-grid-label Street address (no city)
input.form-grid-input(type="text", name="e_street", value="#{e.street}")
label.form-grid-label City
input.form-grid-input(type="text", name="e_city", value="#{e.city}")
label.form-grid-label Province
select#province.form-grid-input(name="e_province", value="#{e.province}")
-foreach(prov; provinces)
-if(prov[0] == e.province)
option(value="#{prov[0]}", selected) #{prov[1]}
-else
option(value="#{prov[0]}") #{prov[1]}
label.form-grid-label Postal code
input.form-grid-input(type="text", name="e_postcode", value="#{e.postcode}")
label.form-grid-label ID Picture
img(src="#{e.photo}", height="100px")
div
input.form-grid-input(type="file", name="picture")
input(type="hidden", name="e_photo", value="#{e.photo}")
input(type="hidden", name="e_id", value="#{e.id}")
input(type="hidden", name="id", value="#{e.id}")
div
div
a(href="all_employees")
button.form-grid-button(type="button") Cancel
input.form-grid-button(type="submit", value="Submit")listall.dt:
Here is views\emplistall.dt:
extends layout
block maincontent
include csstable.dt
-if(error)
div.error-div
span.error-message #{error}
div.table-wrapper
table
tr
th Employee Id
th First name
th Last name
th Department
th Phone number
th Email address
th Street address
th City
th Province
th PostCode
th Action
-foreach(e; emps)
tr
td #{e.empid}
td #{e.fname}
td #{e.lname}
td #{e.deprt}
td #{e.phone}
td #{e.email}
td #{e.street}
td #{e.city}
td #{e.province}
td #{e.postcode}
td
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e.id}")
input(type="image", src="images/pencil.ico", height="15px")
|
form.form-hidden(method="get", action="delete_employee")
input(type="hidden", name="id", value="#{e.id}")
input(type="image", src="images/trash.ico", height="15px")
|
Here is views\employee.dt:
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2.center-align Employee details
div.form-grid
span.form-grid-label Employee number:
span.form-grid-field #{e.empid}
span.form-grid-label Department:
span.form-grid-field #{e.deprt}
span.form-grid-label Salary grade:
span.form-grid-field #{e.paygd}
span.form-grid-label Email address:
span.form-grid-field #{e.email}
span.form-grid-label First name:
span.form-grid-field #{e.fname}
span.form-grid-label Last name:
span.form-grid-field #{e.lname}
span.form-grid-label Phone:
span.form-grid-field #{e.phone}
span.form-grid-label Street address:
span.form-grid-field #{e.street}
span.form-grid-label City:
span.form-grid-field #{e.city}
span.form-grid-label Province:
span.form-grid-field #{e.province}
span.form-grid-label Postal code:
span.form-grid-field #{e.postcode}
span.form-grid-label ID Picture:
img(src="#{e.photo}", height="80px")
div
div
a(href="all_employees")
button.form-grid-button(type="button") Close
Here is views\index.dt:
extends layout
block maincontent
h2 The Lorem Ipsum Company
-if(error)
div.error-div
span.error-message #{error}
div.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nam nec urna arcu. Quisque eleifend posuere vestibulum.
In sed magna mauris. Phasellus bibendum ligula et placerat
vulputate. In non suscipit lectus, a laoreet odio. Donec
at sapien eu nisi porta condimentum. Morbi non varius ex,
nec luctus nisl. Aenean varius dui quis arcu auctor luctus.
Integer efficitur ornare massa, ac suscipit enim sagittis et.
Proin vestibulum tellus in ipsum ultrices, sed imperdiet
sapien euismod. Praesent vel facilisis mauris. Proin finibus
congue tellus, non varius ante. Nullam tincidunt dolor felis.
Pellentesque non luctus tellus. Curabitur et sapien at justo
fringilla feugiat et a erat.
<br /><br />
Here is views\layout.dt:
doctype 5
html
head
title Employee Timekeeping System
include styles.dt
body
div.container
div.menu
include menu.dt
div.content
block maincontent
div.footer
include footer.dt
Here is views\menu.dt:
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="all_employees") All employees
div.menu-item
a.item-link(href="#find_employee") Find employee
div.menu-item
a.item-link(href="add_employee") Add employee
div.menu-item
a.item-link.item-link-right(href="#login") Login
div#find_employee.modal-form
div.modal-form-wrapper-find_employee
form.modal-form-grid(method="post", action="find_employee")
input.text-control(name="fname", type="text", placeholder=" First name", required)
input.text-control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Find")
a.close(href="#close") Cancel
div#login.modal-form
div.modal-form-wrapper-login
form.modal-form-grid(method="post", action="login")
input.text-control(name="email", type="email", placeholder=" email address")
input.text-control(name="password", type="password", placeholder=" password")
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Login")
a.close(href="#close") Cancel
Here is views\footer.dt:
div.copyright Copyright © The Lorem Ipsum Company 2023
Here is views\styles.dt:
:css
body
{
margin: 0;
padding: 0;
}
.container
{
display: grid;
grid-template-rows: 40px auto 30px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.menu
{
position: fixed;
top: 0;
width: 100%;
padding: 10px 0;
background-color: whitesmoke;
}
.menu-item
{
display: inline;
}
.item-link
{
margin-left: 30px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 18px;
color: teal;
text-decoration: none;
}
.item-link-right
{
float: right;
margin-right: 30px;
}
.modal-form
{
position: fixed;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.5);
z-index: 99999;
opacity: 0;
pointer-events: none;
}
#login:target, #find_employee:target
{
opacity: 1;
pointer-events: auto;
}
.modal-form-grid
{
display: grid;
grid-template: 30px 30px 30px / 1fr 1fr;
grid-gap: 10px;
}
.text-control
{
grid-column: 1 / 3;
}
#but-reset
{
grid-column: 1 / 2;
}
#but-submit
{
grid-column: 2 / 3;
}
.modal-form-wrapper-login
{
width: 250px;
position: absolute;
right: 0;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
.modal-form-wrapper-find_employee
{
width: 250px;
position: relative;
left: 280px;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
.content
{
padding: 40px 30px;
}
.footer
{
position: fixed;
bottom: 0;
width: 100%;
background-color: lightgray;
padding: 5px 0;
}
.copyright
{
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 14px;
text-align: center;
color: teal;
}
.form-hidden
{
padding: 0;
margin: 0;
display: inline;
}
.error-div
{
width: 90%;
margin: 20px auto;
}
.error-message
{
color: brown;
font-weight: bold;
}
And here is dub.json:
{
"authors": [
"Owner"
],
"copyright": "Copyright © 2023, Owner",
"dependencies": {
"mysql-native": "~>3.2.0",
"vibe-d": "~>0.9"
},
"description": "A simple vibe.d server application.",
"license": "proprietary",
"name": "lorem"
}
Now on to the timekeeping system.
Last updated