Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -63,13 +63,21 @@ 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({
|
| 67 |
-
'
|
| 68 |
-
'
|
| 69 |
-
'
|
| 70 |
-
'
|
| 71 |
-
'
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
except Exception:
|
| 74 |
pass
|
| 75 |
success = False
|
|
@@ -180,9 +188,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
|
| 184 |
-
if 'target_amount' not in emp
|
| 185 |
-
if 'bonus_percentage' not in emp
|
| 186 |
|
| 187 |
if not os.path.exists(DATA_FILE):
|
| 188 |
try:
|
|
@@ -945,7 +953,7 @@ ORDER_TEMPLATE = '''
|
|
| 945 |
<div class="totals-area">
|
| 946 |
<div class="totals-row">
|
| 947 |
<span>Итого:</span>
|
| 948 |
-
<span>{{
|
| 949 |
</div>
|
| 950 |
{% set discount = order.discount|default(0)|float %}
|
| 951 |
{% if discount > 0 %}
|
|
@@ -1171,7 +1179,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>Отчет по ЗП
|
| 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,15 +1192,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-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
|
| 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,68 +1216,11 @@ SALARY_TEMPLATE = '''
|
|
| 1208 |
<body>
|
| 1209 |
<div class="container">
|
| 1210 |
<div class="header-panel">
|
| 1211 |
-
<h1><i class="fas fa-money-bill-wave"></i> Отчет по ЗП
|
| 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>
|
|
@@ -1285,6 +1236,44 @@ SALARY_TEMPLATE = '''
|
|
| 1285 |
<table class="rep-table" id="tableSalary"></table>
|
| 1286 |
</div>
|
| 1287 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1288 |
</div>
|
| 1289 |
|
| 1290 |
<script>
|
|
@@ -1304,6 +1293,7 @@ SALARY_TEMPLATE = '''
|
|
| 1304 |
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
| 1305 |
document.getElementById('salStart').value = getLocalDateStr(firstDay);
|
| 1306 |
document.getElementById('salEnd').value = todayStr;
|
|
|
|
| 1307 |
generateSalaryReport();
|
| 1308 |
}
|
| 1309 |
|
|
@@ -1312,7 +1302,7 @@ SALARY_TEMPLATE = '''
|
|
| 1312 |
const end = document.getElementById('salEnd').value;
|
| 1313 |
if (!start || !end) return;
|
| 1314 |
|
| 1315 |
-
let html = '<tr><th>Сотрудник</th><th>
|
| 1316 |
|
| 1317 |
let empSales = {};
|
| 1318 |
allOrders.forEach(o => {
|
|
@@ -1323,18 +1313,19 @@ SALARY_TEMPLATE = '''
|
|
| 1323 |
empSales[o.employee_id][oDate] += o.total_price;
|
| 1324 |
}
|
| 1325 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1326 |
|
| 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);
|
|
@@ -1347,36 +1338,67 @@ SALARY_TEMPLATE = '''
|
|
| 1347 |
|
| 1348 |
if (worked) {
|
| 1349 |
daysWorked++;
|
| 1350 |
-
fixedSal += parseFloat(emp.daily_rate ||
|
| 1351 |
}
|
| 1352 |
-
|
| 1353 |
-
const target = parseFloat(emp.target_amount ||
|
| 1354 |
-
const pct = parseFloat(emp.bonus_percentage ||
|
| 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 |
-
|
|
|
|
| 1365 |
html += `<tr>
|
| 1366 |
<td>${emp.name}</td>
|
| 1367 |
<td>${daysWorked}</td>
|
| 1368 |
-
<td>${fixedSal.toFixed(
|
| 1369 |
-
<td>${bonusSal.toFixed(
|
| 1370 |
-
<td style="color:var(--danger);">${totalFines.toFixed(
|
| 1371 |
-
<td style="font-weight:bold; color:var(--success);">${total.toFixed(
|
| 1372 |
</tr>`;
|
| 1373 |
}
|
| 1374 |
});
|
| 1375 |
|
| 1376 |
-
if (html === '<tr><th>Сотрудник</th><th>Дней отработано</th><th>Фикс. ЗП</th><th>Бонусы с продаж</th><th>Штрафы</th><th>Итого ЗП</th></tr>') {
|
| 1377 |
-
html += '<tr><td colspan="6">За выбранный период данных нет</td></tr>';
|
| 1378 |
-
}
|
| 1379 |
document.getElementById('tableSalary').innerHTML = html;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1380 |
}
|
| 1381 |
|
| 1382 |
window.onload = () => {
|
|
@@ -1527,9 +1549,9 @@ ADMIN_TEMPLATE = '''
|
|
| 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="
|
| 1531 |
-
<div style="flex:1; min-width:100px;"><label>Цель (
|
| 1532 |
-
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="
|
| 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>
|
|
@@ -1544,7 +1566,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1544 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="name" value="{{ emp.name }}" required></div>
|
| 1545 |
<div style="flex:1; min-width:100px;"><label>PIN</label><input type="text" name="pin" value="{{ emp.pin }}" pattern="\\d{4}" required></div>
|
| 1546 |
<div style="flex:1; min-width:100px;"><label>Ставка</label><input type="number" name="daily_rate" value="{{ emp.daily_rate }}" required></div>
|
| 1547 |
-
<div style="flex:1; min-width:100px;"><label>Цель (
|
| 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>
|
|
@@ -1969,22 +1991,29 @@ def admin_salary():
|
|
| 1969 |
if request.method == 'POST':
|
| 1970 |
action = request.form.get('action')
|
| 1971 |
if action == 'add_fine':
|
| 1972 |
-
|
| 1973 |
-
|
| 1974 |
-
|
| 1975 |
-
|
| 1976 |
-
|
| 1977 |
-
|
| 1978 |
-
|
| 1979 |
-
|
| 1980 |
-
|
| 1981 |
-
|
| 1982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1983 |
elif action == 'delete_fine':
|
| 1984 |
fine_id = request.form.get('fine_id')
|
| 1985 |
-
|
| 1986 |
-
|
| 1987 |
-
|
| 1988 |
return redirect(url_for('admin_salary'))
|
| 1989 |
|
| 1990 |
orders = data.get('orders', {})
|
|
@@ -1999,7 +2028,6 @@ def admin_salary():
|
|
| 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 |
|
|
@@ -2028,12 +2056,12 @@ def admin():
|
|
| 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',
|
| 2032 |
-
except: daily_rate =
|
| 2033 |
-
try: target_amount = float(request.form.get('target_amount',
|
| 2034 |
-
except: target_amount =
|
| 2035 |
-
try: bonus_percentage = float(request.form.get('bonus_percentage',
|
| 2036 |
-
except: bonus_percentage =
|
| 2037 |
|
| 2038 |
if emp_name:
|
| 2039 |
employees.append({
|
|
@@ -2053,12 +2081,12 @@ def admin():
|
|
| 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',
|
| 2057 |
-
except: e['daily_rate'] =
|
| 2058 |
-
try: e['target_amount'] = float(request.form.get('target_amount',
|
| 2059 |
-
except: e['target_amount'] =
|
| 2060 |
-
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage',
|
| 2061 |
-
except: e['bonus_percentage'] =
|
| 2062 |
break
|
| 2063 |
data['employees'] = employees
|
| 2064 |
save_data(data)
|
|
|
|
| 63 |
try:
|
| 64 |
if file_name == DATA_FILE:
|
| 65 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 66 |
+
json.dump({
|
| 67 |
+
'products': [],
|
| 68 |
+
'categories': [],
|
| 69 |
+
'orders': {},
|
| 70 |
+
'employees': [],
|
| 71 |
+
'workdays': {},
|
| 72 |
+
'fines': [],
|
| 73 |
+
'settings': {
|
| 74 |
+
'cafe_name': 'HongKong',
|
| 75 |
+
'wa_shift1': '+77470623684',
|
| 76 |
+
'wa_shift2': '+77470623684',
|
| 77 |
+
'active_shift': 1,
|
| 78 |
+
'logo_version': '1'
|
| 79 |
+
}
|
| 80 |
+
}, f)
|
| 81 |
except Exception:
|
| 82 |
pass
|
| 83 |
success = False
|
|
|
|
| 188 |
|
| 189 |
for emp in data['employees']:
|
| 190 |
if 'pin' not in emp: emp['pin'] = '0000'
|
| 191 |
+
if 'daily_rate' not in emp or emp['daily_rate'] == 0: emp['daily_rate'] = 230000
|
| 192 |
+
if 'target_amount' not in emp or emp['target_amount'] == 0: emp['target_amount'] = 1500000
|
| 193 |
+
if 'bonus_percentage' not in emp or emp['bonus_percentage'] == 0: emp['bonus_percentage'] = 10
|
| 194 |
|
| 195 |
if not os.path.exists(DATA_FILE):
|
| 196 |
try:
|
|
|
|
| 953 |
<div class="totals-area">
|
| 954 |
<div class="totals-row">
|
| 955 |
<span>Итого:</span>
|
| 956 |
+
<span>{{ order.total_price }}</span>
|
| 957 |
</div>
|
| 958 |
{% set discount = order.discount|default(0)|float %}
|
| 959 |
{% if discount > 0 %}
|
|
|
|
| 1179 |
<head>
|
| 1180 |
<meta charset="UTF-8">
|
| 1181 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 1182 |
+
<title>Отчет по ЗП</title>
|
| 1183 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1184 |
<style>
|
| 1185 |
:root { --primary: #D4AF37; --bg: #000000; --surface: #111111; --border: #333333; --danger: #ff4757; --success: #2ed573; --info: #D4AF37; --warning: #ffa502; --text: #ffffff; --text-muted: #888888; }
|
|
|
|
| 1192 |
.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; }
|
| 1193 |
.btn:active { opacity: 0.8; }
|
| 1194 |
.btn-success { background: var(--success); }
|
| 1195 |
+
.btn-danger { background: var(--danger); color: #fff; }
|
| 1196 |
.btn-dark { background: #1a1a1a; color: var(--primary); border: 1px solid var(--border); }
|
| 1197 |
|
| 1198 |
.report-section { background: var(--surface); padding: 20px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 20px; }
|
| 1199 |
|
| 1200 |
.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 1201 |
.form-row > * { flex: 1; min-width: 150px; }
|
| 1202 |
+
input[type="date"], input[type="number"], input[type="text"], select { 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); }
|
| 1203 |
+
input:focus, select:focus { border-color: var(--primary); background: #222; }
|
| 1204 |
label { display: block; margin-bottom: 5px; color: var(--text-muted); font-size: 0.85rem; font-weight: 600; }
|
| 1205 |
|
| 1206 |
.rep-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
|
|
| 1216 |
<body>
|
| 1217 |
<div class="container">
|
| 1218 |
<div class="header-panel">
|
| 1219 |
+
<h1><i class="fas fa-money-bill-wave"></i> Отчет по ЗП</h1>
|
| 1220 |
<a href="/admin" class="btn btn-dark"><i class="fas fa-arrow-left"></i> Назад в админку</a>
|
| 1221 |
</div>
|
| 1222 |
|
| 1223 |
<div class="report-section">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
<div class="form-row" style="margin-bottom: 20px; align-items: flex-end;">
|
| 1225 |
<div>
|
| 1226 |
<label>Дата начала</label>
|
|
|
|
| 1236 |
<table class="rep-table" id="tableSalary"></table>
|
| 1237 |
</div>
|
| 1238 |
</div>
|
| 1239 |
+
|
| 1240 |
+
<div class="report-section">
|
| 1241 |
+
<h2 style="color: var(--primary); margin-top: 0; margin-bottom: 15px;"><i class="fas fa-exclamation-triangle"></i> Добавить штраф</h2>
|
| 1242 |
+
<form method="POST" action="/admin/salary" style="display:flex; flex-direction:column; gap:12px;">
|
| 1243 |
+
<input type="hidden" name="action" value="add_fine">
|
| 1244 |
+
<div class="form-row">
|
| 1245 |
+
<div>
|
| 1246 |
+
<label>Сотрудник</label>
|
| 1247 |
+
<select name="employee_id" required>
|
| 1248 |
+
<option value="">Выберите сотрудника</option>
|
| 1249 |
+
{% for emp in employees %}
|
| 1250 |
+
<option value="{{ emp.id }}">{{ emp.name }}</option>
|
| 1251 |
+
{% endfor %}
|
| 1252 |
+
</select>
|
| 1253 |
+
</div>
|
| 1254 |
+
<div>
|
| 1255 |
+
<label>Сумма штрафа (сум)</label>
|
| 1256 |
+
<input type="number" name="amount" placeholder="Например, 50000" required>
|
| 1257 |
+
</div>
|
| 1258 |
+
<div>
|
| 1259 |
+
<label>Дата</label>
|
| 1260 |
+
<input type="date" name="date" id="fineDate" required>
|
| 1261 |
+
</div>
|
| 1262 |
+
</div>
|
| 1263 |
+
<div>
|
| 1264 |
+
<label>Причина штрафа</label>
|
| 1265 |
+
<input type="text" name="reason" placeholder="Причина..." required autocomplete="off">
|
| 1266 |
+
</div>
|
| 1267 |
+
<button type="submit" class="btn btn-danger" style="align-self: flex-start;"><i class="fas fa-plus"></i> Наложить штраф</button>
|
| 1268 |
+
</form>
|
| 1269 |
+
</div>
|
| 1270 |
+
|
| 1271 |
+
<div class="report-section">
|
| 1272 |
+
<h2 style="color: var(--primary); margin-top: 0; margin-bottom: 15px;"><i class="fas fa-list"></i> Список штрафов за период</h2>
|
| 1273 |
+
<div style="overflow-x:auto;">
|
| 1274 |
+
<table class="rep-table" id="tableFines"></table>
|
| 1275 |
+
</div>
|
| 1276 |
+
</div>
|
| 1277 |
</div>
|
| 1278 |
|
| 1279 |
<script>
|
|
|
|
| 1293 |
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
| 1294 |
document.getElementById('salStart').value = getLocalDateStr(firstDay);
|
| 1295 |
document.getElementById('salEnd').value = todayStr;
|
| 1296 |
+
document.getElementById('fineDate').value = todayStr;
|
| 1297 |
generateSalaryReport();
|
| 1298 |
}
|
| 1299 |
|
|
|
|
| 1302 |
const end = document.getElementById('salEnd').value;
|
| 1303 |
if (!start || !end) return;
|
| 1304 |
|
| 1305 |
+
let html = '<tr><th>Сотрудник</th><th>Отработано дней</th><th>Фикс. ЗП</th><th>Бонусы (>1.5M)</th><th>Штрафы</th><th>Итого ЗП</th></tr>';
|
| 1306 |
|
| 1307 |
let empSales = {};
|
| 1308 |
allOrders.forEach(o => {
|
|
|
|
| 1313 |
empSales[o.employee_id][oDate] += o.total_price;
|
| 1314 |
}
|
| 1315 |
});
|
| 1316 |
+
|
| 1317 |
+
let empFines = {};
|
| 1318 |
+
fines.forEach(f => {
|
| 1319 |
+
if (f.date >= start && f.date <= end) {
|
| 1320 |
+
if (!empFines[f.employee_id]) empFines[f.employee_id] = 0;
|
| 1321 |
+
empFines[f.employee_id] += parseFloat(f.amount || 0);
|
| 1322 |
+
}
|
| 1323 |
+
});
|
| 1324 |
|
| 1325 |
employees.forEach(emp => {
|
| 1326 |
let daysWorked = 0;
|
| 1327 |
let fixedSal = 0;
|
| 1328 |
let bonusSal = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
|
| 1330 |
let currDate = new Date(start);
|
| 1331 |
const endDate = new Date(end);
|
|
|
|
| 1338 |
|
| 1339 |
if (worked) {
|
| 1340 |
daysWorked++;
|
| 1341 |
+
fixedSal += parseFloat(emp.daily_rate || 230000);
|
| 1342 |
}
|
| 1343 |
+
|
| 1344 |
+
const target = parseFloat(emp.target_amount || 1500000);
|
| 1345 |
+
const pct = parseFloat(emp.bonus_percentage || 10);
|
| 1346 |
|
| 1347 |
if (pct > 0 && dailySales > target) {
|
| 1348 |
bonusSal += (dailySales - target) * (pct / 100);
|
| 1349 |
}
|
| 1350 |
+
|
| 1351 |
currDate.setDate(currDate.getDate() + 1);
|
| 1352 |
}
|
| 1353 |
+
|
| 1354 |
+
const totalFines = empFines[emp.id] || 0;
|
| 1355 |
const total = fixedSal + bonusSal - totalFines;
|
| 1356 |
+
|
| 1357 |
+
if (daysWorked > 0 || total !== 0 || bonusSal > 0 || totalFines > 0) {
|
| 1358 |
html += `<tr>
|
| 1359 |
<td>${emp.name}</td>
|
| 1360 |
<td>${daysWorked}</td>
|
| 1361 |
+
<td>${fixedSal.toFixed(0)} ${currencyCode}</td>
|
| 1362 |
+
<td>${bonusSal.toFixed(0)} ${currencyCode}</td>
|
| 1363 |
+
<td style="color:var(--danger);">${totalFines.toFixed(0)} ${currencyCode}</td>
|
| 1364 |
+
<td style="font-weight:bold; color:var(--success);">${total.toFixed(0)} ${currencyCode}</td>
|
| 1365 |
</tr>`;
|
| 1366 |
}
|
| 1367 |
});
|
| 1368 |
|
|
|
|
|
|
|
|
|
|
| 1369 |
document.getElementById('tableSalary').innerHTML = html;
|
| 1370 |
+
renderFinesTable(start, end);
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
function renderFinesTable(start, end) {
|
| 1374 |
+
let html = '<tr><th>Дата</th><th>Сотрудник</th><th>Причина</th><th>Сумма</th><th>Действие</th></tr>';
|
| 1375 |
+
let count = 0;
|
| 1376 |
+
|
| 1377 |
+
fines.forEach(f => {
|
| 1378 |
+
if (f.date >= start && f.date <= end) {
|
| 1379 |
+
count++;
|
| 1380 |
+
const emp = employees.find(e => e.id === f.employee_id);
|
| 1381 |
+
const empName = emp ? emp.name : 'Удален';
|
| 1382 |
+
html += `<tr>
|
| 1383 |
+
<td>${f.date}</td>
|
| 1384 |
+
<td>${empName}</td>
|
| 1385 |
+
<td>${f.reason}</td>
|
| 1386 |
+
<td style="color:var(--danger); font-weight:bold;">-${f.amount} ${currencyCode}</td>
|
| 1387 |
+
<td>
|
| 1388 |
+
<form method="POST" action="/admin/salary" style="margin:0;" onsubmit="return confirm('Удалить штраф?');">
|
| 1389 |
+
<input type="hidden" name="action" value="delete_fine">
|
| 1390 |
+
<input type="hidden" name="fine_id" value="${f.id}">
|
| 1391 |
+
<button type="submit" class="btn btn-danger" style="padding:4px 8px; font-size:0.8rem;"><i class="fas fa-trash"></i></button>
|
| 1392 |
+
</form>
|
| 1393 |
+
</td>
|
| 1394 |
+
</tr>`;
|
| 1395 |
+
}
|
| 1396 |
+
});
|
| 1397 |
+
|
| 1398 |
+
if (count === 0) {
|
| 1399 |
+
html += '<tr><td colspan="5">Штрафов за выбранный период нет</td></tr>';
|
| 1400 |
+
}
|
| 1401 |
+
document.getElementById('tableFines').innerHTML = html;
|
| 1402 |
}
|
| 1403 |
|
| 1404 |
window.onload = () => {
|
|
|
|
| 1549 |
<input type="hidden" name="action" value="add_employee">
|
| 1550 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="employee_name" required autocomplete="off"></div>
|
| 1551 |
<div style="flex:1; min-width:100px;"><label>PIN (4 цифры)</label><input type="text" name="pin" pattern="\\d{4}" required autocomplete="off"></div>
|
| 1552 |
+
<div style="flex:1; min-width:100px;"><label>Ставка за день</label><input type="number" name="daily_rate" value="230000" required></div>
|
| 1553 |
+
<div style="flex:1; min-width:100px;"><label>Цель (план)</label><input type="number" name="target_amount" value="1500000" required></div>
|
| 1554 |
+
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="10" step="0.1" required></div>
|
| 1555 |
<button type="submit" class="btn btn-dark" style="width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Добавить</button>
|
| 1556 |
</form>
|
| 1557 |
</div>
|
|
|
|
| 1566 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="name" value="{{ emp.name }}" required></div>
|
| 1567 |
<div style="flex:1; min-width:100px;"><label>PIN</label><input type="text" name="pin" value="{{ emp.pin }}" pattern="\\d{4}" required></div>
|
| 1568 |
<div style="flex:1; min-width:100px;"><label>Ставка</label><input type="number" name="daily_rate" value="{{ emp.daily_rate }}" required></div>
|
| 1569 |
+
<div style="flex:1; min-width:100px;"><label>Цель (план)</label><input type="number" name="target_amount" value="{{ emp.target_amount }}" required></div>
|
| 1570 |
<div style="flex:1; min-width:100px;"><label>Бонус %</label><input type="number" name="bonus_percentage" value="{{ emp.bonus_percentage }}" step="0.1" required></div>
|
| 1571 |
<button type="submit" class="btn btn-primary" title="Сохранить"><i class="fas fa-save"></i></button>
|
| 1572 |
</form>
|
|
|
|
| 1991 |
if request.method == 'POST':
|
| 1992 |
action = request.form.get('action')
|
| 1993 |
if action == 'add_fine':
|
| 1994 |
+
emp_id = request.form.get('employee_id')
|
| 1995 |
+
try: amount = float(request.form.get('amount', 0))
|
| 1996 |
+
except: amount = 0
|
| 1997 |
+
reason = request.form.get('reason', '').strip()
|
| 1998 |
+
date_str = request.form.get('date', get_current_date())
|
| 1999 |
+
|
| 2000 |
+
if emp_id and amount > 0:
|
| 2001 |
+
fines = data.get('fines', [])
|
| 2002 |
+
fines.append({
|
| 2003 |
+
'id': uuid4().hex,
|
| 2004 |
+
'employee_id': emp_id,
|
| 2005 |
+
'amount': amount,
|
| 2006 |
+
'reason': reason,
|
| 2007 |
+
'date': date_str
|
| 2008 |
+
})
|
| 2009 |
+
data['fines'] = fines
|
| 2010 |
+
save_data(data)
|
| 2011 |
+
|
| 2012 |
elif action == 'delete_fine':
|
| 2013 |
fine_id = request.form.get('fine_id')
|
| 2014 |
+
data['fines'] = [f for f in data.get('fines', []) if f.get('id') != fine_id]
|
| 2015 |
+
save_data(data)
|
| 2016 |
+
|
| 2017 |
return redirect(url_for('admin_salary'))
|
| 2018 |
|
| 2019 |
orders = data.get('orders', {})
|
|
|
|
| 2028 |
workdays_json=json.dumps(workdays),
|
| 2029 |
fines_json=json.dumps(fines),
|
| 2030 |
employees=employees,
|
|
|
|
| 2031 |
currency_code=CURRENCY_CODE
|
| 2032 |
)
|
| 2033 |
|
|
|
|
| 2056 |
elif action == 'add_employee':
|
| 2057 |
emp_name = request.form.get('employee_name', '').strip()
|
| 2058 |
pin = request.form.get('pin', '0000').strip()
|
| 2059 |
+
try: daily_rate = float(request.form.get('daily_rate', 230000))
|
| 2060 |
+
except: daily_rate = 230000
|
| 2061 |
+
try: target_amount = float(request.form.get('target_amount', 1500000))
|
| 2062 |
+
except: target_amount = 1500000
|
| 2063 |
+
try: bonus_percentage = float(request.form.get('bonus_percentage', 10))
|
| 2064 |
+
except: bonus_percentage = 10
|
| 2065 |
|
| 2066 |
if emp_name:
|
| 2067 |
employees.append({
|
|
|
|
| 2081 |
if e.get('id') == emp_id:
|
| 2082 |
e['name'] = request.form.get('name', '').strip()
|
| 2083 |
e['pin'] = request.form.get('pin', '0000').strip()
|
| 2084 |
+
try: e['daily_rate'] = float(request.form.get('daily_rate', 230000))
|
| 2085 |
+
except: e['daily_rate'] = 230000
|
| 2086 |
+
try: e['target_amount'] = float(request.form.get('target_amount', 1500000))
|
| 2087 |
+
except: e['target_amount'] = 1500000
|
| 2088 |
+
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage', 10))
|
| 2089 |
+
except: e['bonus_percentage'] = 10
|
| 2090 |
break
|
| 2091 |
data['employees'] = employees
|
| 2092 |
save_data(data)
|