/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.web.timesheet;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.validation.IFormValidator;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.IValidator;
import org.hibernate.Hibernate;
import org.projectforge.common.DateHelper;
import org.projectforge.common.DateHolder;
import org.projectforge.common.DatePrecision;
import org.projectforge.core.SystemInfoCache;
import org.projectforge.fibu.KostFormatter;
import org.projectforge.fibu.kost.Kost2DO;
import org.projectforge.task.TaskDO;
import org.projectforge.task.TaskNode;
import org.projectforge.task.TaskTree;
import org.projectforge.timesheet.TimesheetDO;
import org.projectforge.timesheet.TimesheetDao;
import org.projectforge.user.PFUserContext;
import org.projectforge.user.PFUserDO;
import org.projectforge.user.UserGroupCache;
import org.projectforge.user.UserPrefArea;
import org.projectforge.user.UserPrefDO;
import org.projectforge.user.UserPrefDao;
import org.projectforge.web.dialog.ModalDialog;
import org.projectforge.web.task.TaskListPage;
import org.projectforge.web.task.TaskSelectPanel;
import org.projectforge.web.user.UserFormatter;
import org.projectforge.web.user.UserSelectPanel;
import org.projectforge.web.wicket.AbstractEditForm;
import org.projectforge.web.wicket.WicketUtils;
import org.projectforge.web.wicket.autocompletion.AutoCompleteIgnoreForm;
import org.projectforge.web.wicket.autocompletion.PFAutoCompleteMaxLengthTextField;
import org.projectforge.web.wicket.autocompletion.PFAutoCompleteTextField;
import org.projectforge.web.wicket.components.ConsumptionBarPanel;
import org.projectforge.web.wicket.components.DateTimePanel;
import org.projectforge.web.wicket.components.DateTimePanelSettings;
import org.projectforge.web.wicket.components.LabelValueChoiceRenderer;
import org.projectforge.web.wicket.components.MaxLengthTextArea;
import org.projectforge.web.wicket.flowlayout.AbstractFieldsetPanel;
import org.projectforge.web.wicket.flowlayout.DivTextPanel;
import org.projectforge.web.wicket.flowlayout.DropDownChoicePanel;
import org.projectforge.web.wicket.flowlayout.FieldsetPanel;
import org.projectforge.web.wicket.flowlayout.IconLinkPanel;
import org.projectforge.web.wicket.flowlayout.IconType;
import org.projectforge.web.wicket.flowlayout.TextAreaPanel;
public class TimesheetEditForm extends AbstractEditForm<TimesheetDO, TimesheetEditPage> implements AutoCompleteIgnoreForm
{
private static final long serialVersionUID = 3150725003240437752L;
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(TimesheetEditForm.class);
private static final String USERPREF_KEY = "TimesheetEditForm.userPrefs";
ModalDialog recentSheetsModalDialog;
@SpringBean(name = "taskTree")
private TaskTree taskTree;
@SpringBean(name = "timesheetDao")
private TimesheetDao timesheetDao;
@SpringBean(name = "userFormatter")
private UserFormatter userFormatter;
@SpringBean(name = "userGroupCache")
private UserGroupCache userGroupCache;
@SpringBean(name = "userPrefDao")
private UserPrefDao userPrefDao;
private UserPrefDO recentUserPref;
UserSelectPanel userSelectPanel;
PFAutoCompleteMaxLengthTextField locationTextField;
TextArea<String> descriptionArea;
DropDownChoicePanel<Integer> cost2ChoicePanel;
private DropDownChoice<Integer> cost2Choice;
private FieldsetPanel cost2ChoiceFieldset;
private ConsumptionBarPanel consumptionBarPanel;
private List<Kost2DO> cost2List;
protected Boolean saveAsTemplate;
@SuppressWarnings("unused")
private Integer stopHourOfDay, stopMinute;
@SuppressWarnings("unused")
private String templateName, consumptionBarId;
// Components for form validation.
private final FormComponent< ? >[] dependentFormComponentsWithCost2 = new FormComponent[4];
private final FormComponent< ? >[] dependentFormComponentsWithoutCost2 = new FormComponent[3];
private final boolean cost2Exists;
protected TimesheetPageSupport timesheetPageSupport;
private final TimesheetEditFilter filter;
public TimesheetEditForm(final TimesheetEditPage parentPage, final TimesheetDO data)
{
super(parentPage, data);
cost2Exists = SystemInfoCache.instance().isCost2EntriesExists();
filter = initTimesheetFilter();
}
@SuppressWarnings("serial")
@Override
protected void init()
{
super.init();
timesheetPageSupport = new TimesheetPageSupport(parentPage, gridBuilder, timesheetDao, data);
add(new IFormValidator() {
@Override
public FormComponent< ? >[] getDependentFormComponents()
{
if (cost2ChoiceFieldset != null && cost2ChoiceFieldset.isVisible() == true) {
return dependentFormComponentsWithCost2;
} else {
return dependentFormComponentsWithoutCost2;
}
}
@SuppressWarnings("unchecked")
@Override
public void validate(final Form< ? > form)
{
final DateTimePanel startDateTimePanel = (DateTimePanel) dependentFormComponentsWithCost2[0];
final DropDownChoice<Integer> stopHourOfDayDropDownChoice = (DropDownChoice<Integer>) dependentFormComponentsWithCost2[1];
final DropDownChoice<Integer> stopMinuteDropDownChoice = (DropDownChoice<Integer>) dependentFormComponentsWithCost2[2];
final DateHolder startDate = new DateHolder(startDateTimePanel.getConvertedInput());
final DateHolder stopDate = new DateHolder(startDate.getTimestamp());
stopDate.setHourOfDay(stopHourOfDayDropDownChoice.getConvertedInput());
stopDate.setMinute(stopMinuteDropDownChoice.getConvertedInput());
if (stopDate.getTimeOfDay() < startDate.getTimeOfDay()) { // Stop time is
// before start time. Assuming next day for stop time:
stopDate.add(Calendar.DAY_OF_MONTH, 1);
}
data.setStartTime(startDate.getTimestamp());
data.setStopTime(stopDate.getTimestamp());
if (data.getDuration() < 60000) {
// Duration is less than 60 seconds.
stopMinuteDropDownChoice.error(getString("timesheet.error.zeroDuration"));
} else if (data.getDuration() > TimesheetDao.MAXIMUM_DURATION) {
stopMinuteDropDownChoice.error(getString("timesheet.error.maximumDurationExceeded"));
}
if (cost2Exists == true) {
if (cost2Choice != null && cost2Choice.getConvertedInput() == null) {
// cost2Choice is always != null (but may-be invisible) if cost2 entries does exist in the system.
// Kost2 is not available for current task.
final TaskNode taskNode = taskTree.getTaskNodeById(data.getTaskId());
if (taskNode != null) {
final List<Integer> descendents = taskNode.getDescendantIds();
for (final Integer taskId : descendents) {
if (CollectionUtils.isNotEmpty(taskTree.getKost2List(taskId)) == true) {
// But Kost2 is available for sub task, so user should book his time sheet
// on a sub task with kost2s.
if (cost2Choice.isVisible()) {
cost2Choice.error(getString("timesheet.error.kost2NeededChooseSubTask"));
} else {
error(getString("timesheet.error.kost2NeededChooseSubTask"));
}
break;
}
}
}
}
}
}
});
parentPage.preInit();
gridBuilder.newGridPanel();
if (isNew() == true) {
addTemplatesRow();
}
{
// Task
final FieldsetPanel fs = gridBuilder.newFieldset(getString("task"));
final TaskSelectPanel taskSelectPanel = new TaskSelectPanel(fs, new PropertyModel<TaskDO>(data, "task"), parentPage, "taskId") {
@Override
protected void selectTask(final TaskDO task)
{
super.selectTask(task);
refresh(); // Task was changed. Therefore update the kost2 list.
}
/**
* @see org.projectforge.web.task.TaskSelectPanel#onModelSelected(org.apache.wicket.ajax.AjaxRequestTarget,
* org.projectforge.task.TaskDO)
*/
@Override
protected void onModelSelected(final AjaxRequestTarget target, final TaskDO taskDO)
{
refresh();
super.onModelSelected(target, taskDO);
if (cost2ChoiceFieldset != null) {
target.add(cost2ChoiceFieldset.getFieldset());
}
}
};
taskSelectPanel.setAutocompleteOnlyTaskBookableForTimesheets(true);
fs.add(taskSelectPanel);
taskSelectPanel.init();
taskSelectPanel.setRequired(true);
}
if (cost2Exists == true) {
// Cost 2 entries does exist in the data-base.
cost2ChoiceFieldset = gridBuilder.newFieldset(getString("fibu.kost2"));
cost2ChoiceFieldset.getFieldset().setOutputMarkupId(true);
cost2ChoiceFieldset.getFieldset().setOutputMarkupPlaceholderTag(true);
cost2List = taskTree.getKost2List(data.getTaskId());
final LabelValueChoiceRenderer<Integer> cost2ChoiceRenderer = getCost2LabelValueChoiceRenderer(parentPage.getBaseDao(), cost2List,
data, null);
cost2Choice = createCost2ChoiceRenderer(cost2ChoiceFieldset.getDropDownChoiceId(), parentPage.getBaseDao(), taskTree,
cost2ChoiceRenderer, data, cost2List);
cost2ChoicePanel = cost2ChoiceFieldset.add(cost2Choice);
dependentFormComponentsWithCost2[3] = cost2Choice;
updateCost2ChoiceValidation();
}
{
// User
final FieldsetPanel fs = gridBuilder.newFieldset(getString("user"));
PFUserDO user = data.getUser();
if (Hibernate.isInitialized(user) == false) {
user = userGroupCache.getUser(user.getId());
data.setUser(user);
}
userSelectPanel = new UserSelectPanel(fs.newChildId(), new PropertyModel<PFUserDO>(data, "user"), parentPage, "userId");
userSelectPanel.setRequired(true);
fs.add(userSelectPanel);
userSelectPanel.init();
}
{
// Time period
final FieldsetPanel fs = gridBuilder.newFieldset(getString("timePeriod"));
final DateTimePanel startDateTimePanel = new DateTimePanel(fs.newChildId(), new PropertyModel<Date>(data, "startTime"),
(DateTimePanelSettings) DateTimePanelSettings.get().withSelectStartStopTime(true).withTargetType(java.sql.Timestamp.class)
.withRequired(true), DatePrecision.MINUTE_15);
dependentFormComponentsWithCost2[0] = dependentFormComponentsWithoutCost2[0] = startDateTimePanel;
fs.add(startDateTimePanel);
WicketUtils.addTooltip(startDateTimePanel.getDateField(), new Model<String>() {
@Override
public String getObject()
{
final StringBuffer buf = new StringBuffer();
if (data.getStartTime() != null) {
buf.append(DateHelper.TECHNICAL_ISO_UTC.get().format(data.getStartTime()));
if (data.getStopTime() != null) {
buf.append(" - ");
}
}
if (data.getStopTime() != null) {
buf.append(DateHelper.TECHNICAL_ISO_UTC.get().format(data.getStopTime()));
}
return buf.toString();
}
});
fs.add(new DivTextPanel(fs.newChildId(), getString("until")));
// Stop time
final DropDownChoice<Integer> stopHourOfDayDropDownChoice = new DropDownChoice<Integer>(fs.getDropDownChoiceId(),
new PropertyModel<Integer>(this, "stopHourOfDay"), DateTimePanel.getHourOfDayRenderer().getValues(),
DateTimePanel.getHourOfDayRenderer());
stopHourOfDayDropDownChoice.setNullValid(false);
stopHourOfDayDropDownChoice.setRequired(true);
fs.add(stopHourOfDayDropDownChoice);
dependentFormComponentsWithCost2[1] = dependentFormComponentsWithoutCost2[1] = stopHourOfDayDropDownChoice;
final DropDownChoice<Integer> stopMinuteDropDownChoice = new DropDownChoice<Integer>(fs.getDropDownChoiceId(),
new PropertyModel<Integer>(this, "stopMinute"), DateTimePanel.getMinutesRenderer(DatePrecision.MINUTE_15).getValues(),
DateTimePanel.getMinutesRenderer(DatePrecision.MINUTE_15));
stopMinuteDropDownChoice.setNullValid(false);
stopMinuteDropDownChoice.setRequired(true);
fs.add(stopMinuteDropDownChoice);
dependentFormComponentsWithCost2[2] = dependentFormComponentsWithoutCost2[2] = stopMinuteDropDownChoice;
}
{
final FieldsetPanel fs = gridBuilder.newFieldset(getString("task.consumption")).suppressLabelForWarning();
consumptionBarId = fs.newChildId();
fs.add(getConsumptionBar());
fs.add(new DivTextPanel(fs.newChildId(), new Model<String>() {
/**
* @see org.apache.wicket.model.Model#getObject()
*/
@Override
public String getObject()
{
return consumptionBarPanel.getTooltip();
}
}));
}
{
final AbstractFieldsetPanel< ? > fs = timesheetPageSupport.addLocation(filter);
locationTextField = (PFAutoCompleteMaxLengthTextField) fs.getStoreObject();
locationTextField.withDeletableItem(true);
}
{
final FieldsetPanel fs = gridBuilder.newFieldset(getString("timesheet.description"));
final IModel<String> model = new PropertyModel<String>(data, "description");
fs.add(descriptionArea = new MaxLengthTextArea(TextAreaPanel.WICKET_ID, model)).setAutogrow();
fs.addJIRAField(model);
}
{
// Save as template checkbox:
final FieldsetPanel fs = gridBuilder.newFieldset("").suppressLabelForWarning();
fs.addCheckBox(new PropertyModel<Boolean>(this, "saveAsTemplate"), getString("userPref.saveAsTemplate"));
}
addCloneButton();
}
private void renderHookComponents()
{
final List<TimesheetPluginComponentHook> hooks = TimesheetEditPage.getPluginHooks();
if (hooks != null && hooks.isEmpty() == false) {
for (final TimesheetPluginComponentHook hook : hooks) {
hook.renderComponentsToTimesheetEditForm(this.parentPage, getData());
}
}
}
@SuppressWarnings("serial")
private void addTemplatesRow()
{
final FieldsetPanel templatesRow = gridBuilder.newFieldset(getString("templates")).suppressLabelForWarning();
final String[] templateNames = userPrefDao.getPrefNames(UserPrefArea.TIMESHEET_TEMPLATE);
if (templateNames != null && templateNames.length > 0) {
// DropDownChoice templates
final String label = getString("userPref.template.select");
final LabelValueChoiceRenderer<String> templateNamesChoiceRenderer = new LabelValueChoiceRenderer<String>();
templateNamesChoiceRenderer.addValue("", label);
for (final String name : templateNames) {
templateNamesChoiceRenderer.addValue(name, name);
}
final DropDownChoice<String> templateNamesChoice = new DropDownChoice<String>(templatesRow.getDropDownChoiceId(),
new PropertyModel<String>(this, "templateName"), templateNamesChoiceRenderer.getValues(), templateNamesChoiceRenderer) {
@Override
protected boolean wantOnSelectionChangedNotifications()
{
return true;
}
/**
* @see org.apache.wicket.markup.html.form.AbstractSingleSelectChoice#getDefaultChoice(java.lang.String)
*/
@Override
protected CharSequence getDefaultChoice(final String selectedValue)
{
return "";
}
@Override
protected void onSelectionChanged(final String newSelection)
{
if (StringUtils.isNotEmpty(newSelection) == true) {
// Fill fields with selected template values:
final UserPrefDO userPref = userPrefDao.getUserPref(UserPrefArea.TIMESHEET_TEMPLATE, newSelection);
if (userPref != null) {
data.setKost2(null).setTask(null);
locationTextField.processInput(); // Update model.
descriptionArea.processInput(); // Update model.
if (recentUserPref != null) {
final String recentLocation = recentUserPref.getUserPrefEntryAsString("location");
if (StringUtils.equals(recentLocation, data.getLocation()) == true) {
// Previous value was filled by recent user pref so overwrite it:
data.setLocation(null);
}
final String recentDescription = recentUserPref.getUserPrefEntryAsString("description");
if (StringUtils.equals(recentDescription, data.getDescription()) == true) {
// Previous value was filled by recent user pref so overwrite it:
data.setDescription(null);
}
}
userPrefDao.fillFromUserPrefParameters(userPref, data, true);
recentUserPref = userPref;
locationTextField.modelChanged();
descriptionArea.modelChanged();
if (cost2ChoicePanel != null) {
cost2ChoicePanel.getDropDownChoice().modelChanged();
}
}
templateName = "";
refresh();
}
}
};
templateNamesChoice.setNullValid(true);
templatesRow.add(templateNamesChoice);
}
// Needed as submit link because the modal dialog reloads the page and otherwise any previous change will be lost.
final AjaxSubmitLink link = new AjaxSubmitLink(IconLinkPanel.LINK_ID) {
@Override
protected void onSubmit(final AjaxRequestTarget target, final Form< ? > form)
{
recentSheetsModalDialog.open(target);
}
@Override
protected void onError(final AjaxRequestTarget target, final Form< ? > form)
{
}
};
link.setDefaultFormProcessing(false);
templatesRow
.add(new IconLinkPanel(templatesRow.newChildId(), IconType.FOLDER_OPEN, new ResourceModel("timesheet.recent.select"), link));
recentSheetsModalDialog = new TimesheetEditSelectRecentDialogPanel(parentPage.newModalDialogId(), getString("timesheet.recent.select"),
parentPage, TimesheetEditForm.this, cost2Exists, timesheetDao, taskTree, userFormatter);
parentPage.add(recentSheetsModalDialog);
recentSheetsModalDialog.init();
}
private void updateCost2ChoiceValidation()
{
final boolean cost2Visible = CollectionUtils.isNotEmpty(cost2List);
// cost2ChoiceFieldset.setVisible(cost2Visible);
cost2Choice.setRequired(cost2Visible);
}
@SuppressWarnings("serial")
protected static DropDownChoice<Integer> createCost2ChoiceRenderer(final String id, final TimesheetDao timesheetDao,
final TaskTree taskTree, final LabelValueChoiceRenderer<Integer> kost2ChoiceRenderer, final TimesheetDO data,
final List<Kost2DO> kost2List)
{
final DropDownChoice<Integer> choice = new DropDownChoice<Integer>(id, new Model<Integer>() {
@Override
public Integer getObject()
{
return data.getKost2Id();
}
@Override
public void setObject(final Integer kost2Id)
{
if (kost2Id != null) {
timesheetDao.setKost2(data, kost2Id);
} else {
data.setKost2(null);
}
}
}, kost2ChoiceRenderer.getValues(), kost2ChoiceRenderer);
choice.setNullValid(true);
choice.add(new IValidator<Integer>() {
@Override
public void validate(final IValidatable<Integer> validatable)
{
final Integer value = validatable.getValue();
if (value != null && value >= 0) {
return;
}
if (CollectionUtils.isNotEmpty(kost2List) == true) {
// Kost2 available but not selected.
choice.error(PFUserContext.getLocalizedString("timesheet.error.kost2Required"));
}
}
});
return choice;
}
/**
* Used also by TimesheetMassUpdateForm.
* @param timesheetDao
* @param kost2List
* @param data
* @param kost2Choice
* @return
*/
protected static LabelValueChoiceRenderer<Integer> getCost2LabelValueChoiceRenderer(final TimesheetDao timesheetDao,
final List<Kost2DO> kost2List, final TimesheetDO data, final DropDownChoice<Integer> kost2Choice)
{
final LabelValueChoiceRenderer<Integer> kost2ChoiceRenderer = new LabelValueChoiceRenderer<Integer>();
if (kost2List != null && kost2List.size() == 1) {
// Es ist genau ein Eintrag. Deshalb selektieren wir diesen auch:
final Integer kost2Id = kost2List.get(0).getId();
timesheetDao.setKost2(data, kost2Id);
if (kost2Choice != null) {
kost2Choice.modelChanged();
}
}
if (CollectionUtils.isEmpty(kost2List) == true) {
data.setKost2(null); // No kost2 list given, therefore set also kost2 to null.
} else {
for (final Kost2DO kost2 : kost2List) {
kost2ChoiceRenderer.addValue(kost2.getId(), KostFormatter.formatForSelection(kost2));
}
}
return kost2ChoiceRenderer;
}
@Override
public void onBeforeRender()
{
super.onBeforeRender();
final DateHolder stopDateHolder = new DateHolder(data.getStopTime(), DatePrecision.MINUTE_15);
stopHourOfDay = stopDateHolder.getHourOfDay();
stopMinute = stopDateHolder.getMinute();
}
/**
* @see org.apache.wicket.Component#onInitialize()
*/
@Override
protected void onInitialize()
{
super.onInitialize();
renderHookComponents();
}
protected void refresh()
{
if (cost2ChoicePanel != null) {
cost2List = taskTree.getKost2List(data.getTaskId());
final LabelValueChoiceRenderer<Integer> cost2ChoiceRenderer = getCost2LabelValueChoiceRenderer(parentPage.getBaseDao(), cost2List,
data, null);
cost2ChoicePanel.getDropDownChoice().setChoiceRenderer(cost2ChoiceRenderer);
cost2ChoicePanel.getDropDownChoice().setChoices(cost2ChoiceRenderer.getValues());
updateCost2ChoiceValidation();
}
consumptionBarPanel.replaceWith(getConsumptionBar());
}
protected ConsumptionBarPanel getConsumptionBar()
{
final Integer taskId = data.getTaskId();
TaskNode node = taskId != null ? taskTree.getTaskNodeById(taskId) : null;
if (node != null) {
final TaskNode personDaysNode = taskTree.getPersonDaysNode(node);
if (personDaysNode != null) {
node = personDaysNode;
}
}
consumptionBarPanel = TaskListPage.getConsumptionBarPanel(this.parentPage, consumptionBarId, taskTree, false, node);
consumptionBarPanel.setRenderBodyOnly(true);
return consumptionBarPanel;
}
@Override
protected Logger getLogger()
{
return log;
}
/**
*
* @return
*/
protected TimesheetEditFilter initTimesheetFilter()
{
TimesheetEditFilter filter = (TimesheetEditFilter) getUserPrefEntry(USERPREF_KEY);
if (filter == null) {
filter = new TimesheetEditFilter();
putUserPrefEntry(USERPREF_KEY, filter, true);
}
return filter;
}
/**
* @see org.projectforge.web.wicket.autocompletion.AutoCompleteIgnoreForm#ignore(org.projectforge.web.wicket.autocompletion.PFAutoCompleteTextField,
* java.lang.String)
*/
@Override
public void ignore(final PFAutoCompleteTextField< ? > autoCompleteField, final String ignoreText)
{
if (locationTextField != null && locationTextField.equals(autoCompleteField) == true) {
filter.addIgnoredLocation(ignoreText);
}
}
/**
* @return the filter
*/
public TimesheetEditFilter getFilter()
{
return filter;
}
}