The timekeeping system
A new project
As in the previous project, we are going to assume this project is going to be deployed in an intranet setting so we won’t have to worry about security and huge numbers of users accessing data at the same time. Most security concerns are beyond the control of a web app itself anyway, like fronting with an nGinx or Apache proxy server, hiding behind a firewall, setting up a CA certificate like the free Let’s Encrypt for HTTPS encryption, hardening MySQL, etc.
So create a new project named loremtime.
C:\Users\Owner>cd \vibeprojects
C:\vibeprojects>dub init loremtime -t vibe.d
Package recipe format (sdl/json) [json]:
Name [loremtime]:
Description [A simple vibe.d server application.]:
Author name [Owner]:
License [proprietary]:
Copyright string [Copyright 2023, Owner]:
Add dependency (leave empty to skip) []:
Success created empty project in C:\vibeprojects\loremtime
Package successfully created in loremtime
C:\vibeprojects>cd loremtime
And add the mysql-native library to the project.
C:\vibeprojects\loremtime>dub add mysql-native
Adding dependency mysql-native ~>3.2.2
C:\vibeprojects\loremtime>Then, using MySQL Workbench, create a new database named loremdb. After that, we are ready to build the timekeeping system for the Lorem Ipsum company.
The schema
The timekeeping system needs to record the time-in and time-out actions of the employees, like a timecard, so we need a timecard model.
Then, after every salary period, in our case weekly, a timesheet is produced summarizing the number of hours employees worked for the week, so we need a timesheet model.
Here is the structure for the timecard:
Here is the structure for the timesheet:
The reason the empid and fullname are included is to eliminate the need for the timesheet to look up the employee number and names of employees from the employees table.
This time we will create the administrators table. So we need an admins model, like this:
We already have the employees table, so that’s covered.
The timesheet will simply extract and summarize data from the timecards. The timecards will become permanent records while the timesheet will be overwritten every time it is generated. The timecards can be edited but the timesheet does not need to be edited as it depends on the timecards.
The base model
All of the models will be connecting to the same database with the same URL for the connections. To reduce code duplication, we will create a base model.
Create the base model in source\basemodel.d:
Our base model connects to the database at its creation, which will be inherited by all of the models. Of course, you replace the user and password with your own.
The employee model
Edit source\empmodel.d with this contents. The highlighted lines are only for emphasis; there are some small changes here and there so better copy the whole thing:
We added the line
import basemodel;
near the top to make the BaseModel class visible.
The line
class EmployeeModel : BaseModel
means EmployeeModel inherits from BaseModel, which is another way of saying the EmployeeModel class is derived from the base class BaseModel, or EmployeeModel is a subclass of BaseModel.
The line
int[string] payrates =
means create an associative array of integers with strings as keys.
We created this associative array:
So when we say
payrates[“A300”]
it will give us the value of 50.
The line
foreach(k,v; payrates)
can be read as “for each key and value pair in payrates, do the following”. Of course, “k” and “v” are random variable names; you can name them “rate” and “pay” and they will have the same meaning:
foreach(rate,pay; payrates)
Through that construct, we were able to populate the payrates table with data from the payrates associative array.
The timecard model
Create source\cardmodel.d with this contents:
Here we also indicated that TimecardModel is also derived from BaseModel.
The timesheet model
Create source\sheetmodel.d:
And we also indicated here that TimesheetModel is derived from BaseModel.
The administrators model
We are adding an administrators model so in the future we can simply provide a facility to add, edit and delete administrators.
Create source\adminmodel.d:
We added the getAdmin() method as we already used this earlier.
The base controller
We are going to make several controllers, most of them paired to a model. This will make debugging files easier as well as follow the notion of separation of concerns. Besides, having all the business logic code in one controller file will make the file big and unwieldy.
To avoid duplication of code, we are going to make a base controller class that hosts the common code and make all the other controllers inherit from it. We will call it BaseController.
Here is source\basecontrol.d:
There are two common accessibility attributes for variables in D: public and private.
A public variable is accessible from the class and from outside of the class and subclasses.
A private variable is accessible only to the class where it was declared; even subclasses do not have access.
In D, variables are public by default.
A protected variable is not visible to other classes except subclasses.
So, in a sense, a protected variable is “protected” from other classes while accessible from subclasses. You can say it is a cross between a public and a private variable.
We instantiated all the models here so they are ready for use by all the controllers that inherit from this class. We also included the ensureAuth() method here so they are also inherited by the other controllers.
The employee controller
We have already used the employee controller earlier so let’s review it. This time, the employee controller will inherit from the base controller.
Here is source\empcontrol.d; there are changes here and there so simply copy the whole file:
We made the EmployeeController inherit from BaseController.
The login system and a new menu
We want the menu reflect the login status. For example, once the login is successful, the menu item should change from ‘Login’ to ‘Logout’. Also, some parts of the menu should have different links depending on the login status.
Here is views\layout.dt:
Here is views\menu.dt:
Here is the views\footer.dt which remains the same:
Here is views\cssclock.dt:
Here is views\cssfooter.dt:
Here is views\cssformgrid.dt:
Here is views\csslayout.dt:
Here is views\cssmenu.dt:
And here is views\csstable.dt:
The index, time in and time out templates
Here is the views\index.dt:
This is the image I used for the punch in part which I named timein.png:

And this is the image I used for the punch out part, which I named timeout.png:

And these are the other images we used earlier:
The pencil.png:

And the trash.png:

Place all four pictures into public\images.
Here is the views\timein.dt:
And here is views\timeout.dt:
As usual, make sure all the tabs are consistent or you will get errors when compiling. To make sure, click on the ‘Tab size:’ feature on the footer of VS Code and click on either ‘Convert indentation to Spaces’ or ‘Convert indentation to Tabs’ on the drop-down that appears, whichever is your preference. The important thing is to be consistent. Then save the file again.
The employee views
Here is views\empadd.dt:
Here is views\empdelete.dt:
Here is views\empedit.dt:
Here is views\emplist.dt:
Here is views\empshow.dt:
The home controller and app.d
We don’t have a controller to displays the home page which is in views\index.dt. Let’s create a new controller, which we will name the HomeController, which will also inherit from BaseController.
Here is the source\homecontrol.d file:
Here we added the postLogin() and getLogout() methods which do not belong to other controllers.
Since we created another controller and we want it as another web interface, we have to register it to the router, so we have to make changes to app.d.
Here is source\app.d:
Now we can test the whole thing.
Compile, run and refresh the browser. If you see this screen, you did good.

Displaying the punch-in page
If you click on the ‘Punch In’ button, you only get an error like this:

That’s because we don’t have a handler yet for the /time_in link. Let’s create the timecard controller to handle all timecard-related actions.
Here is source\cardcontrol.d:
Since TimecardController is another web interface, we have to register it to the router in app.d.
Before we test it, let us create some database tables.
Edit source\homecontrol.d:
So that when the HomeController is instantiated, the admins table is created. After compilation, you should delete these lines:
Edit source\empcontrol.d by adding this code at the beginning of the class declaration:
This means the employees table and the payrates table will be created when EmployeeController is instantiated.
Go ahead and test the whole thing by compiling, running and refreshing the browser.
Then erase this code from source\empcontrol.d file or else we will always be erasing the employee data:
this()
{
empModel.createTable;
empModel.createPayrates;
}
And erase this from source\homecontrol.d file or else we will always be erasing the admin data:
this()
{
adminModel.createTable;
}
After compilation, erase those lines above or else we will always be recreating those tables, erasing all data every time we compile and run.
Adding employees
After compiling, running and refreshing the browser, click on the ‘Punch In’ button. You should see something like this.

The dropdown list of employees doesn’t show because our employees table has no data since we just created it. So let’s add some employees to the database first.

But then we are redirected to the Login dialogue box, so let’s log in first.
User: admin1@lorem.com
Password: secret

After logging in, we are brought back to the index page, but this time, the ‘Login’ prompt on the menu becomes ‘Logout’, indicating that we are now logged in.

So click on the ‘New employee’ once again to add new employees. Which brings us to this screen.

Add some new employees so we can fill up the employees list like this:

Now let’s go back to the home page and click on the ‘Punch In’ button again. This time, the drop-down list of employees is populated.

Select an employee and click on the ‘Punch In’ button. You see this error:

So let’s create the timecards table.
Edit source\cardcontrol.d:
We only added these lines to call the createTable() method on the timecard model:
this()
{
cardModel.createTable;
}
After compiling, delete these lines or else you will always lose the timecard data every time you compile and run the app.
this()
{
cardModel.createTable;
}
Punching in
When punching in, the system should check if the employee already punched in earlier before a new record is created so there is no duplication.
After compiling, go back to the home page and click on the ‘Punch In’ button. We get this error:

So let’s edit source\cardcontrol.d and append this code:
The lines
string empid = empidname[0..4];
string fullname = empidname[5..$];
mean we are extracting a substring from a string. In D, parts of arrays are called slices, so a substring is a slice. Strings in D are treated like an array of characters so getting substrings out of strings is relatively trivial. Indexing starts with zero as in other languages, but ranges are exclusive of the upper bound. The $ character refers to the length of the array. This means the slice 0..4 actually gets characters 0 to 3, which are four characters.
An example of an empidname data looks like this:
empidname = ‘1002 Emma Rob’
So the line
string empid = empidname[0..4];
means the variable empid will get the characters from 0 to 3, which is ‘1002’.
The line
string fullname = empidname[5..$];
means the variable fullname will get the characters from index 5 up to the end of the string, which is ‘Emma Rob’.
The postTimeIn() method is calling two cardModel methods, noTimeIn() and timeIn(), so let’s write them.
Open source\cardmodel.d and append this code:
The noTimeIn() method checks if the user has timed in previously and is calling the getToday() method, so we added that too.
Compile, run and refresh the browser. Then click on the ‘Punch In’ button again. Select an employee and click ‘Punch In’.
This time we get a successful message.

So go ahead and punch in all the other employees.
Viewing the timecards
Click on the ‘View timecards’ on the menu to see the timecards.
But then you get an error message.

Edit source\cardcontrol.d and append this code:
This code calls cardModel.getTimecards(), which we haven’t written yet, so let’s write that.
Edit source\cardmodel.d and append this code:
If you are logged in, meaning you are an administrator, you get to view all the timecards at whatever date. However, ordinary employees only get to view the timecards of the day so they can review their status for the day.
The cardControl.getTimecards() method needs the cardlist.dt template, so let’s create it.
Here is the views\cardlist.dt:
Compile, run and refresh the browser. If you are not logged in, you see the list of time cards only for the day.

Punching out
When an employee punches out, the system should check first if the employee already punched in earlier, meaning, the record already exists and the timeout column with a null value. If not, then the employee cannot punch out since there is no timeout to edit. If the record exists, then the app should check if the employee has already punched out earlier, in that case the employee cannot punch out again.
As of now, when you click the ‘Punch Out’ button on the home page, this is what we get:

So let’s edit source\cardcontrol.d and append this code:
The postTimeOut() method is calling two new methods from cardModel: noTimeOut() and timeOut(), so let’s write them.
Edit source\cardmodel.d and append this code:
Then compile, run and go back to the home page and click on the ‘Punch Out’ button. This time, the punch out screen appears.

Go ahead and punch out some employees. You get a message each time, either successful

or unsuccessful

Then click on the ‘View timecards’ link on the menu to see the list of timecards. This time, you see the punch out times and hours.

When an employee complains he/she cannot punch out because he/she forgot to punch in, the supervisor (who should be one of the administrators) could simply login to edit or add records.
The steps to follow by the employee should be:
Punch in even though it is late; this will create the record
Then punch out
Let the admin know that you forgot to punch in earlier
The admin edits the punch-in time
So let us mimic the administrator.
Go ahead and log in. Remember that ‘secret’ is the password. After a successful login, the ‘Login’ item on the menu becomes ‘Logout’.
Then click on the ‘View timecards’ link on the menu to see the list of timecards. This time, the ‘edit’ and ‘delete’ icons appear.

Because you have logged in as an administrator, you can now edit or delete any record in the timecards table.
Editing a timecard entry
But then, when you click on one of the pencil icons to edit a record, you get an error message because we still haven’t written the editing part yet.

Open source\cardcontrol.d and append this code:
This code is calling the TimecardModel methods getTimecard() and editTimecard() as well as rendering the cardedit.dt template, so let’s write them.
Append this code to source\cardmodel.d:
And here is views\cardedit.dt:
Now we can compile, run and refresh the browser. Go back to the home page to refresh the browser. Try to log in again. Then try to view the timecards again.
Click on one of the pencil icons. This time it will open the timecard-editing page.

Now you can edit either the time-in or the time-out or both, as long as you follow the format of the date and time, including the space in between.
Deleting a timecard entry
As we have done with the employee records earlier, when the user clicks on the delete button (the trash image), the record should be shown on another page and ask the user for confirmation. That is what we will do next.
Append this code to source\cardcontrol.d:
The getDeleteTimecard() is rendering the carddelete.dt, so let’s create it.
Here is views\carddelete.dt:
The postDeleteTimecard() method is calling the cardModel.deleteTimecard(), so let’s write it.
Append this code to source\cardmodel.d:
Now compile, run and go back to the home page to refresh the browser. Open the list of timecards.
There are no ‘edit’ and ‘delete’ icons. You have to log in again because this is a new version of the app and the session values were erased when you recompiled and ran again.
Open the list of timecards again. This time you will see the ‘edit’ and ‘delete’ icons.
Click on one of the trash icons to open the record in a separate page.

And if you press ‘Delete’, the record is gone from the list.
Generating the timesheet
The timesheet computes the hours worked by each employee at end of wage period, in this case weekly. It looks up the hours from the timecards table and generates the timesheet based on the timecards data. The timesheet is overwritten every time it is created but the timecards remain so they are permanent records.
So let’s edit source\sheetmodel.d:
We added the method createTimesheet() to the model which is the code that actually creates the timesheet, as well as the getTheWeek() method which will be needed by the TimesheetController.
Here is source\sheetcontrol.d:
The getCreateTimesheet() method is calling cardModel.getTheWeeks() before rendering the form sheetcreate.dt, so let’s create them.
Append this code to source\cardmodel.dt:
And here is views\sheetcreate.dt:
The sheetControl.getTimesheets() does the actual display of the timesheet by calling sheetModel.getTimesheets() and sheetModel.getTheWeek(), which we have already written, then rendering sheetlist.dt, which we haven’t created yet, so let’s create it.
Here is views\sheetlist.dt:
Now compile and run the app.
Try to log in again. Then click on the ‘Create timesheet’ link, then choose a week from the drop-down list.

Click on the ‘Generate timesheet’ button.

And you get an error message.

So let’s create the timesheets table.
Edit source\sheetcontrol.d and add this code right after the class declaration:
Then compile and run.
After compiling and running, delete the code that you have just added!
this()
{
sheetModel.createTable;
}
Then compile again.
Click on the ‘Create timesheet’ link. You are redirected to the login dialogue.
Login again, then click on the ‘Create timesheet’ button again.
After selecting the week and clicking ‘Generate timesheet’, you should see this:

You should be able to see the total number of hours the employees worked for the week. As mentioned before, the timesheet is not editable because it simply extracts the data from the timecards table. The timecards table is editable and is a permanent record while the timesheets table is simply generated when needed.
The end
That’s it, we reached the end of this tutorial.
If you want to give me some feedback, you can email me at karitoy@gmail.com.
The following pages show all the source code.
The main program
Here is source\app.d:
The base controller
Here is source\basecontrol.d:
The base model
The home controller
Here is source\homecontrol.d:
The employees controller
Here is source\empcontrol.d:
The employee model
Here is source\empmodel.d:
The timecard controller
Here is source\cardcontrol.d:
The timecard model
Here is source\cardmodel.d:
The timesheet controller
Here is source\sheetcontrol.d:
The timesheet model
Here is source\sheetmodel.d:
The CSS files
Here is views\cssclock.dt:
Here is views\cssfooter.dt:
Here is views\cssformgrid.dt:
Here is views\csslayout.dt:
Here is views\cssmenu.dt:
Here is views\csstable.dt:
The timecard views
views\cardelete.dt:
views\cardedit.dt:
views\cardlist.dt:
views\timein.dt:
views\timeout.dt:
The employee views
views\ empadd.dt:
views\empdelete.dt:
views\empedit.dt:
views\emplist.dt:
views\empshow.dt:
The layout views
views\layout.dt:
views\menu.dt:
views\footer.dt
The home view
views\index.dt:
The timesheet views
views\sheetcreate.dt:
views\sheetlist.dt:
The project configuration file
dub.json:
…and that’s the last one.
That’s all, bye!
Last updated