diff --git a/myems-api/excelexporters/metertracking.py b/myems-api/excelexporters/metertracking.py index 4b251f96..c51f1d2b 100644 --- a/myems-api/excelexporters/metertracking.py +++ b/myems-api/excelexporters/metertracking.py @@ -18,7 +18,7 @@ from openpyxl.chart.label import DataLabelList # Step 3: Encode the excelexporters file to Base64 #################################################################################################################### -def export(result, space_name): +def export(result, space_name, reporting_start_datetime_local, reporting_end_datetime_local): #################################################################################################################### # Step 1: Validate the report data #################################################################################################################### @@ -29,7 +29,9 @@ def export(result, space_name): # Step 2: Generate excel file from the report data #################################################################################################################### filename = generate_excel(result, - space_name) + space_name, + reporting_start_datetime_local, + reporting_end_datetime_local) #################################################################################################################### # Step 3: Encode the excel file to Base64 #################################################################################################################### @@ -51,17 +53,26 @@ def export(result, space_name): return base64_message -def generate_excel(report, space_name): +def generate_excel(report, space_name, reporting_start_datetime_local, reporting_end_datetime_local): wb = Workbook() ws = wb.active # Row height - ws.row_dimensions[1].height = 118 - for i in range(2, 5000 + 1): - ws.row_dimensions[i].height = 30 + ws.row_dimensions[1].height = 102 + for i in range(2, 5 + 1): + ws.row_dimensions[i].height = 42 + + for i in range(6, len(report['meters']) + 15): + ws.row_dimensions[i].height = 60 + # Col width - ws.column_dimensions['A'].width = 1 + ws.column_dimensions['A'].width = 1.5 + + ws.column_dimensions['B'].width = 25.0 + + for i in range(ord('C'), ord('L')): + ws.column_dimensions[chr(i)].width = 15.0 # Font name_font = Font(name='Constantia', size=15, bold=True) @@ -81,63 +92,98 @@ def generate_excel(report, space_name): b_c_alignment = Alignment(vertical='bottom', horizontal='center', text_rotation=0, - wrap_text=False, + wrap_text=True, shrink_to_fit=False, indent=0) c_c_alignment = Alignment(vertical='center', horizontal='center', text_rotation=0, - wrap_text=False, + wrap_text=True, shrink_to_fit=False, indent=0) b_r_alignment = Alignment(vertical='bottom', horizontal='right', text_rotation=0, - wrap_text=False, + wrap_text=True, shrink_to_fit=False, indent=0) c_r_alignment = Alignment(vertical='bottom', horizontal='center', text_rotation=0, - wrap_text=False, + wrap_text=True, shrink_to_fit=False, indent=0) - for i in range(ord('B'), ord('G')): - ws.column_dimensions[chr(i)].width = 25.0 # Img - ws.merge_cells("B1:F1") - ws.merge_cells("B2:F2") img = Image("excelexporters/myems.png") + img.width = img.width * 0.85 + img.height = img.height * 0.85 ws.add_image(img, 'B1') # Title - ws['B3'].border = f_border - ws['B3'].font = name_font - ws['B3'].alignment = b_c_alignment - ws['B3'] = '名称' + ws.row_dimensions[3].height = 60 - ws['C3'].border = f_border + ws['B3'].font = name_font + ws['B3'].alignment = b_r_alignment + ws['B3'] = 'Name:' + ws['C3'].border = b_border ws['C3'].alignment = b_c_alignment ws['C3'].font = name_font - ws['C3'] = '空间' + ws['C3'] = space_name - ws['D3'].border = f_border - ws['D3'].font = name_font - ws['D3'].alignment = b_c_alignment - ws['D3'] = '成本中心:' - - ws['E3'].border = f_border - ws['E3'].alignment = b_c_alignment - ws['E3'].font = name_font - ws['E3'] = '能耗分类' - - ws['F3'].border = f_border ws['F3'].font = name_font - ws['F3'].alignment = b_c_alignment - ws['F3'] = ' 描述' + ws['F3'].alignment = b_r_alignment + ws['F3'] = 'Date:' + ws['G3'].border = b_border + ws['G3'].alignment = b_c_alignment + ws['G3'].font = name_font + ws['G3'] = reporting_start_datetime_local + "__" + reporting_end_datetime_local + ws.merge_cells("G3:H3") - current_row_number = 4 + # Title + ws['B6'].border = f_border + ws['B6'].font = name_font + ws['B6'].alignment = c_c_alignment + ws['B6'].fill = table_fill + ws['B6'] = '名称' + + ws['C6'].border = f_border + ws['C6'].alignment = c_c_alignment + ws['C6'].font = name_font + ws['C6'].fill = table_fill + ws['C6'] = '空间' + + ws['D6'].border = f_border + ws['D6'].font = name_font + ws['D6'].alignment = c_c_alignment + ws['D6'].fill = table_fill + ws['D6'] = '成本中心' + + ws['E6'].border = f_border + ws['E6'].alignment = c_c_alignment + ws['E6'].font = name_font + ws['E6'].fill = table_fill + ws['E6'] = '能耗分类' + + ws['F6'].border = f_border + ws['F6'].font = name_font + ws['F6'].alignment = c_c_alignment + ws['F6'].fill = table_fill + ws['F6'] = ' 描述' + + ws['G6'].border = f_border + ws['G6'].font = name_font + ws['G6'].alignment = c_c_alignment + ws['G6'].fill = table_fill + ws['G6'] = '开始值' + + ws['H6'].border = f_border + ws['H6'].font = name_font + ws['H6'].alignment = c_c_alignment + ws['H6'].fill = table_fill + ws['H6'] = ' 结束值' + + current_row_number = 7 for i in range(0, len(report['meters'])): ws['B' + str(current_row_number)].font = title_font @@ -164,6 +210,17 @@ def generate_excel(report, space_name): ws['F' + str(current_row_number)].border = f_border ws['F' + str(current_row_number)].alignment = c_c_alignment ws['F' + str(current_row_number)] = report['meters'][i]['description'] + + ws['G' + str(current_row_number)].font = title_font + ws['G' + str(current_row_number)].border = f_border + ws['G' + str(current_row_number)].alignment = c_c_alignment + ws['G' + str(current_row_number)] = report['meters'][i]['start_value'] + + ws['H' + str(current_row_number)].font = title_font + ws['H' + str(current_row_number)].border = f_border + ws['H' + str(current_row_number)].alignment = c_c_alignment + ws['H' + str(current_row_number)] = report['meters'][i]['end_value'] + current_row_number += 1 filename = str(uuid.uuid4()) + '.xlsx' diff --git a/myems-api/reports/metertracking.py b/myems-api/reports/metertracking.py index 6c374f94..be37a81f 100644 --- a/myems-api/reports/metertracking.py +++ b/myems-api/reports/metertracking.py @@ -4,6 +4,7 @@ import mysql.connector import config from anytree import Node, AnyNode, LevelOrderIter import excelexporters.metertracking +from datetime import datetime, timedelta, timezone class Reporting: @@ -20,12 +21,15 @@ class Reporting: # Step 1: valid parameters # Step 2: build a space tree # Step 3: query all meters in the space tree - # Step 4: construct the report + # Step 4: query start value and end value + # Step 5: construct the report #################################################################################################################### @staticmethod def on_get(req, resp): print(req.params) space_id = req.params.get('spaceid') + reporting_period_start_datetime_local = req.params.get('reportingperiodstartdatetime') + reporting_period_end_datetime_local = req.params.get('reportingperiodenddatetime') ################################################################################################################ # Step 1: valid parameters @@ -39,19 +43,62 @@ class Reporting: else: space_id = int(space_id) - cnx = mysql.connector.connect(**config.myems_system_db) - cursor = cnx.cursor(dictionary=True) + timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6]) + if config.utc_offset[0] == '-': + timezone_offset = -timezone_offset - cursor.execute(" SELECT name " - " FROM tbl_spaces " - " WHERE id = %s ", (space_id,)) - row = cursor.fetchone() + if reporting_period_start_datetime_local is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_START_DATETIME") + else: + reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local) + try: + reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local, + '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_START_DATETIME") + reporting_start_datetime_utc = reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + if reporting_period_end_datetime_local is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_END_DATETIME") + else: + reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local) + try: + reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local, + '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_END_DATETIME") + reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + if reporting_start_datetime_utc >= reporting_end_datetime_utc: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description='API.INVALID_REPORTING_PERIOD_END_DATETIME') + + if reporting_start_datetime_utc + timedelta(minutes=15) >= reporting_end_datetime_utc: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description='API.THE_REPORTING_PERIOD_MUST_BE_LONGER_THAN_15_MINUTES') + + cnx_system_db = mysql.connector.connect(**config.myems_system_db) + cursor_system_db = cnx_system_db.cursor(dictionary=True) + + cnx_historical = mysql.connector.connect(**config.myems_historical_db) + cursor_historical = cnx_historical.cursor() + + cursor_system_db.execute(" SELECT name " + " FROM tbl_spaces " + " WHERE id = %s ", (space_id,)) + row = cursor_system_db.fetchone() if row is None: - if cursor: - cursor.close() - if cnx: - cnx.disconnect() + if cursor_system_db: + cursor_system_db.close() + if cnx_system_db: + cnx_system_db.disconnect() raise falcon.HTTPError(falcon.HTTP_404, title='API.NOT_FOUND', description='API.SPACE_NOT_FOUND') else: @@ -64,8 +111,8 @@ class Reporting: query = (" SELECT id, name, parent_space_id " " FROM tbl_spaces " " ORDER BY id ") - cursor.execute(query) - rows_spaces = cursor.fetchall() + cursor_system_db.execute(query) + rows_spaces = cursor_system_db.fetchall() node_dict = dict() if rows_spaces is not None and len(rows_spaces) > 0: for row in rows_spaces: @@ -75,41 +122,104 @@ class Reporting: ################################################################################################################ # Step 3: query all meters in the space tree ################################################################################################################ - meter_list = list() + meter_dict = dict() space_dict = dict() for node in LevelOrderIter(node_dict[space_id]): space_dict[node.id] = node.name - cursor.execute(" SELECT m.id, m.name AS meter_name, s.name AS space_name, " - " cc.name AS cost_center_name, ec.name AS energy_category_name, " - " m.description " - " FROM tbl_spaces s, tbl_spaces_meters sm, tbl_meters m, tbl_cost_centers cc, " - " tbl_energy_categories ec " - " WHERE s.id IN ( " + ', '.join(map(str, space_dict.keys())) + ") " - " AND sm.space_id = s.id AND sm.meter_id = m.id " - " AND m.cost_center_id = cc.id AND m.energy_category_id = ec.id ", ) - rows_meters = cursor.fetchall() + cursor_system_db.execute(" SELECT m.id, m.name AS meter_name, s.name AS space_name, " + " cc.name AS cost_center_name, ec.name AS energy_category_name, " + " m.description " + " FROM tbl_spaces s, tbl_spaces_meters sm, tbl_meters m, tbl_cost_centers cc, " + " tbl_energy_categories ec " + " WHERE s.id IN ( " + ', '.join(map(str, space_dict.keys())) + ") " + " AND sm.space_id = s.id AND sm.meter_id = m.id " + " AND m.cost_center_id = cc.id AND m.energy_category_id = ec.id ", ) + rows_meters = cursor_system_db.fetchall() if rows_meters is not None and len(rows_meters) > 0: for row in rows_meters: - meter_list.append({"id": row['id'], - "meter_name": row['meter_name'], - "space_name": row['space_name'], - "cost_center_name": row['cost_center_name'], - "energy_category_name": row['energy_category_name'], - "description": row['description']}) - - if cursor: - cursor.close() - if cnx: - cnx.disconnect() + meter_dict[row['id']] = {"meter_name": row['meter_name'], + "space_name": row['space_name'], + "cost_center_name": row['cost_center_name'], + "energy_category_name": row['energy_category_name'], + "description": row['description']} ################################################################################################################ - # Step 4: construct the report + # Step 4: query start value and end value ################################################################################################################ + for meter_id in meter_dict: + cursor_system_db.execute(" SELECT point_id " + " FROM tbl_meters_points " + " WHERE meter_id = %s ", (meter_id, )) + + rows_points_id = cursor_system_db.fetchall() + + start_value = None + end_value = None + + if rows_points_id is not None and len(rows_points_id) > 0: + query_start_value = (" SELECT actual_value " + " FROM tbl_energy_value " + " where point_id in (" + + ', '.join(map(lambda x: str(x['point_id']), rows_points_id)) + ") " + " AND utc_date_time BETWEEN %s AND %s " + " order by utc_date_time ASC LIMIT 0,1") + query_end_value = (" SELECT actual_value " + " FROM tbl_energy_value " + " where point_id in (" + + ', '.join(map(lambda x: str(x['point_id']), rows_points_id)) + ") " + " AND utc_date_time BETWEEN %s AND %s " + " order by utc_date_time DESC LIMIT 0,1") + cursor_historical.execute(query_start_value, + (reporting_start_datetime_utc, + (reporting_start_datetime_utc + timedelta(minutes=15)), )) + row_start_value = cursor_historical.fetchone() + if row_start_value is not None: + start_value = row_start_value[0] + + cursor_historical.execute(query_end_value, + ((reporting_end_datetime_utc - timedelta(minutes=15)), + reporting_end_datetime_utc, )) + row_end_value = cursor_historical.fetchone() + + if row_end_value is not None: + end_value = row_end_value[0] + + meter_dict[meter_id]['start_value'] = start_value + meter_dict[meter_id]['end_value'] = end_value + + if cursor_system_db: + cursor_system_db.close() + if cnx_system_db: + cnx_system_db.disconnect() + + if cursor_historical: + cursor_historical.close() + if cnx_historical: + cnx_historical.disconnect() + + ################################################################################################################ + # Step 5: construct the report + ################################################################################################################ + meter_list = list() + for meter_id, meter in meter_dict.items(): + meter_list.append({ + "id": meter_id, + "meter_name": meter['meter_name'], + "space_name": meter['space_name'], + "cost_center_name": meter['cost_center_name'], + "energy_category_name": meter['energy_category_name'], + "description": meter['description'], + "start_value": meter['start_value'], + "end_value": meter['end_value'] + }) + result = {'meters': meter_list} # export result to Excel file and then encode the file to base64 string result['excel_bytes_base64'] = \ excelexporters.metertracking.export(result, - space_name) + space_name, + reporting_period_start_datetime_local, + reporting_period_end_datetime_local) resp.body = json.dumps(result) diff --git a/web/src/components/MyEMS/Meter/MeterTracking.js b/web/src/components/MyEMS/Meter/MeterTracking.js index b1399548..93211664 100644 --- a/web/src/components/MyEMS/Meter/MeterTracking.js +++ b/web/src/components/MyEMS/Meter/MeterTracking.js @@ -31,9 +31,12 @@ import { withTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import ButtonIcon from '../../common/ButtonIcon'; import { APIBaseURL } from '../../../config'; +import Datetime from "react-datetime"; +import moment from "moment"; const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { + let current_moment = moment(); useEffect(() => { let is_logged_in = getCookieValue('is_logged_in'); let user_name = getCookieValue('user_name'); @@ -60,6 +63,14 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { const [spinnerHidden, setSpinnerHidden] = useState(false); const [exportButtonHidden, setExportButtonHidden] = useState(true); const [excelBytesBase64, setExcelBytesBase64] = useState(undefined); + const [selectedSpaceID, setSelectedSpaceID] = useState(undefined) + + //Query From + const [reportingPeriodBeginsDatetime, setReportingPeriodBeginsDatetime] = useState(current_moment.clone().startOf('month')); + const [reportingPeriodEndsDatetime, setReportingPeriodEndsDatetime] = useState(current_moment); + + // buttons + const [submitButtonDisabled, setSubmitButtonDisabled] = useState(true); useEffect(() => { // begin of getting space tree @@ -87,51 +98,10 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { setCascaderOptions(json); // set the default selected space setSelectedSpaceName([json[0]].map(o => o.label)); - let selectedSpaceID = [json[0]].map(o => o.value); - // begin of gettting meter list - let isSecondResponseOK = false; - fetch(APIBaseURL + '/reports/metertracking?spaceid=' + selectedSpaceID, { - method: 'GET', - headers: { - "Content-type": "application/json", - "User-UUID": getCookieValue('user_uuid'), - "Token": getCookieValue('token') - }, - body: null, + setSelectedSpaceID([json[0]].map(o => o.value)); - }).then(response => { - if (response.ok) { - isSecondResponseOK = true; - } - return response.json(); - }).then(json => { - if (isSecondResponseOK) { - let json_meters = JSON.parse(JSON.stringify([json['meters']]).split('"id":').join('"value":').split('"name":').join('"label":')); - let meters = []; - json_meters[0].forEach((currentValue, index) => { - meters.push({ - 'id': currentValue['id'], - 'name': currentValue['meter_name'], - 'space': currentValue['space_name'], - 'costcenter': currentValue['cost_center_name'], - 'energycategory': currentValue['energy_category_name'], - 'description': currentValue['description']}); - }); - setMeterList(meters); - - setExcelBytesBase64(json['excel_bytes_base64']); - - // hide spinner - setSpinnerHidden(true); - // show export buttion - setExportButtonHidden(false); - } else { - toast.error(json.description) - } - }).catch(err => { - console.log(err); - }); - // end of getting meter list + setSubmitButtonDisabled(false); + setSpinnerHidden(true); } else { toast.error(json.description); } @@ -203,6 +173,20 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { classes: 'border-0 py-2 align-middle', sort: true }, + { + dataField: 'startvalue', + headerClasses: 'border-0', + text: t('Start Value'), + classes: 'border-0 py-2 align-middle', + sort: true + }, + { + dataField: 'endvalue', + headerClasses: 'border-0', + text: t('End Value'), + classes: 'border-0 py-2 align-middle', + sort: true + }, { dataField: '', headerClasses: 'border-0', @@ -217,58 +201,98 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { let onSpaceCascaderChange = (value, selectedOptions) => { setSelectedSpaceName(selectedOptions.map(o => o.label).join('/')); - let selectedSpaceID = value[value.length - 1]; + setSelectedSpaceID(value[value.length - 1]); + setMeterList([]); + setExportButtonHidden(true); + setSubmitButtonDisabled(false); + }; + + let onReportingPeriodBeginsDatetimeChange = (newDateTime) => { + setReportingPeriodBeginsDatetime(newDateTime); + + } + + let onReportingPeriodEndsDatetimeChange = (newDateTime) => { + setReportingPeriodEndsDatetime(newDateTime); + + } + + var getValidReportingPeriodBeginsDatetimes = function (currentDate) { + return currentDate.isBefore(moment(reportingPeriodEndsDatetime, 'MM/DD/YYYY, hh:mm:ss a')); + } + + var getValidReportingPeriodEndsDatetimes = function (currentDate) { + return currentDate.isAfter(moment(reportingPeriodBeginsDatetime, 'MM/DD/YYYY, hh:mm:ss a')); + } + + // Handler + const handleSubmit = e => { + e.preventDefault(); + console.log('handleSubmit'); + console.log(selectedSpaceID); + console.log(reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss')); + console.log(reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss')); + + // disable submit button + setSubmitButtonDisabled(true); // show spinner setSpinnerHidden(false); // hide export buttion - setExportButtonHidden(true) - // begin of gettting meter list - let isSecondResponseOK = false; - fetch(APIBaseURL + '/reports/metertracking?spaceid=' + selectedSpaceID, { - method: 'GET', - headers: { - "Content-type": "application/json", - "User-UUID": getCookieValue('user_uuid'), - "Token": getCookieValue('token') - }, - body: null, + setExportButtonHidden(true) - }).then(response => { - if (response.ok) { - isSecondResponseOK = true; - } - return response.json(); - }).then(json => { - if (isSecondResponseOK) { - let json_meters = JSON.parse(JSON.stringify([json['meters']]).split('"id":').join('"value":').split('"name":').join('"label":')); - let meters = []; - json_meters[0].forEach((currentValue, index) => { - meters.push({ - 'id': currentValue['id'], - 'name': currentValue['meter_name'], - 'space': currentValue['space_name'], - 'costcenter': currentValue['cost_center_name'], - 'energycategory': currentValue['energy_category_name'], - 'description': currentValue['description']}); - }); - setMeterList(meters); + setMeterList([]); - setExcelBytesBase64(json['excel_bytes_base64']); - - // hide spinner - setSpinnerHidden(true); - // show export buttion - setExportButtonHidden(false); - } else { - toast.error(json.description) - } - }).catch(err => { - console.log(err); - }); - // end of getting meter list + let isResponseOK = false; + fetch(APIBaseURL + '/reports/metertracking?' + + 'spaceid=' + selectedSpaceID + + '&reportingperiodstartdatetime=' + reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss') + + '&reportingperiodenddatetime=' + reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss'), { + method: 'GET', + headers: { + "Content-type": "application/json", + "User-UUID": getCookieValue('user_uuid'), + "Token": getCookieValue('token') + }, + body: null, + + }).then(response => { + if (response.ok) { + isResponseOK = true; + } + return response.json(); + }).then(json => { + if (isResponseOK) { + let json_meters = JSON.parse(JSON.stringify([json['meters']]).split('"id":').join('"value":').split('"name":').join('"label":')); + let meters = []; + json_meters[0].forEach((currentValue, index) => { + meters.push({ + 'id': currentValue['id'], + 'name': currentValue['meter_name'], + 'space': currentValue['space_name'], + 'costcenter': currentValue['cost_center_name'], + 'energycategory': currentValue['energy_category_name'], + 'description': currentValue['description'], + 'startvalue': currentValue['start_value'], + 'endvalue': currentValue['end_value']}); + }); + setMeterList(meters); + + setExcelBytesBase64(json['excel_bytes_base64']); + + // hide spinner + setSpinnerHidden(true); + // show export buttion + setExportButtonHidden(false); + + setSubmitButtonDisabled(false); + } else { + toast.error(json.description) + } + }).catch(err => { + console.log(err); + }); }; - const handleExport = e => { e.preventDefault(); const mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' @@ -295,7 +319,7 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { -
+ @@ -311,6 +335,38 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => { + + + + + + + + + + + + + + +

+ + + +
+

diff --git a/web/src/i18n.js b/web/src/i18n.js index 9452cc27..a36d19ec 100644 --- a/web/src/i18n.js +++ b/web/src/i18n.js @@ -243,6 +243,8 @@ const resources = { //Meter Tracking "Meter List": "Meter List", "Edit Meter": "Edit", + "Start Value": "Start Value", + "End Value": "End Value", //Equipment Tracking "Equipment List": "Equipment List", "Edit Equipment": "Edit", @@ -582,6 +584,8 @@ const resources = { //Meter Tracking "Meter List": "Zählerliste", "Edit Meter": "bearbeiten", + "Start Value": "Wert des Starts", + "End Value": "Wert des Ends", //Equipment Tracking "Equipment List": "Ausrüstungsliste", "Edit Equipment": "bearbeiten", @@ -924,6 +928,8 @@ const resources = { //Meter Tracking "Meter List": "计量表列表", "Edit Meter": "编辑", + "Start Value": "开始值", + "End Value": "结束值", //Equipment Tracking "Equipment List": "设备列表", "Edit Equipment": "编辑",