diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 0e67461e..bb846885 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -234,7 +234,8 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer *******************************************************************************/ public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException { - String displayText = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel); + String plural = StringUtils.plural(noOfRecords, singularLabel, pluralLabel); + String displayText = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + (StringUtils.hasContent(plural) ? (" " + plural) : ""); String tablePath = QContext.getQInstance().getTablePath(tableName); if(tablePath == null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java index c6b45597..a51b23f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java @@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWi import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetValueSource; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -58,22 +59,30 @@ public class NoCodeWidgetRenderer extends AbstractWidgetRenderer // build context by evaluating all values // //////////////////////////////////////////// Map context = new HashMap<>(); + context.put("utils", new NoCodeWidgetVelocityUtils(context, input)); + context.put("input", input); + + for(Map.Entry entry : input.getQueryParams().entrySet()) + { + context.put(entry.getKey(), entry.getValue()); + } for(AbstractWidgetValueSource valueSource : widgetMetaData.getValues()) { - LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "..."); - Object value = valueSource.evaluate(context); - LOG.trace("Computed: " + valueSource.getName() + " = " + value); - context.put(valueSource.getName(), value); - - context.put(valueSource.getName() + ".source", valueSource); + try + { + LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "..."); + Object value = valueSource.evaluate(context, input); + LOG.trace("Computed: " + valueSource.getName() + " = " + value); + context.put(valueSource.getName(), value); + context.put(valueSource.getName() + ".source", valueSource); + } + catch(Exception e) + { + LOG.warn("Error evaluating widget value source", e, logPair("widgetName", input.getWidgetMetaData().getName()), logPair("valueSourceName", valueSource.getName())); + } } - ///////////////////////////////////////////// - // set default utils object in context too // - ///////////////////////////////////////////// - context.put("utils", new NoCodeWidgetVelocityUtils(context)); - ///////////////////////////////////////////// // build content by evaluating all outputs // ///////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java index db29e00b..2c37f31d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java @@ -22,11 +22,18 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.ZoneId; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractHTMLWidgetRenderer; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -39,11 +46,8 @@ public class NoCodeWidgetVelocityUtils { private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetVelocityUtils.class); - - /******************************************************************************* - ** - *******************************************************************************/ - private final Map context; + private Map context; + private RenderWidgetInput input; @@ -51,9 +55,10 @@ public class NoCodeWidgetVelocityUtils ** Constructor ** *******************************************************************************/ - public NoCodeWidgetVelocityUtils(Map context) + public NoCodeWidgetVelocityUtils(Map context, RenderWidgetInput input) { this.context = context; + this.input = input; } @@ -61,7 +66,7 @@ public class NoCodeWidgetVelocityUtils /******************************************************************************* ** *******************************************************************************/ - public final String errorIcon() + public String errorIcon() { return (""" @@ -73,7 +78,7 @@ public class NoCodeWidgetVelocityUtils /******************************************************************************* ** *******************************************************************************/ - public final String checkIcon() + public String checkIcon() { return (""" @@ -82,6 +87,42 @@ public class NoCodeWidgetVelocityUtils + /******************************************************************************* + ** + *******************************************************************************/ + public String spanColorGreen() + { + return (""" + + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String spanColorOrange() + { + return (""" + + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String spanColorRed() + { + return (""" + + """); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -92,6 +133,110 @@ public class NoCodeWidgetVelocityUtils + /******************************************************************************* + ** + *******************************************************************************/ + public String formatDateTime(Instant i) + { + return QValueFormatter.formatDateTimeWithZone(i.atZone(ZoneId.of(QContext.getQInstance().getDefaultTimeZoneId()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatSecondsAsDuration(Integer seconds) + { + StringBuilder rs = new StringBuilder(); + + if(seconds == null) + { + return (""); + } + + int secondsPerDay = 24 * 60 * 60; + if(seconds >= secondsPerDay) + { + int days = seconds / (secondsPerDay); + seconds = seconds % secondsPerDay; + rs.append(days).append(StringUtils.plural(days, " day", " days")).append(" "); + } + + int secondsPerHour = 60 * 60; + if(seconds >= secondsPerHour) + { + int hours = seconds / (secondsPerHour); + seconds = seconds % secondsPerHour; + rs.append(hours).append(StringUtils.plural(hours, " hour", " hours")).append(" "); + } + + int secondsPerMinute = 60; + if(seconds >= secondsPerMinute) + { + int minutes = seconds / (secondsPerMinute); + seconds = seconds % secondsPerMinute; + rs.append(minutes).append(StringUtils.plural(minutes, " minute", " minutes")).append(" "); + } + + if(seconds > 0 || rs.length() == 0) + { + rs.append(seconds).append(StringUtils.plural(seconds, " second", " seconds")).append(" "); + } + + if(rs.length() > 0) + { + rs.deleteCharAt(rs.length() - 1); + } + + return (rs.toString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatSecondsAsRoundedDuration(Integer seconds) + { + StringBuilder rs = new StringBuilder(); + + if(seconds == null) + { + return (""); + } + + int secondsPerDay = 24 * 60 * 60; + if(seconds >= secondsPerDay) + { + int days = seconds / (secondsPerDay); + return (days + StringUtils.plural(days, " day", " days")); + } + + int secondsPerHour = 60 * 60; + if(seconds >= secondsPerHour) + { + int hours = seconds / (secondsPerHour); + return (hours + StringUtils.plural(hours, " hour", " hours")); + } + + int secondsPerMinute = 60; + if(seconds >= secondsPerMinute) + { + int minutes = seconds / (secondsPerMinute); + return (minutes + StringUtils.plural(minutes, " minute", " minutes")); + } + + if(seconds > 0 || rs.length() == 0) + { + return (seconds + StringUtils.plural(seconds, " second", " seconds")); + } + + return (""); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -101,7 +246,7 @@ public class NoCodeWidgetVelocityUtils { WidgetCount widgetCount = (WidgetCount) context.get(countVariableName + ".source"); Integer count = ValueUtils.getValueAsInteger(context.get(countVariableName)); - QQueryFilter filter = widgetCount.getFilter(); + QQueryFilter filter = widgetCount.getEffectiveFilter(input); return (AbstractHTMLWidgetRenderer.aHrefTableFilterNoOfRecords(null, widgetCount.getTableName(), filter, count, singular, plural)); } catch(Exception e) @@ -110,4 +255,14 @@ public class NoCodeWidgetVelocityUtils return (""); } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String round(BigDecimal input, int digits) + { + return String.valueOf(input.setScale(digits, RoundingMode.HALF_UP)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index e3073b03..6304f73f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -47,8 +47,8 @@ public class QValueFormatter { private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class); - private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a"); - private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a z"); + private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a"); + private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a z"); private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm a"); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractConditionalFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractConditionalFilter.java new file mode 100644 index 00000000..102e3846 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractConditionalFilter.java @@ -0,0 +1,46 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; + + +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class AbstractConditionalFilter +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public abstract boolean testCondition(RenderWidgetInput renderWidgetInput); + + + /******************************************************************************* + ** + *******************************************************************************/ + public abstract QQueryFilter getFilter(RenderWidgetInput renderWidgetInput); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java index 570b3890..f3ab6ab2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; /******************************************************************************* @@ -39,7 +40,7 @@ public abstract class AbstractWidgetValueSource /******************************************************************************* ** *******************************************************************************/ - public abstract Object evaluate(Map context) throws QException; + public abstract Object evaluate(Map context, RenderWidgetInput input) throws QException; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSourceWithFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSourceWithFilter.java new file mode 100644 index 00000000..b53fb89f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSourceWithFilter.java @@ -0,0 +1,167 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class AbstractWidgetValueSourceWithFilter extends AbstractWidgetValueSource +{ + protected String tableName; + protected QQueryFilter filter; + + protected List conditionalFilterList; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QQueryFilter getEffectiveFilter(RenderWidgetInput input) + { + QQueryFilter effectiveFilter; + if(filter == null) + { + effectiveFilter = new QQueryFilter(); + } + else + { + effectiveFilter = filter.clone(); + } + + for(AbstractConditionalFilter conditionalFilter : CollectionUtils.nonNullList(conditionalFilterList)) + { + if(conditionalFilter.testCondition(input)) + { + QQueryFilter additionalFilter = conditionalFilter.getFilter(input); + for(QFilterCriteria criterion : additionalFilter.getCriteria()) + { + effectiveFilter.addCriteria(criterion); + } + } + } + + return (effectiveFilter); + } + + + + /******************************************************************************* + ** Getter for tableName + *******************************************************************************/ + public String getTableName() + { + return (this.tableName); + } + + + + /******************************************************************************* + ** Setter for tableName + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + *******************************************************************************/ + public AbstractWidgetValueSourceWithFilter withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for filter + *******************************************************************************/ + public QQueryFilter getFilter() + { + return (this.filter); + } + + + + /******************************************************************************* + ** Setter for filter + *******************************************************************************/ + public void setFilter(QQueryFilter filter) + { + this.filter = filter; + } + + + + /******************************************************************************* + ** Fluent setter for filter + *******************************************************************************/ + public AbstractWidgetValueSourceWithFilter withFilter(QQueryFilter filter) + { + this.filter = filter; + return (this); + } + + + + /******************************************************************************* + ** Getter for conditionalFilterList + *******************************************************************************/ + public List getConditionalFilterList() + { + return (this.conditionalFilterList); + } + + + + /******************************************************************************* + ** Setter for conditionalFilterList + *******************************************************************************/ + public void setConditionalFilterList(List conditionalFilterList) + { + this.conditionalFilterList = conditionalFilterList; + } + + + + /******************************************************************************* + ** Fluent setter for conditionalFilterList + *******************************************************************************/ + public AbstractWidgetValueSourceWithFilter withConditionalFilterList(List conditionalFilterList) + { + this.conditionalFilterList = conditionalFilterList; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java index d1eaa507..aabb008f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java @@ -33,9 +33,13 @@ public class HtmlWrapper private String prefix; private String suffix; - public static final HtmlWrapper SUBHEADER = new HtmlWrapper("

", "

"); - public static final HtmlWrapper INDENT_1 = new HtmlWrapper("
", "
"); - public static final HtmlWrapper INDENT_2 = new HtmlWrapper("
", "
"); + public static final HtmlWrapper SUBHEADER = new HtmlWrapper("

", "

"); + public static final HtmlWrapper BIG_CENTERED = new HtmlWrapper("
", "
"); + public static final HtmlWrapper INDENT_1 = new HtmlWrapper("
", "
"); + public static final HtmlWrapper INDENT_2 = new HtmlWrapper("
", "
"); + public static final HtmlWrapper RULE_ABOVE = new HtmlWrapper(""" +
+ """, ""); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/IfInputVariableExistsFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/IfInputVariableExistsFilter.java new file mode 100644 index 00000000..d4d1e803 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/IfInputVariableExistsFilter.java @@ -0,0 +1,99 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; + + +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MutableList; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class IfInputVariableExistsFilter extends AbstractConditionalFilter +{ + private String inputVariableName; + private QQueryFilter filter; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public IfInputVariableExistsFilter() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public IfInputVariableExistsFilter(String inputVariableName, QQueryFilter filter) + { + this.inputVariableName = inputVariableName; + this.filter = filter; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean testCondition(RenderWidgetInput renderWidgetInput) + { + return (renderWidgetInput.getQueryParams().get(inputVariableName) != null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QQueryFilter getFilter(RenderWidgetInput renderWidgetInput) + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) + { + if(criterion.getValues() != null) + { + criterion.setValues(new MutableList<>(criterion.getValues())); + for(int i = 0; i < criterion.getValues().size(); i++) + { + Serializable value = criterion.getValues().get(i); + if(value instanceof String valueString && valueString.equals("${input." + inputVariableName + "}")) + { + criterion.getValues().set(i, renderWidgetInput.getQueryParams().get(inputVariableName)); + } + } + } + } + + return (filter); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAggregate.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAggregate.java new file mode 100644 index 00000000..aede82b3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetAggregate.java @@ -0,0 +1,175 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class WidgetAggregate extends AbstractWidgetValueSourceWithFilter +{ + private Aggregate aggregate; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetAggregate() + { + setType(getClass().getSimpleName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object evaluate(Map context, RenderWidgetInput input) throws QException + { + AggregateInput aggregateInput = new AggregateInput(); + aggregateInput.setTableName(tableName); + aggregateInput.setAggregates(List.of(aggregate)); + aggregateInput.setFilter(getEffectiveFilter(input)); + + AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput); + List results = aggregateOutput.getResults(); + if(results.isEmpty()) + { + return (null); + } + else + { + AggregateResult aggregateResult = results.get(0); + return (aggregateResult.getAggregateValue(aggregate)); + } + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public WidgetAggregate withName(String name) + { + setName(name); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tableName + *******************************************************************************/ + @Override + public WidgetAggregate withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for filter + *******************************************************************************/ + @Override + public WidgetAggregate withFilter(QQueryFilter filter) + { + this.filter = filter; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for conditionalFilterList + *******************************************************************************/ + @Override + public WidgetAggregate withConditionalFilterList(List conditionalFilterList) + { + this.conditionalFilterList = conditionalFilterList; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter to add a single conditionalFilter + *******************************************************************************/ + public WidgetAggregate withConditionalFilter(AbstractConditionalFilter conditionalFilter) + { + if(this.conditionalFilterList == null) + { + this.conditionalFilterList = new ArrayList<>(); + } + this.conditionalFilterList.add(conditionalFilter); + return (this); + } + + + + /******************************************************************************* + ** Getter for aggregate + *******************************************************************************/ + public Aggregate getAggregate() + { + return (this.aggregate); + } + + + + /******************************************************************************* + ** Setter for aggregate + *******************************************************************************/ + public void setAggregate(Aggregate aggregate) + { + this.aggregate = aggregate; + } + + + + /******************************************************************************* + ** Fluent setter for aggregate + *******************************************************************************/ + public WidgetAggregate withAggregate(Aggregate aggregate) + { + this.aggregate = aggregate; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java index 89e943df..56df2403 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java @@ -22,12 +22,15 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; +import java.math.BigDecimal; +import java.math.MathContext; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -51,8 +54,18 @@ public class WidgetCalculation extends AbstractWidgetValueSource Integer sum = 0; for(String valueName : valueNames) { - Integer addend = ValueUtils.getValueAsInteger(context.get(valueName)); - sum += addend; + try + { + Integer addend = ValueUtils.getValueAsInteger(context.get(valueName)); + sum += addend; + } + catch(Exception e) + { + //////////////////////////////////////////////// + // assume value to be null or 0, don't add it // + //////////////////////////////////////////////// + e.printStackTrace(); + } } return (sum); }), @@ -62,6 +75,30 @@ public class WidgetCalculation extends AbstractWidgetValueSource Instant now = Instant.now(); Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); return (then.until(now, ChronoUnit.MINUTES)); + }), + + AGE_SECONDS((List valueNames, Map context) -> + { + Instant now = Instant.now(); + Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); + return (then.until(now, ChronoUnit.SECONDS)); + }), + + PERCENT_CHANGE((List valueNames, Map context) -> + { + BigDecimal current = ValueUtils.getValueAsBigDecimal(context.get(valueNames.get(0))); + BigDecimal previous = ValueUtils.getValueAsBigDecimal(context.get(valueNames.get(1))); + + /////////////////////////////////////////////// + // 100 * ( (current - previous) / previous ) // + /////////////////////////////////////////////// + BigDecimal difference = current.subtract(previous); + if(BigDecimal.ZERO.equals(previous)) + { + return (null); + } + BigDecimal quotient = difference.divide(previous, MathContext.DECIMAL32); + return new BigDecimal("100").multiply(quotient); }); @@ -105,7 +142,7 @@ public class WidgetCalculation extends AbstractWidgetValueSource ** *******************************************************************************/ @Override - public Object evaluate(Map context) throws QException + public Object evaluate(Map context, RenderWidgetInput input) throws QException { return (operator.execute(values, context)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java index 3d672e0d..631836ea 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java @@ -22,22 +22,22 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; /******************************************************************************* ** *******************************************************************************/ -public class WidgetCount extends AbstractWidgetValueSource +public class WidgetCount extends AbstractWidgetValueSourceWithFilter { - private String tableName; - private QQueryFilter filter; - /******************************************************************************* @@ -55,29 +55,18 @@ public class WidgetCount extends AbstractWidgetValueSource ** *******************************************************************************/ @Override - public Object evaluate(Map context) throws QException + public Object evaluate(Map context, RenderWidgetInput input) throws QException { - // todo - look for params in the filter (fields or values) - // make sure to update it in supplementContext below too!! CountInput countInput = new CountInput(); countInput.setTableName(tableName); - countInput.setFilter(filter); + countInput.setFilter(getEffectiveFilter(input)); + CountOutput countOutput = new CountAction().execute(countInput); return (countOutput.getCount()); } - /******************************************************************************* - ** - *******************************************************************************/ - public void supplementContext(Map context) - { - context.put(getName() + ".filter", filter); - } - - - /******************************************************************************* ** Fluent setter for name *******************************************************************************/ @@ -89,29 +78,10 @@ public class WidgetCount extends AbstractWidgetValueSource - /******************************************************************************* - ** Getter for tableName - *******************************************************************************/ - public String getTableName() - { - return (this.tableName); - } - - - - /******************************************************************************* - ** Setter for tableName - *******************************************************************************/ - public void setTableName(String tableName) - { - this.tableName = tableName; - } - - - /******************************************************************************* ** Fluent setter for tableName *******************************************************************************/ + @Override public WidgetCount withTableName(String tableName) { this.tableName = tableName; @@ -120,33 +90,41 @@ public class WidgetCount extends AbstractWidgetValueSource - /******************************************************************************* - ** Getter for filter - *******************************************************************************/ - public QQueryFilter getFilter() - { - return (this.filter); - } - - - - /******************************************************************************* - ** Setter for filter - *******************************************************************************/ - public void setFilter(QQueryFilter filter) - { - this.filter = filter; - } - - - /******************************************************************************* ** Fluent setter for filter *******************************************************************************/ + @Override public WidgetCount withFilter(QQueryFilter filter) { this.filter = filter; return (this); } + + + /******************************************************************************* + ** Fluent setter for conditionalFilterList + *******************************************************************************/ + @Override + public WidgetCount withConditionalFilterList(List conditionalFilterList) + { + this.conditionalFilterList = conditionalFilterList; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter to add a single conditionalFilter + *******************************************************************************/ + public WidgetCount withConditionalFilter(AbstractConditionalFilter conditionalFilter) + { + if(this.conditionalFilterList == null) + { + this.conditionalFilterList = new ArrayList<>(); + } + this.conditionalFilterList.add(conditionalFilter); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java index c1da7795..3f0e3afb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java @@ -28,17 +28,16 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class WidgetQueryField extends AbstractWidgetValueSource +public class WidgetQueryField extends AbstractWidgetValueSourceWithFilter { - private String tableName; - private String selectFieldName; - private QQueryFilter filter; + private String selectFieldName; @@ -57,13 +56,11 @@ public class WidgetQueryField extends AbstractWidgetValueSource ** *******************************************************************************/ @Override - public Object evaluate(Map context) throws QException + public Object evaluate(Map context, RenderWidgetInput input) throws QException { - // todo - look for params in the filter (fields or values) - // make sure to update it in supplementContext below too!! QueryInput queryInput = new QueryInput(); queryInput.setTableName(tableName); - queryInput.setFilter(filter); + queryInput.setFilter(getEffectiveFilter(input)); queryInput.setLimit(1); QueryOutput queryOutput = new QueryAction().execute(queryInput); if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) @@ -76,16 +73,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource - /******************************************************************************* - ** - *******************************************************************************/ - public void supplementContext(Map context) - { - context.put(getName() + ".filter", filter); - } - - - /******************************************************************************* ** Fluent setter for name *******************************************************************************/ @@ -97,26 +84,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource - /******************************************************************************* - ** Getter for tableName - *******************************************************************************/ - public String getTableName() - { - return (this.tableName); - } - - - - /******************************************************************************* - ** Setter for tableName - *******************************************************************************/ - public void setTableName(String tableName) - { - this.tableName = tableName; - } - - - /******************************************************************************* ** Fluent setter for tableName *******************************************************************************/ @@ -128,26 +95,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource - /******************************************************************************* - ** Getter for filter - *******************************************************************************/ - public QQueryFilter getFilter() - { - return (this.filter); - } - - - - /******************************************************************************* - ** Setter for filter - *******************************************************************************/ - public void setFilter(QQueryFilter filter) - { - this.filter = filter; - } - - - /******************************************************************************* ** Fluent setter for filter *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtilsTest.java new file mode 100644 index 00000000..a2f4b8ff --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtilsTest.java @@ -0,0 +1,109 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.dashboard.widgets; + + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for NoCodeWidgetVelocityUtils + *******************************************************************************/ +class NoCodeWidgetVelocityUtilsTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFormatSecondsAsDuration() + { + int HOUR = 60 * 60; + int DAY = 24 * 60 * 60; + + NoCodeWidgetVelocityUtils utils = new NoCodeWidgetVelocityUtils(null, null); + assertEquals("", utils.formatSecondsAsDuration(null)); + assertEquals("0 seconds", utils.formatSecondsAsDuration(0)); + assertEquals("1 second", utils.formatSecondsAsDuration(1)); + assertEquals("59 seconds", utils.formatSecondsAsDuration(59)); + + assertEquals("1 minute", utils.formatSecondsAsDuration(60)); + assertEquals("1 minute 1 second", utils.formatSecondsAsDuration(61)); + assertEquals("2 minutes 1 second", utils.formatSecondsAsDuration(121)); + assertEquals("2 minutes 2 seconds", utils.formatSecondsAsDuration(122)); + assertEquals("3 minutes", utils.formatSecondsAsDuration(180)); + + assertEquals("1 hour", utils.formatSecondsAsDuration(HOUR)); + assertEquals("1 hour 1 second", utils.formatSecondsAsDuration(HOUR + 1)); + assertEquals("1 hour 1 minute", utils.formatSecondsAsDuration(HOUR + 60)); + assertEquals("1 hour 1 minute 1 second", utils.formatSecondsAsDuration(HOUR + 60 + 1)); + assertEquals("1 hour 2 minutes 1 second", utils.formatSecondsAsDuration(HOUR + 120 + 1)); + assertEquals("2 hours 2 minutes 2 seconds", utils.formatSecondsAsDuration(2 * 60 * 60 + 120 + 2)); + assertEquals("23 hours 59 minutes 59 seconds", utils.formatSecondsAsDuration(DAY - 1)); + + assertEquals("1 day", utils.formatSecondsAsDuration(DAY)); + assertEquals("1 day 1 second", utils.formatSecondsAsDuration(DAY + 1)); + assertEquals("1 day 1 minute", utils.formatSecondsAsDuration(DAY + 60)); + assertEquals("1 day 1 hour 1 minute 1 second", utils.formatSecondsAsDuration(DAY + HOUR + 60 + 1)); + assertEquals("2 days 2 hours 2 minutes 2 seconds", utils.formatSecondsAsDuration(2 * DAY + 2 * 60 * 60 + 120 + 2)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFormatSecondsAsRoundedDuration() + { + int HOUR = 60 * 60; + int DAY = 24 * 60 * 60; + + NoCodeWidgetVelocityUtils utils = new NoCodeWidgetVelocityUtils(null, null); + assertEquals("", utils.formatSecondsAsRoundedDuration(null)); + assertEquals("0 seconds", utils.formatSecondsAsRoundedDuration(0)); + assertEquals("1 second", utils.formatSecondsAsRoundedDuration(1)); + assertEquals("59 seconds", utils.formatSecondsAsRoundedDuration(59)); + + assertEquals("1 minute", utils.formatSecondsAsRoundedDuration(60)); + assertEquals("1 minute", utils.formatSecondsAsRoundedDuration(61)); + assertEquals("2 minutes", utils.formatSecondsAsRoundedDuration(121)); + assertEquals("2 minutes", utils.formatSecondsAsRoundedDuration(122)); + assertEquals("3 minutes", utils.formatSecondsAsRoundedDuration(180)); + + assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR)); + assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 1)); + assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 60)); + assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 60 + 1)); + assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 120 + 1)); + assertEquals("2 hours", utils.formatSecondsAsRoundedDuration(2 * 60 * 60 + 120 + 2)); + assertEquals("23 hours", utils.formatSecondsAsRoundedDuration(DAY - 1)); + + assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY)); + assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + 1)); + assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + 60)); + assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + HOUR + 60 + 1)); + assertEquals("2 days", utils.formatSecondsAsRoundedDuration(2 * DAY + 2 * 60 * 60 + 120 + 2)); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java index 0bbf1a7e..40877f8a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -196,8 +196,8 @@ class QValueFormatterTest extends BaseTest void testFormatDates() { assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1))); - assertEquals("2023-02-01 7:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15))); - assertEquals("2023-02-01 7:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central")))); + assertEquals("2023-02-01 07:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15))); + assertEquals("2023-02-01 07:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central")))); } } \ No newline at end of file