404 lines
21 KiB
HTML
404 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="{{ session.get('language', 'ru') }}">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{ _('MyHoreca OLAPer') }}</title>
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||
</head>
|
||
<body>
|
||
|
||
<h1>{{ _('MyHoreca OLAP-to-GoogleSheets') }}</h1>
|
||
|
||
{% if current_user.is_authenticated %}
|
||
<div class="user-info">
|
||
{{ _('Logged in as:') }} <strong>{{ current_user.username }}</strong> |
|
||
<a href="{{ url_for('.logout') }}">{{ _('Logout') }}</a>
|
||
<span class="lang-switcher">
|
||
<a href="{{ url_for('.set_language', language='ru') }}" title="{{ _('Русский') }}">🇷🇺</a> /
|
||
<a href="{{ url_for('.set_language', language='en') }}" title="{{ _('English') }}">🇬🇧</a>
|
||
</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="user-info">
|
||
<a href="{{ url_for('.login') }}">{{ _('Login') }}</a> |
|
||
<a href="{{ url_for('.register') }}">{{ _('Register') }}</a>
|
||
</div>
|
||
{% endif %}
|
||
|
||
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
{% for category, message in messages %}
|
||
<div class="flash-message flash-{{ category }}">{{ message }}</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
{% if current_user.is_authenticated %}
|
||
<div class="container">
|
||
<!-- Секция RMS-сервера -->
|
||
<button type="button" class="collapsible">1. {{ _('Connection to RMS-server') }}</button>
|
||
<div class="content">
|
||
<h3>{{ _('RMS Server Configuration') }}</h3>
|
||
<p>
|
||
{% trans %}Enter the details for your RMS server API. This information is used to connect,
|
||
authenticate, and retrieve the list of available OLAP report presets.{% endtrans %}
|
||
</p>
|
||
<form action="{{ url_for('.configure_rms') }}" method="post">
|
||
<label for="host">{{ _('RMS-host (e.g., http://your-rms-api.com/resto):') }}</label>
|
||
<input type="text" id="host" name="host" value="{{ rms_config.get('host', '') }}" required /><br />
|
||
|
||
<label for="login">{{ _('API Login:') }}</label>
|
||
<input type="text" id="login" name="login" value="{{ rms_config.get('login', '') }}" required /><br />
|
||
|
||
<label for="password">{{ _('API Password:') }}</label>
|
||
<input type="password" id="password" name="password" value="" {% if not rms_config.password_is_set %}required{% endif %} /><br />
|
||
{% if rms_config.password_is_set %}
|
||
<small>{{ _('Password is saved. Enter a new one only if you need to change it.') }}</small><br/>
|
||
{% else %}
|
||
<small>{{ _('Enter the API password for your RMS server.') }}</small><br/>
|
||
{% endif %}
|
||
|
||
<button type="submit">{{ _('Check and Save RMS-config') }}</button>
|
||
</form>
|
||
{% if presets %}
|
||
<p><strong>{{ _('Status:') }}</strong> {% trans num=presets|length %}Successfully connected to RMS. Found %(num)s OLAP presets.{% endtrans %}</p>
|
||
{% elif rms_config.get('host') %}
|
||
<p><strong>{{ _('Status:') }}</strong> {{ _('RMS configuration saved. Presets not yet loaded or connection failed.') }}</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Секция Google-таблиц -->
|
||
<button type="button" class="collapsible" {% if not rms_config.get('host') %}disabled title="{{ _('Configure RMS first') }}"{% endif %}>
|
||
2. {{ _('Google Sheets Configuration') }}
|
||
</button>
|
||
<div class="content">
|
||
<h3>{{ _('Google Sheets Configuration') }}</h3>
|
||
<p>
|
||
{% trans %}To allow the application to write to your Google Sheet, you need to provide
|
||
credentials for a Google Service Account. This account will act on behalf
|
||
of the application.{% endtrans %}
|
||
</p>
|
||
<p>
|
||
<strong>{{ _('How to get credentials:') }}</strong>
|
||
<br>1. {{ _('Go to Google Cloud Console.') }}
|
||
<br>2. {{ _('Create a new project or select an existing one.') }}
|
||
<br>3. {{ _('Enable the "Google Sheets API" and "Google Drive API" for the project.') }}
|
||
<br>4. {{ _('Go to "Credentials", click "Create Credentials", choose "Service Account".') }}
|
||
<br>5. {{ _('Give it a name and grant it the "Editor" role.') }}
|
||
<br>6. {{ _('Create a JSON key for the service account and download the file.') }}
|
||
<br>7. {% trans %}Share your target Google Sheet with the service account's email address (found in the downloaded JSON file, key `client_email`).{% endtrans %}
|
||
</p>
|
||
<form action="{{ url_for('.upload_credentials') }}" method="post" enctype="multipart/form-data">
|
||
<label for="cred_file">{{ _('Service Account Credentials (JSON file):') }}</label>
|
||
<input type="file" id="cred_file" name="cred_file" accept=".json" /><br />
|
||
{% if client_email %}
|
||
<p><strong>{{ _('Current Service Account Email:') }}</strong> <code>{{ client_email }}</code></p>
|
||
<small>{{ _('Upload a new file only if you need to change credentials.') }}</small><br/>
|
||
{% else %}
|
||
<small>{{ _('Upload the JSON file downloaded from Google Cloud Console.') }}</small><br/>
|
||
{% endif %}
|
||
<button type="submit">{{ _('Upload Credentials') }}</button>
|
||
</form>
|
||
<hr>
|
||
<p>
|
||
{% trans %}Enter the URL of the Google Sheet you want to use. The service account email
|
||
(shown above after uploading credentials) must have edit access to this sheet.{% endtrans %}
|
||
</p>
|
||
<form action="{{ url_for('.configure_google') }}" method="post">
|
||
<label for="sheet_url">{{ _('Google Sheet URL:') }}</label>
|
||
<input type="text" id="sheet_url" name="sheet_url" value="{{ google_config.get('sheet_url', '') }}" required placeholder="https://docs.google.com/spreadsheets/d/..."/>
|
||
<button type="submit" {% if not client_email %}disabled title="{{ _('Upload Service Account Credentials first') }}"{% endif %}>
|
||
{{ _('Connect Google Sheets') }}
|
||
</button>
|
||
{% if sheets %}
|
||
<p><strong>{{ _('Status:') }}</strong> {% trans num=sheets|length %}Successfully connected to Google Sheet. Found %(num)s worksheets.{% endtrans %}</p>
|
||
{% elif google_config.get('sheet_url') %}
|
||
<p><strong>{{ _('Status:') }}</strong> {{ _('Google Sheet URL saved. Worksheets not yet loaded or connection failed.') }}</p>
|
||
{% endif %}
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Секция сопоставления листов-отчетов -->
|
||
<button type="button" class="collapsible" {% if not sheets or not presets %}disabled title="{{ _('Configure RMS and Google Sheets first') }}"{% endif %}>
|
||
3. {{ _('Mapping Sheets to OLAP Reports') }}
|
||
</button>
|
||
<div class="content">
|
||
<h3>{{ _('Map Worksheets to OLAP Reports') }}</h3>
|
||
<p>
|
||
{% trans %}Select which OLAP report from RMS should be rendered into each specific worksheet
|
||
(tab) in your Google Sheet.{% endtrans %}
|
||
</p>
|
||
{% if sheets and presets %}
|
||
<form action="{{ url_for('.mapping_set') }}" method="post">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{{ _('Worksheet (Google Sheets)') }}</th>
|
||
<th>{{ _('OLAP-report (RMS)') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for sheet in sheets %}
|
||
<tr>
|
||
<td>{{ sheet.title }}</td>
|
||
<td>
|
||
<select name="sheet_{{ sheet.id }}">
|
||
<option value="">-- {{ _('Not set') }} --</option>
|
||
{% for preset in presets %}
|
||
<option value="{{ preset['id'] }}" {% if mappings.get(sheet.title, {}).get('report_id') == preset['id'] %}selected{% endif %}>
|
||
{{ preset['name'] }} ({{ preset['id'] }})
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<button type="submit">{{ _('Save Mappings') }}</button>
|
||
</form>
|
||
{% elif not sheets and not presets %}
|
||
<p>{{ _('Worksheets and OLAP presets are not loaded. Please configure RMS and Google Sheets first.') }}</p>
|
||
{% elif not sheets %}
|
||
<p>{{ _('Worksheets are not loaded. Check Google Sheets configuration.') }}</p>
|
||
{% elif not presets %}
|
||
<p>{{ _('OLAP presets are not loaded. Check RMS configuration.') }}</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Секция отрисовки отчетов на листах -->
|
||
<button type="button" class="collapsible" {% if not mappings or mappings|length == 0 %}disabled title="{{ _('Configure Mappings first') }}"{% endif %}>
|
||
4. {{ _('Render Reports to Sheets') }}
|
||
</button>
|
||
<div class="content">
|
||
<h3>{{ _('Render Reports') }}</h3>
|
||
<p>
|
||
{% trans %}Select the date range and click "Render to sheet" for each mapping you wish to execute.
|
||
The application will retrieve the OLAP data from RMS for the selected report and period,
|
||
clear the corresponding worksheet in Google Sheets, and write the new data.{% endtrans %}
|
||
</p>
|
||
{% if mappings and mappings|length > 0 %}
|
||
<form action="{{ url_for('.render_olap') }}" method="post">
|
||
<label for="start_date">{{ _('From Date:') }}</label>
|
||
<input type="date" id="start_date" name="start_date" required /><br />
|
||
|
||
<label for="end_date">{{ _('To Date:') }}</label>
|
||
<input type="date" id="end_date" name="end_date" required /><br />
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{{ _('Worksheet') }}</th>
|
||
<th>{{ _('Mapped OLAP Report') }}</th>
|
||
<th>{{ _('Action') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for sheet_title, mapping_info in mappings.items() %}
|
||
{% if mapping_info and mapping_info.get('report_id') %}
|
||
{% set report_id = mapping_info.get('report_id') %}
|
||
{% set matching_presets = presets | selectattr('id', 'equalto', report_id) | list %}
|
||
|
||
<!-- Отображаем строку только если для этого report_id найден соответствующий пресет -->
|
||
{% if matching_presets %}
|
||
{% set preset = matching_presets[0] %}
|
||
{% set preset_name = preset.get('name', _('Unnamed Preset')) %}
|
||
|
||
<tr>
|
||
<td>{{ sheet_title }}</td>
|
||
<td>{{ preset_name }}</td>
|
||
<td>
|
||
<button type="submit" name="render_{{ sheet_title }}">
|
||
{{ _('Render to sheet') }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</form>
|
||
{% else %}
|
||
<p>{{ _('No mappings configured yet.') }}</p>
|
||
<p><small>{{ _('Please go to the "Mapping Sheets to OLAP Reports" section (Step 3) to set up mappings.') }}</small></p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<button type="button" class="collapsible" {% if not mappings or mappings|length == 0 %}disabled title="{{ _('Configure Mappings first') }}"{% endif %}>
|
||
5. {{ _('Scheduling Automatic Reports') }}
|
||
</button>
|
||
<div class="content">
|
||
<h3>{{ _('Schedule Settings') }}</h3>
|
||
<p>
|
||
{% trans %}Here you can set up a CRON schedule for automatic report generation.
|
||
The report will be generated for the specified period relative to the execution time.{% endtrans %}
|
||
</p>
|
||
|
||
<div class="cron-constructor">
|
||
<h4>{{ _('Cron Schedule Builder') }}</h4>
|
||
<p><small>{% trans %}Use this tool to build a cron string, then copy it to the desired field below.{% endtrans %}</small></p>
|
||
<div class="cron-row">
|
||
<select id="cron-minute"><option value="*">*</option>{% for i in range(60) %}<option value="{{i}}">{{'%02d'|format(i)}}</option>{% endfor %}</select>
|
||
<select id="cron-hour"><option value="*">*</option>{% for i in range(24) %}<option value="{{i}}">{{'%02d'|format(i)}}</option>{% endfor %}</select>
|
||
<select id="cron-day"><option value="*">*</option>{% for i in range(1, 32) %}<option value="{{i}}">{{i}}</option>{% endfor %}</select>
|
||
<select id="cron-month"><option value="*">*</option>{% for i in range(1, 13) %}<option value="{{i}}">{{i}}</option>{% endfor %}</select>
|
||
<select id="cron-day-of-week"><option value="*">*</option><option value="1">{{_('Mon')}}</option><option value="2">{{_('Tue')}}</option><option value="3">{{_('Wed')}}</option><option value="4">{{_('Thu')}}</option><option value="5">{{_('Fri')}}</option><option value="6">{{_('Sat')}}</option><option value="0">{{_('Sun')}}</option></select>
|
||
</div>
|
||
<label for="cron-output">{{ _('Generated Cron String:') }}</label>
|
||
<input type="text" id="cron-output">
|
||
<p id="cron-human-readable"></p>
|
||
</div>
|
||
<hr>
|
||
|
||
<form action="{{ url_for('.save_schedule') }}" method="post">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{{ _('Worksheet') }}</th>
|
||
<th>{{ _('Schedule (Cron)') }}</th>
|
||
<th>{{ _('Report Period') }}</th>
|
||
<th>{{ _('Action') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for sheet_title, params in mappings.items() %}
|
||
<tr>
|
||
<td>{{ sheet_title }}</td>
|
||
<td>
|
||
<input type="text" name="cron-{{ sheet_title }}" value="{{ params.get('schedule_cron', '') }}" placeholder="e.g., 0 2 * * 1">
|
||
</td>
|
||
<td>
|
||
{% set current_period = params.get('schedule_period', '') %}
|
||
<select name="period-{{ sheet_title }}" class="period-select" data-target="custom-days-{{ sheet_title }}">
|
||
<option value="">-- {{ _('Not Scheduled') }} --</option>
|
||
<option value="previous_week" {% if current_period == 'previous_week' %}selected{% endif %}>{{ _('Previous Week') }}</option>
|
||
<option value="last_7_days" {% if current_period == 'last_7_days' %}selected{% endif %}>{{ _('Last 7 Days') }}</option>
|
||
<option value="previous_month" {% if current_period == 'previous_month' %}selected{% endif %}>{{ _('Previous Month') }}</option>
|
||
<option value="current_month" {% if current_period == 'current_month' %}selected{% endif %}>{{ _('Current Month (to yesterday)') }}</option>
|
||
<option value="last_N_days" {% if current_period and current_period.startswith('last_') and current_period.endswith('_days') %}selected{% endif %}>{{ _('Last N Days') }}</option>
|
||
</select>
|
||
<div id="custom-days-{{ sheet_title }}" class="custom-days-input" style="display: none; margin-top: 5px;">
|
||
<input type="number" name="custom_days-{{ sheet_title }}" min="1" placeholder="N" style="width: 60px;" value="{% if current_period and current_period.startswith('last_') %}{{ current_period.split('_')[1] }}{% endif %}">
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<button type="submit">{{ _('Save Schedule') }}</button>
|
||
</form>
|
||
</div>
|
||
|
||
</div> <!-- End Container -->
|
||
|
||
<script>
|
||
// JavaScript для сворачивания/разворачивания секций
|
||
var coll = document.getElementsByClassName("collapsible");
|
||
for (var i = 0; i < coll.length; i++) {
|
||
coll[i].addEventListener("click", function () {
|
||
// Не выполнять действие, если кнопка отключена
|
||
if (this.disabled) {
|
||
return;
|
||
}
|
||
|
||
this.classList.toggle("active");
|
||
var content = this.nextElementSibling;
|
||
|
||
// Если max-height установлен (т.е. секция открыта), то скрыть ее
|
||
if (content.style.maxHeight) {
|
||
content.style.maxHeight = null;
|
||
} else {
|
||
// Иначе (секция закрыта), установить max-height равным высоте контента
|
||
// Это "раскроет" секцию с плавной анимацией
|
||
content.style.maxHeight = content.scrollHeight + "px";
|
||
}
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// --- Cron конструктор и переводчик ---
|
||
const cronInputs = ['cron-minute', 'cron-hour', 'cron-day', 'cron-month', 'cron-day-of-week'];
|
||
const cronOutput = document.getElementById('cron-output');
|
||
const humanReadableOutput = document.getElementById('cron-human-readable');
|
||
|
||
// Функция для перевода cron-строки
|
||
async function translateCronString(cronStr) {
|
||
if (!humanReadableOutput) return;
|
||
|
||
try {
|
||
const response = await fetch("{{ url_for('.translate_cron') }}", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ cron_string: cronStr })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
humanReadableOutput.textContent = 'Error communicating with server.';
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
humanReadableOutput.textContent = data.description;
|
||
humanReadableOutput.style.color = data.description.startsWith('Invalid') ? 'red' : '#555';
|
||
|
||
} catch (error) {
|
||
console.error('Error translating cron string:', error);
|
||
humanReadableOutput.textContent = 'Translation error.';
|
||
}
|
||
}
|
||
|
||
// Функция для обновления строки (из селектов) и запуска перевода
|
||
function updateCronStringFromSelects() {
|
||
if (!cronOutput) return;
|
||
const values = cronInputs.map(id => document.getElementById(id).value);
|
||
const newCronString = values.join(' ');
|
||
cronOutput.value = newCronString;
|
||
|
||
// Вызываем перевод
|
||
translateCronString(newCronString);
|
||
}
|
||
|
||
// Слушаем изменения в выпадающих списках конструктора
|
||
cronInputs.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.addEventListener('change', updateCronStringFromSelects);
|
||
});
|
||
|
||
if (cronOutput) {
|
||
cronOutput.addEventListener('input', function() {
|
||
translateCronString(this.value); // Переводим то, что введено вручную
|
||
});
|
||
}
|
||
|
||
// Первоначальный вызов при загрузке страницы (для начальной строки)
|
||
updateCronStringFromSelects();
|
||
|
||
// --- Управление видимостью поля "N дней" ---
|
||
document.querySelectorAll('.period-select').forEach(select => {
|
||
const targetId = select.dataset.target;
|
||
const targetDiv = document.getElementById(targetId);
|
||
|
||
function toggleCustomInput() {
|
||
if (!targetDiv) return;
|
||
if (select.value === 'last_N_days') {
|
||
targetDiv.style.display = 'block';
|
||
} else {
|
||
targetDiv.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
select.addEventListener('change', toggleCustomInput);
|
||
toggleCustomInput();
|
||
});
|
||
});
|
||
</script>
|
||
{% else %}
|
||
<p style="text-align: center; margin-top: 50px;">{{ _('Please,') }} <a href="{{ url_for('.login') }}">{{ _('login') }}</a> {{ _('or') }} <a href="{{ url_for('.register') }}">{{ _('register') }}</a></p>
|
||
{% endif %}
|
||
</body>
|
||
</html> |