38808-vm/hr_zkteco.php
2026-04-13 14:24:18 +00:00

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'; ?>