Ever since I turned an Excel file into the Buy Currency App for Michelle in the administration department, we became closer. This time, I ask her to help me get prepared for my business trip.
Michelle goes through my Business Trip Application form, identifies if a visa is required for the country I am going, and if I need any insurance or accommodation arrangements, etc. etc. Then, she logs on to our file server where lots of templates and forms are stored and copies relevant Excel forms. She emails those to me, asking me to fill them out and send them back to her so that she can take care of everything else for me.
I appreciate her help -- but again, search manually and copy forms around? isn't there a more efficient way to do this? What if a system can load these forms and combine the required sheets for us?
I quickly draw a plan in my head: list down all the form names in an Excel sheet, give them each a checkbox, and at the end add a "build book" button. Import it to Keikai Spreadsheet, wire the logic on the button -- upon receiving the clicking event, check the value of the checkboxes, and pull out all the relevant (checked) sheets from various files, insert them to the same book. The resulting book can either be a Web sheet or can be exported as an Excel book.
Yes, should be easily possible in 2 hours.
First, Michelle gives me the sheet she designs to select template forms. Import it to Keikai in a zul, then our main user interface is done:
<spreadsheet apply="io.keikai.devref.usecase.ReportGeneratorController"
height="100%" width="100%"
src="/WEB-INF/books/report/Travel Management.xlsx"
maxVisibleRows="12" maxVisibleColumns="6"
hidecolumnhead="true" hiderowhead="true"/>
From the screenshot, you can see I hide toolbar, sheetbar, formula bar, and row/column heading to make Keikai more like a normal web page.
Following my plan, I ask Michelle to give me those 4 xlsx
template files. Each xlsx
file name match its form name e.g. Travel Insurance Form.xlsx
. What I need to do is to import all template Excel files into Keikai for future use.
1 public class ReportGeneratorController extends SelectorComposer {
2 private static Map<String, Book> templateMap = new HashMap();
3 ...
4 @Override
5 public void doAfterCompose(Component comp) throws Exception {
6 super.doAfterCompose(comp);
7 ...
8 importTemplates();
9 }
10 ...
11 private void importTemplates() throws IOException {
12 if (templateMap.size() > 0){
13 return; // already imported
14 }
15 int row = table.getRow();
16 int lastRow = table.getLastRow();
17 int templateColumn = table.getLastColumn();
18 for ( ; row <= lastRow ; row++) {
19 String name = Ranges.range(spreadsheet.getSelectedSheet(), row, templateColumn).getCellValue().toString() ;
20 Book templateBook = Importers.getImporter().imports(new File(DEFAULT_TEMPLATE_FOLDER, name+ ".xlsx"), name);
21 templateMap.put(name, templateBook);
22 }
23 }
Book
) into a Map without assigning them to a Spreadsheet
. Because I don't need to show templates at the beginning.Since there's no checkbox control in the current Keikai version I use, I put a checkbox symbol in each cell instead. Each time a user clicks the checkbox, my code will switch the cell between the checked checkbox and the unchecked one.
ON_CELL_CLICK
Register an ON_CELL_CLICK
event listener by @Listen
, then each time a user click a cell. Keikai will invoke this method in the controller.
@Listen(Events.ON_CELL_CLICK + "= spreadsheet")
public void onCellClick(CellMouseEvent e){
if (inCheckMarks(e)) {
toggleCheckMark(getRange(e));
}...
}
I can change a cell value by Range.setCellValue()
.
private static String NOT_CHECKED = "\uD83D\uDDF8";
private static String CHECKED = "✓";
...
private void toggleCheckMark(Range checkMarkCell) {
if (checkMarkCell.getCellValue().equals(NOT_CHECKED)){
checkMarkCell.setCellValue(CHECKED);
}else{
checkMarkCell.setCellValue(NOT_CHECKED);
}
}
Finally, I need to copy those sheets from selected templates to my personalized report when clicking "Build" button. Listen ON_CELL_CLICK
and check if someone clicks the "Build" button at cell E11
.
...
@Listen(Events.ON_CELL_CLICK + "= spreadsheet")
public void onCellClick(CellMouseEvent e){
if (inCheckMarks(e)) {
...
}else if (inBuildButton(e)){
build();
}
}
Then copy the selected template into a new book.
1 private void build() {
2 Book newReport = Books.createBook("newReport");
3 int row = table.getRow();
4 int lastRow = table.getLastRow();
5 for ( ; row <= lastRow ; row++) {
6 if (isChecked(Ranges.range(spreadsheet.getSelectedSheet(), row, table.getColumn()))){
7 String fileName = Ranges.range(spreadsheet.getSelectedSheet(), row, table.getLastColumn()).getCellValue().toString();
8 Book template = templateMap.get(fileName);
9 Ranges.range(newReport).cloneSheetFrom(template.getSheetAt(0).getSheetName(), template.getSheetAt(0));
10 }
11 }
12 if (newReport.getNumberOfSheets() > 0 ){
13 spreadsheet.setBook(newReport);
14 enableEditMode();
15 }
16 }
Book
without any sheet.Range.cloneSheetFrom()
.Spreadsheet
, so that Keikai will show new book in a browser.After creating a custom report based on templates, I should make sheet bar visible and enlarge the visible rows and columns.
private void enableEditMode() {
spreadsheet.setShowSheetbar(true);
spreadsheet.setMaxVisibleRows(40);
spreadsheet.setMaxVisibleColumns(15);
}
That's all! Now I can produce a custom Excel report by selecting desired sheets from checkboxes. This simple app works like this:
The small "custom reporting" program is now done, and I am ready to depart for my trip. I have a few ideas to make the app more complete after I am back:
There are some common fields that reside in every template, like "Category" or "Form Number". Instead of having to read the value of the field at different cells (e.g. A3 or D4) on each template, I shall define a named range for such cell in each template so that I can easily read all the common fields using the same line of code:
Ranges.rangeByName("category").getCellValue();
I will integrate our program with our existing SSO service and database, by doing so I can pre-fill some personal information e.g. Name when inserting a template sheet so that the end user does not have to fill out personal information again and again in multiple sheets:
Ranges.rangeByName("user-name").setCellValue(userName);
After a user submits a report, I will append the sheet name with a consistent prefix and make it, for example, JohnDoe_InsuranceApplication for ease of reading:
Ranges.range(spreadsheet.getBook().getSheetAt(0)).setSheetName(prefix + sheetName);
I hope you enjoy reading my article. The complete source code of this report generator is available at Github.
Just run the project and visit http://localhost:8080/dev-ref/usecase/reportGenerator.zul