Server / app.py
Kgshop's picture
Update app.py
47e82d3 verified
from flask import Flask, render_template_string, request, redirect, url_for, flash, session
import json
import os
import logging
import threading
import time
from datetime import datetime, timedelta, timezone
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from dotenv import load_dotenv
import uuid
import calendar
load_dotenv()
app = Flask(__name__)
app.secret_key = 'employee_sys_secret_key'
DATA_FILE = 'data.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = os.getenv("REPO_ID", "Kgshop/kopeika")
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
hf_hub_download(
repo_id=REPO_ID,
filename=file_name,
repo_type="dataset",
token=token_to_use,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
success = True
break
except RepositoryNotFoundError:
return False
except HfHubHTTPError as e:
if e.response.status_code == 404:
if attempt == 0 and not os.path.exists(file_name):
try:
if file_name == DATA_FILE:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({'employees': [], 'attendance': {}, 'adjustments': [], 'settings': {'workday_start_time': '09:00'}}, f)
except Exception:
pass
success = False
break
except Exception:
pass
if attempt < retries:
time.sleep(delay)
if not success:
all_successful = False
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
for file_name in files_to_upload:
if os.path.exists(file_name):
try:
api.upload_file(
path_or_fileobj=file_name,
path_in_repo=file_name,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
pass
except Exception:
pass
def periodic_backup():
backup_interval = 1800
while True:
time.sleep(backup_interval)
upload_db_to_hf()
def load_data():
default_data = {'employees': [], 'attendance': {}, 'adjustments': [], 'settings': {'workday_start_time': '09:00'}}
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
raise FileNotFoundError
if 'employees' not in data: data['employees'] = []
if 'attendance' not in data: data['attendance'] = {}
if 'adjustments' not in data: data['adjustments'] = []
if 'settings' not in data: data['settings'] = {'workday_start_time': '09:00'}
return data
except FileNotFoundError:
pass
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
return default_data
if 'employees' not in data: data['employees'] = []
if 'attendance' not in data: data['attendance'] = {}
if 'adjustments' not in data: data['adjustments'] = []
if 'settings' not in data: data['settings'] = {'workday_start_time': '09:00'}
return data
except Exception:
return default_data
else:
if not os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(default_data, f)
except Exception:
pass
return default_data
def save_data(data):
try:
if not isinstance(data, dict):
return
if 'employees' not in data: data['employees'] = []
if 'attendance' not in data: data['attendance'] = {}
if 'adjustments' not in data: data['adjustments'] = []
if 'settings' not in data: data['settings'] = {'workday_start_time': '09:00'}
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf(specific_file=DATA_FILE)
except Exception:
pass
def get_almaty_date():
almaty_tz = timezone(timedelta(hours=5))
return datetime.now(almaty_tz).strftime('%Y-%m-%d')
BASE_TEMPLATE = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Учет сотрудников</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #fff; color: #000; margin: 0; padding: 0; line-height: 1.6; }
.nav { background: #000; color: #fff; padding: 15px 20px; display: flex; gap: 20px; flex-wrap: wrap; }
.nav a { color: #fff; text-decoration: none; font-weight: bold; text-transform: uppercase; font-size: 14px; letter-spacing: 1px; }
.nav a:hover { text-decoration: underline; }
.container { max-width: 1200px; margin: 30px auto; padding: 0 20px; }
h2, h3 { border-bottom: 2px solid #000; padding-bottom: 5px; margin-bottom: 20px; text-transform: uppercase; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 30px; }
th, td { border: 1px solid #000; padding: 12px; text-align: left; }
th { background: #000; color: #fff; text-transform: uppercase; font-size: 13px; }
tr:nth-child(even) { background-color: #f9f9f9; }
.btn { background: #000; color: #fff; padding: 10px 20px; border: 2px solid #000; cursor: pointer; text-decoration: none; display: inline-block; font-size: 14px; font-weight: bold; transition: all 0.2s; text-transform: uppercase;}
.btn:hover { background: #fff; color: #000; }
.btn-danger { background: #fff; color: #000; border: 2px solid #000; }
.btn-danger:hover { background: #000; color: #fff; }
input[type="text"], input[type="number"], input[type="date"], input[type="time"], select { padding: 10px; margin: 5px 0 15px 0; border: 1px solid #000; box-sizing: border-box; width: 100%; font-family: inherit; font-size: 14px; }
input:focus, select:focus { outline: none; border-width: 2px; }
.search-input { padding: 12px; margin: 0 0 20px 0; border: 2px solid #000; box-sizing: border-box; width: 100%; font-size: 14px; font-weight: bold; background-color: #fafafa; }
.search-input:focus { background-color: #fff; outline: none; }
label { font-weight: bold; display: block; text-transform: uppercase; font-size: 12px; }
.alert { padding: 15px; border: 2px solid #000; margin-bottom: 20px; background: #fff; font-weight: bold; text-transform: uppercase; font-size: 13px; }
.grid-2 { display: grid; grid-template-columns: 1fr; gap: 30px; }
@media (min-width: 768px) { .grid-2 { grid-template-columns: 1fr 1fr; } }
.radio-group { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }
.radio-group label { display: flex; align-items: center; gap: 5px; font-weight: normal; text-transform: none; font-size: 14px; cursor: pointer; margin: 0; padding: 5px 0;}
.radio-group input[type="radio"] { margin: 0; cursor: pointer; width: 18px; height: 18px; }
form { max-width: 100%; }
.form-container { border: 1px solid #000; padding: 20px; background: #fafafa; }
.table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; width: 100%; }
.cal-table-wrapper { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid #000; border-top: none; }
.cal-table { margin-top: 0; margin-bottom: 0; white-space: nowrap; border: none; }
.cal-table th, .cal-table td { padding: 8px 5px; text-align: center; border: 1px solid #ddd; min-width: 25px; font-size: 12px; }
.cal-table th { background: #eee; color: #000; border-top: 1px solid #000; }
.cal-table th:first-child, .cal-table td:first-child { text-align: left; position: sticky; left: 0; background: #fff; border-right: 2px solid #000; font-weight: bold; z-index: 2; }
.cal-table th:first-child { background: #000; color: #fff; z-index: 3; border-top: 1px solid #000; }
.cell-work { background-color: #4CAF50; color: #fff; }
.cell-off { background-color: #F44336; color: #fff; }
.cell-intern { background-color: #2196F3; color: #fff; }
.cell-half { background-color: #FF9800; color: #fff; }
.cell-none { background-color: #f1f1f1; color: #aaa; }
.text-green { color: #4CAF50; font-weight: bold; }
.text-red { color: #F44336; font-weight: bold; }
@media (max-width: 600px) {
.cal-table th, .cal-table td { padding: 5px 3px; font-size: 11px; min-width: 20px; }
th, td { padding: 8px; font-size: 12px; }
.radio-group { flex-direction: column; align-items: flex-start; gap: 5px; }
}
</style>
</head>
<body>
<div class="nav">
<a href="/">Сотрудники</a>
<a href="/attendance">Посещаемость</a>
<a href="/finance">Финансы</a>
<a href="/report">Отчеты</a>
<a href="/settings">Настройки</a>
</div>
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script>
function filterTable(inputId, tableId) {
var input = document.getElementById(inputId);
var filter = input.value.toLowerCase();
var table = document.getElementById(tableId);
var tr = table.getElementsByTagName("tr");
for (var i = 1; i < tr.length; i++) {
var display = "none";
var td = tr[i].getElementsByTagName("td");
for (var j = 0; j < td.length; j++) {
if (td[j]) {
var txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toLowerCase().indexOf(filter) > -1) {
display = "";
break;
}
}
}
tr[i].style.display = display;
}
}
</script>
</body>
</html>
"""
INDEX_TEMPLATE = """
<div class="grid-2">
<div>
<h2>Добавить сотрудника</h2>
<div class="form-container">
<form method="POST" action="/add_employee">
<label>ФИО</label>
<input type="text" name="fio" required autocomplete="off">
<label>ИИН</label>
<input type="text" name="iin" required autocomplete="off">
<label>Телефон</label>
<input type="text" name="phone" required autocomplete="off">
<label>ЗП в день</label>
<input type="number" name="daily_salary" step="0.01" required autocomplete="off">
<button type="submit" class="btn">Добавить</button>
</form>
</div>
</div>
</div>
<h2 style="margin-top: 40px;">Список сотрудников</h2>
<input type="text" id="searchEmp" class="search-input" onkeyup="filterTable('searchEmp', 'empTable')" placeholder="Поиск по ФИО, ИИН или телефону...">
<div class="table-responsive">
<table id="empTable">
<tr>
<th>ФИО</th>
<th>ИИН</th>
<th>Телефон</th>
<th>ЗП в день</th>
<th>Действия</th>
</tr>
{% for emp in employees %}
<tr>
<td><strong>{{ emp.fio }}</strong></td>
<td>{{ emp.iin }}</td>
<td>{{ emp.phone }}</td>
<td>{{ emp.daily_salary }}</td>
<td>
<form method="POST" action="/delete_employee/{{ emp.id }}" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить сотрудника?');">
<button type="submit" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">Удалить</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="5" style="text-align:center;">Сотрудников пока нет</td></tr>
{% endfor %}
</table>
</div>
"""
ATTENDANCE_TEMPLATE = """
<h2>Посещаемость</h2>
<div class="form-container" style="margin-bottom: 30px;">
<form method="GET" action="/attendance" style="display: flex; align-items: flex-end; gap: 15px; flex-wrap: wrap;">
<div style="flex-grow: 1; max-width: 250px;">
<label style="margin-bottom: 5px;">Выберите дату:</label>
<input type="date" name="date" value="{{ selected_date }}" style="margin: 0;">
</div>
<button type="submit" class="btn">Загрузить</button>
</form>
</div>
<input type="text" id="searchAtt" class="search-input" onkeyup="filterTable('searchAtt', 'attTable')" placeholder="Поиск по сотрудникам...">
<form method="POST" action="/attendance">
<input type="hidden" name="date" value="{{ selected_date }}">
<div class="table-responsive">
<table id="attTable">
<tr>
<th>Сотрудник</th>
<th>Статус на {{ selected_date }}</th>
</tr>
{% for emp in employees %}
<tr>
<td style="vertical-align: middle;"><strong>{{ emp.fio }}</strong></td>
<td>
<div class="radio-group">
<label>
<input type="radio" name="status_{{ emp.id }}" value="work" {% if attendance.get(emp.id) == 'work' %}checked{% endif %}>
Рабочий день
</label>
<label>
<input type="radio" name="status_{{ emp.id }}" value="intern" {% if attendance.get(emp.id) == 'intern' %}checked{% endif %}>
Стажировка (50%)
</label>
<label>
<input type="radio" name="status_{{ emp.id }}" value="half" {% if attendance.get(emp.id) == 'half' %}checked{% endif %}>
Пол дня (50%)
</label>
<label>
<input type="radio" name="status_{{ emp.id }}" value="off" {% if attendance.get(emp.id) == 'off' %}checked{% endif %}>
Выходной
</label>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="2" style="text-align:center;">Сотрудников пока нет</td></tr>
{% endfor %}
</table>
</div>
{% if employees %}
<button type="submit" class="btn">Сохранить посещаемость</button>
{% endif %}
</form>
"""
FINANCE_TEMPLATE = """
<div class="grid-2">
<div>
<h2>Выдать аванс / штраф</h2>
<div class="form-container">
<form method="POST" action="/add_finance">
<label>Сотрудник</label>
<select name="emp_id" required>
<option value="" disabled selected>Выберите сотрудника</option>
{% for emp in employees %}
<option value="{{ emp.id }}">{{ emp.fio }}</option>
{% endfor %}
</select>
<label>Дата</label>
<input type="date" name="date" value="{{ today }}" required>
<label>Тип операции</label>
<select name="type" required>
<option value="advance">Аванс</option>
<option value="penalty">Штраф</option>
</select>
<label>Сумма</label>
<input type="number" name="amount" step="0.01" required autocomplete="off">
<label>Примечание</label>
<input type="text" name="note" autocomplete="off">
<button type="submit" class="btn">Сохранить</button>
</form>
</div>
</div>
</div>
<h2 style="margin-top: 40px;">История операций (текущий месяц)</h2>
<input type="text" id="searchFin" class="search-input" onkeyup="filterTable('searchFin', 'finTable')" placeholder="Поиск по дате, имени, типу или примечанию...">
<div class="table-responsive">
<table id="finTable">
<tr>
<th>Дата</th>
<th>Сотрудник</th>
<th>Тип</th>
<th>Сумма</th>
<th>Примечание</th>
<th>Действия</th>
</tr>
{% for adj in adjustments|sort(attribute='date', reverse=True) %}
<tr>
<td>{{ adj.date }}</td>
<td><strong>{{ emp_dict.get(adj.emp_id, 'Удаленный сотрудник') }}</strong></td>
<td>
{% if adj.type == 'advance' %}
<span style="color: #2196F3; font-weight: bold;">Аванс</span>
{% else %}
<span style="color: #F44336; font-weight: bold;">Штраф</span>
{% endif %}
</td>
<td>{{ "%.2f"|format(adj.amount) }}</td>
<td>{{ adj.note }}</td>
<td>
<form method="POST" action="/delete_finance/{{ adj.id }}" style="margin:0;" onsubmit="return confirm('Удалить эту запись?');">
<button type="submit" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">Удалить</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="6" style="text-align:center;">Операций пока нет</td></tr>
{% endfor %}
</table>
</div>
"""
REPORT_TEMPLATE = """
<h2>Отчет по ЗП и Календарь</h2>
<div class="form-container" style="margin-bottom: 30px;">
<form method="GET" action="/report" class="grid-2" style="gap: 15px;">
<div>
<label>Дата начала:</label>
<input type="date" name="start_date" value="{{ start_date }}" required style="margin-bottom: 0;">
</div>
<div>
<label>Дата окончания:</label>
<input type="date" name="end_date" value="{{ end_date }}" required style="margin-bottom: 0;">
</div>
<div style="grid-column: 1 / -1;">
<button type="submit" class="btn">Сформировать отчет</button>
</div>
</form>
</div>
{% if report_data %}
<h3 style="margin-top: 40px;">Итоговая зарплата ({{ start_date }} — {{ end_date }})</h3>
<input type="text" id="searchRep" class="search-input" onkeyup="filterTable('searchRep', 'repTable'); filterTable('searchRep', 'calTable')" placeholder="Поиск по сотруднику...">
<div class="table-responsive" style="margin-bottom: 40px;">
<table id="repTable">
<tr>
<th>Сотрудник</th>
<th>Раб.</th>
<th>0.5 дн.</th>
<th>Вых.</th>
<th>Начислено (база)</th>
<th>Авансы</th>
<th>Штрафы</th>
<th style="background: #e8e8e8; color: #000;">К выдаче</th>
</tr>
{% for row in report_data %}
<tr>
<td><strong>{{ row.fio }}</strong></td>
<td>{{ row.work_days }}</td>
<td>{{ row.half_shift_days }}</td>
<td>{{ row.off_days }}</td>
<td>{{ "%.2f"|format(row.base_salary) }}</td>
<td class="text-green">{{ "%.2f"|format(row.advances) }}</td>
<td class="text-red">{{ "%.2f"|format(row.penalties) }}</td>
<td style="background: #fafafa; font-size: 16px;"><strong>{{ "%.2f"|format(row.final_salary) }}</strong></td>
</tr>
{% else %}
<tr><td colspan="8" style="text-align:center;">Нет данных для отображения</td></tr>
{% endfor %}
</table>
</div>
<h3>Календарь посещаемости</h3>
<div style="display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px; font-size: 13px; font-weight: bold;">
<div style="display: flex; align-items: center; gap: 5px;"><div style="width: 15px; height: 15px; background: #4CAF50;"></div> - Рабочий</div>
<div style="display: flex; align-items: center; gap: 5px;"><div style="width: 15px; height: 15px; background: #2196F3;"></div> - Стажировка</div>
<div style="display: flex; align-items: center; gap: 5px;"><div style="width: 15px; height: 15px; background: #FF9800;"></div> - Пол дня</div>
<div style="display: flex; align-items: center; gap: 5px;"><div style="width: 15px; height: 15px; background: #F44336;"></div> - Выходной</div>
<div style="display: flex; align-items: center; gap: 5px;"><div style="width: 15px; height: 15px; background: #f1f1f1; border: 1px solid #ddd;"></div> - Нет данных</div>
</div>
<div class="cal-table-wrapper">
<table class="cal-table" id="calTable">
<tr>
<th>Сотрудник</th>
{% for d in dates_list %}
<th title="{{ d.full }}">{{ d.short }}</th>
{% endfor %}
</tr>
{% for row in report_data %}
<tr>
<td>{{ row.fio }}</td>
{% for cell in row.calendar %}
{% if cell == 'work' %}
<td class="cell-work" title="Рабочий">Р</td>
{% elif cell == 'intern' %}
<td class="cell-intern" title="Стажировка">С</td>
{% elif cell == 'half' %}
<td class="cell-half" title="Пол дня">½</td>
{% elif cell == 'off' %}
<td class="cell-off" title="Выходной">В</td>
{% else %}
<td class="cell-none">-</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
"""
SETTINGS_TEMPLATE = """
<h2>Настройки системы</h2>
<div class="form-container" style="max-width: 400px;">
<form method="POST" action="/settings">
<label>Начало рабочего дня (Алматы):</label>
<input type="time" name="workday_start_time" value="{{ settings.get('workday_start_time', '09:00') }}" required>
<button type="submit" class="btn" style="width: 100%;">Сохранить настройки</button>
</form>
</div>
"""
@app.route('/')
def index():
data = load_data()
return render_template_string(BASE_TEMPLATE.replace('{% block content %}{% endblock %}', INDEX_TEMPLATE), employees=data.get('employees', []))
@app.route('/add_employee', methods=['POST'])
def add_employee():
data = load_data()
try:
new_emp = {
'id': str(uuid.uuid4()),
'fio': request.form['fio'].strip(),
'iin': request.form['iin'].strip(),
'phone': request.form['phone'].strip(),
'daily_salary': float(request.form['daily_salary'])
}
if 'employees' not in data: data['employees'] = []
data['employees'].append(new_emp)
save_data(data)
flash('Сотрудник успешно добавлен.')
except Exception:
flash('Ошибка при добавлении сотрудника. Проверьте правильность ввода данных.')
return redirect(url_for('index'))
@app.route('/delete_employee/<emp_id>', methods=['POST'])
def delete_employee(emp_id):
data = load_data()
data['employees'] = [e for e in data.get('employees', []) if e['id'] != emp_id]
save_data(data)
flash('Сотрудник удален.')
return redirect(url_for('index'))
@app.route('/attendance', methods=['GET', 'POST'])
def attendance():
data = load_data()
selected_date = request.args.get('date') or request.form.get('date') or get_almaty_date()
if request.method == 'POST':
if 'attendance' not in data: data['attendance'] = {}
if selected_date not in data['attendance']: data['attendance'][selected_date] = {}
for emp in data.get('employees', []):
status = request.form.get(f"status_{emp['id']}")
if status in ['work', 'off', 'intern', 'half']:
data['attendance'][selected_date][emp['id']] = status
save_data(data)
flash('Данные о посещаемости успешно сохранены.')
return redirect(url_for('attendance', date=selected_date))
att_records = data.get('attendance', {}).get(selected_date, {})
return render_template_string(BASE_TEMPLATE.replace('{% block content %}{% endblock %}', ATTENDANCE_TEMPLATE),
employees=data.get('employees', []),
selected_date=selected_date,
attendance=att_records)
@app.route('/finance')
def finance():
data = load_data()
today = get_almaty_date()
current_month_prefix = today[:7]
employees = data.get('employees', [])
emp_dict = {e['id']: e['fio'] for e in employees}
adjustments = [a for a in data.get('adjustments', []) if a['date'].startswith(current_month_prefix)]
return render_template_string(BASE_TEMPLATE.replace('{% block content %}{% endblock %}', FINANCE_TEMPLATE),
employees=employees,
emp_dict=emp_dict,
adjustments=adjustments,
today=today)
@app.route('/add_finance', methods=['POST'])
def add_finance():
data = load_data()
try:
new_adj = {
'id': str(uuid.uuid4()),
'emp_id': request.form['emp_id'],
'date': request.form['date'],
'type': request.form['type'],
'amount': float(request.form['amount']),
'note': request.form.get('note', '').strip()
}
if 'adjustments' not in data: data['adjustments'] = []
data['adjustments'].append(new_adj)
save_data(data)
flash('Операция успешно добавлена.')
except Exception:
flash('Ошибка при добавлении операции.')
return redirect(url_for('finance'))
@app.route('/delete_finance/<adj_id>', methods=['POST'])
def delete_finance(adj_id):
data = load_data()
data['adjustments'] = [a for a in data.get('adjustments', []) if a['id'] != adj_id]
save_data(data)
flash('Запись удалена.')
return redirect(url_for('finance'))
@app.route('/report', methods=['GET'])
def report():
data = load_data()
almaty_today = datetime.now(timezone(timedelta(hours=5)))
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
if not start_date:
start_date = almaty_today.replace(day=1).strftime('%Y-%m-%d')
if not end_date:
_, last_day = calendar.monthrange(almaty_today.year, almaty_today.month)
end_date = almaty_today.replace(day=last_day).strftime('%Y-%m-%d')
report_data = []
dates_list = []
try:
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
curr = start_dt
while curr <= end_dt:
dates_list.append({
'full': curr.strftime('%Y-%m-%d'),
'short': curr.strftime('%d.%m')
})
curr += timedelta(days=1)
employees = data.get('employees', [])
attendance = data.get('attendance', {})
adjustments = data.get('adjustments', [])
for emp in employees:
work_days = 0
off_days = 0
intern_days = 0
half_days = 0
cal_row = []
for d in dates_list:
status = attendance.get(d['full'], {}).get(emp['id'], 'none')
cal_row.append(status)
if status == 'work': work_days += 1
elif status == 'off': off_days += 1
elif status == 'intern': intern_days += 1
elif status == 'half': half_days += 1
half_shift_days = intern_days + half_days
daily_salary = emp.get('daily_salary', 0)
base_salary = (work_days * daily_salary) + (half_shift_days * (daily_salary / 2.0))
emp_adjs = [a for a in adjustments if a['emp_id'] == emp['id'] and start_date <= a['date'] <= end_date]
advances = sum(a['amount'] for a in emp_adjs if a['type'] == 'advance')
penalties = sum(a['amount'] for a in emp_adjs if a['type'] == 'penalty')
final_salary = base_salary - advances - penalties
report_data.append({
'fio': emp['fio'],
'work_days': work_days,
'half_shift_days': half_shift_days,
'off_days': off_days,
'base_salary': base_salary,
'advances': advances,
'penalties': penalties,
'final_salary': final_salary,
'calendar': cal_row
})
except Exception:
flash('Некорректный формат дат.')
return render_template_string(BASE_TEMPLATE.replace('{% block content %}{% endblock %}', REPORT_TEMPLATE),
start_date=start_date, end_date=end_date,
report_data=report_data, dates_list=dates_list)
@app.route('/settings', methods=['GET', 'POST'])
def settings():
data = load_data()
if request.method == 'POST':
if 'settings' not in data: data['settings'] = {}
data['settings']['workday_start_time'] = request.form['workday_start_time']
save_data(data)
flash('Настройки успешно сохранены.')
return redirect(url_for('settings'))
return render_template_string(BASE_TEMPLATE.replace('{% block content %}{% endblock %}', SETTINGS_TEMPLATE), settings=data.get('settings', {}))
if __name__ == '__main__':
download_db_from_hf()
load_data()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)