Commit 9d1bebb8 authored by doc@bgerp.org's avatar doc@bgerp.org

p12046 Java reports, JSP tags.

parent fcd594c8
N: Plugin Report: enhanced UI, Java classes for data retrieving, https://bgerp.ru/doc/3.0/manual/plugin/report/index.html
N: Plugin Report: report "Processes", https://bgerp.ru/doc/3.0/manual/plugin/report/index.html
N: JSP tags "shell:title", "shell:state", "ui:user-link", "ui:page-control" instead of includes.
C: Rewrite of deprecated code in JS and JSP.
C: CSS "width: 100%" for table classes "data" and "hdata".
F: Exception on refresh the table of linked processed.
C: Increased width of date input fields.
......@@ -3,7 +3,8 @@
<item title="Отчеты">
<item action="ru.bgcrm.plugin.report.struts.action.ReportAction:null" title="Список">
<b>allowedReports</b> - разрешает отображение только определенных отчетов (указываются id отчетов в XML-описаниях файлов отчетов через запятую)<br/>
</item>
</item>
<item action="ru.bgcrm.plugin.report.struts.action.ReportAction:get" title="Вывод отчёта"/>
</item>
</item>
</item>
\ No newline at end of file
......@@ -4,8 +4,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
......@@ -46,9 +44,6 @@ public class CommonDAO {
protected final static String SQL_REPLACE = " REPLACE INTO ";
protected final static String SQL_ON_DUP_KEY_UPDATE = " ON DUPLICATE KEY UPDATE ";
protected DateFormat dateFormat_DDMMYYYY = new SimpleDateFormat("dd.MM.yyyy");
protected DateFormat dateFormat_DDMMYYYY_HHMM = new SimpleDateFormat("dd.MM.yyyy HH:mm");
protected final Log log = Log.getLog(this.getClass());
protected Connection con;
......@@ -111,8 +106,21 @@ public class CommonDAO {
}
pd.close();
}
/**
* Use {@link #getPageLimit(Page)}.
*/
@Deprecated
protected String getMySQLLimit(Page page) {
return getPageLimit(page);
}
/**
* Generates page limits.
* @param page
* @return
*/
protected String getPageLimit(Page page) {
StringBuilder sql = new StringBuilder();
if (page != null && page.getPageSize() > 0) {
sql.append(" LIMIT ");
......
package ru.bgcrm.plugin.report.dao;
import static ru.bgcrm.dao.process.Tables.TABLE_PROCESS;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import ru.bgcrm.cache.ProcessTypeCache;
import ru.bgcrm.cache.UserCache;
import ru.bgcrm.model.BGIllegalArgumentException;
import ru.bgcrm.model.SearchResult;
import ru.bgcrm.struts.form.DynActionForm;
import ru.bgcrm.util.Setup;
import ru.bgcrm.util.TimeUtils;
import ru.bgcrm.util.sql.PreparedDelay;
import ru.bgerp.util.Log;
public class ProcessReportDAO extends ReportDAO {
private static final Log log = Log.getLog();
@Override
public String getJspFile() {
return "/WEB-INF/jspf/user/plugin/report/report/process.jsp";
}
@Override
public void get(DynActionForm form) throws Exception {
Date dateFrom = form.getParamDate("dateFrom", new Date(), true);
Date dateTo = form.getParamDate("dateTo", new Date(), true);
String type = form.getParam("type", "create", true);
if (!StringUtils.equalsAny(type, "create", "close"))
throw new BGIllegalArgumentException();
try (Connection con = Setup.getSetup().getDBSlaveConnectionFromPool()) {
SearchResult<Object[]> result = new SearchResult<>(form);
PreparedDelay pd = new PreparedDelay(con);
pd.addQuery(SQL_SELECT_COUNT_ROWS + "id, type_id, " + type + "_user_id, description" + SQL_FROM + TABLE_PROCESS + SQL_WHERE);
pd.addQuery(type + "_dt");
pd.addQuery(" BETWEEN ? AND ?");
pd.addDate(dateFrom);
pd.addDate(TimeUtils.getNextDay(dateTo));
pd.addQuery(SQL_ORDER_BY);
pd.addQuery(type + "_user_id");
ResultSet rs = pd.executeQuery();
while (rs.next()) {
// TODO: Keep in mind necessity of retrieving file uploadable data in future.
Object[] row = new Object[4];
row[0] = rs.getInt(1);
row[1] = ProcessTypeCache.getProcessType(rs.getInt(2));
row[2] = UserCache.getUser(rs.getInt(3));
row[3] = rs.getString(4);
result.getList().add(row);
}
setRecordCount(form.getPage(), pd.getPrepared());
}
}
}
package ru.bgcrm.plugin.report.dao;
import ru.bgcrm.dao.CommonDAO;
import ru.bgcrm.struts.form.DynActionForm;
public abstract class ReportDAO extends CommonDAO {
public ReportDAO() {
super(null);
}
/**
* Returns JSP file for data presentation.
* @return
*/
public abstract String getJspFile();
/**
* Does the report data retrieving logic.
* @param form
*/
public abstract void get(DynActionForm form) throws Exception;
}
\ No newline at end of file
......@@ -11,92 +11,76 @@ import ru.bgcrm.util.Config;
import ru.bgcrm.util.ParameterMap;
import ru.bgcrm.util.Utils;
public class PrintQueueConfig
extends Config
{
private LinkedHashMap<Integer, PrintType> printTypes = new LinkedHashMap<Integer, PrintType>();
public PrintQueueConfig( ParameterMap config )
{
super( config );
for( Map.Entry<Integer, ParameterMap> me : config.subIndexed( "media.print." ).entrySet() )
{
PrintType type = new PrintType( me.getKey(), me.getValue() );
printTypes.put( type.getId(), type );
}
}
public Collection<PrintType> getPrintTypes()
{
return printTypes.values();
}
public PrintType getPrintType( int id )
{
return printTypes.get( id );
}
public static class PrintType
extends IdTitle
{
public static final String TYPE_PDF = "pdf";
public static final String ORIENTATION_PORTRAIT = "portrait";
public static final String ORIENTATION_LANDSCAPE = "landscape";
// в реальности пока не используется, может генерироваться только PDF
private final String type;
private final String orientation;
private final String fileName;
private final List<String> columnIds = new ArrayList<String>();
private final List<Integer> columnWidths = new ArrayList<Integer>();
private PrintType( int id, ParameterMap config )
{
super( id, config.get( "title" ) );
this.type = config.get( "type", TYPE_PDF );
this.orientation = config.get( "orientation", ORIENTATION_PORTRAIT );
this.fileName = config.get( "fileName", "queue.pdf" );
for( String pair : Utils.toList( config.get( "columns" ) ) )
{
String[] tokens = pair.split( ":" );
if( tokens.length != 2 )
{
continue;
}
columnIds.add( tokens[0] );
columnWidths.add( Utils.parseInt( tokens[1] ) );
}
}
public String getType()
{
return type;
}
public String getOrientation()
{
return orientation;
}
public String getFileName()
{
return fileName;
}
public List<String> getColumnIds()
{
return columnIds;
}
public List<Integer> getColumnWidths()
{
return columnWidths;
}
}
public class PrintQueueConfig extends Config {
private LinkedHashMap<Integer, PrintType> printTypes = new LinkedHashMap<Integer, PrintType>();
public PrintQueueConfig(ParameterMap config) {
super(config);
for (Map.Entry<Integer, ParameterMap> me : config.subIndexed("media.print.").entrySet()) {
PrintType type = new PrintType(me.getKey(), me.getValue());
printTypes.put(type.getId(), type);
}
}
public Collection<PrintType> getPrintTypes() {
return printTypes.values();
}
public PrintType getPrintType(int id) {
return printTypes.get(id);
}
public static class PrintType extends IdTitle {
public static final String TYPE_PDF = "pdf";
public static final String ORIENTATION_PORTRAIT = "portrait";
public static final String ORIENTATION_LANDSCAPE = "landscape";
// в реальности пока не используется, может генерироваться только PDF
private final String type;
private final String orientation;
private final String fileName;
private final List<String> columnIds = new ArrayList<String>();
private final List<Integer> columnWidths = new ArrayList<Integer>();
private PrintType(int id, ParameterMap config) {
super(id, config.get("title"));
this.type = config.get("type", TYPE_PDF);
this.orientation = config.get("orientation", ORIENTATION_PORTRAIT);
this.fileName = config.get("fileName", "queue.pdf");
for (String pair : Utils.toList(config.get("columns"))) {
String[] tokens = pair.split(":");
if (tokens.length != 2) {
continue;
}
columnIds.add(tokens[0]);
columnWidths.add(Utils.parseInt(tokens[1]));
}
}
public String getType() {
return type;
}
public String getOrientation() {
return orientation;
}
public String getFileName() {
return fileName;
}
public List<String> getColumnIds() {
return columnIds;
}
public List<Integer> getColumnWidths() {
return columnWidths;
}
}
}
\ No newline at end of file
......@@ -8,19 +8,22 @@ import ru.bgcrm.util.ParameterMap;
public class Report {
private final String id;
private final String title;
private final String jspFile;
private final String daoClass;
private final String jspFile;
@Deprecated
public Report(Document doc) {
Element docEl = doc.getDocumentElement();
id = docEl.getAttribute("id");
daoClass = null;
jspFile = docEl.getAttribute("jspFile");
title = docEl.getAttribute("title");
}
public Report(String id, ParameterMap config) {
this.id = id;
this.daoClass = config.get("daoClass");
this.jspFile = config.get("jspFile");
this.title = config.get("title");
}
......@@ -32,6 +35,10 @@ public class Report {
public String getTitle() {
return title;
}
public String getDaoClass() {
return daoClass;
}
public String getJspFile() {
return jspFile;
......
package ru.bgcrm.plugin.report.struts.action;
import java.sql.Connection;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import ru.bgcrm.dynamic.DynamicClassManager;
import ru.bgcrm.model.BGMessageException;
import ru.bgcrm.plugin.report.dao.ReportDAO;
import ru.bgcrm.plugin.report.model.Config;
import ru.bgcrm.plugin.report.model.Report;
import ru.bgcrm.struts.action.BaseAction;
import ru.bgcrm.struts.form.DynActionForm;
import ru.bgcrm.util.Utils;
import ru.bgcrm.util.sql.ConnectionSet;
public class ReportAction extends BaseAction {
public ReportAction() {
super();
}
public ActionForward doReport(ActionMapping mapping, DynActionForm form, Connection con) throws Exception {
return processUserTypedForward(con, mapping, form, null);
public ActionForward get(ActionMapping mapping, DynActionForm form, ConnectionSet conSet) throws Exception {
Report report = setup.getConfig(Config.class).getReportMap().get(form.get("reportId"));
if (report == null)
throw new BGMessageException(l.l("Report not found"));
if (StringUtils.isNotBlank(report.getDaoClass())) {
log.debug("Creating Java DAO class: %s", report.getDaoClass());
ReportDAO dao = DynamicClassManager.newInstance(report.getDaoClass());
dao.get(form);
form.setForwardFile(dao.getJspFile());
return processUserTypedForward(conSet, mapping, form);
}
form.setForwardFile(report.getJspFile());
return processUserTypedForward(conSet, mapping, form);
}
@Override
protected ActionForward unspecified(ActionMapping mapping, DynActionForm form, Connection con) throws Exception {
protected ActionForward unspecified(ActionMapping mapping, DynActionForm form, ConnectionSet conSet) throws Exception {
form.getResponse().setData("allowedReports", Utils.toSet(form.getPermission().get("allowedReports")));
return mapping.findForward(FORWARD_DEFAULT);
......
......@@ -22,6 +22,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
......@@ -376,36 +377,37 @@ public class ProcessAction extends BaseAction {
queue.processDataForMedia(form, "xls", media);
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("BGERP process");
List<ColumnConf> columnList = queue.getMediaColumnList("xls");
Row titleRow = sheet.createRow(0);
for (int i = 0; i < columnList.size(); i++) {
Cell titleCell = titleRow.createCell(i);
titleCell.setCellValue(columnList.get(i).getTitle());
}
for (int k = 0; k < media.size(); k++) {
//Create a new row in current sheet
Row row = sheet.createRow(k + 1);
Object[] dataRow = media.get(k);
for (int i = 0; i < dataRow.length; i++) {
if (dataRow[i].equals("null")) {
continue;
} else {
//Create a new cell in current row
Cell cell = row.createCell(i);
cell.setCellValue(dataRow[i].toString());
try (HSSFWorkbook workbook = new HSSFWorkbook()) {
HSSFSheet sheet = workbook.createSheet("BGERP process");
List<ColumnConf> columnList = queue.getMediaColumnList("xls");
Row titleRow = sheet.createRow(0);
for (int i = 0; i < columnList.size(); i++) {
Cell titleCell = titleRow.createCell(i);
titleCell.setCellValue(columnList.get(i).getTitle());
}
for (int k = 0; k < media.size(); k++) {
//Create a new row in current sheet
Row row = sheet.createRow(k + 1);
Object[] dataRow = media.get(k);
for (int i = 0; i < dataRow.length; i++) {
if (dataRow[i].equals("null")) {
continue;
} else {
//Create a new cell in current row
Cell cell = row.createCell(i);
cell.setCellValue(dataRow[i].toString());
}
}
}
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=bgcrm_process_list.xls");
workbook.write(response.getOutputStream());
}
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=bgcrm_process_list.xls");
workbook.write(response.getOutputStream());
return null;
}
......@@ -436,7 +438,7 @@ public class ProcessAction extends BaseAction {
}
String paramKey = QUEUE_FULL_FILTER_PARAMS + queueId;
personalizationMap.set(paramKey, Base64.getEncoder().encodeToString(SerializationUtils.serialize(ahm)));
personalizationMap.put(paramKey, Base64.getEncoder().encodeToString(SerializationUtils.serialize(ahm)));
}
public ActionForward process(ActionMapping mapping, DynActionForm form, Connection con) throws Exception {
......@@ -1243,11 +1245,13 @@ public class ProcessAction extends BaseAction {
}
// проверка и обновление статуса вкладки, если нужно
IfaceState ifaceState = new IfaceState(form);
IfaceState currentState = new IfaceState(Process.OBJECT_TYPE, id, form,
String.valueOf(searchResultLinked.getPage().getRecordCount()),
String.valueOf(searchResultLink.getPage().getRecordCount()));
new IfaceStateDAO(con).compareAndUpdateState(ifaceState, currentState, form);
if (Strings.isNotBlank(form.getParam(IfaceState.REQUEST_PARAM_IFACE_ID))) {
IfaceState ifaceState = new IfaceState(form);
IfaceState currentState = new IfaceState(Process.OBJECT_TYPE, id, form,
String.valueOf(searchResultLinked.getPage().getRecordCount()),
String.valueOf(searchResultLink.getPage().getRecordCount()));
new IfaceStateDAO(con).compareAndUpdateState(ifaceState, currentState, form);
}
return processUserTypedForward(con, mapping, form, "linkProcessList");
}
......
This diff is collapsed.
......@@ -74,6 +74,10 @@ public class Setup extends Preferences {
public Connection getDBConnectionFromPool() {
return connectionPool.getDBConnectionFromPool();
}
public Connection getDBSlaveConnectionFromPool() {
return connectionPool.getDBSlaveConnectionFromPool();
}
private void reloadConfig(Connection con) {
try {
......
......@@ -314,6 +314,32 @@
|секрет для запроса файла
|===
=== iface_state
Кэш состояний для интерфейса, например подписи на вкладке привязанных процессов об их количестве.
Данные файлов.
[cols="a,a,50%a", options="header"]
|===
|Столбец
|Тип
|Описание
|object_type
|VARCHAR
|тип объекта
|object_id
|INT
|код объекта
|iface_id
|VARCHAR
|идентификатор элемента интерфейса
|state
|VARCHAR
|закэшированная строка
|===
=== message
Сообщения, дополнительные сведения о значениях полей - в исходном коде javadoc:ru.bgcrm.model.message.Message[].
[cols="a,a,50%a", options="header"]
......
......@@ -2,7 +2,13 @@
:toc:
Плагин предназначен для разработки отчётов в HTML формате с гибкими фильтрами.
Логика и отображение отчёта реализуется с помощью <<../../kernel/extension.adoc#jsp, JSP>>.
Логика отчёта реализуется с помощью обычных либо <<../../kernel/extension.adoc#dyn, динамических>> Java классов, отображение - <<../../kernel/extension.adoc#jsp, JSP>> шаблоном.
Возможна разработка отчётов, где и логика выборки помещена в JSP шаблон, так называемые *JSP отчёты*.
Данный подход обладает следующими недостатками:
[square]
* работа с БД удобнее и стандартнее выполняется в Java коде;
* невозможно реализация выгрузки данных в различные форматы;
* смешивание слоёв логики и представления усложняют сопровождение.
[[config]]
== Конфигурация
......@@ -11,23 +17,41 @@
[source]
----
report:report.<id>.title=<title>
report:report.<id>.daoClass=<daoClass>
----
Либо для JSP отчёта:
[source]
----
report:report.<id>.title=<title>
report:report.<id>.jspFile=<jspFile>
----
Где:
[square]
* *<id>* - числовая идентификатор отчёта;
* *<daoClass>* - обычный либо динамический Java класс, расширяющий
* *<jspFile>* - путь к JSP шаблону, генерирующему отчёт, расположенный в каталоге *WEB-INF/custom/plugin/report/*.
IMPORTANT: Перенести старые объявления отчётов из XML файлов каталога report в конфигурацию. Поддержка будет в скором времени отключена.
=== Стандартные отчёты
С программой поставляются встроенные отчёты, конфигурация:
[source]
----
report:report.100.title=Процессы
report:report.100.daoClass=ru.bgcrm.plugin.report.dao.ProcessReportDAO
----
Их исходный код может использоваться в качестве примера.
== Оснастка "Отчеты"
В оснастке отображаются все сконфигурированные в системе отчёты. При выборе кнопки отчёта вызывается связанный с отчётом JSP файл.
В оснастке отображаются все сконфигурированные в системе отчёты.
image::_res/report_general.png[]
[[samples]]
== Примеры
== Примеры JSP отчётов
Числовые коды отчётов в конфигурациях примеров даны случайно и в реальной системе могут быть любыми.
=== Пример отчёта
......
......@@ -740,15 +740,15 @@ link:../../src/ru/bgcrm/struts/action/MessageAction.java#L237-L249[ru.bgcrm.stru
Сохранение плюс восстановление сразу:
[snippet, from="//", to=");", remove-leading=" "]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1089-L1093[ru.bgcrm.struts.action.ProcessAction]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1091-L1095[ru.bgcrm.struts.action.ProcessAction]
=== Отображение на вкладке количества элементов
Например, количества связанных процессов. Сохраняется при первом вызове.
Используется класс javadoc:ru.bgcrm.model.IfaceState[]
Обновление значения:
[snippet, from="// п", to="m);", remove-leading=" "]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1245-L1250[ru.bgcrm.struts.action.ProcessAction]
[snippet, from="// п", to="}", remove-leading=" "]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1247-L1254[ru.bgcrm.struts.action.ProcessAction]
Отображение в JSP:
[snippet, from="<c:if", to="if>", remove-leading=" "]
......@@ -806,15 +806,13 @@ link:../../webapps/js/crm.shell.js#L5-L12[webapps/js/crm.shell.js]
link:../../webapps/js/crm.js#L18-L21[webapps/js/crm.js]
=== Постраничный вывод
NOTE: AJAX функции в данном примере <<sample-jsp-ui, неактуальны>>.
Вывод результатов в JSP и отображение формы:
[snippet, from="<c:", to="sc>", remove-leading=" "]
link:../../webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp#L74-L85[webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp]
[snippet, from="<c:", to="rm>", remove-leading=" "]
link:../../webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp#L56-L64[webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp]
Java action:
[snippet, from="//", to="id);", remove-leading=" "]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1236-L1238[ru.bgcrm.struts.action.ProcessAction]
link:../../src/ru/bgcrm/struts/action/ProcessAction.java#L1238-L1240[ru.bgcrm.struts.action.ProcessAction]
[[sample-jsp-ui]]
=== JSP UI
......
......@@ -12,11 +12,9 @@
<td>Сессии (вход / последняя активность)</td>
</tr>
<c:forEach var="item" items="${logged}">
<c:set var="userId" value="${item.key.id}"/>
<c:set var="sessions" value="${item.value}"/>
<tr>
<td><%@ include file="/WEB-INF/jspf/user_link.jsp"%></td>
<td><ui:user-link id="${item.key.id}"/></td>
<td>
<c:forEach var="session" items="${sessions}">
${u:formatDate( session.loginTime, 'ymdhms' )} /
......
......@@ -65,9 +65,7 @@
</div>
</html:form>
<c:set var="state" value="Редактор"/>
<c:set var="help" value="http://www.bgcrm.ru/doc/3.0/manual/kernel/setup.html#config"/>
<%@ include file="/WEB-INF/jspf/shell_state.jsp"%>
<shell:state text="${l.l('Редактор')}" help="http://www.bgcrm.ru/doc/3.0/manual/kernel/setup.html#config"/>
<script>
$(function () {
......
......@@ -81,12 +81,7 @@
<c:otherwise>Неизвестный статус (${item.status})</c:otherwise>
</c:choose>
</td>
<td>
<u:sc>
<c:set var="userId" value="${item.id}"/>
<%@ include file="/WEB-INF/jspf/user_link.jsp"%>
</u:sc>
</td>