391
README.md
391
README.md
@@ -1,61 +1,366 @@
|
|||||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
# Asset Management System
|
||||||
|
|
||||||
<p align="center">
|

|
||||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## About Laravel
|
Sistem Manajemen Aset berbasis Laravel untuk pencatatan aset fisik end-to-end: **registrasi aset + identifikasi (QR/RFID/NFC), movement, disposal + reverse, maintenance, audit/stocktake, approval workflow, laporan (Excel/PDF), changelog, dan pengaturan sistem berbasis database**.
|
||||||
|
|
||||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
Project ini dibuat dengan fokus pada **best practice**, **clean code**, dan siap untuk dikembangkan menjadi open-source.
|
||||||
|
|
||||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
## Daftar Isi
|
||||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
|
||||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
|
||||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
|
||||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
|
||||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
|
||||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
|
||||||
|
|
||||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
- [Fitur Utama](#fitur-utama)
|
||||||
|
- [Arsitektur Singkat](#arsitektur-singkat)
|
||||||
|
- [Kebutuhan Sistem](#kebutuhan-sistem)
|
||||||
|
- [Quick Start (Local Development)](#quick-start-local-development)
|
||||||
|
- [Setup Wizard (First Run)](#setup-wizard-first-run)
|
||||||
|
- [Akun Default (Sample Data)](#akun-default-sample-data)
|
||||||
|
- [RBAC (Role & Permission)](#rbac-role--permission)
|
||||||
|
- [System Settings (Database-driven)](#system-settings-database-driven)
|
||||||
|
- [Identifikasi Aset (QR/RFID/NFC)](#identifikasi-aset-qrrfidnfc)
|
||||||
|
- [Transaksi Aset](#transaksi-aset)
|
||||||
|
- [Approval Workflow](#approval-workflow)
|
||||||
|
- [Authentication, 2FA, Session Timeout](#authentication-2fa-session-timeout)
|
||||||
|
- [Audit Trail & Logging](#audit-trail--logging)
|
||||||
|
- [Import/Export Aset](#importexport-aset)
|
||||||
|
- [Laporan (Excel & PDF)](#laporan-excel--pdf)
|
||||||
|
- [Landing Page & Public Asset View (QR Scan)](#landing-page--public-asset-view-qr-scan)
|
||||||
|
- [Notifikasi Email (SMTP)](#notifikasi-email-smtp)
|
||||||
|
- [Notifikasi Terjadwal (Command)](#notifikasi-terjadwal-command)
|
||||||
|
- [Maintenance Mode (Read-only)](#maintenance-mode-read-only)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Deployment Notes](#deployment-notes)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Dokumentasi Lengkap](#dokumentasi-lengkap)
|
||||||
|
|
||||||
## Learning Laravel
|
## Fitur Utama
|
||||||
|
|
||||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
**1) User Management + RBAC (Spatie Permission)**
|
||||||
|
- Role & permission berbasis UUID
|
||||||
|
- Pembatasan akses per modul melalui middleware `permission:*`
|
||||||
|
|
||||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
**2) Master Data**
|
||||||
|
- Status Aset, Kelas Aset, Unit, Departemen, Person in Charge, Pengguna Aset, Kategori, Lokasi, Warranty, Vendor Contract
|
||||||
|
|
||||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
**3) Manajemen Aset**
|
||||||
|
- Registrasi aset dengan kode otomatis
|
||||||
|
- Foto/thumbnail aset (multi foto) tersimpan di storage
|
||||||
|
- QR code otomatis menuju halaman publik detail aset (tanpa login)
|
||||||
|
- RFID/NFC tag (opsional + bisa auto-generate via setting)
|
||||||
|
- Arsip/retensi + soft delete + restore
|
||||||
|
|
||||||
## Laravel Sponsors
|
**4) Transaksi Aset**
|
||||||
|
- Movement (mutasi lokasi/departemen/user)
|
||||||
|
- Disposal + Reverse Disposal
|
||||||
|
- Audit/Stocktake
|
||||||
|
- Maintenance (planned/in_progress/completed + dukungan flow approval `pending`)
|
||||||
|
|
||||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
**5) Approval Workflow**
|
||||||
|
- Permintaan approval untuk movement/disposal/maintenance (pending/approved/rejected)
|
||||||
|
- Halaman approvals untuk approver
|
||||||
|
|
||||||
### Premium Partners
|
**6) Laporan**
|
||||||
|
- Export Excel (multi sheet) + PDF untuk aset dan transaksi
|
||||||
|
- Filter laporan (date range, lokasi, dsb.) sesuai halaman laporan
|
||||||
|
|
||||||
- **[Vehikl](https://vehikl.com/)**
|
**7) Landing & Dokumentasi**
|
||||||
- **[Tighten Co.](https://tighten.co)**
|
- Landing one-page untuk memperkenalkan sistem
|
||||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
- Public asset view untuk QR scan: `GET /asset-view/{asset}`
|
||||||
- **[64 Robots](https://64robots.com)**
|
- Dokumentasi API internal app: `GET /api-docs`
|
||||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
|
||||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
## Arsitektur Singkat
|
||||||
- **[Redberry](https://redberry.international/laravel-development/)**
|
|
||||||
- **[Active Logic](https://activelogic.com)**
|
- **Framework:** Laravel
|
||||||
|
- **Template UI:** Vyzor (Bootstrap)
|
||||||
|
- **Settings:** disimpan di tabel `settings` dan diakses melalui `App\Services\System\SystemSettingService`
|
||||||
|
- **Audit log:** file log khusus channel `asset_activity` (bukan database)
|
||||||
|
- **UUID:** hampir seluruh tabel inti menggunakan UUID sebagai primary key
|
||||||
|
|
||||||
|
Folder penting:
|
||||||
|
- `app/Services/` → business logic (tanpa repository pattern)
|
||||||
|
- `app/Http/Controllers/` → controller tipis, delegasi ke service
|
||||||
|
- `database/seeders/` → sample data
|
||||||
|
- `resources/views/` → Blade UI
|
||||||
|
|
||||||
|
## Kebutuhan Sistem
|
||||||
|
|
||||||
|
- PHP 8.2+ (sesuaikan dengan `composer.json`)
|
||||||
|
- Composer
|
||||||
|
- Node.js + NPM (Vite build)
|
||||||
|
- Database: MySQL/MariaDB (production) / SQLite (testing)
|
||||||
|
- (Opsional) Laragon untuk dev di Windows
|
||||||
|
|
||||||
|
## Quick Start (Local Development)
|
||||||
|
|
||||||
|
1) Install dependency PHP & JS
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Siapkan `.env`
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
3) Jalankan Vite
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4) Jalankan aplikasi
|
||||||
|
```bash
|
||||||
|
php artisan serve
|
||||||
|
```
|
||||||
|
|
||||||
|
5) Buat symbolic link storage (wajib untuk foto aset & QR)
|
||||||
|
```bash
|
||||||
|
php artisan storage:link
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Wizard (First Run)
|
||||||
|
|
||||||
|
Saat aplikasi pertama kali dijalankan dan **koneksi database belum siap**, middleware akan mengarahkan ke wizard:
|
||||||
|
- `GET /setup`
|
||||||
|
|
||||||
|
Wizard terdiri dari 2 tahap besar:
|
||||||
|
|
||||||
|
1) **Database Setup**
|
||||||
|
- Aplikasi akan menampilkan panduan mengisi `.env` (DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD)
|
||||||
|
- Tombol migrasi tersedia:
|
||||||
|
- `Migrate & Seed Minimal (RBAC)` → seed `RbacSeeder` + `SettingSeeder`
|
||||||
|
- `Migrate + Sample Data (Full Seed)` → seed `DatabaseSeeder`
|
||||||
|
|
||||||
|
2) **Wizard Aplikasi (4 langkah)**
|
||||||
|
- Step 1: buat user admin
|
||||||
|
- Step 2: isi nilai setting (form dibentuk dari data di tabel `settings`)
|
||||||
|
- Step 3: buat master data dasar
|
||||||
|
- Step 4: buat aset pertama (sample)
|
||||||
|
|
||||||
|
## Akun Default (Sample Data)
|
||||||
|
|
||||||
|
Jika memilih **Full Seed**, akun admin default dibuat oleh `database/seeders/RbacSeeder.php`:
|
||||||
|
- Email: `admin@example.com`
|
||||||
|
- Password: `password123`
|
||||||
|
|
||||||
|
## RBAC (Role & Permission)
|
||||||
|
|
||||||
|
RBAC memakai `spatie/laravel-permission` yang sudah disesuaikan untuk UUID:
|
||||||
|
- Primary key role/permission: `uuid`
|
||||||
|
- Pivot model: `model_uuid` (lihat `config/permission.php`)
|
||||||
|
|
||||||
|
Permission dasar (seed):
|
||||||
|
- `settings.manage`
|
||||||
|
- `users.manage`, `roles.manage`, `permissions.manage`
|
||||||
|
- `assets.view`, `assets.manage`
|
||||||
|
- `movements.manage`, `disposals.manage`, `audits.manage`, `maintenance.manage`
|
||||||
|
- `reports.view`, `approvals.manage`
|
||||||
|
|
||||||
|
Role dasar (seed):
|
||||||
|
- `super-admin`, `asset-manager`, `auditor`, `maintenance`, `viewer`
|
||||||
|
|
||||||
|
Catatan scoping data:
|
||||||
|
- Model `Asset` memiliki `scopeForUser()` dan `isVisibleTo()` untuk pembatasan berbasis departemen/lokasi.
|
||||||
|
- (Opsional) Permission `assets.view_all` dapat ditambahkan untuk bypass scoping (lihat `app/Models/Asset.php`).
|
||||||
|
|
||||||
|
## System Settings (Database-driven)
|
||||||
|
|
||||||
|
Setting disimpan di tabel `settings` dan dapat dikelola lewat:
|
||||||
|
- Dashboard: `GET /dashboard/settings`
|
||||||
|
- Setup Wizard step 2: `GET /setup?step=2`
|
||||||
|
|
||||||
|
Setting dibaca via `App\Services\System\SystemSettingService`:
|
||||||
|
- Default berasal dari `config/system.php`
|
||||||
|
- Nilai DB akan override default
|
||||||
|
|
||||||
|
Contoh setting penting (lihat `database/seeders/SettingSeeder.php`):
|
||||||
|
- `asset.code_prefix`, `asset.qr_enabled`, `asset.qr_format`, `asset.qr_size`
|
||||||
|
- `asset.rfid_enabled`, `asset.nfc_enabled`, `asset.rfid_auto_generate`, `asset.nfc_auto_generate`
|
||||||
|
- `maintenance.readonly_mode`
|
||||||
|
- `security.audit_log`, `security.session_idle_minutes`, `security.two_factor_enforced`
|
||||||
|
- `integration.api_key`, `integration.webhook_*`
|
||||||
|
- `asset.import_*` (import limit/dedupe/default lookup)
|
||||||
|
|
||||||
|
## Identifikasi Aset (QR/RFID/NFC)
|
||||||
|
|
||||||
|
**QR Code**
|
||||||
|
- Dibuat otomatis saat aset dibuat/di-update (jika `asset.qr_enabled = true`)
|
||||||
|
- Tersimpan di `storage/app/public/qr/`
|
||||||
|
- Isi QR mengarah ke public asset view: `route('assets.public.show')` (`/asset-view/{asset}`)
|
||||||
|
|
||||||
|
**RFID/NFC**
|
||||||
|
- Disimpan sebagai `rfid_tag` dan `nfc_tag` di tabel `assets`
|
||||||
|
- Bisa diwajibkan (`asset.rfid_required` / `asset.nfc_required`) dan/atau auto-generate (`asset.rfid_auto_generate` / `asset.nfc_auto_generate`) oleh `AssetService`.
|
||||||
|
|
||||||
|
## Transaksi Aset
|
||||||
|
|
||||||
|
1) **Movement**
|
||||||
|
- Membuat record `asset_movements`
|
||||||
|
- Jika auto-approve aktif, aset akan ikut berubah: lokasi/departemen/user
|
||||||
|
|
||||||
|
2) **Disposal**
|
||||||
|
- Membuat record `asset_disposals`
|
||||||
|
- Jika approved, aset diarahkan ke status `DISPOSED` (jika status ini ada)
|
||||||
|
- Reverse disposal mengembalikan status/lokasi/departemen sebelumnya
|
||||||
|
|
||||||
|
3) **Audit/Stocktake**
|
||||||
|
- Membuat record `asset_audits` (matched/missing/damaged)
|
||||||
|
|
||||||
|
4) **Maintenance**
|
||||||
|
- Membuat record `asset_maintenances`
|
||||||
|
- Flow status: `pending` (menunggu approval) → `planned` → `in_progress` → `completed`
|
||||||
|
|
||||||
|
## Approval Workflow
|
||||||
|
|
||||||
|
Approval disimpan di tabel `asset_approval_requests` (morph ke movement/disposal/maintenance):
|
||||||
|
- Status: `pending`, `approved`, `rejected`
|
||||||
|
|
||||||
|
Halaman approvals:
|
||||||
|
- `GET /dashboard/approvals` (butuh permission `approvals.manage`)
|
||||||
|
|
||||||
|
## Authentication, 2FA, Session Timeout
|
||||||
|
|
||||||
|
**Login/Register**
|
||||||
|
- Login: `GET /login` / `POST /login`
|
||||||
|
- Register: `GET /register` / `POST /register`
|
||||||
|
- Logout: `POST /logout`
|
||||||
|
|
||||||
|
**2FA (TOTP 6 digit)**
|
||||||
|
- User dapat mengaktifkan 2FA dari menu profile:
|
||||||
|
- `GET /dashboard/profile`
|
||||||
|
- Flow login:
|
||||||
|
- Jika `two_factor_enabled = true`, setelah password benar user akan diarahkan ke:
|
||||||
|
- `GET /two-factor-challenge`
|
||||||
|
- User memasukkan 6 digit dari aplikasi authenticator (Google Authenticator/1Password/Authy) melalui:
|
||||||
|
- `POST /two-factor-challenge`
|
||||||
|
|
||||||
|
**Session Timeout (idle timeout)**
|
||||||
|
- Middleware `session.timeout` memaksa logout ketika tidak ada aktivitas selama X menit.
|
||||||
|
- Setting terkait:
|
||||||
|
- `security.session_idle_minutes`
|
||||||
|
|
||||||
|
## Audit Trail & Logging
|
||||||
|
|
||||||
|
**1) Request Audit (End-to-end)**
|
||||||
|
- Middleware `audit.request` mencatat request non-GET beserta durasi response.
|
||||||
|
- Disimpan via `ActivityLogger::audit()`.
|
||||||
|
|
||||||
|
**2) Log Aktivitas Domain**
|
||||||
|
- Helper:
|
||||||
|
- `asset_activity_log(...)`
|
||||||
|
- `asset_request_log(...)`
|
||||||
|
- Service: `app/Services/Logging/ActivityLogger.php`
|
||||||
|
|
||||||
|
**Output log**
|
||||||
|
- Channel: `asset_activity`
|
||||||
|
- Lokasi: `storage/logs/asset-activity.log` (daily rotation)
|
||||||
|
- ENV yang relevan:
|
||||||
|
- `ASSET_LOG_FILE`, `ASSET_LOG_LEVEL`, `ASSET_LOG_DAYS`
|
||||||
|
|
||||||
|
## Import/Export Aset
|
||||||
|
|
||||||
|
**Import**
|
||||||
|
- UI import ada di halaman daftar aset (`/dashboard/assets`)
|
||||||
|
- Mendukung:
|
||||||
|
- `Preview` (tanpa menyimpan) + ringkasan create/update/invalid
|
||||||
|
- `Import` langsung (create/update)
|
||||||
|
- Gambar dari:
|
||||||
|
- URL (`image_url`) *(jika `asset.import_allow_remote_images = true`)*
|
||||||
|
- ZIP upload (`images_zip`) dengan mapping nama file (`image_file`)
|
||||||
|
|
||||||
|
Sample file import tersedia:
|
||||||
|
- `public/samples/assets_import_sample.csv`
|
||||||
|
|
||||||
|
**Export**
|
||||||
|
- CSV export (limit 2000 rows) tersedia via route:
|
||||||
|
- `GET /dashboard/assets-export`
|
||||||
|
|
||||||
|
## Laporan (Excel & PDF)
|
||||||
|
|
||||||
|
Menu laporan:
|
||||||
|
- `GET /dashboard/reports/assets`
|
||||||
|
- `GET /dashboard/reports/movements`
|
||||||
|
- `GET /dashboard/reports/disposals`
|
||||||
|
- `GET /dashboard/reports/audits`
|
||||||
|
|
||||||
|
Export:
|
||||||
|
- Excel: multi-sheet (termasuk relasi utama)
|
||||||
|
- PDF: layout khusus report
|
||||||
|
|
||||||
|
## Landing Page & Public Asset View (QR Scan)
|
||||||
|
|
||||||
|
Landing:
|
||||||
|
- `GET /` atau `GET /landing`
|
||||||
|
|
||||||
|
Public asset view (tanpa login):
|
||||||
|
- `GET /asset-view/{asset}`
|
||||||
|
|
||||||
|
Transaksi dari halaman public (wajib login + permission terkait):
|
||||||
|
- Movement: `POST /asset-view/{asset}/movement`
|
||||||
|
- Disposal: `POST /asset-view/{asset}/disposal`
|
||||||
|
- Maintenance: `POST /asset-view/{asset}/maintenance`
|
||||||
|
|
||||||
|
## Notifikasi Email (SMTP)
|
||||||
|
|
||||||
|
Notifikasi saat approval menggunakan **Laravel Notifications** (channel `mail`):
|
||||||
|
- Approver menerima email “approval requested”
|
||||||
|
- Requester menerima email “approval approved/rejected”
|
||||||
|
|
||||||
|
Agar email terkirim, konfigurasi SMTP pada `.env` (contoh):
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=smtp.mailtrap.io
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=your_username
|
||||||
|
MAIL_PASSWORD=your_password
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS="noreply@example.com"
|
||||||
|
MAIL_FROM_NAME="Asset Management System"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notifikasi Terjadwal (Command)
|
||||||
|
|
||||||
|
Tersedia command untuk reminder sederhana (mencatat ke log `asset_activity`):
|
||||||
|
```bash
|
||||||
|
php artisan asset:notify-due
|
||||||
|
```
|
||||||
|
|
||||||
|
Anda bisa menjadwalkannya via cron (contoh setiap hari jam 08:00):
|
||||||
|
```cron
|
||||||
|
0 8 * * * cd /path/to/project && php artisan asset:notify-due >> /dev/null 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance Mode (Read-only)
|
||||||
|
|
||||||
|
Jika setting `maintenance.readonly_mode = true`:
|
||||||
|
- Middleware `maintenance.readonly` akan memblokir aksi tulis.
|
||||||
|
- Dashboard menampilkan badge “Mode Read-only Aktif”.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Menjalankan test (SQLite):
|
||||||
|
```bash
|
||||||
|
php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
Checklist singkat production:
|
||||||
|
- `APP_ENV=production`, `APP_DEBUG=false`, `APP_KEY` sudah terisi
|
||||||
|
- `php artisan migrate --force`
|
||||||
|
- `php artisan storage:link`
|
||||||
|
- `npm run build` (atau gunakan build artifact yang sudah ada di `public/build`)
|
||||||
|
- Pastikan permission folder:
|
||||||
|
- `storage/` dan `bootstrap/cache/` writable
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
Kontribusi sangat diterima.
|
||||||
|
- Gunakan style & struktur yang sudah ada (controller tipis, logic di service)
|
||||||
|
- Tambahkan test untuk fitur baru bila memungkinkan
|
||||||
|
- Buat PR kecil dan fokus per fitur
|
||||||
|
|
||||||
## Code of Conduct
|
## Dokumentasi Lengkap
|
||||||
|
|
||||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
Dokumentasi teknis terperinci (alur, setting, workflow, integrasi, notifikasi, dsb.) tersedia di:
|
||||||
|
- `documentation/guide.md`
|
||||||
## Security Vulnerabilities
|
|
||||||
|
|
||||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
|
||||||
|
|||||||
@@ -2,66 +2,24 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use App\Services\Dashboard\DashboardService;
|
||||||
use App\Models\Asset;
|
use Illuminate\View\View;
|
||||||
use App\Models\AssetStatus;
|
|
||||||
use App\Models\AssetLocation;
|
|
||||||
use App\Models\AssetMovement;
|
|
||||||
use App\Models\AssetDisposal;
|
|
||||||
use App\Models\AssetAudit;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class DashboardsController extends Controller
|
class DashboardsController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly DashboardService $dashboard)
|
||||||
public function index()
|
|
||||||
{
|
{
|
||||||
$now = Carbon::now();
|
|
||||||
$last30 = $now->copy()->subDays(30);
|
|
||||||
|
|
||||||
$totalAssets = Asset::count();
|
|
||||||
$movementCount = AssetMovement::where('created_at', '>=', $last30)->count();
|
|
||||||
$disposalCount = AssetDisposal::whereNull('reversed_at')->count();
|
|
||||||
$auditIssues = AssetAudit::whereIn('status', ['missing','damaged'])->count();
|
|
||||||
|
|
||||||
$byStatus = AssetStatus::orderBy('name')
|
|
||||||
->get()
|
|
||||||
->map(function ($status) {
|
|
||||||
return [
|
|
||||||
'label' => $status->name,
|
|
||||||
'value' => $status->assets()->count(),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$byLocation = AssetLocation::orderBy('name')
|
|
||||||
->get()
|
|
||||||
->map(function ($loc) {
|
|
||||||
return [
|
|
||||||
'label' => $loc->name,
|
|
||||||
'value' => $loc->assets()->count(),
|
|
||||||
];
|
|
||||||
})->filter(fn($row) => $row['value'] > 0)->values();
|
|
||||||
|
|
||||||
$recentAudits = AssetAudit::with('asset')
|
|
||||||
->latest()
|
|
||||||
->take(5)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$recentDisposals = AssetDisposal::with('asset')
|
|
||||||
->latest()
|
|
||||||
->take(5)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('pages.dashboards.index', compact(
|
|
||||||
'totalAssets',
|
|
||||||
'movementCount',
|
|
||||||
'disposalCount',
|
|
||||||
'auditIssues',
|
|
||||||
'byStatus',
|
|
||||||
'byLocation',
|
|
||||||
'recentAudits',
|
|
||||||
'recentDisposals'
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$data = $this->dashboard->build($user);
|
||||||
|
|
||||||
|
// Backward compatible variable names (dipakai view lama).
|
||||||
|
$data['movementCount'] = $data['movementCount30'] ?? 0;
|
||||||
|
$data['disposalCount'] = $data['activeDisposals'] ?? 0;
|
||||||
|
|
||||||
|
return view('pages.dashboards.index', $data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ class SessionTimeout
|
|||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if ($request->user()) {
|
if ($request->user()) {
|
||||||
$timeout = config('system.security.session_idle_timeout_minutes', 30);
|
// Mengikuti key setting yang disimpan di database (`security.session_idle_minutes`).
|
||||||
|
// Fallback ke key lama jika ada, agar backward compatible.
|
||||||
|
$timeout = (int) (config('system.security.session_idle_minutes')
|
||||||
|
?? config('system.security.session_idle_timeout_minutes', 30));
|
||||||
$last = session('last_active_at');
|
$last = session('last_active_at');
|
||||||
|
|
||||||
if ($last && now()->diffInMinutes($last) >= $timeout) {
|
if ($last && now()->diffInMinutes($last) >= $timeout) {
|
||||||
|
|||||||
234
app/Services/Dashboard/DashboardService.php
Normal file
234
app/Services/Dashboard/DashboardService.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Dashboard;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\AssetApprovalRequest;
|
||||||
|
use App\Models\AssetAudit;
|
||||||
|
use App\Models\AssetCategory;
|
||||||
|
use App\Models\AssetDisposal;
|
||||||
|
use App\Models\AssetLocation;
|
||||||
|
use App\Models\AssetMaintenance;
|
||||||
|
use App\Models\AssetMovement;
|
||||||
|
use App\Models\AssetStatus;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\System\SystemSettingService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class DashboardService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly SystemSettingService $settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyusun seluruh data ringkasan untuk dashboard.
|
||||||
|
*
|
||||||
|
* Catatan:
|
||||||
|
* - Menggunakan scoping per user (departemen/lokasi) jika user tidak punya permission `assets.view_all`.
|
||||||
|
* - Menggunakan cache singkat untuk mengurangi beban query pada halaman yang sering diakses.
|
||||||
|
*/
|
||||||
|
public function build(User $user): array
|
||||||
|
{
|
||||||
|
$cacheKey = sprintf('dashboard.summary.%s', $user->id);
|
||||||
|
|
||||||
|
return Cache::remember($cacheKey, now()->addMinutes(2), function () use ($user) {
|
||||||
|
$now = Carbon::now();
|
||||||
|
$last30 = $now->copy()->subDays(30);
|
||||||
|
$trendDays = 14;
|
||||||
|
$trendStart = $now->copy()->subDays($trendDays - 1)->startOfDay();
|
||||||
|
|
||||||
|
$warrantyDays = (int) $this->settings->get('asset.warranty_reminder_days', 30);
|
||||||
|
$lowStockThreshold = (int) $this->settings->get('asset.consumable_low_stock_threshold', 5);
|
||||||
|
|
||||||
|
$assetsQuery = Asset::query()->forUser($user);
|
||||||
|
|
||||||
|
$totalAssets = (clone $assetsQuery)->count();
|
||||||
|
$archivedAssets = (clone $assetsQuery)->whereNotNull('archived_at')->count();
|
||||||
|
$softDeletedAssets = (clone $assetsQuery)->onlyTrashed()->count();
|
||||||
|
|
||||||
|
$activeDisposals = AssetDisposal::query()
|
||||||
|
->whereNull('reversed_at')
|
||||||
|
->where('status', 'approved')
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$movementCount30 = AssetMovement::query()
|
||||||
|
->where('created_at', '>=', $last30)
|
||||||
|
->where('status', 'approved')
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$auditIssues = AssetAudit::query()
|
||||||
|
->whereIn('status', ['missing', 'damaged'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$warrantyExpiring = (clone $assetsQuery)
|
||||||
|
->whereNotNull('warranty_end')
|
||||||
|
->whereBetween('warranty_end', [$now->toDateString(), $now->copy()->addDays($warrantyDays)->toDateString()])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$maintenanceOpen = AssetMaintenance::query()
|
||||||
|
->whereIn('status', ['planned', 'in_progress', 'pending'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$lowStockConsumables = (clone $assetsQuery)
|
||||||
|
->where('is_consumable', true)
|
||||||
|
->whereNotNull('available_quantity')
|
||||||
|
->where('available_quantity', '<=', $lowStockThreshold)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$pendingApprovalsCount = AssetApprovalRequest::query()
|
||||||
|
->where('status', 'pending')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$byStatus = AssetStatus::query()
|
||||||
|
->withCount(['assets as assets_count' => fn ($q) => $q->forUser($user)])
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(fn ($status) => ['label' => $status->name, 'value' => (int) $status->assets_count])
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$byLocation = AssetLocation::query()
|
||||||
|
->withCount(['assets as assets_count' => fn ($q) => $q->forUser($user)])
|
||||||
|
->orderByDesc('assets_count')
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(fn ($loc) => ['label' => $loc->name, 'value' => (int) $loc->assets_count])
|
||||||
|
->filter(fn ($row) => $row['value'] > 0)
|
||||||
|
->take(10)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$byCategory = AssetCategory::query()
|
||||||
|
->withCount(['assets as assets_count' => fn ($q) => $q->forUser($user)])
|
||||||
|
->orderByDesc('assets_count')
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(fn ($cat) => ['label' => $cat->name, 'value' => (int) $cat->assets_count])
|
||||||
|
->filter(fn ($row) => $row['value'] > 0)
|
||||||
|
->take(10)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$trendLabels = $this->buildTrendLabels($trendStart, $trendDays);
|
||||||
|
$movementTrend = $this->buildDailyTrend(
|
||||||
|
AssetMovement::query()
|
||||||
|
->where('status', 'approved')
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user)),
|
||||||
|
'created_at',
|
||||||
|
$trendStart,
|
||||||
|
$trendDays
|
||||||
|
);
|
||||||
|
$disposalTrend = $this->buildDailyTrend(
|
||||||
|
AssetDisposal::query()
|
||||||
|
->where('status', 'approved')
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user)),
|
||||||
|
'created_at',
|
||||||
|
$trendStart,
|
||||||
|
$trendDays
|
||||||
|
);
|
||||||
|
$auditIssueTrend = $this->buildDailyTrend(
|
||||||
|
AssetAudit::query()
|
||||||
|
->whereIn('status', ['missing', 'damaged'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user)),
|
||||||
|
'created_at',
|
||||||
|
$trendStart,
|
||||||
|
$trendDays
|
||||||
|
);
|
||||||
|
|
||||||
|
$recentAudits = AssetAudit::with(['asset'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$recentDisposals = AssetDisposal::with(['asset'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$recentMovements = AssetMovement::with(['asset', 'fromLocation', 'toLocation', 'fromDepartment', 'toDepartment', 'fromUser', 'toUser'])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$pendingApprovals = AssetApprovalRequest::with(['approvable', 'requester'])
|
||||||
|
->where('status', 'pending')
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$maintenanceDueSoon = AssetMaintenance::with('asset')
|
||||||
|
->whereIn('status', ['planned', 'in_progress', 'pending'])
|
||||||
|
->whereNotNull('performed_at')
|
||||||
|
->whereBetween('performed_at', [$now->copy()->subDays(1), $now->copy()->addDays(7)])
|
||||||
|
->whereHas('asset', fn ($q) => $q->forUser($user))
|
||||||
|
->orderBy('performed_at')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$readonlyMode = (bool) $this->settings->get('maintenance.readonly_mode', false);
|
||||||
|
|
||||||
|
return compact(
|
||||||
|
'totalAssets',
|
||||||
|
'archivedAssets',
|
||||||
|
'softDeletedAssets',
|
||||||
|
'movementCount30',
|
||||||
|
'activeDisposals',
|
||||||
|
'auditIssues',
|
||||||
|
'warrantyExpiring',
|
||||||
|
'maintenanceOpen',
|
||||||
|
'lowStockConsumables',
|
||||||
|
'pendingApprovalsCount',
|
||||||
|
'byStatus',
|
||||||
|
'byLocation',
|
||||||
|
'byCategory',
|
||||||
|
'trendLabels',
|
||||||
|
'movementTrend',
|
||||||
|
'disposalTrend',
|
||||||
|
'auditIssueTrend',
|
||||||
|
'recentAudits',
|
||||||
|
'recentDisposals',
|
||||||
|
'recentMovements',
|
||||||
|
'pendingApprovals',
|
||||||
|
'maintenanceDueSoon',
|
||||||
|
'readonlyMode'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTrendLabels(Carbon $start, int $days): array
|
||||||
|
{
|
||||||
|
return collect(range(0, $days - 1))
|
||||||
|
->map(fn ($i) => $start->copy()->addDays($i)->format('d/m'))
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghasilkan array count per hari (panjang = $days) untuk dataset chart.
|
||||||
|
*/
|
||||||
|
private function buildDailyTrend($query, string $dateColumn, Carbon $start, int $days): array
|
||||||
|
{
|
||||||
|
$end = $start->copy()->addDays($days)->endOfDay();
|
||||||
|
|
||||||
|
/** @var Collection<string,int> $raw */
|
||||||
|
$raw = $query
|
||||||
|
->whereBetween($dateColumn, [$start, $end])
|
||||||
|
->selectRaw('DATE(' . $dateColumn . ') as d, COUNT(*) as c')
|
||||||
|
->groupByRaw('DATE(' . $dateColumn . ')')
|
||||||
|
->pluck('c', 'd')
|
||||||
|
->map(fn ($v) => (int) $v);
|
||||||
|
|
||||||
|
return collect(range(0, $days - 1))
|
||||||
|
->map(function ($i) use ($start, $raw) {
|
||||||
|
$key = $start->copy()->addDays($i)->toDateString();
|
||||||
|
return (int) ($raw[$key] ?? 0);
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,15 +110,23 @@ class AssetSeeder extends Seeder
|
|||||||
// Download 2 foto berbeda untuk tiap aset dari pool.
|
// Download 2 foto berbeda untuk tiap aset dari pool.
|
||||||
$pics = collect($imagePool)->shuffle()->take(2);
|
$pics = collect($imagePool)->shuffle()->take(2);
|
||||||
foreach ($pics as $idx => $url) {
|
foreach ($pics as $idx => $url) {
|
||||||
$response = Http::get($url);
|
try {
|
||||||
if ($response->ok()) {
|
$response = Http::retry(3, 1000)
|
||||||
$path = "assets/{$asset->id}/photo-{$idx}.jpg";
|
->connectTimeout(10)
|
||||||
Storage::disk('public')->put($path, $response->body());
|
->timeout(20)
|
||||||
AssetPhoto::create([
|
->get($url);
|
||||||
'asset_id' => $asset->id,
|
if ($response->ok()) {
|
||||||
'path' => $path,
|
$path = "assets/{$asset->id}/photo-{$idx}.jpg";
|
||||||
'is_primary' => $idx === 0 && $asset->photos()->count() === 0,
|
Storage::disk('public')->put($path, $response->body());
|
||||||
]);
|
AssetPhoto::create([
|
||||||
|
'asset_id' => $asset->id,
|
||||||
|
'path' => $path,
|
||||||
|
'is_primary' => $idx === 0 && $asset->photos()->count() === 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Jika download gagal (timeout/SSL/diblok), seeder tetap lanjut tanpa foto.
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,15 +169,22 @@ class AssetSeeder extends Seeder
|
|||||||
|
|
||||||
// Gunakan foto generik untuk consumable
|
// Gunakan foto generik untuk consumable
|
||||||
$url = 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=800&q=80';
|
$url = 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=800&q=80';
|
||||||
$response = Http::get($url);
|
try {
|
||||||
if ($response->ok()) {
|
$response = Http::retry(3, 1000)
|
||||||
$path = "assets/{$asset->id}/photo-consumable.jpg";
|
->connectTimeout(10)
|
||||||
Storage::disk('public')->put($path, $response->body());
|
->timeout(20)
|
||||||
AssetPhoto::create([
|
->get($url);
|
||||||
'asset_id' => $asset->id,
|
if ($response->ok()) {
|
||||||
'path' => $path,
|
$path = "assets/{$asset->id}/photo-consumable.jpg";
|
||||||
'is_primary' => true,
|
Storage::disk('public')->put($path, $response->body());
|
||||||
]);
|
AssetPhoto::create([
|
||||||
|
'asset_id' => $asset->id,
|
||||||
|
'path' => $path,
|
||||||
|
'is_primary' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
documentation/guide.md
Normal file
149
documentation/guide.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Dokumentasi Lengkap Asset Management System
|
||||||
|
|
||||||
|
Dokumen ini menjadi referensi utama untuk developer/ops yang ingin memahami alur, konfigurasi, dan fitur lengkap sistem.
|
||||||
|
|
||||||
|
## Isi Singkat
|
||||||
|
- [Arsitektur Ringkas](#arsitektur-ringkas)
|
||||||
|
- [Alur Penggunaan](#alur-penggunaan)
|
||||||
|
- [Setup & Wizard](#setup--wizard)
|
||||||
|
- [Authentication, 2FA, Session Timeout](#authentication-2fa-session-timeout)
|
||||||
|
- [RBAC](#rbac)
|
||||||
|
- [System Settings (Referensi)](#system-settings-referensi)
|
||||||
|
- [Identifikasi Aset (QR/RFID/NFC)](#identifikasi-aset-qrrfidnfc)
|
||||||
|
- [Transaksi & Approval Workflow](#transaksi--approval-workflow)
|
||||||
|
- [Maintenance & Read-only Mode](#maintenance--read-only-mode)
|
||||||
|
- [Logging & Audit Trail](#logging--audit-trail)
|
||||||
|
- [Import/Export & Bulk Data](#importexport--bulk-data)
|
||||||
|
- [Laporan (Excel/PDF)](#laporan-excelpdf)
|
||||||
|
- [Integrasi API & Webhook](#integrasi-api--webhook)
|
||||||
|
- [Notifikasi](#notifikasi)
|
||||||
|
- [Public Asset View (QR Scan)](#public-asset-view-qr-scan)
|
||||||
|
- [Queues & Kinerja](#queues--kinerja)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Deployment Checklist](#deployment-checklist)
|
||||||
|
|
||||||
|
## Arsitektur Ringkas
|
||||||
|
- **Laravel** + template **Vyzor (Bootstrap)**.
|
||||||
|
- **Service-first**: logic domain di `app/Services/*`, controller tipis.
|
||||||
|
- **UUID** sebagai primary key (role/permission juga sudah di-custom untuk UUID).
|
||||||
|
- **Settings** berbasis DB di tabel `settings`, dibaca via `App\Services\System\SystemSettingService`.
|
||||||
|
- **Log** ke file khusus channel `asset_activity` (bukan DB).
|
||||||
|
|
||||||
|
## Alur Penggunaan
|
||||||
|
1. Buka aplikasi; jika DB belum siap, otomatis redirect ke **/setup**.
|
||||||
|
2. Jalankan wizard:
|
||||||
|
- Step DB: migrate + seed minimal/full.
|
||||||
|
- Step 1: buat admin.
|
||||||
|
- Step 2: isi nilai setting (form dari data di DB).
|
||||||
|
- Step 3: generate master data dasar.
|
||||||
|
- Step 4: buat aset sample (opsional).
|
||||||
|
3. Login ke dashboard (`/dashboard/index`), cek ringkasan di dashboard.
|
||||||
|
4. Kelola master data (status, kelas, kategori, lokasi, PIC, dsb.).
|
||||||
|
5. Input aset + foto, QR otomatis di-generate jika diaktifkan.
|
||||||
|
6. Jalankan transaksi (movement, disposal, maintenance, audit) sesuai izin.
|
||||||
|
7. Approval (jika workflow pending) dikerjakan di menu **Approvals**.
|
||||||
|
8. Export laporan (Excel/PDF) atau lakukan import massal jika perlu.
|
||||||
|
|
||||||
|
## Setup & Wizard
|
||||||
|
- **Route**: `GET /setup`, `POST /setup`, `POST /setup/migrate`.
|
||||||
|
- Bila koneksi DB gagal → tampilkan panduan isi `.env` + tombol migrate (minimal/full seed).
|
||||||
|
- Wizard langkah 1–4 (admin, setting, master, aset sample).
|
||||||
|
- Middleware `EnsureDatabaseReady` mencegah akses normal sebelum DB siap.
|
||||||
|
|
||||||
|
## Authentication, 2FA, Session Timeout
|
||||||
|
- Login: `/login`, Register: `/register`, Logout: `POST /logout`.
|
||||||
|
- **2FA**: setelah password benar, user yang aktifkan 2FA akan diminta kode 6 digit di `/two-factor-challenge`.
|
||||||
|
- **Session timeout**: middleware `session.timeout` memakai setting `security.session_idle_minutes`.
|
||||||
|
|
||||||
|
## RBAC
|
||||||
|
- Paket `spatie/laravel-permission` dengan UUID (lihat `config/permission.php`).
|
||||||
|
- Permissions utama: `settings.manage`, `users.manage`, `roles.manage`, `permissions.manage`, `assets.view`, `assets.manage`, `movements.manage`, `disposals.manage`, `audits.manage`, `maintenance.manage`, `reports.view`, `approvals.manage`.
|
||||||
|
- Role seed: `super-admin`, `asset-manager`, `auditor`, `maintenance`, `viewer`.
|
||||||
|
- Scoping data aset: `Asset::forUser($user)` membatasi berdasar departemen/lokasi kecuali user punya `assets.view_all`.
|
||||||
|
|
||||||
|
## System Settings (Referensi)
|
||||||
|
Semua ada di tabel `settings` (seeded oleh `SettingSeeder`). Contoh kunci penting:
|
||||||
|
- **Aplikasi/UI**: `application.name`, `application.timezone`, `ui.date_format`, `ui.time_format`, `ui.table_page_size`.
|
||||||
|
- **Aset & Identifikasi**: `asset.code_prefix`, `asset.qr_enabled`, `asset.qr_format`, `asset.qr_size`, `asset.rfid_enabled`, `asset.nfc_enabled`, `asset.rfid_auto_generate`, `asset.nfc_auto_generate`, `asset.warranty_reminder_days`.
|
||||||
|
- **Workflow**: `asset.workflow.auto_approve_movement|disposal|maintenance`.
|
||||||
|
- **Import**: `asset.import_max_rows`, `asset.import_allow_remote_images`, `asset.import_dedupe_priority`, `asset.import_default_status/category/location`.
|
||||||
|
- **Retensi/Arsip**: `asset.archive_retention_days`, `asset.soft_delete_retention_days`.
|
||||||
|
- **Security**: `security.audit_log`, `security.session_idle_minutes`, `security.two_factor_enforced`.
|
||||||
|
- **Notification**: `notification.email_enabled`, `notification.slack_webhook_url`, `notification.import_result_email`.
|
||||||
|
- **Maintenance mode**: `maintenance.readonly_mode`.
|
||||||
|
- **Integrasi**: `integration.api_key`, `integration.webhook_enabled`, `integration.webhook_url`, `integration.webhook_secret`, `integration.api_rate_limit`.
|
||||||
|
|
||||||
|
## Identifikasi Aset (QR/RFID/NFC)
|
||||||
|
- QR otomatis saat create/update aset (jika `asset.qr_enabled = true`), disimpan di `storage/app/public/qr/`, isi QR: `route('assets.public.show')`.
|
||||||
|
- RFID/NFC: field `rfid_tag`, `nfc_tag`. Bisa diwajibkan (`asset.rfid_required`, `asset.nfc_required`) dan auto-generate (`asset.rfid_auto_generate`, `asset.nfc_auto_generate`) oleh `AssetService`.
|
||||||
|
|
||||||
|
## Transaksi & Approval Workflow
|
||||||
|
- **Movement**: ubah lokasi/departemen/user; jika auto-approve OFF → status `pending` dan butuh approval.
|
||||||
|
- **Disposal**: status `approved` akan mengubah status aset ke `DISPOSED` (jika ada); bisa di-reverse.
|
||||||
|
- **Maintenance**: status awal `pending`/`planned`, lanjut `in_progress`, `completed`.
|
||||||
|
- **Audit/Stocktake**: status `matched/missing/damaged`.
|
||||||
|
- **Approval**: tersimpan di `asset_approval_requests`, dikelola via `ApprovalService`, notifikasi email ke approver & requester.
|
||||||
|
- Menu approvals: `/dashboard/approvals`.
|
||||||
|
|
||||||
|
## Maintenance & Read-only Mode
|
||||||
|
- Jika `maintenance.readonly_mode = true`, middleware `maintenance.readonly` memblok aksi tulis dan dashboard menampilkan badge peringatan.
|
||||||
|
|
||||||
|
## Logging & Audit Trail
|
||||||
|
- **Request audit**: middleware `audit.request` mencatat semua non-GET beserta durasi dan status.
|
||||||
|
- **Helper**: `asset_activity_log()`, `asset_request_log()` → channel `asset_activity` (`storage/logs/asset-activity.log`, daily).
|
||||||
|
- **ActivityLogger**: masking field sensitif (password, token) dan memotong body terlalu panjang.
|
||||||
|
|
||||||
|
## Import/Export & Bulk Data
|
||||||
|
- **Import**: modal di `/dashboard/assets`, mendukung:
|
||||||
|
- Preview (tanpa commit) + ringkasan create/update/invalid.
|
||||||
|
- Import langsung.
|
||||||
|
- Gambar dari URL (jika `asset.import_allow_remote_images`) atau ZIP (`images_zip` + kolom `image_file`).
|
||||||
|
- Sample CSV: `public/samples/assets_import_sample.csv`.
|
||||||
|
- **Export**: CSV via `GET /dashboard/assets-export` (limit 2000 rows).
|
||||||
|
- Dedupe berdasarkan setting `asset.import_dedupe_priority` (code/serial).
|
||||||
|
|
||||||
|
## Laporan (Excel/PDF)
|
||||||
|
- Menu laporan: `/dashboard/reports/assets|movements|disposals|audits`.
|
||||||
|
- Export:
|
||||||
|
- Excel multi-sheet (aset + relasi utama, dll.)
|
||||||
|
- PDF via DomPDF.
|
||||||
|
- Filter mengikuti halaman laporan (date range, lokasi, dsb.).
|
||||||
|
|
||||||
|
## Integrasi API & Webhook
|
||||||
|
- Public API key di `integration.api_key`; middleware `VerifyApiKey` (header `X-Api-Key`).
|
||||||
|
- Webhook event aset di `App\Services\Integration\WebhookDispatcher` (respect `integration.webhook_enabled/url/secret/timeout`).
|
||||||
|
- Dokumentasi API disediakan di `GET /api-docs`.
|
||||||
|
|
||||||
|
## Notifikasi
|
||||||
|
- Email notifikasi approval: `ApprovalRequestedNotification`, `ApprovalDecisionNotification`.
|
||||||
|
- SMTP wajib dikonfigurasi di `.env` (MAIL_*). Jika `notification.email_enabled=false`, notifikasi email tidak dikirim.
|
||||||
|
- Contoh cron reminder sederhana: `php artisan asset:notify-due` (garansi akan habis, maintenance due, audit overdue) → tambahkan ke cron jika perlu.
|
||||||
|
|
||||||
|
## Public Asset View (QR Scan)
|
||||||
|
- Route: `GET /asset-view/{asset}` (tanpa login).
|
||||||
|
- Di halaman ini tersedia tombol transaksi (movement/disposal/maintenance) dalam bentuk modal jika user login & punya permission; jika tidak, hanya view data + foto aset.
|
||||||
|
|
||||||
|
## Queues & Kinerja
|
||||||
|
- Import besar dianjurkan dijalankan di queue (kini masih sinkron); arsitektur kode disiapkan agar mudah dialihkan ke job queue jika volume tinggi.
|
||||||
|
- Gunakan `php artisan queue:work` untuk job lain (notifikasi/email) jika diubah ke asynchronous.
|
||||||
|
- Pagination server-side diterapkan pada tabel besar (misal approvals, master, transaksi).
|
||||||
|
- Index DB sudah disiapkan di migrasi utama (cek migrasi masing-masing tabel).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Jalankan semua test:
|
||||||
|
```bash
|
||||||
|
php artisan test
|
||||||
|
```
|
||||||
|
- Test penting:
|
||||||
|
- `tests/Feature/DashboardPageTest.php`
|
||||||
|
- `tests/Feature/AssetTransactionFlowTest.php`
|
||||||
|
- `tests/Feature/SystemSettingsPageTest.php`
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
- `APP_ENV=production`, `APP_DEBUG=false`, `APP_KEY` terisi.
|
||||||
|
- Konfigurasi DB + jalankan `php artisan migrate --force`.
|
||||||
|
- `php artisan storage:link`.
|
||||||
|
- Build frontend: `npm run build` (atau gunakan artifact `public/build` yang sudah ada).
|
||||||
|
- Permission folder: `storage/` dan `bootstrap/cache/` writable.
|
||||||
|
- Set SMTP (MAIL_*), dan cron untuk queue (jika dipakai) / `asset:notify-due` (opsional).
|
||||||
|
|
||||||
BIN
resources/assets/images/landing/cta.jpg
Normal file
BIN
resources/assets/images/landing/cta.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
BIN
resources/assets/images/landing/feature.jpg
Normal file
BIN
resources/assets/images/landing/feature.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
resources/assets/images/landing/flow.jpg
Normal file
BIN
resources/assets/images/landing/flow.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
resources/assets/images/landing/hero.jpg
Normal file
BIN
resources/assets/images/landing/hero.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 195 KiB |
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center gy-3">
|
<div class="row justify-content-center gy-3">
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<p class="fw-semibold mb-3 brand-image"><a href="{{url('index')}}"><img src="{{asset('build/assets/images/brand-logos/desktop-dark.png')}}" alt=""></a></p>
|
<p class="fw-semibold mb-3 brand-image"><a href="{{ route('landing') }}"><img src="{{asset('build/assets/images/brand-logos/desktop-dark.png')}}" alt=""></a></p>
|
||||||
<p class="mb-2 op-6 fw-normal">
|
<p class="mb-2 op-6 fw-normal">
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit et magnam, fuga est mollitia eius, quo illum illo inventore optio aut quas omnis rem. Dolores accusantium aspernatur minus ea incidunt.
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit et magnam, fuga est mollitia eius, quo illum illo inventore optio aut quas omnis rem. Dolores accusantium aspernatur minus ea incidunt.
|
||||||
</p>
|
</p>
|
||||||
@@ -158,4 +158,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<!-- Start::header-element -->
|
<!-- Start::header-element -->
|
||||||
<div class="header-element">
|
<div class="header-element">
|
||||||
<div class="horizontal-logo">
|
<div class="horizontal-logo">
|
||||||
<a href="{{url('index')}}" class="header-logo">
|
<a href="{{ route('landing') }}" class="header-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/toggle-logo.png')}}" alt="logo" class="toggle-logo">
|
<img src="{{asset('build/assets/images/brand-logos/toggle-logo.png')}}" alt="logo" class="toggle-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/toggle-dark.png')}}" alt="logo" class="toggle-dark">
|
<img src="{{asset('build/assets/images/brand-logos/toggle-dark.png')}}" alt="logo" class="toggle-dark">
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<!-- Start::header-element -->
|
<!-- Start::header-element -->
|
||||||
<div class="header-element">
|
<div class="header-element">
|
||||||
<div class="horizontal-logo">
|
<div class="horizontal-logo">
|
||||||
<a href="{{url('index')}}" class="header-logo">
|
<a href="{{ route('landing') }}" class="header-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/toggle-logo.png')}}" alt="logo" class="toggle-logo">
|
<img src="{{asset('build/assets/images/brand-logos/toggle-logo.png')}}" alt="logo" class="toggle-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/toggle-dark.png')}}" alt="logo" class="toggle-dark">
|
<img src="{{asset('build/assets/images/brand-logos/toggle-dark.png')}}" alt="logo" class="toggle-dark">
|
||||||
</a>
|
</a>
|
||||||
@@ -40,9 +40,11 @@
|
|||||||
<div class="header-element align-items-center">
|
<div class="header-element align-items-center">
|
||||||
<!-- Start::header-link|switcher-icon -->
|
<!-- Start::header-link|switcher-icon -->
|
||||||
<div class="btn-list d-lg-none d-block">
|
<div class="btn-list d-lg-none d-block">
|
||||||
<a href="{{url('sign-up-basic')}}" class="btn btn-primary-light">
|
@auth
|
||||||
Sign Up
|
<a href="{{ route('index') }}" class="btn btn-primary-light">Dashboard</a>
|
||||||
</a>
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-primary-light">Login</a>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
<!-- End::header-link|switcher-icon -->
|
<!-- End::header-link|switcher-icon -->
|
||||||
</div>
|
</div>
|
||||||
@@ -54,4 +56,4 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- End::main-header-container -->
|
<!-- End::main-header-container -->
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<nav class="main-menu-container nav nav-pills sub-open">
|
<nav class="main-menu-container nav nav-pills sub-open">
|
||||||
<div class="landing-logo-container">
|
<div class="landing-logo-container">
|
||||||
<div class="horizontal-logo">
|
<div class="horizontal-logo">
|
||||||
<a href="{{ url('index') }}" class="header-logo">
|
<a href="{{ route('landing') }}" class="header-logo">
|
||||||
<img src="{{ asset('build/assets/images/brand-logos/desktop-logo.png') }}" alt="logo"
|
<img src="{{ asset('build/assets/images/brand-logos/desktop-logo.png') }}" alt="logo"
|
||||||
class="desktop-logo">
|
class="desktop-logo">
|
||||||
<img src="{{ asset('build/assets/images/brand-logos/desktop-dark.png') }}" alt="logo"
|
<img src="{{ asset('build/assets/images/brand-logos/desktop-dark.png') }}" alt="logo"
|
||||||
@@ -44,15 +44,15 @@
|
|||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
<!-- Start::slide -->
|
<!-- Start::slide -->
|
||||||
<li class="slide">
|
<li class="slide">
|
||||||
<a href="#price" class="side-menu__item">
|
<a href="#faq" class="side-menu__item">
|
||||||
<span class="side-menu__label">Pricing</span>
|
<span class="side-menu__label">FAQ</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
<!-- Start::slide -->
|
<!-- Start::slide -->
|
||||||
<li class="slide">
|
<li class="slide">
|
||||||
<a href="#contactus" class="side-menu__item">
|
<a href="#get-started" class="side-menu__item">
|
||||||
<span class="side-menu__label">Kontak</span>
|
<span class="side-menu__label">Mulai</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
|
|
||||||
<!-- Start::nav -->
|
<!-- Start::nav -->
|
||||||
<nav class="main-menu-container nav nav-pills sub-open">
|
<nav class="main-menu-container nav nav-pills sub-open">
|
||||||
<div class="landing-logo-container">
|
<div class="landing-logo-container">
|
||||||
<div class="horizontal-logo">
|
<div class="horizontal-logo">
|
||||||
<a href="{{url('index')}}" class="header-logo">
|
<a href="{{ route('landing') }}" class="header-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/desktop-logo.png')}}" alt="logo" class="desktop-logo">
|
<img src="{{asset('build/assets/images/brand-logos/desktop-logo.png')}}" alt="logo" class="desktop-logo">
|
||||||
<img src="{{asset('build/assets/images/brand-logos/desktop-dark.png')}}" alt="logo" class="desktop-dark">
|
<img src="{{asset('build/assets/images/brand-logos/desktop-dark.png')}}" alt="logo" class="desktop-dark">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="slide-left" id="slide-left"><svg xmlns="http://www.w3.org/2000/svg" fill="#7b8191" width="24" height="24" viewBox="0 0 24 24"> <path d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"></path> </svg></div>
|
<div class="slide-left" id="slide-left"><svg xmlns="http://www.w3.org/2000/svg" fill="#7b8191" width="24" height="24" viewBox="0 0 24 24"> <path d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"></path> </svg></div>
|
||||||
<ul class="main-menu flex-fill justify-content-center">
|
<ul class="main-menu flex-fill justify-content-center">
|
||||||
<!-- Start::slide -->
|
<!-- Start::slide -->
|
||||||
@@ -80,15 +80,15 @@
|
|||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
<!-- Start::slide -->
|
<!-- Start::slide -->
|
||||||
<li class="slide">
|
<li class="slide">
|
||||||
<a href="#price" class="side-menu__item">
|
<a href="#faq" class="side-menu__item">
|
||||||
<span class="side-menu__label">Subscription</span>
|
<span class="side-menu__label">FAQ</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
<!-- Start::slide -->
|
<!-- Start::slide -->
|
||||||
<li class="slide">
|
<li class="slide">
|
||||||
<a href="#contactus" class="side-menu__item">
|
<a href="#get-started" class="side-menu__item">
|
||||||
<span class="side-menu__label">Contact Us</span>
|
<span class="side-menu__label">Get Started</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
@@ -96,9 +96,11 @@
|
|||||||
<div class="slide-right" id="slide-right"><svg xmlns="http://www.w3.org/2000/svg" fill="#7b8191" width="24" height="24" viewBox="0 0 24 24"> <path d="M10.707 17.707 16.414 12l-5.707-5.707-1.414 1.414L13.586 12l-4.293 4.293z"></path> </svg></div>
|
<div class="slide-right" id="slide-right"><svg xmlns="http://www.w3.org/2000/svg" fill="#7b8191" width="24" height="24" viewBox="0 0 24 24"> <path d="M10.707 17.707 16.414 12l-5.707-5.707-1.414 1.414L13.586 12l-4.293 4.293z"></path> </svg></div>
|
||||||
<div class="d-lg-flex d-none align-items-center">
|
<div class="d-lg-flex d-none align-items-center">
|
||||||
<div class="btn-list d-xl-flex d-none">
|
<div class="btn-list d-xl-flex d-none">
|
||||||
<a href="{{url('sign-in-basic')}}" class="btn btn-wave btn-primary border">
|
@auth
|
||||||
Login / Register
|
<a href="{{ route('index') }}" class="btn btn-wave btn-primary border">Dashboard</a>
|
||||||
</a>
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-wave btn-primary border">Login / Register</a>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -108,4 +110,4 @@
|
|||||||
<!-- End::main-sidebar -->
|
<!-- End::main-sidebar -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<title> Vyzor - Laravel Bootstrap 5 Premium Admin & Dashboard Template </title>
|
<title> Vyzor - Laravel Bootstrap 5 Premium Admin & Dashboard Template </title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/toggle-logo.png') }}" type="image/png">
|
||||||
|
|
||||||
<!-- Main Theme Js -->
|
<!-- Main Theme Js -->
|
||||||
<script src="{{ asset('build/assets/authentication-main.js') }}"></script>
|
<script src="{{ asset('build/assets/authentication-main.js') }}"></script>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<title> Vyzor - Laravel Bootstrap 5 Premium Admin & Dashboard Template </title>
|
<title> Vyzor - Laravel Bootstrap 5 Premium Admin & Dashboard Template </title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/toggle-logo.png') }}" type="image/png">
|
||||||
|
|
||||||
<!-- Icons CSS -->
|
<!-- Icons CSS -->
|
||||||
<link href="{{ asset('build/assets/icon-fonts/icons.css') }}" rel="stylesheet">
|
<link href="{{ asset('build/assets/icon-fonts/icons.css') }}" rel="stylesheet">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<title> Asset Management System</title>
|
<title> Asset Management System</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ asset('build/assets/images/brand-logos/toggle-logo.png') }}" type="image/png">
|
||||||
|
|
||||||
<!-- Main Theme Js -->
|
<!-- Main Theme Js -->
|
||||||
<script src="{{ asset('build/assets/main.js') }}"></script>
|
<script src="{{ asset('build/assets/main.js') }}"></script>
|
||||||
|
|||||||
@@ -11,13 +11,33 @@
|
|||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3 page-header-breadcrumb flex-wrap gap-2">
|
<div class="d-flex align-items-center justify-content-between mb-3 page-header-breadcrumb flex-wrap gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title fw-medium fs-20 mb-0">Dashboard Aset</h1>
|
<h1 class="page-title fw-medium fs-20 mb-0">Dashboard</h1>
|
||||||
<p class="text-muted mb-0">Ringkasan kesehatan aset, pergerakan, disposal, dan audit.</p>
|
<p class="text-muted mb-0">Ringkasan operasional aset, risiko, workflow approval, dan tindakan yang perlu ditindaklanjuti.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
@if(!empty($readonlyMode))
|
||||||
|
<span class="badge bg-warning-transparent text-warning">
|
||||||
|
<i class="ri-shield-flash-line me-1"></i>Mode Read-only Aktif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<span class="badge bg-primary-transparent text-primary">
|
||||||
|
<i class="ri-time-line me-1"></i>{{ now()->format('d/m/Y H:i') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($readonlyMode))
|
||||||
|
<div class="alert alert-warning d-flex align-items-start gap-2" role="alert">
|
||||||
|
<i class="ri-alert-line fs-18 mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Mode Read-only sedang aktif</div>
|
||||||
|
<div class="text-muted">Semua aksi tulis (create/update/delete) dibatasi. Nonaktifkan melalui System Settings jika maintenance sudah selesai.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-4 col-xl-2">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
@@ -29,10 +49,14 @@
|
|||||||
<i class="ri-archive-2-line fs-18"></i>
|
<i class="ri-archive-2-line fs-18"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
|
<span class="badge bg-light text-default">Arsip: {{ $archivedAssets ?? 0 }}</span>
|
||||||
|
<span class="badge bg-light text-default">Trash: {{ $softDeletedAssets ?? 0 }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4 col-xl-2">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
@@ -44,10 +68,11 @@
|
|||||||
<i class="ri-route-line fs-18"></i>
|
<i class="ri-route-line fs-18"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fs-12 text-muted mt-2">Hanya yang berstatus approved.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4 col-xl-2">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
@@ -59,10 +84,11 @@
|
|||||||
<i class="ri-delete-bin-6-line fs-18"></i>
|
<i class="ri-delete-bin-6-line fs-18"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fs-12 text-muted mt-2">Belum di-reverse.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4 col-xl-2">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
@@ -74,29 +100,149 @@
|
|||||||
<i class="ri-error-warning-line fs-18"></i>
|
<i class="ri-error-warning-line fs-18"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fs-12 text-muted mt-2">Status missing/damaged.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-xl-2">
|
||||||
|
<div class="card custom-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-muted">Warranty Akan Habis</p>
|
||||||
|
<h4 class="mb-0">{{ $warrantyExpiring ?? 0 }}</h4>
|
||||||
|
</div>
|
||||||
|
<span class="avatar avatar-md bg-success-transparent text-success">
|
||||||
|
<i class="ri-shield-check-line fs-18"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="fs-12 text-muted mt-2">Dalam window reminder setting.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-xl-2">
|
||||||
|
<div class="card custom-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-muted">Approval Pending</p>
|
||||||
|
<h4 class="mb-0">{{ $pendingApprovalsCount ?? 0 }}</h4>
|
||||||
|
</div>
|
||||||
|
<span class="avatar avatar-md bg-secondary-transparent text-secondary">
|
||||||
|
<i class="ri-shield-user-line fs-18"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="fs-12 text-muted mt-2">Movement/Disposal/Maintenance.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-8">
|
||||||
|
<div class="card custom-card h-100">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="card-title mb-0">Tren Aktivitas (14 Hari)</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<span class="badge bg-info-transparent text-info">Movement</span>
|
||||||
|
<span class="badge bg-warning-transparent text-warning">Disposal</span>
|
||||||
|
<span class="badge bg-danger-transparent text-danger">Audit Issues</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartTrend" height="110"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4">
|
||||||
|
<div class="card custom-card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title mb-0">Tindak Lanjut Cepat</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Maintenance Terbuka</div>
|
||||||
|
<div class="text-muted fs-12">Planned / in progress yang masih berjalan.</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-semibold">{{ $maintenanceOpen ?? 0 }}</div>
|
||||||
|
@can('maintenance.manage')
|
||||||
|
<a class="fs-12" href="{{ route('asset-maintenances.index') }}">Lihat</a>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Consumable Low Stock</div>
|
||||||
|
<div class="text-muted fs-12">Stok ≤ threshold setting.</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-semibold">{{ $lowStockConsumables ?? 0 }}</div>
|
||||||
|
@canany(['assets.view','assets.manage'])
|
||||||
|
<a class="fs-12" href="{{ route('assets.index', ['type' => 'consumable']) }}">Filter</a>
|
||||||
|
@endcanany
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Warranty Akan Habis</div>
|
||||||
|
<div class="text-muted fs-12">Perlu reminder/penjadwalan.</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-semibold">{{ $warrantyExpiring ?? 0 }}</div>
|
||||||
|
@canany(['assets.view','assets.manage'])
|
||||||
|
<a class="fs-12" href="{{ route('assets.index', ['warranty' => 'expiring']) }}">Filter</a>
|
||||||
|
@endcanany
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@can('approvals.manage')
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Approval Pending</div>
|
||||||
|
<div class="text-muted fs-12">Perlu keputusan approve/reject.</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-semibold">{{ $pendingApprovalsCount ?? 0 }}</div>
|
||||||
|
<a class="fs-12" href="{{ route('approvals.index') }}">Buka</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mt-0">
|
||||||
|
<div class="col-xl-4 col-md-6">
|
||||||
<div class="card custom-card h-100">
|
<div class="card custom-card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title mb-0">Distribusi Aset per Status</div>
|
<div class="card-title mb-0">Distribusi Aset per Status</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<canvas id="chartStatus"></canvas>
|
<canvas id="chartStatus" height="110"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-4 col-md-6">
|
||||||
<div class="card custom-card h-100">
|
<div class="card custom-card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title mb-0">Distribusi Aset per Lokasi</div>
|
<div class="card-title mb-0">Top Lokasi (10)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<canvas id="chartLocation"></canvas>
|
<canvas id="chartLocation" height="110"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4 col-md-12">
|
||||||
|
<div class="card custom-card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title mb-0">Top Kategori (10)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="chartCategory" height="110"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,13 +251,16 @@
|
|||||||
<div class="row g-3 mt-3">
|
<div class="row g-3 mt-3">
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-6">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
<div class="card-title mb-0">Audit Terakhir</div>
|
<div class="card-title mb-0">Audit Terakhir</div>
|
||||||
|
@can('audits.manage')
|
||||||
|
<a href="{{ route('asset-audits.index') }}" class="btn btn-sm btn-light">Lihat Semua</a>
|
||||||
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered text-nowrap align-middle mb-0">
|
<table class="table text-nowrap align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Aset</th>
|
<th>Aset</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
@@ -138,13 +287,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-6">
|
||||||
<div class="card custom-card">
|
<div class="card custom-card">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
<div class="card-title mb-0">Disposal Terakhir</div>
|
<div class="card-title mb-0">Disposal Terakhir</div>
|
||||||
|
@can('disposals.manage')
|
||||||
|
<a href="{{ route('asset-disposals.index') }}" class="btn btn-sm btn-light">Lihat Semua</a>
|
||||||
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered text-nowrap align-middle mb-0">
|
<table class="table text-nowrap align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Aset</th>
|
<th>Aset</th>
|
||||||
<th>Alasan</th>
|
<th>Alasan</th>
|
||||||
@@ -176,39 +328,197 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mt-3">
|
||||||
|
<div class="col-xl-7">
|
||||||
|
<div class="card custom-card">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="card-title mb-0">Movement Terakhir</div>
|
||||||
|
@can('movements.manage')
|
||||||
|
<a href="{{ route('asset-movements.index') }}" class="btn btn-sm btn-light">Lihat Semua</a>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table text-nowrap align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Aset</th>
|
||||||
|
<th>Dari</th>
|
||||||
|
<th>Ke</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Waktu</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($recentMovements as $mv)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $mv->asset?->code }} - {{ $mv->asset?->name }}</td>
|
||||||
|
<td class="text-muted">
|
||||||
|
{{ $mv->fromLocation?->name ?: '-' }} / {{ $mv->fromDepartment?->name ?: '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-muted">
|
||||||
|
{{ $mv->toLocation?->name ?: '-' }} / {{ $mv->toDepartment?->name ?: '-' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@php($st = $mv->status ?: 'approved')
|
||||||
|
@if($st === 'approved')
|
||||||
|
<span class="badge bg-success-transparent text-success">Approved</span>
|
||||||
|
@elseif($st === 'pending')
|
||||||
|
<span class="badge bg-warning-transparent text-warning">Pending</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger-transparent text-danger">{{ ucfirst($st) }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>{{ optional($mv->performed_at)->format('d/m/Y H:i') ?: optional($mv->created_at)->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="text-center text-muted">Belum ada data movement.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-5">
|
||||||
|
<div class="card custom-card">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="card-title mb-0">Approval Pending (Terbaru)</div>
|
||||||
|
@can('approvals.manage')
|
||||||
|
<a href="{{ route('approvals.index') }}" class="btn btn-sm btn-light">Kelola</a>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table text-nowrap align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Requester</th>
|
||||||
|
<th>Dibuat</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($pendingApprovals as $appr)
|
||||||
|
<tr>
|
||||||
|
<td><span class="badge bg-secondary-transparent text-secondary">{{ strtoupper($appr->type) }}</span></td>
|
||||||
|
<td>{{ $appr->requester?->name ?: '-' }}</td>
|
||||||
|
<td>{{ optional($appr->created_at)->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="3" class="text-center text-muted">Tidak ada approval pending.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@if(!empty($maintenanceDueSoon) && $maintenanceDueSoon->count())
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="fw-semibold mb-2">Maintenance Due (7 Hari)</div>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
@foreach($maintenanceDueSoon as $m)
|
||||||
|
<li class="d-flex align-items-start justify-content-between gap-2 py-1">
|
||||||
|
<div class="text-truncate">
|
||||||
|
<div class="fw-medium text-truncate">{{ $m->asset?->code }} - {{ $m->asset?->name }}</div>
|
||||||
|
<div class="text-muted fs-12 text-truncate">{{ $m->description }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted fs-12 text-nowrap">{{ optional($m->performed_at)->format('d/m/Y') }}</div>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="{{ asset('build/assets/libs/chart.js/chart.umd.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const trendLabels = @json($trendLabels ?? []);
|
||||||
|
const movementTrend = @json($movementTrend ?? []);
|
||||||
|
const disposalTrend = @json($disposalTrend ?? []);
|
||||||
|
const auditIssueTrend = @json($auditIssueTrend ?? []);
|
||||||
|
|
||||||
|
const trendCtx = document.getElementById('chartTrend');
|
||||||
|
if (trendCtx) {
|
||||||
|
new Chart(trendCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: trendLabels,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Movement', data: movementTrend, borderColor: '#0ea5e9', backgroundColor: 'rgba(14,165,233,0.15)', tension: 0.35, fill: true },
|
||||||
|
{ label: 'Disposal', data: disposalTrend, borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.12)', tension: 0.35, fill: true },
|
||||||
|
{ label: 'Audit Issues', data: auditIssueTrend, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.10)', tension: 0.35, fill: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const statusCtx = document.getElementById('chartStatus');
|
const statusCtx = document.getElementById('chartStatus');
|
||||||
const statusData = @json($byStatus);
|
const statusData = @json($byStatus ?? []);
|
||||||
new Chart(statusCtx, {
|
if (statusCtx) {
|
||||||
type: 'doughnut',
|
new Chart(statusCtx, {
|
||||||
data: {
|
type: 'doughnut',
|
||||||
labels: statusData.map(i => i.label),
|
data: {
|
||||||
datasets: [{
|
labels: statusData.map(i => i.label),
|
||||||
data: statusData.map(i => i.value),
|
datasets: [{
|
||||||
backgroundColor: ['#60a5fa','#34d399','#fbbf24','#f87171','#a78bfa']
|
data: statusData.map(i => i.value),
|
||||||
}]
|
backgroundColor: ['#0ea5e9','#22c55e','#f59e0b','#ef4444','#a78bfa','#94a3b8','#14b8a6','#f97316']
|
||||||
},
|
}]
|
||||||
options: { plugins: { legend: { position: 'bottom' } } }
|
},
|
||||||
});
|
options: { plugins: { legend: { position: 'bottom' } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const locCtx = document.getElementById('chartLocation');
|
const locCtx = document.getElementById('chartLocation');
|
||||||
const locData = @json($byLocation);
|
const locData = @json($byLocation ?? []);
|
||||||
new Chart(locCtx, {
|
if (locCtx) {
|
||||||
type: 'bar',
|
new Chart(locCtx, {
|
||||||
data: {
|
type: 'bar',
|
||||||
labels: locData.map(i => i.label),
|
data: {
|
||||||
datasets: [{
|
labels: locData.map(i => i.label),
|
||||||
label: 'Jumlah',
|
datasets: [{
|
||||||
data: locData.map(i => i.value),
|
label: 'Jumlah Aset',
|
||||||
backgroundColor: '#22c55e'
|
data: locData.map(i => i.value),
|
||||||
}]
|
backgroundColor: '#22c55e'
|
||||||
},
|
}]
|
||||||
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }
|
},
|
||||||
});
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: { x: { beginAtZero: true, ticks: { precision: 0 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const catCtx = document.getElementById('chartCategory');
|
||||||
|
const catData = @json($byCategory ?? []);
|
||||||
|
if (catCtx) {
|
||||||
|
new Chart(catCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: catData.map(i => i.label),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Aset',
|
||||||
|
data: catData.map(i => i.value),
|
||||||
|
backgroundColor: '#a78bfa'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: { x: { beginAtZero: true, ticks: { precision: 0 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- Start:: Landing Banner -->
|
<!-- Start:: Landing Banner -->
|
||||||
<div class="landing-banner" id="home">
|
<div class="landing-banner" id="home">
|
||||||
<div class="banner-image-container">
|
<div class="banner-image-container">
|
||||||
<img src="{{ asset('landing-asset-hero.jpg') }}" alt="Asset hero">
|
<img src="{{ Vite::asset('resources/assets/images/landing/hero.jpg') }}" alt="Asset Management System">
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -21,13 +21,17 @@
|
|||||||
<h1 class="fw-semibold mt-3 landing-banner-heading">Audit, lacak, dan rawat <br> seluruh <span class="text-primary">aset fisik</span> Anda</h1>
|
<h1 class="fw-semibold mt-3 landing-banner-heading">Audit, lacak, dan rawat <br> seluruh <span class="text-primary">aset fisik</span> Anda</h1>
|
||||||
<span class="d-block fs-18">Registrasi aset lengkap, QR otomatis, movement/disposal, maintenance, audit, dan laporan siap unduh.</span>
|
<span class="d-block fs-18">Registrasi aset lengkap, QR otomatis, movement/disposal, maintenance, audit, dan laporan siap unduh.</span>
|
||||||
<div class="btn-list banner-buttons">
|
<div class="btn-list banner-buttons">
|
||||||
<a href="{{url('index')}}" class="btn btn-primary btn-lg rounded-pill btn-w-lg">Masuk Dashboard</a>
|
@auth
|
||||||
<a class="btn btn-lg btn-light border rounded-pill btn-w-lg" href="{{ route('assets.index') }}">Lihat Data Aset</a>
|
<a href="{{ route('index') }}" class="btn btn-primary btn-lg rounded-pill btn-w-lg">Masuk Dashboard</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-primary btn-lg rounded-pill btn-w-lg">Login</a>
|
||||||
|
@endauth
|
||||||
|
<a class="btn btn-lg btn-light border rounded-pill btn-w-lg" href="#feature">Lihat Fitur</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-6">
|
||||||
<div class="banner-main-img text-end d-xl-block d-none">
|
<div class="banner-main-img text-end d-xl-block d-none">
|
||||||
<img src="{{ asset('landing-feature.jpg') }}" alt="" class="img-fluid">
|
<img src="{{ Vite::asset('resources/assets/images/landing/feature.jpg') }}" alt="Dashboard Preview" class="img-fluid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,87 +39,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- End:: Landing Banner -->
|
<!-- End:: Landing Banner -->
|
||||||
|
|
||||||
<!-- Start:: Section-1 -->
|
|
||||||
<section class="section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="heading-section">
|
|
||||||
<div class="heading-subtitle">Clients</div>
|
|
||||||
<div class="heading-title">Trusted by Leading Companies Worldwide</div>
|
|
||||||
<div class="heading-description">
|
|
||||||
Thousands of businesses trust our solutions <br> to optimize their operations and drive growth.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper trusted-clients">
|
|
||||||
<div class="swiper-wrapper">
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/13.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/14.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/15.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/16.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/17.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/18.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/19.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/20.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="card custom-card trusted-clients-container mb-0 border border-dashed">
|
|
||||||
<div class="card-body">
|
|
||||||
<img src="{{asset('build/assets/images/company-logos/12.png')}}" alt="" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- End:: Section-1 -->
|
|
||||||
|
|
||||||
<!-- Start:: Section-2 -->
|
<!-- Start:: Section-2 -->
|
||||||
<section class="section" id="feature">
|
<section class="section" id="feature">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -135,9 +58,9 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M104,208V104H32v96a8,8,0,0,0,8,8H96" opacity="0.2"/><line x1="32" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="104" y1="104" x2="104" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><rect x="32" y="48" width="192" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M104,208V104H32v96a8,8,0,0,0,8,8H96" opacity="0.2"/><line x1="32" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="104" y1="104" x2="104" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><rect x="32" y="48" width="192" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="fw-semibold">Registrasi Aset & QR</h5>
|
<h5 class="fw-semibold">Identifikasi QR / RFID / NFC</h5>
|
||||||
<span class="fs-15 text-muted">
|
<span class="fs-15 text-muted">
|
||||||
Kode aset otomatis, QR ter-generate, detail lengkap (status, kategori, lokasi, PIC, user).
|
Kode aset otomatis, QR ter-generate, dukungan RFID/NFC tag, detail lengkap (status, kategori, lokasi, PIC, user).
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,9 +118,9 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="32" y="104" width="56" height="96" opacity="0.2"/><path d="M32,56H224a0,0,0,0,1,0,0V192a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56A0,0,0,0,1,32,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="152" x2="224" y2="152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="88" y1="104" x2="88" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="32" y="104" width="56" height="96" opacity="0.2"/><path d="M32,56H224a0,0,0,0,1,0,0V192a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56A0,0,0,0,1,32,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="152" x2="224" y2="152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="88" y1="104" x2="88" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="fw-semibold">Table Management</h5>
|
<h5 class="fw-semibold">Bulk Import/Export</h5>
|
||||||
<span class="fs-15 text-muted">
|
<span class="fs-15 text-muted">
|
||||||
Manage and display large datasets with advanced table components, filters, and pagination.
|
Import CSV (preview, deduplikasi, gambar URL/ZIP) dan otomatis diproses via queue untuk data besar (mis. 25rb baris).
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,9 +133,9 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M192,120,136,64l29.66-29.66a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31Z" opacity="0.2"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M192,120,136,64l29.66-29.66a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31Z" opacity="0.2"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="fw-semibold">Forms and Validation</h5>
|
<h5 class="fw-semibold">Arsip & Retensi Data</h5>
|
||||||
<span class="fs-15 text-muted">
|
<span class="fs-15 text-muted">
|
||||||
Create robust forms with validation features for collecting and processing user data.
|
Soft delete + restore, mode arsip, dan kontrol retensi agar data tetap rapi dan mudah ditelusuri.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,9 +148,9 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M108.11,28.11A96.09,96.09,0,0,0,227.89,147.89,96,96,0,1,1,108.11,28.11Z" opacity="0.2"/><path d="M108.11,28.11A96.09,96.09,0,0,0,227.89,147.89,96,96,0,1,1,108.11,28.11Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M108.11,28.11A96.09,96.09,0,0,0,227.89,147.89,96,96,0,1,1,108.11,28.11Z" opacity="0.2"/><path d="M108.11,28.11A96.09,96.09,0,0,0,227.89,147.89,96,96,0,1,1,108.11,28.11Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="fw-semibold">Dark Mode / Light Mode</h5>
|
<h5 class="fw-semibold">Integrasi API & Webhook</h5>
|
||||||
<span class="fs-15 text-muted">
|
<span class="fs-15 text-muted">
|
||||||
Switch between dark and light modes to suit user preferences and improve readability.
|
Public REST API untuk sinkronisasi ERP/CMMS/Helpdesk serta webhook event (movement/disposal/audit/maintenance).
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +197,7 @@
|
|||||||
<div class="heading-subtitle">Alur End-to-End</div>
|
<div class="heading-subtitle">Alur End-to-End</div>
|
||||||
<div class="heading-title">Dari registrasi sampai audit & perawatan</div>
|
<div class="heading-title">Dari registrasi sampai audit & perawatan</div>
|
||||||
<div class="heading-description">
|
<div class="heading-description">
|
||||||
Ikuti alur standar manajemen aset: daftar & QR, movement, disposal/reverse, maintenance, audit, dan laporan.
|
Ikuti alur standar manajemen aset: setup awal, identifikasi (QR/RFID/NFC), transaksi, approval, audit, dan laporan.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -290,8 +213,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="d-block fw-semibold">Registrasi & QR</h6>
|
<h6 class="d-block fw-semibold">Setup & Registrasi</h6>
|
||||||
<span class="d-block text-muted">Kode aset otomatis, QR untuk scan cepat, link ke detail & transaksi.</span>
|
<span class="d-block text-muted">Wizard setup, setting, master data, lalu registrasi aset dengan kode otomatis.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,7 +265,41 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="d-block fw-semibold">Audit & Laporan</h6>
|
<h6 class="d-block fw-semibold">Audit & Laporan</h6>
|
||||||
<span class="d-block text-muted">Audit fisik, status matched/missing/damaged, laporan Excel/PDF multi-sheet.</span>
|
<span class="d-block text-muted">Audit fisik (scan QR/NFC), changelog, dan laporan Excel/PDF siap kirim.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card custom-card landing-services-card info">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<div class="lh-1">
|
||||||
|
<span class="avatar avatar-lg avatar-rounded bg-info-transparent svg-info">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,40a16,16,0,1,1-16,16A16,16,0,0,1,128,64Zm24,128H104a8,8,0,0,1,0-16h8V120h-8a8,8,0,0,1,0-16h24a8,8,0,0,1,8,8v64h16a8,8,0,0,1,0,16Z" opacity="0.2"/><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,40a16,16,0,1,1-16,16A16,16,0,0,1,128,64Zm24,128H104a8,8,0,0,1,0-16h8V120h-8a8,8,0,0,1,0-16h24a8,8,0,0,1,8,8v64h16a8,8,0,0,1,0,16Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="d-block fw-semibold">Approval & Audit Trail</h6>
|
||||||
|
<span class="d-block text-muted">Movement/disposal/maintenance dapat butuh approval; notifikasi email & jejak audit tercatat.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card custom-card landing-services-card danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<div class="lh-1">
|
||||||
|
<span class="avatar avatar-lg avatar-rounded bg-danger-transparent svg-danger">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M224,56V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H216A8,8,0,0,1,224,56Z" opacity="0.2"/><path d="M224,56V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H216A8,8,0,0,1,224,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="96" x2="224" y2="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="80" y1="96" x2="80" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="d-block fw-semibold">Import & Skalabilitas</h6>
|
||||||
|
<span class="d-block text-muted">Import besar diproses via queue + status batch, pagination server-side, dan indexing DB.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,7 +309,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xl-5 my-auto">
|
<div class="col-xl-5 my-auto">
|
||||||
<div class="services-image-container text-end d-xl-block d-none">
|
<div class="services-image-container text-end d-xl-block d-none">
|
||||||
<img src="{{asset('build/assets/images/media/media-67.png')}}" alt="" class="img-fluid">
|
<img src="{{ Vite::asset('resources/assets/images/landing/flow.jpg') }}" alt="Alur Manajemen Aset" class="img-fluid rounded-3 shadow-sm">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,272 +317,14 @@
|
|||||||
</section>
|
</section>
|
||||||
<!-- End:: Section-3 -->
|
<!-- End:: Section-3 -->
|
||||||
|
|
||||||
<!-- Start:: Buy Now Section -->
|
|
||||||
<section class="section section-md section-primary text-fixed-white py-5 buy-now-section">
|
|
||||||
<div class="testimonials-background-container">
|
|
||||||
<img src="{{ asset('landing-cta.jpg') }}" alt="">
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="d-flex align-items-center gap-2 justify-content-between flex-wrap">
|
|
||||||
<div>
|
|
||||||
<h4 class="fw-semibold text-fixed-white">Transform Your Workflow Today</h4>
|
|
||||||
<span class="d-block fs-16 op-8">Unlock all the powerful features of our admin template. <br> Purchase now or try the demo to see it in action!</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-list">
|
|
||||||
<a href="{{url('index')}}" class="btn btn-danger btn-lg btn-w-md d-inline-flex align-items-center">View Demo<i class="ti ti-arrow-narrow-right ms-2"></i></a>
|
|
||||||
<button class="btn btn-success btn-lg btn-w-md d-inline-flex align-items-center">Buy Now<i class="ti ti-shopping-cart ms-2"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- End:: Buy Now Section -->
|
|
||||||
|
|
||||||
<!-- Start:: Section-4 -->
|
|
||||||
<section class="section" id="price">
|
|
||||||
<div class="container">
|
|
||||||
<div class="heading-section">
|
|
||||||
<div class="heading-subtitle">Our Pricing</div>
|
|
||||||
<div class="heading-title">Choose the Plan That Fits Your Needs</div>
|
|
||||||
<div class="heading-description mb-3">
|
|
||||||
Select the right fit and enjoy seamless access to all features.
|
|
||||||
</div>
|
|
||||||
<div class="tab-style-1 border bg-white rounded-0 d-inline-block">
|
|
||||||
<ul class="nav nav-pills" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button type="button" class="nav-link rounded-0 fw-medium active" data-bs-toggle="pill" data-bs-target="#pricing1-monthly" aria-selected="true" role="tab">Monthly</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button type="button" class="nav-link rounded-0 fw-medium" data-bs-toggle="pill" data-bs-target="#pricing1-yearly" aria-selected="false" role="tab" tabindex="-1">Yearly</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane show active p-0 border-0" id="pricing1-monthly" role="tabpanel">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3">Basic</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-primary">$15</h2>
|
|
||||||
<span class="fs-13 text-muted">/ Month</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
Access to core features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
5GB storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Basic customer support
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
1 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Email notifications
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-secondary-transparent py-1 px-3 fs-13 rounded-pill fw-normal">25% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-primary btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card card-bg-primary">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3 text-fixed-white">Pro</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-fixed-white">$45</h2>
|
|
||||||
<span class="fs-13">/ Month</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
All Basic Plan Features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
50GB storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Priority customer support
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
5 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Advanced analytics
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-white-transparent py-1 px-3 fs-13 rounded-pill fw-normal">40% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-light btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3">Enterprise</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-primary">$99</h2>
|
|
||||||
<span class="fs-13 text-muted">/ Month</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
All Pro Plan Features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Unlimited storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Dedicated account manager
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
20 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Customizable workflows
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-secondary-transparent py-1 px-3 fs-13 rounded-pill fw-normal">50% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-primary btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane p-0 border-0" id="pricing1-yearly" role="tabpanel">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3">Basic</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-primary">$150</h2>
|
|
||||||
<span class="fs-13 text-muted">/ Year</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
Access to core features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
5GB storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Basic customer support
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
1 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Email notifications
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-secondary-transparent py-1 px-3 fs-13 rounded-pill fw-normal">25% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-primary btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card card-bg-primary">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3 text-fixed-white">Pro</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-fixed-white">$450</h2>
|
|
||||||
<span class="fs-13">/ Year</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
All Basic Plan Features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
50GB storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Priority customer support
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
5 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Advanced analytics
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-white-transparent py-1 px-3 fs-13 rounded-pill fw-normal">40% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-light btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="card custom-card">
|
|
||||||
<div class="card-body p-4 text-center">
|
|
||||||
<h5 class="fw-semibold mb-3">Enterprise</h5>
|
|
||||||
<div class="d-flex align-items-end justify-content-center gap-1 mb-4">
|
|
||||||
<h2 class="fw-semibold mb-0 lh-1 text-primary">$990</h2>
|
|
||||||
<span class="fs-13 text-muted">/ Year</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled pricing-features-list-1 mx-5 text-start mb-4">
|
|
||||||
<li>
|
|
||||||
All Pro Plan Features
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Unlimited storage
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Dedicated account manager
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
20 user access
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Customizable workflows
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-secondary-transparent py-1 px-3 fs-13 rounded-pill fw-normal">50% Off</span>
|
|
||||||
<div class="d-grid mt-3">
|
|
||||||
<button class="btn btn-primary btn-lg">Choose Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- End:: Section-4 -->
|
|
||||||
|
|
||||||
<!-- Start:: Section-5 -->
|
<!-- Start:: Section-5 -->
|
||||||
<section class="section">
|
<section class="section" id="faq">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="heading-section">
|
<div class="heading-section">
|
||||||
<div class="heading-subtitle">FAQ's</div>
|
<div class="heading-subtitle">FAQ</div>
|
||||||
<div class="heading-title">Need Help? Find Your Answers Here</div>
|
<div class="heading-title">Pertanyaan Umum</div>
|
||||||
<div class="heading-description">
|
<div class="heading-description">
|
||||||
Browse through common questions to get quick solutions. We're here to help!
|
Ringkasan jawaban untuk alur, fitur, dan konfigurasi sistem manajemen aset.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -634,72 +333,72 @@
|
|||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Eleven">
|
<h2 class="accordion-header" id="headingcustomicon2Eleven">
|
||||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Eleven" aria-expanded="true" aria-controls="collapsecustomicon2Eleven">
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Eleven" aria-expanded="true" aria-controls="collapsecustomicon2Eleven">
|
||||||
<i class="ri-layout-4-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>How do I customize my dashboard layout?
|
<i class="ri-layout-4-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Bagaimana cara memulai setup pertama kali?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Eleven" class="accordion-collapse collapse show" aria-labelledby="headingcustomicon2Eleven" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Eleven" class="accordion-collapse collapse show" aria-labelledby="headingcustomicon2Eleven" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
You can easily customize the dashboard by dragging and dropping widgets. Go to the settings menu and select 'Customize Dashboard' to rearrange the layout.
|
Saat database belum siap, aplikasi otomatis mengarahkan ke Wizard Setup. Anda membuat admin, mengisi setting (termasuk SMTP), lalu master data, dan aset pertama.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Twelve">
|
<h2 class="accordion-header" id="headingcustomicon2Twelve">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Twelve" aria-expanded="false" aria-controls="collapsecustomicon2Twelve">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Twelve" aria-expanded="false" aria-controls="collapsecustomicon2Twelve">
|
||||||
<i class="ri-plug-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Can I integrate third-party apps with the admin template?
|
<i class="ri-plug-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Apakah sistem mendukung integrasi ERP/Helpdesk?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Twelve" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Twelve" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Twelve" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Twelve" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Yes! Our admin template supports seamless integrations with third-party apps. You can easily connect tools like Google Analytics, CRM software, and more.
|
Ya. Tersedia Public REST API (header X-Api-Key) dan webhook event aset (movement/disposal/audit/maintenance). Dokumentasi ada di halaman API Docs.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Thirteen">
|
<h2 class="accordion-header" id="headingcustomicon2Thirteen">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Thirteen" aria-expanded="false" aria-controls="collapsecustomicon2Thirteen">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Thirteen" aria-expanded="false" aria-controls="collapsecustomicon2Thirteen">
|
||||||
<i class="ri-phone-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Is this admin template mobile responsive?
|
<i class="ri-phone-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Apakah QR/RFID/NFC wajib untuk semua aset?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Thirteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Thirteen" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Thirteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Thirteen" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Absolutely! The admin template is fully responsive and optimized for mobile, ensuring that it works perfectly on smartphones and tablets.
|
QR bisa diaktifkan/nonaktifkan lewat setting. RFID/NFC juga opsional, dan dapat dibuat otomatis berdasarkan kode aset (auto-generate) atau diwajibkan (required) sesuai kebutuhan perusahaan.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Fourteen">
|
<h2 class="accordion-header" id="headingcustomicon2Fourteen">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Fourteen" aria-expanded="false" aria-controls="collapsecustomicon2Fourteen">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Fourteen" aria-expanded="false" aria-controls="collapsecustomicon2Fourteen">
|
||||||
<i class="ri-user-settings-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>How do I manage user roles and permissions?
|
<i class="ri-user-settings-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Bagaimana flow approval bekerja?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Fourteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Fourteen" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Fourteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Fourteen" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
You can manage user roles and permissions under the 'User Management' section. Simply assign roles like Admin, Manager, or Viewer to control access.
|
Untuk movement/disposal/maintenance, sistem dapat menandai transaksi sebagai pending. Approver menerima notifikasi email (SMTP setting) dan dapat approve/reject. Semua keputusan dicatat sebagai audit trail.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Fifteen">
|
<h2 class="accordion-header" id="headingcustomicon2Fifteen">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Fifteen" aria-expanded="false" aria-controls="collapsecustomicon2Fifteen">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Fifteen" aria-expanded="false" aria-controls="collapsecustomicon2Fifteen">
|
||||||
<i class="ri-file-excel-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Can I export data from the reports section?
|
<i class="ri-file-excel-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Bagaimana import data besar (mis. 25.000 baris)?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Fifteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Fifteen" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Fifteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Fifteen" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Yes, you can easily export reports as CSV, PDF, or Excel files. Just click the export button at the top of the reports page."
|
Sistem menyediakan preview import dan deduplikasi. Jika jumlah baris melewati threshold, import otomatis diantrikan ke queue dan statusnya tercatat di batch (queued/processing/success/failed). Gambar bisa dari URL atau ZIP.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingcustomicon2Sixteen">
|
<h2 class="accordion-header" id="headingcustomicon2Sixteen">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Sixteen" aria-expanded="false" aria-controls="collapsecustomicon2Sixteen">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsecustomicon2Sixteen" aria-expanded="false" aria-controls="collapsecustomicon2Sixteen">
|
||||||
<i class="ri-notification-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>How do I enable notifications for updates?
|
<i class="ri-notification-line fw-medium avatar avatar-sm avatar-rounded bg-primary-transparent fs-5 me-2 text-primary flex-shrink-0"></i>Bagaimana mengaktifkan notifikasi email?
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapsecustomicon2Sixteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Sixteen" data-bs-parent="#accordionFAQ3">
|
<div id="collapsecustomicon2Sixteen" class="accordion-collapse collapse" aria-labelledby="headingcustomicon2Sixteen" data-bs-parent="#accordionFAQ3">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Notifications can be enabled under the 'Settings' section. You can choose to receive real-time alerts via email or in-app for important updates.
|
Isi setting SMTP di menu Settings (mail.host/port/username/password/encryption/from). Setelah itu jalankan queue worker (php artisan queue:work) agar email notifikasi terkirim.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -710,6 +409,31 @@
|
|||||||
</section>
|
</section>
|
||||||
<!-- End:: Section-5 -->
|
<!-- End:: Section-5 -->
|
||||||
|
|
||||||
|
<!-- Start:: CTA -->
|
||||||
|
<section class="section section-md section-primary text-fixed-white py-5" id="get-started">
|
||||||
|
<div class="testimonials-background-container">
|
||||||
|
<img src="{{ Vite::asset('resources/assets/images/landing/cta.jpg') }}" alt="Get Started">
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex align-items-center gap-2 justify-content-between flex-wrap">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-semibold text-fixed-white mb-1">Mulai kelola aset dengan rapi, terukur, dan siap audit</h4>
|
||||||
|
<span class="d-block fs-16 op-8">Setup cepat, role & permission, approval, audit trail, import besar via queue, dan integrasi API/webhook.</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-list">
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('index') }}" class="btn btn-light btn-lg btn-w-md d-inline-flex align-items-center">Dashboard<i class="ti ti-arrow-narrow-right ms-2"></i></a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('register') }}" class="btn btn-light btn-lg btn-w-md d-inline-flex align-items-center">Buat Akun<i class="ti ti-arrow-narrow-right ms-2"></i></a>
|
||||||
|
@endauth
|
||||||
|
<a href="{{ route('api.docs') }}" class="btn btn-outline-light btn-lg btn-w-md d-inline-flex align-items-center">API Docs<i class="ti ti-book-2 ms-2"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- End:: CTA -->
|
||||||
|
|
||||||
|
{{--
|
||||||
<!-- Start:: Section-6 -->
|
<!-- Start:: Section-6 -->
|
||||||
<section class="section section-primary testimonials-section">
|
<section class="section section-primary testimonials-section">
|
||||||
<div class="testimonials-background-container">
|
<div class="testimonials-background-container">
|
||||||
@@ -1227,6 +951,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- End:: Buy Now Section -->
|
<!-- End:: Buy Now Section -->
|
||||||
|
--}}
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|||||||
@@ -9,112 +9,135 @@ use App\Models\AssetMovement;
|
|||||||
use App\Models\AssetStatus;
|
use App\Models\AssetStatus;
|
||||||
use App\Models\AssetUser;
|
use App\Models\AssetUser;
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
|
use App\Models\Permission;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Asset\AssetService;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Illuminate\Support\Str;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class AssetTransactionFlowTest extends TestCase
|
class AssetTransactionFlowTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
private function userWithPermission(string ): User
|
private function userWithPermission(string $permission): User
|
||||||
{
|
{
|
||||||
Permission::findOrCreate(, 'web');
|
Permission::findOrCreate($permission, 'web');
|
||||||
= User::factory()->create();
|
|
||||||
->givePermissionTo();
|
$user = User::factory()->create();
|
||||||
return ;
|
$user->givePermissionTo($permission);
|
||||||
|
|
||||||
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function baseAsset(): Asset
|
private function baseAsset(): Asset
|
||||||
{
|
{
|
||||||
= AssetStatus::create(['name' => 'Active', 'code' => 'ACTIVE']);
|
$status = AssetStatus::create(['name' => 'Active', 'code' => 'ACTIVE']);
|
||||||
= AssetLocation::create(['name' => 'Loc A', 'code' => 'L-A']);
|
$loc = AssetLocation::create(['name' => 'Loc A', 'code' => 'L-A']);
|
||||||
= Department::create(['name' => 'Dept A', 'code' => 'D-A']);
|
$dept = Department::create(['name' => 'Dept A', 'code' => 'D-A']);
|
||||||
= AssetUser::create(['name' => 'User A']);
|
$assetUser = AssetUser::create(['name' => 'User A']);
|
||||||
|
|
||||||
return Asset::create([
|
return Asset::create([
|
||||||
'code' => 'AST-00001',
|
'code' => 'AST-00001',
|
||||||
'name' => 'Asset Test',
|
'name' => 'Asset Test',
|
||||||
'asset_status_id' => ->id,
|
'asset_status_id' => $status->id,
|
||||||
'asset_location_id' => ->id,
|
'asset_location_id' => $loc->id,
|
||||||
'department_id' => ->id,
|
'department_id' => $dept->id,
|
||||||
'asset_user_id' => ->id,
|
'asset_user_id' => $assetUser->id,
|
||||||
'qr_token' => 'tok-1',
|
'qr_token' => (string) Str::uuid(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_movement_flow_updates_asset_and_marks_approved()
|
public function test_movement_flow_updates_asset_and_marks_approved_when_auto_approve(): void
|
||||||
{
|
{
|
||||||
config(['system.workflow.require_approval.movement' => false]);
|
config(['system.workflow.require_approval.movement' => false]);
|
||||||
= app(AssetService::class);
|
|
||||||
= ->baseAsset();
|
|
||||||
|
|
||||||
= AssetLocation::create(['name' => 'Loc B', 'code' => 'L-B']);
|
$user = $this->userWithPermission('movements.manage');
|
||||||
= Department::create(['name' => 'Dept B', 'code' => 'D-B']);
|
$asset = $this->baseAsset();
|
||||||
= AssetUser::create(['name' => 'User B']);
|
|
||||||
|
|
||||||
= ->move(, [
|
$toLoc = AssetLocation::create(['name' => 'Loc B', 'code' => 'L-B']);
|
||||||
'to_location_id' => ->id,
|
$toDept = Department::create(['name' => 'Dept B', 'code' => 'D-B']);
|
||||||
'to_department_id' => ->id,
|
$toUser = AssetUser::create(['name' => 'User B']);
|
||||||
'to_asset_user_id' => ->id,
|
|
||||||
|
$response = $this->actingAs($user)->post(route('assets.movements.store', $asset), [
|
||||||
|
'to_location_id' => $toLoc->id,
|
||||||
|
'to_department_id' => $toDept->id,
|
||||||
|
'to_asset_user_id' => $toUser->id,
|
||||||
'notes' => 'Test move',
|
'notes' => 'Test move',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
->refresh();
|
$response->assertSessionHasNoErrors();
|
||||||
->assertEquals('approved', ->status);
|
|
||||||
->assertEquals(->id, ->asset_location_id);
|
$asset->refresh();
|
||||||
->assertEquals(->id, ->department_id);
|
$this->assertEquals($toLoc->id, $asset->asset_location_id);
|
||||||
->assertEquals(->id, ->asset_user_id);
|
$this->assertEquals($toDept->id, $asset->department_id);
|
||||||
|
$this->assertEquals($toUser->id, $asset->asset_user_id);
|
||||||
|
|
||||||
|
$movement = AssetMovement::first();
|
||||||
|
$this->assertNotNull($movement);
|
||||||
|
$this->assertEquals('approved', $movement->status);
|
||||||
|
$this->assertNotNull($movement->approved_at);
|
||||||
|
$this->assertEquals($user->id, $movement->approved_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_disposal_requires_permission_and_sets_disposed_status()
|
public function test_disposal_requires_permission_and_sets_disposed_status_when_auto_approve(): void
|
||||||
{
|
{
|
||||||
config(['system.workflow.require_approval.disposal' => false]);
|
config(['system.workflow.require_approval.disposal' => false]);
|
||||||
= app(AssetService::class);
|
|
||||||
= ->baseAsset();
|
|
||||||
|
|
||||||
// tanpa permission -> 403
|
// Buat status disposed agar service mengubah status aset.
|
||||||
->actingAs(User::factory()->create());
|
|
||||||
= ->post(route('assets.disposals.store', ), [
|
|
||||||
'reason' => 'Rusak',
|
|
||||||
]);
|
|
||||||
->assertForbidden();
|
|
||||||
|
|
||||||
// dengan permission
|
|
||||||
= ->userWithPermission('disposals.manage');
|
|
||||||
->actingAs();
|
|
||||||
= ->post(route('assets.disposals.store', ), [
|
|
||||||
'reason' => 'Rusak',
|
|
||||||
]);
|
|
||||||
->assertSessionHasNoErrors();
|
|
||||||
|
|
||||||
->refresh();
|
|
||||||
= AssetDisposal::first();
|
|
||||||
->assertEquals('approved', ->status);
|
|
||||||
->assertEquals('DISPOSED', ->status?->code ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reverse_restores_asset_and_marks_disposal_reversed()
|
|
||||||
{
|
|
||||||
config(['system.workflow.require_approval.disposal' => false]);
|
|
||||||
= app(AssetService::class);
|
|
||||||
= ->baseAsset();
|
|
||||||
= ->asset_location_id;
|
|
||||||
= ->department_id;
|
|
||||||
|
|
||||||
// buat status disposed untuk update status
|
|
||||||
AssetStatus::firstOrCreate(['code' => 'DISPOSED'], ['name' => 'Disposed']);
|
AssetStatus::firstOrCreate(['code' => 'DISPOSED'], ['name' => 'Disposed']);
|
||||||
|
|
||||||
= ->dispose(, ['reason' => 'Rusak']);
|
$asset = $this->baseAsset();
|
||||||
->reverseDisposal(, 'Tes reverse');
|
|
||||||
|
|
||||||
->refresh();
|
// Tanpa permission: harus 403 dari middleware `permission:disposals.manage`.
|
||||||
->refresh();
|
$response = $this->actingAs(User::factory()->create())->post(route('assets.disposals.store', $asset), [
|
||||||
|
'reason' => 'Rusak',
|
||||||
|
]);
|
||||||
|
$response->assertForbidden();
|
||||||
|
|
||||||
->assertEquals(, ->asset_location_id);
|
// Dengan permission: berhasil, status disposal approved, asset menjadi DISPOSED.
|
||||||
->assertEquals(, ->department_id);
|
$user = $this->userWithPermission('disposals.manage');
|
||||||
->assertEquals('reversed', ->status);
|
$response = $this->actingAs($user)->post(route('assets.disposals.store', $asset), [
|
||||||
|
'reason' => 'Rusak',
|
||||||
|
'notes' => 'Pengujian disposal',
|
||||||
|
]);
|
||||||
|
$response->assertSessionHasNoErrors();
|
||||||
|
|
||||||
|
$asset->refresh();
|
||||||
|
$disposal = AssetDisposal::first();
|
||||||
|
$this->assertNotNull($disposal);
|
||||||
|
$this->assertEquals('approved', $disposal->status);
|
||||||
|
$this->assertEquals('DISPOSED', optional($asset->status)->code);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function test_reverse_restores_asset_and_marks_disposal_reversed(): void
|
||||||
|
{
|
||||||
|
config(['system.workflow.require_approval.disposal' => false]);
|
||||||
|
AssetStatus::firstOrCreate(['code' => 'DISPOSED'], ['name' => 'Disposed']);
|
||||||
|
|
||||||
|
$user = $this->userWithPermission('disposals.manage');
|
||||||
|
$asset = $this->baseAsset();
|
||||||
|
$previousStatusId = $asset->asset_status_id;
|
||||||
|
$previousLocationId = $asset->asset_location_id;
|
||||||
|
$previousDepartmentId = $asset->department_id;
|
||||||
|
|
||||||
|
$this->actingAs($user)->post(route('assets.disposals.store', $asset), [
|
||||||
|
'reason' => 'Rusak',
|
||||||
|
])->assertSessionHasNoErrors();
|
||||||
|
|
||||||
|
$disposal = AssetDisposal::first();
|
||||||
|
$this->assertNotNull($disposal);
|
||||||
|
|
||||||
|
$this->actingAs($user)->post(route('assets.disposals.reverse', $disposal), [
|
||||||
|
'notes' => 'Tes reverse',
|
||||||
|
])->assertSessionHasNoErrors();
|
||||||
|
|
||||||
|
$asset->refresh();
|
||||||
|
$disposal->refresh();
|
||||||
|
|
||||||
|
$this->assertEquals($previousStatusId, $asset->asset_status_id);
|
||||||
|
$this->assertEquals($previousLocationId, $asset->asset_location_id);
|
||||||
|
$this->assertEquals($previousDepartmentId, $asset->department_id);
|
||||||
|
$this->assertEquals('reversed', $disposal->status);
|
||||||
|
$this->assertNotNull($disposal->reversed_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
108
tests/Feature/DashboardPageTest.php
Normal file
108
tests/Feature/DashboardPageTest.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\AssetApprovalRequest;
|
||||||
|
use App\Models\AssetAudit;
|
||||||
|
use App\Models\AssetDisposal;
|
||||||
|
use App\Models\AssetLocation;
|
||||||
|
use App\Models\AssetMaintenance;
|
||||||
|
use App\Models\AssetMovement;
|
||||||
|
use App\Models\AssetStatus;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DashboardPageTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_dashboard_loads_with_summary_data(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$statusActive = AssetStatus::create(['name' => 'Active', 'code' => 'ACTIVE']);
|
||||||
|
$statusRepair = AssetStatus::create(['name' => 'Repair', 'code' => 'REPAIR']);
|
||||||
|
|
||||||
|
$locHq = AssetLocation::create(['name' => 'HQ', 'code' => 'HQ']);
|
||||||
|
$locBranch = AssetLocation::create(['name' => 'Branch', 'code' => 'BR']);
|
||||||
|
|
||||||
|
$asset1 = Asset::create([
|
||||||
|
'code' => 'AST-001',
|
||||||
|
'name' => 'Laptop A',
|
||||||
|
'asset_status_id' => $statusActive->id,
|
||||||
|
'asset_location_id' => $locHq->id,
|
||||||
|
'qr_token' => (string) Str::uuid(),
|
||||||
|
'warranty_end' => now()->addDays(7)->toDateString(),
|
||||||
|
'cost' => 10000000,
|
||||||
|
]);
|
||||||
|
$asset2 = Asset::create([
|
||||||
|
'code' => 'AST-002',
|
||||||
|
'name' => 'Printer B',
|
||||||
|
'asset_status_id' => $statusRepair->id,
|
||||||
|
'asset_location_id' => $locBranch->id,
|
||||||
|
'qr_token' => (string) Str::uuid(),
|
||||||
|
'warranty_end' => now()->addDays(10)->toDateString(),
|
||||||
|
'cost' => 2500000,
|
||||||
|
]);
|
||||||
|
$asset3 = Asset::create([
|
||||||
|
'code' => 'AST-003',
|
||||||
|
'name' => 'Tinta Printer',
|
||||||
|
'asset_status_id' => $statusActive->id,
|
||||||
|
'asset_location_id' => $locHq->id,
|
||||||
|
'qr_token' => (string) Str::uuid(),
|
||||||
|
'is_consumable' => true,
|
||||||
|
'quantity' => 10,
|
||||||
|
'available_quantity' => 3,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Arsip & trash (soft delete).
|
||||||
|
$asset2->update(['archived_at' => now()]);
|
||||||
|
$asset3->delete();
|
||||||
|
|
||||||
|
// Aktivitas: movement/disposal/audit issue/maintenance.
|
||||||
|
$movement = AssetMovement::create([
|
||||||
|
'asset_id' => $asset1->id,
|
||||||
|
'status' => 'approved',
|
||||||
|
'performed_at' => now()->subDays(2),
|
||||||
|
]);
|
||||||
|
AssetDisposal::create([
|
||||||
|
'asset_id' => $asset1->id,
|
||||||
|
'status' => 'approved',
|
||||||
|
'disposed_at' => now()->subDays(1),
|
||||||
|
'reversed_at' => null,
|
||||||
|
]);
|
||||||
|
AssetAudit::create([
|
||||||
|
'asset_id' => $asset1->id,
|
||||||
|
'status' => 'missing',
|
||||||
|
'audited_at' => now()->subDays(1),
|
||||||
|
]);
|
||||||
|
AssetMaintenance::create([
|
||||||
|
'asset_id' => $asset1->id,
|
||||||
|
'status' => 'planned',
|
||||||
|
'performed_at' => now()->addDays(2),
|
||||||
|
'description' => 'Preventive maintenance',
|
||||||
|
]);
|
||||||
|
|
||||||
|
AssetApprovalRequest::create([
|
||||||
|
'approvable_id' => $movement->id,
|
||||||
|
'approvable_type' => AssetMovement::class,
|
||||||
|
'type' => 'movement',
|
||||||
|
'status' => 'pending',
|
||||||
|
'requested_by' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get(route('index'));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Dashboard');
|
||||||
|
$response->assertViewHas('totalAssets', 2);
|
||||||
|
$response->assertViewHas('archivedAssets', 1);
|
||||||
|
$response->assertViewHas('softDeletedAssets', 1);
|
||||||
|
$response->assertViewHas('pendingApprovalsCount', 1);
|
||||||
|
$response->assertViewHas('warrantyExpiring', 2);
|
||||||
|
$response->assertViewHas('auditIssues', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user