diff --git a/db/routines/vn/procedures/expeditionPallet_build.sql b/db/routines/vn/procedures/expeditionPallet_build.sql
index 2df73bb85..a33439061 100644
--- a/db/routines/vn/procedures/expeditionPallet_build.sql
+++ b/db/routines/vn/procedures/expeditionPallet_build.sql
@@ -5,22 +5,26 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`expeditionPallet_buil
vWorkerFk INT,
OUT vPalletFk INT
)
-BEGIN
-/** Construye un pallet de expediciones.
+proc: BEGIN
+/**
+ * Builds an expedition pallet.
*
- * Primero comprueba si esas expediciones ya pertenecen a otro pallet,
- * en cuyo caso actualiza ese pallet.
+ * First, it checks if these expeditions already belong to another pallet,
+ * in which case it returns an error.
*
- * @param vExpeditions JSON_ARRAY con esta estructura [exp1, exp2, exp3, ...]
- * @param vArcId INT Identificador de arcRead
- * @param vWorkerFk INT Identificador de worker
- * @param out vPalletFk Identificador de expeditionPallet
+ * @param vExpeditions JSON_ARRAY with this structure [exp1, exp2, exp3, ...]
+ * @param vArcId INT Identifier of arcRead
+ * @param vWorkerFk INT Identifier of worker
+ * @param out vPalletFk Identifier of expeditionPallet
*/
+
DECLARE vCounter INT;
DECLARE vExpeditionFk INT;
DECLARE vTruckFk INT;
DECLARE vPrinterFk INT;
DECLARE vExpeditionStateTypeFk INT;
+ DECLARE vFreeExpeditionCount INT;
+ DECLARE vExpeditionWithPallet INT;
CREATE OR REPLACE TEMPORARY TABLE tExpedition (
expeditionFk INT,
@@ -44,48 +48,63 @@ BEGIN
WHERE e.id = vExpeditionFk;
END WHILE;
- SELECT palletFk INTO vPalletFk
- FROM (
- SELECT palletFk, count(*) n
- FROM tExpedition
- WHERE palletFk > 0
- GROUP BY palletFk
- ORDER BY n DESC
- LIMIT 100
- ) sub
- LIMIT 1;
+ SELECT COUNT(expeditionFk) INTO vFreeExpeditionCount
+ FROM tExpedition
+ WHERE palletFk IS NULL;
- IF vPalletFk IS NULL THEN
- SELECT roadmapStopFk INTO vTruckFk
- FROM (
- SELECT rm.roadmapStopFk, count(*) n
- FROM routesMonitor rm
- JOIN tExpedition e ON e.routeFk = rm.routeFk
- GROUP BY roadmapStopFk
- ORDER BY n DESC
- LIMIT 1
- ) sub;
+ SELECT COUNT(expeditionFk) INTO vExpeditionWithPallet
+ FROM tExpedition
+ WHERE palletFk;
- IF vTruckFk IS NULL THEN
- CALL util.throw ('TRUCK_NOT_AVAILABLE');
- END IF;
-
- INSERT INTO expeditionPallet SET truckFk = vTruckFk;
-
- SET vPalletFk = LAST_INSERT_ID();
+ IF vExpeditionWithPallet THEN
+ UPDATE arcRead
+ SET error = (
+ SELECT GROUP_CONCAT(expeditionFk SEPARATOR ', ')
+ FROM tExpedition
+ WHERE palletFk
+ )
+ WHERE id = vArcId;
+ LEAVE proc;
END IF;
+ IF NOT vFreeExpeditionCount THEN
+ CALL util.throw ('NO_FREE_EXPEDITIONS');
+ END IF;
+
+ SELECT roadmapStopFk INTO vTruckFk
+ FROM (
+ SELECT rm.roadmapStopFk, count(*) n
+ FROM routesMonitor rm
+ JOIN tExpedition e ON e.routeFk = rm.routeFk
+ WHERE e.palletFk IS NULL
+ GROUP BY roadmapStopFk
+ ORDER BY n DESC
+ LIMIT 1
+ ) sub;
+
+ IF vTruckFk IS NULL THEN
+ CALL util.throw ('TRUCK_NOT_AVAILABLE');
+ END IF;
+
+ INSERT INTO expeditionPallet SET truckFk = vTruckFk;
+
+ SET vPalletFk = LAST_INSERT_ID();
+
INSERT INTO expeditionScan(expeditionFk, palletFk, workerFk)
SELECT expeditionFk, vPalletFk, vWorkerFk
FROM tExpedition
- ON DUPLICATE KEY UPDATE palletFk = vPalletFk, workerFk = vWorkerFk;
+ WHERE palletFk IS NULL;
SELECT id INTO vExpeditionStateTypeFk
FROM expeditionStateType
WHERE code = 'PALLETIZED';
-
+
INSERT INTO expeditionState(expeditionFk, typeFk)
- SELECT expeditionFk, vExpeditionStateTypeFk FROM tExpedition;
+ SELECT expeditionFk, vExpeditionStateTypeFk
+ FROM tExpedition
+ WHERE palletFk IS NULL;
+
+ UPDATE arcRead SET error = NULL WHERE id = vArcId;
SELECT printerFk INTO vPrinterFk FROM arcRead WHERE id = vArcId;
diff --git a/db/routines/vn/procedures/item_getSimilar.sql b/db/routines/vn/procedures/item_getSimilar.sql
index 243aacc2f..537f53848 100644
--- a/db/routines/vn/procedures/item_getSimilar.sql
+++ b/db/routines/vn/procedures/item_getSimilar.sql
@@ -8,17 +8,18 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`item_getSimilar`(
)
BEGIN
/**
-* Propone articulos ordenados, con la cantidad
-* de veces usado y segun sus caracteristicas.
-*
-* @param vSelf Id de artículo
-* @param vWarehouseFk Id de almacen
-* @param vDated Fecha
-* @param vShowType Mostrar tipos
-* @param vDaysInForward Días de alcance para las ventas
-*/
+ * Propone articulos ordenados, con la cantidad
+ * de veces usado y segun sus caracteristicas.
+ *
+ * @param vSelf Id de artículo
+ * @param vWarehouseFk Id de almacen
+ * @param vDated Fecha
+ * @param vShowType Mostrar tipos
+ * @param vDaysInForward Días de alcance para las ventas (https://redmine.verdnatura.es/issues/7956#note-4)
+ */
DECLARE vAvailableCalcFk INT;
DECLARE vVisibleCalcFk INT;
+ DECLARE vTypeFk INT;
DECLARE vPriority INT DEFAULT 1;
CALL cache.available_refresh(vAvailableCalcFk, FALSE, vWarehouseFk, vDated);
@@ -42,19 +43,9 @@ BEGIN
AND it.priority = vPriority
LEFT JOIN vn.tag t ON t.id = it.tagFk
WHERE i.id = vSelf
- ),
- sold AS (
- SELECT SUM(s.quantity) quantity, s.itemFk
- FROM vn.sale s
- JOIN vn.ticket t ON t.id = s.ticketFk
- LEFT JOIN vn.itemShelvingSale iss ON iss.saleFk = s.id
- WHERE t.shipped >= CURDATE() + INTERVAL vDaysInForward DAY
- AND iss.saleFk IS NULL
- AND t.warehouseFk = vWarehouseFk
- GROUP BY s.itemFk
)
SELECT i.id itemFk,
- LEAST(CAST(sd.quantity AS INT), v.visible) advanceable,
+ NULL advanceable, -- https://redmine.verdnatura.es/issues/7956#note-4
i.longName,
i.subName,
i.tag5,
@@ -79,7 +70,6 @@ BEGIN
v.visible located,
b.price2
FROM vn.item i
- LEFT JOIN sold sd ON sd.itemFk = i.id
JOIN cache.available a ON a.item_id = i.id
AND a.calc_id = vAvailableCalcFk
LEFT JOIN cache.visible v ON v.item_id = i.id
@@ -93,21 +83,20 @@ BEGIN
LEFT JOIN vn.tag t ON t.id = it.tagFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
JOIN itemTags its
- WHERE (a.available > 0 OR sd.quantity < v.visible)
+ WHERE a.available > 0
AND (i.typeFk = its.typeFk OR NOT vShowType)
AND i.id <> vSelf
- ORDER BY (a.available > 0) DESC,
- `counter` DESC,
- (t.name = its.name) DESC,
- (it.value = its.value) DESC,
- (i.tag5 = its.tag5) DESC,
- match5 DESC,
- (i.tag6 = its.tag6) DESC,
- match6 DESC,
- (i.tag7 = its.tag7) DESC,
- match7 DESC,
- (i.tag8 = its.tag8) DESC,
- match8 DESC
+ ORDER BY `counter` DESC,
+ (t.name = its.name) DESC,
+ (it.value = its.value) DESC,
+ (i.tag5 = its.tag5) DESC,
+ match5 DESC,
+ (i.tag6 = its.tag6) DESC,
+ match6 DESC,
+ (i.tag7 = its.tag7) DESC,
+ match7 DESC,
+ (i.tag8 = its.tag8) DESC,
+ match8 DESC
LIMIT 100;
END$$
DELIMITER ;
diff --git a/front/core/services/app.js b/front/core/services/app.js
index b8fcc43e1..fa129c3fc 100644
--- a/front/core/services/app.js
+++ b/front/core/services/app.js
@@ -66,10 +66,16 @@ export default class App {
]}
};
-
- if (this.logger.$params.q)
- newRoute = newRoute.concat(`?table=${this.logger.$params.q}`);
+ if (this.logger.$params.q) {
+ let tableValue = this.logger.$params.q;
+ const q = JSON.parse(tableValue);
+ if (typeof q === 'number')
+ tableValue = JSON.stringify({id: tableValue});
+ newRoute = newRoute.concat(`?table=${tableValue}`);
+ }
+ if (this.logger.$params.id && newRoute.indexOf(this.logger.$params.id) < 0)
+ newRoute = newRoute.concat(`${this.logger.$params.id}`);
return this.logger.$http.get('Urls/findOne', {filter})
.then(res => {
diff --git a/modules/client/front/defaulter/index.html b/modules/client/front/defaulter/index.html
index 33bb751f1..440f34d3d 100644
--- a/modules/client/front/defaulter/index.html
+++ b/modules/client/front/defaulter/index.html
@@ -54,7 +54,7 @@
Client
|
-
+ |
Es trabajador
|
diff --git a/modules/client/front/defaulter/index.js b/modules/client/front/defaulter/index.js
index bc8686c10..2ec53d380 100644
--- a/modules/client/front/defaulter/index.js
+++ b/modules/client/front/defaulter/index.js
@@ -57,6 +57,11 @@ export default class Controller extends Section {
field: 'observation',
searchable: false
},
+ {
+ field: 'isWorker',
+ checkbox: true,
+
+ },
{
field: 'created',
datepicker: true
@@ -73,9 +78,6 @@ export default class Controller extends Section {
set defaulters(value) {
if (!value || !value.length) return;
- for (let defaulter of value)
- defaulter.isWorker = defaulter.businessTypeFk === 'worker';
-
this._defaulters = value;
}
@@ -164,6 +166,8 @@ export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
+ case 'isWorker':
+ return {isWorker: value};
case 'creditInsurance':
case 'amount':
case 'clientFk':
diff --git a/modules/entry/front/descriptor-popover/index.html b/modules/entry/front/descriptor-popover/index.html
new file mode 100644
index 000000000..23f641632
--- /dev/null
+++ b/modules/entry/front/descriptor-popover/index.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/modules/entry/front/descriptor-popover/index.js b/modules/entry/front/descriptor-popover/index.js
new file mode 100644
index 000000000..d79aed03e
--- /dev/null
+++ b/modules/entry/front/descriptor-popover/index.js
@@ -0,0 +1,9 @@
+import ngModule from '../module';
+import DescriptorPopover from 'salix/components/descriptor-popover';
+
+class Controller extends DescriptorPopover {}
+
+ngModule.vnComponent('vnEntryDescriptorPopover', {
+ slotTemplate: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/entry/front/descriptor/index.html b/modules/entry/front/descriptor/index.html
new file mode 100644
index 000000000..3354d4155
--- /dev/null
+++ b/modules/entry/front/descriptor/index.html
@@ -0,0 +1,65 @@
+
+
+
+ Show entry report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/entry/front/descriptor/index.js b/modules/entry/front/descriptor/index.js
new file mode 100644
index 000000000..3452a6d34
--- /dev/null
+++ b/modules/entry/front/descriptor/index.js
@@ -0,0 +1,99 @@
+import ngModule from '../module';
+import Descriptor from 'salix/components/descriptor';
+
+class Controller extends Descriptor {
+ get entry() {
+ return this.entity;
+ }
+
+ set entry(value) {
+ this.entity = value;
+ }
+
+ get travelFilter() {
+ let travelFilter;
+ const entryTravel = this.entry && this.entry.travel;
+
+ if (entryTravel && entryTravel.agencyModeFk) {
+ travelFilter = this.entry && JSON.stringify({
+ agencyModeFk: entryTravel.agencyModeFk
+ });
+ }
+ return travelFilter;
+ }
+
+ get entryFilter() {
+ let entryTravel = this.entry && this.entry.travel;
+
+ if (!entryTravel || !entryTravel.landed) return null;
+
+ const date = new Date(entryTravel.landed);
+ date.setHours(0, 0, 0, 0);
+
+ const from = new Date(date.getTime());
+ from.setDate(from.getDate() - 10);
+
+ const to = new Date(date.getTime());
+ to.setDate(to.getDate() + 10);
+
+ return JSON.stringify({
+ supplierFk: this.entry.supplierFk,
+ from,
+ to
+ });
+ }
+
+ loadData() {
+ const filter = {
+ include: [
+ {
+ relation: 'travel',
+ scope: {
+ fields: ['id', 'landed', 'agencyModeFk', 'warehouseOutFk'],
+ include: [
+ {
+ relation: 'agency',
+ scope: {
+ fields: ['name']
+ }
+ },
+ {
+ relation: 'warehouseOut',
+ scope: {
+ fields: ['name']
+ }
+ },
+ {
+ relation: 'warehouseIn',
+ scope: {
+ fields: ['name']
+ }
+ }
+ ]
+ }
+ },
+ {
+ relation: 'supplier',
+ scope: {
+ fields: ['id', 'nickname']
+ }
+ }
+ ]
+ };
+
+ return this.getData(`Entries/${this.id}`, {filter})
+ .then(res => this.entity = res.data);
+ }
+
+ showEntryReport() {
+ this.vnReport.show(`Entries/${this.id}/entry-order-pdf`);
+ }
+}
+
+ngModule.vnComponent('vnEntryDescriptor', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ entry: '<'
+ }
+});
diff --git a/modules/entry/front/index.js b/modules/entry/front/index.js
index a7209a0bd..0f2208862 100644
--- a/modules/entry/front/index.js
+++ b/modules/entry/front/index.js
@@ -1,3 +1,6 @@
export * from './module';
import './main';
+import './descriptor';
+import './descriptor-popover';
+import './summary';
diff --git a/modules/entry/front/routes.json b/modules/entry/front/routes.json
index 53c599cf1..a2e70e37d 100644
--- a/modules/entry/front/routes.json
+++ b/modules/entry/front/routes.json
@@ -8,6 +8,12 @@
"main": [
{"state": "entry.index", "icon": "icon-entry"},
{"state": "entry.latestBuys", "icon": "contact_support"}
+ ],
+ "card": [
+ {"state": "entry.card.basicData", "icon": "settings"},
+ {"state": "entry.card.buy.index", "icon": "icon-lines"},
+ {"state": "entry.card.observation", "icon": "insert_drive_file"},
+ {"state": "entry.card.log", "icon": "history"}
]
},
"keybindings": [
@@ -27,6 +33,90 @@
"component": "vn-entry-index",
"description": "Entries",
"acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/latest-buys?q",
+ "state": "entry.latestBuys",
+ "component": "vn-entry-latest-buys",
+ "description": "Latest buys",
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/create?supplierFk&travelFk&companyFk",
+ "state": "entry.create",
+ "component": "vn-entry-create",
+ "description": "New entry",
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/:id",
+ "state": "entry.card",
+ "abstract": true,
+ "component": "vn-entry-card"
+ },
+ {
+ "url": "/summary",
+ "state": "entry.card.summary",
+ "component": "vn-entry-summary",
+ "description": "Summary",
+ "params": {
+ "entry": "$ctrl.entry"
+ },
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/basic-data",
+ "state": "entry.card.basicData",
+ "component": "vn-entry-basic-data",
+ "description": "Basic data",
+ "params": {
+ "entry": "$ctrl.entry"
+ },
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/observation",
+ "state": "entry.card.observation",
+ "component": "vn-entry-observation",
+ "description": "Notes",
+ "params": {
+ "entry": "$ctrl.entry"
+ },
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url" : "/log",
+ "state": "entry.card.log",
+ "component": "vn-entry-log",
+ "description": "Log",
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url": "/buy",
+ "state": "entry.card.buy",
+ "abstract": true,
+ "component": "ui-view",
+ "acl": ["buyer"]
+ },
+ {
+ "url" : "/index",
+ "state": "entry.card.buy.index",
+ "component": "vn-entry-buy-index",
+ "description": "Buys",
+ "params": {
+ "entry": "$ctrl.entry"
+ },
+ "acl": ["buyer", "administrative"]
+ },
+ {
+ "url" : "/import",
+ "state": "entry.card.buy.import",
+ "component": "vn-entry-buy-import",
+ "description": "Import buys",
+ "params": {
+ "entry": "$ctrl.entry"
+ },
+ "acl": ["buyer"]
}
]
}
diff --git a/modules/entry/front/summary/index.html b/modules/entry/front/summary/index.html
new file mode 100644
index 000000000..22ea87bdf
--- /dev/null
+++ b/modules/entry/front/summary/index.html
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+ #{{$ctrl.entryData.id}} - {{$ctrl.entryData.supplier.nickname}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$ctrl.entryData.travel.ref}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Buys
+
+
+
+ Quantity |
+ Stickers |
+ Package |
+ Weight |
+ Packing |
+ Grouping |
+ Buying value |
+ Import |
+ PVP |
+
+
+
+
+ {{::line.quantity}} |
+ {{::line.stickers | dashIfEmpty}} |
+ {{::line.packagingFk | dashIfEmpty}} |
+ {{::line.weight}} |
+
+
+ {{::line.packing | dashIfEmpty}}
+
+ |
+
+
+ {{::line.grouping | dashIfEmpty}}
+
+
+ | {{::line.buyingValue | currency: 'EUR':2}} |
+ {{::line.quantity * line.buyingValue | currency: 'EUR':2}} |
+ {{::line.price2 | currency: 'EUR':2 | dashIfEmpty}} / {{::line.price3 | currency: 'EUR':2 | dashIfEmpty}} |
+
+
+
+
+ {{::line.item.itemType.code}}
+
+ |
+
+
+ {{::line.item.id}}
+
+ |
+
+
+ {{::line.item.size}}
+
+ |
+
+
+ {{::line.item.minPrice | currency: 'EUR':2}}
+
+ |
+
+
+ {{::line.item.name}}
+
+ {{::line.item.subName}}
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/entry/front/summary/index.js b/modules/entry/front/summary/index.js
new file mode 100644
index 000000000..6e18bc959
--- /dev/null
+++ b/modules/entry/front/summary/index.js
@@ -0,0 +1,33 @@
+import ngModule from '../module';
+import './style.scss';
+import Summary from 'salix/components/summary';
+
+class Controller extends Summary {
+ get entry() {
+ if (!this._entry)
+ return this.$params;
+
+ return this._entry;
+ }
+
+ set entry(value) {
+ this._entry = value;
+
+ if (value && value.id)
+ this.getEntryData();
+ }
+
+ getEntryData() {
+ return this.$http.get(`Entries/${this.entry.id}/getEntry`).then(response => {
+ this.entryData = response.data;
+ });
+ }
+}
+
+ngModule.vnComponent('vnEntrySummary', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ entry: '<'
+ }
+});
diff --git a/modules/entry/front/summary/style.scss b/modules/entry/front/summary/style.scss
new file mode 100644
index 000000000..1d5b22e30
--- /dev/null
+++ b/modules/entry/front/summary/style.scss
@@ -0,0 +1,30 @@
+@import "variables";
+
+
+vn-entry-summary .summary {
+ max-width: $width-lg;
+
+ .dark-row {
+ background-color: lighten($color-marginal, 10%);
+ }
+
+ tbody tr:nth-child(1) {
+ border-top: $border-thin;
+ }
+
+ tbody tr:nth-child(1),
+ tbody tr:nth-child(2) {
+ border-left: $border-thin;
+ border-right: $border-thin
+ }
+
+ tbody tr:nth-child(3) {
+ height: inherit
+ }
+
+ tr {
+ margin-bottom: 10px;
+ }
+}
+
+$color-font-link-medium: lighten($color-font-link, 20%)
\ No newline at end of file
diff --git a/modules/item/front/routes.json b/modules/item/front/routes.json
index 4b7cd1490..05b887a96 100644
--- a/modules/item/front/routes.json
+++ b/modules/item/front/routes.json
@@ -3,7 +3,7 @@
"name": "Items",
"icon": "icon-item",
"validations" : true,
- "dependencies": ["worker", "client", "ticket"],
+ "dependencies": ["worker", "client", "ticket", "entry"],
"menus": {
"main": [
{"state": "item.index", "icon": "icon-item"},
diff --git a/modules/route/back/methods/route/driverRouteEmail.js b/modules/route/back/methods/route/driverRouteEmail.js
index 62147db87..bbac2b0e8 100644
--- a/modules/route/back/methods/route/driverRouteEmail.js
+++ b/modules/route/back/methods/route/driverRouteEmail.js
@@ -39,8 +39,6 @@ module.exports = Self => {
const {reportMail} = agencyMode();
let user;
let account;
- let userEmail;
- ctx.args.recipients = reportMail ? reportMail.split(',').map(email => email.trim()) : [];
if (workerFk) {
user = await models.VnUser.findById(workerFk, {
@@ -50,17 +48,10 @@ module.exports = Self => {
account = await models.Account.findById(workerFk);
}
- if (user?.active && account)
- userEmail = user.emailUser().email;
-
- if (userEmail)
- ctx.args.recipients.push(userEmail);
-
- ctx.args.recipients = [...new Set(ctx.args.recipients)];
-
- if (!ctx.args.recipients.length)
- throw new UserError('An email is necessary');
+ if (user?.active && account) ctx.args.recipient = user.emailUser().email;
+ else ctx.args.recipient = reportMail;
+ if (!ctx.args.recipient) throw new UserError('An email is necessary');
return Self.sendTemplate(ctx, 'driver-route');
};
};
diff --git a/modules/travel/front/routes.json b/modules/travel/front/routes.json
index 6ae61bd02..6891a46cf 100644
--- a/modules/travel/front/routes.json
+++ b/modules/travel/front/routes.json
@@ -3,7 +3,7 @@
"name": "Travels",
"icon": "local_airport",
"validations": true,
- "dependencies": ["worker"],
+ "dependencies": ["worker", "entry"],
"menus": {
"main": [
{"state": "travel.index", "icon": "local_airport"},
|