Fixed add case bug
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -71,7 +71,7 @@
|
|||||||
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
|
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
|
||||||
"CYGNUS_PAYLOAD_PRIVATE_KEY": "file:${workspaceFolder}/config/keys/case-save-private.pem",
|
"CYGNUS_PAYLOAD_PRIVATE_KEY": "file:${workspaceFolder}/config/keys/case-save-private.pem",
|
||||||
"CYGNUS_PAYLOAD_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/case-save-public.pem",
|
"CYGNUS_PAYLOAD_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/case-save-public.pem",
|
||||||
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT10S",
|
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT30S",
|
||||||
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
|
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
|
||||||
},
|
},
|
||||||
"vmArgs": "-Dserver.port=8080 -Djava.awt.headless=true -Dmatrix.webapp=${workspaceFolder}/cygnus-onprem-app/build/WebContent -Dmatrix.classes=${workspaceFolder}/cygnus-onprem-app/target/classes",
|
"vmArgs": "-Dserver.port=8080 -Djava.awt.headless=true -Dmatrix.webapp=${workspaceFolder}/cygnus-onprem-app/build/WebContent -Dmatrix.classes=${workspaceFolder}/cygnus-onprem-app/target/classes",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.time.Clock;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
@@ -18,6 +19,9 @@ import reactor.core.publisher.Mono;
|
|||||||
|
|
||||||
public class CloudIdentityClient {
|
public class CloudIdentityClient {
|
||||||
|
|
||||||
|
private static final ParameterizedTypeReference<List<CloudDataItem>> CLOUD_DATA_LIST =
|
||||||
|
new ParameterizedTypeReference<>() { };
|
||||||
|
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
private final MachineTokenProvider tokenProvider;
|
private final MachineTokenProvider tokenProvider;
|
||||||
private final LoginEnvelopeEncryptor encryptor;
|
private final LoginEnvelopeEncryptor encryptor;
|
||||||
@@ -87,8 +91,7 @@ public class CloudIdentityClient {
|
|||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(new CloudDataRequest(scope, Map.copyOf(data)))
|
.bodyValue(new CloudDataRequest(scope, Map.copyOf(data)))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToFlux(CloudDataItem.class)
|
.bodyToMono(CLOUD_DATA_LIST))
|
||||||
.collectList())
|
|
||||||
.timeout(properties.requestTimeout());
|
.timeout(properties.requestTimeout());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.cygnus.client.model;
|
package com.cygnus.client.model;
|
||||||
|
|
||||||
public record CloudDataItem(Object value, String label, String group) {
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record CloudDataItem(Object value, String label, String group, Map<String, Object> data) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.cygnus.cloud.platform.api;
|
package com.cygnus.cloud.platform.api;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public record ScopedDataItem(
|
public record ScopedDataItem(
|
||||||
Object value,
|
Object value,
|
||||||
String label,
|
String label,
|
||||||
String group) {
|
String group,
|
||||||
|
Map<String, Object> data) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.cygnus.cloud.platform.api.ScopedDataItem;
|
|||||||
import io.vertx.sqlclient.Row;
|
import io.vertx.sqlclient.Row;
|
||||||
import io.vertx.sqlclient.Tuple;
|
import io.vertx.sqlclient.Tuple;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -46,6 +47,16 @@ public class ScopedDataRepository {
|
|||||||
AND o.description = ANY($5::text[])
|
AND o.description = ANY($5::text[])
|
||||||
ORDER BY o.description, ov.val_description
|
ORDER BY o.description, ov.val_description
|
||||||
""";
|
""";
|
||||||
|
private static final String REPORT_DEFINITION = """
|
||||||
|
SELECT r.report_key, r.report_title, r.hide_leading_zero_columns,
|
||||||
|
c.column_key, c.column_label, c.display_order, c.alignment,
|
||||||
|
c.column_width, c.is_visible, c.is_totalled
|
||||||
|
FROM platform.report_definition r
|
||||||
|
JOIN platform.report_column_definition c ON c.report_key = r.report_key
|
||||||
|
WHERE r.report_key = $1
|
||||||
|
AND r.enabled = true
|
||||||
|
ORDER BY c.display_order
|
||||||
|
""";
|
||||||
|
|
||||||
private final ReactiveDatabaseClient database;
|
private final ReactiveDatabaseClient database;
|
||||||
|
|
||||||
@@ -66,6 +77,22 @@ public class ScopedDataRepository {
|
|||||||
descriptions.toArray(String[]::new)));
|
descriptions.toArray(String[]::new)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Flux<ScopedDataItem> reportDefinition(String reportKey) {
|
||||||
|
return database.preparedQuery(REPORT_DEFINITION, Tuple.of(reportKey))
|
||||||
|
.flatMapMany(result -> Flux.fromIterable(result))
|
||||||
|
.map(row -> new ScopedDataItem(null, null, null, Map.of(
|
||||||
|
"reportKey", row.getString("report_key"),
|
||||||
|
"reportTitle", row.getString("report_title"),
|
||||||
|
"hideLeadingZeroColumns", row.getBoolean("hide_leading_zero_columns"),
|
||||||
|
"columnKey", row.getString("column_key"),
|
||||||
|
"columnLabel", row.getString("column_label"),
|
||||||
|
"displayOrder", row.getInteger("display_order"),
|
||||||
|
"alignment", row.getString("alignment"),
|
||||||
|
"columnWidth", row.getInteger("column_width"),
|
||||||
|
"visible", row.getBoolean("is_visible"),
|
||||||
|
"totalled", row.getBoolean("is_totalled"))));
|
||||||
|
}
|
||||||
|
|
||||||
private Flux<ScopedDataItem> rows(String sql, Tuple parameters) {
|
private Flux<ScopedDataItem> rows(String sql, Tuple parameters) {
|
||||||
return database.preparedQuery(sql, parameters)
|
return database.preparedQuery(sql, parameters)
|
||||||
.flatMapMany(result -> Flux.fromIterable(result))
|
.flatMapMany(result -> Flux.fromIterable(result))
|
||||||
@@ -74,6 +101,6 @@ public class ScopedDataRepository {
|
|||||||
|
|
||||||
private ScopedDataItem item(Row row) {
|
private ScopedDataItem item(Row row) {
|
||||||
return new ScopedDataItem(
|
return new ScopedDataItem(
|
||||||
row.getValue("value"), row.getString("label"), row.getString("group_name"));
|
row.getValue("value"), row.getString("label"), row.getString("group_name"), Map.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import reactor.core.publisher.Flux;
|
|||||||
public class ScopedDataService {
|
public class ScopedDataService {
|
||||||
public static final String COMPANY_OPTIONS = "company-options";
|
public static final String COMPANY_OPTIONS = "company-options";
|
||||||
public static final String PORTFOLIO_OPTIONS = "portfolio-options";
|
public static final String PORTFOLIO_OPTIONS = "portfolio-options";
|
||||||
|
public static final String REPORT_DEFINITION = "report-definition";
|
||||||
|
|
||||||
private final ScopedDataRepository repository;
|
private final ScopedDataRepository repository;
|
||||||
|
|
||||||
@@ -23,9 +24,13 @@ public class ScopedDataService {
|
|||||||
|
|
||||||
public Flux<ScopedDataItem> fetch(UUID tenantId, ScopedDataRequest request) {
|
public Flux<ScopedDataItem> fetch(UUID tenantId, ScopedDataRequest request) {
|
||||||
Map<String, Object> data = request.data();
|
Map<String, Object> data = request.data();
|
||||||
|
String scope = request.scope().trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (REPORT_DEFINITION.equals(scope)) {
|
||||||
|
return repository.reportDefinition(textValue(data, "reportKey"));
|
||||||
|
}
|
||||||
short companyId = shortValue(data, "companyId");
|
short companyId = shortValue(data, "companyId");
|
||||||
List<String> descriptions = descriptions(data);
|
List<String> descriptions = descriptions(data);
|
||||||
return switch (request.scope().trim().toLowerCase(Locale.ROOT)) {
|
return switch (scope) {
|
||||||
case COMPANY_OPTIONS -> repository.companyOptions(tenantId, companyId, descriptions);
|
case COMPANY_OPTIONS -> repository.companyOptions(tenantId, companyId, descriptions);
|
||||||
case PORTFOLIO_OPTIONS -> repository.portfolioOptions(
|
case PORTFOLIO_OPTIONS -> repository.portfolioOptions(
|
||||||
tenantId, companyId, shortValue(data, "branchId"),
|
tenantId, companyId, shortValue(data, "branchId"),
|
||||||
@@ -34,6 +39,14 @@ public class ScopedDataService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String textValue(Map<String, Object> data, String name) {
|
||||||
|
String value = String.valueOf(data.getOrDefault(name, "")).trim();
|
||||||
|
if (value.isEmpty() || value.length() > 100 || !value.matches("[a-z0-9-]+")) {
|
||||||
|
throw new ScopedDataException(name + " is invalid");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private UUID uuidValue(Map<String, Object> data, String name) {
|
private UUID uuidValue(Map<String, Object> data, String name) {
|
||||||
try {
|
try {
|
||||||
return UUID.fromString(String.valueOf(data.get(name)));
|
return UUID.fromString(String.valueOf(data.get(name)));
|
||||||
|
|||||||
@@ -34,11 +34,5 @@ SELECT 'hourly-mis', 'hour'||hour, hour::text, hour+1, 'center', 55, true, true
|
|||||||
FROM generate_series(0,23) hour
|
FROM generate_series(0,23) hour
|
||||||
ON CONFLICT (report_key,column_key) DO NOTHING;
|
ON CONFLICT (report_key,column_key) DO NOTHING;
|
||||||
|
|
||||||
UPDATE platform.application_query
|
DELETE FROM platform.application_query
|
||||||
SET query_text='select!C0L!SELECT r.report_title,r.hide_leading_zero_columns,c.column_key,c.column_label,c.display_order,c.alignment,c.column_width,c.is_visible,c.is_totalled FROM platform.report_definition r JOIN platform.report_column_definition c ON c.report_key=r.report_key WHERE r.report_key=? AND r.enabled=true ORDER BY c.display_order',
|
|
||||||
enabled=true, updated_at=current_timestamp
|
|
||||||
WHERE query_key = 'report-definition';
|
WHERE query_key = 'report-definition';
|
||||||
|
|
||||||
INSERT INTO platform.application_query(query_key, query_text, enabled)
|
|
||||||
SELECT 'report-definition', 'select!C0L!SELECT r.report_title,r.hide_leading_zero_columns,c.column_key,c.column_label,c.display_order,c.alignment,c.column_width,c.is_visible,c.is_totalled FROM platform.report_definition r JOIN platform.report_column_definition c ON c.report_key=r.report_key WHERE r.report_key=? AND r.enabled=true ORDER BY c.display_order', true
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM platform.application_query WHERE query_key='report-definition');
|
|
||||||
@@ -41,11 +41,13 @@
|
|||||||
<link href="/matrix/css/tool-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/tool-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/punch-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/punch-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/add-cases-v4.css?v=6" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/add-cases-v4.css?v=6" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/matrix/css/date-time-input-v1.css?v=1" rel="stylesheet" type="text/css" />
|
||||||
<script src="/matrix/js/bootstrap-5.3.8.bundle.min.js" defer></script>
|
<script src="/matrix/js/bootstrap-5.3.8.bundle.min.js" defer></script>
|
||||||
<script src="/matrix/js/matrix-shell-v2.js" defer></script>
|
<script src="/matrix/js/matrix-shell-v2.js" defer></script>
|
||||||
<link href="/matrix/css/matrix-frame-dialog.css?v=6" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-frame-dialog.css?v=6" rel="stylesheet" type="text/css" />
|
||||||
<script src="/matrix/js/matrix-frame-dialog.js?v=2" defer></script>
|
<script src="/matrix/js/matrix-frame-dialog.js?v=2" defer></script>
|
||||||
<script src="/matrix/js/matrix-accessibility-v1.js?v=1" defer></script>
|
<script src="/matrix/js/matrix-accessibility-v1.js?v=1" defer></script>
|
||||||
|
<script src="/matrix/js/lib/date-time-input-v1.js?v=1" defer></script>
|
||||||
<link href="/matrix/css/matrix-theme-v2.css?v=4" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-theme-v2.css?v=4" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,14 @@
|
|||||||
<link href="/matrix/css/matrix-v2.css?v=6" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-v2.css?v=6" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/matrix-shell-v2.css?v=7" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-shell-v2.css?v=7" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/tool-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/tool-workspace-v3.css?v=2" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/mis-filter-v4.css?v=1" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/mis-filter-v4.css?v=2" rel="stylesheet" type="text/css" />
|
||||||
<link href="/matrix/css/date-time-input-v1.css?v=1" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/date-time-input-v1.css?v=2" rel="stylesheet" type="text/css" />
|
||||||
<script src="/matrix/js/bootstrap-5.3.8.bundle.min.js" defer></script>
|
<script src="/matrix/js/bootstrap-5.3.8.bundle.min.js" defer></script>
|
||||||
<script src="/matrix/js/matrix-shell-v2.js" defer></script>
|
<script src="/matrix/js/matrix-shell-v2.js" defer></script>
|
||||||
<link href="/matrix/css/matrix-frame-dialog.css?v=6" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-frame-dialog.css?v=6" rel="stylesheet" type="text/css" />
|
||||||
<script src="/matrix/js/matrix-frame-dialog.js?v=2" defer></script>
|
<script src="/matrix/js/matrix-frame-dialog.js?v=2" defer></script>
|
||||||
<script src="/matrix/js/matrix-accessibility-v1.js?v=1" defer></script>
|
<script src="/matrix/js/matrix-accessibility-v1.js?v=1" defer></script>
|
||||||
<script src="/matrix/js/lib/date-time-input-v1.js?v=1" defer></script>
|
<script src="/matrix/js/lib/date-time-input-v1.js?v=2" defer></script>
|
||||||
<link href="/matrix/css/matrix-theme-v2.css?v=4" rel="stylesheet" type="text/css" />
|
<link href="/matrix/css/matrix-theme-v2.css?v=4" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.matrix-date-control {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-date-control > .matrix-date-input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding-right: 38px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-date-control__button {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
display: grid;
|
||||||
|
width: 34px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid #c4d0da;
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
background: #edf3f7;
|
||||||
|
color: #285f8f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-date-control__button:hover,
|
||||||
|
.matrix-date-control__button:focus-visible {
|
||||||
|
background: #dfeaf3;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-date-control__picker {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-mis-filter .FormPanel .matrix-date-field {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
@@ -64,25 +64,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel {
|
.matrix-mis-filter .FormPanel {
|
||||||
padding: 20px 18px;
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding: 16px 18px;
|
||||||
border: 1px solid #c9d4de;
|
border: 1px solid #c9d4de;
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel table {
|
.matrix-mis-filter .FormPanel table {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.matrix-mis-filter .FormPanel table tbody,
|
||||||
|
.matrix-mis-filter .FormPanel table tr,
|
||||||
|
.matrix-mis-filter .FormPanel table tr td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel table tr td {
|
.matrix-mis-filter .FormPanel table tr td {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: end;
|
|
||||||
gap: 14px 18px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.matrix-mis-filter .FormPanel table tr:first-child td {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-mis-filter .FormPanel table tr:last-child td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel .widget {
|
.matrix-mis-filter .FormPanel .widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 230px;
|
min-width: 230px;
|
||||||
@@ -124,12 +142,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel input.button {
|
.matrix-mis-filter .FormPanel input.button {
|
||||||
|
width: auto !important;
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 6px 14px 6px 30px !important;
|
padding: 6px 14px 6px 30px !important;
|
||||||
border-color: #7f8b96;
|
border-color: #7f8b96;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #e5e8eb;
|
background-color: #e5e8eb;
|
||||||
|
float: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-mis-filter .FormPanel br {
|
.matrix-mis-filter .FormPanel br {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const pad = (value) => String(value).padStart(2, "0");
|
||||||
|
|
||||||
|
const parseDisplayValue = (value, mode) => {
|
||||||
|
const pattern = mode === "datetime"
|
||||||
|
? /^(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):(\d{2}):(\d{2})$/
|
||||||
|
: /^(\d{2})\/(\d{2})\/(\d{4})$/;
|
||||||
|
const match = value.trim().match(pattern);
|
||||||
|
if (!match) return "";
|
||||||
|
|
||||||
|
const [, day, month, year, hour = "00", minute = "00", second = "00"] = match;
|
||||||
|
const date = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
||||||
|
if (date.getFullYear() !== Number(year) || date.getMonth() !== Number(month) - 1
|
||||||
|
|| date.getDate() !== Number(day) || date.getHours() !== Number(hour)
|
||||||
|
|| date.getMinutes() !== Number(minute) || date.getSeconds() !== Number(second)) return "";
|
||||||
|
|
||||||
|
const isoDate = `${year}-${month}-${day}`;
|
||||||
|
return mode === "datetime" ? `${isoDate}T${hour}:${minute}:${second}` : isoDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPickerValue = (value, mode) => {
|
||||||
|
if (!value) return "";
|
||||||
|
const [datePart, timePart = "00:00:00"] = value.split("T");
|
||||||
|
const [year, month, day] = datePart.split("-");
|
||||||
|
if (!year || !month || !day) return "";
|
||||||
|
if (mode === "date") return `${day}/${month}/${year}`;
|
||||||
|
const [hour = "00", minute = "00", second = "00"] = timePart.split(":");
|
||||||
|
return `${day}/${month}/${year} ${pad(hour)}:${pad(minute)}:${pad(second)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialise = (input) => {
|
||||||
|
if (input.dataset.datePickerReady === "true") return;
|
||||||
|
input.dataset.datePickerReady = "true";
|
||||||
|
|
||||||
|
const mode = input.dataset.dateMode === "date" ? "date" : "datetime";
|
||||||
|
const wrapper = document.createElement("span");
|
||||||
|
wrapper.className = "matrix-date-control";
|
||||||
|
input.parentNode.insertBefore(wrapper, input);
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
|
||||||
|
const picker = document.createElement("input");
|
||||||
|
picker.type = mode === "datetime" ? "datetime-local" : "date";
|
||||||
|
picker.className = "matrix-date-control__picker";
|
||||||
|
picker.tabIndex = -1;
|
||||||
|
picker.setAttribute("aria-hidden", "true");
|
||||||
|
if (mode === "datetime") picker.step = "1";
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "matrix-date-control__button";
|
||||||
|
button.setAttribute("aria-label", mode === "datetime" ? "Choose date and time" : "Choose date");
|
||||||
|
button.innerHTML = "📅";
|
||||||
|
|
||||||
|
wrapper.append(picker, button);
|
||||||
|
input.closest(".widget")?.classList.add("matrix-date-field");
|
||||||
|
|
||||||
|
const syncPicker = () => {
|
||||||
|
const parsed = parseDisplayValue(input.value, mode);
|
||||||
|
if (parsed) picker.value = parsed;
|
||||||
|
};
|
||||||
|
syncPicker();
|
||||||
|
|
||||||
|
picker.addEventListener("change", () => {
|
||||||
|
input.value = formatPickerValue(picker.value, mode);
|
||||||
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
input.addEventListener("change", syncPicker);
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
syncPicker();
|
||||||
|
if (typeof picker.showPicker === "function") picker.showPicker();
|
||||||
|
else picker.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialiseAll = (root = document) => root.querySelectorAll(".matrix-date-input").forEach(initialise);
|
||||||
|
document.addEventListener("DOMContentLoaded", () => initialiseAll());
|
||||||
|
window.CygnusDateTimeInputs = { initialise: initialiseAll };
|
||||||
|
})();
|
||||||
@@ -8,8 +8,10 @@ import jakarta.servlet.http.HttpSession;
|
|||||||
|
|
||||||
import matrix.nimble.mis.model.Filters;
|
import matrix.nimble.mis.model.Filters;
|
||||||
import matrix.nimble.mis.model.MISHandler;
|
import matrix.nimble.mis.model.MISHandler;
|
||||||
|
import matrix.nimble.cloud.identity.CloudSessionMapper;
|
||||||
import matrix.nimble.model.Session;
|
import matrix.nimble.model.Session;
|
||||||
import matrix.nimble.utilities.GlobalClass;
|
import matrix.nimble.utilities.GlobalClass;
|
||||||
|
import matrix.services.commons.CommonService;
|
||||||
import matrix.services.mis.HourlyMisService;
|
import matrix.services.mis.HourlyMisService;
|
||||||
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -27,9 +29,31 @@ import org.springframework.http.ResponseEntity;
|
|||||||
@SessionAttributes({"Sessvals"})
|
@SessionAttributes({"Sessvals"})
|
||||||
public class MIS {
|
public class MIS {
|
||||||
private final HourlyMisService hourlyMisService;
|
private final HourlyMisService hourlyMisService;
|
||||||
|
private final CommonService commonService;
|
||||||
|
private final CloudSessionMapper cloudSessionMapper;
|
||||||
|
|
||||||
public MIS(HourlyMisService hourlyMisService) {
|
public MIS(
|
||||||
|
HourlyMisService hourlyMisService,
|
||||||
|
CommonService commonService,
|
||||||
|
CloudSessionMapper cloudSessionMapper) {
|
||||||
this.hourlyMisService = hourlyMisService;
|
this.hourlyMisService = hourlyMisService;
|
||||||
|
this.commonService = commonService;
|
||||||
|
this.cloudSessionMapper = cloudSessionMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("Sessvals")
|
||||||
|
public Session legacySession(HttpSession httpSession) {
|
||||||
|
Session legacySession = commonService.getSession(httpSession);
|
||||||
|
if (legacySession != null) {
|
||||||
|
return legacySession;
|
||||||
|
}
|
||||||
|
var userSession = commonService.getUserSession(httpSession);
|
||||||
|
if (userSession == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
legacySession = cloudSessionMapper.toLegacy(userSession);
|
||||||
|
commonService.storeSession(httpSession, legacySession);
|
||||||
|
return legacySession;
|
||||||
}
|
}
|
||||||
@RequestMapping(value="datetimerange",method=RequestMethod.POST )
|
@RequestMapping(value="datetimerange",method=RequestMethod.POST )
|
||||||
public String DateRangeFilter(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,HttpSession session)
|
public String DateRangeFilter(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,HttpSession session)
|
||||||
|
|||||||
@@ -12,18 +12,33 @@ public final class CloudQuerySource implements QuerySource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String fetch(int queryId) {
|
public String fetch(int queryId) {
|
||||||
|
try {
|
||||||
return client.fetchQuery(queryId)
|
return client.fetchQuery(queryId)
|
||||||
.map(response -> response.query())
|
.map(response -> response.query())
|
||||||
.blockOptional()
|
.blockOptional()
|
||||||
.filter(query -> !query.isBlank())
|
.filter(query -> !query.isBlank())
|
||||||
.orElseThrow(() -> new QueryProviderException(
|
.orElseThrow(() -> new QueryProviderException(
|
||||||
"Cloud query was empty: " + queryId));
|
"Cloud query was empty: " + queryId));
|
||||||
|
} catch (QueryProviderException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
throw new QueryProviderException(
|
||||||
|
"Cloud query request failed for query " + queryId, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String fetch(String queryKey) {
|
public String fetch(String queryKey) {
|
||||||
|
try {
|
||||||
return client.fetchQuery(queryKey).map(response -> response.query()).blockOptional()
|
return client.fetchQuery(queryKey).map(response -> response.query()).blockOptional()
|
||||||
.filter(query -> !query.isBlank())
|
.filter(query -> !query.isBlank())
|
||||||
.orElseThrow(() -> new QueryProviderException("Cloud query was empty: " + queryKey));
|
.orElseThrow(() -> new QueryProviderException(
|
||||||
|
"Cloud query was empty: " + queryKey));
|
||||||
|
} catch (QueryProviderException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
throw new QueryProviderException(
|
||||||
|
"Cloud query request failed for query " + queryKey, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import lib.models.UserSession;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -151,6 +152,9 @@ public class CommonService {
|
|||||||
}
|
}
|
||||||
return cloudIdentityClient.fetchData(scope, data)
|
return cloudIdentityClient.fetchData(scope, data)
|
||||||
.map(items -> items.stream().map(this::option).toList())
|
.map(items -> items.stream().map(this::option).toList())
|
||||||
|
.onErrorMap(TimeoutException.class, exception -> new IllegalStateException(
|
||||||
|
"Cloud platform-data request timed out for scope: " + scope,
|
||||||
|
exception))
|
||||||
.blockOptional()
|
.blockOptional()
|
||||||
.orElseGet(List::of);
|
.orElseGet(List::of);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
package matrix.services.mis;
|
package matrix.services.mis;
|
||||||
|
|
||||||
import com.cygnus.db.CygnusDbExecutor;
|
import com.cygnus.client.CloudIdentityClient;
|
||||||
import com.cygnus.db.RowView;
|
import com.cygnus.client.model.CloudDataItem;
|
||||||
import lib.models.ReportColumnDefinition;
|
import lib.models.ReportColumnDefinition;
|
||||||
import lib.models.ReportDefinition;
|
import lib.models.ReportDefinition;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ReportDefinitionService {
|
public class ReportDefinitionService {
|
||||||
private static final String DEFINITION_QUERY = "report-definition";
|
private static final String DEFINITION_SCOPE = "report-definition";
|
||||||
private final CygnusDbExecutor dbExecutor;
|
private final CloudIdentityClient cloudIdentityClient;
|
||||||
|
|
||||||
public ReportDefinitionService(CygnusDbExecutor dbExecutor) {
|
public ReportDefinitionService(CloudIdentityClient cloudIdentityClient) {
|
||||||
this.dbExecutor = dbExecutor;
|
this.cloudIdentityClient = cloudIdentityClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReportDefinition hourlyMis(String fallbackTitle) {
|
public ReportDefinition hourlyMis(String fallbackTitle) {
|
||||||
try {
|
try {
|
||||||
List<RowView> rows = dbExecutor.query(DEFINITION_QUERY, new Object[] { "hourly-mis" });
|
List<CloudDataItem> rows = cloudIdentityClient
|
||||||
|
.fetchData(DEFINITION_SCOPE, Map.of("reportKey", "hourly-mis"))
|
||||||
|
.onErrorMap(TimeoutException.class,
|
||||||
|
exception -> new IllegalStateException("Cloud report definition request timed out", exception))
|
||||||
|
.blockOptional().orElseGet(List::of);
|
||||||
if (rows.isEmpty()) return defaults(fallbackTitle);
|
if (rows.isEmpty()) return defaults(fallbackTitle);
|
||||||
RowView first = rows.getFirst();
|
Map<String, Object> first = rows.getFirst().data();
|
||||||
List<ReportColumnDefinition> columns = new ArrayList<>(rows.size());
|
List<ReportColumnDefinition> columns = new ArrayList<>(rows.size());
|
||||||
for (RowView row : rows) {
|
for (CloudDataItem item : rows) {
|
||||||
|
Map<String, Object> row = item.data();
|
||||||
columns.add(new ReportColumnDefinition(
|
columns.add(new ReportColumnDefinition(
|
||||||
text(row, "column_key"), text(row, "column_label"),
|
text(row, "columnKey"), text(row, "columnLabel"),
|
||||||
number(row, "display_order"), text(row, "alignment"),
|
number(row, "displayOrder"), text(row, "alignment"),
|
||||||
number(row, "column_width"), bool(row, "is_visible"),
|
number(row, "columnWidth"), bool(row, "visible"),
|
||||||
bool(row, "is_totalled")));
|
bool(row, "totalled")));
|
||||||
}
|
}
|
||||||
return new ReportDefinition("hourly-mis", text(first, "report_title"),
|
return new ReportDefinition("hourly-mis", text(first, "reportTitle"),
|
||||||
bool(first, "hide_leading_zero_columns"), List.copyOf(columns));
|
bool(first, "hideLeadingZeroColumns"), List.copyOf(columns));
|
||||||
} catch (RuntimeException unavailable) {
|
} catch (RuntimeException unavailable) {
|
||||||
return defaults(fallbackTitle);
|
return defaults(fallbackTitle);
|
||||||
}
|
}
|
||||||
@@ -49,7 +56,7 @@ public class ReportDefinitionService {
|
|||||||
return new ReportDefinition("hourly-mis", title, true, List.copyOf(columns));
|
return new ReportDefinition("hourly-mis", title, true, List.copyOf(columns));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String text(RowView row, String column) { return String.valueOf(row.get(column)); }
|
private String text(Map<String, Object> row, String column) { return String.valueOf(row.get(column)); }
|
||||||
private int number(RowView row, String column) { return ((Number) row.get(column)).intValue(); }
|
private int number(Map<String, Object> row, String column) { return ((Number) row.get(column)).intValue(); }
|
||||||
private boolean bool(RowView row, String column) { return Boolean.TRUE.equals(row.get(column)); }
|
private boolean bool(Map<String, Object> row, String column) { return Boolean.TRUE.equals(row.get(column)); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user