Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -63,7 +63,7 @@ def download_db_from_hf(specific_file=None, retries=3, delay=5):
|
|
| 63 |
try:
|
| 64 |
if file_name == DATA_FILE:
|
| 65 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 66 |
-
json.dump({'products': [], 'categories': [], 'orders': {}, 'employees': [], 'workdays': {}, 'fines':
|
| 67 |
'cafe_name': 'HongKong',
|
| 68 |
'wa_shift1': '+77470623684',
|
| 69 |
'wa_shift2': '+77470623684',
|
|
@@ -122,7 +122,7 @@ def load_data():
|
|
| 122 |
'orders': {},
|
| 123 |
'employees': [],
|
| 124 |
'workdays': {},
|
| 125 |
-
'fines':
|
| 126 |
'settings': {
|
| 127 |
'cafe_name': 'Sova',
|
| 128 |
'wa_shift1': '+77470623684',
|
|
@@ -142,7 +142,7 @@ def load_data():
|
|
| 142 |
if 'orders' not in data: data['orders'] = {}
|
| 143 |
if 'employees' not in data: data['employees'] = []
|
| 144 |
if 'workdays' not in data: data['workdays'] = {}
|
| 145 |
-
if 'fines' not in data: data['fines'] =
|
| 146 |
if 'settings' not in data: data['settings'] = default_data['settings']
|
| 147 |
if 'logo_version' not in data['settings']: data['settings']['logo_version'] = '1'
|
| 148 |
except (FileNotFoundError, json.JSONDecodeError):
|
|
@@ -155,7 +155,7 @@ def load_data():
|
|
| 155 |
if 'orders' not in data: data['orders'] = {}
|
| 156 |
if 'employees' not in data: data['employees'] = []
|
| 157 |
if 'workdays' not in data: data['workdays'] = {}
|
| 158 |
-
if 'fines' not in data: data['fines'] =
|
| 159 |
if 'settings' not in data: data['settings'] = default_data['settings']
|
| 160 |
if 'logo_version' not in data['settings']: data['settings']['logo_version'] = '1'
|
| 161 |
except Exception:
|
|
@@ -180,9 +180,9 @@ def load_data():
|
|
| 180 |
|
| 181 |
for emp in data['employees']:
|
| 182 |
if 'pin' not in emp: emp['pin'] = '0000'
|
| 183 |
-
if 'daily_rate' not in emp: emp['daily_rate'] =
|
| 184 |
-
if 'target_amount' not in emp: emp['target_amount'] =
|
| 185 |
-
if 'bonus_percentage' not in emp: emp['bonus_percentage'] =
|
| 186 |
|
| 187 |
if not os.path.exists(DATA_FILE):
|
| 188 |
try:
|
|
@@ -201,7 +201,7 @@ def save_data(data):
|
|
| 201 |
if 'orders' not in data: data['orders'] = {}
|
| 202 |
if 'employees' not in data: data['employees'] = []
|
| 203 |
if 'workdays' not in data: data['workdays'] = {}
|
| 204 |
-
if 'fines' not in data: data['fines'] =
|
| 205 |
if 'settings' not in data: data['settings'] = {
|
| 206 |
'cafe_name': 'Sova',
|
| 207 |
'wa_shift1': '',
|
|
@@ -1171,7 +1171,7 @@ SALARY_TEMPLATE = '''
|
|
| 1171 |
<head>
|
| 1172 |
<meta charset="UTF-8">
|
| 1173 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 1174 |
-
<title>Отчет по ЗП</title>
|
| 1175 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1176 |
<style>
|
| 1177 |
:root { --primary: #D4AF37; --bg: #000000; --surface: #111111; --border: #333333; --danger: #ff4757; --success: #2ed573; --info: #D4AF37; --warning: #ffa502; --text: #ffffff; --text-muted: #888888; }
|
|
@@ -1184,14 +1184,15 @@ SALARY_TEMPLATE = '''
|
|
| 1184 |
.btn { padding: 12px 20px; border: none; border-radius: 10px; font-weight: 700; cursor: pointer; color: #000; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.95rem; transition: opacity 0.2s; }
|
| 1185 |
.btn:active { opacity: 0.8; }
|
| 1186 |
.btn-success { background: var(--success); }
|
|
|
|
| 1187 |
.btn-dark { background: #1a1a1a; color: var(--primary); border: 1px solid var(--border); }
|
| 1188 |
|
| 1189 |
.report-section { background: var(--surface); padding: 20px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 20px; }
|
| 1190 |
|
| 1191 |
.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 1192 |
.form-row > * { flex: 1; min-width: 150px; }
|
| 1193 |
-
input[type="date"] { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; background: #1a1a1a; color: var(--text); }
|
| 1194 |
-
input[type="date"]:focus { border-color: var(--primary); background: #222; }
|
| 1195 |
label { display: block; margin-bottom: 5px; color: var(--text-muted); font-size: 0.85rem; font-weight: 600; }
|
| 1196 |
|
| 1197 |
.rep-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
@@ -1207,11 +1208,68 @@ SALARY_TEMPLATE = '''
|
|
| 1207 |
<body>
|
| 1208 |
<div class="container">
|
| 1209 |
<div class="header-panel">
|
| 1210 |
-
<h1><i class="fas fa-money-bill-wave"></i> Отчет по ЗП</h1>
|
| 1211 |
<a href="/admin" class="btn btn-dark"><i class="fas fa-arrow-left"></i> Назад в админку</a>
|
| 1212 |
</div>
|
| 1213 |
|
| 1214 |
<div class="report-section">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
<div class="form-row" style="margin-bottom: 20px; align-items: flex-end;">
|
| 1216 |
<div>
|
| 1217 |
<label>Дата начала</label>
|
|
@@ -1269,7 +1327,14 @@ SALARY_TEMPLATE = '''
|
|
| 1269 |
employees.forEach(emp => {
|
| 1270 |
let daysWorked = 0;
|
| 1271 |
let fixedSal = 0;
|
| 1272 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1273 |
|
| 1274 |
let currDate = new Date(start);
|
| 1275 |
const endDate = new Date(end);
|
|
@@ -1280,33 +1345,23 @@ SALARY_TEMPLATE = '''
|
|
| 1280 |
const worked = workdays[dateStr] && workdays[dateStr].includes(emp.id);
|
| 1281 |
const dailySales = (empSales[emp.id] && empSales[emp.id][dateStr]) ? empSales[emp.id][dateStr] : 0;
|
| 1282 |
|
| 1283 |
-
if (worked
|
| 1284 |
daysWorked++;
|
| 1285 |
-
fixedSal += parseFloat(emp.daily_rate ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1286 |
}
|
| 1287 |
-
|
| 1288 |
currDate.setDate(currDate.getDate() + 1);
|
| 1289 |
}
|
| 1290 |
|
| 1291 |
-
let bonusSal = 0;
|
| 1292 |
-
const target = parseFloat(emp.target_amount || 1500000);
|
| 1293 |
-
const pct = parseFloat(emp.bonus_percentage || 10);
|
| 1294 |
-
|
| 1295 |
-
if (pct > 0 && totalSales > target) {
|
| 1296 |
-
bonusSal = (totalSales - target) * (pct / 100);
|
| 1297 |
-
}
|
| 1298 |
-
|
| 1299 |
-
let totalFines = 0;
|
| 1300 |
-
if (fines[emp.id]) {
|
| 1301 |
-
fines[emp.id].forEach(f => {
|
| 1302 |
-
if (f.date >= start && f.date <= end) {
|
| 1303 |
-
totalFines += parseFloat(f.amount || 0);
|
| 1304 |
-
}
|
| 1305 |
-
});
|
| 1306 |
-
}
|
| 1307 |
-
|
| 1308 |
const total = fixedSal + bonusSal - totalFines;
|
| 1309 |
-
if (daysWorked > 0 ||
|
| 1310 |
html += `<tr>
|
| 1311 |
<td>${emp.name}</td>
|
| 1312 |
<td>${daysWorked}</td>
|
|
@@ -1404,10 +1459,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1404 |
.orders-table th { padding: 12px; background: #1a1a1a; border-bottom: 2px solid var(--border); color: var(--primary); font-size: 0.85rem; text-transform: uppercase; }
|
| 1405 |
.orders-table td { padding: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; color: var(--text); }
|
| 1406 |
.orders-table tr:hover { background: #1a1a1a; }
|
| 1407 |
-
|
| 1408 |
-
.rep-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; color: var(--text); }
|
| 1409 |
-
.rep-table th, .rep-table td { padding: 8px 5px; border-bottom: 1px solid var(--border); text-align: left; }
|
| 1410 |
-
.rep-table th { color: var(--text-muted); font-weight: normal; }
|
| 1411 |
|
| 1412 |
@media (max-width: 600px) {
|
| 1413 |
.header-panel { flex-direction: column; align-items: stretch; text-align: center; }
|
|
@@ -1476,9 +1527,9 @@ ADMIN_TEMPLATE = '''
|
|
| 1476 |
<input type="hidden" name="action" value="add_employee">
|
| 1477 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="employee_name" required autocomplete="off"></div>
|
| 1478 |
<div style="flex:1; min-width:100px;"><label>PIN (4 цифры)</label><input type="text" name="pin" pattern="\\d{4}" required autocomplete="off"></div>
|
| 1479 |
-
<div style="flex:1; min-width:100px;"><label>Ставка за день</label><input type="number" name="daily_rate" value="
|
| 1480 |
-
<div style="flex:1; min-width:100px;"><label>Цель (n сумма)</label><input type="number" name="target_amount" value="
|
| 1481 |
-
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="
|
| 1482 |
<button type="submit" class="btn btn-dark" style="width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Добавить</button>
|
| 1483 |
</form>
|
| 1484 |
</div>
|
|
@@ -1497,45 +1548,10 @@ ADMIN_TEMPLATE = '''
|
|
| 1497 |
<div style="flex:1; min-width:100px;"><label>Бонус %</label><input type="number" name="bonus_percentage" value="{{ emp.bonus_percentage }}" step="0.1" required></div>
|
| 1498 |
<button type="submit" class="btn btn-primary" title="Сохранить"><i class="fas fa-save"></i></button>
|
| 1499 |
</form>
|
| 1500 |
-
|
| 1501 |
-
<div style="margin-top:15px; border-top:1px dashed var(--border); padding-top:15px;">
|
| 1502 |
-
<h4 style="margin:0 0 10px 0; color:var(--primary);">Штрафы</h4>
|
| 1503 |
-
<form method="POST" style="display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end; margin-bottom:10px;">
|
| 1504 |
-
<input type="hidden" name="action" value="add_fine">
|
| 1505 |
-
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1506 |
-
<div style="flex:1; min-width:120px;"><label>Дата</label><input type="date" name="fine_date" required></div>
|
| 1507 |
-
<div style="flex:1; min-width:100px;"><label>Сумма</label><input type="number" name="fine_amount" required></div>
|
| 1508 |
-
<div style="flex:2; min-width:150px;"><label>Причина</label><input type="text" name="fine_reason" required autocomplete="off"></div>
|
| 1509 |
-
<button type="submit" class="btn btn-danger" style="padding:12px 15px;"><i class="fas fa-plus"></i></button>
|
| 1510 |
-
</form>
|
| 1511 |
-
{% if fines.get(emp.id) %}
|
| 1512 |
-
<table class="rep-table" style="font-size:0.85rem;">
|
| 1513 |
-
<tr><th>Дата</th><th>Причина</th><th>Сумма</th><th>Удалить</th></tr>
|
| 1514 |
-
{% for f in fines.get(emp.id) %}
|
| 1515 |
-
<tr>
|
| 1516 |
-
<td>{{ f.date }}</td>
|
| 1517 |
-
<td>{{ f.reason }}</td>
|
| 1518 |
-
<td>{{ f.amount }} {{ currency_code }}</td>
|
| 1519 |
-
<td>
|
| 1520 |
-
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить штраф?');">
|
| 1521 |
-
<input type="hidden" name="action" value="delete_fine">
|
| 1522 |
-
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1523 |
-
<input type="hidden" name="fine_id" value="{{ f.id }}">
|
| 1524 |
-
<button type="submit" class="btn btn-danger" style="padding:3px 8px; font-size:0.75rem;"><i class="fas fa-trash"></i></button>
|
| 1525 |
-
</form>
|
| 1526 |
-
</td>
|
| 1527 |
-
</tr>
|
| 1528 |
-
{% endfor %}
|
| 1529 |
-
</table>
|
| 1530 |
-
{% else %}
|
| 1531 |
-
<div style="font-size:0.85rem; color:var(--text-muted);">Нет штрафов</div>
|
| 1532 |
-
{% endif %}
|
| 1533 |
-
</div>
|
| 1534 |
-
|
| 1535 |
-
<form method="POST" style="margin-top:15px; text-align:right;" onsubmit="return confirm('Удалить сотрудника?');">
|
| 1536 |
<input type="hidden" name="action" value="delete_employee">
|
| 1537 |
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1538 |
-
<button type="submit" class="btn btn-danger"
|
| 1539 |
</form>
|
| 1540 |
</div>
|
| 1541 |
{% endfor %}
|
|
@@ -1946,13 +1962,35 @@ def admin_reports():
|
|
| 1946 |
currency_code=CURRENCY_CODE
|
| 1947 |
)
|
| 1948 |
|
| 1949 |
-
@app.route('/admin/salary')
|
| 1950 |
def admin_salary():
|
| 1951 |
data = load_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1952 |
orders = data.get('orders', {})
|
| 1953 |
employees = data.get('employees', [])
|
| 1954 |
workdays = data.get('workdays', {})
|
| 1955 |
-
fines = data.get('fines',
|
| 1956 |
|
| 1957 |
return render_template_string(
|
| 1958 |
SALARY_TEMPLATE,
|
|
@@ -1960,6 +1998,8 @@ def admin_salary():
|
|
| 1960 |
employees_json=json.dumps(employees),
|
| 1961 |
workdays_json=json.dumps(workdays),
|
| 1962 |
fines_json=json.dumps(fines),
|
|
|
|
|
|
|
| 1963 |
currency_code=CURRENCY_CODE
|
| 1964 |
)
|
| 1965 |
|
|
@@ -1970,7 +2010,6 @@ def admin():
|
|
| 1970 |
categories = data.get('categories', [])
|
| 1971 |
orders = data.get('orders', {})
|
| 1972 |
employees = data.get('employees', [])
|
| 1973 |
-
fines = data.get('fines', {})
|
| 1974 |
settings = data.get('settings', {})
|
| 1975 |
|
| 1976 |
if request.method == 'POST':
|
|
@@ -1989,12 +2028,12 @@ def admin():
|
|
| 1989 |
elif action == 'add_employee':
|
| 1990 |
emp_name = request.form.get('employee_name', '').strip()
|
| 1991 |
pin = request.form.get('pin', '0000').strip()
|
| 1992 |
-
try: daily_rate = float(request.form.get('daily_rate',
|
| 1993 |
-
except: daily_rate =
|
| 1994 |
-
try: target_amount = float(request.form.get('target_amount',
|
| 1995 |
-
except: target_amount =
|
| 1996 |
-
try: bonus_percentage = float(request.form.get('bonus_percentage',
|
| 1997 |
-
except: bonus_percentage =
|
| 1998 |
|
| 1999 |
if emp_name:
|
| 2000 |
employees.append({
|
|
@@ -2014,12 +2053,12 @@ def admin():
|
|
| 2014 |
if e.get('id') == emp_id:
|
| 2015 |
e['name'] = request.form.get('name', '').strip()
|
| 2016 |
e['pin'] = request.form.get('pin', '0000').strip()
|
| 2017 |
-
try: e['daily_rate'] = float(request.form.get('daily_rate',
|
| 2018 |
-
except: e['daily_rate'] =
|
| 2019 |
-
try: e['target_amount'] = float(request.form.get('target_amount',
|
| 2020 |
-
except: e['target_amount'] =
|
| 2021 |
-
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage',
|
| 2022 |
-
except: e['bonus_percentage'] =
|
| 2023 |
break
|
| 2024 |
data['employees'] = employees
|
| 2025 |
save_data(data)
|
|
@@ -2029,33 +2068,6 @@ def admin():
|
|
| 2029 |
data['employees'] = [e for e in employees if e.get('id') != emp_id]
|
| 2030 |
save_data(data)
|
| 2031 |
|
| 2032 |
-
elif action == 'add_fine':
|
| 2033 |
-
emp_id = request.form.get('employee_id')
|
| 2034 |
-
fine_date = request.form.get('fine_date')
|
| 2035 |
-
try: fine_amount = float(request.form.get('fine_amount', 0))
|
| 2036 |
-
except: fine_amount = 0
|
| 2037 |
-
fine_reason = request.form.get('fine_reason', '').strip()
|
| 2038 |
-
|
| 2039 |
-
if emp_id and fine_date and fine_amount > 0:
|
| 2040 |
-
if 'fines' not in data:
|
| 2041 |
-
data['fines'] = {}
|
| 2042 |
-
if emp_id not in data['fines']:
|
| 2043 |
-
data['fines'][emp_id] = []
|
| 2044 |
-
data['fines'][emp_id].append({
|
| 2045 |
-
'id': uuid4().hex,
|
| 2046 |
-
'date': fine_date,
|
| 2047 |
-
'amount': fine_amount,
|
| 2048 |
-
'reason': fine_reason
|
| 2049 |
-
})
|
| 2050 |
-
save_data(data)
|
| 2051 |
-
|
| 2052 |
-
elif action == 'delete_fine':
|
| 2053 |
-
emp_id = request.form.get('employee_id')
|
| 2054 |
-
fine_id = request.form.get('fine_id')
|
| 2055 |
-
if 'fines' in data and emp_id in data['fines']:
|
| 2056 |
-
data['fines'][emp_id] = [f for f in data['fines'][emp_id] if f.get('id') != fine_id]
|
| 2057 |
-
save_data(data)
|
| 2058 |
-
|
| 2059 |
elif action == 'delete_order':
|
| 2060 |
order_id = request.form.get('order_id')
|
| 2061 |
if order_id in orders:
|
|
@@ -2200,7 +2212,6 @@ def admin():
|
|
| 2200 |
categories=categories,
|
| 2201 |
orders=orders,
|
| 2202 |
employees=employees,
|
| 2203 |
-
fines=data.get('fines', {}),
|
| 2204 |
repo_id=REPO_ID,
|
| 2205 |
currency_code=CURRENCY_CODE,
|
| 2206 |
settings=settings
|
|
|
|
| 63 |
try:
|
| 64 |
if file_name == DATA_FILE:
|
| 65 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 66 |
+
json.dump({'products': [], 'categories': [], 'orders': {}, 'employees': [], 'workdays': {}, 'fines': [], 'settings': {
|
| 67 |
'cafe_name': 'HongKong',
|
| 68 |
'wa_shift1': '+77470623684',
|
| 69 |
'wa_shift2': '+77470623684',
|
|
|
|
| 122 |
'orders': {},
|
| 123 |
'employees': [],
|
| 124 |
'workdays': {},
|
| 125 |
+
'fines': [],
|
| 126 |
'settings': {
|
| 127 |
'cafe_name': 'Sova',
|
| 128 |
'wa_shift1': '+77470623684',
|
|
|
|
| 142 |
if 'orders' not in data: data['orders'] = {}
|
| 143 |
if 'employees' not in data: data['employees'] = []
|
| 144 |
if 'workdays' not in data: data['workdays'] = {}
|
| 145 |
+
if 'fines' not in data: data['fines'] = []
|
| 146 |
if 'settings' not in data: data['settings'] = default_data['settings']
|
| 147 |
if 'logo_version' not in data['settings']: data['settings']['logo_version'] = '1'
|
| 148 |
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
| 155 |
if 'orders' not in data: data['orders'] = {}
|
| 156 |
if 'employees' not in data: data['employees'] = []
|
| 157 |
if 'workdays' not in data: data['workdays'] = {}
|
| 158 |
+
if 'fines' not in data: data['fines'] = []
|
| 159 |
if 'settings' not in data: data['settings'] = default_data['settings']
|
| 160 |
if 'logo_version' not in data['settings']: data['settings']['logo_version'] = '1'
|
| 161 |
except Exception:
|
|
|
|
| 180 |
|
| 181 |
for emp in data['employees']:
|
| 182 |
if 'pin' not in emp: emp['pin'] = '0000'
|
| 183 |
+
if 'daily_rate' not in emp: emp['daily_rate'] = 0
|
| 184 |
+
if 'target_amount' not in emp: emp['target_amount'] = 0
|
| 185 |
+
if 'bonus_percentage' not in emp: emp['bonus_percentage'] = 0
|
| 186 |
|
| 187 |
if not os.path.exists(DATA_FILE):
|
| 188 |
try:
|
|
|
|
| 201 |
if 'orders' not in data: data['orders'] = {}
|
| 202 |
if 'employees' not in data: data['employees'] = []
|
| 203 |
if 'workdays' not in data: data['workdays'] = {}
|
| 204 |
+
if 'fines' not in data: data['fines'] = []
|
| 205 |
if 'settings' not in data: data['settings'] = {
|
| 206 |
'cafe_name': 'Sova',
|
| 207 |
'wa_shift1': '',
|
|
|
|
| 1171 |
<head>
|
| 1172 |
<meta charset="UTF-8">
|
| 1173 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 1174 |
+
<title>Отчет по ЗП и Штрафы</title>
|
| 1175 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1176 |
<style>
|
| 1177 |
:root { --primary: #D4AF37; --bg: #000000; --surface: #111111; --border: #333333; --danger: #ff4757; --success: #2ed573; --info: #D4AF37; --warning: #ffa502; --text: #ffffff; --text-muted: #888888; }
|
|
|
|
| 1184 |
.btn { padding: 12px 20px; border: none; border-radius: 10px; font-weight: 700; cursor: pointer; color: #000; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.95rem; transition: opacity 0.2s; }
|
| 1185 |
.btn:active { opacity: 0.8; }
|
| 1186 |
.btn-success { background: var(--success); }
|
| 1187 |
+
.btn-danger { background: var(--danger); }
|
| 1188 |
.btn-dark { background: #1a1a1a; color: var(--primary); border: 1px solid var(--border); }
|
| 1189 |
|
| 1190 |
.report-section { background: var(--surface); padding: 20px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 20px; }
|
| 1191 |
|
| 1192 |
.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 1193 |
.form-row > * { flex: 1; min-width: 150px; }
|
| 1194 |
+
input[type="date"], input[type="number"], input[type="text"] { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; background: #1a1a1a; color: var(--text); }
|
| 1195 |
+
input[type="date"]:focus, input[type="number"]:focus, input[type="text"]:focus { border-color: var(--primary); background: #222; }
|
| 1196 |
label { display: block; margin-bottom: 5px; color: var(--text-muted); font-size: 0.85rem; font-weight: 600; }
|
| 1197 |
|
| 1198 |
.rep-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
|
|
| 1208 |
<body>
|
| 1209 |
<div class="container">
|
| 1210 |
<div class="header-panel">
|
| 1211 |
+
<h1><i class="fas fa-money-bill-wave"></i> Отчет по ЗП и Штрафы</h1>
|
| 1212 |
<a href="/admin" class="btn btn-dark"><i class="fas fa-arrow-left"></i> Назад в админку</a>
|
| 1213 |
</div>
|
| 1214 |
|
| 1215 |
<div class="report-section">
|
| 1216 |
+
<div style="background:var(--surface); border:1px solid var(--border); padding:15px; border-radius:10px; margin-bottom:20px;">
|
| 1217 |
+
<h3 style="margin-top:0; color:var(--primary);">Добавить штраф</h3>
|
| 1218 |
+
<form method="POST" style="display:flex; flex-wrap:wrap; gap:10px;">
|
| 1219 |
+
<input type="hidden" name="action" value="add_fine">
|
| 1220 |
+
<div style="flex:1; min-width:150px;">
|
| 1221 |
+
<label>Сотрудник</label>
|
| 1222 |
+
<select name="employee_id" required style="width:100%; padding:12px 15px; border:1px solid var(--border); border-radius:10px; background:#1a1a1a; color:#fff; outline:none;">
|
| 1223 |
+
{% for emp in employees %}
|
| 1224 |
+
<option value="{{ emp.id }}">{{ emp.name }}</option>
|
| 1225 |
+
{% endfor %}
|
| 1226 |
+
</select>
|
| 1227 |
+
</div>
|
| 1228 |
+
<div style="flex:1; min-width:150px;">
|
| 1229 |
+
<label>Дата</label>
|
| 1230 |
+
<input type="date" name="date" required>
|
| 1231 |
+
</div>
|
| 1232 |
+
<div style="flex:1; min-width:150px;">
|
| 1233 |
+
<label>Сумма</label>
|
| 1234 |
+
<input type="number" name="amount" required step="0.01">
|
| 1235 |
+
</div>
|
| 1236 |
+
<div style="flex:2; min-width:200px;">
|
| 1237 |
+
<label>Причина</label>
|
| 1238 |
+
<input type="text" name="reason" required>
|
| 1239 |
+
</div>
|
| 1240 |
+
<div style="display:flex; align-items:flex-end;">
|
| 1241 |
+
<button type="submit" class="btn btn-danger" style="height:44px;"><i class="fas fa-minus-circle"></i> Добавить</button>
|
| 1242 |
+
</div>
|
| 1243 |
+
</form>
|
| 1244 |
+
</div>
|
| 1245 |
+
|
| 1246 |
+
<h3 style="color:var(--primary); margin-top:20px;">Список штрафов</h3>
|
| 1247 |
+
<div style="overflow-x:auto;">
|
| 1248 |
+
<table class="rep-table" style="margin-bottom:20px;">
|
| 1249 |
+
<tr><th>Сотрудник</th><th>Дата</th><th>Причина</th><th>Сумма</th><th>Действие</th></tr>
|
| 1250 |
+
{% for fine in fines|sort(attribute='date', reverse=True) %}
|
| 1251 |
+
<tr>
|
| 1252 |
+
<td>
|
| 1253 |
+
{% for e in employees %}{% if e.id == fine.employee_id %}{{ e.name }}{% endif %}{% endfor %}
|
| 1254 |
+
</td>
|
| 1255 |
+
<td>{{ fine.date }}</td>
|
| 1256 |
+
<td>{{ fine.reason }}</td>
|
| 1257 |
+
<td style="color:var(--danger);">-{{ fine.amount }} {{ currency_code }}</td>
|
| 1258 |
+
<td>
|
| 1259 |
+
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить штраф?');">
|
| 1260 |
+
<input type="hidden" name="action" value="delete_fine">
|
| 1261 |
+
<input type="hidden" name="fine_id" value="{{ fine.id }}">
|
| 1262 |
+
<button type="submit" class="btn btn-danger" style="padding:5px 10px; border-radius:5px;"><i class="fas fa-trash"></i></button>
|
| 1263 |
+
</form>
|
| 1264 |
+
</td>
|
| 1265 |
+
</tr>
|
| 1266 |
+
{% endfor %}
|
| 1267 |
+
</table>
|
| 1268 |
+
</div>
|
| 1269 |
+
|
| 1270 |
+
<hr style="border:1px dashed var(--border); margin:20px 0;">
|
| 1271 |
+
<h3 style="color:var(--primary); margin-top:20px;">Отчет по ЗП</h3>
|
| 1272 |
+
|
| 1273 |
<div class="form-row" style="margin-bottom: 20px; align-items: flex-end;">
|
| 1274 |
<div>
|
| 1275 |
<label>Дата начала</label>
|
|
|
|
| 1327 |
employees.forEach(emp => {
|
| 1328 |
let daysWorked = 0;
|
| 1329 |
let fixedSal = 0;
|
| 1330 |
+
let bonusSal = 0;
|
| 1331 |
+
let totalFines = 0;
|
| 1332 |
+
|
| 1333 |
+
fines.forEach(f => {
|
| 1334 |
+
if (f.employee_id === emp.id && f.date >= start && f.date <= end) {
|
| 1335 |
+
totalFines += parseFloat(f.amount || 0);
|
| 1336 |
+
}
|
| 1337 |
+
});
|
| 1338 |
|
| 1339 |
let currDate = new Date(start);
|
| 1340 |
const endDate = new Date(end);
|
|
|
|
| 1345 |
const worked = workdays[dateStr] && workdays[dateStr].includes(emp.id);
|
| 1346 |
const dailySales = (empSales[emp.id] && empSales[emp.id][dateStr]) ? empSales[emp.id][dateStr] : 0;
|
| 1347 |
|
| 1348 |
+
if (worked) {
|
| 1349 |
daysWorked++;
|
| 1350 |
+
fixedSal += parseFloat(emp.daily_rate || 0);
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
const target = parseFloat(emp.target_amount || 0);
|
| 1354 |
+
const pct = parseFloat(emp.bonus_percentage || 0);
|
| 1355 |
+
|
| 1356 |
+
if (pct > 0 && dailySales > target) {
|
| 1357 |
+
bonusSal += (dailySales - target) * (pct / 100);
|
| 1358 |
}
|
| 1359 |
+
|
| 1360 |
currDate.setDate(currDate.getDate() + 1);
|
| 1361 |
}
|
| 1362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
const total = fixedSal + bonusSal - totalFines;
|
| 1364 |
+
if (daysWorked > 0 || fixedSal > 0 || bonusSal > 0 || totalFines > 0) {
|
| 1365 |
html += `<tr>
|
| 1366 |
<td>${emp.name}</td>
|
| 1367 |
<td>${daysWorked}</td>
|
|
|
|
| 1459 |
.orders-table th { padding: 12px; background: #1a1a1a; border-bottom: 2px solid var(--border); color: var(--primary); font-size: 0.85rem; text-transform: uppercase; }
|
| 1460 |
.orders-table td { padding: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; color: var(--text); }
|
| 1461 |
.orders-table tr:hover { background: #1a1a1a; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1462 |
|
| 1463 |
@media (max-width: 600px) {
|
| 1464 |
.header-panel { flex-direction: column; align-items: stretch; text-align: center; }
|
|
|
|
| 1527 |
<input type="hidden" name="action" value="add_employee">
|
| 1528 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="employee_name" required autocomplete="off"></div>
|
| 1529 |
<div style="flex:1; min-width:100px;"><label>PIN (4 цифры)</label><input type="text" name="pin" pattern="\\d{4}" required autocomplete="off"></div>
|
| 1530 |
+
<div style="flex:1; min-width:100px;"><label>Ставка за день</label><input type="number" name="daily_rate" value="0" required></div>
|
| 1531 |
+
<div style="flex:1; min-width:100px;"><label>Цель (n сумма)</label><input type="number" name="target_amount" value="0" required></div>
|
| 1532 |
+
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="0" step="0.1" required></div>
|
| 1533 |
<button type="submit" class="btn btn-dark" style="width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Добавить</button>
|
| 1534 |
</form>
|
| 1535 |
</div>
|
|
|
|
| 1548 |
<div style="flex:1; min-width:100px;"><label>Бонус %</label><input type="number" name="bonus_percentage" value="{{ emp.bonus_percentage }}" step="0.1" required></div>
|
| 1549 |
<button type="submit" class="btn btn-primary" title="Сохранить"><i class="fas fa-save"></i></button>
|
| 1550 |
</form>
|
| 1551 |
+
<form method="POST" style="margin-top:10px; text-align:right;" onsubmit="return confirm('Удалить сотрудника?');">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1552 |
<input type="hidden" name="action" value="delete_employee">
|
| 1553 |
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1554 |
+
<button type="submit" class="btn btn-danger"><i class="fas fa-trash"></i> Удалить</button>
|
| 1555 |
</form>
|
| 1556 |
</div>
|
| 1557 |
{% endfor %}
|
|
|
|
| 1962 |
currency_code=CURRENCY_CODE
|
| 1963 |
)
|
| 1964 |
|
| 1965 |
+
@app.route('/admin/salary', methods=['GET', 'POST'])
|
| 1966 |
def admin_salary():
|
| 1967 |
data = load_data()
|
| 1968 |
+
|
| 1969 |
+
if request.method == 'POST':
|
| 1970 |
+
action = request.form.get('action')
|
| 1971 |
+
if action == 'add_fine':
|
| 1972 |
+
fine = {
|
| 1973 |
+
'id': uuid4().hex,
|
| 1974 |
+
'employee_id': request.form.get('employee_id'),
|
| 1975 |
+
'date': request.form.get('date'),
|
| 1976 |
+
'amount': float(request.form.get('amount', 0)),
|
| 1977 |
+
'reason': request.form.get('reason', '')
|
| 1978 |
+
}
|
| 1979 |
+
if 'fines' not in data:
|
| 1980 |
+
data['fines'] = []
|
| 1981 |
+
data['fines'].append(fine)
|
| 1982 |
+
save_data(data)
|
| 1983 |
+
elif action == 'delete_fine':
|
| 1984 |
+
fine_id = request.form.get('fine_id')
|
| 1985 |
+
if 'fines' in data:
|
| 1986 |
+
data['fines'] = [f for f in data['fines'] if f.get('id') != fine_id]
|
| 1987 |
+
save_data(data)
|
| 1988 |
+
return redirect(url_for('admin_salary'))
|
| 1989 |
+
|
| 1990 |
orders = data.get('orders', {})
|
| 1991 |
employees = data.get('employees', [])
|
| 1992 |
workdays = data.get('workdays', {})
|
| 1993 |
+
fines = data.get('fines', [])
|
| 1994 |
|
| 1995 |
return render_template_string(
|
| 1996 |
SALARY_TEMPLATE,
|
|
|
|
| 1998 |
employees_json=json.dumps(employees),
|
| 1999 |
workdays_json=json.dumps(workdays),
|
| 2000 |
fines_json=json.dumps(fines),
|
| 2001 |
+
employees=employees,
|
| 2002 |
+
fines=fines,
|
| 2003 |
currency_code=CURRENCY_CODE
|
| 2004 |
)
|
| 2005 |
|
|
|
|
| 2010 |
categories = data.get('categories', [])
|
| 2011 |
orders = data.get('orders', {})
|
| 2012 |
employees = data.get('employees', [])
|
|
|
|
| 2013 |
settings = data.get('settings', {})
|
| 2014 |
|
| 2015 |
if request.method == 'POST':
|
|
|
|
| 2028 |
elif action == 'add_employee':
|
| 2029 |
emp_name = request.form.get('employee_name', '').strip()
|
| 2030 |
pin = request.form.get('pin', '0000').strip()
|
| 2031 |
+
try: daily_rate = float(request.form.get('daily_rate', 0))
|
| 2032 |
+
except: daily_rate = 0
|
| 2033 |
+
try: target_amount = float(request.form.get('target_amount', 0))
|
| 2034 |
+
except: target_amount = 0
|
| 2035 |
+
try: bonus_percentage = float(request.form.get('bonus_percentage', 0))
|
| 2036 |
+
except: bonus_percentage = 0
|
| 2037 |
|
| 2038 |
if emp_name:
|
| 2039 |
employees.append({
|
|
|
|
| 2053 |
if e.get('id') == emp_id:
|
| 2054 |
e['name'] = request.form.get('name', '').strip()
|
| 2055 |
e['pin'] = request.form.get('pin', '0000').strip()
|
| 2056 |
+
try: e['daily_rate'] = float(request.form.get('daily_rate', 0))
|
| 2057 |
+
except: e['daily_rate'] = 0
|
| 2058 |
+
try: e['target_amount'] = float(request.form.get('target_amount', 0))
|
| 2059 |
+
except: e['target_amount'] = 0
|
| 2060 |
+
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage', 0))
|
| 2061 |
+
except: e['bonus_percentage'] = 0
|
| 2062 |
break
|
| 2063 |
data['employees'] = employees
|
| 2064 |
save_data(data)
|
|
|
|
| 2068 |
data['employees'] = [e for e in employees if e.get('id') != emp_id]
|
| 2069 |
save_data(data)
|
| 2070 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2071 |
elif action == 'delete_order':
|
| 2072 |
order_id = request.form.get('order_id')
|
| 2073 |
if order_id in orders:
|
|
|
|
| 2212 |
categories=categories,
|
| 2213 |
orders=orders,
|
| 2214 |
employees=employees,
|
|
|
|
| 2215 |
repo_id=REPO_ID,
|
| 2216 |
currency_code=CURRENCY_CODE,
|
| 2217 |
settings=settings
|