update
This commit is contained in:
parent
5b8f276437
commit
0c08fefabe
|
|
@ -11,12 +11,21 @@
|
||||||
use App\Jobs\ReextractAllFrames;
|
use App\Jobs\ReextractAllFrames;
|
||||||
use App\Jobs\RetrainAllModels;
|
use App\Jobs\RetrainAllModels;
|
||||||
use App\Jobs\MigrateExistingEmbeddings;
|
use App\Jobs\MigrateExistingEmbeddings;
|
||||||
|
use App\Services\FaceRecognitionService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class FaceApprovalController extends Controller
|
class FaceApprovalController extends Controller
|
||||||
{
|
{
|
||||||
|
protected $faceService;
|
||||||
|
|
||||||
|
public function __construct(FaceRecognitionService $faceService)
|
||||||
|
{
|
||||||
|
$this->faceService = $faceService;
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = User::with(['jabatan', 'divisi', 'dataWajah']);
|
$query = User::with(['jabatan', 'divisi', 'dataWajah']);
|
||||||
|
|
@ -251,4 +260,26 @@ public function migrateEmbeddings()
|
||||||
dispatch(new MigrateExistingEmbeddings());
|
dispatch(new MigrateExistingEmbeddings());
|
||||||
return redirect()->back()->with('success', 'Proses migrasi embedding sedang berjalan di background. Anda akan menerima notifikasi setelah selesai.');
|
return redirect()->back()->with('success', 'Proses migrasi embedding sedang berjalan di background. Anda akan menerima notifikasi setelah selesai.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uploadVideo(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'video_wajah' => 'required|file|mimetypes:video/mp4,video/quicktime,video/x-msvideo,video/3gpp,video/x-matroska|max:51200',
|
||||||
|
], [
|
||||||
|
'video_wajah.required' => 'File video wajah wajib diupload.',
|
||||||
|
'video_wajah.mimetypes' => 'Format video tidak didukung. Gunakan MP4, MOV, atau AVI.',
|
||||||
|
'video_wajah.max' => 'Ukuran video terlalu besar (maksimal 50MB).',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->faceService->enrollFace($user->id, $request->file('video_wajah'));
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', "Video wajah untuk {$user->nama_lengkap} berhasil diupload. Proses ekstraksi frame sedang berjalan.");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Upload video wajah gagal untuk user {$id}: " . $e->getMessage());
|
||||||
|
return redirect()->back()->with('error', 'Gagal upload video: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,8 +271,23 @@ class="inline-flex items-center px-3 py-1.5 border border-amber-200 text-xs font
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="openUploadModal({{ $user->id }}, '{{ addslashes($user->nama_lengkap) }}')"
|
||||||
|
class="inline-flex items-center px-3 py-1.5 border border-blue-200 text-xs font-medium rounded-lg text-blue-700 bg-blue-50 hover:bg-blue-100 transition-colors shadow-sm"
|
||||||
|
title="Upload Ulang Video Wajah">
|
||||||
|
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
Re-Upload
|
||||||
|
</button>
|
||||||
@else
|
@else
|
||||||
<span class="text-xs text-slate-400 italic">—</span>
|
<button onclick="openUploadModal({{ $user->id }}, '{{ addslashes($user->nama_lengkap) }}')"
|
||||||
|
class="inline-flex items-center px-3 py-1.5 border border-slate-200 text-xs font-medium rounded-lg text-slate-700 bg-white hover:bg-slate-50 transition-colors shadow-sm"
|
||||||
|
title="Upload Video Wajah">
|
||||||
|
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
Upload Video
|
||||||
|
</button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -304,6 +319,80 @@ class="inline-flex items-center px-3 py-1.5 border border-amber-200 text-xs font
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="uploadVideoOverlay" class="fixed inset-0 z-50 hidden items-center justify-center bg-gray-500/50 backdrop-blur-sm" onclick="closeUploadModal()">
|
||||||
|
<div class="relative max-w-md w-full mx-4" onclick="event.stopPropagation()">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-slate-800">Upload Video Wajah</h3>
|
||||||
|
<p class="text-sm text-slate-500 mt-0.5" id="uploadUserName">Nama Karyawan</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="closeUploadModal()" class="p-1.5 hover:bg-slate-100 rounded-lg transition">
|
||||||
|
<svg class="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="uploadVideoForm" method="POST" enctype="multipart/form-data" class="p-6">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-semibold text-slate-700 mb-2">File Video</label>
|
||||||
|
<div id="dropZone" class="border-2 border-dashed border-slate-300 rounded-xl p-6 text-center hover:border-primary transition-colors cursor-pointer">
|
||||||
|
<svg class="w-10 h-10 text-slate-400 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm text-slate-600 font-medium">Klik atau drag & drop video di sini</p>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Format: MP4, MOV, AVI (maks. 50MB)</p>
|
||||||
|
<input type="file" name="video_wajah" id="videoFileInput" accept="video/mp4,video/quicktime,video/x-msvideo,.mp4,.mov,.avi" class="hidden" required>
|
||||||
|
</div>
|
||||||
|
<div id="fileInfo" class="hidden mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-slate-700" id="fileName"></p>
|
||||||
|
<p class="text-xs text-slate-400" id="fileSize"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="clearFile()" class="p-1 hover:bg-slate-200 rounded transition">
|
||||||
|
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<svg class="w-4 h-4 text-amber-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-xs text-amber-700">Video akan diproses untuk ekstraksi frame wajah. Jika user sudah punya data wajah sebelumnya, data lama akan ditimpa.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button type="button" onclick="closeUploadModal()"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="uploadSubmitBtn"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-lg transition shadow-sm disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
||||||
|
<span id="uploadBtnText">Upload & Proses</span>
|
||||||
|
<span id="uploadBtnLoading" class="hidden inline-flex items-center">
|
||||||
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Mengupload...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('script')
|
@section('script')
|
||||||
|
|
@ -405,5 +494,74 @@ function stopPolling() {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove('opacity-50', 'cursor-not-allowed');
|
btn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openUploadModal(userId, userName) {
|
||||||
|
const form = document.getElementById('uploadVideoForm');
|
||||||
|
form.action = `/face-approval/${userId}/upload-video`;
|
||||||
|
document.getElementById('uploadUserName').textContent = userName;
|
||||||
|
clearFile();
|
||||||
|
const overlay = document.getElementById('uploadVideoOverlay');
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
overlay.classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUploadModal() {
|
||||||
|
const overlay = document.getElementById('uploadVideoOverlay');
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
overlay.classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone & file input
|
||||||
|
const dropZone = document.getElementById('dropZone');
|
||||||
|
const fileInput = document.getElementById('videoFileInput');
|
||||||
|
|
||||||
|
dropZone.addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('border-primary', 'bg-primary/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('border-primary', 'bg-primary/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('border-primary', 'bg-primary/5');
|
||||||
|
if (e.dataTransfer.files.length > 0) {
|
||||||
|
fileInput.files = e.dataTransfer.files;
|
||||||
|
showFileInfo(e.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
showFileInfo(e.target.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFileInfo(file) {
|
||||||
|
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
document.getElementById('fileName').textContent = file.name;
|
||||||
|
document.getElementById('fileSize').textContent = sizeMB + ' MB';
|
||||||
|
document.getElementById('fileInfo').classList.remove('hidden');
|
||||||
|
dropZone.classList.add('hidden');
|
||||||
|
document.getElementById('uploadSubmitBtn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFile() {
|
||||||
|
fileInput.value = '';
|
||||||
|
document.getElementById('fileInfo').classList.add('hidden');
|
||||||
|
dropZone.classList.remove('hidden');
|
||||||
|
document.getElementById('uploadSubmitBtn').disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('uploadVideoForm').addEventListener('submit', function() {
|
||||||
|
const btn = document.getElementById('uploadSubmitBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
document.getElementById('uploadBtnText').classList.add('hidden');
|
||||||
|
document.getElementById('uploadBtnLoading').classList.remove('hidden');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@
|
||||||
Route::put('/face-approval/{id}/approve', [FaceApprovalController::class, 'approve'])->name('face.approve');
|
Route::put('/face-approval/{id}/approve', [FaceApprovalController::class, 'approve'])->name('face.approve');
|
||||||
Route::delete('/face-approval/{id}/reject', [FaceApprovalController::class, 'reject'])->name('face.reject');
|
Route::delete('/face-approval/{id}/reject', [FaceApprovalController::class, 'reject'])->name('face.reject');
|
||||||
Route::delete('/face-approval/{id}/reset', [FaceApprovalController::class, 'reset'])->name('face.reset');
|
Route::delete('/face-approval/{id}/reset', [FaceApprovalController::class, 'reset'])->name('face.reset');
|
||||||
|
Route::post('/face-approval/{id}/upload-video', [FaceApprovalController::class, 'uploadVideo'])->name('face.upload_video');
|
||||||
|
|
||||||
Route::resource('role', RoleController::class)->except(['create', 'edit']);
|
Route::resource('role', RoleController::class)->except(['create', 'edit']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue