Files
olaper/templates/index.html
SERTY 0f1c749b33
All checks were successful
Test Build / test-build (push) Successful in 23s
Scheduler v1
2025-07-30 18:28:55 +03:00

359 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 in sheets %}
{% set mappings = mappings.get(sheet.title) %}
{% 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 %}
{% set preset_name = _('ID: ') + report_id %}
{% if matching_presets %}
{% set preset = matching_presets[0] %}
{% set preset_name = preset.get('name', _('Unnamed Preset')) %}
{% endif %}
<tr>
<td>{{ sheet.title }}</td>
<td>{{ preset_name }}</td>
<td>
<button type="submit" name="render_{{ sheet.title }}">
{{ _('Render to sheet') }}
</button>
</td>
</tr>
{% 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" readonly>
<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>
</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');
function updateCronString() {
if (!cronOutput) return;
const values = cronInputs.map(id => document.getElementById(id).value);
cronOutput.value = values.join(' ');
}
cronInputs.forEach(id => {
const el = document.getElementById(id);
if(el) el.addEventListener('change', updateCronString);
});
updateCronString(); // Initial call
// Управление видимостью поля "N дней"
document.querySelectorAll('.period-select').forEach(select => {
const targetId = select.dataset.target;
const targetDiv = document.getElementById(targetId);
function toggleCustomInput() {
if (select.value === 'last_N_days') {
targetDiv.style.display = 'block';
} else {
targetDiv.style.display = 'none';
}
}
select.addEventListener('change', toggleCustomInput);
toggleCustomInput(); // Initial call on page load
});
});
</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>