first
This commit is contained in:
40
app/Http/Controllers/User/AudioController.php
Normal file
40
app/Http/Controllers/User/AudioController.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Services\StorageService;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AudioController extends Controller
|
||||
{
|
||||
public function __construct(private readonly StorageService $storage) {}
|
||||
|
||||
public function stream(TranscriptionProject $project): StreamedResponse
|
||||
{
|
||||
$this->authorize('viewAudio', $project);
|
||||
|
||||
$path = $project->stored_audio_path;
|
||||
|
||||
abort_unless($this->storage->exists($path), 404);
|
||||
|
||||
$size = $this->storage->size($path);
|
||||
$mimeType = $project->mime_type;
|
||||
|
||||
return response()->stream(function () use ($path) {
|
||||
$stream = $this->storage->readStream($path);
|
||||
if ($stream) {
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}
|
||||
}, 200, [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Length' => $size,
|
||||
'Content-Disposition' => 'inline',
|
||||
'Cache-Control' => 'no-store, no-cache, private, must-revalidate',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Accept-Ranges' => 'bytes',
|
||||
]);
|
||||
}
|
||||
}
|
||||
72
app/Http/Controllers/User/CollaboratorController.php
Normal file
72
app/Http/Controllers/User/CollaboratorController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ProjectCollaborator;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Models\User;
|
||||
use App\Services\AuditLogService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CollaboratorController extends Controller
|
||||
{
|
||||
public function store(Request $request, TranscriptionProject $project, AuditLogService $audit): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageCollaborators', $project);
|
||||
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email', 'exists:users,email'],
|
||||
'role' => ['nullable', 'in:editor,viewer'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $data['email'])->where('is_active', true)->firstOrFail();
|
||||
|
||||
// Owner tidak boleh jadi collaborator
|
||||
if ($project->isOwnedBy($user)) {
|
||||
return back()->withErrors(['email' => 'Pengguna ini adalah pemilik projek.']);
|
||||
}
|
||||
|
||||
// Elak duplicate
|
||||
$existing = ProjectCollaborator::where('project_id', $project->id)
|
||||
->where('user_id', $user->id)
|
||||
->exists();
|
||||
|
||||
if ($existing) {
|
||||
return back()->withErrors(['email' => 'Pengguna ini sudah menjadi collaborator.']);
|
||||
}
|
||||
|
||||
ProjectCollaborator::create([
|
||||
'project_id' => $project->id,
|
||||
'user_id' => $user->id,
|
||||
'role' => $data['role'] ?? 'editor',
|
||||
'added_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
$audit->log('collaborator_added', [
|
||||
'project_id' => $project->id,
|
||||
'target_user_id' => $user->id,
|
||||
'new_values' => ['email' => $user->email, 'role' => $data['role'] ?? 'editor'],
|
||||
]);
|
||||
|
||||
return back()->with('success', "{$user->name} berjaya ditambah sebagai collaborator.");
|
||||
}
|
||||
|
||||
public function destroy(TranscriptionProject $project, User $user, AuditLogService $audit): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageCollaborators', $project);
|
||||
|
||||
ProjectCollaborator::where('project_id', $project->id)
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
|
||||
$audit->log('collaborator_removed', [
|
||||
'project_id' => $project->id,
|
||||
'target_user_id' => $user->id,
|
||||
'old_values' => ['email' => $user->email],
|
||||
]);
|
||||
|
||||
return back()->with('success', "{$user->name} telah dibuang daripada collaborator.");
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/User/CommentController.php
Normal file
52
app/Http/Controllers/User/CommentController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\StoreCommentRequest;
|
||||
use App\Models\ProjectComment;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Services\AuditLogService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
public function store(
|
||||
StoreCommentRequest $request,
|
||||
TranscriptionProject $project,
|
||||
AuditLogService $audit
|
||||
): RedirectResponse {
|
||||
$this->authorize('create', [ProjectComment::class, $project]);
|
||||
|
||||
$comment = $project->comments()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'message' => $request->message,
|
||||
]);
|
||||
|
||||
$audit->log('comment_added', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => ['comment_id' => $comment->id],
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Komen berjaya ditambah.');
|
||||
}
|
||||
|
||||
public function destroy(
|
||||
TranscriptionProject $project,
|
||||
ProjectComment $comment,
|
||||
AuditLogService $audit
|
||||
): RedirectResponse {
|
||||
abort_if($comment->project_id !== $project->id, 404);
|
||||
|
||||
$this->authorize('delete', $comment);
|
||||
|
||||
$audit->log('comment_deleted', [
|
||||
'project_id' => $project->id,
|
||||
'old_values' => ['comment_id' => $comment->id],
|
||||
]);
|
||||
|
||||
$comment->delete();
|
||||
|
||||
return back()->with('success', 'Komen berjaya dipadam.');
|
||||
}
|
||||
}
|
||||
26
app/Http/Controllers/User/DashboardController.php
Normal file
26
app/Http/Controllers/User/DashboardController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TranscriptionProject;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$ownedProjects = TranscriptionProject::where('owner_user_id', $user->id)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10, ['*'], 'owned');
|
||||
|
||||
$sharedProjects = $user->collaboratingProjects()
|
||||
->orderByDesc('transcription_projects.created_at')
|
||||
->paginate(10, ['*'], 'shared');
|
||||
|
||||
return view('user.dashboard', compact('ownedProjects', 'sharedProjects'));
|
||||
}
|
||||
}
|
||||
122
app/Http/Controllers/User/ProjectController.php
Normal file
122
app/Http/Controllers/User/ProjectController.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\CreateProjectRequest;
|
||||
use App\Jobs\TranscribeAudioJob;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Services\AuditLogService;
|
||||
use App\Services\StorageService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', TranscriptionProject::class);
|
||||
|
||||
return view('user.projects.create');
|
||||
}
|
||||
|
||||
public function store(CreateProjectRequest $request, StorageService $storage, AuditLogService $audit): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', TranscriptionProject::class);
|
||||
|
||||
$file = $request->file('audio');
|
||||
$uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
$path = $storage->storeAudio($file, $uuid);
|
||||
|
||||
$project = TranscriptionProject::create([
|
||||
'uuid' => $uuid,
|
||||
'title' => $request->title,
|
||||
'description' => $request->description,
|
||||
'owner_user_id' => Auth::id(),
|
||||
'original_filename' => $file->getClientOriginalName(),
|
||||
'stored_audio_path' => $path,
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'file_size' => $file->getSize(),
|
||||
'language' => $request->language ?? 'ms',
|
||||
'transcription_status' => 'pending',
|
||||
'transcription_engine' => config('speech2text.transcription.engine'),
|
||||
]);
|
||||
|
||||
$audit->log('project_created', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => ['title' => $project->title, 'uuid' => $project->uuid],
|
||||
]);
|
||||
|
||||
$audit->log('audio_uploaded', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => [
|
||||
'original_filename' => $project->original_filename,
|
||||
'file_size' => $project->file_size,
|
||||
'mime_type' => $project->mime_type,
|
||||
],
|
||||
]);
|
||||
|
||||
TranscribeAudioJob::dispatch($project);
|
||||
|
||||
return redirect()->route('user.projects.show', $project)
|
||||
->with('success', 'Projek berjaya dicipta. Audio sedang dalam barisan untuk ditranskripkan.');
|
||||
}
|
||||
|
||||
public function show(TranscriptionProject $project): View
|
||||
{
|
||||
$this->authorize('view', $project);
|
||||
|
||||
$project->load(['owner', 'collaborators.department', 'transcriptVersions.editor', 'comments.user']);
|
||||
|
||||
return view('user.projects.show', compact('project'));
|
||||
}
|
||||
|
||||
// Endpoint polling status — JSON, tidak mendedahkan transcript
|
||||
public function status(TranscriptionProject $project): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $project);
|
||||
|
||||
return response()->json([
|
||||
'status' => $project->transcription_status,
|
||||
'duration_seconds' => $project->duration_seconds,
|
||||
'processed_at' => $project->processed_at?->format('d/m/Y H:i'),
|
||||
'error_message' => $project->isFailed() ? $project->error_message : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function retry(TranscriptionProject $project, AuditLogService $audit): RedirectResponse
|
||||
{
|
||||
$this->authorize('retryTranscription', $project);
|
||||
|
||||
$project->update([
|
||||
'transcription_status' => 'pending',
|
||||
'error_message' => null,
|
||||
]);
|
||||
|
||||
$audit->log('transcription_started', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => ['retry' => true],
|
||||
]);
|
||||
|
||||
TranscribeAudioJob::dispatch($project);
|
||||
|
||||
return back()->with('success', 'Transkripsi sedang dicuba semula.');
|
||||
}
|
||||
|
||||
public function destroy(TranscriptionProject $project, AuditLogService $audit): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $project);
|
||||
|
||||
$audit->log('project_deleted', [
|
||||
'project_id' => $project->id,
|
||||
'old_values' => ['title' => $project->title, 'uuid' => $project->uuid],
|
||||
]);
|
||||
|
||||
$project->delete();
|
||||
|
||||
return redirect()->route('user.dashboard')
|
||||
->with('success', 'Projek berjaya dipadam.');
|
||||
}
|
||||
}
|
||||
112
app/Http/Controllers/User/TranscriptController.php
Normal file
112
app/Http/Controllers/User/TranscriptController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\UpdateTranscriptRequest;
|
||||
use App\Http\Requests\User\UploadExternalTranscriptRequest;
|
||||
use App\Jobs\OllamaPostProcessJob;
|
||||
use App\Models\TranscriptVersion;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Services\AuditLogService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TranscriptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Simpan perubahan teks transkripsi dan cipta versi baru.
|
||||
*/
|
||||
public function update(
|
||||
UpdateTranscriptRequest $request,
|
||||
TranscriptionProject $project,
|
||||
AuditLogService $audit
|
||||
): RedirectResponse {
|
||||
$this->authorize('editTranscript', $project);
|
||||
|
||||
$oldText = $project->transcript_text;
|
||||
|
||||
$nextVersion = ($project->transcriptVersions()->max('version_number') ?? 0) + 1;
|
||||
|
||||
TranscriptVersion::create([
|
||||
'project_id' => $project->id,
|
||||
'edited_by' => Auth::id(),
|
||||
'version_number' => $nextVersion,
|
||||
'old_text' => $oldText,
|
||||
'new_text' => $request->transcript_text,
|
||||
'change_summary' => $request->change_summary ?? 'Dikemaskini oleh pengguna',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$project->update([
|
||||
'transcript_text' => $request->transcript_text,
|
||||
'transcription_status' => 'completed',
|
||||
]);
|
||||
|
||||
$audit->log('transcript_edited', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => ['version_number' => $nextVersion],
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Transkripsi berjaya disimpan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muat naik fail transkripsi .txt dari luar sistem.
|
||||
*/
|
||||
public function uploadExternal(
|
||||
UploadExternalTranscriptRequest $request,
|
||||
TranscriptionProject $project,
|
||||
AuditLogService $audit
|
||||
): RedirectResponse {
|
||||
$this->authorize('editTranscript', $project);
|
||||
|
||||
$text = file_get_contents($request->file('transcript_file')->getRealPath());
|
||||
|
||||
// Bersihkan BOM dan normalkan line endings
|
||||
$text = preg_replace('/^\xef\xbb\xbf/', '', $text);
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
$text = trim($text);
|
||||
|
||||
if ($text === '') {
|
||||
return back()->withErrors(['transcript_file' => 'Fail transkripsi kosong.']);
|
||||
}
|
||||
|
||||
$oldText = $project->transcript_text;
|
||||
$nextVersion = ($project->transcriptVersions()->max('version_number') ?? 0) + 1;
|
||||
|
||||
TranscriptVersion::create([
|
||||
'project_id' => $project->id,
|
||||
'edited_by' => Auth::id(),
|
||||
'version_number' => $nextVersion,
|
||||
'old_text' => $oldText,
|
||||
'new_text' => $text,
|
||||
'change_summary' => 'Transkripsi dimuat naik secara manual',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$project->update([
|
||||
'transcript_text' => $text,
|
||||
'transcription_status' => 'completed',
|
||||
'transcription_engine' => 'manual',
|
||||
'processed_at' => $project->processed_at ?? now(),
|
||||
]);
|
||||
|
||||
$audit->log('transcript_uploaded_external', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => ['version_number' => $nextVersion],
|
||||
]);
|
||||
|
||||
// Hantar ke Ollama jika pengguna minta dan Ollama dikonfigurasi
|
||||
if ($request->boolean('clean_with_ollama') && config('speech2text.ollama.enabled')) {
|
||||
OllamaPostProcessJob::dispatch($project);
|
||||
}
|
||||
|
||||
$msg = 'Transkripsi berjaya dimuat naik.';
|
||||
if ($request->boolean('clean_with_ollama') && config('speech2text.ollama.enabled')) {
|
||||
$msg .= ' Pembersihan Ollama sedang diproses.';
|
||||
}
|
||||
|
||||
return back()->with('success', $msg);
|
||||
}
|
||||
}
|
||||
67
app/Http/Controllers/User/TranscriptVersionController.php
Normal file
67
app/Http/Controllers/User/TranscriptVersionController.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TranscriptVersion;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Services\AuditLogService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TranscriptVersionController extends Controller
|
||||
{
|
||||
public function index(TranscriptionProject $project): View
|
||||
{
|
||||
$this->authorize('viewVersionHistory', $project);
|
||||
|
||||
$versions = $project->transcriptVersions()
|
||||
->with('editor')
|
||||
->orderByDesc('version_number')
|
||||
->paginate(20);
|
||||
|
||||
return view('user.projects.versions', compact('project', 'versions'));
|
||||
}
|
||||
|
||||
public function restore(
|
||||
TranscriptionProject $project,
|
||||
TranscriptVersion $version,
|
||||
AuditLogService $audit
|
||||
): RedirectResponse {
|
||||
$this->authorize('restoreVersion', $project);
|
||||
|
||||
// Pastikan versi ini milik projek ini
|
||||
abort_if($version->project_id !== $project->id, 404);
|
||||
|
||||
$oldText = $project->transcript_text;
|
||||
$nextVersion = ($project->transcriptVersions()->max('version_number') ?? 0) + 1;
|
||||
|
||||
TranscriptVersion::create([
|
||||
'project_id' => $project->id,
|
||||
'edited_by' => Auth::id(),
|
||||
'version_number' => $nextVersion,
|
||||
'old_text' => $oldText,
|
||||
'new_text' => $version->new_text,
|
||||
'change_summary' => "Dipulihkan dari versi {$version->version_number}",
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$project->update([
|
||||
'transcript_text' => $version->new_text,
|
||||
'transcription_status' => 'completed',
|
||||
]);
|
||||
|
||||
$audit->log('transcript_version_restored', [
|
||||
'project_id' => $project->id,
|
||||
'new_values' => [
|
||||
'restored_from_version' => $version->version_number,
|
||||
'new_version' => $nextVersion,
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('user.projects.show', $project)
|
||||
->with('success', "Versi {$version->version_number} berjaya dipulihkan.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user