Fixed add case bug

This commit is contained in:
2026-08-12 18:10:39 +05:30
parent a757799724
commit 03dfdd6218
16 changed files with 302 additions and 56 deletions

2
.vscode/launch.json vendored
View File

@@ -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",

View File

@@ -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());
} }
} }

View File

@@ -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) {
} }

View File

@@ -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) {
} }

View File

@@ -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());
} }
} }

View File

@@ -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)));

View File

@@ -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', WHERE query_key = 'report-definition';
enabled=true, updated_at=current_timestamp
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');

View File

@@ -40,12 +40,14 @@
<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/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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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 = "&#128197;";
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 };
})();

View File

@@ -6,10 +6,12 @@ import java.util.Arrays;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; 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.model.Session; import matrix.nimble.cloud.identity.CloudSessionMapper;
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)

View File

@@ -12,18 +12,33 @@ public final class CloudQuerySource implements QuerySource {
@Override @Override
public String fetch(int queryId) { public String fetch(int queryId) {
return client.fetchQuery(queryId) try {
.map(response -> response.query()) return client.fetchQuery(queryId)
.blockOptional() .map(response -> response.query())
.filter(query -> !query.isBlank()) .blockOptional()
.orElseThrow(() -> new QueryProviderException( .filter(query -> !query.isBlank())
"Cloud query was empty: " + queryId)); .orElseThrow(() -> new QueryProviderException(
"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) {
return client.fetchQuery(queryKey).map(response -> response.query()).blockOptional() try {
.filter(query -> !query.isBlank()) return client.fetchQuery(queryKey).map(response -> response.query()).blockOptional()
.orElseThrow(() -> new QueryProviderException("Cloud query was empty: " + queryKey)); .filter(query -> !query.isBlank())
.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);
}
} }
} }

View File

@@ -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);
} }

View File

@@ -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)); }
} }