diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 37afe17..27ab445 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -66,11 +66,6 @@ const routes = [
component: AdminSettings,
meta: { requiresAuth: true, requiresAdmin: true, title: 'Settings' }
},
-// {
-// path: '/olap-constructor',
-// component: OLAPConstructor,
-// meta: { requiresAuth: true, title: 'OLAP Constructor' }
-// },
{
path: '/olap/queries',
component: OlapQueriesPage,
diff --git a/frontend/src/views/OlapConstructor.vue b/frontend/src/views/OlapConstructor.vue
index 079054c..486527a 100644
--- a/frontend/src/views/OlapConstructor.vue
+++ b/frontend/src/views/OlapConstructor.vue
@@ -135,7 +135,7 @@
-
+
@@ -581,7 +581,7 @@ const daysBack = ref(7)
const tableName = ref('')
const tableNameValid = ref(true)
const tableNameTouched = ref(false)
-const active = ref(true)
+const active = ref(false)
const searchQuery = ref('')
const activeTab = ref<'table' | 'sql'>('table')
const collapsed = ref({ number: false, category: false, filter: false })
@@ -874,6 +874,8 @@ async function loadQuery(id: number) {
// 1. Базовые поля запроса
queryName.value = data.name
+ active.value = data.active ?? true;
+
// 2. Подключение к БД
if (data.dbConnectionId) {
await loadDbConnections()
@@ -974,13 +976,18 @@ async function saveQuery() {
showNotification('Выберите хотя бы один ресторан', 'error')
return
}
+ if (!tableName.value.trim()) {
+ showNotification('Укажите название таблицы SQL', 'error');
+ return;
+ }
const config = buildConfigObject()
const payload = {
name: queryName.value,
dbConnectionId: selectedDbConnection.value.id,
config: config,
- restaurantIds: selectedRestaurants.value.map(r => r.id)
- }
+ restaurantIds: selectedRestaurants.value.map(r => r.id),
+ active: active.value
+ };
const url = queryId.value ? `/api/olap/queries/${queryId.value}` : '/api/olap/queries'
const method = queryId.value ? 'PUT' : 'POST'
try {
@@ -1011,9 +1018,17 @@ const filteredCategoryFields = computed(() => availableFields.value.filter((f):
const filteredFilterFields = computed(() => availableFields.value.filter((f): f is FilterField => f.role === 'filter' && matchesSearch(f)))
const validateTableName = () => {
- tableNameTouched.value = true
- if (!tableName.value) { tableNameValid.value = true; return }
- tableNameValid.value = /^[A-Za-zА-Яа-я]/.test(tableName.value)
+ tableNameTouched.value = true;
+ const value = tableName.value.trim();
+ if (!value) {
+ tableNameValid.value = false;
+ return;
+ }
+ // Регулярное выражение:
+ // ^[A-Za-z] - первая буква (только английская)
+ // [A-Za-z0-9]* - далее любые английские буквы или цифры
+ const regex = /^[A-Za-z][A-Za-z0-9]*$/;
+ tableNameValid.value = regex.test(value);
}
const parseValues = (f: FilterField) => {
@@ -1184,7 +1199,7 @@ const confirmReset = () => {
refreshFieldsAndReset()
dateTo.value = ''
daysBack.value = 7
- active.value = true
+ active.value = false
tableName.value = ''
tableNameValid.value = true
tableNameTouched.value = false
diff --git a/frontend/src/views/OlapQueriesPage.vue b/frontend/src/views/OlapQueriesPage.vue
index e9de66f..8e542e7 100644
--- a/frontend/src/views/OlapQueriesPage.vue
+++ b/frontend/src/views/OlapQueriesPage.vue
@@ -12,6 +12,9 @@
| ID |
Название |
+ Активен |
+ Последнее выполнение |
+ Результат |
Подключение |
Рестораны |
Создан |
@@ -22,6 +25,17 @@
| {{ q.id }} |
{{ q.name }} |
+
+
+ {{ q.active ? 'Да' : 'Нет' }}
+
+ |
+ {{ q.lastRun ? formatDate(q.lastRun) : '—' }} |
+
+ —
+ Успешно
+ Ошибка
+ |
{{ q.dbConnectionName }} |
{{ q.restaurants }} |
{{ formatDate(q.created) }} |
diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java
index 3be79e3..6b36d25 100644
--- a/src/main/java/su/xserver/iikocon/MainVerticle.java
+++ b/src/main/java/su/xserver/iikocon/MainVerticle.java
@@ -552,14 +552,31 @@ public class MainVerticle extends AbstractVerticle {
JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
List restaurantIds = restaurantIdsArray.stream().map(id -> (Integer) id).collect(Collectors.toList());
+ // Получаем active: сначала из тела, иначе из конфига, иначе true
+ Boolean active = body.getBoolean("active");
+ if (active == null && config != null) active = config.getBoolean("active", true);
+ if (active == null) active = true;
+
if (name == null || dbConnectionId == null || config == null) {
rc.response().setStatusCode(400).end("Missing required fields");
return;
}
- // Сначала генерируем SQL
+ String tableName = config.getString("tableName");
+
+ if (tableName.isEmpty()) {
+ rc.response().setStatusCode(400).end("Missing required fields");
+ return;
+ }
+
+ if (!olapQueryService.isValidTableName(tableName)) {
+ rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits");
+ return;
+ }
+
+ Boolean finalActive = active;
olapQueryService.generateSql(config, dbConnectionId)
- .compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql))
+ .compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql, finalActive))
.onSuccess(id -> rc.response().setStatusCode(201).putHeader("Content-Type", "application/json").end(new JsonObject().put("id", id).encode()))
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
@@ -573,13 +590,30 @@ public class MainVerticle extends AbstractVerticle {
JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
List restaurantIds = restaurantIdsArray.stream().map(v -> (Integer) v).collect(Collectors.toList());
+ Boolean active = body.getBoolean("active");
+ if (active == null && config != null) active = config.getBoolean("active", true);
+ if (active == null) active = true;
+
if (name == null || dbConnectionId == null || config == null) {
rc.response().setStatusCode(400).end("Missing required fields");
return;
}
+ String tableName = config.getString("tableName");
+
+ if (tableName.isEmpty()) {
+ rc.response().setStatusCode(400).end("Missing required fields");
+ return;
+ }
+
+ if (!olapQueryService.isValidTableName(tableName)) {
+ rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits");
+ return;
+ }
+
+ Boolean finalActive = active;
olapQueryService.generateSql(config, dbConnectionId)
- .compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql))
+ .compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql, finalActive))
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
diff --git a/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java
index f219d0f..dd7c864 100644
--- a/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java
+++ b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java
@@ -5,6 +5,7 @@ import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.templates.SqlTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +34,9 @@ public class OlapQueryService {
config_json JSON NOT NULL,
full_config_json JSON NOT NULL,
sql_text TEXT,
+ active BOOLEAN NOT NULL DEFAULT true,
+ last_run TIMESTAMP NULL,
+ last_run_success BOOLEAN NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (db_connection_id) REFERENCES external_database(id) ON DELETE RESTRICT
@@ -56,16 +60,17 @@ public class OlapQueryService {
// Создание запроса
public Future createQuery(String name, int dbConnectionId, JsonObject config,
- List restaurantIds, String generatedSql) {
+ List restaurantIds, String generatedSql, boolean active) {
JsonObject fullConfig = generateFullIikoJson(config);
Map params = Map.of(
"name", name,
"db_connection_id", dbConnectionId,
"config_json", config.encode(),
"full_config_json", fullConfig.encode(),
- "sql_text", generatedSql != null ? generatedSql : ""
+ "sql_text", generatedSql != null ? generatedSql : "",
+ "active", active
);
- String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text})";
+ String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text, active) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text}, #{active})";
return SqlTemplate.forUpdate(pool, sql)
.execute(params)
.compose(rows -> getLastInsertId())
@@ -73,7 +78,7 @@ public class OlapQueryService {
}
public Future updateQuery(int id, String name, int dbConnectionId, JsonObject config,
- List restaurantIds, String generatedSql) {
+ List restaurantIds, String generatedSql, boolean active) {
JsonObject fullConfig = generateFullIikoJson(config);
Map params = Map.of(
"id", id,
@@ -81,15 +86,14 @@ public class OlapQueryService {
"db_connection_id", dbConnectionId,
"config_json", config.encode(),
"full_config_json", fullConfig.encode(),
- "sql_text", generatedSql != null ? generatedSql : ""
+ "sql_text", generatedSql != null ? generatedSql : "",
+ "active", active
);
- String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text} WHERE id = #{id}";
+ String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text}, active = #{active} WHERE id = #{id}";
return SqlTemplate.forUpdate(pool, sql)
.execute(params)
- .compose(v -> {
- return pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute()
- .compose(del -> linkRestaurants(id, restaurantIds));
- }).mapEmpty();
+ .compose(v -> pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute()
+ .compose(del -> linkRestaurants(id, restaurantIds))).mapEmpty();
}
public JsonObject generateFullIikoJson(JsonObject clientConfig) {
@@ -139,14 +143,13 @@ public class OlapQueryService {
// Добавляем системные
allFilters.mergeIn(systemFilters);
- JsonObject result = new JsonObject()
+ return new JsonObject()
.put("reportType", reportType)
.put("buildSummary", buildSummary)
.put("groupByRowFields", rowFields)
.put("groupByColFields", columnFields)
.put("aggregateFields", valueFields)
.put("filters", allFilters);
- return result;
}
private JsonObject buildDateFilter(String reportType, String dateToStr, int daysBack) {
@@ -195,36 +198,38 @@ public class OlapQueryService {
// Получить все запросы (без config_json, для списка)
public Future getAllQueries() {
String sql = """
- SELECT q.id, q.name, q.db_connection_id, q.created, q.updated,
- GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants,
- dc.name AS db_connection_name
- FROM olap_queries q
- LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id
- LEFT JOIN restaurants r ON qr.restaurant_id = r.id
- LEFT JOIN external_database dc ON q.db_connection_id = dc.id
- GROUP BY q.id
- ORDER BY q.id DESC
- """;
+ SELECT q.id, q.name, q.db_connection_id, q.active, q.last_run, q.last_run_success, q.created, q.updated,
+ GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants,
+ dc.name AS db_connection_name
+ FROM olap_queries q
+ LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id
+ LEFT JOIN restaurants r ON qr.restaurant_id = r.id
+ LEFT JOIN external_database dc ON q.db_connection_id = dc.id
+ GROUP BY q.id
+ ORDER BY q.id DESC
+ """;
return pool.query(sql).execute()
.map(rows -> {
JsonArray arr = new JsonArray();
- rows.forEach(row -> {
- arr.add(new JsonObject()
- .put("id", row.getInteger("id"))
- .put("name", row.getString("name"))
- .put("dbConnectionId", row.getInteger("db_connection_id"))
- .put("dbConnectionName", row.getString("db_connection_name"))
- .put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "")
- .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
- .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null));
- });
+ rows.forEach(row -> arr.add(new JsonObject()
+ .put("id", row.getInteger("id"))
+ .put("name", row.getString("name"))
+ .put("dbConnectionId", row.getInteger("db_connection_id"))
+ .put("dbConnectionName", row.getString("db_connection_name"))
+ .put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "")
+ .put("active", row.getBoolean("active"))
+ .put("lastRun", row.getLocalDateTime("last_run") != null ? row.getLocalDateTime("last_run").toString() : null)
+ .put("lastRunSuccess", row.getBoolean("last_run_success"))
+ .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
+ .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null)));
return arr;
});
}
+
// Получить один запрос с полной конфигурацией
public Future getQueryById(int id) {
- String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, created, updated FROM olap_queries WHERE id = ?";
+ String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, active, created, updated FROM olap_queries WHERE id = ?";
String restaurantsSql = "SELECT restaurant_id FROM olap_query_restaurants WHERE query_id = ?";
return pool.preparedQuery(querySql).execute(io.vertx.sqlclient.Tuple.of(id))
@@ -237,6 +242,7 @@ public class OlapQueryService {
.put("dbConnectionId", row.getInteger("db_connection_id"))
.put("config", new JsonObject(row.getString("config_json")))
.put("sql", row.getString("sql_text"))
+ .put("active", row.getBoolean("active"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null);
@@ -250,6 +256,12 @@ public class OlapQueryService {
});
}
+ // Метод для обновления статуса выполнения
+ public Future updateRunStatus(int queryId, boolean success) {
+ String sql = "UPDATE olap_queries SET last_run = NOW(), last_run_success = ? WHERE id = ?";
+ return pool.preparedQuery(sql).execute(Tuple.of(success, queryId)).mapEmpty();
+ }
+
// Генерация SQL на основе конфигурации и ID подключения
public Future generateSql(JsonObject config, int dbConnectionId) {
return externalDataBaseService.findById(dbConnectionId)
@@ -264,4 +276,11 @@ public class OlapQueryService {
}
});
}
+
+ public boolean isValidTableName(String tableName) {
+ if (tableName == null) return false;
+ String trimmed = tableName.trim();
+ // Первый символ — английская буква, далее буквы или цифры
+ return trimmed.matches("^[A-Za-z][A-Za-z0-9]*$");
+ }
}