Heavy Workload to Create an Attendance System?

Keith Chan
17 min readDec 26, 2020
Photo by Mikael Kristenson on Unsplash

This is a story about solving a real life problem. Ordinary software solutions are away over-weighted for a simple attendance system.

Serve a Need

A non-profit organization (Non-Government Organization, as known as “NGO”) organizes gatherings and needs to record attendance of participants. Attendance rate is needed to determine the eligibility of the participant to upgrade to the next level. The organizer will consider to contact the participant who absent for a number of times.

Simple Task

It is a simple task, really simple. There are thousands of spreadsheet templates freely downloadable, just print out and use a ball pen to take attendance, then finished. But, wait, the organizer needs to know the attendance rate, should we calculate on sheet of paper manually?

It is twenty-first century now, obviously we want the attendance record can be recorded and stored in the cloud or somewhere on Internet and can be accessed securely on any computers including handsets.

Relatively Heavy Workload

A normal approach to achieve this requirement is creating a web application with database. User needs to manage the list of participants, selected the attended person for specific date to finish the attendance taking process. Finally the system will generate report for users.

The effort to establish a web application with database is similar to create a portal website or a CMS (Content Management System). Since the attendance system is extremely simple, the workload of the above “normal” approach is already over-weighted.

Basic Requirements

Let’s see the basic requirements:

  1. Participants are separated in a few groups and each group have a group leader
  2. Attendance rate reset every year
  3. Newcomers are welcome to join in the middle of year
  4. Attendance rate of newcomers are calculated from the first attended date, not first gathering date of the year. For example, there are 50 gatherings this year, a newcomer attended all gatherings from 26th to 50th, therefore this newcomer has 100% attendance rate even he/she absent in first 25 gatherings
  5. Attendance report is need

From the above requirements, the NGO created systems to serve this purpose.

Attendance System — QR Code

QR code is designed to transfer a small amount of data. It is fascinating because it can be used to promote your website, or used as a movie ticket or prove of purchase. After scanning a code, you may get more information you need from a hyperlink, or show the QR code received by email on your handset to enter the movie theatre.

The NGO stores email addresses of every participants, then send an email with QR code to each person before each gathering. The QR code contains the participant ID and the date. The person only needs to show the QR code from the received email in the gathering venue, the attendance taker has a dedicated tablet to scan it.

The administration team needs to manage the list of existing participants. It is a lot of workload because the gatherings are opened to everyone and every newcomer can join in at any gathering. Therefore the administration team needs to update the name list with email addresses after nearly every gathering.

Another problem is there are two different programs need to maintain as follow:

  1. Participant management, with QR code generation and email sending
  2. QR code scanning and report generation

Finally, the administration team found the system malfunction and found no one to fix it. The system is abandoned.

Attendance System — One-Click

After using the fascinating QR code system, the team tried to simplify the attendance system. The new design is simple, the system shows the participant names, when anyone attend, just click the name. It is easy and simple, and the team found QR code is not a must.

This approach eliminates the email sending and QR code scanning processes which is largely simplified the whole workflow. However, the administration team still need to manage the list of participants.

There is another problem, the budget of this project is limited. The NGO is not storing the data on the cloud, no matter the QR code or one-click approach. The one-click system and those data are stored in a single computer, which is a tablet. The system is unavailable if the person who take the tablet is absent.

A Feasible Solution Has Been Done Elsewhere

In my working experience, I implemented an attendance function for another NGO. That NGO already has a web-based system which manages the application of courses for their members. Apparently, attendance taking is just an additional function.

Since the database backed web application is already available, adding an attendance taking function is easy. Unfortunately, this web-application approach is inapplicable for current case.

Detailed Aim and User Requirements

After the previous experiences, user state clear of the aims and added some new requirements for the upcoming system:

Aims:

  1. Contact member if they absent for a few gatherings
  2. Attendance rate needs to up to a limit to get reward

Additional Requirements:

  1. Easy to learn
  2. Stable platform
  3. Convenient to generate statistical data
  4. Prevent accidental data deletion
  5. Able to access by different electronic devices
  6. Issuing IDs to participants is not necessary, only creates more work. It is because people are free to join the gathering anytime and have no membership card. There is no way for people to remember their ID numbers

On the other hand, there are also some unsay requirements. Since the NGO is not aim to make profit, and all people are volunteers, not paid staffs. In summary, the NGO has the following nice to have requirements:

  1. Low or even no cost to store the system and data
  2. Volunteers spend as little effect as possible to get the job done

A Think of Simple Solution — Manually Input

A simple solution must be out there. It will be prefect if no programming is need at all. But due to a customized report is needed, some programming work should be there.

The participants are in different groups and group leaders are helping to gather them in the group. Therefore, group leaders must familiar with people in the group. Therefore, it is a good idea to let group leaders to take attendance for their group.

Just an example if Barry, Robin and Maurice are in Group A, and they attended on 24/12/2020, group leader of Group A should send the follow line to an specific email address.

2020-12-24, Group A, Barry, Robin, Maurice

It is easy for everyone who can type and send email. Finally someone should consolidate those received emails to create the following data.

2020-12-24, Group A, Barry, Robin, Maurice2020-12-24, Group B, Richard, Karen2020-12-24, Group C, John, Paul, George, Ringo2020-12-24, Group D, Jakie, Tito, Jermaine, Marlon, Michael

It is easy for programmers to generate attendance report from the above data.

Improving and Simplifying the Solution

For the above email approach, we need an administrator to check email and consolidate the entries. For simplification, it would be great if there is a shared spreadsheet that all group leaders can enter data. Google Sheets can be shared for group leaders to edit, however, it doesn’t guide them what to input. To resolve this problem, Google Forms can help.

Google form created

Create the form as above with a field for newcomers, then go to “Responses” tab, click the Google Sheets icon and create a new spreadsheet as follow.

Click “Responses” on the top, then click green icon on the right
Click “Create” on bottom-right

The following is an example of attendance from 10 to 24 December. John is the newcomer on second gathering; Jermaine absent for the first one.

Google form responses

Select all (click the box on left of “A” and top of “1”), copy and paste the above text, you will get the following:

Timestamp Date Group Participants Newcomers
12/25/2020 21:18:39 12/10/2020 Group A Barry, Robin, Maurice
12/25/2020 21:19:06 12/10/2020 Group B Richard, Karen
12/25/2020 21:19:46 12/10/2020 Group C Paul, George, Ringo
12/25/2020 21:20:56 12/10/2020 Group D Jakie, Tito, Marlon, Michael
12/25/2020 21:28:13 12/17/2020 Group A Barry, Robin, Maurice
12/25/2020 21:29:11 12/17/2020 Group B Richard, Karen
12/25/2020 21:29:36 12/17/2020 Group C John, Paul, George, Ringo John
12/25/2020 21:29:51 12/17/2020 Group D Jakie, Tito, Jermaine, Marlon, Michael
12/25/2020 21:32:30 12/24/2020 Group A Barry, Robin, Maurice
12/25/2020 21:32:47 12/24/2020 Group B Richard, Karen
12/25/2020 21:33:06 12/24/2020 Group C John, Paul, George, Ringo
12/25/2020 21:33:21 12/24/2020 Group D Jakie, Tito, Jermaine, Marlon, Michael

Programing for this Task

The only program we need is the one which display the attendance report. My first choice of the programming language for this application is Javascript. It is because it is simple and can be easily run in any web browser on any computer, as I created a simple sliding puzzle with Javascript.

The following source code “attendanceViewer.html” also available in Github. There are some parameters in “Customizable Values” on the top of the source code which you can change as you want.

<html>
<head>
<script>
// *************************************************************
// Customizable Values
// *************************************************************
const NEW_SIGN = "*New"; // Text to indicate new member (e.g. "*" or "*New*")
const PASS_RATE = 75; // Passing percentage (e.g. 75)
const DECI_PLACE = 1; // Decimal placing (e.g. 1 or 2)
const DATE_DISPLAY_TIME = 2500; // Date display time after mouse out in milliseconds (e.g. 1000 = 1 sec.)
// *************************************************************
// End of Customizable Values
// *************************************************************
// Constant Values
const PRESENT = "O"; // Present Symbol
const ABSENT = "X"; // Absent Symbol
const INDEX_OF_DATE = 1;
const INDEX_OF_GROUP = 2;
const INDEX_OF_NAMELIST = 3;
const INDEX_OF_NEWCOMERLIST = 4;
const NUMBER_OF_FIELDS_B4_ATTENDANCE = 4; // Num of fields before attendance record
// Sort by 1st Column
function sortByFirstColumn(a, b) {
if (a[0] === b[0]) {
return 0;
}
else {
return (a[0] < b[0]) ? -1 : 1;
}
}
// Set Date
function setDate(obj, displayDate) {
var totalTime = 1600; // Total time in milliseconds
var step = 5; // Motion step (totalTime and 100 must be multiple of steps)
var motion = 100/step;
obj.innerHTML = displayDate;
setInterval(function(){ while(motion<=100) {obj.style.fontSize=`${motion+=(100/step)}%`;} }, totalTime/step);
}
// Set empty
function setEmpty(obj) {
var totalTime = 2500; // Total time in milliseconds
var step = 5; // Motion step (totalTime and 100 must be multiple of step)
var motion = 100;
setInterval(function(){ obj.style.fontSize=`${motion-=(100/step)}%`; }, totalTime/step);
setTimeout(function(){ obj.innerHTML=""; }, totalTime); // Empty
}
// Generate Report
function generate(groupFilter) {
var fullText;
var rawLines;
var lines = new Array();
var tempYear, tempMonth, tempDate;
var dateArray = new Array();
document.getElementById("inputPanel").style.display = "none";// Split by EOL (Win, Mac or Unix)
fullText = document.getElementById("data").value.trim()+"\t\t";
fullText = fullText.split("\r\n").join("\n"); // Convert Win (CR LF) to Unix (LF)
fullText = fullText.split("\r").join("\n"); // Convert Mac (CR) to Unix (LF)
rawLines = fullText.split("\n"); // Lines

// Split by Tab - timestamp, date, group, nameList
for (var all=0; all<rawLines.length; all++) {
lines.push(rawLines[all].split("\t"));
}
for (var all=1; all<lines.length; all++) {
// Convert Date Format to YYYY-MM-DD (from M/D/YYYY)
// Change this part if the input date format is different
tempMonth = lines[all][INDEX_OF_DATE].split("/")[0];
tempDate = lines[all][INDEX_OF_DATE].split("/")[1];
tempYear = lines[all][INDEX_OF_DATE].split("/")[2];

lines[all][1] = tempYear + "-" +
((tempMonth < 10)?"0"+tempMonth:tempMonth) + "-" +
((tempDate < 10)?"0"+tempDate:tempDate);
// Push all dates in
dateArray.push(lines[all][INDEX_OF_DATE]);
}
// Distinct (by Set) and sort
dateArray = new Array(... new Set(dateArray)).sort();
// Newcomer List
var newcomerArray = new Array();
// Name List
var nameArray = new Array();
// Reverse Order - Participant goes to last declare group
for (var all=lines.length-1; all>0; all--) {
// Temp Newcomers
var tempNewcomers = new Array();
// Split Newcomers
tempNewcomers = lines[all][INDEX_OF_NEWCOMERLIST].split(",").filter(n => n.trim().length > 0);
// Distinct Newcomers
for (var nameIndex=0; nameIndex<tempNewcomers.length; nameIndex++) {
// Push all newcomers in
newcomerArray.push(tempNewcomers[nameIndex].trim());
}
// Distinct (by Set) and sort
newcomerArray = new Array(... new Set(newcomerArray)).sort();
// Temp Names
var tempNames = new Array();
// Split Names
tempNames = lines[all][INDEX_OF_NAMELIST].split(",").filter(n => n.trim().length > 0);
//tempNames = tempNames.filter(function() { return true; });
// Distinct Names
for (var nameIndex=0; nameIndex<tempNames.length; nameIndex++) {
var isNew = true;
for (var finalNameIndex=0; finalNameIndex<nameArray.length; finalNameIndex++) {
// Compare Names
if (nameArray[finalNameIndex][0] == tempNames[nameIndex].trim()) {
isNew = false;
}
}
// Check is newcomer
var isNewcomer = false;
for (var newcomerIndex=0; newcomerIndex<newcomerArray.length; newcomerIndex++) {
if (tempNames[nameIndex].trim() == newcomerArray[newcomerIndex].trim()) {
isNewcomer = true;
}
}
if (isNew) {
// New participant found
// Participant data - {Name, Group, isNewcomer, ""}
// The empty string is space for attendance rate
var paritcipantData = new Array(tempNames[nameIndex].trim(), lines[all][INDEX_OF_GROUP], ((isNewcomer)?true:false), "");
// Mark attendance date, if don't know yet, put 0
for (var dateIndex=0; dateIndex<dateArray.length; dateIndex++) {
if (dateArray[dateIndex] == lines[all][INDEX_OF_DATE]) { // Set Present if date matched
paritcipantData.push(PRESENT);
} else {
paritcipantData.push(ABSENT);
}
}
// Push Into Name Array
nameArray.push(paritcipantData);
} else {
// Find the participant
for (var participantIndex=0; participantIndex<nameArray.length; participantIndex++) {
if (nameArray[participantIndex][0] == tempNames[nameIndex].trim()) {
// Set as newcomer
nameArray[participantIndex][2] = isNewcomer;
// Participant already in list, mark attendance date
for (var dateIndex=0; dateIndex<dateArray.length; dateIndex++) {
if (dateArray[dateIndex] == lines[all][INDEX_OF_DATE]) { // Set Present if date matched
// There are 4 fields ahead the attendance data
nameArray[participantIndex][dateIndex+NUMBER_OF_FIELDS_B4_ATTENDANCE] = PRESENT;
}
}
}
}
}
}
}
nameArray.sort(sortByFirstColumn);
// Distinct Groups
var groupArray = new Array();
for (var all=1; all<lines.length; all++) {
// Push all groups in
groupArray.push(lines[all][INDEX_OF_GROUP].trim());
}
// Distinct (by Set) and sort
groupArray = new Array(... new Set(groupArray)).sort();
// Calculate Attendance Rate
for (var nameIndex=0; nameIndex<nameArray.length; nameIndex++) {
var countAttend = 0;
var totalDaysFromFirst = 0;
for (var attend=NUMBER_OF_FIELDS_B4_ATTENDANCE; attend<dateArray.length+NUMBER_OF_FIELDS_B4_ATTENDANCE; attend++) {
if (nameArray[nameIndex][attend] == PRESENT) {
// Accumulate attended dates
countAttend++;
// If attended, set the total days from first date
if (totalDaysFromFirst == 0) {
totalDaysFromFirst = dateArray.length + NUMBER_OF_FIELDS_B4_ATTENDANCE - attend;
}
}
}
// Set attendance rate
var decimalPlace = Math.pow(10, DECI_PLACE);
var rateFromFirstAttend = Math.round(countAttend / totalDaysFromFirst * 100 * decimalPlace) / decimalPlace;
var rateAllDates = Math.round(countAttend / dateArray.length * 100 * decimalPlace) / decimalPlace;
nameArray[nameIndex][3] = (
(nameArray[nameIndex][2])? // isNewcomer?
`<span class="${(rateFromFirstAttend < PASS_RATE)?"fail":"pass"}">${rateFromFirstAttend}%</span>` : // From first attend
`<span class="${(rateAllDates < PASS_RATE)?"fail":"pass"}">${rateAllDates}%<span>` // All dates count
);
}
// Final Display
var show = "";
// Filter
show += `<br/>${lines[0][INDEX_OF_GROUP]}: `;
show += `<span class="dropdown">`;
show += ` <select onchange="generate(this.value);">`;
show += ` <option value='' ${(groupFilter=="")?"selected":""}> *** ALL *** </option>`;
for (var g=0; g<groupArray.length; g++) {
show += ` <option ${(groupFilter==groupArray[g])?"selected":""}>${groupArray[g]}</option>`;
}
show += ` </select> &nbsp;&nbsp;`;
show += `</span>`;
// Input Again Button
show += `<input type="button" class="button" value=" Input Again " `;
show += ` onclick='`;
show += ` document.getElementById("inputPanel").style.display = "block"; `;
show += ` document.getElementById("report").style.display = "none"; `;
show += ` '/>`;
show += `<br/><br/>`;
// Display Table
show += `<table>`;
show += ` <tr id="header" class="row">`;
show += ` <th>${lines[0][INDEX_OF_NAMELIST]}</th>`;
show += ` <th>${lines[0][INDEX_OF_GROUP]}</th>`;
show += ` <th>Rate</th>`;
show += ` <th>Attendance</th>`;
show += ` </tr>`;
for (var nameIndex=0; nameIndex<nameArray.length; nameIndex++) {
// Display All or Filtered by Group
if (groupFilter == "" || nameArray[nameIndex][1] == groupFilter) {
show += ` <tr class="row ${nameArray[nameIndex][1]}">`; // Set Group Name as Class
for (var dataIndex=0; dataIndex<dateArray.length+NUMBER_OF_FIELDS_B4_ATTENDANCE; dataIndex++) {
if (dataIndex < NUMBER_OF_FIELDS_B4_ATTENDANCE) {
if (dataIndex != 2) { // 2 - isNewcomer
// Name, Group, Rate
show += ` <td>${nameArray[nameIndex][dataIndex]}`;
show += (dataIndex == 0 && nameArray[nameIndex][2])?`<span class='new_sign'>&nbsp;${NEW_SIGN}</span>`:``; // Name && isNewcomer
show += `</td>`;
}
} else {
// Date List
var tempMonth = parseInt(dateArray[dataIndex-NUMBER_OF_FIELDS_B4_ATTENDANCE].split("-")[1], 10);
var tempDate = parseInt(dateArray[dataIndex-NUMBER_OF_FIELDS_B4_ATTENDANCE].split("-")[2], 10);
if (dataIndex == NUMBER_OF_FIELDS_B4_ATTENDANCE) {
// Starting of attendance bar
show += `<td><table class="attendance-bar"><tr>`;
}
// Attendance
var color = ((nameArray[nameIndex][dataIndex] == PRESENT)?"green":"red");
show += ` <td style="background-color: ${color};" `;
show += ` onmouseover="this.innerHTML='&nbsp;${tempDate}/${tempMonth}&nbsp;';" `;
show += ` onmouseout="setTimeout(()=>{this.innerHTML='';}, DATE_DISPLAY_TIME);">`;
show += ` </td>`;
if (dataIndex == dateArray.length+NUMBER_OF_FIELDS_B4_ATTENDANCE-1) {
// Ending of attendance bar
show += `</tr></table></td>`;
}
}
}
show += ` </tr>`;
}
}
show += `</table>`;
// Display
document.getElementById("report").innerHTML = show;
document.getElementById("report").style.display = "block";
}
</script>
<style>
div {
text-align: center;
font-family: Arial;
font-size: 16px;
}
textarea {
outline: none;
resize: none;
border: 5px solid #4CAF50;
border-radius: 25px;
font-size: 16px;
}
table {
margin: 0 auto;
text-align: center;
border-collapse: collapse;
border-radius: 12px;
font-family: Arial;
font-size: 20px;
}
th:first-child { border-radius: 15px 0 0 0; } /* Table Top Left */
th:last-child { border-radius: 0 15px 0 0; } /* Table Top Right */
tr:last-child td:first-child { border-radius: 0 0 0 15px; } /* Table Bottom Left */
tr:last-child td:last-child { border-radius: 0 0 15px 0; } /* Table Bottom Right */
th, td { padding: 1.2rem; font-size: 1.3rem; }
tr, td { padding: 8px; transition: .2s ease-in; }
tr:first-child {background: #4CAF50; } /* Header Color */
tr {background: #9CFFA0; } /* Row Background Color */
tr:nth-child(even) { background: #7CDF90; } /* Even Row Background Color */
tr:hover:not(#firstrow), tr:hover td:empty {
background: #CCFFC7; /* Hover Row Background Color */
pointer-events: visible;
}
tr:hover:not(#firstrow) {
transform: scale(1.02);
font-weight: 700;
box-shadow: 0px 3px 7px rgba(0, 0, 0, 0.5);
}
.fail {
font-weight: bold;
color: red;
}
.new_sign {
font-size: .7em;
font-style: italic;
font-weight: bold;
vertical-align: super;
color: red;
}
.button {
background-color: #4CAF50;
border: none;
border-radius: 12px;
color: #FFFFFF;
padding: 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
}
.dropdown {
position: relative;
display: inline-block;
vertical-align: middle;
margin: 10px; /* demo only */
}
.dropdown select {
background-color: #4CAF50;
color: #FFFFFF;
font-size: inherit;
padding: .5em;
padding-right: 2.5em;
border: 0;
margin: 0;
border-radius: 12px;
text-indent: 0.01px;
text-overflow: '';
-webkit-appearance: button; /* hide default arrow in chrome OSX */
}
.attendance-bar {
border: 1px solid grey;
border-spacing: 4px;
border-collapse: separate;
}
.attendance-bar tr:hover {
/*transform: scale(1) !important;*/
transform: none !important;
box-shadow: 0 0 0 0 !important;
}
.attendance-bar tr td {
color: white !important;
width: 5px !important;
height: 25px !important;
padding: 0px !important;
border-radius: 0px !important;
}
</style>
</head>
<body>
<div id="inputPanel">
<textarea id="data" rows="15" cols="100"
onchange="this.value=this.value.trim();"
onkeyup="this.value=this.value.trim();"
oninput="this.value=this.value.trim();"></textarea>
<br/><br/>
<input type="button" class="button" value="Clear" onclick="document.getElementById('data').value=''" />
<input type="button" class="button" value="Generate Report" onclick="generate('');" />
</div>
<div id="report"/>
</body>
</html>

Download the code or copy and paste, and save as “attendanceViewer.html”, then open the file in any web browser, the following screen will be shown.

“attendanceViewer” loaded in browser

Copy and paste all data from the worksheet into the textbox.

Paste data in textarea

After clicking “Generate Report”, the report will be shown.

Report generated for all participants, filter by group using the dropdown box on the top

The attendance data is presented in color bars, the attendance dates will be shown when the mouse pointer rolling on them. Also, there is a drop-down box to filter the data by group.

Final Deployment

Surely it can be run on the browser on any computer, but if you want to put it online, Google Sites is a free choice. Create a Google site, double-click empty space, click the “Embed” icon (<>), click “Embedded Code” and paste all code inside.

After double-click the Google Sites page

How this Approach Fulfill All Requirements

Let’s review the requirements:

  1. Easy to learn — Fill in form, copy & paste should be easy enough
  2. Stable platform — Run on Google Sites or local computer
  3. Convenient to generate statistical data — Only need to paste data and click a button
  4. Prevent accidental data deletion — The data worksheet is owned by the form owner, group leaders can only submit form but cannot edit the worksheet
  5. Able to access by different electronic devices — Always online, accessible by any web browser
  6. Issuing IDs to participants is not necessary, only creates more work. It is because people are free to join the gathering anytime and have no membership card. There is no way for people to remember their ID numbers — No ID is need

Hidden requirements:

  1. Low or even no cost to store the system and data — All services are free of charge
  2. Volunteers spend as little effect as possible to get the job done — Paste source code to Google Sites, submit form, copy & paste data

Feedbacks from Users and Follow-up

I asked all users of this system for their feedbacks after a few month actual operation. Here are those feedbacks and follow-up actions.

Repeatedly input all participant names is tedious, it is easier to input them by clicking checkboxes.

Very good suggestion, but if checkboxes of participant names is available, someone should manage the name list. I just think of making checkboxes in Javascript (yes, Javascript again!), by checked those names and click a button, the name list is copied and ready to be pasted to the Google form. In such case, group leaders should manage the name list of their own group, which is reasonably easy since they are family with their group members.

Group leaders should modify the following source file “nameList.html” to enter their group member names before use. The source code below is ready for Group C. It is also available in Github.

<html>
<script>
// ******************************************************************
// Enter names into double quotes (""), separated with comma (,)
// ******************************************************************
var names = ["John", "Paul", "George", "Ringo"];
// ******************************************************************
// End of Customizable Values
// ******************************************************************
names.sort();

function concat() {
var finalStr = "";
for (i=0; i<names.length; i++) {
if (document.getElementById("n"+i).checked) {
finalStr += ((finalStr=="")?"":", ") + names[i];
}
}
document.getElementById("display").value = finalStr;
}

function setAll(check) {
for (i=0; i<names.length; i++) {
document.getElementById("n"+i).checked = check;
}
concat();
}

function copyToClipboard() {
var copyText = document.getElementById("display");
copyText.select();
copyText.setSelectionRange(0, 99999)
document.execCommand("copy");
alert("Copied the text: " + copyText.value);
}
</script>
<body>
<form>
<button onclick="setAll(true); return false;"> All </button>
<button onclick="setAll(false); return false;"> None </button>
<br/>
<script>
for (i=0; i<names.length; i++) {
document.write('<input id="n' + i + '" type="checkbox" onclick="concat();">' + names[i] + '</input> ');
}
</script>
<br/><br/><input type="text" id="display">
<br/><br/><button onclick="copyToClipboard(); return false;"> Copy to Clipboard </button>
</form>
</body>
</html>

I suggest group leaders to embed the above code in their own Google Sites. Therefore they can manage and access it easily. Another advantage is pages in Google Sites are already compatible for large and mobile device displays. The following is an example of how it looks on mobile device.

“nameList.html” loaded in mobile device

It is good if it can generates PDF / Google Docs by clicking a button.

I don’t know is it possible to generate a Google Docs, it sounds not so easy. For PDF, instead of “generate”, actually we can print the web page as PDF.

Display on screen
Print preview — all format lost

However, the original design is solely for display on screen. I will suggest users decide the printout format and then I can create a printout-friendly version of same program.

I found the report count the attendance of a newcomer wrongly. Finally, I found that the name of the newcomer must be entered in both “Participants” and “Newcomers” to generate the correct report, but I think the name entered in “Newcomers” is enough.

Not all group leaders think the same, some of them think the same way of me. When I designed the input method, I think the name should be inputted in “Participants” if he/she attended, no matter he/she is newcomer or not. The “Newcomers” field is just for indication of the category of that person.

I think it is no right or wrong of this design. We just need to compromise how the program behave.

Thoughts

This is a voluntian job, an interesting experience since it is very different from business projects. It leads me to think out of the box, provides me a problem solving experience by adding limitations to the requirement.

This solution is not going to happen in business project. Imagine how can you bill your customer by letting them copy and paste from a worksheet?

Just for Fun

Those names I used in the examples are real people and the grouping is not arbitrary. Can you tell who they are?

--

--