Combining user-built UI and button actions turning Excel forms into a Web application.
Pag navigation is done by switching sheets and importing different files.
<?xml version="1.0" encoding="UTF-8"?> <zk xmlns="http://www.zkoss.org/2005/zul" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.zkoss.org/2005/zul http://keikai.io/2019/zul/zul.xsd"> <spreadsheet id="spreadsheet" width="100%" apply="demo.workflow.WorkflowComposer" src="/WEB-INF/books/workflow.xlsx" maxVisibleRows="17" maxVisibleColumns="15" vflex="1" hiderowhead="true" hidecolumnhead="true"/> </zk>
package demo.workflow; import demo.Util; import io.keikai.api.*; import io.keikai.api.SheetProtection; import io.keikai.api.model.*; import io.keikai.ui.Spreadsheet; import io.keikai.ui.event.*; import org.zkoss.zk.ui.*; import org.zkoss.zk.ui.select.SelectorComposer; import org.zkoss.zk.ui.select.annotation.*; import java.io.*; import java.util.*; /** * - an employee can submit a form as a {@link Submission} * * a supervisor * - can select one from submission list to review and approve (or reject) a submission * - can't edit an employee's submission * - one cell click event listener for each sheet */ public class WorkflowComposer extends SelectorComposer<Component> { @Wire private Spreadsheet spreadsheet; //sheet name private static final String MAIN = "main"; private static final String SUBMISSION_LIST = "submission list"; private static final String FORM_LIST = "form list"; //display related private Book mainBook; private Range roleCell; private static final int START_ROW = 5; private static final String BUTTON1 = "button1"; private static final String BUTTON2 = "button2"; private static final SheetProtection ALLOW_SELECT = SheetProtection.Builder.create().withSelectLockedCellsAllowed(true).withAutoFilterAllowed(true).build(); private static final SheetProtection READ_ONLY = SheetProtection.Builder.create().withSelectLockedCellsAllowed(true).build(); //workflow related private final static String SUPERVISOR = "Supervisor"; private final static String EMPLOYEE = "Employee"; private String currentRole; private final static String CANCEL = "Cancel"; private final static String SUBMIT = "Submit"; private final static String REJECT = "Reject"; private final static String APPROVE = "Approve"; private List<Submission> submissionList = new LinkedList<>(); private List<File> formList = new LinkedList<>(); private final static String FORM_FOLDER = "/WEB-INF/books/form/"; private boolean submissionChange = true; private WorkflowDao workflowDao = new WorkflowDao(); private Submission submissionToReview; //the submission under review @Override public void doAfterCompose(Component comp) throws Exception { super.doAfterCompose(comp); mainBook = spreadsheet.getBook(); spreadsheet.setShowSheetbar(false); spreadsheet.setShowToolbar(false); roleCell = Ranges.rangeByName(mainBook.getSheetAt(0), "Role"); navigateTo(MAIN); } @Listen(Events.ON_CELL_CLICK + "= #spreadsheet") public void onClick(CellMouseEvent event) { String sheetName = event.getSheet().getSheetName(); switch (sheetName) { case MAIN: handleClickMain(event); break; case FORM_LIST: handleClickFormList(event); break; case SUBMISSION_LIST: handleClickSubmissionList(event); break; case "form": handleClickForm(event); break; } } private void navigateTo(String sheetName) { spreadsheet.setSelectedSheet(sheetName); if (MAIN.equals(sheetName)) { spreadsheet.focusTo(roleCell.getRow(), roleCell.getColumn()); } } /** * navigateToList a user to a sheet according to the current role */ private void navigateToList() { switch (currentRole) { case SUPERVISOR: navigateTo(SUBMISSION_LIST); break; case EMPLOYEE: navigateTo(FORM_LIST); break; } } private void handleClickMain(CellMouseEvent event) { if (isEnterClicked(getClickedCell(event))) { enter(); } } private void handleClickFormList(CellMouseEvent event) { if (isLeaveClicked(getClickedCell(event))) { navigateTo(MAIN); } else if (event.getColumn() >= 2 && event.getColumn() <=4) { int index = getClickedCell(event).getRow() - START_ROW; if (index >= 0 && index < formList.size()) { File selectedForm = formList.get(index); try { Book book = Importers.getImporter().imports(selectedForm, selectedForm.getName()); spreadsheet.setBook(book); addButtons(book.getSheetAt(0)); Ranges.range(spreadsheet.getSelectedSheet()).protectSheet(ALLOW_SELECT); } catch (IOException e) { e.printStackTrace(); } } } } private void handleClickSubmissionList(CellMouseEvent event) { if (isLeaveClicked(getClickedCell(event))) { navigateTo(MAIN); } else if (event.getColumn() >= 2 && event.getColumn() <= 6) { int index = getClickedCell(event).getRow() - START_ROW; if (index >= 0 && index < submissionList.size()) { submissionToReview = submissionList.get(index); if (submissionToReview.getState() == Submission.State.WAITING){ showSubmission(submissionToReview); Ranges.range(spreadsheet.getSelectedSheet()).protectSheet(READ_ONLY); } } } } private void showSubmission(Submission submission) { try { Book form = Importers.getImporter().imports(new ByteArrayInputStream(submission.getForm().toByteArray()), submission.getFormName()); spreadsheet.setBook(form); addButtons(form.getSheetAt(0)); } catch (IOException e) { throw new RuntimeException(e); } } private void handleClickForm(CellMouseEvent event) { Range button1 = Ranges.rangeByName(event.getSheet(), BUTTON1); Range button2 = Ranges.rangeByName(event.getSheet(), BUTTON2); Range clickedCell = getClickedCell(event); if (clickedCell.asString().equals(button1.asString()) || clickedCell.asString().equals(button2.asString())) { String label = clickedCell.getCellValue().toString(); switch (label) { case APPROVE: approve(); break; case REJECT: reject(); break; case SUBMIT: submitForm(); break; } exitForm(); } } private void approve() { submissionToReview.setState(Submission.State.APPROVED); submissionToReview.setLastUpdate(Calendar.getInstance().getTime()); workflowDao.update(submissionToReview); submissionChange = true; } private void reject() { submissionToReview.setState(Submission.State.REJECTED); submissionToReview.setLastUpdate(Calendar.getInstance().getTime()); workflowDao.update(submissionToReview); submissionChange = true; } private void submitForm() { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { lockCells(); Exporters.getExporter().export(spreadsheet.getBook(), outputStream); Submission submission = new Submission(); submission.setForm(outputStream); submission.setFormName(spreadsheet.getBook().getBookName()); submission.setOwner(this.currentRole); workflowDao.insert(submission); submissionChange = true; } catch (IOException e) { throw new RuntimeException(e); } } /** * set cells locked to avoid being modify by a supervisor */ private void lockCells() { final String[] unlockedCells1 = {"E4", "E5", "D6"}; final String[] unlockedCells2 = {"D6", "D8", "J6", "L6", "D11", "J12", "D14"}; String bookName = spreadsheet.getBook().getBookName(); switch (bookName){ case "Leave Application.xlsx": lockCells(unlockedCells1); break; case "Business Trip Request.xlsx": lockCells(unlockedCells2); break; } } private void lockCells(String... cells){ for (String cell : cells){ Range range = Ranges.range(spreadsheet.getSelectedSheet(), cell); CellStyle oldStyle = range.getCellStyle(); EditableCellStyle newStyle = range.getCellStyleHelper().createCellStyle(oldStyle); newStyle.setLocked(true); range.setCellStyle(newStyle); } } private void exitForm() { spreadsheet.setBook(mainBook); navigateToList(); populateList(); } /** * add available buttons into a sheet according to the current role. */ private void addButtons(Sheet sheet) { Range button1 = Ranges.rangeByName(sheet, BUTTON1); Range button2 = Ranges.rangeByName(sheet, BUTTON2); switch (currentRole) { case SUPERVISOR: button1.setCellValue(REJECT); button2.setCellValue(APPROVE); break; case EMPLOYEE: button1.setCellValue(CANCEL); button2.setCellValue(SUBMIT); break; } } private boolean isLeaveClicked(Range range) { return range.getCellValue() != null && range.getCellValue().toString().equals("Leave"); } private boolean isEnterClicked(Range range) { return range.getCellValue() != null && range.getCellValue().toString().equals("Enter"); } private Range getClickedCell(CellMouseEvent event) { return Ranges.range(spreadsheet.getSelectedSheet(), event.getRow(), event.getColumn()); } private void enter() { currentRole = roleCell.getCellValue().toString(); navigateToList(); populateList(); Ranges.range(spreadsheet.getSelectedSheet()).protectSheet(ALLOW_SELECT); } private void populateList() { switch (currentRole) { case SUPERVISOR: showSubmissionList(); break; case EMPLOYEE: if (formList.size() == 0) { loadFormList(); showFormList(); } break; } } private void showSubmissionList() { if (submissionChange) { clearTable(); submissionList = workflowDao.queryAll(); Sheet sheet = mainBook.getSheet(SUBMISSION_LIST); for (int index = 0; index < submissionList.size() - 1; index++) { Ranges.range(sheet, START_ROW, 2).toRowRange().insert(Range.InsertShift.DOWN, Range.InsertCopyOrigin.FORMAT_LEFT_ABOVE); } for (int index = 0; index < submissionList.size(); index++) { Submission s = submissionList.get(index); Util.setCellValuesInRow(Ranges.range(sheet, START_ROW + index, 2) , s::getId, s::getFormName, s::getOwner, s.getState()::toString, s::getLastUpdate); } submissionChange = false; } } private void clearTable() { Sheet sheet = mainBook.getSheet(SUBMISSION_LIST); if (submissionList.size() > 1) { Ranges.range(sheet, START_ROW, 2, START_ROW + submissionList.size() - 2, 6).delete(Range.DeleteShift.UP); } } private void showFormList() { Sheet sheet = mainBook.getSheet(FORM_LIST); for (int index = 0; index < formList.size() - 1; index++) { Ranges.range(sheet, START_ROW, 2).toRowRange().insert(Range.InsertShift.DOWN, Range.InsertCopyOrigin.FORMAT_LEFT_ABOVE); } for (int index = 0; index < formList.size(); index++) { Ranges.range(sheet, START_ROW + index, 2).setCellValue(formList.get(index).getName()); } } private void loadFormList() { File[] fileList = new File(WebApps.getCurrent().getRealPath(FORM_FOLDER)).listFiles((File pathname) -> pathname.isFile() && pathname.getPath().endsWith(".xlsx")); formList = Arrays.asList(fileList); } }
package demo.workflow; import java.util.*; /** * Data Access Object (DAO), simulates a persistence layer to access a simulated database */ public class WorkflowDao { private List<Submission> submissionTable = new LinkedList<>(); //simulated database table private int id = 0; public List<Submission> queryAll(){ List result = new LinkedList(); result.addAll(submissionTable); return Collections.unmodifiableList(result); } public void insert(Submission submission){ submission.setId(id); id ++; submissionTable.add(submission); } public void update(Submission submission){ int index = find(submission); if (index > -1){ submissionTable.set(index, submission); } } /** * * @param submission * @return -1 not found */ public int find(Submission submission){ int index = -1; for (int i = 0 ; i < submissionTable.size() ; i++){ if (submission.getId() == submissionTable.get(i).getId()){ index = i; break; } } return index; } }
package demo.workflow; import java.io.*; import java.time.LocalDateTime; import java.util.*; /** * represent a form submission */ public class Submission { public enum State{ WAITING, APPROVED, REJECTED } private int id; private State state = State.WAITING; private Date lastUpdate = Calendar.getInstance().getTime(); private ByteArrayOutputStream form; private String formName; private String owner; public int getId() { return id; } public void setId(int id) { this.id = id; } public State getState() { return state; } public void setState(State state) { this.state = state; } public Date getLastUpdate() { return lastUpdate; } public void setLastUpdate(Date lastUpdate) { this.lastUpdate = lastUpdate; } public ByteArrayOutputStream getForm() { return form; } public void setForm(ByteArrayOutputStream form) { this.form = form; } public String getFormName() { return formName; } public void setFormName(String formName) { this.formName = formName; } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } }