package net.rrm.ehour.ui.timesheet.panel; import net.rrm.ehour.config.EhourConfig; import net.rrm.ehour.domain.Customer; import net.rrm.ehour.domain.User; import net.rrm.ehour.project.status.ProjectAssignmentStatus; import net.rrm.ehour.timesheet.service.TimesheetLockService; import net.rrm.ehour.ui.common.border.CustomTitledGreyRoundedBorder; import net.rrm.ehour.ui.common.border.GreyBlueRoundedBorder; import net.rrm.ehour.ui.common.component.JavaScriptConfirmation; import net.rrm.ehour.ui.common.decorator.LoadingSpinnerDecorator; import net.rrm.ehour.ui.common.event.AjaxEvent; import net.rrm.ehour.ui.common.event.EventPublisher; import net.rrm.ehour.ui.common.form.FormHighlighter; import net.rrm.ehour.ui.common.formguard.GuardedAjaxLink; import net.rrm.ehour.ui.common.model.DateModel; import net.rrm.ehour.ui.common.model.MessageResourceModel; import net.rrm.ehour.ui.common.panel.AbstractBasePanel; import net.rrm.ehour.ui.common.session.EhourWebSession; import net.rrm.ehour.ui.timesheet.common.TimesheetAjaxEventType; import net.rrm.ehour.ui.timesheet.dto.GrandTotal; import net.rrm.ehour.ui.timesheet.dto.Timesheet; import net.rrm.ehour.ui.timesheet.dto.TimesheetRow; import net.rrm.ehour.ui.timesheet.model.PersistableTimesheetModel; import net.rrm.ehour.ui.timesheet.model.TimesheetContainer; import net.rrm.ehour.ui.timesheet.model.TimesheetModel; import net.rrm.ehour.ui.timesheet.panel.renderer.SectionRenderFactory; import net.rrm.ehour.ui.timesheet.panel.renderer.SectionRenderFactoryCollection; import net.rrm.ehour.util.DateUtil; import org.apache.log4j.Logger; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.attributes.IAjaxCallListener; import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.event.IEvent; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.WebComponent; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.ResourceModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.resource.CssResourceReference; import org.apache.wicket.request.resource.JavaScriptResourceReference; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; public class TimesheetPanel extends AbstractBasePanel<TimesheetContainer> { private static final long serialVersionUID = 7704288648724599187L; private static final JavaScriptResourceReference GUARDFORM_JS = new JavaScriptResourceReference(TimesheetPanel.class, "guardform.js"); private static final JavaScriptResourceReference TIMESHEET_JS = new JavaScriptResourceReference(TimesheetPanel.class, "timesheet.js"); private static final CssResourceReference TIMESHEET_CSS = new CssResourceReference(TimesheetPanel.class, "css/timesheetForm.css"); public static final String SERVER_MESSAGE_ID = "serverMessage"; private static final Logger LOGGER = Logger.getLogger(TimesheetPanel.class); private EhourConfig config; private Component serverMsgLabel; private Form<TimesheetContainer> timesheetForm; @SpringBean private PersistableTimesheetModel<TimesheetContainer> model; @SpringBean private SectionRenderFactoryCollection sectionFactory; @SpringBean private TimesheetLockService timesheetLockService; public TimesheetPanel(String id, User user, Calendar forWeek) { super(id); config = EhourWebSession.getEhourConfig(); this.setOutputMarkupId(true); // set the model model.init(user, forWeek); setDefaultModel(model); Timesheet timesheet = model.getObject().getTimesheet(); // grey & blue frame border WebMarkupContainer weekNavigation = getWeekNavigation(timesheet.getWeekStart(), timesheet.getWeekEnd()); CustomTitledGreyRoundedBorder greyBorder = new CustomTitledGreyRoundedBorder("timesheetFrame", weekNavigation); add(greyBorder); this.timesheetForm = buildForm(model); greyBorder.add(timesheetForm); } @Override public void renderHead(IHeaderResponse response) { response.render(JavaScriptHeaderItem.forReference(GUARDFORM_JS)); response.render(CssHeaderItem.forReference(TIMESHEET_CSS)); String msg = new ResourceModel("timesheet.dirtyForm").getObject(); String escapedMsg = msg.replace("'", "\\\'"); response.render(JavaScriptHeaderItem.forScript(String.format("var WARNING_MSG = '%s';", escapedMsg), "msg")); response.render(JavaScriptHeaderItem.forReference(TIMESHEET_JS)); response.render(OnDomReadyHeaderItem.forScript("window.timesheet = new Timesheet();window.timesheet.init();")); } private RepeatingView renderSections() { RepeatingView options = new RepeatingView("sections"); for (SectionRenderFactory renderFactory : sectionFactory.getRenderFactories()) { options.add(renderFactory.renderForId(options.newChildId(), getPanelModel())); } return options; } /** * Add week navigation to title */ @SuppressWarnings("serial") private WebMarkupContainer getWeekNavigation(final Date weekStart, final Date weekEnd) { Fragment titleFragment = new Fragment("title", "title", TimesheetPanel.this); SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM yyyy", config.getFormattingLocale()); int weekOfYear = DateUtil.getWeekNumberForDate(weekStart, config.getFirstDayOfWeek()); IModel<String> weekLabelModel = new MessageResourceModel("timesheet.weekTitle", this, weekOfYear, dateFormatter.format(weekStart), dateFormatter.format(weekEnd)); titleFragment.add(new Label("titleLabel", weekLabelModel)); GuardedAjaxLink<Void> previousWeekLink = new GuardedWeekLink("previousWeek", weekStart, -1); titleFragment.add(previousWeekLink); GuardedAjaxLink<Void> nextWeekLink = new GuardedWeekLink("nextWeek", weekStart, 1); titleFragment.add(nextWeekLink); return titleFragment; } private void addGrandTotals(WebMarkupContainer parent, GrandTotal grandTotals, Date weekStart) { Calendar dateIterator = new GregorianCalendar(); dateIterator.setTime(weekStart); for (int i = 1; i <= 7; i++, dateIterator.add(Calendar.DAY_OF_YEAR, 1)) { final int index = dateIterator.get(Calendar.DAY_OF_WEEK) - 1; Label total = new Label("day" + i + "Total", new PropertyModel<Float>(grandTotals, "getValues[" + index + "]")) { @Override public void onEvent(IEvent<?> event) { if (event.getPayload() instanceof TimesheetInputModifiedEvent) { TimesheetInputModifiedEvent payload = (TimesheetInputModifiedEvent) event.getPayload(); if (payload.getForDayOfWeek() == index) { payload.getTarget().add(this); } } } }; total.setOutputMarkupId(true); parent.add(total); } Label grandTotal = new Label("grandTotal", new PropertyModel<Float>(grandTotals, "grandTotal")) { @Override public void onEvent(IEvent<?> event) { if (event.getPayload() instanceof TimesheetInputModifiedEvent) { TimesheetInputModifiedEvent payload = (TimesheetInputModifiedEvent) event.getPayload(); payload.getTarget().add(this); } } }; grandTotal.setOutputMarkupId(true); parent.add(grandTotal); } private void setSubmitActions(Form<?> form, MarkupContainer parent, final Timesheet timesheet) { // default submit SubmitButton submitButton = new SubmitButton("submitButton", form, timesheet); submitButton.setOutputMarkupId(true); submitButton.setMarkupId("submit"); parent.add(submitButton); // reset, should fetch the original contents AjaxButton resetButton = new AjaxButton("resetButton", form) { private static final long serialVersionUID = 1L; @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { // basically fake a week click EventPublisher.publishAjaxEvent(this, new AjaxEvent(TimesheetAjaxEventType.WEEK_NAV)); } @Override protected void onError(AjaxRequestTarget target, Form<?> form) { // reset doesn't error } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); List<IAjaxCallListener> callListeners = attributes.getAjaxCallListeners(); callListeners.add(new JavaScriptConfirmation(new ResourceModel("timesheet.confirmReset"))); callListeners.add(new LoadingSpinnerDecorator()); } @Override public boolean isVisible() { return !timesheet.isAllLocked(); } }; resetButton.setDefaultFormProcessing(false); parent.add(resetButton); } private void addFailedProjectMessages(List<ProjectAssignmentStatus> failedProjects, final AjaxRequestTarget target) { getPanelModelObject().getTimesheet().updateFailedProjects(failedProjects); timesheetForm.visitChildren(Label.class, new IVisitor<Label, Void>() { @Override public void component(Label label, IVisit visit) { if ("status".equals(label.getId())) { label.setVisible(true); target.add(label); } } }); } /** * Set message that the hours are saved */ private Label updatePostPersistMessage() { // server message IModel<String> serverMsg = new StringResourceModel("timesheet.weekSaved", TimesheetPanel.this, null, new PropertyModel<Date>(getDefaultModel(), "timesheet.totalBookedHours"), new DateModel(new PropertyModel<Date>(getDefaultModel(), "timesheet.weekStart"), config, DateModel.DATESTYLE_FULL_SHORT), new DateModel(new PropertyModel<Date>(getDefaultModel(), "timesheet.weekEnd"), config, DateModel.DATESTYLE_FULL_SHORT)); Label label = new Label(SERVER_MESSAGE_ID, serverMsg); label.add(AttributeModifier.replace("style", "timesheetPersisted")); label.setOutputMarkupId(true); serverMsgLabel.replaceWith(label); serverMsgLabel = label; return label; } private WebMarkupContainer updateErrorMessage(String msgModel) { IModel<String> model = new MessageResourceModel(msgModel, TimesheetPanel.this); Fragment fragment = new Fragment(SERVER_MESSAGE_ID, "persistenceError", this); fragment.add(new Label("msg", model)); fragment.add(AttributeModifier.replace("style", "padding: 5px 10px")); fragment.add(AttributeModifier.replace("class", "warningBanner")); fragment.setOutputMarkupId(true); serverMsgLabel.replaceWith(fragment); serverMsgLabel = fragment; return fragment; } /** * Add date labels (sun/mon etc) */ private void addDateLabels(WebMarkupContainer parent) { Timesheet timesheet = getPanelModelObject().getTimesheet(); for (int i = 1, j = 0; i <= 7; i++, j++) { String id = "day" + i + "Label"; Fragment headerFragment = new Fragment(id, "dayHeader", TimesheetPanel.this); PropertyModel<Date> model = new PropertyModel<>(getDefaultModelObject(), "timesheet.dateSequence[" + j + "]"); headerFragment.add(new Label("weekDay", new DateModel(model, config, DateModel.DATESTYLE_TIMESHEET_DAYONLY))); headerFragment.add(new Label("day", new DateModel(model, config, DateModel.DATESTYLE_DAYONLY))); Fragment lockFragment; if (timesheet.isLocked(j)) { lockFragment = new Fragment("lock", "lockedDay", this); WebMarkupContainer container = new WebMarkupContainer("lockedContainer"); container.add(AttributeModifier.replace("title", new MessageResourceModel("timesheet.daylocked", TimesheetPanel.this))); lockFragment.add(container); } else { lockFragment = new Fragment("lock", "unlockedDay", this); } headerFragment.add(lockFragment); parent.add(headerFragment); } } /** * Move to next week after succesfull form submit or week navigation */ private void moveWeek(Date onScreenDate, int weekDiff) { EhourWebSession session = EhourWebSession.getSession(); Calendar cal = DateUtil.getCalendar(config); cal.setTime(onScreenDate); cal.add(Calendar.WEEK_OF_YEAR, weekDiff); // should update calendar as well session.setNavCalendar(cal); EventPublisher.publishAjaxEvent(this, new AjaxEvent(TimesheetAjaxEventType.WEEK_NAV)); } @SuppressWarnings("unchecked") private List<ProjectAssignmentStatus> persistTimesheetEntries() throws TimesheetModel.UnknownPersistenceException { return ((PersistableTimesheetModel) getPanelModel()).persist(); } private Form<TimesheetContainer> buildForm(IModel<TimesheetContainer> timesheetModel) { // add form Form<TimesheetContainer> timesheetForm = new Form<>("timesheetForm"); timesheetForm.setMarkupId("timesheetForm"); timesheetForm.setOutputMarkupId(true); timesheetForm.setModel(timesheetModel); GreyBlueRoundedBorder blueBorder = new GreyBlueRoundedBorder("blueFrame"); timesheetForm.add(blueBorder); // setup form GrandTotal grandTotals = buildForm(timesheetForm, blueBorder); // add last row with grand totals Timesheet timesheet = timesheetModel.getObject().getTimesheet(); addGrandTotals(blueBorder, grandTotals, timesheet.getWeekStart()); // add label dates addDateLabels(blueBorder); // attach onsubmit ajax events setSubmitActions(timesheetForm, timesheetForm, timesheet); SubmitButton submitButtonTop = new SubmitButton("submitButtonTop", timesheetForm, timesheet); submitButtonTop.setOutputMarkupId(true); submitButtonTop.setMarkupId("submitButtonTop"); blueBorder.add(submitButtonTop); // server message serverMsgLabel = new WebComponent(SERVER_MESSAGE_ID); serverMsgLabel.setOutputMarkupId(true); timesheetForm.add(serverMsgLabel); timesheetForm.add(renderSections()); return timesheetForm; } private GrandTotal buildForm(final Form<?> form, WebMarkupContainer parent) { final GrandTotal grandTotals = new GrandTotal(); ListView<Customer> customers = new ListView<Customer>("customers", new PropertyModel<List<Customer>>(getDefaultModelObject(), "timesheet.customerList")) { private static final long serialVersionUID = 1L; @Override protected void populateItem(ListItem<Customer> item) { final Customer customer = item.getModelObject(); Timesheet timesheet = TimesheetPanel.this.getPanelModelObject().getTimesheet(); item.add(new Label("customer", customer.getName())); item.add(createTimesheetRows("rows", grandTotals, form, timesheet.getTimesheetRows(customer))); } }; customers.setReuseItems(true); parent.add(customers); return grandTotals; } protected TimesheetRowList createTimesheetRows(String id, GrandTotal grandTotals, Form<?> form, List<TimesheetRow> rows) { return new TimesheetRowList(id, rows, grandTotals, getPanelModel(), form, TimesheetPanel.this); } private class SubmitButton extends AjaxButton { private static final long serialVersionUID = 1L; private final Timesheet timesheet; public SubmitButton(String id, Form<?> form, Timesheet timesheet) { super(id, form); this.timesheet = timesheet; } @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { try { List<ProjectAssignmentStatus> failedProjects = persistTimesheetEntries(); if (failedProjects.isEmpty()) { target.add(updatePostPersistMessage()); } else { target.add(updateErrorMessage("timesheet.errorPersist")); } addFailedProjectMessages(failedProjects, target); EventPublisher.publishAjaxEvent(this, new AjaxEvent(TimesheetAjaxEventType.TIMESHEET_SUBMIT)); } catch (TimesheetModel.UnknownPersistenceException e) { LOGGER.error("Failed to persist", e); target.add(updateErrorMessage("timesheet.generalPersistenceError")); } } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.getAjaxCallListeners().add(new LoadingSpinnerDecorator()); } @Override protected void onError(final AjaxRequestTarget target, Form<?> form) { form.visitFormComponents(new FormHighlighter(target)); } @Override public boolean isVisible() { return !timesheet.isAllLocked(); } } private class GuardedWeekLink extends GuardedAjaxLink<Void> { private int delta; private Date weekStart; private GuardedWeekLink(String id, Date weekStart, int delta) { super(id); this.delta = delta; this.weekStart = weekStart; } @Override public void onClick(AjaxRequestTarget target) { moveWeek(weekStart, delta); } } }