Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -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:
|
|
@@ -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"], 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); }
|
| 1194 |
-
input[type="date"]:focus, input[type="number"]:focus, input[type="text"]: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; }
|
|
@@ -1212,6 +1213,64 @@ SALARY_TEMPLATE = '''
|
|
| 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>
|
|
@@ -1227,49 +1286,13 @@ SALARY_TEMPLATE = '''
|
|
| 1227 |
<table class="rep-table" id="tableSalary"></table>
|
| 1228 |
</div>
|
| 1229 |
</div>
|
| 1230 |
-
|
| 1231 |
-
<div class="report-section" style="margin-top:20px;">
|
| 1232 |
-
<h2 style="color:var(--primary); margin-top:0;">Штрафы</h2>
|
| 1233 |
-
|
| 1234 |
-
<div style="background:var(--surface); border:1px solid var(--border); padding:15px; border-radius:10px; margin-bottom:20px;">
|
| 1235 |
-
<h3 style="margin-top:0; color:var(--danger);">Добавить штраф</h3>
|
| 1236 |
-
<form method="POST" style="display:flex; flex-wrap:wrap; gap:10px;">
|
| 1237 |
-
<input type="hidden" name="action" value="add_fine">
|
| 1238 |
-
<div style="flex:1; min-width:150px;">
|
| 1239 |
-
<label>Сотрудник</label>
|
| 1240 |
-
<select name="employee_id" required style="width:100%; padding:12px; border:1px solid var(--border); border-radius:10px; background:#1a1a1a; color:var(--text); outline:none;">
|
| 1241 |
-
{% for emp in employees %}
|
| 1242 |
-
<option value="{{ emp.id }}">{{ emp.name }}</option>
|
| 1243 |
-
{% endfor %}
|
| 1244 |
-
</select>
|
| 1245 |
-
</div>
|
| 1246 |
-
<div style="flex:1; min-width:150px;">
|
| 1247 |
-
<label>Дата</label>
|
| 1248 |
-
<input type="date" name="date" required id="fineDateInput">
|
| 1249 |
-
</div>
|
| 1250 |
-
<div style="flex:1; min-width:100px;">
|
| 1251 |
-
<label>Сумма</label>
|
| 1252 |
-
<input type="number" name="amount" required>
|
| 1253 |
-
</div>
|
| 1254 |
-
<div style="flex:2; min-width:200px;">
|
| 1255 |
-
<label>Причина</label>
|
| 1256 |
-
<input type="text" name="reason" required autocomplete="off">
|
| 1257 |
-
</div>
|
| 1258 |
-
<button type="submit" class="btn" style="background:var(--danger); color:#fff; width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Назначить штраф</button>
|
| 1259 |
-
</form>
|
| 1260 |
-
</div>
|
| 1261 |
-
|
| 1262 |
-
<div style="overflow-x:auto;">
|
| 1263 |
-
<table class="rep-table" id="tableFines"></table>
|
| 1264 |
-
</div>
|
| 1265 |
-
</div>
|
| 1266 |
</div>
|
| 1267 |
|
| 1268 |
<script>
|
| 1269 |
const allOrders = {{ orders_json|safe }};
|
| 1270 |
const workdays = {{ workdays_json|safe }};
|
| 1271 |
const employees = {{ employees_json|safe }};
|
| 1272 |
-
const
|
| 1273 |
const currencyCode = '{{ currency_code }}';
|
| 1274 |
|
| 1275 |
function getLocalDateStr(d) {
|
|
@@ -1282,7 +1305,8 @@ SALARY_TEMPLATE = '''
|
|
| 1282 |
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
| 1283 |
document.getElementById('salStart').value = getLocalDateStr(firstDay);
|
| 1284 |
document.getElementById('salEnd').value = todayStr;
|
| 1285 |
-
document.getElementById('
|
|
|
|
| 1286 |
generateSalaryReport();
|
| 1287 |
}
|
| 1288 |
|
|
@@ -1291,7 +1315,7 @@ SALARY_TEMPLATE = '''
|
|
| 1291 |
const end = document.getElementById('salEnd').value;
|
| 1292 |
if (!start || !end) return;
|
| 1293 |
|
| 1294 |
-
let html = '<tr><th>Сотрудник</th><th>Дней
|
| 1295 |
|
| 1296 |
let empSales = {};
|
| 1297 |
allOrders.forEach(o => {
|
|
@@ -1307,13 +1331,14 @@ SALARY_TEMPLATE = '''
|
|
| 1307 |
let daysWorked = 0;
|
| 1308 |
let fixedSal = 0;
|
| 1309 |
let bonusSal = 0;
|
| 1310 |
-
let
|
| 1311 |
|
| 1312 |
let currDate = new Date(start);
|
| 1313 |
const endDate = new Date(end);
|
| 1314 |
|
| 1315 |
while(currDate <= endDate) {
|
| 1316 |
const dateStr = getLocalDateStr(currDate);
|
|
|
|
| 1317 |
const dailySales = (empSales[emp.id] && empSales[emp.id][dateStr]) ? empSales[emp.id][dateStr] : 0;
|
| 1318 |
|
| 1319 |
if (dailySales > 0) {
|
|
@@ -1329,58 +1354,30 @@ SALARY_TEMPLATE = '''
|
|
| 1329 |
}
|
| 1330 |
currDate.setDate(currDate.getDate() + 1);
|
| 1331 |
}
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
if (f.employee_id === emp.id && f.date >= start && f.date <= end) {
|
| 1335 |
-
|
| 1336 |
}
|
| 1337 |
});
|
| 1338 |
|
| 1339 |
-
const total = fixedSal + bonusSal -
|
| 1340 |
-
if (daysWorked > 0 || total
|
| 1341 |
html += `<tr>
|
| 1342 |
<td>${emp.name}</td>
|
| 1343 |
<td>${daysWorked}</td>
|
| 1344 |
<td>${fixedSal.toFixed(2)} ${currencyCode}</td>
|
| 1345 |
<td>${bonusSal.toFixed(2)} ${currencyCode}</td>
|
| 1346 |
-
<td style="color:var(--danger);">${
|
| 1347 |
<td style="font-weight:bold; color:var(--success);">${total.toFixed(2)} ${currencyCode}</td>
|
| 1348 |
</tr>`;
|
| 1349 |
}
|
| 1350 |
});
|
| 1351 |
|
| 1352 |
-
if (html === '<tr><th>Сотрудник</th><th>Дней
|
| 1353 |
html += '<tr><td colspan="6">За выбранный период данных нет</td></tr>';
|
| 1354 |
}
|
| 1355 |
document.getElementById('tableSalary').innerHTML = html;
|
| 1356 |
-
renderFinesTable(start, end);
|
| 1357 |
-
}
|
| 1358 |
-
|
| 1359 |
-
function renderFinesTable(start, end) {
|
| 1360 |
-
let html = '<tr><th>Дата</th><th>Сотрудник</th><th>Сумма</th><th>Причина</th><th>Действие</th></tr>';
|
| 1361 |
-
let found = false;
|
| 1362 |
-
allFines.forEach(f => {
|
| 1363 |
-
if (f.date >= start && f.date <= end) {
|
| 1364 |
-
found = true;
|
| 1365 |
-
const emp = employees.find(e => e.id === f.employee_id);
|
| 1366 |
-
const empName = emp ? emp.name : 'Удален';
|
| 1367 |
-
html += `<tr>
|
| 1368 |
-
<td>${f.date}</td>
|
| 1369 |
-
<td>${empName}</td>
|
| 1370 |
-
<td style="color:var(--danger);">${f.amount} ${currencyCode}</td>
|
| 1371 |
-
<td>${f.reason}</td>
|
| 1372 |
-
<td>
|
| 1373 |
-
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить штраф?');">
|
| 1374 |
-
<input type="hidden" name="action" value="delete_fine">
|
| 1375 |
-
<input type="hidden" name="fine_id" value="${f.id}">
|
| 1376 |
-
<button type="submit" style="background:var(--danger); color:#fff; border:none; padding:5px 10px; border-radius:5px; cursor:pointer;"><i class="fas fa-trash"></i></button>
|
| 1377 |
-
</form>
|
| 1378 |
-
</td>
|
| 1379 |
-
</tr>`;
|
| 1380 |
-
}
|
| 1381 |
-
});
|
| 1382 |
-
if (!found) html += '<tr><td colspan="5">Штрафов за период нет</td></tr>';
|
| 1383 |
-
document.getElementById('tableFines').innerHTML = html;
|
| 1384 |
}
|
| 1385 |
|
| 1386 |
window.onload = () => {
|
|
@@ -1530,9 +1527,9 @@ ADMIN_TEMPLATE = '''
|
|
| 1530 |
<form method="POST" style="display:flex; flex-wrap:wrap; gap:10px;">
|
| 1531 |
<input type="hidden" name="action" value="add_employee">
|
| 1532 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="employee_name" required autocomplete="off"></div>
|
| 1533 |
-
<div style="flex:1; min-width:100px;"><label>PIN (4 цифры)</label><input type="text" name="pin" pattern="\d{4}" required autocomplete="off"></div>
|
| 1534 |
<div style="flex:1; min-width:100px;"><label>Ставка за день</label><input type="number" name="daily_rate" value="230000" required></div>
|
| 1535 |
-
<div style="flex:1; min-width:100px;"><label>Цель (n сумма)</label><input type="number" name="target_amount" value="
|
| 1536 |
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="10" step="0.1" required></div>
|
| 1537 |
<button type="submit" class="btn btn-dark" style="width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Добавить</button>
|
| 1538 |
</form>
|
|
@@ -1546,7 +1543,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1546 |
<input type="hidden" name="action" value="edit_employee">
|
| 1547 |
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1548 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="name" value="{{ emp.name }}" required></div>
|
| 1549 |
-
<div style="flex:1; min-width:100px;"><label>PIN</label><input type="text" name="pin" value="{{ emp.pin }}" pattern="\d{4}" required></div>
|
| 1550 |
<div style="flex:1; min-width:100px;"><label>Ставка</label><input type="number" name="daily_rate" value="{{ emp.daily_rate }}" required></div>
|
| 1551 |
<div style="flex:1; min-width:100px;"><label>Цель (n)</label><input type="number" name="target_amount" value="{{ emp.target_amount }}" required></div>
|
| 1552 |
<div style="flex:1; min-width:100px;"><label>Бонус %</label><input type="number" name="bonus_percentage" value="{{ emp.bonus_percentage }}" step="0.1" required></div>
|
|
@@ -1969,40 +1966,43 @@ def admin_reports():
|
|
| 1969 |
@app.route('/admin/salary', methods=['GET', 'POST'])
|
| 1970 |
def admin_salary():
|
| 1971 |
data = load_data()
|
| 1972 |
-
|
| 1973 |
-
employees = data.get('employees', [])
|
| 1974 |
-
workdays = data.get('workdays', {})
|
| 1975 |
-
fines = data.get('fines', [])
|
| 1976 |
-
|
| 1977 |
if request.method == 'POST':
|
| 1978 |
action = request.form.get('action')
|
| 1979 |
if action == 'add_fine':
|
| 1980 |
emp_id = request.form.get('employee_id')
|
| 1981 |
-
|
| 1982 |
amount = float(request.form.get('amount', 0))
|
| 1983 |
-
reason = request.form.get('reason', '')
|
| 1984 |
-
fines.append({
|
| 1985 |
'id': uuid4().hex,
|
| 1986 |
'employee_id': emp_id,
|
| 1987 |
-
'date':
|
| 1988 |
'amount': amount,
|
| 1989 |
'reason': reason
|
| 1990 |
})
|
| 1991 |
-
data['fines'] = fines
|
| 1992 |
save_data(data)
|
| 1993 |
return redirect(url_for('admin_salary'))
|
| 1994 |
elif action == 'delete_fine':
|
| 1995 |
fine_id = request.form.get('fine_id')
|
| 1996 |
-
data['fines'] = [f for f in fines if f.get('id') != fine_id]
|
| 1997 |
save_data(data)
|
| 1998 |
return redirect(url_for('admin_salary'))
|
| 1999 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2000 |
return render_template_string(
|
| 2001 |
SALARY_TEMPLATE,
|
| 2002 |
orders_json=json.dumps(list(orders.values())),
|
| 2003 |
employees_json=json.dumps(employees),
|
| 2004 |
workdays_json=json.dumps(workdays),
|
| 2005 |
-
fines_json=json.dumps(
|
|
|
|
| 2006 |
employees=employees,
|
| 2007 |
currency_code=CURRENCY_CODE
|
| 2008 |
)
|
|
@@ -2033,11 +2033,11 @@ def admin():
|
|
| 2033 |
emp_name = request.form.get('employee_name', '').strip()
|
| 2034 |
pin = request.form.get('pin', '0000').strip()
|
| 2035 |
try: daily_rate = float(request.form.get('daily_rate', 0))
|
| 2036 |
-
except: daily_rate =
|
| 2037 |
try: target_amount = float(request.form.get('target_amount', 0))
|
| 2038 |
-
except: target_amount =
|
| 2039 |
try: bonus_percentage = float(request.form.get('bonus_percentage', 0))
|
| 2040 |
-
except: bonus_percentage =
|
| 2041 |
|
| 2042 |
if emp_name:
|
| 2043 |
employees.append({
|
|
@@ -2058,11 +2058,11 @@ def admin():
|
|
| 2058 |
e['name'] = request.form.get('name', '').strip()
|
| 2059 |
e['pin'] = request.form.get('pin', '0000').strip()
|
| 2060 |
try: e['daily_rate'] = float(request.form.get('daily_rate', 0))
|
| 2061 |
-
except: e['daily_rate'] =
|
| 2062 |
try: e['target_amount'] = float(request.form.get('target_amount', 0))
|
| 2063 |
-
except: e['target_amount'] =
|
| 2064 |
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage', 0))
|
| 2065 |
-
except: e['bonus_percentage'] =
|
| 2066 |
break
|
| 2067 |
data['employees'] = employees
|
| 2068 |
save_data(data)
|
|
@@ -2239,4 +2239,4 @@ if __name__ == '__main__':
|
|
| 2239 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 2240 |
|
| 2241 |
port = int(os.environ.get('PORT', 7860))
|
| 2242 |
-
app.run(host='0.0.0.0', port=port)
|
|
|
|
| 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'] = 230000
|
| 184 |
+
if 'target_amount' not in emp: emp['target_amount'] = 1500000
|
| 185 |
+
if 'bonus_percentage' not in emp: emp['bonus_percentage'] = 10
|
| 186 |
|
| 187 |
if not os.path.exists(DATA_FILE):
|
| 188 |
try:
|
|
|
|
| 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"], 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); }
|
| 1195 |
+
input[type="date"]:focus, input[type="number"]:focus, input[type="text"]:focus, select: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; }
|
|
|
|
| 1213 |
</div>
|
| 1214 |
|
| 1215 |
<div class="report-section">
|
| 1216 |
+
<h2 style="margin-top:0; color:var(--primary); font-size:1.2rem; margin-bottom: 15px;">Управление штрафами</h2>
|
| 1217 |
+
<form method="POST" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
| 1218 |
+
<input type="hidden" name="action" value="add_fine">
|
| 1219 |
+
<div style="flex:1; min-width:150px;">
|
| 1220 |
+
<label>Сотрудник</label>
|
| 1221 |
+
<select name="employee_id" required>
|
| 1222 |
+
{% for emp in employees %}
|
| 1223 |
+
<option value="{{ emp.id }}">{{ emp.name }}</option>
|
| 1224 |
+
{% endfor %}
|
| 1225 |
+
</select>
|
| 1226 |
+
</div>
|
| 1227 |
+
<div style="flex:1; min-width:120px;">
|
| 1228 |
+
<label>Дата</label>
|
| 1229 |
+
<input type="date" name="date" required id="fineDate">
|
| 1230 |
+
</div>
|
| 1231 |
+
<div style="flex:1; min-width:120px;">
|
| 1232 |
+
<label>Сумма</label>
|
| 1233 |
+
<input type="number" name="amount" required step="1" min="0">
|
| 1234 |
+
</div>
|
| 1235 |
+
<div style="flex:2; min-width:150px;">
|
| 1236 |
+
<label>Причина</label>
|
| 1237 |
+
<input type="text" name="reason" required autocomplete="off">
|
| 1238 |
+
</div>
|
| 1239 |
+
<button type="submit" class="btn btn-danger" style="height:44px; color:#fff;"><i class="fas fa-plus"></i> Добавить штраф</button>
|
| 1240 |
+
</form>
|
| 1241 |
+
|
| 1242 |
+
<h3 style="margin-top:25px; margin-bottom:10px; font-size:1.1rem; color:var(--primary);">История штрафов</h3>
|
| 1243 |
+
<div style="overflow-x:auto;">
|
| 1244 |
+
<table class="rep-table">
|
| 1245 |
+
<tr><th>Сотрудник</th><th>Дата</th><th>Причина</th><th>Сумма</th><th>Действия</th></tr>
|
| 1246 |
+
{% for fine in fines %}
|
| 1247 |
+
<tr>
|
| 1248 |
+
<td>
|
| 1249 |
+
{% for emp in employees %}
|
| 1250 |
+
{% if emp.id == fine.employee_id %}{{ emp.name }}{% endif %}
|
| 1251 |
+
{% endfor %}
|
| 1252 |
+
</td>
|
| 1253 |
+
<td>{{ fine.date }}</td>
|
| 1254 |
+
<td>{{ fine.reason }}</td>
|
| 1255 |
+
<td style="color:var(--danger); font-weight:bold;">-{{ fine.amount }} {{ currency_code }}</td>
|
| 1256 |
+
<td>
|
| 1257 |
+
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить штраф?');">
|
| 1258 |
+
<input type="hidden" name="action" value="delete_fine">
|
| 1259 |
+
<input type="hidden" name="fine_id" value="{{ fine.id }}">
|
| 1260 |
+
<button type="submit" class="btn btn-dark" style="padding:5px 10px; border-color:var(--danger); color:var(--danger);"><i class="fas fa-trash"></i></button>
|
| 1261 |
+
</form>
|
| 1262 |
+
</td>
|
| 1263 |
+
</tr>
|
| 1264 |
+
{% endfor %}
|
| 1265 |
+
{% if not fines %}
|
| 1266 |
+
<tr><td colspan="5" style="text-align:center; padding:15px; color:var(--text-muted);">Нет штрафов</td></tr>
|
| 1267 |
+
{% endif %}
|
| 1268 |
+
</table>
|
| 1269 |
+
</div>
|
| 1270 |
+
</div>
|
| 1271 |
+
|
| 1272 |
+
<div class="report-section">
|
| 1273 |
+
<h2 style="margin-top:0; color:var(--primary); font-size:1.2rem; margin-bottom: 15px;">Генерация Зарплаты</h2>
|
| 1274 |
<div class="form-row" style="margin-bottom: 20px; align-items: flex-end;">
|
| 1275 |
<div>
|
| 1276 |
<label>Дата начала</label>
|
|
|
|
| 1286 |
<table class="rep-table" id="tableSalary"></table>
|
| 1287 |
</div>
|
| 1288 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
</div>
|
| 1290 |
|
| 1291 |
<script>
|
| 1292 |
const allOrders = {{ orders_json|safe }};
|
| 1293 |
const workdays = {{ workdays_json|safe }};
|
| 1294 |
const employees = {{ employees_json|safe }};
|
| 1295 |
+
const fines = {{ fines_json|safe }};
|
| 1296 |
const currencyCode = '{{ currency_code }}';
|
| 1297 |
|
| 1298 |
function getLocalDateStr(d) {
|
|
|
|
| 1305 |
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
| 1306 |
document.getElementById('salStart').value = getLocalDateStr(firstDay);
|
| 1307 |
document.getElementById('salEnd').value = todayStr;
|
| 1308 |
+
const fineDateInput = document.getElementById('fineDate');
|
| 1309 |
+
if(fineDateInput) { fineDateInput.value = todayStr; }
|
| 1310 |
generateSalaryReport();
|
| 1311 |
}
|
| 1312 |
|
|
|
|
| 1315 |
const end = document.getElementById('salEnd').value;
|
| 1316 |
if (!start || !end) return;
|
| 1317 |
|
| 1318 |
+
let html = '<tr><th>Сотрудник</th><th>Дней отработано</th><th>Фикс. ЗП</th><th>Бонусы с продаж</th><th>Штрафы</th><th>Итого ЗП</th></tr>';
|
| 1319 |
|
| 1320 |
let empSales = {};
|
| 1321 |
allOrders.forEach(o => {
|
|
|
|
| 1331 |
let daysWorked = 0;
|
| 1332 |
let fixedSal = 0;
|
| 1333 |
let bonusSal = 0;
|
| 1334 |
+
let totalFines = 0;
|
| 1335 |
|
| 1336 |
let currDate = new Date(start);
|
| 1337 |
const endDate = new Date(end);
|
| 1338 |
|
| 1339 |
while(currDate <= endDate) {
|
| 1340 |
const dateStr = getLocalDateStr(currDate);
|
| 1341 |
+
|
| 1342 |
const dailySales = (empSales[emp.id] && empSales[emp.id][dateStr]) ? empSales[emp.id][dateStr] : 0;
|
| 1343 |
|
| 1344 |
if (dailySales > 0) {
|
|
|
|
| 1354 |
}
|
| 1355 |
currDate.setDate(currDate.getDate() + 1);
|
| 1356 |
}
|
| 1357 |
+
|
| 1358 |
+
fines.forEach(f => {
|
| 1359 |
if (f.employee_id === emp.id && f.date >= start && f.date <= end) {
|
| 1360 |
+
totalFines += parseFloat(f.amount);
|
| 1361 |
}
|
| 1362 |
});
|
| 1363 |
|
| 1364 |
+
const total = fixedSal + bonusSal - totalFines;
|
| 1365 |
+
if (daysWorked > 0 || total > 0 || totalFines > 0) {
|
| 1366 |
html += `<tr>
|
| 1367 |
<td>${emp.name}</td>
|
| 1368 |
<td>${daysWorked}</td>
|
| 1369 |
<td>${fixedSal.toFixed(2)} ${currencyCode}</td>
|
| 1370 |
<td>${bonusSal.toFixed(2)} ${currencyCode}</td>
|
| 1371 |
+
<td style="color:var(--danger);">-${totalFines.toFixed(2)} ${currencyCode}</td>
|
| 1372 |
<td style="font-weight:bold; color:var(--success);">${total.toFixed(2)} ${currencyCode}</td>
|
| 1373 |
</tr>`;
|
| 1374 |
}
|
| 1375 |
});
|
| 1376 |
|
| 1377 |
+
if (html === '<tr><th>Сотрудник</th><th>Дней отработано</th><th>Фикс. ЗП</th><th>Бонусы с продаж</th><th>Штрафы</th><th>Итого ЗП</th></tr>') {
|
| 1378 |
html += '<tr><td colspan="6">За выбранный период данных нет</td></tr>';
|
| 1379 |
}
|
| 1380 |
document.getElementById('tableSalary').innerHTML = html;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1381 |
}
|
| 1382 |
|
| 1383 |
window.onload = () => {
|
|
|
|
| 1527 |
<form method="POST" style="display:flex; flex-wrap:wrap; gap:10px;">
|
| 1528 |
<input type="hidden" name="action" value="add_employee">
|
| 1529 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="employee_name" required autocomplete="off"></div>
|
| 1530 |
+
<div style="flex:1; min-width:100px;"><label>PIN (4 цифры)</label><input type="text" name="pin" pattern="\\d{4}" required autocomplete="off"></div>
|
| 1531 |
<div style="flex:1; min-width:100px;"><label>Ставка за день</label><input type="number" name="daily_rate" value="230000" required></div>
|
| 1532 |
+
<div style="flex:1; min-width:100px;"><label>Цель (n сумма)</label><input type="number" name="target_amount" value="1500000" required></div>
|
| 1533 |
<div style="flex:1; min-width:100px;"><label>Бонус (%)</label><input type="number" name="bonus_percentage" value="10" step="0.1" required></div>
|
| 1534 |
<button type="submit" class="btn btn-dark" style="width:100%; margin-top:10px;"><i class="fas fa-plus"></i> Добавить</button>
|
| 1535 |
</form>
|
|
|
|
| 1543 |
<input type="hidden" name="action" value="edit_employee">
|
| 1544 |
<input type="hidden" name="employee_id" value="{{ emp.id }}">
|
| 1545 |
<div style="flex:1; min-width:150px;"><label>Имя</label><input type="text" name="name" value="{{ emp.name }}" required></div>
|
| 1546 |
+
<div style="flex:1; min-width:100px;"><label>PIN</label><input type="text" name="pin" value="{{ emp.pin }}" pattern="\\d{4}" required></div>
|
| 1547 |
<div style="flex:1; min-width:100px;"><label>Ставка</label><input type="number" name="daily_rate" value="{{ emp.daily_rate }}" required></div>
|
| 1548 |
<div style="flex:1; min-width:100px;"><label>Цель (n)</label><input type="number" name="target_amount" value="{{ emp.target_amount }}" required></div>
|
| 1549 |
<div style="flex:1; min-width:100px;"><label>Бонус %</label><input type="number" name="bonus_percentage" value="{{ emp.bonus_percentage }}" step="0.1" required></div>
|
|
|
|
| 1966 |
@app.route('/admin/salary', methods=['GET', 'POST'])
|
| 1967 |
def admin_salary():
|
| 1968 |
data = load_data()
|
| 1969 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1970 |
if request.method == 'POST':
|
| 1971 |
action = request.form.get('action')
|
| 1972 |
if action == 'add_fine':
|
| 1973 |
emp_id = request.form.get('employee_id')
|
| 1974 |
+
date_fine = request.form.get('date')
|
| 1975 |
amount = float(request.form.get('amount', 0))
|
| 1976 |
+
reason = request.form.get('reason', '')
|
| 1977 |
+
data.setdefault('fines', []).append({
|
| 1978 |
'id': uuid4().hex,
|
| 1979 |
'employee_id': emp_id,
|
| 1980 |
+
'date': date_fine,
|
| 1981 |
'amount': amount,
|
| 1982 |
'reason': reason
|
| 1983 |
})
|
|
|
|
| 1984 |
save_data(data)
|
| 1985 |
return redirect(url_for('admin_salary'))
|
| 1986 |
elif action == 'delete_fine':
|
| 1987 |
fine_id = request.form.get('fine_id')
|
| 1988 |
+
data['fines'] = [f for f in data.get('fines', []) if f.get('id') != fine_id]
|
| 1989 |
save_data(data)
|
| 1990 |
return redirect(url_for('admin_salary'))
|
| 1991 |
|
| 1992 |
+
orders = data.get('orders', {})
|
| 1993 |
+
employees = data.get('employees', [])
|
| 1994 |
+
workdays = data.get('workdays', {})
|
| 1995 |
+
fines = data.get('fines', [])
|
| 1996 |
+
|
| 1997 |
+
fines = sorted(fines, key=lambda x: x.get('date', ''), reverse=True)
|
| 1998 |
+
|
| 1999 |
return render_template_string(
|
| 2000 |
SALARY_TEMPLATE,
|
| 2001 |
orders_json=json.dumps(list(orders.values())),
|
| 2002 |
employees_json=json.dumps(employees),
|
| 2003 |
workdays_json=json.dumps(workdays),
|
| 2004 |
+
fines_json=json.dumps(fines),
|
| 2005 |
+
fines=fines,
|
| 2006 |
employees=employees,
|
| 2007 |
currency_code=CURRENCY_CODE
|
| 2008 |
)
|
|
|
|
| 2033 |
emp_name = request.form.get('employee_name', '').strip()
|
| 2034 |
pin = request.form.get('pin', '0000').strip()
|
| 2035 |
try: daily_rate = float(request.form.get('daily_rate', 0))
|
| 2036 |
+
except: daily_rate = 230000
|
| 2037 |
try: target_amount = float(request.form.get('target_amount', 0))
|
| 2038 |
+
except: target_amount = 1500000
|
| 2039 |
try: bonus_percentage = float(request.form.get('bonus_percentage', 0))
|
| 2040 |
+
except: bonus_percentage = 10
|
| 2041 |
|
| 2042 |
if emp_name:
|
| 2043 |
employees.append({
|
|
|
|
| 2058 |
e['name'] = request.form.get('name', '').strip()
|
| 2059 |
e['pin'] = request.form.get('pin', '0000').strip()
|
| 2060 |
try: e['daily_rate'] = float(request.form.get('daily_rate', 0))
|
| 2061 |
+
except: e['daily_rate'] = 230000
|
| 2062 |
try: e['target_amount'] = float(request.form.get('target_amount', 0))
|
| 2063 |
+
except: e['target_amount'] = 1500000
|
| 2064 |
try: e['bonus_percentage'] = float(request.form.get('bonus_percentage', 0))
|
| 2065 |
+
except: e['bonus_percentage'] = 10
|
| 2066 |
break
|
| 2067 |
data['employees'] = employees
|
| 2068 |
save_data(data)
|
|
|
|
| 2239 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 2240 |
|
| 2241 |
port = int(os.environ.get('PORT', 7860))
|
| 2242 |
+
app.run(host='0.0.0.0', port=port)
|