update
This commit is contained in:
parent
5b8f276437
commit
0c08fefabe
|
|
@ -11,12 +11,21 @@
|
|||
use App\Jobs\ReextractAllFrames;
|
||||
use App\Jobs\RetrainAllModels;
|
||||
use App\Jobs\MigrateExistingEmbeddings;
|
||||
use App\Services\FaceRecognitionService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FaceApprovalController extends Controller
|
||||
{
|
||||
protected $faceService;
|
||||
|
||||
public function __construct(FaceRecognitionService $faceService)
|
||||
{
|
||||
$this->faceService = $faceService;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = User::with(['jabatan', 'divisi', 'dataWajah']);
|
||||
|
|
@ -251,4 +260,26 @@ public function migrateEmbeddings()
|
|||
dispatch(new MigrateExistingEmbeddings());
|
||||
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>
|
||||
Reset
|
||||
</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
|
||||
<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
|
||||
</div>
|
||||
</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 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
|
||||
|
||||
@section('script')
|
||||
|
|
@ -405,5 +494,74 @@ function stopPolling() {
|
|||
btn.disabled = false;
|
||||
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>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@
|
|||
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}/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']);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue