first
This commit is contained in:
11
resources/css/app.css
Normal file
11
resources/css/app.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
resources/js/app.js
Normal file
1
resources/js/app.js
Normal file
@@ -0,0 +1 @@
|
||||
//
|
||||
150
resources/views/admin/audit-logs/index.blade.php
Normal file
150
resources/views/admin/audit-logs/index.blade.php
Normal file
@@ -0,0 +1,150 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Log Audit')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-journal-text me-2 text-primary"></i>Log Audit</h4>
|
||||
<a href="{{ route('admin.audit-logs.export', request()->query()) }}"
|
||||
class="btn btn-outline-success btn-sm">
|
||||
<i class="bi bi-download me-1"></i>Eksport CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-2">
|
||||
<select class="form-select form-select-sm" name="action">
|
||||
<option value="">Semua Tindakan</option>
|
||||
@foreach($actions as $action)
|
||||
<option value="{{ $action }}" {{ request('action') === $action ? 'selected' : '' }}>
|
||||
{{ $action }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control form-control-sm" name="search"
|
||||
value="{{ request('search') }}" placeholder="Cari tindakan / justifikasi...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control form-control-sm" name="date_from"
|
||||
value="{{ request('date_from') }}" placeholder="Dari tarikh">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control form-control-sm" name="date_to"
|
||||
value="{{ request('date_to') }}" placeholder="Hingga tarikh">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">
|
||||
<i class="bi bi-search me-1"></i>Cari
|
||||
</button>
|
||||
<a href="{{ route('admin.audit-logs.index') }}" class="btn btn-sm btn-outline-secondary ms-1">
|
||||
<i class="bi bi-x me-1"></i>Padam
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Masa</th>
|
||||
<th>Oleh</th>
|
||||
<th>Tindakan</th>
|
||||
<th>Sasaran</th>
|
||||
<th>IP</th>
|
||||
<th>Butiran</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
<tr>
|
||||
<td class="text-nowrap small text-muted">
|
||||
{{ $log->created_at?->format('d/m/Y H:i:s') }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold small">{{ $log->actor?->name ?? 'Sistem' }}</span>
|
||||
@if($log->actor_role)
|
||||
<span class="badge bg-light text-dark ms-1 small">{{ $log->actor_role }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<code class="small">{{ $log->action }}</code>
|
||||
</td>
|
||||
<td class="small">
|
||||
{{ $log->targetUser?->name ?? ($log->subject_type ? class_basename($log->subject_type).'#'.$log->subject_id : '—') }}
|
||||
</td>
|
||||
<td class="small text-muted">{{ $log->ip_address ?? '—' }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary py-0"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#logModal{{ $log->id }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Detail Modal --}}
|
||||
<div class="modal fade" id="logModal{{ $log->id }}" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title"><code>{{ $log->action }}</code></h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body small">
|
||||
<table class="table table-sm">
|
||||
<tr><td width="30%">Masa</td><td>{{ $log->created_at?->format('d/m/Y H:i:s') }}</td></tr>
|
||||
<tr><td>Oleh</td><td>{{ $log->actor?->name ?? 'Sistem' }} ({{ $log->actor_role }})</td></tr>
|
||||
<tr><td>IP</td><td>{{ $log->ip_address ?? '—' }}</td></tr>
|
||||
<tr>
|
||||
<td>Justifikasi</td>
|
||||
<td>{{ $log->justification ?? '—' }}</td>
|
||||
</tr>
|
||||
@if($log->old_values)
|
||||
<tr>
|
||||
<td>Nilai Lama</td>
|
||||
<td><pre class="mb-0 small bg-light p-2 rounded">{{ json_encode($log->old_values, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($log->new_values)
|
||||
<tr>
|
||||
<td>Nilai Baru</td>
|
||||
<td><pre class="mb-0 small bg-light p-2 rounded">{{ json_encode($log->new_values, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-4 d-block mb-2"></i>Tiada rekod log.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($logs->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $logs->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
254
resources/views/admin/dashboard.blade.php
Normal file
254
resources/views/admin/dashboard.blade.php
Normal file
@@ -0,0 +1,254 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-speedometer2 me-2 text-primary"></i>Dashboard Pentadbir</h4>
|
||||
<span class="text-muted small">{{ now()->format('d M Y, H:i') }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Baris 1: Pengguna & Projek --}}
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card bg-success text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fs-1 fw-bold">{{ $stats['users_active'] }}</div>
|
||||
<div class="opacity-75 small">Pengguna Aktif</div>
|
||||
<div class="mt-1 opacity-60" style="font-size:0.78rem">
|
||||
{{ $stats['users_total'] }} jumlah •
|
||||
{{ $stats['users_inactive'] }} dinyahaktif
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-people-fill fs-2 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card bg-primary text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fs-1 fw-bold">{{ $stats['projects_total'] }}</div>
|
||||
<div class="opacity-75 small">Jumlah Projek</div>
|
||||
<div class="mt-1 opacity-60" style="font-size:0.78rem">
|
||||
+{{ $stats['projects_last_7'] }} (7 hari) •
|
||||
+{{ $stats['projects_last_30'] }} (30 hari)
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-folder-fill fs-2 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card bg-info text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fs-1 fw-bold">{{ $stats['projects_completed'] }}</div>
|
||||
<div class="opacity-75 small">Transkripsi Selesai</div>
|
||||
@php
|
||||
$pct = $stats['projects_total'] > 0
|
||||
? round($stats['projects_completed'] / $stats['projects_total'] * 100)
|
||||
: 0;
|
||||
@endphp
|
||||
<div class="mt-1 opacity-60" style="font-size:0.78rem">
|
||||
{{ $pct }}% kadar kejayaan
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-check-circle-fill fs-2 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card bg-warning text-dark h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@php
|
||||
$totalBytes = $stats['storage_bytes'];
|
||||
if ($totalBytes >= 1073741824) {
|
||||
$storageLabel = number_format($totalBytes / 1073741824, 2) . ' GB';
|
||||
} elseif ($totalBytes >= 1048576) {
|
||||
$storageLabel = number_format($totalBytes / 1048576, 1) . ' MB';
|
||||
} else {
|
||||
$storageLabel = number_format($totalBytes / 1024, 0) . ' KB';
|
||||
}
|
||||
$durationH = intdiv($stats['duration_total_seconds'], 3600);
|
||||
$durationM = intdiv($stats['duration_total_seconds'] % 3600, 60);
|
||||
@endphp
|
||||
<div class="fs-3 fw-bold">{{ $storageLabel }}</div>
|
||||
<div class="small" style="opacity:0.8">Storan Digunakan</div>
|
||||
<div class="mt-1" style="font-size:0.78rem; opacity:0.7">
|
||||
{{ $durationH }}j {{ $durationM }}m jumlah audio
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-hdd-fill fs-2 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Baris 2: Status + Trend --}}
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-pie-chart me-2 text-primary"></i>Status Transkripsi
|
||||
</div>
|
||||
<div class="card-body pb-2">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@php
|
||||
$statusRows = [
|
||||
['bg-secondary', 'Menunggu', $stats['projects_pending']],
|
||||
['bg-warning text-dark', 'Sedang Diproses', $stats['projects_processing']],
|
||||
['bg-success', 'Selesai', $stats['projects_completed']],
|
||||
['bg-danger', 'Gagal', $stats['projects_failed']],
|
||||
['bg-light text-muted border', 'Dipadam (soft)', $stats['projects_deleted']],
|
||||
];
|
||||
@endphp
|
||||
@foreach($statusRows as [$cls, $label, $count])
|
||||
<tr>
|
||||
<td><span class="badge {{ $cls }}">{{ $label }}</span></td>
|
||||
<td class="fw-bold">{{ $count }}</td>
|
||||
<td class="text-muted small">
|
||||
@if($stats['projects_total'] > 0)
|
||||
{{ round($count / ($stats['projects_total'] ?: 1) * 100) }}%
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-bar-chart me-2 text-success"></i>Trend 30 Hari Lepas
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($trendRaw->isEmpty())
|
||||
<div class="text-muted small text-center py-4">Tiada data projek dalam 30 hari lepas.</div>
|
||||
@else
|
||||
<div class="table-responsive" style="max-height:220px; overflow-y:auto">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Tarikh</th>
|
||||
<th class="text-center">Baru</th>
|
||||
<th class="text-center text-success">Selesai</th>
|
||||
<th class="text-center text-danger">Gagal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($trendRaw->sortByDesc('date') as $row)
|
||||
<tr>
|
||||
<td class="small">{{ \Carbon\Carbon::parse($row->date)->format('d/m') }}</td>
|
||||
<td class="text-center">{{ $row->total }}</td>
|
||||
<td class="text-center text-success">{{ $row->completed }}</td>
|
||||
<td class="text-center text-danger">{{ $row->failed }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-trophy me-2 text-warning"></i>Pengguna Paling Aktif
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($topUsers->isEmpty())
|
||||
<div class="text-muted small text-center py-4">Tiada data pengguna.</div>
|
||||
@else
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@foreach($topUsers as $i => $u)
|
||||
<tr>
|
||||
<td style="width:28px" class="text-center text-muted small">{{ $i + 1 }}</td>
|
||||
<td>
|
||||
<div class="small fw-semibold">{{ $u->name }}</div>
|
||||
<div class="text-muted" style="font-size:0.72rem">{{ $u->email }}</div>
|
||||
</td>
|
||||
<td class="text-end fw-bold small">{{ $u->owned_projects_count }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Baris 3: Jabatan + Tindakan Pantas --}}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-building me-2 text-secondary"></i>Projek Mengikut Jabatan
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($deptStats->isEmpty())
|
||||
<div class="text-muted small text-center py-4">Tiada data jabatan.</div>
|
||||
@else
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
@foreach($deptStats as $dept)
|
||||
<tr>
|
||||
<td class="small">{{ $dept->name }}</td>
|
||||
<td class="text-end">
|
||||
<span class="badge bg-primary">{{ $dept->project_count }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-lightning me-2 text-warning"></i>Tindakan Pantas
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-people me-2"></i>Urus Pengguna
|
||||
<span class="badge bg-primary ms-1">{{ $stats['users_total'] }}</span>
|
||||
</a>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-outline-success">
|
||||
<i class="bi bi-person-plus me-2"></i>Daftar Pengguna Baru
|
||||
</a>
|
||||
<a href="{{ route('admin.projects.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-folder2 me-2"></i>Senarai Projek (Metadata)
|
||||
<span class="badge bg-secondary ms-1">{{ $stats['projects_total'] }}</span>
|
||||
</a>
|
||||
<a href="{{ route('admin.audit-logs.index') }}" class="btn btn-outline-dark">
|
||||
<i class="bi bi-journal-text me-2"></i>Log Audit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
215
resources/views/admin/projects/index.blade.php
Normal file
215
resources/views/admin/projects/index.blade.php
Normal file
@@ -0,0 +1,215 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Senarai Projek')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-folder2 me-2 text-primary"></i>Senarai Projek</h4>
|
||||
<p class="text-muted small mb-0 mt-1">Metadata sahaja — kandungan audio dan teks tidak dipaparkan.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.dashboard') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-speedometer2 me-2"></i>Kembali ke Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" name="search"
|
||||
value="{{ request('search') }}" placeholder="Cari tajuk projek...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select form-select-sm" name="status">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }}>Menunggu</option>
|
||||
<option value="processing" {{ request('status') === 'processing' ? 'selected' : '' }}>Sedang Diproses</option>
|
||||
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }}>Selesai</option>
|
||||
<option value="failed" {{ request('status') === 'failed' ? 'selected' : '' }}>Gagal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select form-select-sm" name="owner_id">
|
||||
<option value="">Semua Pemilik</option>
|
||||
@foreach($owners as $owner)
|
||||
<option value="{{ $owner->id }}" {{ request('owner_id') == $owner->id ? 'selected' : '' }}>
|
||||
{{ $owner->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">
|
||||
<i class="bi bi-search me-1"></i>Cari
|
||||
</button>
|
||||
<a href="{{ route('admin.projects.index') }}" class="btn btn-sm btn-outline-secondary ms-1">Padam</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tajuk</th>
|
||||
<th>Pemilik</th>
|
||||
<th>Status</th>
|
||||
<th>Saiz Fail</th>
|
||||
<th>Tempoh</th>
|
||||
<th>Kolaborator</th>
|
||||
<th>Tarikh Cipta</th>
|
||||
<th style="width:80px">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$statusMap = [
|
||||
'pending' => ['bg-secondary', 'Menunggu'],
|
||||
'processing' => ['bg-warning text-dark', 'Sedang Diproses'],
|
||||
'completed' => ['bg-success', 'Selesai'],
|
||||
'failed' => ['bg-danger', 'Gagal'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@forelse($projects as $project)
|
||||
<tr class="{{ $project->deleted_at ? 'table-secondary text-muted' : '' }}">
|
||||
<td>
|
||||
<span class="fw-semibold">{{ $project->title }}</span>
|
||||
@if($project->deleted_at)
|
||||
<span class="badge bg-secondary ms-1 small">Dipadam</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="small">{{ $project->owner->name ?? '—' }}</span>
|
||||
<div class="text-muted" style="font-size:0.75rem">{{ $project->owner->email ?? '' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@php [$cls, $label] = $statusMap[$project->transcription_status] ?? ['bg-secondary', $project->transcription_status]; @endphp
|
||||
<span class="badge {{ $cls }}">{{ $label }}</span>
|
||||
</td>
|
||||
<td class="small">
|
||||
@php
|
||||
$bytes = $project->file_size;
|
||||
echo $bytes >= 1048576
|
||||
? number_format($bytes/1048576, 1) . ' MB'
|
||||
: number_format($bytes/1024, 0) . ' KB';
|
||||
@endphp
|
||||
</td>
|
||||
<td class="small">
|
||||
@if($project->duration_seconds)
|
||||
{{ intdiv($project->duration_seconds, 60) }}:{{ str_pad($project->duration_seconds % 60, 2, '0', STR_PAD_LEFT) }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark border">
|
||||
{{ $project->projectCollaborators->count() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-muted text-nowrap">
|
||||
{{ $project->created_at->format('d/m/Y H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
@if(! $project->deleted_at)
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
title="Pindah Pemilikan"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalTransfer{{ $project->id }}"
|
||||
data-project-title="{{ $project->title }}"
|
||||
data-project-owner="{{ $project->owner->name ?? '—' }}">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-4 d-block mb-2"></i>Tiada projek ditemui.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($projects->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $projects->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-muted small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Kandungan audio, teks transkripsi, dan ulasan projek tidak dipaparkan kepada pentadbir.
|
||||
</div>
|
||||
|
||||
{{-- Transfer Ownership Modals --}}
|
||||
@foreach($projects as $project)
|
||||
@if(! $project->deleted_at)
|
||||
<div class="modal fade" id="modalTransfer{{ $project->id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">
|
||||
<i class="bi bi-arrow-left-right me-2 text-warning"></i>Pindah Pemilikan Projek
|
||||
</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.projects.transfer-owner', $project) }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning py-2 small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Tindakan ini akan memindahkan semua hak pemilik projek
|
||||
<strong>"{{ $project->title }}"</strong> kepada pengguna lain.
|
||||
Pemilik asal: <strong>{{ $project->owner->name ?? '—' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Pemilik Baru <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="new_owner_id" required>
|
||||
<option value="">— Pilih pengguna —</option>
|
||||
@foreach($owners as $owner)
|
||||
@if($owner->id !== $project->owner_user_id)
|
||||
<option value="{{ $owner->id }}">{{ $owner->name }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">
|
||||
Justifikasi <span class="text-danger">*</span>
|
||||
<span class="text-muted fw-normal small">(min. 10 aksara)</span>
|
||||
</label>
|
||||
<textarea class="form-control" name="justification" rows="3"
|
||||
placeholder="Nyatakan sebab pemindahan projek ini..."
|
||||
minlength="10" maxlength="1000" required></textarea>
|
||||
<div class="form-text">Justifikasi ini akan direkodkan dalam log audit.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-warning"
|
||||
onclick="return confirm('Sahkan pemindahan pemilikan projek ini?')">
|
||||
<i class="bi bi-arrow-left-right me-1"></i>Pindah Pemilikan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@endsection
|
||||
93
resources/views/admin/users/create.blade.php
Normal file
93
resources/views/admin/users/create.blade.php
Normal file
@@ -0,0 +1,93 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Daftar Pengguna Baru')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-person-plus me-2 text-primary"></i>Daftar Pengguna Baru</h4>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.users.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="name" class="form-label fw-semibold">Nama Penuh <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror"
|
||||
id="name" name="name" value="{{ old('name') }}" required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="email" class="form-label fw-semibold">E-mel <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control @error('email') is-invalid @enderror"
|
||||
id="email" name="email" value="{{ old('email') }}" required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password" class="form-label fw-semibold">Kata Laluan <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control @error('password') is-invalid @enderror"
|
||||
id="password" name="password" required>
|
||||
<div class="form-text">Minimum 8 aksara, huruf besar, huruf kecil, dan nombor.</div>
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password_confirmation" class="form-label fw-semibold">Sahkan Kata Laluan <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control"
|
||||
id="password_confirmation" name="password_confirmation" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="role" class="form-label fw-semibold">Peranan <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('role') is-invalid @enderror" id="role" name="role" required>
|
||||
<option value="user" {{ old('role', 'user') === 'user' ? 'selected' : '' }}>Pengguna Jabatan</option>
|
||||
<option value="admin" {{ old('role') === 'admin' ? 'selected' : '' }}>Pentadbir</option>
|
||||
</select>
|
||||
@error('role')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="department_id" class="form-label fw-semibold">Jabatan</label>
|
||||
<select class="form-select @error('department_id') is-invalid @enderror"
|
||||
id="department_id" name="department_id">
|
||||
<option value="">— Pilih Jabatan —</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}"
|
||||
{{ old('department_id') == $dept->id ? 'selected' : '' }}>
|
||||
{{ $dept->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('department_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-person-check me-1"></i>Daftarkan Pengguna
|
||||
</button>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">Batal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
81
resources/views/admin/users/edit.blade.php
Normal file
81
resources/views/admin/users/edit.blade.php
Normal file
@@ -0,0 +1,81 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Kemaskini E-mel Pengguna')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-pencil me-2 text-primary"></i>Kemaskini E-mel Pengguna</h4>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{-- User Info Card --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light fw-semibold">Maklumat Pengguna</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr>
|
||||
<td class="text-muted" width="35%">Nama</td>
|
||||
<td class="fw-semibold">{{ $user->name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">E-mel Semasa</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Jabatan</td>
|
||||
<td>{{ $user->department?->name ?? '—' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Status</td>
|
||||
<td>
|
||||
@if($user->is_active)
|
||||
<span class="badge bg-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Dinyahaktif</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email Update Form --}}
|
||||
<div class="card">
|
||||
<div class="card-header bg-light fw-semibold">Kemaskini E-mel</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.users.update-email', $user) }}">
|
||||
@csrf @method('PATCH')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label fw-semibold">E-mel Baru <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control @error('email') is-invalid @enderror"
|
||||
id="email" name="email" value="{{ old('email', $user->email) }}" required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="justification" class="form-label fw-semibold">Justifikasi</label>
|
||||
<textarea class="form-control" id="justification" name="justification"
|
||||
rows="3" maxlength="500"
|
||||
placeholder="Nyatakan sebab perubahan e-mel...">{{ old('justification') }}</textarea>
|
||||
<div class="form-text">Perubahan ini akan direkod dalam log audit.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-envelope-check me-1"></i>Kemaskini E-mel
|
||||
</button>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">Batal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
125
resources/views/admin/users/index.blade.php
Normal file
125
resources/views/admin/users/index.blade.php
Normal file
@@ -0,0 +1,125 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Senarai Pengguna')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-people me-2 text-primary"></i>Senarai Pengguna</h4>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-1"></i>Daftar Pengguna Baru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" name="search"
|
||||
value="{{ request('search') }}" placeholder="Cari nama atau e-mel...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select form-select-sm" name="status">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Dinyahaktif</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">
|
||||
<i class="bi bi-search me-1"></i>Cari
|
||||
</button>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-sm btn-outline-secondary ms-1">Padam Carian</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nama</th>
|
||||
<th>E-mel</th>
|
||||
<th>Jabatan</th>
|
||||
<th>Status</th>
|
||||
<th>Log Masuk Terakhir</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($users as $user)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold">{{ $user->name }}</span>
|
||||
</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td>{{ $user->department?->name ?? '—' }}</td>
|
||||
<td>
|
||||
@if($user->is_active)
|
||||
<span class="badge bg-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Dinyahaktif</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
{{ $user->last_login_at?->format('d/m/Y H:i') ?? 'Belum pernah' }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ route('admin.users.edit', $user) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="Edit E-mel">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
|
||||
@if($user->is_active)
|
||||
<form method="POST" action="{{ route('admin.users.deactivate', $user) }}"
|
||||
onsubmit="return confirm('Dinyahaktifkan akaun {{ $user->name }}?')">
|
||||
@csrf @method('PATCH')
|
||||
<button class="btn btn-sm btn-outline-warning" title="Nyahaktif">
|
||||
<i class="bi bi-person-x"></i>
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<form method="POST" action="{{ route('admin.users.activate', $user) }}">
|
||||
@csrf @method('PATCH')
|
||||
<button class="btn btn-sm btn-outline-success" title="Aktifkan">
|
||||
<i class="bi bi-person-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if(! $user->hasUsage())
|
||||
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
|
||||
onsubmit="return confirm('PADAM akaun {{ $user->name }} secara kekal?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger" title="Padam">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-4 d-block mb-2"></i>Tiada pengguna ditemui.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($users->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
103
resources/views/auth/login.blade.php
Normal file
103
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log Masuk — Speech2Text MBIP</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.login-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, #0d6efd, #0a58ca);
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.login-header .logo-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo-icon"><i class="bi bi-mic-fill"></i></div>
|
||||
<h4 class="mb-0 fw-bold">Speech2Text MBIP</h4>
|
||||
<p class="mb-0 opacity-75 small mt-1">Sistem Transkripsi Audio</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<h6 class="text-muted mb-4 text-center">Sila log masuk untuk meneruskan</h6>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
@foreach($errors->all() as $error)
|
||||
<div><i class="bi bi-exclamation-circle me-1"></i>{{ $error }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label fw-semibold">E-mel</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email" name="email"
|
||||
value="{{ old('email') }}"
|
||||
required autofocus autocomplete="email">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label fw-semibold">Kata Laluan</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password" name="password"
|
||||
required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">Ingat saya</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 py-2 fw-semibold">
|
||||
<i class="bi bi-box-arrow-in-right me-2"></i>Log Masuk
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-light text-center py-2">
|
||||
<small class="text-muted">Majlis Bandaraya Ipoh © {{ date('Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
128
resources/views/layouts/admin.blade.php
Normal file
128
resources/views/layouts/admin.blade.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Admin — @yield('title', 'Pentadbir') — MBIP</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
|
||||
<style>
|
||||
body { background-color: #f1f3f5; }
|
||||
.navbar-brand { font-weight: 700; }
|
||||
.sidebar { min-height: calc(100vh - 56px); background: #212529; }
|
||||
.sidebar .nav-link { color: #adb5bd; border-radius: 4px; padding: 8px 12px; margin-bottom: 2px; }
|
||||
.sidebar .nav-link:hover { background: #343a40; color: #fff; }
|
||||
.sidebar .nav-link.active { background: #0d6efd; color: #fff; }
|
||||
.sidebar .nav-link i { width: 20px; }
|
||||
.sidebar .sidebar-heading { color: #6c757d; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; padding: 8px 12px; margin-top: 12px; }
|
||||
.main-content { min-height: calc(100vh - 56px); }
|
||||
.stat-card { border: none; border-radius: 8px; }
|
||||
</style>
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
{{-- Navbar --}}
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ route('admin.dashboard') }}">
|
||||
<i class="bi bi-shield-lock-fill me-2 text-warning"></i>Speech2Text — Pentadbir
|
||||
</a>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<span class="text-muted small me-3">{{ auth()->user()->name }}</span>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="btn btn-sm btn-outline-light" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-1"></i>Log Keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
{{-- Sidebar --}}
|
||||
<div class="col-md-2 sidebar p-3">
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"
|
||||
href="{{ route('admin.dashboard') }}">
|
||||
<i class="bi bi-speedometer2 me-2"></i>Dashboard
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading mt-2">Projek</div>
|
||||
|
||||
<a class="nav-link {{ request()->routeIs('admin.projects.*') ? 'active' : '' }}"
|
||||
href="{{ route('admin.projects.index') }}">
|
||||
<i class="bi bi-folder2 me-2"></i>Senarai Projek
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading mt-2">Pengurusan Pengguna</div>
|
||||
|
||||
<a class="nav-link {{ request()->routeIs('admin.users.*') ? 'active' : '' }}"
|
||||
href="{{ route('admin.users.index') }}">
|
||||
<i class="bi bi-people me-2"></i>Senarai Pengguna
|
||||
</a>
|
||||
<a class="nav-link {{ request()->routeIs('admin.users.create') ? 'active' : '' }}"
|
||||
href="{{ route('admin.users.create') }}">
|
||||
<i class="bi bi-person-plus me-2"></i>Daftar Pengguna
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading mt-2">Log & Laporan</div>
|
||||
|
||||
<a class="nav-link {{ request()->routeIs('admin.audit-logs.*') ? 'active' : '' }}"
|
||||
href="{{ route('admin.audit-logs.index') }}">
|
||||
<i class="bi bi-journal-text me-2"></i>Log Audit
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading mt-2">Portal</div>
|
||||
|
||||
<a class="nav-link" href="{{ route('user.dashboard') }}">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>Portal Pengguna
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{-- Main Content --}}
|
||||
<div class="col-md-10 main-content p-4">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
118
resources/views/layouts/app.blade.php
Normal file
118
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Speech2Text') — MBIP</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
|
||||
<style>
|
||||
body { background-color: #f8f9fa; }
|
||||
.navbar-brand { font-weight: 700; letter-spacing: 0.5px; }
|
||||
.sidebar { min-height: calc(100vh - 56px); background: #fff; border-right: 1px solid #dee2e6; }
|
||||
.sidebar .nav-link { color: #495057; border-radius: 6px; margin-bottom: 2px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { background: #e9ecef; color: #0d6efd; }
|
||||
.sidebar .nav-link i { width: 20px; }
|
||||
.main-content { min-height: calc(100vh - 56px); }
|
||||
.badge-status-pending { background-color: #6c757d; }
|
||||
.badge-status-processing { background-color: #fd7e14; }
|
||||
.badge-status-completed { background-color: #198754; }
|
||||
.badge-status-failed { background-color: #dc3545; }
|
||||
</style>
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
{{-- Navbar --}}
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ route('user.dashboard') }}">
|
||||
<i class="bi bi-mic-fill me-2"></i>Speech2Text MBIP
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-1"></i>
|
||||
{{ auth()->user()->name }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><span class="dropdown-item-text text-muted small">{{ auth()->user()->email }}</span></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="dropdown-item text-danger" type="submit">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>Log Keluar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
{{-- Sidebar --}}
|
||||
<div class="col-md-2 sidebar p-3">
|
||||
<nav class="nav flex-column">
|
||||
<a class="nav-link {{ request()->routeIs('user.dashboard') ? 'active' : '' }}"
|
||||
href="{{ route('user.dashboard') }}">
|
||||
<i class="bi bi-house me-2"></i>Dashboard
|
||||
</a>
|
||||
<a class="nav-link {{ request()->routeIs('user.projects.*') ? 'active' : '' }}"
|
||||
href="{{ route('user.dashboard') }}">
|
||||
<i class="bi bi-folder2 me-2"></i>Projek Saya
|
||||
</a>
|
||||
<a class="nav-link {{ request()->routeIs('user.projects.create') ? 'active' : '' }}"
|
||||
href="{{ route('user.projects.create') }}">
|
||||
<i class="bi bi-plus-circle me-2"></i>Projek Baru
|
||||
</a>
|
||||
|
||||
@if(auth()->user()->isAdmin())
|
||||
<hr class="border-secondary my-2">
|
||||
<a class="nav-link text-warning" href="{{ route('admin.dashboard') }}">
|
||||
<i class="bi bi-shield-lock me-2"></i>Portal Admin
|
||||
</a>
|
||||
@endif
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{-- Main Content --}}
|
||||
<div class="col-md-10 main-content p-4">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
117
resources/views/user/dashboard.blade.php
Normal file
117
resources/views/user/dashboard.blade.php
Normal file
@@ -0,0 +1,117 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-house me-2 text-primary"></i>Dashboard</h4>
|
||||
<a href="{{ route('user.projects.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>Projek Baru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Owned Projects --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-folder-fill me-2 text-primary"></i>Projek Saya
|
||||
<span class="badge bg-primary ms-1">{{ $ownedProjects->total() }}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tajuk</th>
|
||||
<th>Status</th>
|
||||
<th>Saiz Fail</th>
|
||||
<th>Tempoh</th>
|
||||
<th>Tarikh Cipta</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($ownedProjects as $project)
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ $project->title }}</td>
|
||||
<td>
|
||||
@php $statusClasses = ['pending'=>'secondary','processing'=>'warning','completed'=>'success','failed'=>'danger']; @endphp
|
||||
<span class="badge bg-{{ $statusClasses[$project->transcription_status] ?? 'secondary' }}">
|
||||
{{ ucfirst($project->transcription_status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $project->fileSizeForHumans() }}</td>
|
||||
<td>{{ $project->durationForHumans() ?? '—' }}</td>
|
||||
<td class="small text-muted">{{ $project->created_at->format('d/m/Y') }}</td>
|
||||
<td>
|
||||
<a href="{{ route('user.projects.show', $project) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-5">
|
||||
<i class="bi bi-mic fs-1 d-block mb-2 text-muted"></i>
|
||||
<p class="mb-2">Belum ada projek transkripsi.</p>
|
||||
<a href="#" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle me-1"></i>Muat Naik Audio Pertama
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($ownedProjects->hasPages())
|
||||
<div class="card-footer bg-white">{{ $ownedProjects->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Shared Projects --}}
|
||||
@if($sharedProjects->count() > 0)
|
||||
<div class="card">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-people-fill me-2 text-info"></i>Projek Dikongsi Bersama Saya
|
||||
<span class="badge bg-info ms-1">{{ $sharedProjects->total() }}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tajuk</th>
|
||||
<th>Pemilik</th>
|
||||
<th>Status</th>
|
||||
<th>Tarikh Cipta</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sharedProjects as $project)
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ $project->title }}</td>
|
||||
<td>{{ $project->owner->name }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ $statusClasses[$project->transcription_status] ?? 'secondary' }}">
|
||||
{{ ucfirst($project->transcription_status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-muted">{{ $project->created_at->format('d/m/Y') }}</td>
|
||||
<td>
|
||||
<a href="#" class="btn btn-sm btn-outline-info">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($sharedProjects->hasPages())
|
||||
<div class="card-footer bg-white">{{ $sharedProjects->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
118
resources/views/user/projects/create.blade.php
Normal file
118
resources/views/user/projects/create.blade.php
Normal file
@@ -0,0 +1,118 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Projek Baru')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0"><i class="bi bi-plus-circle me-2 text-primary"></i>Cipta Projek Transkripsi Baru</h4>
|
||||
<a href="{{ route('user.dashboard') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('user.projects.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label fw-semibold">Tajuk Projek <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('title') is-invalid @enderror"
|
||||
id="title" name="title" value="{{ old('title') }}"
|
||||
placeholder="cth: Mesyuarat Jabatan Mac 2025" required>
|
||||
@error('title')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label fw-semibold">Penerangan</label>
|
||||
<textarea class="form-control @error('description') is-invalid @enderror"
|
||||
id="description" name="description" rows="3"
|
||||
placeholder="Penerangan ringkas tentang audio ini...">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label fw-semibold">Bahasa Audio</label>
|
||||
<select class="form-select" id="language" name="language">
|
||||
<option value="ms" {{ old('language', 'ms') === 'ms' ? 'selected' : '' }}>Bahasa Melayu</option>
|
||||
<option value="en" {{ old('language') === 'en' ? 'selected' : '' }}>English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="audio" class="form-label fw-semibold">Fail Audio <span class="text-danger">*</span></label>
|
||||
<input type="file" class="form-control @error('audio') is-invalid @enderror"
|
||||
id="audio" name="audio" accept=".mp3,.wav,.m4a,.mp4,.aac,.ogg,.flac,.webm" required>
|
||||
<div class="form-text">
|
||||
Format disokong: MP3, WAV, M4A, MP4, AAC, OGG, FLAC, WEBM.
|
||||
Saiz maksimum: {{ config('speech2text.upload.max_mb') }}MB.
|
||||
</div>
|
||||
@error('audio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
{{-- Preview nama fail yang dipilih --}}
|
||||
<div id="file-info" class="mt-2 d-none">
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-file-earmark-music me-1"></i>
|
||||
<span id="file-name"></span>
|
||||
— <span id="file-size"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||
<i class="bi bi-upload me-1"></i>Muat Naik & Mulakan Transkripsi
|
||||
</button>
|
||||
<a href="{{ route('user.dashboard') }}" class="btn btn-outline-secondary">Batal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-semibold mb-3"><i class="bi bi-info-circle me-2 text-info"></i>Maklumat</h6>
|
||||
<ul class="small text-muted mb-0">
|
||||
<li class="mb-2">Audio akan diproses secara automatik dalam barisan antrian.</li>
|
||||
<li class="mb-2">Masa pemprosesan bergantung pada panjang audio dan saiz model.</li>
|
||||
<li class="mb-2">Anda boleh menutup halaman ini — pemprosesan akan terus berjalan.</li>
|
||||
<li class="mb-2">Notifikasi status akan dikemaskini dalam halaman projek.</li>
|
||||
<li>Fail audio disimpan secara selamat dan tidak boleh diakses umum.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
$(function () {
|
||||
$('#audio').on('change', function () {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
const mb = (file.size / 1048576).toFixed(2);
|
||||
$('#file-name').text(file.name);
|
||||
$('#file-size').text(mb + ' MB');
|
||||
$('#file-info').removeClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
$('form').on('submit', function () {
|
||||
$('#submit-btn').prop('disabled', true).html(
|
||||
'<span class="spinner-border spinner-border-sm me-2"></span>Sedang dimuat naik...'
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
465
resources/views/user/projects/show.blade.php
Normal file
465
resources/views/user/projects/show.blade.php
Normal file
@@ -0,0 +1,465 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $project->title)
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1">{{ $project->title }}</h4>
|
||||
@if($project->description)
|
||||
<p class="text-muted mb-0">{{ $project->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('user.dashboard') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Dashboard
|
||||
</a>
|
||||
@if($project->isOwnedBy(auth()->user()))
|
||||
<form method="POST" action="{{ route('user.projects.destroy', $project) }}"
|
||||
onsubmit="return confirm('Padam projek ini? Tindakan ini tidak boleh dibatalkan.')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-outline-danger">
|
||||
<i class="bi bi-trash me-1"></i>Padam Projek
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- Lajur Kiri: Audio + Transcript + Komen --}}
|
||||
<div class="col-lg-8">
|
||||
|
||||
{{-- Status Badge --}}
|
||||
@php
|
||||
$statusConfig = [
|
||||
'pending' => ['secondary', 'bi-hourglass', 'Menunggu dalam barisan...'],
|
||||
'processing' => ['warning', 'bi-gear-wide-connected', 'Sedang ditranskripkan...'],
|
||||
'completed' => ['success', 'bi-check-circle', 'Transkripsi selesai'],
|
||||
'failed' => ['danger', 'bi-x-circle', 'Transkripsi gagal'],
|
||||
];
|
||||
[$sc, $si, $sl] = $statusConfig[$project->transcription_status] ?? ['secondary', 'bi-question', 'Tidak diketahui'];
|
||||
@endphp
|
||||
|
||||
<div id="status-alert" class="alert alert-{{ $sc }} d-flex align-items-center justify-content-between mb-3 py-2">
|
||||
<span id="status-text"><i class="bi {{ $si }} me-2"></i>{{ $sl }}</span>
|
||||
@if($project->isFailed() && $project->isOwnedBy(auth()->user()))
|
||||
<form method="POST" action="{{ route('user.projects.retry', $project) }}" id="retry-form">
|
||||
@csrf
|
||||
<button class="btn btn-sm btn-danger">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Cuba Semula
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if($project->isProcessing() || $project->isPending())
|
||||
<span id="status-spinner" class="spinner-border spinner-border-sm text-{{ $sc }}"></span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Audio Player --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-music-note-beamed me-2 text-primary"></i>Audio Asal
|
||||
<span class="text-muted fw-normal small ms-2">{{ $project->original_filename }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<audio controls class="w-100"
|
||||
src="{{ route('user.projects.audio.stream', $project) }}"
|
||||
preload="metadata">
|
||||
Pelayar anda tidak menyokong audio HTML5.
|
||||
</audio>
|
||||
<div class="mt-2 small text-muted">
|
||||
<i class="bi bi-hdd me-1"></i>{{ $project->fileSizeForHumans() }}
|
||||
@if($project->durationForHumans())
|
||||
• <i class="bi bi-clock me-1"></i>{{ $project->durationForHumans() }}
|
||||
@endif
|
||||
• <i class="bi bi-calendar me-1"></i>{{ $project->created_at->format('d/m/Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Transcript Card --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold"><i class="bi bi-file-text me-2 text-primary"></i>Teks Transkripsi</span>
|
||||
<div class="d-flex gap-2">
|
||||
@if($project->transcriptVersions->count() > 0)
|
||||
<a href="{{ route('user.projects.versions.index', $project) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-clock-history me-1"></i>Sejarah Versi
|
||||
<span class="badge bg-secondary ms-1">{{ $project->transcriptVersions->count() }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@can('editTranscript', $project)
|
||||
@if($project->isCompleted() && $project->transcript_text)
|
||||
<button class="btn btn-sm btn-outline-primary" id="btn-edit-toggle"
|
||||
onclick="toggleEditor()">
|
||||
<i class="bi bi-pencil me-1"></i>Edit
|
||||
</button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{{-- Read mode --}}
|
||||
<div id="transcript-read">
|
||||
@if($project->isCompleted() && $project->transcript_text)
|
||||
<div class="p-3 bg-light rounded" style="white-space: pre-wrap; font-size: 0.95rem; line-height: 1.8;">{{ $project->transcript_text }}</div>
|
||||
@if($project->transcript_confidence)
|
||||
<div class="mt-2 small text-muted">
|
||||
<i class="bi bi-bar-chart me-1"></i>
|
||||
Keyakinan: {{ number_format($project->transcript_confidence * 100, 1) }}%
|
||||
• Enjin: {{ $project->transcription_engine ?? '—' }}
|
||||
</div>
|
||||
@endif
|
||||
@elseif($project->isFailed())
|
||||
<div class="text-danger small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
{{ $project->error_message ?? 'Ralat tidak diketahui.' }}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted mb-0 text-center py-3">
|
||||
<i class="bi bi-hourglass-split fs-4 d-block mb-2"></i>
|
||||
Teks transkripsi akan dipaparkan di sini setelah pemprosesan selesai.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Edit mode (hidden by default) --}}
|
||||
@can('editTranscript', $project)
|
||||
<div id="transcript-edit" style="display:none">
|
||||
<form method="POST" action="{{ route('user.projects.transcript.update', $project) }}">
|
||||
@csrf @method('PATCH')
|
||||
<textarea class="form-control mb-2" name="transcript_text" rows="15"
|
||||
style="font-size:0.92rem; line-height:1.7">{{ old('transcript_text', $project->transcript_text) }}</textarea>
|
||||
<div class="mb-2">
|
||||
<input type="text" class="form-control form-control-sm" name="change_summary"
|
||||
placeholder="Ringkasan perubahan (pilihan)">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-save me-1"></i>Simpan
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleEditor()">
|
||||
Batal
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Card footer: Upload transkripsi luar --}}
|
||||
@can('editTranscript', $project)
|
||||
<div class="card-footer bg-white border-top-0 pt-0">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
|
||||
data-bs-target="#modalUploadTranscript">
|
||||
<i class="bi bi-upload me-1"></i>Muat Naik Transkripsi Luar (.txt)
|
||||
</button>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Komen --}}
|
||||
@can('view', [App\Models\ProjectComment::class, $project])
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-chat-left-text me-2 text-secondary"></i>Perbincangan
|
||||
@if($project->comments->count())
|
||||
<span class="badge bg-secondary ms-1">{{ $project->comments->count() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- Senarai komen --}}
|
||||
@forelse($project->comments as $comment)
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge bg-secondary rounded-circle p-2" style="font-size:0.75rem">
|
||||
{{ mb_strtoupper(mb_substr($comment->user->name, 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="fw-semibold small">{{ $comment->user->name }}</span>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="text-muted" style="font-size:0.75rem">{{ $comment->created_at->format('d/m/Y H:i') }}</span>
|
||||
@can('delete', $comment)
|
||||
<form method="POST"
|
||||
action="{{ route('user.projects.comments.destroy', [$project, $comment]) }}"
|
||||
onsubmit="return confirm('Padam komen ini?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-link btn-sm text-danger p-0 ms-1" title="Padam">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1" style="white-space:pre-wrap; font-size:0.9rem">{{ $comment->message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-muted small mb-3">Tiada perbincangan lagi.</p>
|
||||
@endforelse
|
||||
|
||||
{{-- Form tambah komen --}}
|
||||
@can('create', [App\Models\ProjectComment::class, $project])
|
||||
<hr class="my-2">
|
||||
<form method="POST" action="{{ route('user.projects.comments.store', $project) }}">
|
||||
@csrf
|
||||
<div class="mb-2">
|
||||
<textarea class="form-control form-control-sm @error('message') is-invalid @enderror"
|
||||
name="message" rows="2" placeholder="Tulis komen..." required></textarea>
|
||||
@error('message')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-send me-1"></i>Hantar
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Lajur Kanan: Maklumat + Collaborators --}}
|
||||
<div class="col-lg-4">
|
||||
|
||||
{{-- Project Info --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-info-circle me-2 text-secondary"></i>Maklumat Projek
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr>
|
||||
<td class="text-muted ps-3" width="40%">Pemilik</td>
|
||||
<td class="fw-semibold">{{ $project->owner->name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted ps-3">Bahasa</td>
|
||||
<td>{{ strtoupper($project->language) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted ps-3">Enjin</td>
|
||||
<td>{{ $project->transcription_engine ?? '—' }}</td>
|
||||
</tr>
|
||||
@if($project->processed_at)
|
||||
<tr>
|
||||
<td class="text-muted ps-3">Selesai pada</td>
|
||||
<td class="small">{{ $project->processed_at->format('d/m/Y H:i') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Collaborators --}}
|
||||
<div class="card">
|
||||
<div class="card-header bg-white fw-semibold">
|
||||
<i class="bi bi-people me-2 text-secondary"></i>Collaborators
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@forelse($project->collaborators as $collab)
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<span class="fw-semibold small">{{ $collab->name }}</span>
|
||||
<span class="badge bg-light text-dark ms-1" style="font-size:0.7rem">{{ $collab->pivot->role }}</span>
|
||||
<div class="text-muted" style="font-size:0.75rem">{{ $collab->email }}</div>
|
||||
</div>
|
||||
@if($project->isOwnedBy(auth()->user()))
|
||||
<form method="POST"
|
||||
action="{{ route('user.projects.collaborators.destroy', [$project, $collab]) }}"
|
||||
onsubmit="return confirm('Buang {{ $collab->name }} daripada collaborator?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger py-0 px-1" title="Buang">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-muted small mb-2">Tiada collaborator.</p>
|
||||
@endforelse
|
||||
|
||||
@if($project->isOwnedBy(auth()->user()))
|
||||
<hr class="my-2">
|
||||
<form method="POST" action="{{ route('user.projects.collaborators.store', $project) }}">
|
||||
@csrf
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="email" class="form-control @error('email') is-invalid @enderror"
|
||||
name="email" placeholder="emel@mbip.my" required>
|
||||
<button class="btn btn-outline-primary" type="submit">
|
||||
<i class="bi bi-person-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
@error('email')
|
||||
<div class="text-danger small">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="mb-2">
|
||||
<select class="form-select form-select-sm" name="role">
|
||||
<option value="editor">Editor — boleh edit teks</option>
|
||||
<option value="viewer">Viewer — lihat sahaja</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modal: Upload Transkripsi Luar --}}
|
||||
@can('editTranscript', $project)
|
||||
<div class="modal fade" id="modalUploadTranscript" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Muat Naik Transkripsi Luar</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST"
|
||||
action="{{ route('user.projects.transcript.upload', $project) }}"
|
||||
enctype="multipart/form-data"
|
||||
id="form-upload-transcript"
|
||||
data-has-transcript="{{ $project->transcript_text ? 'true' : 'false' }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Fail Transkripsi (.txt) <span class="text-danger">*</span></label>
|
||||
<input type="file" class="form-control @error('transcript_file') is-invalid @enderror"
|
||||
name="transcript_file" accept=".txt,text/plain" required>
|
||||
<div class="form-text">Hanya fail .txt, maksimum 10 MB. Pengekodan UTF-8 disyorkan.</div>
|
||||
@error('transcript_file')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if(config('speech2text.ollama.enabled'))
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="clean_with_ollama"
|
||||
value="1" id="chkOllama">
|
||||
<label class="form-check-label" for="chkOllama">
|
||||
<i class="bi bi-stars text-warning me-1"></i>
|
||||
Bersihkan teks dengan Ollama (AI)
|
||||
</label>
|
||||
<div class="form-text">Teks akan dibaiki ejaan dan tanda baca menggunakan model {{ config('speech2text.ollama.model') }}.</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($project->transcript_text)
|
||||
<div class="alert alert-warning py-2 small mb-0">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Projek ini sudah mempunyai transkripsi. Memuat naik fail baru akan <strong>menggantikan</strong> teks sedia ada dan menyimpan versi lama dalam sejarah.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-upload me-1"></i>Muat Naik
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
#transcript-read .bg-light { font-family: 'Segoe UI', sans-serif; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
|
||||
{{-- AJAX status polling --}}
|
||||
@if($project->isPending() || $project->isProcessing())
|
||||
<script>
|
||||
(function () {
|
||||
const statusUrl = "{{ route('user.projects.status', $project) }}";
|
||||
const alertEl = document.getElementById('status-alert');
|
||||
const textEl = document.getElementById('status-text');
|
||||
const spinnerEl = document.getElementById('status-spinner');
|
||||
|
||||
const statusMap = {
|
||||
pending: { cls: 'warning', icon: 'bi-hourglass', label: 'Menunggu dalam barisan...', spin: true },
|
||||
processing: { cls: 'warning', icon: 'bi-gear-wide-connected', label: 'Sedang ditranskripkan...', spin: true },
|
||||
completed: { cls: 'success', icon: 'bi-check-circle', label: 'Transkripsi selesai', spin: false },
|
||||
failed: { cls: 'danger', icon: 'bi-x-circle', label: 'Transkripsi gagal', spin: false },
|
||||
};
|
||||
|
||||
let timer;
|
||||
|
||||
function poll() {
|
||||
fetch(statusUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const cfg = statusMap[data.status] ?? statusMap.pending;
|
||||
alertEl.className = `alert alert-${cfg.cls} d-flex align-items-center justify-content-between mb-3 py-2`;
|
||||
textEl.innerHTML = `<i class="bi ${cfg.icon} me-2"></i>${cfg.label}`;
|
||||
if (spinnerEl) spinnerEl.style.display = cfg.spin ? '' : 'none';
|
||||
if (data.status === 'completed' || data.status === 'failed') {
|
||||
clearInterval(timer);
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
timer = setInterval(poll, 8000);
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
{{-- Upload transcript modal: confirm if already has transcript --}}
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('form-upload-transcript');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
if (form.dataset.hasTranscript === 'true') {
|
||||
if (!confirm('Projek ini sudah ada transkripsi. Adakah anda pasti ingin menggantikannya? Versi lama akan disimpan dalam sejarah versi.')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{-- Transcript editor toggle --}}
|
||||
<script>
|
||||
function toggleEditor() {
|
||||
const read = document.getElementById('transcript-read');
|
||||
const edit = document.getElementById('transcript-edit');
|
||||
const btnTgl = document.getElementById('btn-edit-toggle');
|
||||
if (!read || !edit) return;
|
||||
const editing = edit.style.display !== 'none';
|
||||
read.style.display = editing ? '' : 'none';
|
||||
edit.style.display = editing ? 'none' : '';
|
||||
if (btnTgl) btnTgl.innerHTML = editing
|
||||
? '<i class="bi bi-pencil me-1"></i>Edit'
|
||||
: '<i class="bi bi-eye me-1"></i>Lihat';
|
||||
}
|
||||
</script>
|
||||
|
||||
@endpush
|
||||
109
resources/views/user/projects/versions.blade.php
Normal file
109
resources/views/user/projects/versions.blade.php
Normal file
@@ -0,0 +1,109 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Sejarah Versi — ' . $project->title)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1"><i class="bi bi-clock-history me-2 text-secondary"></i>Sejarah Versi</h5>
|
||||
<p class="text-muted mb-0 small">{{ $project->title }}</p>
|
||||
</div>
|
||||
<a href="{{ route('user.projects.show', $project) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Kembali ke Projek
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($versions->isEmpty())
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>Tiada sejarah versi untuk projek ini.
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:60px">Versi</th>
|
||||
<th>Penyunting</th>
|
||||
<th>Ringkasan Perubahan</th>
|
||||
<th>Tarikh</th>
|
||||
<th style="width:100px">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($versions as $version)
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-secondary">v{{ $version->version_number }}</span>
|
||||
</td>
|
||||
<td>{{ $version->editor->name ?? '—' }}</td>
|
||||
<td class="text-muted small">{{ $version->change_summary ?? '—' }}</td>
|
||||
<td class="small text-muted">{{ $version->created_at->format('d/m/Y H:i') }}</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalPreview{{ $version->id }}"
|
||||
title="Lihat teks">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
@can('restoreVersion', $project)
|
||||
<form method="POST"
|
||||
action="{{ route('user.projects.versions.restore', [$project, $version]) }}"
|
||||
onsubmit="return confirm('Pulihkan ke versi {{ $version->version_number }}? Teks semasa akan disimpan sebagai versi baru.')">
|
||||
@csrf
|
||||
<button class="btn btn-sm btn-outline-warning" title="Pulihkan versi ini">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ $versions->links() }}
|
||||
|
||||
{{-- Modals: preview versi --}}
|
||||
@foreach($versions as $version)
|
||||
<div class="modal fade" id="modalPreview{{ $version->id }}" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">
|
||||
Versi {{ $version->version_number }}
|
||||
<span class="text-muted fw-normal small ms-2">{{ $version->created_at->format('d/m/Y H:i') }}</span>
|
||||
</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="p-3 bg-light rounded" style="white-space: pre-wrap; font-size: 0.9rem; line-height: 1.8; max-height: 60vh; overflow-y: auto;">{{ $version->new_text }}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Tutup</button>
|
||||
@can('restoreVersion', $project)
|
||||
<form method="POST"
|
||||
action="{{ route('user.projects.versions.restore', [$project, $version]) }}"
|
||||
onsubmit="return confirm('Pulihkan ke versi {{ $version->version_number }}?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Pulihkan Versi Ini
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
223
resources/views/welcome.blade.php
Normal file
223
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user