diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java
new file mode 100644
index 00000000..133c3f8e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java
@@ -0,0 +1,386 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.savedreports;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
+
+
+/*******************************************************************************
+ ** Entity bean for the saved report table
+ *******************************************************************************/
+public class SavedReport extends QRecordEntity
+{
+ public static final String TABLE_NAME = "savedReport";
+
+ @QField(isEditable = false)
+ private Integer id;
+
+ @QField(isEditable = false)
+ private Instant createDate;
+
+ @QField(isEditable = false)
+ private Instant modifyDate;
+
+ @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
+ private String label;
+
+ @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String tableName; // todo - qqqTableId... ?
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
+ private String userId;
+
+ @QField(label = "Query Filter")
+ private String queryFilterJson;
+
+ @QField(label = "Columns")
+ private String columnsJson;
+
+ @QField(label = "Input Fields")
+ private String inputFieldsJson;
+
+ @QField(label = "Pivot Table")
+ private String pivotTableJson;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public SavedReport()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public SavedReport(QRecord qRecord) throws QException
+ {
+ populateFromQRecord(qRecord);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ **
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for label
+ **
+ *******************************************************************************/
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ **
+ *******************************************************************************/
+ public SavedReport withLabel(String label)
+ {
+ this.label = label;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ **
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public SavedReport withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userId
+ **
+ *******************************************************************************/
+ public String getUserId()
+ {
+ return userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userId
+ **
+ *******************************************************************************/
+ public void setUserId(String userId)
+ {
+ this.userId = userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userId
+ **
+ *******************************************************************************/
+ public SavedReport withUserId(String userId)
+ {
+ this.userId = userId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for queryFilterJson
+ *******************************************************************************/
+ public String getQueryFilterJson()
+ {
+ return (this.queryFilterJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for queryFilterJson
+ *******************************************************************************/
+ public void setQueryFilterJson(String queryFilterJson)
+ {
+ this.queryFilterJson = queryFilterJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for queryFilterJson
+ *******************************************************************************/
+ public SavedReport withQueryFilterJson(String queryFilterJson)
+ {
+ this.queryFilterJson = queryFilterJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for columnsJson
+ *******************************************************************************/
+ public String getColumnsJson()
+ {
+ return (this.columnsJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for columnsJson
+ *******************************************************************************/
+ public void setColumnsJson(String columnsJson)
+ {
+ this.columnsJson = columnsJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for columnsJson
+ *******************************************************************************/
+ public SavedReport withColumnsJson(String columnsJson)
+ {
+ this.columnsJson = columnsJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inputFieldsJson
+ *******************************************************************************/
+ public String getInputFieldsJson()
+ {
+ return (this.inputFieldsJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inputFieldsJson
+ *******************************************************************************/
+ public void setInputFieldsJson(String inputFieldsJson)
+ {
+ this.inputFieldsJson = inputFieldsJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inputFieldsJson
+ *******************************************************************************/
+ public SavedReport withInputFieldsJson(String inputFieldsJson)
+ {
+ this.inputFieldsJson = inputFieldsJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for pivotTableJson
+ *******************************************************************************/
+ public String getPivotTableJson()
+ {
+ return (this.pivotTableJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for pivotTableJson
+ *******************************************************************************/
+ public void setPivotTableJson(String pivotTableJson)
+ {
+ this.pivotTableJson = pivotTableJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for pivotTableJson
+ *******************************************************************************/
+ public SavedReport withPivotTableJson(String pivotTableJson)
+ {
+ this.pivotTableJson = pivotTableJson;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
new file mode 100644
index 00000000..e8354a6d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
@@ -0,0 +1,101 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.savedreports;
+
+
+import java.util.List;
+import java.util.function.Consumer;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SavedReportsMetaDataProvider
+{
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ instance.addTable(defineSavedReportTable(backendName, backendDetailEnricher));
+ instance.addPossibleValueSource(defineSavedReportPossibleValueSource());
+ instance.addProcess(new RenderSavedReportMetaDataProducer().produce(instance));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QTableMetaData defineSavedReportTable(String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ QTableMetaData table = new QTableMetaData()
+ .withName(SavedReport.TABLE_NAME)
+ .withLabel("Saved Report")
+ .withIcon(new QIcon().withName("article"))
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("label")
+ .withBackendName(backendName)
+ .withPrimaryKeyField("id")
+ .withFieldsFromEntity(SavedReport.class)
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label")))
+ .withSection(new QFieldSection("settings", new QIcon().withName("settings"), Tier.T2, List.of("tableName")))
+ .withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("queryFilterJson", "columnsJson", "pivotTableJson")))
+ .withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
+
+ for(String jsonFieldName : List.of("queryFilterJson", "columnsJson", "inputFieldsJson", "pivotTableJson"))
+ {
+ table.getField(jsonFieldName).withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json")));
+ }
+
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+
+ return (table);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QPossibleValueSource defineSavedReportPossibleValueSource()
+ {
+ return QPossibleValueSource.newForTable(SavedReport.TABLE_NAME);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
new file mode 100644
index 00000000..4c768b86
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
@@ -0,0 +1,110 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.processes.implementations.savedreports;
+
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.Serializable;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RenderSavedReportExecuteStep implements BackendStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ try
+ {
+ SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
+ File tmpFile = File.createTempFile("SavedReport" + savedReport.getId(), ".xlsx", new File("/tmp/"));
+
+ runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report");
+
+ QReportMetaData reportMetaData = new SavedReportToReportMetaDataAdapter().adapt(savedReport);
+
+ try(FileOutputStream reportOutputStream = new FileOutputStream(tmpFile))
+ {
+ ReportInput reportInput = new ReportInput();
+ reportInput.setReportMetaData(reportMetaData);
+ reportInput.setReportDestination(new ReportDestination()
+ .withReportFormat(ReportFormat.XLSX) // todo - variable
+ .withReportOutputStream(reportOutputStream));
+
+ Map values = runBackendStepInput.getValues();
+ reportInput.setInputValues(values);
+
+ new GenerateReportAction().execute(reportInput);
+ }
+
+ String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
+ runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + ".xlsx");
+ runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath());
+ }
+ catch(Exception e)
+ {
+ // todo - render error screen?
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, SavedReport report)
+ {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault());
+ String datePart = formatter.format(Instant.now());
+
+ String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName");
+ if(!StringUtils.hasContent(downloadFileBaseName))
+ {
+ downloadFileBaseName = report.getLabel();
+ }
+
+ downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-");
+
+ return (downloadFileBaseName + " - " + datePart);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
new file mode 100644
index 00000000..06f6648c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
@@ -0,0 +1,79 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.processes.implementations.savedreports;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterface
+{
+ public static final String NAME = "renderSavedReport";
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData()
+ .withName(NAME)
+ .withTableName(SavedReport.TABLE_NAME)
+ .withIcon(new QIcon().withName("print"))
+ .addStep(new QBackendStepMetaData()
+ .withName("pre")
+ .withInputData(new QFunctionInputMetaData().withRecordListMetaData(new QRecordListMetaData()
+ .withTableName(SavedReport.TABLE_NAME)))
+ .withCode(new QCodeReference(RenderSavedReportPreStep.class)))
+ .addStep(new QFrontendStepMetaData()
+ .withName("input")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)))
+ .addStep(new QBackendStepMetaData()
+ .withName("execute")
+ .withInputData(new QFunctionInputMetaData().withRecordListMetaData(new QRecordListMetaData()
+ .withTableName(SavedReport.TABLE_NAME)))
+ .withCode(new QCodeReference(RenderSavedReportExecuteStep.class)))
+ // todo - no no, stream the damn thing... how to do that??
+ .addStep(new QFrontendStepMetaData()
+ .withName("output")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM)));
+
+ return (process);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java
new file mode 100644
index 00000000..277bc431
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java
@@ -0,0 +1,48 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.processes.implementations.savedreports;
+
+
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RenderSavedReportPreStep implements BackendStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ // todo - verify ran on 1
+ // todo - load the SavedReport
+ // todo - check for inputs - set up the input screen...
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java
new file mode 100644
index 00000000..0d9f3f3a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java
@@ -0,0 +1,183 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.processes.implementations.savedreports;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.core.type.TypeReference;
+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.reporting.pivottable.PivotTableDefinition;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SavedReportToReportMetaDataAdapter
+{
+ private static final QLogger LOG = QLogger.getLogger(SavedReportToReportMetaDataAdapter.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QReportMetaData adapt(SavedReport savedReport) throws QException
+ {
+ try
+ {
+ QReportMetaData reportMetaData = new QReportMetaData();
+ reportMetaData.setLabel(savedReport.getLabel());
+
+ ////////////////////////////
+ // set up the data-source //
+ ////////////////////////////
+ QReportDataSource dataSource = new QReportDataSource();
+ reportMetaData.setDataSources(List.of(dataSource));
+ dataSource.setName("main");
+
+ QTableMetaData table = QContext.getQInstance().getTable(savedReport.getTableName());
+ dataSource.setSourceTable(savedReport.getTableName());
+
+ dataSource.setQueryFilter(JsonUtils.toObject(savedReport.getQueryFilterJson(), QQueryFilter.class));
+
+ // todo!!! oh my.
+ List queryJoins = null;
+ dataSource.setQueryJoins(queryJoins);
+
+ //////////////////////////
+ // set up the main view //
+ //////////////////////////
+ QReportView view = new QReportView();
+ reportMetaData.setViews(ListBuilder.of(view));
+ view.setName("main");
+ view.setType(ReportType.TABLE);
+ view.setDataSourceName(dataSource.getName());
+ view.setLabel(savedReport.getLabel()); // todo eh?
+ view.setIncludeHeaderRow(true);
+
+ // don't need:
+ // view.setOrderByFields(); - only used for summary reports
+ // view.setTitleFormat(); - not using at this time
+ // view.setTitleFields(); - not using at this time
+ // view.setRecordTransformStep();
+ // view.setViewCustomizer();
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // columns in the saved-report look like a JSON object, w/ a key "columns", which is an array of objects //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Map columnsObject = JsonUtils.toObject(savedReport.getColumnsJson(), new TypeReference<>() {});
+ List