290 lines
17 KiB
PHP
290 lines
17 KiB
PHP
<?php
|
|
require_once 'includes/header.php';
|
|
$autoloadPath = __DIR__ . '/vendor/autoload.php';
|
|
if (!file_exists($autoloadPath)) {
|
|
echo "<div class='container mt-5'><div class='alert alert-danger shadow-sm border-0 border-start border-danger border-4'><i class='fas fa-exclamation-triangle me-2'></i> <strong>خطأ:</strong> مجلد <code>vendor</code> غير موجود على الاستضافة. يرجى التأكد من رفع مجلد المكتبات (vendor) مع باقي ملفات النظام، أو تنفيذ أمر <code>composer install</code> إذا كنت تستخدم سطر الأوامر.</div></div>";
|
|
require_once 'includes/footer.php';
|
|
exit;
|
|
}
|
|
require $autoloadPath;
|
|
|
|
use Rats\Zkteco\Lib\ZKTeco;
|
|
|
|
if (!isAdmin()) {
|
|
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
|
require_once 'includes/footer.php';
|
|
exit;
|
|
}
|
|
|
|
$error = '';
|
|
$success = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (isset($_POST['save_settings'])) {
|
|
$ip = trim($_POST['ip_address']);
|
|
$port = (int)$_POST['port'];
|
|
if ($ip && $port) {
|
|
$stmt = db()->prepare("UPDATE hr_zkteco_settings SET ip_address = ?, port = ? WHERE id = 1");
|
|
$stmt->execute([$ip, $port]);
|
|
$success = "تم حفظ الإعدادات بنجاح.";
|
|
} else {
|
|
$error = "يرجى تعبئة الحقول المطلوبة.";
|
|
}
|
|
}
|
|
|
|
$stmt = db()->query("SELECT * FROM hr_zkteco_settings LIMIT 1");
|
|
$settings = $stmt->fetch();
|
|
$ip = $settings['ip_address'] ?? '192.168.1.201';
|
|
$port = $settings['port'] ?? 4370;
|
|
|
|
if (isset($_POST['test_connection'])) {
|
|
try {
|
|
$zk = new ZKTeco($ip, $port);
|
|
if ($zk->connect()) {
|
|
$success = "تم الاتصال بالجهاز بنجاح (الإصدار: " . htmlspecialchars($zk->version() ?? 'غير معروف') . ")";
|
|
$zk->disconnect();
|
|
} else {
|
|
$error = "فشل الاتصال بالجهاز. يرجى التأكد من أن الجهاز على نفس الشبكة والمنفذ مفتوح (عادة 4370).";
|
|
}
|
|
} catch (Exception $e) {
|
|
$error = "خطأ أثناء الاتصال: " . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['sync_logs'])) {
|
|
try {
|
|
$zk = new ZKTeco($ip, $port);
|
|
if ($zk->connect()) {
|
|
$attendance = $zk->getAttendance();
|
|
|
|
$empStmt = db()->query("SELECT id, zkteco_uid FROM hr_employees WHERE zkteco_uid IS NOT NULL AND zkteco_uid != '' AND status = 'active'");
|
|
$empMap = [];
|
|
while ($emp = $empStmt->fetch()) {
|
|
$empMap[(string)$emp['zkteco_uid']] = $emp['id'];
|
|
}
|
|
|
|
$syncedCount = 0;
|
|
if (is_array($attendance)) {
|
|
$attData = [];
|
|
foreach ($attendance as $att) {
|
|
$uid = (string)$att['id'];
|
|
if (isset($empMap[$uid])) {
|
|
$emp_id = $empMap[$uid];
|
|
$timeStr = $att['timestamp'];
|
|
$date = date('Y-m-d', strtotime($timeStr));
|
|
$time = date('H:i:s', strtotime($timeStr));
|
|
|
|
if (!isset($attData[$emp_id][$date])) {
|
|
$attData[$emp_id][$date] = ['min' => $time, 'max' => $time];
|
|
} else {
|
|
if ($time < $attData[$emp_id][$date]['min']) $attData[$emp_id][$date]['min'] = $time;
|
|
if ($time > $attData[$emp_id][$date]['max']) $attData[$emp_id][$date]['max'] = $time;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($attData as $emp_id => $dates) {
|
|
foreach ($dates as $date => $times) {
|
|
$check_in = $times['min'];
|
|
$check_out = ($times['max'] != $times['min']) ? $times['max'] : null;
|
|
|
|
$chkStmt = db()->prepare("SELECT id, check_in, check_out FROM hr_attendance WHERE employee_id = ? AND date = ?");
|
|
$chkStmt->execute([$emp_id, $date]);
|
|
$existing = $chkStmt->fetch();
|
|
|
|
if ($existing) {
|
|
$upd_in = $existing['check_in'];
|
|
$upd_out = $existing['check_out'];
|
|
$changed = false;
|
|
|
|
if (empty($upd_in) || $check_in < $upd_in) { $upd_in = $check_in; $changed = true; }
|
|
if ($check_out && (empty($upd_out) || $check_out > $upd_out)) { $upd_out = $check_out; $changed = true; }
|
|
|
|
if ($changed) {
|
|
$uStmt = db()->prepare("UPDATE hr_attendance SET check_in = ?, check_out = ?, status = 'present' WHERE id = ?");
|
|
$uStmt->execute([$upd_in, $upd_out, $existing['id']]);
|
|
$syncedCount++;
|
|
}
|
|
} else {
|
|
$iStmt = db()->prepare("INSERT INTO hr_attendance (employee_id, date, check_in, check_out, status) VALUES (?, ?, ?, ?, 'present')");
|
|
$iStmt->execute([$emp_id, $date, $check_in, $check_out]);
|
|
$syncedCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$zk->disconnect();
|
|
if ($syncedCount > 0) {
|
|
$success = "تم مزامنة $syncedCount سجل حضور (تحديث / إنشاء) بنجاح.";
|
|
} else {
|
|
$success = "تم الاتصال بنجاح ولكن لم يتم العثور على سجلات جديدة للموظفين المعرفين.";
|
|
}
|
|
|
|
} else {
|
|
$error = "فشل الاتصال بالجهاز.";
|
|
}
|
|
} catch (Exception $e) {
|
|
$error = "حدث خطأ أثناء المزامنة: " . $e->getMessage();
|
|
}
|
|
}
|
|
} else {
|
|
$stmt = db()->query("SELECT * FROM hr_zkteco_settings LIMIT 1");
|
|
$settings = $stmt->fetch();
|
|
$ip = $settings['ip_address'] ?? '192.168.1.201';
|
|
$port = $settings['port'] ?? 4370;
|
|
}
|
|
?>
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="row mb-4">
|
|
<div class="col-md-8">
|
|
<h2 class="h3 mb-0 text-gray-800">ربط جهاز البصمة ZKTeco</h2>
|
|
<p class="text-muted">اختر طريقة الربط المناسبة للشبكة الخاصة بك لسحب سجلات الحضور.</p>
|
|
</div>
|
|
<div class="col-md-4 text-end">
|
|
<a href="hr_attendance.php" class="btn btn-secondary"><i class="fas fa-arrow-left"></i> العودة لسجل الحضور</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger shadow-sm border-0 border-start border-danger border-4 alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-exclamation-circle me-2"></i> <?= htmlspecialchars($error ?? '') ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($success): ?>
|
|
<div class="alert alert-success shadow-sm border-0 border-start border-success border-4 alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i> <?= htmlspecialchars($success ?? '') ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Nav tabs -->
|
|
<ul class="nav nav-tabs mb-4" id="zktecoTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active fw-bold text-dark" id="pull-tab" data-bs-toggle="tab" data-bs-target="#pull" type="button" role="tab" aria-controls="pull" aria-selected="true">
|
|
<i class="fas fa-network-wired me-1 text-primary"></i> الطريقة 1: الاتصال المباشر (TCP Pull)
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link fw-bold text-dark" id="push-tab" data-bs-toggle="tab" data-bs-target="#push" type="button" role="tab" aria-controls="push" aria-selected="false">
|
|
<i class="fas fa-cloud-upload-alt me-1 text-success"></i> الطريقة 2: الدفع التلقائي (ADMS Push)
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab panes -->
|
|
<div class="tab-content" id="zktecoTabsContent">
|
|
<!-- Tab 1: TCP Pull -->
|
|
<div class="tab-pane fade show active" id="pull" role="tabpanel" aria-labelledby="pull-tab">
|
|
<div class="alert alert-info border-0 border-start border-info border-4">
|
|
<i class="fas fa-info-circle me-2"></i> هذه الطريقة تتطلب أن يكون الخادم (السيرفر) قادراً على الوصول إلى عنوان IP الخاص بالجهاز. إذا كان الجهاز في شبكة محلية مختلفة ولا يوجد IP ثابت، استخدم <strong>الطريقة 2</strong>.
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-xl-6 col-lg-6 mb-4">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header bg-white pb-0 border-bottom-0">
|
|
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-cog me-2"></i> إعدادات الاتصال المباشر</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="">
|
|
<div class="mb-3">
|
|
<label class="form-label">IP Address (عنوان الجهاز)</label>
|
|
<input type="text" class="form-control" name="ip_address" value="<?= htmlspecialchars($ip ?? '') ?>" required placeholder="مثال: 192.168.1.201">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Port (المنفذ)</label>
|
|
<input type="number" class="form-control" name="port" value="<?= htmlspecialchars($port ?? '') ?>" required placeholder="الافتراضي: 4370">
|
|
</div>
|
|
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
|
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save me-1"></i> حفظ الإعدادات</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-6 col-lg-6 mb-4">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header bg-white pb-0 border-bottom-0">
|
|
<h6 class="m-0 font-weight-bold text-info"><i class="fas fa-sync-alt me-2"></i> العمليات</h6>
|
|
</div>
|
|
<div class="card-body text-center py-5">
|
|
<p class="text-muted mb-4">اختبر الاتصال أو ابدأ بسحب البيانات الان.</p>
|
|
<form method="POST" action="">
|
|
<div class="d-grid gap-3 col-8 mx-auto">
|
|
<button type="submit" name="test_connection" class="btn btn-outline-info btn-lg shadow-sm">
|
|
<i class="fas fa-wifi me-2"></i> اختبار الاتصال
|
|
</button>
|
|
<button type="submit" name="sync_logs" class="btn btn-success btn-lg shadow-sm">
|
|
<i class="fas fa-download me-2"></i> مزامنة الحضور والانصراف
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: ADMS Push -->
|
|
<div class="tab-pane fade" id="push" role="tabpanel" aria-labelledby="push-tab">
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-body">
|
|
<h5 class="card-title text-success mb-3"><i class="fas fa-check-circle me-2"></i> النظام جاهز لاستقبال البصمات تلقائياً (ADMS/WDMS)</h5>
|
|
<p class="card-text text-muted mb-4">من خلال هذه الطريقة، يقوم جهاز البصمة بإرسال سجلات الحضور تلقائياً إلى هذا النظام عبر الإنترنت. <strong>هذه الطريقة مثالية إذا كان جهاز البصمة في شبكة محلية بدون IP ثابت (Public IP).</strong></p>
|
|
|
|
<h6 class="font-weight-bold text-dark mb-3"><i class="fas fa-desktop me-2"></i> خطوات الإعداد على جهاز البصمة ZKTeco:</h6>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<ul class="list-group list-group-flush mb-4 shadow-sm rounded border">
|
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
|
<div class="ms-2 me-auto">
|
|
<div class="fw-bold text-primary mb-1">1. إعدادات الشبكة (Comm.)</div>
|
|
افتح القائمة الرئيسية للجهاز واذهب إلى <kbd>COMM.</kbd> ثم <kbd>Cloud Server Setting</kbd> أو <kbd>ADMS</kbd>.
|
|
</div>
|
|
<span class="badge bg-primary rounded-pill"><i class="fas fa-arrow-left"></i></span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
|
<div class="ms-2 me-auto">
|
|
<div class="fw-bold text-primary mb-1">2. عنوان السيرفر (Server Address)</div>
|
|
أدخل عنوان هذا النظام أو الـ IP الخاص به.
|
|
<br><small class="text-muted">مثال: إذا كان عنوان التطبيق <code>http://your-app.com</code> أدخل فقط <code>your-app.com</code></small>
|
|
</div>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
|
<div class="ms-2 me-auto">
|
|
<div class="fw-bold text-primary mb-1">3. منفذ السيرفر (Server Port)</div>
|
|
أدخل المنفذ: <kbd>80</kbd> للاتصال العادي (أو 443 إذا كنت تستخدم HTTPS مدعوم).
|
|
</div>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
|
<div class="ms-2 me-auto">
|
|
<div class="fw-bold text-primary mb-1">4. تفعيل المزامنة</div>
|
|
بمجرد الحفظ، سيبدأ الجهاز بإرسال بيانات الحضور تلقائياً إلى السيرفر. ستظهر السجلات في صفحة "سجل الحضور".
|
|
</div>
|
|
<span class="badge bg-success rounded-pill"><i class="fas fa-check"></i></span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="alert alert-warning border-0 border-start border-warning border-4 h-100 d-flex flex-column justify-content-center">
|
|
<div>
|
|
<h6 class="alert-heading font-weight-bold"><i class="fas fa-exclamation-triangle me-2"></i> خطوة هامة لربط الموظفين</h6>
|
|
<p class="mb-0 small">لكي يتم احتساب البصمات للموظف الصحيح، تأكد من الذهاب إلى <a href="hr_employees.php" class="alert-link fw-bold">قائمة الموظفين</a> وتعديل بيانات كل موظف وإضافة <strong>ZKTeco UID</strong> (رقم الموظف في جهاز البصمة).</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php require_once 'includes/footer.php'; ?>
|