Compare commits
39 Commits
fix/nginx-
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e681f6455 | ||
|
|
88bed2a3ef | ||
|
|
c1f8e2986b | ||
|
|
e94dd1ff81 | ||
|
|
f4234ee573 | ||
|
|
ac1183ac5e | ||
|
|
3092ecf34b | ||
|
|
ed920e8e7b | ||
|
|
c01d95a61b | ||
|
|
9f500a5da2 | ||
|
|
45f79e7027 | ||
|
|
db4c586535 | ||
|
|
cab0d2e9a8 | ||
|
|
e2a49530b7 | ||
|
|
193f8c36af | ||
|
|
9a39cabee3 | ||
|
|
f123e082f9 | ||
|
|
833d5abbb5 | ||
|
|
4b9be55d32 | ||
|
|
5b14523f84 | ||
|
|
b97a5f4740 | ||
|
|
dff0f7ceba | ||
|
|
96a9729a35 | ||
|
|
a59f685d41 | ||
|
|
68e7eb3087 | ||
|
|
720e314bbd | ||
|
|
0b1589d173 | ||
|
|
e3956ae0e4 | ||
|
|
748ac8a77e | ||
|
|
e52c4d1d27 | ||
|
|
cec11d6385 | ||
|
|
b632996052 | ||
|
|
e59841fd23 | ||
|
|
e468672bbe | ||
|
|
685c6df82e | ||
|
|
cfef3775d7 | ||
|
|
956df5cfe6 | ||
|
|
fa554446ca | ||
|
|
0ef03fe7cb |
@@ -1,52 +1,71 @@
|
|||||||
# Git
|
# Dependencies
|
||||||
.git
|
node_modules/
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
|
|
||||||
# Docker files
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Development files
|
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.staging
|
|
||||||
node_modules
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# IDE files
|
# Laravel
|
||||||
.vscode
|
/vendor/
|
||||||
.idea
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Storage and logs
|
||||||
|
/storage/*.key
|
||||||
|
/storage/logs/*
|
||||||
|
/storage/framework/cache/*
|
||||||
|
/storage/framework/sessions/*
|
||||||
|
/storage/framework/views/*
|
||||||
|
/bootstrap/cache/*
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
||||||
# OS files
|
# OS generated files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Laravel specific
|
# Git
|
||||||
storage/app/*
|
.git/
|
||||||
!storage/app/.gitignore
|
.gitignore
|
||||||
storage/framework/cache/*
|
|
||||||
!storage/framework/cache/.gitignore
|
|
||||||
storage/framework/sessions/*
|
|
||||||
!storage/framework/sessions/.gitignore
|
|
||||||
storage/framework/views/*
|
|
||||||
!storage/framework/views/.gitignore
|
|
||||||
storage/logs/*
|
|
||||||
!storage/logs/.gitignore
|
|
||||||
bootstrap/cache/*
|
|
||||||
!bootstrap/cache/.gitignore
|
|
||||||
|
|
||||||
# Backup files
|
# Documentation
|
||||||
*.zip
|
README.md
|
||||||
*.tar.gz
|
*.md
|
||||||
*.sql
|
|
||||||
|
|
||||||
# Test files
|
# Docker
|
||||||
tests/
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
/tests/
|
||||||
phpunit.xml
|
phpunit.xml
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
||||||
|
# Build tools
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Keep vendor assets in public folder
|
||||||
|
!public/js/vendor/
|
||||||
|
!public/css/vendor/
|
||||||
|
!public/js/locales/
|
||||||
|
|
||||||
|
# Keep built assets
|
||||||
|
!public/js/app.js
|
||||||
|
!public/js/vendor.js
|
||||||
|
!public/css/app.css
|
||||||
|
!public/mix-manifest.json
|
||||||
356
BACKUP_README.md
356
BACKUP_README.md
@@ -1,356 +0,0 @@
|
|||||||
# 🗃️ CKB Database Backup & Restore Guide
|
|
||||||
|
|
||||||
Panduan lengkap untuk backup dan restore database Docker CKB menggunakan mysqldump.
|
|
||||||
|
|
||||||
## 📋 Informasi Database
|
|
||||||
|
|
||||||
- **Container Name**: `ckb-mysql-dev`
|
|
||||||
- **Database Name**: `ckb_db`
|
|
||||||
- **Database Type**: MariaDB 10.6
|
|
||||||
- **Port**: 3306
|
|
||||||
- **Username**: `root` / `laravel`
|
|
||||||
- **Password**: `root` / `password`
|
|
||||||
|
|
||||||
## 🛠️ Available Scripts
|
|
||||||
|
|
||||||
### 1. **backup_db.sh** - Simple Backup
|
|
||||||
|
|
||||||
Script backup sederhana dengan kompresi opsional.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./backup_db.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- ✅ Backup database dengan timestamp
|
|
||||||
- ✅ Kompresi opsional (gzip)
|
|
||||||
- ✅ Validasi container status
|
|
||||||
- ✅ Progress indicator
|
|
||||||
|
|
||||||
### 2. **restore_db.sh** - Database Restore
|
|
||||||
|
|
||||||
Script restore dengan interface interaktif.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./restore_db.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- ✅ List backup files yang tersedia
|
|
||||||
- ✅ Support compressed & uncompressed files
|
|
||||||
- ✅ Konfirmasi sebelum restore
|
|
||||||
- ✅ Automatic decompression
|
|
||||||
|
|
||||||
### 3. **backup_advanced.sh** - Advanced Backup
|
|
||||||
|
|
||||||
Script backup lengkap dengan berbagai opsi.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./backup_advanced.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- ✅ Multiple backup types (Full, Structure, Data)
|
|
||||||
- ✅ Automatic cleanup old backups
|
|
||||||
- ✅ Colored output
|
|
||||||
- ✅ Backup statistics
|
|
||||||
- ✅ Compression with ratio info
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### Basic Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Simple backup
|
|
||||||
./backup_db.sh
|
|
||||||
|
|
||||||
# Manual backup command
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db > backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Interactive advanced backup
|
|
||||||
./backup_advanced.sh
|
|
||||||
|
|
||||||
# Full backup with all options
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--add-drop-table \
|
|
||||||
ckb_db > full_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restore Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Interactive restore
|
|
||||||
./restore_db.sh
|
|
||||||
|
|
||||||
# Manual restore
|
|
||||||
docker exec -i ckb-mysql-dev mysql -u root -proot ckb_db < backup.sql
|
|
||||||
|
|
||||||
# Restore compressed backup
|
|
||||||
gunzip -c backup.sql.gz | docker exec -i ckb-mysql-dev mysql -u root -proot ckb_db
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📂 Backup Types
|
|
||||||
|
|
||||||
### 1. **Full Backup** (Default)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--add-drop-table \
|
|
||||||
ckb_db > full_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Structure Only**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--no-data \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
ckb_db > structure_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Data Only**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--no-create-info \
|
|
||||||
--single-transaction \
|
|
||||||
ckb_db > data_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Compressed Backup**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db | gzip > backup.sql.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Manual Commands
|
|
||||||
|
|
||||||
### Basic Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check container status
|
|
||||||
docker ps | grep ckb-mysql-dev
|
|
||||||
|
|
||||||
# Backup with timestamp
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db > ckb_backup_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
|
|
||||||
# Backup all databases
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot --all-databases > all_databases.sql
|
|
||||||
|
|
||||||
# Backup specific tables
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot ckb_db table1 table2 > specific_tables.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Options
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup with extended options
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--events \
|
|
||||||
--add-drop-table \
|
|
||||||
--add-drop-trigger \
|
|
||||||
--hex-blob \
|
|
||||||
--complete-insert \
|
|
||||||
ckb_db > advanced_backup.sql
|
|
||||||
|
|
||||||
# Backup without locking tables (for MyISAM)
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--lock-tables=false \
|
|
||||||
ckb_db > no_lock_backup.sql
|
|
||||||
|
|
||||||
# Backup with conditions
|
|
||||||
docker exec ckb-mysql-dev mysqldump -u root -proot \
|
|
||||||
--where="created_at >= '2023-01-01'" \
|
|
||||||
ckb_db table_name > conditional_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Automated Backups
|
|
||||||
|
|
||||||
### Cron Job Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Edit crontab
|
|
||||||
crontab -e
|
|
||||||
|
|
||||||
# Daily backup at 2 AM
|
|
||||||
0 2 * * * /path/to/backup_db.sh
|
|
||||||
|
|
||||||
# Weekly full backup on Sunday at 3 AM
|
|
||||||
0 3 * * 0 /path/to/backup_advanced.sh
|
|
||||||
|
|
||||||
# Monthly cleanup (keep last 30 days)
|
|
||||||
0 4 1 * * find /path/to/backups -name "*.sql*" -mtime +30 -delete
|
|
||||||
```
|
|
||||||
|
|
||||||
### Systemd Timer (Alternative to Cron)
|
|
||||||
|
|
||||||
Create `/etc/systemd/system/ckb-backup.service`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=CKB Database Backup
|
|
||||||
After=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/path/to/backup_db.sh
|
|
||||||
User=root
|
|
||||||
```
|
|
||||||
|
|
||||||
Create `/etc/systemd/system/ckb-backup.timer`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Run CKB backup daily
|
|
||||||
Requires=ckb-backup.service
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Enable timer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl enable ckb-backup.timer
|
|
||||||
sudo systemctl start ckb-backup.timer
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Backup Management
|
|
||||||
|
|
||||||
### Check Backup Size
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Show backup directory size
|
|
||||||
du -sh backups/
|
|
||||||
|
|
||||||
# List all backups with sizes
|
|
||||||
ls -lah backups/
|
|
||||||
|
|
||||||
# Show compression ratio
|
|
||||||
for file in backups/*.sql.gz; do
|
|
||||||
original_size=$(gunzip -l "$file" | tail -1 | awk '{print $2}')
|
|
||||||
compressed_size=$(stat -c%s "$file")
|
|
||||||
ratio=$(echo "scale=1; (1 - $compressed_size/$original_size) * 100" | bc)
|
|
||||||
echo "$(basename "$file"): ${ratio}% compression"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cleanup Old Backups
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Delete backups older than 30 days
|
|
||||||
find backups/ -name "*.sql*" -mtime +30 -delete
|
|
||||||
|
|
||||||
# Keep only last 10 backups
|
|
||||||
ls -1t backups/ckb_backup_*.sql* | tail -n +11 | xargs rm -f
|
|
||||||
|
|
||||||
# Clean by size (keep if total < 1GB)
|
|
||||||
total_size=$(du -s backups/ | cut -f1)
|
|
||||||
if [ $total_size -gt 1048576 ]; then
|
|
||||||
# Delete oldest files until under 1GB
|
|
||||||
echo "Cleaning up old backups..."
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Container not running**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start container
|
|
||||||
docker-compose up -d db
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
docker logs ckb-mysql-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Permission denied**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fix script permissions
|
|
||||||
chmod +x *.sh
|
|
||||||
|
|
||||||
# Fix backup directory permissions
|
|
||||||
sudo chown -R $USER:$USER backups/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Out of disk space**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check disk usage
|
|
||||||
df -h
|
|
||||||
|
|
||||||
# Clean old backups
|
|
||||||
find backups/ -name "*.sql*" -mtime +7 -delete
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **MySQL connection error**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test connection
|
|
||||||
docker exec ckb-mysql-dev mysql -u root -proot -e "SELECT 1;"
|
|
||||||
|
|
||||||
# Check MySQL status
|
|
||||||
docker exec ckb-mysql-dev mysqladmin -u root -proot status
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Security Notes
|
|
||||||
|
|
||||||
1. **Never store passwords in plain text** - Consider using MySQL config files
|
|
||||||
2. **Encrypt sensitive backups** - Use GPG for production environments
|
|
||||||
3. **Secure backup storage** - Store backups in secure, offsite locations
|
|
||||||
4. **Regular restore testing** - Test backups regularly to ensure they work
|
|
||||||
|
|
||||||
## 📈 Best Practices
|
|
||||||
|
|
||||||
1. **Regular Backups**: Daily for production, weekly for development
|
|
||||||
2. **Multiple Backup Types**: Keep both full and incremental backups
|
|
||||||
3. **Offsite Storage**: Store backups in different physical/cloud locations
|
|
||||||
4. **Compression**: Use gzip to save storage space
|
|
||||||
5. **Retention Policy**: Define how long to keep backups
|
|
||||||
6. **Monitoring**: Monitor backup success/failure
|
|
||||||
7. **Documentation**: Document backup procedures and recovery steps
|
|
||||||
8. **Testing**: Regularly test restore procedures
|
|
||||||
|
|
||||||
## 📝 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
backups/
|
|
||||||
├── ckb_backup_20231225_143022.sql.gz # Full backup (compressed)
|
|
||||||
├── ckb_structure_20231225_143022.sql # Structure only
|
|
||||||
├── ckb_data_20231225_143022.sql.gz # Data only (compressed)
|
|
||||||
└── README.md # This documentation
|
|
||||||
|
|
||||||
Scripts:
|
|
||||||
├── backup_db.sh # Simple backup script
|
|
||||||
├── backup_advanced.sh # Advanced backup with options
|
|
||||||
├── restore_db.sh # Interactive restore script
|
|
||||||
└── BACKUP_README.md # This documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Made with ❤️ for CKB Project**
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
# 📊 Database Import Guide untuk CKB Laravel Application
|
|
||||||
|
|
||||||
## 🚀 Quick Start (Paling Mudah)
|
|
||||||
|
|
||||||
Jika Anda baru pertama kali setup aplikasi:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Jalankan quick setup yang otomatis import database
|
|
||||||
./docker-quick-setup.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📥 Manual Import Database
|
|
||||||
|
|
||||||
### 1. Import ke Development Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pastikan containers berjalan terlebih dahulu
|
|
||||||
./docker-start.sh dev up
|
|
||||||
|
|
||||||
# Import database ckb.sql
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
|
|
||||||
# Atau import file SQL lain
|
|
||||||
./docker-import-db.sh dev nama-file-backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Import ke Production Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start production environment
|
|
||||||
./docker-start.sh prod up
|
|
||||||
|
|
||||||
# Import database
|
|
||||||
./docker-import-db.sh prod
|
|
||||||
|
|
||||||
# Atau dengan file khusus
|
|
||||||
./docker-import-db.sh prod production-backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Auto Import (Recommended untuk First Time Setup)
|
|
||||||
|
|
||||||
Ketika Anda menjalankan Docker containers untuk pertama kali, file `ckb.sql` akan otomatis diimport ke database. Ini terjadi karena:
|
|
||||||
|
|
||||||
1. File `ckb.sql` di-mount ke `/docker-entrypoint-initdb.d/01-init.sql` di MySQL container
|
|
||||||
2. MySQL otomatis menjalankan semua file `.sql` di direktori tersebut saat inisialisasi
|
|
||||||
3. Auto import hanya terjadi jika database kosong/belum ada
|
|
||||||
|
|
||||||
## 🛠️ Troubleshooting Import
|
|
||||||
|
|
||||||
### Problem: Database tidak terimport otomatis
|
|
||||||
|
|
||||||
**Solusi:**
|
|
||||||
```bash
|
|
||||||
# 1. Stop containers
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# 2. Hapus volume database (HATI-HATI: akan hapus data!)
|
|
||||||
docker-compose down -v
|
|
||||||
|
|
||||||
# 3. Start ulang (akan trigger auto import)
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 4. Atau import manual
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem: Permission denied saat import
|
|
||||||
|
|
||||||
**Solusi:**
|
|
||||||
```bash
|
|
||||||
# Pastikan script executable
|
|
||||||
chmod +x docker-import-db.sh
|
|
||||||
chmod +x docker-quick-setup.sh
|
|
||||||
|
|
||||||
# Pastikan file SQL readable
|
|
||||||
chmod 644 ckb.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem: Database terlalu besar, import timeout
|
|
||||||
|
|
||||||
**Solusi:**
|
|
||||||
```bash
|
|
||||||
# Import langsung ke container dengan timeout yang lebih besar
|
|
||||||
docker-compose exec -T db mysql -u root -proot ckb_db < ckb.sql
|
|
||||||
|
|
||||||
# Atau split file SQL jika sangat besar
|
|
||||||
split -l 10000 ckb.sql ckb_split_
|
|
||||||
# Kemudian import satu per satu
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Verifikasi Import Berhasil
|
|
||||||
|
|
||||||
### 1. Cek via phpMyAdmin
|
|
||||||
- Buka http://localhost:8080
|
|
||||||
- Login dengan: server=db, username=root, password=root
|
|
||||||
- Pilih database `ckb_db`
|
|
||||||
- Lihat tabel yang sudah terimport
|
|
||||||
|
|
||||||
### 2. Cek via Command Line
|
|
||||||
```bash
|
|
||||||
# Lihat daftar tabel
|
|
||||||
docker-compose exec db mysql -u root -proot -e "USE ckb_db; SHOW TABLES;"
|
|
||||||
|
|
||||||
# Hitung jumlah tabel
|
|
||||||
docker-compose exec db mysql -u root -proot -e "USE ckb_db; SELECT COUNT(*) as total_tables FROM information_schema.tables WHERE table_schema='ckb_db';"
|
|
||||||
|
|
||||||
# Lihat contoh data dari salah satu tabel
|
|
||||||
docker-compose exec db mysql -u root -proot -e "USE ckb_db; SELECT * FROM users LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Aplikasi Laravel
|
|
||||||
```bash
|
|
||||||
# Cek koneksi database dari Laravel
|
|
||||||
docker-compose exec app php artisan tinker
|
|
||||||
# Di dalam tinker:
|
|
||||||
# DB::connection()->getPdo();
|
|
||||||
# \App\Models\User::count();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💾 Backup Database
|
|
||||||
|
|
||||||
### Backup Development
|
|
||||||
```bash
|
|
||||||
# Backup dengan timestamp
|
|
||||||
docker-compose exec db mysqldump -u root -proot ckb_db > backup_dev_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
|
|
||||||
# Backup sederhana
|
|
||||||
docker-compose exec db mysqldump -u root -proot ckb_db > backup_current.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup Production
|
|
||||||
```bash
|
|
||||||
# Backup production database
|
|
||||||
docker-compose -f docker-compose.prod.yml exec db mysqldump -u root -p ckb_production > backup_prod_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Replace Database dengan Backup Baru
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Backup database saat ini (safety)
|
|
||||||
docker-compose exec db mysqldump -u root -proot ckb_db > backup_before_replace.sql
|
|
||||||
|
|
||||||
# 2. Import database baru
|
|
||||||
./docker-import-db.sh dev new-backup.sql
|
|
||||||
|
|
||||||
# 3. Clear Laravel cache
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
docker-compose exec app php artisan config:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Notes Penting
|
|
||||||
|
|
||||||
1. **File ckb.sql**: Pastikan file ini selalu ada di root project untuk auto-import
|
|
||||||
2. **Backup Safety**: Script import otomatis membuat backup sebelum replace database
|
|
||||||
3. **Environment**: Selalu pastikan Anda menggunakan environment yang benar (dev/prod)
|
|
||||||
4. **Permissions**: Database user harus punya permission CREATE, DROP, INSERT untuk import
|
|
||||||
5. **Size Limit**: File SQL besar (>100MB) mungkin perlu setting timeout MySQL yang lebih besar
|
|
||||||
|
|
||||||
## 🎯 Best Practices
|
|
||||||
|
|
||||||
1. **Selalu backup** sebelum import database baru
|
|
||||||
2. **Test di development** dulu sebelum import ke production
|
|
||||||
3. **Gunakan quick setup** untuk setup pertama kali
|
|
||||||
4. **Monitor logs** saat import: `docker-compose logs -f db`
|
|
||||||
5. **Verify data** setelah import berhasil
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Untuk bantuan lebih lanjut, lihat file `DOCKER-README.md` atau `docker-import-db.sh --help`**
|
|
||||||
289
DEPLOYMENT.md
289
DEPLOYMENT.md
@@ -1,289 +0,0 @@
|
|||||||
# CKB Application Deployment Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This guide explains how to deploy the CKB Laravel application with Docker, SSL certificate, and reverse proxy configuration.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Ubuntu/Debian server
|
|
||||||
- Docker and Docker Compose installed
|
|
||||||
- Domain pointing to server IP
|
|
||||||
- Nginx installed on main server
|
|
||||||
- Root/sudo access
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Internet → Nginx (Port 80/443) → Docker Container (Port 8082) → Laravel App
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
/var/www/ckb/
|
|
||||||
├── docker-compose.prod.yml # Docker services configuration
|
|
||||||
├── Dockerfile # Laravel app container
|
|
||||||
├── docker/
|
|
||||||
│ ├── nginx-proxy.conf # Internal nginx proxy
|
|
||||||
│ ├── php.ini # PHP configuration
|
|
||||||
│ ├── mysql.cnf # MySQL configuration
|
|
||||||
│ └── supervisord.conf # Process manager
|
|
||||||
├── nginx-ckb-reverse-proxy.conf # Main server nginx config
|
|
||||||
├── deploy-ckb.sh # Deployment script
|
|
||||||
├── setup-ssl.sh # SSL certificate setup script
|
|
||||||
└── DEPLOYMENT.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Container Names and Volumes
|
|
||||||
|
|
||||||
All containers and volumes are prefixed with `ckb-` to avoid conflicts:
|
|
||||||
|
|
||||||
### Containers:
|
|
||||||
|
|
||||||
- `ckb-laravel-app` - Laravel application
|
|
||||||
- `ckb-mariadb` - Database
|
|
||||||
- `ckb-redis` - Cache/Queue
|
|
||||||
- `ckb-nginx-proxy` - Internal nginx proxy
|
|
||||||
|
|
||||||
### Volumes:
|
|
||||||
|
|
||||||
- `ckb_mysql_data` - Database data
|
|
||||||
- `ckb_redis_data` - Redis data
|
|
||||||
- `ckb_nginx_logs` - Nginx logs
|
|
||||||
- `ckb_storage_logs` - Laravel logs
|
|
||||||
- `ckb_storage_cache` - Laravel cache
|
|
||||||
|
|
||||||
## Step-by-Step Deployment
|
|
||||||
|
|
||||||
### Step 1: Prepare the Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/ckb
|
|
||||||
|
|
||||||
# Make scripts executable
|
|
||||||
chmod +x deploy-ckb.sh
|
|
||||||
chmod +x setup-ssl.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Deploy Docker Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run deployment script
|
|
||||||
./deploy-ckb.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script will:
|
|
||||||
|
|
||||||
- Stop existing containers
|
|
||||||
- Build and start new containers
|
|
||||||
- Check if containers are running
|
|
||||||
- Verify port 8082 is accessible
|
|
||||||
|
|
||||||
### Step 3: Setup SSL Certificate
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run SSL setup script (requires sudo)
|
|
||||||
sudo ./setup-ssl.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script will:
|
|
||||||
|
|
||||||
- Install certbot if not present
|
|
||||||
- Create temporary nginx configuration
|
|
||||||
- Generate Let's Encrypt certificate
|
|
||||||
- Update nginx with SSL configuration
|
|
||||||
- Setup auto-renewal
|
|
||||||
|
|
||||||
### Step 4: Manual Verification
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if containers are running
|
|
||||||
docker ps | grep ckb
|
|
||||||
|
|
||||||
# Check if port 8082 is accessible
|
|
||||||
curl -I http://localhost:8082
|
|
||||||
|
|
||||||
# Check SSL certificate
|
|
||||||
sudo certbot certificates
|
|
||||||
|
|
||||||
# Test HTTPS access
|
|
||||||
curl -I https://bengkel.digitaloasis.xyz
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
### docker-compose.prod.yml
|
|
||||||
|
|
||||||
- Updated container names with `ckb-` prefix
|
|
||||||
- Removed certbot service (handled by main server)
|
|
||||||
- Updated APP_URL to use HTTPS
|
|
||||||
- Specific volume names to avoid conflicts
|
|
||||||
|
|
||||||
### nginx-proxy.conf
|
|
||||||
|
|
||||||
- Simplified configuration (no SSL handling)
|
|
||||||
- Proxy to `ckb-app` container
|
|
||||||
- Rate limiting and security headers
|
|
||||||
- Static file caching
|
|
||||||
|
|
||||||
### nginx-ckb-reverse-proxy.conf
|
|
||||||
|
|
||||||
- Main server nginx configuration
|
|
||||||
- SSL termination
|
|
||||||
- Reverse proxy to port 8082
|
|
||||||
- Security headers and SSL settings
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Create `.env` file in `/var/www/ckb/`:
|
|
||||||
|
|
||||||
```env
|
|
||||||
APP_ENV=production
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_URL=https://bengkel.digitaloasis.xyz
|
|
||||||
DB_DATABASE=ckb_production
|
|
||||||
DB_USERNAME=laravel
|
|
||||||
DB_PASSWORD=your_password
|
|
||||||
DB_ROOT_PASSWORD=your_root_password
|
|
||||||
REDIS_PASSWORD=your_redis_password
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring and Maintenance
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker logs
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f
|
|
||||||
|
|
||||||
# Nginx logs (main server)
|
|
||||||
sudo tail -f /var/log/nginx/access.log
|
|
||||||
sudo tail -f /var/log/nginx/error.log
|
|
||||||
|
|
||||||
# Laravel logs
|
|
||||||
docker exec ckb-laravel-app tail -f /var/www/html/storage/logs/laravel.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL Certificate Renewal
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Manual renewal
|
|
||||||
sudo certbot renew
|
|
||||||
|
|
||||||
# Check renewal status
|
|
||||||
sudo certbot certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Management
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restart all services
|
|
||||||
docker-compose -f docker-compose.prod.yml restart
|
|
||||||
|
|
||||||
# Update application
|
|
||||||
git pull
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
|
|
||||||
# Stop all services
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
|
|
||||||
# Remove all data (WARNING: This will delete all data)
|
|
||||||
docker-compose -f docker-compose.prod.yml down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Port 8082 Not Accessible
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if container is running
|
|
||||||
docker ps | grep ckb-nginx-proxy
|
|
||||||
|
|
||||||
# Check container logs
|
|
||||||
docker-compose -f docker-compose.prod.yml logs ckb-nginx-proxy
|
|
||||||
|
|
||||||
# Check if port is bound
|
|
||||||
netstat -tlnp | grep 8082
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL Certificate Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check certificate status
|
|
||||||
sudo certbot certificates
|
|
||||||
|
|
||||||
# Test certificate
|
|
||||||
sudo certbot renew --dry-run
|
|
||||||
|
|
||||||
# Check nginx configuration
|
|
||||||
sudo nginx -t
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Connection Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check database container
|
|
||||||
docker exec ckb-mariadb mysql -u root -p -e "SHOW DATABASES;"
|
|
||||||
|
|
||||||
# Check Laravel database connection
|
|
||||||
docker exec ckb-laravel-app php artisan tinker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fix Laravel permissions
|
|
||||||
docker exec ckb-laravel-app chown -R www-data:www-data /var/www/html
|
|
||||||
docker exec ckb-laravel-app chmod -R 775 /var/www/html/storage
|
|
||||||
docker exec ckb-laravel-app chmod -R 775 /var/www/html/bootstrap/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Firewall**: Ensure only necessary ports are open
|
|
||||||
2. **SSL**: Certificate auto-renewal is configured
|
|
||||||
3. **Rate Limiting**: Configured for login and API endpoints
|
|
||||||
4. **Security Headers**: HSTS, XSS protection, etc.
|
|
||||||
5. **File Permissions**: Proper Laravel file permissions
|
|
||||||
6. **Database**: Strong passwords and limited access
|
|
||||||
|
|
||||||
## Backup Strategy
|
|
||||||
|
|
||||||
### Database Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create backup
|
|
||||||
docker exec ckb-mariadb mysqldump -u root -p ckb_production > backup.sql
|
|
||||||
|
|
||||||
# Restore backup
|
|
||||||
docker exec -i ckb-mariadb mysql -u root -p ckb_production < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup application files
|
|
||||||
tar -czf ckb-backup-$(date +%Y%m%d).tar.gz /var/www/ckb/
|
|
||||||
|
|
||||||
# Backup volumes
|
|
||||||
docker run --rm -v ckb_mysql_data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-backup.tar.gz -C /data .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Optimization
|
|
||||||
|
|
||||||
1. **Nginx**: Gzip compression enabled
|
|
||||||
2. **Laravel**: Production optimizations
|
|
||||||
3. **Database**: Proper indexing
|
|
||||||
4. **Redis**: Caching and session storage
|
|
||||||
5. **Static Files**: Long-term caching headers
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions:
|
|
||||||
|
|
||||||
1. Check logs first
|
|
||||||
2. Verify configuration files
|
|
||||||
3. Test connectivity step by step
|
|
||||||
4. Check system resources
|
|
||||||
5. Review security settings
|
|
||||||
404
DOCKER-README.md
404
DOCKER-README.md
@@ -1,404 +0,0 @@
|
|||||||
# Docker Setup untuk CKB Laravel Application
|
|
||||||
|
|
||||||
Dokumentasi ini menjelaskan cara menjalankan aplikasi CKB menggunakan Docker untuk environment local development dan staging/production.
|
|
||||||
|
|
||||||
## Struktur File Docker
|
|
||||||
|
|
||||||
```
|
|
||||||
├── Dockerfile # Production/Staging Docker image
|
|
||||||
├── Dockerfile.dev # Development Docker image
|
|
||||||
├── docker-compose.yml # Local development setup
|
|
||||||
├── docker-compose.prod.yml # Production/Staging setup
|
|
||||||
├── .dockerignore # Files to exclude from build
|
|
||||||
└── docker/
|
|
||||||
├── env.example # Environment variables template
|
|
||||||
├── nginx.conf # Production Nginx config
|
|
||||||
├── nginx.dev.conf # Development Nginx config
|
|
||||||
├── supervisord.conf # Production supervisor config
|
|
||||||
├── supervisord.dev.conf # Development supervisor config
|
|
||||||
├── xdebug.ini # Xdebug configuration
|
|
||||||
├── php.ini # PHP configuration
|
|
||||||
└── mysql.cnf # MySQL configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker Engine 20.10+
|
|
||||||
- Docker Compose 2.0+
|
|
||||||
- Git
|
|
||||||
|
|
||||||
## Setup untuk Local Development
|
|
||||||
|
|
||||||
### 1. Quick Setup (Recommended)
|
|
||||||
|
|
||||||
Untuk setup cepat dengan auto-import database:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone repository
|
|
||||||
git clone <your-repo-url>
|
|
||||||
cd CKB
|
|
||||||
|
|
||||||
# Pastikan file ckb.sql ada di root project
|
|
||||||
ls ckb.sql
|
|
||||||
|
|
||||||
# Jalankan quick setup
|
|
||||||
./docker-quick-setup.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Script ini akan otomatis:
|
|
||||||
|
|
||||||
- Setup environment file
|
|
||||||
- Start semua containers
|
|
||||||
- Import database dari ckb.sql
|
|
||||||
- Generate application key
|
|
||||||
- Setup Laravel application
|
|
||||||
|
|
||||||
### 2. Manual Setup
|
|
||||||
|
|
||||||
Jika Anda ingin setup manual:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup local environment
|
|
||||||
./docker-setup-env.sh local
|
|
||||||
|
|
||||||
# Start containers
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Import database
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
|
|
||||||
# Generate Laravel application key
|
|
||||||
docker-compose exec app php artisan key:generate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Menjalankan Development Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build dan jalankan containers
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Atau tanpa rebuild
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Akses Aplikasi
|
|
||||||
|
|
||||||
- **Web Application**: http://localhost:8000
|
|
||||||
- **Database (phpMyAdmin)**: http://localhost:8080
|
|
||||||
- **Mail Testing (MailHog)**: http://localhost:8025
|
|
||||||
- **MySQL Direct**: localhost:3306
|
|
||||||
- **Redis**: localhost:6379
|
|
||||||
|
|
||||||
### 4. Menjalankan Laravel Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Masuk ke container aplikasi
|
|
||||||
docker-compose exec app bash
|
|
||||||
|
|
||||||
# Atau jalankan command langsung
|
|
||||||
docker-compose exec app php artisan migrate
|
|
||||||
docker-compose exec app php artisan db:seed
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Development dengan Hot Reload
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
docker-compose exec app npm install
|
|
||||||
|
|
||||||
# Jalankan webpack dev server
|
|
||||||
docker-compose exec app npm run hot
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup untuk Staging/Production
|
|
||||||
|
|
||||||
### 1. Persiapan Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy dan edit environment file production
|
|
||||||
cp docker/env.example .env.production
|
|
||||||
|
|
||||||
# Edit file .env.production sesuai kebutuhan production
|
|
||||||
vim .env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
Contoh konfigurasi production:
|
|
||||||
|
|
||||||
```env
|
|
||||||
APP_ENV=production
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_URL=https://your-domain.com
|
|
||||||
|
|
||||||
DB_HOST=db
|
|
||||||
DB_DATABASE=ckb_production
|
|
||||||
DB_USERNAME=your_db_user
|
|
||||||
DB_PASSWORD=your_secure_password
|
|
||||||
DB_ROOT_PASSWORD=your_root_password
|
|
||||||
|
|
||||||
REDIS_PASSWORD=your_redis_password
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Menjalankan Production Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build dan jalankan dengan konfigurasi production
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
|
|
||||||
# Atau menggunakan environment file spesifik
|
|
||||||
docker-compose -f docker-compose.prod.yml --env-file .env.production up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Database Migration dan Seeding
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Jalankan migrations
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan migrate --force
|
|
||||||
|
|
||||||
# Jalankan seeders (jika diperlukan)
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan db:seed --force
|
|
||||||
|
|
||||||
# Optimize aplikasi untuk production
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan config:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan route:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan view:cache
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring dan Debugging
|
|
||||||
|
|
||||||
### 1. Melihat Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Semua services
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Service specific
|
|
||||||
docker-compose logs -f app
|
|
||||||
docker-compose logs -f db
|
|
||||||
docker-compose logs -f redis
|
|
||||||
|
|
||||||
# Production
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Debugging dengan Xdebug (Development)
|
|
||||||
|
|
||||||
Xdebug sudah dikonfigurasi untuk development environment:
|
|
||||||
|
|
||||||
- Port: 9003
|
|
||||||
- IDE Key: PHPSTORM
|
|
||||||
- Host: host.docker.internal
|
|
||||||
|
|
||||||
### 3. Monitoring Resources
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Lihat resource usage
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# Lihat containers yang berjalan
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Management
|
|
||||||
|
|
||||||
### 1. Import Database dari Backup
|
|
||||||
|
|
||||||
Untuk mengimport database dari file backup ckb.sql:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Import ke development environment
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
|
|
||||||
# Import ke production environment
|
|
||||||
./docker-import-db.sh prod
|
|
||||||
|
|
||||||
# Import file SQL khusus
|
|
||||||
./docker-import-db.sh dev my-backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Script import akan otomatis:
|
|
||||||
|
|
||||||
- Backup database yang sudah ada (safety)
|
|
||||||
- Drop dan recreate database
|
|
||||||
- Import data dari file SQL
|
|
||||||
- Jalankan migrations jika diperlukan
|
|
||||||
- Clear cache Laravel
|
|
||||||
|
|
||||||
### 2. Backup Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup database development
|
|
||||||
docker-compose exec db mysqldump -u root -proot ckb_db > backup_$(date +%Y%m%d).sql
|
|
||||||
|
|
||||||
# Backup database production
|
|
||||||
docker-compose -f docker-compose.prod.yml exec db mysqldump -u root -p ckb_production > backup_prod_$(date +%Y%m%d).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Manual Restore Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restore database development
|
|
||||||
docker-compose exec -T db mysql -u root -proot ckb_db < backup.sql
|
|
||||||
|
|
||||||
# Restore database production
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T db mysql -u root -p ckb_production < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Auto Import saat Pertama Kali Setup
|
|
||||||
|
|
||||||
File `ckb.sql` di root project akan otomatis diimport saat pertama kali menjalankan containers baru. Ini terjadi karena MySQL menggunakan `/docker-entrypoint-initdb.d/` untuk auto-import.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### 1. Docker Build Issues
|
|
||||||
|
|
||||||
Jika mengalami error saat build (seperti PHP extension compilation errors):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clean rebuild dengan script otomatis
|
|
||||||
./docker-rebuild.sh dev
|
|
||||||
|
|
||||||
# Atau manual cleanup dan rebuild
|
|
||||||
docker-compose down
|
|
||||||
docker system prune -a -f
|
|
||||||
docker-compose build --no-cache --pull
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Build Errors:**
|
|
||||||
|
|
||||||
- **curl extension error**: Fixed dengan menambah `libcurl4-openssl-dev` dan `pkg-config`
|
|
||||||
- **gd extension error**: Pastikan `libfreetype6-dev` dan `libjpeg62-turbo-dev` terinstall
|
|
||||||
- **Out of space**: Jalankan `docker system prune -a -f` untuk cleanup
|
|
||||||
|
|
||||||
### 2. Permission Issues
|
|
||||||
|
|
||||||
**Laravel Storage Permission Errors** (seperti "laravel.log could not be opened"):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Quick fix dengan script otomatis
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
|
|
||||||
# Atau manual fix
|
|
||||||
docker-compose exec app chown -R www-data:www-data /var/www/html/storage
|
|
||||||
docker-compose exec app chmod -R 775 /var/www/html/storage
|
|
||||||
docker-compose exec app mkdir -p /var/www/html/storage/logs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Host Permission Issues:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fix permission di host system
|
|
||||||
sudo chown -R $(id -u):$(id -g) storage/
|
|
||||||
sudo chown -R $(id -u):$(id -g) bootstrap/cache/
|
|
||||||
chmod -R 775 storage/
|
|
||||||
chmod -R 775 bootstrap/cache/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Reset Containers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop dan remove containers
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Remove volumes (HATI-HATI: akan menghapus data database)
|
|
||||||
docker-compose down -v
|
|
||||||
|
|
||||||
# Rebuild dari awal
|
|
||||||
docker-compose up -d --build --force-recreate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Cache Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clear semua cache Laravel
|
|
||||||
docker-compose exec app php artisan optimize:clear
|
|
||||||
|
|
||||||
# Clear Docker build cache
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# Clean rebuild everything
|
|
||||||
./docker-rebuild.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Database Import Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Jika auto-import gagal
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
|
|
||||||
# Jika database corrupt
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Redis Connection Issues
|
|
||||||
|
|
||||||
Jika mengalami error "Class Redis not found":
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test Redis functionality
|
|
||||||
./docker-test-redis.sh dev
|
|
||||||
|
|
||||||
# Rebuild containers dengan Redis extension
|
|
||||||
./docker-rebuild.sh dev
|
|
||||||
|
|
||||||
# Manual fix: Clear cache dan config
|
|
||||||
docker-compose exec app php artisan config:clear
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Redis Errors:**
|
|
||||||
|
|
||||||
- **Class Redis not found**: Fixed dengan install `pecl install redis`
|
|
||||||
- **Connection refused**: Pastikan Redis container berjalan
|
|
||||||
- **Config not loaded**: Jalankan `php artisan config:clear`
|
|
||||||
|
|
||||||
## Security Notes untuk Production
|
|
||||||
|
|
||||||
1. **Environment Variables**: Jangan commit file `.env` ke repository
|
|
||||||
2. **Database Passwords**: Gunakan password yang kuat
|
|
||||||
3. **SSL/TLS**: Setup SSL certificate untuk HTTPS
|
|
||||||
4. **Firewall**: Konfigurasi firewall untuk membatasi akses port
|
|
||||||
5. **Updates**: Regular update Docker images dan dependencies
|
|
||||||
|
|
||||||
## Performance Optimization
|
|
||||||
|
|
||||||
### 1. Production Optimizations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Laravel optimizations
|
|
||||||
docker-compose exec app php artisan config:cache
|
|
||||||
docker-compose exec app php artisan route:cache
|
|
||||||
docker-compose exec app php artisan view:cache
|
|
||||||
docker-compose exec app composer install --optimize-autoloader --no-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Docker Optimizations
|
|
||||||
|
|
||||||
- Gunakan multi-stage builds untuk image yang lebih kecil
|
|
||||||
- Leverage Docker layer caching
|
|
||||||
- Optimize .dockerignore untuk build speed
|
|
||||||
|
|
||||||
## Backup Strategy
|
|
||||||
|
|
||||||
### 1. Automated Backups
|
|
||||||
|
|
||||||
Buat script backup otomatis:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# backup.sh
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
docker-compose exec db mysqldump -u root -p${DB_ROOT_PASSWORD} ckb_production > "backup_${DATE}.sql"
|
|
||||||
tar -czf "backup_${DATE}.tar.gz" backup_${DATE}.sql storage/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Volume Backups
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup Docker volumes
|
|
||||||
docker run --rm -v ckb_mysql_data:/data -v $(pwd):/backup alpine tar czf /backup/mysql_backup.tar.gz /data
|
|
||||||
```
|
|
||||||
|
|
||||||
Untuk pertanyaan lebih lanjut atau issues, silakan buat issue di repository ini.
|
|
||||||
47
Dockerfile
47
Dockerfile
@@ -9,7 +9,6 @@ RUN apt-get update && apt-get install -y \
|
|||||||
curl \
|
curl \
|
||||||
libcurl4-openssl-dev \
|
libcurl4-openssl-dev \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
libpng-dev \
|
|
||||||
libonig-dev \
|
libonig-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
@@ -47,39 +46,39 @@ RUN pecl install redis \
|
|||||||
# Install Composer
|
# Install Composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
# Copy existing application directory contents
|
# Copy only composer files first for caching
|
||||||
COPY . /var/www/html
|
COPY composer.json composer.lock ./
|
||||||
|
|
||||||
# Copy existing application directory permissions
|
# Install PHP dependencies (cached if lock file unchanged)
|
||||||
COPY --chown=www-data:www-data . /var/www/html
|
RUN composer install --optimize-autoloader --no-dev --no-interaction --no-scripts
|
||||||
|
|
||||||
# Install PHP dependencies
|
# Now copy the full Laravel application code
|
||||||
RUN composer install --optimize-autoloader --no-dev --no-interaction
|
COPY . .
|
||||||
|
|
||||||
# Install Node.js dependencies and build assets
|
# Run composer scripts and install Node dependencies
|
||||||
RUN npm ci \
|
RUN composer run-script post-autoload-dump && \
|
||||||
&& npm run production \
|
npm install && \
|
||||||
&& rm -rf node_modules
|
npm run production && \
|
||||||
|
php artisan storage:link
|
||||||
|
|
||||||
# Create necessary directories and set permissions
|
# Set proper permissions (for production only do this once)
|
||||||
RUN mkdir -p /var/www/html/storage/logs \
|
RUN mkdir -p storage/logs \
|
||||||
&& mkdir -p /var/www/html/storage/framework/cache \
|
&& mkdir -p storage/framework/{cache,sessions,views} \
|
||||||
&& mkdir -p /var/www/html/storage/framework/sessions \
|
&& mkdir -p storage/app/public \
|
||||||
&& mkdir -p /var/www/html/storage/framework/views \
|
&& mkdir -p bootstrap/cache \
|
||||||
&& mkdir -p /var/www/html/storage/app \
|
|
||||||
&& mkdir -p /var/www/html/bootstrap/cache \
|
|
||||||
&& chown -R www-data:www-data /var/www/html \
|
&& chown -R www-data:www-data /var/www/html \
|
||||||
&& chmod -R 775 /var/www/html/storage \
|
&& chmod -R 775 storage \
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache \
|
&& chmod -R 775 bootstrap/cache \
|
||||||
&& chmod -R 755 /var/www/html/public
|
&& chmod -R 755 public \
|
||||||
|
&& chmod -R 777 storage/app/public
|
||||||
|
|
||||||
# Create nginx config
|
# Nginx config
|
||||||
COPY ./docker/nginx.conf /etc/nginx/sites-available/default
|
COPY ./docker/nginx.conf /etc/nginx/sites-available/default
|
||||||
|
|
||||||
# Create supervisor config
|
# Supervisor config
|
||||||
COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
# Expose port 9000 and start php-fpm server
|
# Expose web port
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
@@ -17,13 +17,10 @@ RUN apt-get update && apt-get install -y \
|
|||||||
unzip \
|
unzip \
|
||||||
libfreetype6-dev \
|
libfreetype6-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
libpng-dev \
|
|
||||||
libxpm-dev \
|
libxpm-dev \
|
||||||
libvpx-dev \
|
libvpx-dev \
|
||||||
supervisor \
|
supervisor \
|
||||||
nginx \
|
nginx \
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
vim \
|
vim \
|
||||||
nano \
|
nano \
|
||||||
htop \
|
htop \
|
||||||
@@ -43,47 +40,37 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-xpm \
|
|||||||
dom \
|
dom \
|
||||||
xml
|
xml
|
||||||
|
|
||||||
# Install Redis and Xdebug for development
|
# Install Redis and Xdebug
|
||||||
RUN pecl install redis xdebug \
|
RUN pecl install redis xdebug \
|
||||||
&& docker-php-ext-enable redis xdebug
|
&& docker-php-ext-enable redis xdebug
|
||||||
|
|
||||||
# Install Composer
|
# Install Composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
# Copy existing application directory contents
|
# Copy dependency files first for better caching
|
||||||
COPY . /var/www/html
|
COPY composer.json composer.lock ./
|
||||||
|
|
||||||
# Copy existing application directory permissions
|
# Now copy the entire application code (after composer install)
|
||||||
COPY --chown=www-data:www-data . /var/www/html
|
COPY . .
|
||||||
|
|
||||||
# Install PHP dependencies with dev packages
|
# Install PHP dependencies (with dev)
|
||||||
RUN composer install --optimize-autoloader --no-interaction
|
RUN composer install --no-interaction
|
||||||
|
|
||||||
# Install Node.js dependencies
|
# Set ownership and permissions
|
||||||
RUN npm install
|
RUN mkdir -p storage/logs \
|
||||||
|
&& mkdir -p storage/framework/{cache,sessions,views} \
|
||||||
# Create necessary directories and set permissions
|
&& mkdir -p storage/app \
|
||||||
RUN mkdir -p /var/www/html/storage/logs \
|
&& mkdir -p bootstrap/cache \
|
||||||
&& mkdir -p /var/www/html/storage/framework/cache \
|
|
||||||
&& mkdir -p /var/www/html/storage/framework/sessions \
|
|
||||||
&& mkdir -p /var/www/html/storage/framework/views \
|
|
||||||
&& mkdir -p /var/www/html/storage/app \
|
|
||||||
&& mkdir -p /var/www/html/bootstrap/cache \
|
|
||||||
&& chown -R www-data:www-data /var/www/html \
|
&& chown -R www-data:www-data /var/www/html \
|
||||||
&& chmod -R 775 /var/www/html/storage \
|
&& chmod -R 775 storage bootstrap/cache \
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache \
|
&& chmod -R 755 public
|
||||||
&& chmod -R 755 /var/www/html/public
|
|
||||||
|
|
||||||
# Create nginx config for development
|
# Copy configs
|
||||||
COPY ./docker/nginx.dev.conf /etc/nginx/sites-available/default
|
COPY ./docker/nginx.dev.conf /etc/nginx/sites-available/default
|
||||||
|
|
||||||
# Create supervisor config for development
|
|
||||||
COPY ./docker/supervisord.dev.conf /etc/supervisor/conf.d/supervisord.conf
|
COPY ./docker/supervisord.dev.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
# Create Xdebug config
|
|
||||||
COPY ./docker/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
COPY ./docker/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
|
|
||||||
# Expose ports
|
# Expose web port
|
||||||
EXPOSE 80 3000
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
# Environment Setup Guide
|
|
||||||
|
|
||||||
Panduan lengkap untuk setup environment file CKB Laravel Application dengan file template terpisah untuk local dan production.
|
|
||||||
|
|
||||||
## 📂 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
docker/
|
|
||||||
├── env.example.local # Template untuk local development
|
|
||||||
├── env.example.production # Template untuk production
|
|
||||||
└── (env.example) # File lama, dapat dihapus
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Quick Setup
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup environment untuk local development
|
|
||||||
./docker-setup-env.sh local
|
|
||||||
|
|
||||||
# Atau manual copy
|
|
||||||
cp docker/env.example.local .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup environment untuk production
|
|
||||||
./docker-setup-env.sh production
|
|
||||||
|
|
||||||
# IMPORTANT: Edit .env dan ganti semua CHANGE_THIS_* values!
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Template Comparison
|
|
||||||
|
|
||||||
### 🏠 Local Development (`env.example.local`)
|
|
||||||
|
|
||||||
| Setting | Value | Description |
|
|
||||||
| ------------------- | ----------------------- | ----------------------- |
|
|
||||||
| `APP_ENV` | `local` | Development environment |
|
|
||||||
| `APP_DEBUG` | `true` | Debug mode enabled |
|
|
||||||
| `APP_URL` | `http://localhost:8000` | Local URL |
|
|
||||||
| `LOG_LEVEL` | `debug` | Verbose logging |
|
|
||||||
| `DB_DATABASE` | `ckb_db` | Development database |
|
|
||||||
| `DB_USERNAME` | `root` | Simple credentials |
|
|
||||||
| `DB_PASSWORD` | `root` | Simple credentials |
|
|
||||||
| `REDIS_PASSWORD` | `null` | No password needed |
|
|
||||||
| `MAIL_HOST` | `mailhog` | Local mail testing |
|
|
||||||
| `QUEUE_CONNECTION` | `sync` | Synchronous queue |
|
|
||||||
| `TELESCOPE_ENABLED` | `true` | Debugging tool enabled |
|
|
||||||
|
|
||||||
### 🚀 Production (`env.example.production`)
|
|
||||||
|
|
||||||
| Setting | Value | Description |
|
|
||||||
| ------------------- | ---------------------------------- | ----------------------- |
|
|
||||||
| `APP_ENV` | `production` | Production environment |
|
|
||||||
| `APP_DEBUG` | `false` | Debug mode disabled |
|
|
||||||
| `APP_URL` | `https://bengkel.digitaloasis.xyz` | Production domain |
|
|
||||||
| `LOG_LEVEL` | `error` | Error-only logging |
|
|
||||||
| `DB_DATABASE` | `ckb_production` | Production database |
|
|
||||||
| `DB_USERNAME` | `ckb_user` | Secure username |
|
|
||||||
| `DB_PASSWORD` | `CHANGE_THIS_*` | **Must be changed!** |
|
|
||||||
| `REDIS_PASSWORD` | `CHANGE_THIS_*` | **Must be changed!** |
|
|
||||||
| `MAIL_HOST` | `smtp.gmail.com` | Real SMTP server |
|
|
||||||
| `QUEUE_CONNECTION` | `redis` | Redis-based queue |
|
|
||||||
| `TELESCOPE_ENABLED` | `false` | Debugging tool disabled |
|
|
||||||
|
|
||||||
## 🔐 Security Configuration for Production
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
**MUST CHANGE** these values in production `.env`:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Strong database passwords
|
|
||||||
DB_PASSWORD=your_super_secure_password_here
|
|
||||||
DB_ROOT_PASSWORD=your_root_password_here
|
|
||||||
|
|
||||||
# Redis security
|
|
||||||
REDIS_PASSWORD=your_redis_password_here
|
|
||||||
|
|
||||||
# Mail configuration
|
|
||||||
MAIL_USERNAME=your-email@domain.com
|
|
||||||
MAIL_PASSWORD=your-app-specific-password
|
|
||||||
```
|
|
||||||
|
|
||||||
### Optional but Recommended
|
|
||||||
|
|
||||||
```env
|
|
||||||
# AWS S3 for file storage
|
|
||||||
AWS_ACCESS_KEY_ID=your-aws-key
|
|
||||||
AWS_SECRET_ACCESS_KEY=your-aws-secret
|
|
||||||
|
|
||||||
# Real-time features
|
|
||||||
PUSHER_APP_ID=your-pusher-app-id
|
|
||||||
PUSHER_APP_KEY=your-pusher-key
|
|
||||||
PUSHER_APP_SECRET=your-pusher-secret
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Environment Helper Script
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup local environment
|
|
||||||
./docker-setup-env.sh local
|
|
||||||
|
|
||||||
# Setup production environment
|
|
||||||
./docker-setup-env.sh production
|
|
||||||
|
|
||||||
# Show current environment info
|
|
||||||
./docker-setup-env.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- ✅ **Auto-backup** existing `.env` before changes
|
|
||||||
- ✅ **Environment validation** checks required variables
|
|
||||||
- ✅ **Security warnings** for production misconfiguration
|
|
||||||
- ✅ **Configuration summary** shows current settings
|
|
||||||
- ✅ **Next steps guidance** for deployment
|
|
||||||
|
|
||||||
## 📊 Environment Comparison
|
|
||||||
|
|
||||||
### Local Development Features
|
|
||||||
|
|
||||||
- 🐛 **Debug Mode**: Full error reporting and debugging tools
|
|
||||||
- 📧 **MailHog**: Local email testing server
|
|
||||||
- 🗄️ **Simple DB**: Basic MySQL credentials
|
|
||||||
- 🔓 **No SSL**: HTTP-only for speed
|
|
||||||
- 🧪 **Development Tools**: Telescope, Debugbar enabled
|
|
||||||
- ⚡ **Sync Queue**: Immediate processing for testing
|
|
||||||
|
|
||||||
### Production Features
|
|
||||||
|
|
||||||
- 🔒 **Security First**: Strong passwords and encryption
|
|
||||||
- 📧 **Real SMTP**: Professional email delivery
|
|
||||||
- 🗄️ **Secure DB**: Production-grade credentials
|
|
||||||
- 🔐 **SSL/HTTPS**: Let's Encrypt certificates
|
|
||||||
- 📊 **Monitoring**: Error-only logging
|
|
||||||
- 🚀 **Redis Queue**: Background job processing
|
|
||||||
|
|
||||||
## 🚨 Common Issues & Solutions
|
|
||||||
|
|
||||||
### 1. "CHANGE*THIS*\*" Values in Production
|
|
||||||
|
|
||||||
**Problem**: Forgot to change template values
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check for remaining template values
|
|
||||||
grep "CHANGE_THIS" .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use the helper script to check
|
|
||||||
./docker-setup-env.sh
|
|
||||||
# It will warn about CHANGE_THIS_* values
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Wrong Environment File
|
|
||||||
|
|
||||||
**Problem**: Using local config in production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check current environment
|
|
||||||
grep "APP_ENV=" .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Recreate with correct template
|
|
||||||
./docker-setup-env.sh production
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Missing Environment Variables
|
|
||||||
|
|
||||||
**Problem**: Laravel errors about missing config
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate current .env
|
|
||||||
./docker-setup-env.sh validate
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Check required variables list and add missing ones
|
|
||||||
|
|
||||||
## 📝 Environment Variables Reference
|
|
||||||
|
|
||||||
### Core Application
|
|
||||||
|
|
||||||
```env
|
|
||||||
APP_NAME="CKB Bengkel System"
|
|
||||||
APP_ENV=production|local
|
|
||||||
APP_KEY=base64:...
|
|
||||||
APP_DEBUG=true|false
|
|
||||||
APP_URL=https://domain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database
|
|
||||||
|
|
||||||
```env
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=db
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=ckb_production|ckb_db
|
|
||||||
DB_USERNAME=username
|
|
||||||
DB_PASSWORD=password
|
|
||||||
DB_ROOT_PASSWORD=root_password
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cache & Session
|
|
||||||
|
|
||||||
```env
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PASSWORD=password|null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
CACHE_DRIVER=redis
|
|
||||||
SESSION_DRIVER=redis
|
|
||||||
QUEUE_CONNECTION=redis|sync
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mail Configuration
|
|
||||||
|
|
||||||
```env
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_HOST=smtp.domain.com
|
|
||||||
MAIL_PORT=587
|
|
||||||
MAIL_USERNAME=email@domain.com
|
|
||||||
MAIL_PASSWORD=password
|
|
||||||
MAIL_ENCRYPTION=tls
|
|
||||||
MAIL_FROM_ADDRESS=noreply@domain.com
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
```env
|
|
||||||
TRUSTED_PROXIES=*
|
|
||||||
SESSION_SECURE_COOKIE=true
|
|
||||||
SESSION_SAME_SITE=strict
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Migration Guide
|
|
||||||
|
|
||||||
### From Old Single Template
|
|
||||||
|
|
||||||
If you're migrating from the old `docker/env.example`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup current .env
|
|
||||||
cp .env .env.backup
|
|
||||||
|
|
||||||
# Choose appropriate template
|
|
||||||
./docker-setup-env.sh local # for development
|
|
||||||
./docker-setup-env.sh production # for production
|
|
||||||
|
|
||||||
# Compare and migrate custom settings
|
|
||||||
diff .env.backup .env
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
For environment setup issues:
|
|
||||||
|
|
||||||
- **Documentation**: This file
|
|
||||||
- **Helper Script**: `./docker-setup-env.sh`
|
|
||||||
- **Validation**: Built-in security checks
|
|
||||||
- **Backup**: Automatic .env backup before changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**💡 Pro Tip**: Always use the helper script `./docker-setup-env.sh` instead of manual copying to ensure proper configuration and security checks!
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
# 🔧 Laravel Permission Fix Guide untuk Docker
|
|
||||||
|
|
||||||
## 🎯 Masalah yang Diselesaikan
|
|
||||||
|
|
||||||
**Error yang umum terjadi:**
|
|
||||||
```
|
|
||||||
The stream or file "/var/www/html/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
**Root Cause:**
|
|
||||||
- Laravel tidak bisa menulis ke direktori `storage/logs/`
|
|
||||||
- Permission dan ownership direktori storage tidak sesuai
|
|
||||||
- Direktori storage yang diperlukan belum dibuat
|
|
||||||
|
|
||||||
## 🚀 Solusi Quick Fix
|
|
||||||
|
|
||||||
### **Option 1: Automatic Fix (Recommended)**
|
|
||||||
```bash
|
|
||||||
# Fix semua permission issues otomatis
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
|
|
||||||
# Untuk production
|
|
||||||
./docker-fix-permissions.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Option 2: Manual Fix**
|
|
||||||
```bash
|
|
||||||
# Buat direktori yang diperlukan
|
|
||||||
docker-compose exec app mkdir -p /var/www/html/storage/logs
|
|
||||||
docker-compose exec app mkdir -p /var/www/html/storage/framework/cache
|
|
||||||
docker-compose exec app mkdir -p /var/www/html/storage/framework/sessions
|
|
||||||
docker-compose exec app mkdir -p /var/www/html/storage/framework/views
|
|
||||||
|
|
||||||
# Fix ownership
|
|
||||||
docker-compose exec app chown -R www-data:www-data /var/www/html/storage
|
|
||||||
docker-compose exec app chown -R www-data:www-data /var/www/html/bootstrap/cache
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
docker-compose exec app chmod -R 775 /var/www/html/storage
|
|
||||||
docker-compose exec app chmod -R 775 /var/www/html/bootstrap/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Option 3: Rebuild Containers (Jika masalah persisten)**
|
|
||||||
```bash
|
|
||||||
# Clean rebuild containers
|
|
||||||
./docker-rebuild.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Verifikasi Fix Berhasil
|
|
||||||
|
|
||||||
### **1. Cek Permission Direktori**
|
|
||||||
```bash
|
|
||||||
# Lihat permission storage
|
|
||||||
docker-compose exec app ls -la /var/www/html/storage/
|
|
||||||
|
|
||||||
# Cek ownership logs
|
|
||||||
docker-compose exec app ls -la /var/www/html/storage/logs/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output yang benar:**
|
|
||||||
```
|
|
||||||
drwxrwxr-x 5 www-data www-data 4096 Jun 10 15:01 storage
|
|
||||||
drwxrwxr-x 2 www-data www-data 4096 Jun 10 15:01 logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Test Laravel Logging**
|
|
||||||
```bash
|
|
||||||
# Test write ke log
|
|
||||||
docker-compose exec app php -r "file_put_contents('/var/www/html/storage/logs/laravel.log', 'Test log: ' . date('Y-m-d H:i:s') . PHP_EOL, FILE_APPEND);"
|
|
||||||
|
|
||||||
# Cek isi log
|
|
||||||
docker-compose exec app tail -5 /var/www/html/storage/logs/laravel.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Test Laravel Artisan**
|
|
||||||
```bash
|
|
||||||
# Test cache clear
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
|
|
||||||
# Test storage link
|
|
||||||
docker-compose exec app php artisan storage:link
|
|
||||||
|
|
||||||
# Test route cache
|
|
||||||
docker-compose exec app php artisan route:cache
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛡️ Prevention - Dockerfile Updates
|
|
||||||
|
|
||||||
**Dockerfile sudah diperbarui untuk mencegah masalah ini:**
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Create necessary directories and set permissions
|
|
||||||
RUN mkdir -p /var/www/html/storage/logs \
|
|
||||||
&& mkdir -p /var/www/html/storage/framework/cache \
|
|
||||||
&& mkdir -p /var/www/html/storage/framework/sessions \
|
|
||||||
&& mkdir -p /var/www/html/storage/framework/views \
|
|
||||||
&& mkdir -p /var/www/html/storage/app \
|
|
||||||
&& mkdir -p /var/www/html/bootstrap/cache \
|
|
||||||
&& chown -R www-data:www-data /var/www/html \
|
|
||||||
&& chmod -R 775 /var/www/html/storage \
|
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Script Features
|
|
||||||
|
|
||||||
### **`docker-fix-permissions.sh`**
|
|
||||||
- ✅ **Auto-detect environment** (dev/prod)
|
|
||||||
- ✅ **Create missing directories**
|
|
||||||
- ✅ **Fix ownership** (www-data:www-data)
|
|
||||||
- ✅ **Set proper permissions** (775 untuk storage)
|
|
||||||
- ✅ **Test logging functionality**
|
|
||||||
- ✅ **Create storage link**
|
|
||||||
- ✅ **Show before/after permissions**
|
|
||||||
|
|
||||||
### **Usage Examples**
|
|
||||||
```bash
|
|
||||||
# Fix development environment
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
|
|
||||||
# Fix production environment
|
|
||||||
./docker-fix-permissions.sh prod
|
|
||||||
|
|
||||||
# Show help
|
|
||||||
./docker-fix-permissions.sh --help
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 Common Issues & Solutions
|
|
||||||
|
|
||||||
### **1. Permission Denied setelah Fix**
|
|
||||||
**Cause:** Volume mounting conflict
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Cek volume mounts
|
|
||||||
docker-compose config
|
|
||||||
|
|
||||||
# Restart containers
|
|
||||||
docker-compose restart app
|
|
||||||
|
|
||||||
# Re-run permission fix
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Ownership reverted setelah restart**
|
|
||||||
**Cause:** Volume mounting dari host
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Fix di host system juga
|
|
||||||
sudo chown -R $(id -u):$(id -g) storage/
|
|
||||||
chmod -R 775 storage/
|
|
||||||
|
|
||||||
# Atau gunakan named volumes di docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Log file tetap tidak bisa ditulis**
|
|
||||||
**Cause:** Log file sudah ada dengan permission salah
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Hapus log file lama
|
|
||||||
docker-compose exec app rm -f /var/www/html/storage/logs/laravel.log
|
|
||||||
|
|
||||||
# Re-run permission fix
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### **4. Selinux/AppArmor blocking**
|
|
||||||
**Cause:** Security policies
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Disable selinux temporarily (CentOS/RHEL)
|
|
||||||
sudo setenforce 0
|
|
||||||
|
|
||||||
# Check AppArmor status (Ubuntu)
|
|
||||||
sudo aa-status
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Directory Structure yang Benar
|
|
||||||
|
|
||||||
Setelah fix, struktur direktori storage harus seperti ini:
|
|
||||||
|
|
||||||
```
|
|
||||||
storage/
|
|
||||||
├── app/
|
|
||||||
│ ├── public/
|
|
||||||
│ └── .gitkeep
|
|
||||||
├── framework/
|
|
||||||
│ ├── cache/
|
|
||||||
│ ├── sessions/
|
|
||||||
│ ├── testing/
|
|
||||||
│ └── views/
|
|
||||||
└── logs/
|
|
||||||
├── laravel.log
|
|
||||||
└── .gitkeep
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Best Practices
|
|
||||||
|
|
||||||
1. **Always use scripts**: Gunakan `docker-fix-permissions.sh` untuk consistency
|
|
||||||
2. **Regular checks**: Monitor permission setelah update containers
|
|
||||||
3. **Volume strategy**: Gunakan named volumes untuk persistent storage
|
|
||||||
4. **Backup first**: Backup data sebelum fix permission
|
|
||||||
5. **Test thoroughly**: Verify semua Laravel functionality setelah fix
|
|
||||||
|
|
||||||
## 📞 Troubleshooting Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debug permission issues
|
|
||||||
docker-compose exec app ls -laR /var/www/html/storage/
|
|
||||||
|
|
||||||
# Check Laravel configuration
|
|
||||||
docker-compose exec app php artisan config:show logging
|
|
||||||
|
|
||||||
# Monitor Laravel logs
|
|
||||||
docker-compose exec app tail -f /var/www/html/storage/logs/laravel.log
|
|
||||||
|
|
||||||
# Test file writing
|
|
||||||
docker-compose exec app touch /var/www/html/storage/test.txt
|
|
||||||
|
|
||||||
# Check container user
|
|
||||||
docker-compose exec app whoami
|
|
||||||
docker-compose exec app id
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**✅ Dengan mengikuti panduan ini, masalah Laravel permission di Docker container akan teratasi.**
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
# CKB Production Deployment Guide
|
|
||||||
|
|
||||||
Panduan deployment aplikasi CKB Laravel ke production server dengan domain `bengkel.digitaloasis.xyz`.
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### 1. Deploy ke Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Full deployment (recommended untuk pertama kali)
|
|
||||||
./docker-deploy-prod.sh deploy
|
|
||||||
|
|
||||||
# Hanya build containers
|
|
||||||
./docker-deploy-prod.sh build
|
|
||||||
|
|
||||||
# Setup SSL certificate
|
|
||||||
./docker-deploy-prod.sh ssl
|
|
||||||
|
|
||||||
# Check deployment status
|
|
||||||
./docker-deploy-prod.sh status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Akses Aplikasi
|
|
||||||
|
|
||||||
- **Domain**: https://bengkel.digitaloasis.xyz
|
|
||||||
- **Health Check**: https://bengkel.digitaloasis.xyz/health
|
|
||||||
|
|
||||||
## 📋 Prerequisites
|
|
||||||
|
|
||||||
### Server Requirements
|
|
||||||
|
|
||||||
- **OS**: Ubuntu 20.04+ atau CentOS 7+
|
|
||||||
- **Memory**: Minimum 2GB RAM (4GB recommended)
|
|
||||||
- **Storage**: Minimum 20GB SSD
|
|
||||||
- **Docker**: Version 20.10+
|
|
||||||
- **Docker Compose**: Version 2.0+
|
|
||||||
|
|
||||||
### Domain Setup
|
|
||||||
|
|
||||||
1. **DNS Configuration**:
|
|
||||||
|
|
||||||
```
|
|
||||||
A Record: bengkel.digitaloasis.xyz → [Server IP]
|
|
||||||
CNAME: www.bengkel.digitaloasis.xyz → bengkel.digitaloasis.xyz
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Firewall Configuration**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Allow HTTP/HTTPS traffic
|
|
||||||
sudo ufw allow 80/tcp
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
|
|
||||||
# Allow SSH (if needed)
|
|
||||||
sudo ufw allow 22/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛡️ Security Configuration
|
|
||||||
|
|
||||||
### 1. Environment Variables
|
|
||||||
|
|
||||||
Edit `.env` file untuk production:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Application
|
|
||||||
APP_ENV=production
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_URL=https://bengkel.digitaloasis.xyz
|
|
||||||
APP_KEY=base64:...
|
|
||||||
|
|
||||||
# Database (GANTI dengan credentials yang aman!)
|
|
||||||
DB_HOST=db
|
|
||||||
DB_DATABASE=ckb_production
|
|
||||||
DB_USERNAME=ckb_user
|
|
||||||
DB_PASSWORD=secure_password_here
|
|
||||||
DB_ROOT_PASSWORD=secure_root_password_here
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PASSWORD=secure_redis_password
|
|
||||||
|
|
||||||
# Mail
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_HOST=your-smtp-host
|
|
||||||
MAIL_PORT=587
|
|
||||||
MAIL_USERNAME=your-email@domain.com
|
|
||||||
MAIL_PASSWORD=your-email-password
|
|
||||||
MAIL_ENCRYPTION=tls
|
|
||||||
|
|
||||||
# Session & Cache
|
|
||||||
SESSION_DRIVER=redis
|
|
||||||
CACHE_DRIVER=redis
|
|
||||||
QUEUE_CONNECTION=redis
|
|
||||||
|
|
||||||
# Trusted Proxies
|
|
||||||
TRUSTED_PROXIES=*
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Database Security
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setelah deployment, jalankan MySQL secure installation
|
|
||||||
docker-compose -f docker-compose.prod.yml exec db mysql_secure_installation
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Deployment Process
|
|
||||||
|
|
||||||
### Manual Step-by-Step
|
|
||||||
|
|
||||||
1. **Persiapan Server**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update system
|
|
||||||
sudo apt update && sudo apt upgrade -y
|
|
||||||
|
|
||||||
# Install Docker
|
|
||||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
||||||
sudo sh get-docker.sh
|
|
||||||
|
|
||||||
# Install Docker Compose
|
|
||||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Clone Repository**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/your-repo/ckb.git
|
|
||||||
cd ckb
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Setup Environment**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For production environment
|
|
||||||
./docker-setup-env.sh production
|
|
||||||
|
|
||||||
# Edit production settings (IMPORTANT!)
|
|
||||||
nano .env
|
|
||||||
# Change all CHANGE_THIS_* values with secure passwords
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Deploy Application**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./docker-deploy-prod.sh deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Setup SSL Certificate**:
|
|
||||||
```bash
|
|
||||||
./docker-deploy-prod.sh ssl
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Monitoring & Maintenance
|
|
||||||
|
|
||||||
### 1. Health Checks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check application status
|
|
||||||
./docker-deploy-prod.sh status
|
|
||||||
|
|
||||||
# Check specific service logs
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f app
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f nginx-proxy
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f db
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Database Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Manual backup
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T db mysqldump -u root -p"$DB_ROOT_PASSWORD" ckb_production > backup_$(date +%Y%m%d).sql
|
|
||||||
|
|
||||||
# Automated backup (add to crontab)
|
|
||||||
0 2 * * * /path/to/ckb/docker-backup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. SSL Certificate Renewal
|
|
||||||
|
|
||||||
Certificate akan otomatis renewal. Untuk manual renewal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test renewal
|
|
||||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew --dry-run
|
|
||||||
|
|
||||||
# Manual renewal
|
|
||||||
./docker-ssl-renew.sh
|
|
||||||
|
|
||||||
# Setup auto-renewal (add to crontab)
|
|
||||||
0 12 * * * /path/to/ckb/docker-ssl-renew.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Application Not Loading**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check container status
|
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
|
||||||
|
|
||||||
# Check application logs
|
|
||||||
docker-compose -f docker-compose.prod.yml logs app
|
|
||||||
|
|
||||||
# Restart application
|
|
||||||
docker-compose -f docker-compose.prod.yml restart app
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **SSL Certificate Issues**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check certificate status
|
|
||||||
openssl s_client -connect bengkel.digitaloasis.xyz:443 -servername bengkel.digitaloasis.xyz
|
|
||||||
|
|
||||||
# Re-setup SSL
|
|
||||||
./docker-ssl-setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Database Connection Issues**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check database logs
|
|
||||||
docker-compose -f docker-compose.prod.yml logs db
|
|
||||||
|
|
||||||
# Test database connection
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan tinker
|
|
||||||
>>> DB::connection()->getPdo();
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Permission Issues**:
|
|
||||||
```bash
|
|
||||||
# Fix Laravel permissions
|
|
||||||
./docker-fix-permissions.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check resource usage
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# Clean up Docker system
|
|
||||||
docker system prune -a -f
|
|
||||||
|
|
||||||
# Optimize Laravel
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan optimize
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚦 Load Testing
|
|
||||||
|
|
||||||
Before going live, test your application:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install testing tools
|
|
||||||
sudo apt install apache2-utils
|
|
||||||
|
|
||||||
# Basic load test
|
|
||||||
ab -n 1000 -c 10 https://bengkel.digitaloasis.xyz/
|
|
||||||
|
|
||||||
# More comprehensive testing with siege
|
|
||||||
sudo apt install siege
|
|
||||||
siege -c 25 -t 60s https://bengkel.digitaloasis.xyz/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Performance Optimization
|
|
||||||
|
|
||||||
### 1. Laravel Optimizations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run after each deployment
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan config:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan route:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan view:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app composer install --optimize-autoloader --no-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Database Optimization
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# MySQL tuning
|
|
||||||
docker-compose -f docker-compose.prod.yml exec db mysql -u root -p -e "
|
|
||||||
SET GLOBAL innodb_buffer_pool_size = 1073741824;
|
|
||||||
SET GLOBAL query_cache_size = 67108864;
|
|
||||||
SET GLOBAL query_cache_type = 1;
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Nginx Optimization
|
|
||||||
|
|
||||||
Edit `docker/nginx-proxy.conf` untuk mengoptimalkan:
|
|
||||||
|
|
||||||
- Gzip compression
|
|
||||||
- Browser caching
|
|
||||||
- Connection pooling
|
|
||||||
|
|
||||||
## 🔄 Updates & Maintenance
|
|
||||||
|
|
||||||
### Application Updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest code
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Backup before update
|
|
||||||
./docker-deploy-prod.sh backup
|
|
||||||
|
|
||||||
# Deploy updates
|
|
||||||
./docker-deploy-prod.sh deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update base images
|
|
||||||
docker-compose -f docker-compose.prod.yml pull
|
|
||||||
|
|
||||||
# Rebuild with latest security patches
|
|
||||||
./docker-deploy-prod.sh build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support & Contact
|
|
||||||
|
|
||||||
Untuk bantuan deployment atau issues:
|
|
||||||
|
|
||||||
- **Email**: admin@digitaloasis.xyz
|
|
||||||
- **Documentation**: https://github.com/your-repo/ckb/docs
|
|
||||||
- **Issues**: https://github.com/your-repo/ckb/issues
|
|
||||||
|
|
||||||
## 📄 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ckb/
|
|
||||||
├── docker/
|
|
||||||
│ ├── nginx-proxy.conf # Main nginx configuration
|
|
||||||
│ ├── nginx-temp.conf # Temporary config for SSL setup
|
|
||||||
│ ├── env.example # Environment template
|
|
||||||
│ └── ...
|
|
||||||
├── docker-compose.prod.yml # Production compose file
|
|
||||||
├── docker-deploy-prod.sh # Main deployment script
|
|
||||||
├── docker-ssl-setup.sh # SSL certificate setup
|
|
||||||
├── docker-ssl-renew.sh # SSL renewal script
|
|
||||||
└── PRODUCTION-DEPLOYMENT.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Production Checklist
|
|
||||||
|
|
||||||
- [ ] Domain DNS configured
|
|
||||||
- [ ] Firewall rules configured
|
|
||||||
- [ ] .env file configured with production values
|
|
||||||
- [ ] Database credentials changed from defaults
|
|
||||||
- [ ] SSL certificate obtained and configured
|
|
||||||
- [ ] Backup system configured
|
|
||||||
- [ ] Monitoring setup
|
|
||||||
- [ ] Load testing completed
|
|
||||||
- [ ] Security audit completed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🚨 Remember**: Always test in staging environment before deploying to production!
|
|
||||||
261
README.md
261
README.md
@@ -1,64 +1,237 @@
|
|||||||
<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"></a></p>
|
# CKB - Bengkel Management System
|
||||||
|
|
||||||
<p align="center">
|
Sistem manajemen bengkel yang dibangun dengan Laravel 8 dan menggunakan JavaScript inline untuk performa optimal.
|
||||||
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.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
|
## 🚀 Overview
|
||||||
|
|
||||||
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:
|
Aplikasi ini menggunakan pendekatan JavaScript inline untuk menghindari kebutuhan build process di production server. Semua vendor assets sudah disalin ke folder `public` dan siap untuk deployment.
|
||||||
|
|
||||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
## 📦 Prerequisites
|
||||||
- [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.
|
- PHP 8.1+
|
||||||
|
- Composer
|
||||||
|
- MySQL/MariaDB
|
||||||
|
- Redis (optional)
|
||||||
|
- Docker (optional)
|
||||||
|
|
||||||
## Learning Laravel
|
## 🛠️ Installation
|
||||||
|
|
||||||
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.
|
### Local Development
|
||||||
|
|
||||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 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.
|
1. **Clone repository**
|
||||||
|
|
||||||
## Laravel Sponsors
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd ckb
|
||||||
|
```
|
||||||
|
|
||||||
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 [Patreon page](https://patreon.com/taylorotwell).
|
2. **Install PHP dependencies**
|
||||||
|
|
||||||
### Premium Partners
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
- **[Vehikl](https://vehikl.com/)**
|
3. **Copy environment file**
|
||||||
- **[Tighten Co.](https://tighten.co)**
|
|
||||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
|
||||||
- **[64 Robots](https://64robots.com)**
|
|
||||||
- **[Cubet Techno Labs](https://cubettech.com)**
|
|
||||||
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
|
||||||
- **[Many](https://www.many.co.uk)**
|
|
||||||
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
|
|
||||||
- **[DevSquad](https://devsquad.com)**
|
|
||||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
|
||||||
- **[OP.GG](https://op.gg)**
|
|
||||||
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
|
|
||||||
- **[Lendio](https://lendio.com)**
|
|
||||||
|
|
||||||
## Contributing
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
4. **Configure database**
|
||||||
|
|
||||||
## Code of Conduct
|
```bash
|
||||||
|
# Edit .env file with your database credentials
|
||||||
|
php artisan migrate
|
||||||
|
php artisan db:seed
|
||||||
|
```
|
||||||
|
|
||||||
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).
|
5. **Start development server**
|
||||||
|
```bash
|
||||||
|
php artisan serve
|
||||||
|
```
|
||||||
|
|
||||||
## Security Vulnerabilities
|
### Docker Development
|
||||||
|
|
||||||
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.
|
```bash
|
||||||
|
# Build development image
|
||||||
|
docker build -f Dockerfile.dev -t ckb-dev .
|
||||||
|
|
||||||
## License
|
# Run container
|
||||||
|
docker run -p 8080:80 ckb-dev
|
||||||
|
```
|
||||||
|
|
||||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
### Docker Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build production image
|
||||||
|
docker build -f Dockerfile -t ckb-prod .
|
||||||
|
|
||||||
|
# Run container
|
||||||
|
docker run -p 8080:80 ckb-prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker Optimization
|
||||||
|
|
||||||
|
### ⚡ Optimizations Made
|
||||||
|
|
||||||
|
1. **Removed Node.js Dependencies**
|
||||||
|
|
||||||
|
- ❌ `nodejs` dan `npm` tidak lagi diinstall di container
|
||||||
|
- ✅ Mengurangi ukuran image sekitar 200-300MB
|
||||||
|
- ✅ Build time lebih cepat
|
||||||
|
|
||||||
|
2. **No JavaScript Compilation**
|
||||||
|
|
||||||
|
- ❌ Tidak ada `npm install` atau `npm run production`
|
||||||
|
- ✅ Vendor assets sudah ada di `public/js/vendor/` dan `public/css/vendor/`
|
||||||
|
- ✅ Library diakses langsung dari file yang sudah di-minify
|
||||||
|
|
||||||
|
3. **Optimized .dockerignore**
|
||||||
|
|
||||||
|
- ❌ Exclude `node_modules/`, `package.json`, `webpack.mix.js`
|
||||||
|
- ✅ Keep vendor assets di `public/` folder
|
||||||
|
- ✅ Mengurangi build context size
|
||||||
|
|
||||||
|
4. **Better Layer Caching**
|
||||||
|
- ✅ Copy `composer.json` terlebih dahulu
|
||||||
|
- ✅ Install PHP dependencies sebelum copy source code
|
||||||
|
- ✅ Cache layer untuk composer dependencies
|
||||||
|
|
||||||
|
### 📊 Performance Improvements
|
||||||
|
|
||||||
|
| Metric | Before | After | Improvement |
|
||||||
|
| ------------ | ------------- | -------- | ----------- |
|
||||||
|
| Image Size | ~800MB | ~500MB | -37.5% |
|
||||||
|
| Build Time | ~5-8 min | ~2-3 min | -60% |
|
||||||
|
| Dependencies | Node.js + PHP | PHP only | -50% |
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ckb/
|
||||||
|
├── app/
|
||||||
|
│ ├── Http/
|
||||||
|
│ │ ├── Controllers/
|
||||||
|
│ │ └── Requests/
|
||||||
|
│ ├── Models/
|
||||||
|
│ └── Services/
|
||||||
|
├── resources/
|
||||||
|
│ ├── views/
|
||||||
|
│ │ ├── layouts/
|
||||||
|
│ │ │ ├── frontapp.blade.php
|
||||||
|
│ │ │ └── backapp.blade.php
|
||||||
|
│ │ └── transaction/
|
||||||
|
│ └── sass/
|
||||||
|
├── public/
|
||||||
|
│ ├── js/
|
||||||
|
│ │ ├── vendor/
|
||||||
|
│ │ │ ├── jquery.dataTables.min.js
|
||||||
|
│ │ │ ├── dataTables.bootstrap4.min.js
|
||||||
|
│ │ │ ├── sweetalert2.min.js
|
||||||
|
│ │ │ ├── chart.umd.js
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── bootstrap-datepicker.min.js
|
||||||
|
│ └── css/
|
||||||
|
│ ├── vendor/
|
||||||
|
│ │ ├── dataTables.bootstrap4.min.css
|
||||||
|
│ │ ├── sweetalert2.min.css
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── bootstrap-datepicker.min.css
|
||||||
|
└── docker/
|
||||||
|
├── Dockerfile
|
||||||
|
├── Dockerfile.dev
|
||||||
|
└── nginx.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Key Features
|
||||||
|
|
||||||
|
### Frontend (Mobile App)
|
||||||
|
|
||||||
|
- **Camera Integration** - Foto precheck dan postcheck dengan kontrol penuh
|
||||||
|
- **File Upload** - Support hingga 20MB
|
||||||
|
- **Responsive Design** - Optimized untuk mobile devices
|
||||||
|
- **Real-time Updates** - WebSocket integration
|
||||||
|
|
||||||
|
### Backend (Admin Panel)
|
||||||
|
|
||||||
|
- **Transaction Management** - Manajemen transaksi bengkel
|
||||||
|
- **KPI Tracking** - Sistem KPI dengan perhitungan otomatis
|
||||||
|
- **DataTables** - Tabel data dengan fitur advanced
|
||||||
|
- **Chart.js** - Visualisasi data dan laporan
|
||||||
|
- **SweetAlert2** - Notifikasi yang user-friendly
|
||||||
|
|
||||||
|
### Warehouse Management
|
||||||
|
|
||||||
|
- **Stock Audit** - Audit stok dengan filter advanced
|
||||||
|
- **Mutations** - Mutasi antar dealer
|
||||||
|
- **Opnames** - Penghitungan stok
|
||||||
|
- **Product Management** - Manajemen produk dan kategori
|
||||||
|
|
||||||
|
## 🔧 Technology Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- **Laravel 8** - PHP Framework
|
||||||
|
- **MySQL/MariaDB** - Database
|
||||||
|
- **Redis** - Cache & Session
|
||||||
|
- **PHP 8.1** - Runtime
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Bootstrap 4** - CSS Framework
|
||||||
|
- **jQuery** - JavaScript Library
|
||||||
|
- **DataTables** - Table Enhancement
|
||||||
|
- **Chart.js** - Chart Library
|
||||||
|
- **SweetAlert2** - Alert Library
|
||||||
|
- **Bootstrap Datepicker** - Date Picker
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
|
||||||
|
- **Docker** - Containerization
|
||||||
|
- **Nginx** - Web Server
|
||||||
|
- **Supervisor** - Process Management
|
||||||
|
|
||||||
|
## 🚨 Important Notes
|
||||||
|
|
||||||
|
1. **Vendor assets sudah ada di folder `public/`** dan akan di-push ke git
|
||||||
|
2. **Tidak perlu npm install di production server**
|
||||||
|
3. **Semua JavaScript sudah inline** di Blade templates
|
||||||
|
4. **CSS masih perlu dikompilasi** jika ada perubahan di `resources/sass/`
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Docker Build Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Gunakan Docker BuildKit untuk build lebih cepat
|
||||||
|
export DOCKER_BUILDKIT=1
|
||||||
|
docker build -f Dockerfile -t ckb-prod .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendor Assets Missing
|
||||||
|
|
||||||
|
Pastikan folder `public/js/vendor/` dan `public/css/vendor/` sudah ada dan berisi file-file yang diperlukan.
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear cache
|
||||||
|
php artisan cache:clear
|
||||||
|
php artisan config:clear
|
||||||
|
|
||||||
|
# Recreate database
|
||||||
|
php artisan migrate:fresh --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
# 🔴 Redis Fix Guide untuk Laravel Docker
|
|
||||||
|
|
||||||
## 🎯 Masalah yang Diselesaikan
|
|
||||||
|
|
||||||
**Error yang dialami:**
|
|
||||||
```
|
|
||||||
Class "Redis" not found
|
|
||||||
```
|
|
||||||
|
|
||||||
**Root Cause:**
|
|
||||||
- PHP Redis extension tidak terinstall di container
|
|
||||||
- Laravel dikonfigurasi untuk menggunakan Redis tetapi extension tidak tersedia
|
|
||||||
- Container perlu rebuild untuk install Redis extension
|
|
||||||
|
|
||||||
## 🚀 Solusi yang Diimplementasi
|
|
||||||
|
|
||||||
### **1. Updated Dockerfiles**
|
|
||||||
|
|
||||||
**Production (Dockerfile):**
|
|
||||||
```dockerfile
|
|
||||||
# Install Redis extension
|
|
||||||
RUN pecl install redis \
|
|
||||||
&& docker-php-ext-enable redis
|
|
||||||
```
|
|
||||||
|
|
||||||
**Development (Dockerfile.dev):**
|
|
||||||
```dockerfile
|
|
||||||
# Install Redis and Xdebug for development
|
|
||||||
RUN pecl install redis xdebug \
|
|
||||||
&& docker-php-ext-enable redis xdebug
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Fix Steps yang Dijalankan**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Update Dockerfile dengan Redis extension
|
|
||||||
# 2. Rebuild container
|
|
||||||
docker-compose build --no-cache app
|
|
||||||
|
|
||||||
# 3. Restart container dengan image baru
|
|
||||||
docker-compose up -d app
|
|
||||||
|
|
||||||
# 4. Verify Redis extension installed
|
|
||||||
docker-compose exec app php -m | grep redis
|
|
||||||
|
|
||||||
# 5. Test Redis connection
|
|
||||||
docker-compose exec app php -r "
|
|
||||||
\$redis = new Redis();
|
|
||||||
\$redis->connect('redis', 6379);
|
|
||||||
echo 'Redis connected successfully';
|
|
||||||
"
|
|
||||||
|
|
||||||
# 6. Clear Laravel cache
|
|
||||||
docker-compose exec app php artisan config:clear
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Verifikasi Fix Berhasil
|
|
||||||
|
|
||||||
### **1. PHP Redis Extension**
|
|
||||||
```bash
|
|
||||||
# Cek extension terinstall
|
|
||||||
docker-compose exec app php -m | grep redis
|
|
||||||
# Output: redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Redis Connection Test**
|
|
||||||
```bash
|
|
||||||
# Test koneksi Redis
|
|
||||||
./docker-test-redis.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Output:**
|
|
||||||
```
|
|
||||||
[SUCCESS] PHP Redis extension is installed
|
|
||||||
[SUCCESS] Redis server is responding
|
|
||||||
[SUCCESS] PHP Redis connection working
|
|
||||||
[SUCCESS] Laravel cache operations working
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Web Application**
|
|
||||||
```bash
|
|
||||||
# Test web response
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/
|
|
||||||
# Output: 302 (redirect ke login page)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **4. Laravel Cache Operations**
|
|
||||||
```bash
|
|
||||||
# Test Laravel cache dengan Redis
|
|
||||||
docker-compose exec app php artisan tinker --execute="
|
|
||||||
Cache::put('test', 'redis-working', 60);
|
|
||||||
echo Cache::get('test');
|
|
||||||
"
|
|
||||||
# Output: redis-working
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Tools dan Scripts
|
|
||||||
|
|
||||||
### **`docker-test-redis.sh`**
|
|
||||||
Comprehensive Redis testing script:
|
|
||||||
- ✅ Test PHP Redis extension
|
|
||||||
- ✅ Test Redis server connection
|
|
||||||
- ✅ Test Laravel cache operations
|
|
||||||
- ✅ Show Redis configuration
|
|
||||||
- ✅ Show server information
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
# Test development environment
|
|
||||||
./docker-test-redis.sh dev
|
|
||||||
|
|
||||||
# Test production environment
|
|
||||||
./docker-test-redis.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### **`docker-rebuild.sh`**
|
|
||||||
Updated untuk include Redis testing:
|
|
||||||
- ✅ Test Redis extension di build process
|
|
||||||
- ✅ Verify Redis connection setelah rebuild
|
|
||||||
- ✅ Comprehensive testing semua extensions
|
|
||||||
|
|
||||||
## 🔧 Laravel Configuration
|
|
||||||
|
|
||||||
### **Environment Variables (.env)**
|
|
||||||
```env
|
|
||||||
# Redis Configuration
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# Cache using Redis
|
|
||||||
CACHE_DRIVER=redis
|
|
||||||
|
|
||||||
# Sessions using Redis
|
|
||||||
SESSION_DRIVER=redis
|
|
||||||
|
|
||||||
# Queue using Redis
|
|
||||||
QUEUE_CONNECTION=redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Config Files**
|
|
||||||
Laravel otomatis membaca konfigurasi dari environment variables untuk:
|
|
||||||
- `config/cache.php` - Cache driver
|
|
||||||
- `config/session.php` - Session driver
|
|
||||||
- `config/queue.php` - Queue driver
|
|
||||||
- `config/database.php` - Redis connection
|
|
||||||
|
|
||||||
## 🚨 Common Issues & Solutions
|
|
||||||
|
|
||||||
### **1. Redis Extension Missing**
|
|
||||||
**Symptoms:** `Class "Redis" not found`
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Rebuild containers
|
|
||||||
./docker-rebuild.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Redis Connection Failed**
|
|
||||||
**Symptoms:** `Connection refused`
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check Redis container
|
|
||||||
docker-compose ps | grep redis
|
|
||||||
|
|
||||||
# Restart Redis
|
|
||||||
docker-compose restart redis
|
|
||||||
|
|
||||||
# Test connection
|
|
||||||
./docker-test-redis.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Laravel Config Not Loading**
|
|
||||||
**Symptoms:** Cache/session tidak menggunakan Redis
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Clear Laravel cache
|
|
||||||
docker-compose exec app php artisan config:clear
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
docker-compose exec app php artisan view:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
### **4. Permission Issues with Redis**
|
|
||||||
**Symptoms:** Cannot write to cache
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Fix permissions
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
|
|
||||||
# Clear cache
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Best Practices
|
|
||||||
|
|
||||||
### **1. Container Management**
|
|
||||||
- Always rebuild containers setelah update Dockerfile
|
|
||||||
- Use scripts untuk consistent operations
|
|
||||||
- Test functionality setelah changes
|
|
||||||
|
|
||||||
### **2. Development Workflow**
|
|
||||||
```bash
|
|
||||||
# Complete setup dengan Redis
|
|
||||||
./docker-quick-setup.sh dev
|
|
||||||
|
|
||||||
# Test semua functionality
|
|
||||||
./docker-test-redis.sh dev
|
|
||||||
|
|
||||||
# Fix jika ada issues
|
|
||||||
./docker-fix-permissions.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Production Deployment**
|
|
||||||
```bash
|
|
||||||
# Build production containers
|
|
||||||
./docker-rebuild.sh prod
|
|
||||||
|
|
||||||
# Verify Redis working
|
|
||||||
./docker-test-redis.sh prod
|
|
||||||
|
|
||||||
# Import database
|
|
||||||
./docker-import-db.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Monitoring & Debugging
|
|
||||||
|
|
||||||
### **Redis Monitoring**
|
|
||||||
```bash
|
|
||||||
# Redis logs
|
|
||||||
docker-compose logs redis
|
|
||||||
|
|
||||||
# Redis CLI access
|
|
||||||
docker-compose exec redis redis-cli
|
|
||||||
|
|
||||||
# Redis info
|
|
||||||
docker-compose exec redis redis-cli info
|
|
||||||
|
|
||||||
# Monitor Redis commands
|
|
||||||
docker-compose exec redis redis-cli monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Laravel Debugging**
|
|
||||||
```bash
|
|
||||||
# Check Laravel logs
|
|
||||||
docker-compose exec app tail -f storage/logs/laravel.log
|
|
||||||
|
|
||||||
# Check cache status
|
|
||||||
docker-compose exec app php artisan cache:table
|
|
||||||
|
|
||||||
# Test cache manually
|
|
||||||
docker-compose exec app php artisan tinker
|
|
||||||
# Cache::put('test', 'value', 60);
|
|
||||||
# Cache::get('test');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Performance Tips
|
|
||||||
|
|
||||||
### **1. Redis Optimization**
|
|
||||||
- Use appropriate data types
|
|
||||||
- Set proper expiration times
|
|
||||||
- Monitor memory usage
|
|
||||||
|
|
||||||
### **2. Laravel Cache Strategy**
|
|
||||||
```bash
|
|
||||||
# Cache configuration
|
|
||||||
php artisan config:cache
|
|
||||||
|
|
||||||
# Cache routes
|
|
||||||
php artisan route:cache
|
|
||||||
|
|
||||||
# Cache views
|
|
||||||
php artisan view:cache
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**✅ Dengan implementasi fix ini, masalah "Class Redis not found" sudah teratasi dan aplikasi Laravel berjalan normal dengan Redis.**
|
|
||||||
316
app/Exports/StockProductsExport.php
Normal file
316
app/Exports/StockProductsExport.php
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnWidths;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class StockProductsExport implements FromCollection, WithHeadings, WithStyles, WithColumnWidths
|
||||||
|
{
|
||||||
|
protected $reportData;
|
||||||
|
|
||||||
|
public function __construct($reportData)
|
||||||
|
{
|
||||||
|
// Validate and sanitize report data
|
||||||
|
if (!is_array($reportData)) {
|
||||||
|
throw new \InvalidArgumentException('Report data must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($reportData['data']) || !isset($reportData['dealers'])) {
|
||||||
|
throw new \InvalidArgumentException('Report data must contain "data" and "dealers" keys');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure dealers is a collection
|
||||||
|
if (!($reportData['dealers'] instanceof Collection)) {
|
||||||
|
$reportData['dealers'] = collect($reportData['dealers']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure data is an array
|
||||||
|
if (!is_array($reportData['data'])) {
|
||||||
|
$reportData['data'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reportData = $reportData;
|
||||||
|
|
||||||
|
// Debug: Log the structure of report data
|
||||||
|
Log::info('StockProductsExport constructor', [
|
||||||
|
'has_data' => isset($reportData['data']),
|
||||||
|
'has_dealers' => isset($reportData['dealers']),
|
||||||
|
'data_count' => isset($reportData['data']) ? count($reportData['data']) : 0,
|
||||||
|
'dealers_count' => isset($reportData['dealers']) ? count($reportData['dealers']) : 0,
|
||||||
|
'dealers' => isset($reportData['dealers']) ? $reportData['dealers']->pluck('name')->toArray() : []
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = collect();
|
||||||
|
$no = 1;
|
||||||
|
|
||||||
|
foreach ($this->reportData['data'] as $row) {
|
||||||
|
$exportRow = [
|
||||||
|
'no' => $no++,
|
||||||
|
'kode_produk' => $row['product_code'] ?? '',
|
||||||
|
'nama_produk' => $row['product_name'] ?? '',
|
||||||
|
'kategori' => $row['category_name'] ?? '',
|
||||||
|
'satuan' => $row['unit'] ?? ''
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add dealer columns
|
||||||
|
foreach ($this->reportData['dealers'] as $dealer) {
|
||||||
|
try {
|
||||||
|
$dealerKey = "dealer_{$dealer->id}";
|
||||||
|
// Clean dealer name for array key to avoid special characters
|
||||||
|
$cleanDealerName = $this->cleanDealerName($dealer->name);
|
||||||
|
$exportRow[$cleanDealerName] = $row[$dealerKey] ?? 0;
|
||||||
|
|
||||||
|
Log::info('Processing dealer column', [
|
||||||
|
'original_name' => $dealer->name,
|
||||||
|
'clean_name' => $cleanDealerName,
|
||||||
|
'dealer_key' => $dealerKey,
|
||||||
|
'value' => $row[$dealerKey] ?? 0
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error processing dealer column', [
|
||||||
|
'dealer_id' => $dealer->id,
|
||||||
|
'dealer_name' => $dealer->name,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
// Use a safe fallback name
|
||||||
|
$exportRow['Dealer_' . $dealer->id] = $row["dealer_{$dealer->id}"] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total stock
|
||||||
|
$exportRow['total_stok'] = $row['total_stock'] ?? 0;
|
||||||
|
|
||||||
|
$data->push($exportRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in collection method', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return empty collection as fallback
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$headings = [
|
||||||
|
'No',
|
||||||
|
'Kode Produk',
|
||||||
|
'Nama Produk',
|
||||||
|
'Kategori',
|
||||||
|
'Satuan'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add dealer headings
|
||||||
|
foreach ($this->reportData['dealers'] as $dealer) {
|
||||||
|
try {
|
||||||
|
$cleanName = $this->cleanDealerName($dealer->name);
|
||||||
|
$headings[] = $cleanName;
|
||||||
|
|
||||||
|
Log::info('Processing dealer heading', [
|
||||||
|
'original_name' => $dealer->name,
|
||||||
|
'clean_name' => $cleanName
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error processing dealer heading', [
|
||||||
|
'dealer_id' => $dealer->id,
|
||||||
|
'dealer_name' => $dealer->name,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
// Use a safe fallback name
|
||||||
|
$headings[] = 'Dealer_' . $dealer->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total heading
|
||||||
|
$headings[] = 'Total Stok';
|
||||||
|
|
||||||
|
return $headings;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in headings method', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return basic headings as fallback
|
||||||
|
return ['No', 'Kode Produk', 'Nama Produk', 'Kategori', 'Satuan', 'Total Stok'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$lastColumn = $sheet->getHighestColumn();
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
|
||||||
|
// Validate column and row values
|
||||||
|
if (!$lastColumn || !$lastRow || $lastRow < 1) {
|
||||||
|
Log::warning('Invalid sheet dimensions', ['lastColumn' => $lastColumn, 'lastRow' => $lastRow]);
|
||||||
|
return $sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style header row
|
||||||
|
$sheet->getStyle('A1:' . $lastColumn . '1')->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'color' => ['rgb' => 'FFFFFF'],
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => '4472C4'],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Style all cells
|
||||||
|
$sheet->getStyle('A1:' . $lastColumn . $lastRow)->applyFromArray([
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Center align specific columns
|
||||||
|
$sheet->getStyle('A:A')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('D:D')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
// Right align numeric columns (dealer columns and total)
|
||||||
|
$dealerStartCol = 'E';
|
||||||
|
$dealerCount = count($this->reportData['dealers']);
|
||||||
|
|
||||||
|
if ($dealerCount > 0) {
|
||||||
|
$dealerEndCol = chr(ord('E') + $dealerCount - 1);
|
||||||
|
$totalCol = chr(ord($dealerStartCol) + $dealerCount);
|
||||||
|
|
||||||
|
// Validate column letters
|
||||||
|
if (ord($dealerEndCol) <= ord('Z') && ord($totalCol) <= ord('Z')) {
|
||||||
|
$sheet->getStyle($dealerStartCol . ':' . $dealerEndCol)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||||
|
$sheet->getStyle($totalCol . ':' . $totalCol)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||||
|
|
||||||
|
// Bold total column
|
||||||
|
$sheet->getStyle($totalCol . '1:' . $totalCol . $lastRow)->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-size columns safely
|
||||||
|
foreach (range('A', $lastColumn) as $column) {
|
||||||
|
try {
|
||||||
|
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning('Failed to auto-size column', ['column' => $column, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheet;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in styles method', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $sheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columnWidths(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$widths = [
|
||||||
|
'A' => 8, // No
|
||||||
|
'B' => 15, // Kode Produk
|
||||||
|
'C' => 30, // Nama Produk
|
||||||
|
'D' => 15, // Kategori
|
||||||
|
'E' => 10, // Satuan
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add dealer column widths safely
|
||||||
|
$currentCol = 'F';
|
||||||
|
$dealerCount = count($this->reportData['dealers']);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $dealerCount; $i++) {
|
||||||
|
// Validate column letter
|
||||||
|
if (ord($currentCol) <= ord('Z')) {
|
||||||
|
$widths[$currentCol] = 15;
|
||||||
|
$currentCol = chr(ord($currentCol) + 1);
|
||||||
|
} else {
|
||||||
|
Log::warning('Too many dealer columns, stopping at column Z');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total column width if we haven't exceeded Z
|
||||||
|
if (ord($currentCol) <= ord('Z')) {
|
||||||
|
$widths[$currentCol] = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $widths;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in columnWidths method', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return basic widths as fallback
|
||||||
|
return [
|
||||||
|
'A' => 8, // No
|
||||||
|
'B' => 15, // Kode Produk
|
||||||
|
'C' => 30, // Nama Produk
|
||||||
|
'D' => 15, // Kategori
|
||||||
|
'E' => 10, // Satuan
|
||||||
|
'F' => 15 // Total Stok
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean dealer name to make it safe for array keys and Excel headers
|
||||||
|
*/
|
||||||
|
private function cleanDealerName($dealerName)
|
||||||
|
{
|
||||||
|
// Remove or replace special characters that can cause issues with Excel
|
||||||
|
$cleanName = preg_replace('/[^a-zA-Z0-9\s\-_]/', '', $dealerName);
|
||||||
|
$cleanName = trim($cleanName);
|
||||||
|
|
||||||
|
// If name becomes empty, use a default
|
||||||
|
if (empty($cleanName)) {
|
||||||
|
$cleanName = 'Dealer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit length to avoid Excel issues
|
||||||
|
if (strlen($cleanName) > 31) {
|
||||||
|
$cleanName = substr($cleanName, 0, 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cleanName;
|
||||||
|
}
|
||||||
|
}
|
||||||
435
app/Exports/TechnicianReportExport.php
Normal file
435
app/Exports/TechnicianReportExport.php
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Services\TechnicianReportService;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithColumnWidths;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||||
|
use Maatwebsite\Excel\Events\AfterSheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class TechnicianReportExport implements FromCollection, WithHeadings, WithStyles, WithColumnWidths, WithEvents
|
||||||
|
{
|
||||||
|
protected $dealerId;
|
||||||
|
protected $startDate;
|
||||||
|
protected $endDate;
|
||||||
|
protected $technicianReportService;
|
||||||
|
protected $mechanics;
|
||||||
|
protected $headings;
|
||||||
|
protected $filterInfo;
|
||||||
|
|
||||||
|
public function __construct($dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
$this->dealerId = $dealerId;
|
||||||
|
$this->startDate = $startDate;
|
||||||
|
$this->endDate = $endDate;
|
||||||
|
$this->technicianReportService = new TechnicianReportService();
|
||||||
|
|
||||||
|
// Get mechanics and prepare headings
|
||||||
|
$this->prepareHeadings();
|
||||||
|
$this->prepareFilterInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareHeadings()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||||
|
$this->dealerId,
|
||||||
|
$this->startDate,
|
||||||
|
$this->endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mechanics = $reportData['mechanics'];
|
||||||
|
|
||||||
|
// Build headings - simplified structure
|
||||||
|
$this->headings = [
|
||||||
|
'No',
|
||||||
|
'Nama Pekerjaan',
|
||||||
|
'Kode Pekerjaan',
|
||||||
|
'Kategori'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add mechanic columns (only total, no completed/pending)
|
||||||
|
foreach ($this->mechanics as $mechanic) {
|
||||||
|
$mechanicName = $this->cleanName($mechanic->name);
|
||||||
|
$this->headings[] = $mechanicName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total column at the end
|
||||||
|
$this->headings[] = 'Total';
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error preparing headings: ' . $e->getMessage());
|
||||||
|
$this->headings = ['Error preparing data'];
|
||||||
|
$this->mechanics = collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareFilterInfo()
|
||||||
|
{
|
||||||
|
$this->filterInfo = [];
|
||||||
|
|
||||||
|
// Dealer filter
|
||||||
|
if ($this->dealerId) {
|
||||||
|
$dealer = \App\Models\Dealer::find($this->dealerId);
|
||||||
|
$dealerName = $dealer ? $dealer->name : 'Unknown Dealer';
|
||||||
|
$this->filterInfo[] = "Dealer: {$dealerName}";
|
||||||
|
} else {
|
||||||
|
// Check user access for "Semua Dealer"
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user && $user->role_id) {
|
||||||
|
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||||
|
if ($role) {
|
||||||
|
$technicianReportService = new \App\Services\TechnicianReportService();
|
||||||
|
if ($technicianReportService->isAdminRole($role)) {
|
||||||
|
$this->filterInfo[] = "Dealer: Semua Dealer (Admin)";
|
||||||
|
} else if ($role->dealers->count() > 0) {
|
||||||
|
$dealerNames = $role->dealers->pluck('name')->implode(', ');
|
||||||
|
$this->filterInfo[] = "Dealer: Semua Dealer (Pivot: {$dealerNames})";
|
||||||
|
} else {
|
||||||
|
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filter
|
||||||
|
if ($this->startDate && $this->endDate) {
|
||||||
|
$startDateFormatted = Carbon::parse($this->startDate)->format('d/m/Y');
|
||||||
|
$endDateFormatted = Carbon::parse($this->endDate)->format('d/m/Y');
|
||||||
|
$this->filterInfo[] = "Periode: {$startDateFormatted} - {$endDateFormatted}";
|
||||||
|
} elseif ($this->startDate) {
|
||||||
|
$startDateFormatted = Carbon::parse($this->startDate)->format('d/m/Y');
|
||||||
|
$this->filterInfo[] = "Tanggal Mulai: {$startDateFormatted}";
|
||||||
|
} elseif ($this->endDate) {
|
||||||
|
$endDateFormatted = Carbon::parse($this->endDate)->format('d/m/Y');
|
||||||
|
$this->filterInfo[] = "Tanggal Akhir: {$endDateFormatted}";
|
||||||
|
} else {
|
||||||
|
$this->filterInfo[] = "Periode: Semua Periode";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export date
|
||||||
|
$exportDate = Carbon::now()->format('d/m/Y H:i:s');
|
||||||
|
$this->filterInfo[] = "Tanggal Export: {$exportDate}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean name for Excel compatibility
|
||||||
|
*/
|
||||||
|
private function cleanName($name)
|
||||||
|
{
|
||||||
|
// Remove special characters and limit length
|
||||||
|
$cleaned = preg_replace('/[^a-zA-Z0-9\s]/', '', $name);
|
||||||
|
$cleaned = trim($cleaned);
|
||||||
|
|
||||||
|
// Limit to 31 characters (Excel sheet name limit)
|
||||||
|
if (strlen($cleaned) > 31) {
|
||||||
|
$cleaned = substr($cleaned, 0, 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cleaned ?: 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||||
|
$this->dealerId,
|
||||||
|
$this->startDate,
|
||||||
|
$this->endDate
|
||||||
|
);
|
||||||
|
$data = [];
|
||||||
|
$no = 1;
|
||||||
|
$columnTotals = [];
|
||||||
|
foreach ($this->mechanics as $mechanic) {
|
||||||
|
$columnTotals["mechanic_{$mechanic->id}_total"] = 0;
|
||||||
|
}
|
||||||
|
$columnTotals['row_total'] = 0;
|
||||||
|
foreach ($reportData['data'] as $row) {
|
||||||
|
$rowTotal = 0;
|
||||||
|
$exportRow = [
|
||||||
|
$no++,
|
||||||
|
$row['work_name'],
|
||||||
|
$row['work_code'],
|
||||||
|
$row['category_name']
|
||||||
|
];
|
||||||
|
foreach ($this->mechanics as $mechanic) {
|
||||||
|
$mechanicTotal = $row["mechanic_{$mechanic->id}_total"] ?? 0;
|
||||||
|
$exportRow[] = $mechanicTotal;
|
||||||
|
$rowTotal += $mechanicTotal;
|
||||||
|
$columnTotals["mechanic_{$mechanic->id}_total"] += $mechanicTotal;
|
||||||
|
}
|
||||||
|
$exportRow[] = $rowTotal;
|
||||||
|
$columnTotals['row_total'] += $rowTotal;
|
||||||
|
$data[] = $exportRow;
|
||||||
|
}
|
||||||
|
// Add total row
|
||||||
|
$totalRow = ['', 'TOTAL', '', ''];
|
||||||
|
foreach ($this->mechanics as $mechanic) {
|
||||||
|
$totalRow[] = $columnTotals["mechanic_{$mechanic->id}_total"];
|
||||||
|
}
|
||||||
|
$totalRow[] = $columnTotals['row_total'];
|
||||||
|
$data[] = $totalRow;
|
||||||
|
return collect($data);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in collection: ' . $e->getMessage());
|
||||||
|
return collect([['Error loading data']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return $this->headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function styles(Worksheet $sheet)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$lastColumn = $sheet->getHighestColumn();
|
||||||
|
$lastRow = $sheet->getHighestRow();
|
||||||
|
|
||||||
|
// Calculate positions
|
||||||
|
$titleRow = 1;
|
||||||
|
$headerRow = 1; // Headers are now in row 2
|
||||||
|
$dataStartRow = 2; // Data starts in row 3
|
||||||
|
|
||||||
|
// Calculate total row position (after data)
|
||||||
|
$dataRows = count($this->technicianReportService->getTechnicianReportData($this->dealerId, $this->startDate, $this->endDate)['data']);
|
||||||
|
$totalRow = $dataStartRow + $dataRows;
|
||||||
|
$filterStartRow = $totalRow + 2; // After total row + empty row
|
||||||
|
|
||||||
|
// Style the title row (row 1)
|
||||||
|
$sheet->getStyle('A' . $titleRow . ':' . $lastColumn . $titleRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'size' => 16,
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Header styling (row 2)
|
||||||
|
$sheet->getStyle('A' . $headerRow . ':' . $lastColumn . $headerRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'color' => ['rgb' => 'FFFFFF'],
|
||||||
|
'size' => 10,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => '2E5BBA'],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Data styling (starting from row 3)
|
||||||
|
if ($lastRow > $headerRow) {
|
||||||
|
$dataEndRow = $totalRow;
|
||||||
|
$sheet->getStyle('A' . $dataStartRow . ':' . $lastColumn . $dataEndRow)->applyFromArray([
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Left align text columns
|
||||||
|
$sheet->getStyle('B' . $dataStartRow . ':D' . $dataEndRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||||
|
|
||||||
|
// Style the total row
|
||||||
|
$sheet->getStyle('A' . $totalRow . ':' . $lastColumn . $totalRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'size' => 11,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'F2F2F2'],
|
||||||
|
],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the export information section
|
||||||
|
if ($filterStartRow <= $lastRow) {
|
||||||
|
$exportInfoRow = $totalRow + 2; // After total row + empty row
|
||||||
|
$filterEndRow = $lastRow;
|
||||||
|
|
||||||
|
// Style the "INFORMASI EXPORT" title
|
||||||
|
$sheet->getStyle('A' . $exportInfoRow . ':' . $lastColumn . $exportInfoRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'size' => 12,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'E6E6E6'],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Style the filter info rows
|
||||||
|
$filterInfoStartRow = $exportInfoRow + 3; // After title + empty + "Filter yang Digunakan:"
|
||||||
|
$sheet->getStyle('A' . $filterInfoStartRow . ':' . $lastColumn . $filterEndRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'size' => 10,
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_TOP,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-size columns
|
||||||
|
foreach (range('A', $lastColumn) as $column) {
|
||||||
|
if ($column === 'A') {
|
||||||
|
// Set specific width for column A (No) - don't auto-size
|
||||||
|
$sheet->getColumnDimension($column)->setWidth(5);
|
||||||
|
} else {
|
||||||
|
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error applying styles: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columnWidths(): array
|
||||||
|
{
|
||||||
|
$widths = [
|
||||||
|
'A' => 5, // No - reduced from 8 to 5
|
||||||
|
'B' => 30, // Nama Pekerjaan
|
||||||
|
'C' => 15, // Kode Pekerjaan
|
||||||
|
'D' => 20, // Kategori
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add widths for mechanic columns
|
||||||
|
$currentColumn = 'E';
|
||||||
|
foreach ($this->mechanics as $mechanic) {
|
||||||
|
$widths[$currentColumn++] = 15; // Mechanic total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add width for total column
|
||||||
|
$widths[$currentColumn] = 15; // Total
|
||||||
|
|
||||||
|
return $widths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
AfterSheet::class => function(AfterSheet $event) {
|
||||||
|
$sheet = $event->sheet->getDelegate();
|
||||||
|
$highestColumn = $sheet->getHighestColumn();
|
||||||
|
$highestRow = $sheet->getHighestRow();
|
||||||
|
// Header styling ONLY for row 1
|
||||||
|
$sheet->getStyle('A1:' . $highestColumn . '1')->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'color' => ['rgb' => 'FFFFFF'],
|
||||||
|
'size' => 10,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => '2E5BBA'],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
// Total row styling (only last row)
|
||||||
|
$sheet->getStyle('A' . $highestRow . ':' . $highestColumn . $highestRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'size' => 11,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'F2F2F2'],
|
||||||
|
],
|
||||||
|
'borders' => [
|
||||||
|
'allBorders' => [
|
||||||
|
'borderStyle' => Border::BORDER_THIN,
|
||||||
|
'color' => ['rgb' => '000000'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
// Export info below table
|
||||||
|
$infoStartRow = $highestRow + 2;
|
||||||
|
$sheet->setCellValue('A' . $infoStartRow, 'INFORMASI EXPORT');
|
||||||
|
$sheet->getStyle('A' . $infoStartRow . ':' . $highestColumn . $infoStartRow)->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'bold' => true,
|
||||||
|
'size' => 12,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'E6E6E6'],
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sheet->setCellValue('A' . ($infoStartRow + 2), 'Filter yang Digunakan:');
|
||||||
|
$row = $infoStartRow + 3;
|
||||||
|
foreach ($this->filterInfo as $info) {
|
||||||
|
$sheet->setCellValue('A' . $row, $info);
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
$sheet->getStyle('A' . ($infoStartRow + 2) . ':A' . ($row-1))->applyFromArray([
|
||||||
|
'font' => [ 'size' => 10 ],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_TOP,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Dealer;
|
use App\Models\Dealer;
|
||||||
use App\Models\Menu;
|
use App\Models\Menu;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Work;
|
use App\Models\Work;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
@@ -37,9 +39,22 @@ class AdminController extends Controller
|
|||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
$dealer = $request->dealer;
|
$dealer = $request->dealer;
|
||||||
$year = $request->year;
|
$year = $request->year;
|
||||||
$dealer_datas = Dealer::all();
|
|
||||||
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
||||||
// dd($ajax_url);
|
|
||||||
return view('dashboard', compact('month','year', 'ajax_url', 'dealer', 'dealer_datas'));
|
return view('dashboard', compact('month','year', 'ajax_url', 'dealer', 'dealer_datas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,22 +87,52 @@ class AdminController extends Controller
|
|||||||
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
||||||
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
|
||||||
$sql .= " and t.dealer_id = '". $dealer ."'";
|
|
||||||
}
|
|
||||||
|
|
||||||
$dealer_work_trx = DB::statement($sql);
|
$dealer_work_trx = DB::statement($sql);
|
||||||
|
|
||||||
|
// Get dealers based on user role - only change this part
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the requested dealer is allowed for this user
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
if($dealer_datas->count() > 0) {
|
||||||
}else{
|
$allowedDealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
if(!in_array($dealer, $allowedDealerIds)) {
|
||||||
|
// If dealer is not allowed, reset to 'all'
|
||||||
|
$dealer = 'all';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no dealers are allowed, reset to 'all'
|
||||||
|
$dealer = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dealer filter based on user role
|
||||||
|
$dealerFilter = '';
|
||||||
|
if($dealer_datas->count() > 0) {
|
||||||
|
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
|
$dealerFilter = " and s.dealer_id IN (" . implode(',', $dealerIds) . ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
|
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."'". $dealerFilter ." GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."'". $dealerFilter ." GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||||
|
} else {
|
||||||
|
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||||
}
|
}
|
||||||
|
|
||||||
$dealer_work_trx = DB::statement("PREPARE stmt FROM @sql");
|
$dealer_work_trx = DB::statement("PREPARE stmt FROM @sql");
|
||||||
$dealer_work_trx = DB::select(DB::raw("EXECUTE stmt"));
|
$dealer_work_trx = DB::select(DB::raw("EXECUTE stmt"));
|
||||||
DB::statement('DEALLOCATE PREPARE stmt');
|
DB::statement('DEALLOCATE PREPARE stmt');
|
||||||
// DD($dealer_work_trx);
|
|
||||||
$theads = ['DEALER'];
|
$theads = ['DEALER'];
|
||||||
$dealer_names = [];
|
$dealer_names = [];
|
||||||
$dealer_trx = [];
|
$dealer_trx = [];
|
||||||
@@ -118,7 +163,6 @@ class AdminController extends Controller
|
|||||||
|
|
||||||
$dealer_names[] = $dealer_work->DEALER;
|
$dealer_names[] = $dealer_work->DEALER;
|
||||||
}
|
}
|
||||||
// dd($dealer_trx);
|
|
||||||
$dealer_trx = array_values($dealer_trx);
|
$dealer_trx = array_values($dealer_trx);
|
||||||
$dealer = $request->dealer;
|
$dealer = $request->dealer;
|
||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
@@ -128,10 +172,12 @@ class AdminController extends Controller
|
|||||||
|
|
||||||
$prev_mth_start = date('Y-m-d', strtotime(date($year.'-'. $request->month .'-1')." -1 month"));
|
$prev_mth_start = date('Y-m-d', strtotime(date($year.'-'. $request->month .'-1')." -1 month"));
|
||||||
$prev_mth = explode('-', $prev_mth_start);
|
$prev_mth = explode('-', $prev_mth_start);
|
||||||
if($request->month == date('m')) {
|
if($request->month == date('m') && $year == date('Y')) {
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}else{
|
}else{
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}
|
}
|
||||||
|
|
||||||
$prev_month_trx = [];
|
$prev_month_trx = [];
|
||||||
@@ -143,6 +189,11 @@ class AdminController extends Controller
|
|||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$prev_month = $prev_month->where('dealer_id', $request->dealer);
|
$prev_month = $prev_month->where('dealer_id', $request->dealer);
|
||||||
$now_month = $now_month->where('dealer_id', $request->dealer);
|
$now_month = $now_month->where('dealer_id', $request->dealer);
|
||||||
|
} else if($dealer_datas->count() > 0) {
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
|
$prev_month = $prev_month->whereIn('dealer_id', $dealerIds);
|
||||||
|
$now_month = $now_month->whereIn('dealer_id', $dealerIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
$prev_month_trx[] = $prev_month->sum('qty');
|
$prev_month_trx[] = $prev_month->sum('qty');
|
||||||
@@ -160,6 +211,36 @@ class AdminController extends Controller
|
|||||||
return view('dashboard_data', compact('theads', 'work_trx', 'month', 'year', 'dealer_names', 'dealer_trx', 'dealer', 'totals'));
|
return view('dashboard_data', compact('theads', 'work_trx', 'month', 'year', 'dealer_names', 'dealer_trx', 'dealer', 'totals'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if role is admin type
|
||||||
|
*/
|
||||||
|
private function isAdminRole($role)
|
||||||
|
{
|
||||||
|
if (!$role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define admin role names that should have access to all dealers
|
||||||
|
$adminRoleNames = [
|
||||||
|
'admin'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if role name contains admin keywords (but not "area")
|
||||||
|
$roleName = strtolower(trim($role->name));
|
||||||
|
foreach ($adminRoleNames as $adminName) {
|
||||||
|
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role with "area" in name should use pivot dealers, not all dealers
|
||||||
|
if (strpos($roleName, 'area') !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function dealer_work_trx(Request $request) {
|
public function dealer_work_trx(Request $request) {
|
||||||
$dealer_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request) {
|
$dealer_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
@@ -227,13 +308,14 @@ class AdminController extends Controller
|
|||||||
foreach($works as $work1) {
|
foreach($works as $work1) {
|
||||||
$prev_mth_start = date('Y-m-d', strtotime(date('Y-'. $request->month .'-1')." -1 month"));
|
$prev_mth_start = date('Y-m-d', strtotime(date('Y-'. $request->month .'-1')." -1 month"));
|
||||||
$prev_mth = explode('-', $prev_mth_start);
|
$prev_mth = explode('-', $prev_mth_start);
|
||||||
if($request->month == date('m')) {
|
if($request->month == date('m') && date('Y') == date('Y')) {
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}else{
|
}else{
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}
|
}
|
||||||
|
|
||||||
// dd($prev_mth_end);
|
|
||||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $dealer->id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $dealer->id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||||
|
|
||||||
if(array_key_exists($work1->id, $prev_month_trxs_total)) {
|
if(array_key_exists($work1->id, $prev_month_trxs_total)) {
|
||||||
@@ -348,10 +430,12 @@ class AdminController extends Controller
|
|||||||
foreach($works as $work1) {
|
foreach($works as $work1) {
|
||||||
$prev_mth_start = date('Y-m-d', strtotime(date($request->year.'-'. $request->month .'-1')." -1 month"));
|
$prev_mth_start = date('Y-m-d', strtotime(date($request->year.'-'. $request->month .'-1')." -1 month"));
|
||||||
$prev_mth = explode('-', $prev_mth_start);
|
$prev_mth = explode('-', $prev_mth_start);
|
||||||
if($request->month == date('m')) {
|
if($request->month == date('m') && $request->year == date('Y')) {
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}else{
|
}else{
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||||
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||||
}
|
}
|
||||||
|
|
||||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||||
@@ -440,16 +524,12 @@ class AdminController extends Controller
|
|||||||
|
|
||||||
// $month_trxs_total = array_values($month_trxs_total);
|
// $month_trxs_total = array_values($month_trxs_total);
|
||||||
// $yesterday_month_trxs_total = array_values($yesterday_month_trxs_total);
|
// $yesterday_month_trxs_total = array_values($yesterday_month_trxs_total);
|
||||||
// dd(["month_trxs_total" => $month_trxs_total, "yesterday_month_trxs_total" => $yesterday_month_trxs_total, "works" => $works->toArray()]);
|
|
||||||
// dd($month_trxs_total);
|
|
||||||
// dd($yesterday_month_trxs_total);
|
|
||||||
$final_month_trxs_total = [];
|
$final_month_trxs_total = [];
|
||||||
$final_yesterday_month_trxs_total = [];
|
$final_yesterday_month_trxs_total = [];
|
||||||
foreach($works as $work1) {
|
foreach($works as $work1) {
|
||||||
$final_month_trxs_total[$work1->id] = array_key_exists($work1->id, $month_trxs_total) ? $month_trxs_total[$work1->id] : 0;
|
$final_month_trxs_total[$work1->id] = array_key_exists($work1->id, $month_trxs_total) ? $month_trxs_total[$work1->id] : 0;
|
||||||
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
||||||
}
|
}
|
||||||
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
|
|
||||||
$month_trxs_total = array_values($final_month_trxs_total);
|
$month_trxs_total = array_values($final_month_trxs_total);
|
||||||
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
||||||
$totals = [];
|
$totals = [];
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ class ApiController extends Controller
|
|||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
||||||
}
|
}
|
||||||
|
|
||||||
// dd($prev_mth_end);
|
|
||||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||||
|
|
||||||
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
||||||
@@ -153,7 +152,6 @@ class ApiController extends Controller
|
|||||||
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
|
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
|
||||||
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
||||||
}
|
}
|
||||||
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
|
|
||||||
$month_trxs_total = array_values($final_month_trxs_total);
|
$month_trxs_total = array_values($final_month_trxs_total);
|
||||||
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
||||||
|
|
||||||
@@ -287,7 +285,11 @@ class ApiController extends Controller
|
|||||||
|
|
||||||
public function logout()
|
public function logout()
|
||||||
{
|
{
|
||||||
Auth::user()->tokens()->delete();
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth('sanctum')->user();
|
||||||
|
if ($user) {
|
||||||
|
$user->tokens()->delete();
|
||||||
|
}
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Logout success',
|
'message' => 'Logout success',
|
||||||
'status' => true,
|
'status' => true,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Privilege;
|
use App\Models\Privilege;
|
||||||
|
use App\Models\User;
|
||||||
use App\Providers\RouteServiceProvider;
|
use App\Providers\RouteServiceProvider;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
@@ -50,11 +51,39 @@ class LoginController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function authenticated(Request $request, $user)
|
protected function authenticated(Request $request, $user)
|
||||||
{
|
{
|
||||||
$user = Privilege::where('menu_id', 10)->where('role_id', Auth::user()->role_id)->where('view', 1)->first();
|
// Get user's role_id
|
||||||
|
$roleId = Auth::user()->role_id;
|
||||||
|
|
||||||
if ($user != null) {
|
if (!$roleId) {
|
||||||
return redirect()->route('dashboard');
|
// User has no role, redirect to default
|
||||||
}else{
|
return redirect(RouteServiceProvider::HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has access to adminarea menu
|
||||||
|
if (!User::roleCanAccessMenu($roleId, 'adminarea')) {
|
||||||
|
// User doesn't have admin area access, redirect to default home
|
||||||
|
return redirect(RouteServiceProvider::HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User has admin area access, get first accessible menu (excluding adminarea and mechanicarea)
|
||||||
|
$firstMenu = Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||||
|
->where('privileges.role_id', $roleId)
|
||||||
|
->where('privileges.view', 1)
|
||||||
|
->whereNotIn('menus.link', ['adminarea', 'mechanicarea'])
|
||||||
|
->select('menus.*', 'privileges.view', 'privileges.create', 'privileges.update', 'privileges.delete')
|
||||||
|
->orderBy('menus.id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$firstMenu) {
|
||||||
|
// User has no accessible menus (excluding adminarea/mechanicarea), redirect to default
|
||||||
|
return redirect(RouteServiceProvider::HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to redirect to the first accessible menu
|
||||||
|
return redirect()->route($firstMenu->link);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Route doesn't exist, fallback to default home
|
||||||
return redirect(RouteServiceProvider::HOME);
|
return redirect(RouteServiceProvider::HOME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
app/Http/Controllers/KPI/TargetsController.php
Normal file
237
app/Http/Controllers/KPI/TargetsController.php
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\KPI;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\KPI\StoreKpiTargetRequest;
|
||||||
|
use App\Http\Requests\KPI\UpdateKpiTargetRequest;
|
||||||
|
use App\Models\KpiTarget;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TargetsController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of KPI targets
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$targets = KpiTarget::with(['user', 'user.role'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->where('role_id', 3)
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// If no mechanics found, get all users as fallback
|
||||||
|
if ($mechanics->isEmpty()) {
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('kpi.targets.index', compact('targets', 'mechanics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new KPI target
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->where('role_id', 3)
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Debug: Log the mechanics found
|
||||||
|
Log::info('Mechanics found for KPI target creation:', [
|
||||||
|
'count' => $mechanics->count(),
|
||||||
|
'mechanics' => $mechanics->pluck('name', 'id')->toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If no mechanics found, get all users as fallback
|
||||||
|
if ($mechanics->isEmpty()) {
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
Log::warning('No mechanics found, using all users as fallback', [
|
||||||
|
'count' => $mechanics->count()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('kpi.targets.create', compact('mechanics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created KPI target
|
||||||
|
*/
|
||||||
|
public function store(StoreKpiTargetRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Log the validated data
|
||||||
|
Log::info('Creating KPI target with data:', $request->validated());
|
||||||
|
|
||||||
|
// Check if user already has an active target and deactivate it
|
||||||
|
$existingTarget = KpiTarget::where('user_id', $request->user_id)
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingTarget) {
|
||||||
|
Log::info('Deactivating existing active KPI target', [
|
||||||
|
'user_id' => $request->user_id,
|
||||||
|
'existing_target_id' => $existingTarget->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Deactivate the existing target
|
||||||
|
$existingTarget->update(['is_active' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = KpiTarget::create($request->validated());
|
||||||
|
|
||||||
|
Log::info('KPI target created successfully', [
|
||||||
|
'target_id' => $target->id,
|
||||||
|
'user_id' => $target->user_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('kpi.targets.index')
|
||||||
|
->with('success', 'Target KPI berhasil ditambahkan');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to create KPI target', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'data' => $request->validated()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Gagal menambahkan target KPI: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified KPI target
|
||||||
|
*/
|
||||||
|
public function show(KpiTarget $target)
|
||||||
|
{
|
||||||
|
$target->load(['user.dealer', 'achievements']);
|
||||||
|
|
||||||
|
return view('kpi.targets.show', compact('target'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified KPI target
|
||||||
|
*/
|
||||||
|
public function edit(KpiTarget $target)
|
||||||
|
{
|
||||||
|
// Debug: Check if target is loaded correctly
|
||||||
|
if (!$target) {
|
||||||
|
abort(404, 'Target KPI tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load target with user relationship
|
||||||
|
$target->load('user');
|
||||||
|
|
||||||
|
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->where('role_id', 3)
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// If no mechanics found, get all users as fallback
|
||||||
|
if ($mechanics->isEmpty()) {
|
||||||
|
$mechanics = User::with('dealer')
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure data types are correct for comparison
|
||||||
|
$target->user_id = (int)$target->user_id;
|
||||||
|
|
||||||
|
return view('kpi.targets.edit', compact('target', 'mechanics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified KPI target
|
||||||
|
*/
|
||||||
|
public function update(UpdateKpiTargetRequest $request, KpiTarget $target)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$target->update($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('kpi.targets.index')
|
||||||
|
->with('success', 'Target KPI berhasil diperbarui');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Gagal memperbarui target KPI: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified KPI target
|
||||||
|
*/
|
||||||
|
public function destroy(KpiTarget $target)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$target->delete();
|
||||||
|
|
||||||
|
return redirect()->route('kpi.targets.index')
|
||||||
|
->with('success', 'Target KPI berhasil dihapus');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('error', 'Gagal menghapus target KPI: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle active status of KPI target
|
||||||
|
*/
|
||||||
|
public function toggleStatus(KpiTarget $target)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$target->update(['is_active' => !$target->is_active]);
|
||||||
|
|
||||||
|
$status = $target->is_active ? 'diaktifkan' : 'dinonaktifkan';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Target KPI berhasil {$status}",
|
||||||
|
'is_active' => $target->is_active
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gagal mengubah status target KPI'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI targets for specific user
|
||||||
|
*/
|
||||||
|
public function getUserTargets(User $user)
|
||||||
|
{
|
||||||
|
$targets = $user->kpiTargets()
|
||||||
|
->with('achievements')
|
||||||
|
->orderBy('year', 'desc')
|
||||||
|
->orderBy('month', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $targets
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Yajra\DataTables\Facades\DataTables;
|
use Yajra\DataTables\Facades\DataTables;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
class ReportController extends Controller
|
class ReportController extends Controller
|
||||||
{
|
{
|
||||||
@@ -36,13 +37,41 @@ class ReportController extends Controller
|
|||||||
$request['sa'] = 'all';
|
$request['sa'] = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request) {
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$q = $q->whereIn('dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$q = $q->where('dealer_id', '=', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$q = $q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$q = $q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
@@ -52,8 +81,27 @@ class ReportController extends Controller
|
|||||||
return $q;
|
return $q;
|
||||||
})->orderBy('id', 'ASC')->get();
|
})->orderBy('id', 'ASC')->get();
|
||||||
|
|
||||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
// Get dealers based on user role
|
||||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SA users based on dealer access
|
||||||
|
if($dealer_datas->count() > 0) {
|
||||||
|
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
|
$sa_datas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||||
|
} else {
|
||||||
|
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||||
|
}
|
||||||
$sa = $request->sa;
|
$sa = $request->sa;
|
||||||
$dealer = $request->dealer;
|
$dealer = $request->dealer;
|
||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
@@ -82,8 +130,27 @@ class ReportController extends Controller
|
|||||||
$request['sa'] = 'all';
|
$request['sa'] = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
// Get dealers based on user role
|
||||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SA users based on dealer access
|
||||||
|
if($dealer_datas->count() > 0) {
|
||||||
|
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
|
$sa_datas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||||
|
} else {
|
||||||
|
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||||
|
}
|
||||||
|
|
||||||
$sa = $request->sa;
|
$sa = $request->sa;
|
||||||
$dealer = $request->dealer;
|
$dealer = $request->dealer;
|
||||||
@@ -126,11 +193,40 @@ class ReportController extends Controller
|
|||||||
$sa = $request->sa;
|
$sa = $request->sa;
|
||||||
$year = $request->year;
|
$year = $request->year;
|
||||||
|
|
||||||
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
||||||
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$dealerIdsStr = implode(',', $dealerIds);
|
||||||
|
$sql .= " and t.dealer_id IN (". $dealerIdsStr .")";
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$sql .= " and t.dealer_id = '". $dealer ."'";
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$sql .= " and t.dealer_id = '". $dealer ."'";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$sql .= " and t.dealer_id = '". $dealer ."'";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
@@ -139,17 +235,35 @@ class ReportController extends Controller
|
|||||||
|
|
||||||
$sa_work_trx = DB::statement($sql);
|
$sa_work_trx = DB::statement($sql);
|
||||||
|
|
||||||
|
// Validate dealer access before building the main query
|
||||||
|
$dealerFilter = "";
|
||||||
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$dealerFilter = " and s.dealer_id = '". $dealer ."'";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$dealerFilter = " and s.dealer_id = '". $dealer ."'";
|
||||||
|
}
|
||||||
|
} else if($allowedDealers->count() > 0) {
|
||||||
|
// If no specific dealer requested, filter by allowed dealers
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$dealerIdsStr = implode(',', $dealerIds);
|
||||||
|
$dealerFilter = " and s.dealer_id IN (". $dealerIdsStr .")";
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||||
}else{
|
}else{
|
||||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||||
}else{
|
}else{
|
||||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,13 +332,41 @@ class ReportController extends Controller
|
|||||||
$request['month'] = date('m');
|
$request['month'] = date('m');
|
||||||
}
|
}
|
||||||
|
|
||||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request) {
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$q->whereMonth('date', '=', $request->month);
|
$q->whereMonth('date', '=', $request->month);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$q->whereIn('dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$q->where('dealer_id', '=', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
@@ -232,7 +374,27 @@ class ReportController extends Controller
|
|||||||
}
|
}
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
$sas = User::select('id', 'name')->where('role_id', 4)->get();
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SA users based on dealer access
|
||||||
|
if($dealer_datas->count() > 0) {
|
||||||
|
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||||
|
$sas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||||
|
} else {
|
||||||
|
$sas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||||
|
}
|
||||||
|
|
||||||
$trxs = [];
|
$trxs = [];
|
||||||
foreach($sas as $key => $sa) {
|
foreach($sas as $key => $sa) {
|
||||||
@@ -244,8 +406,22 @@ class ReportController extends Controller
|
|||||||
$d = $d->whereMonth('date', '=', $request->month);
|
$d = $d->whereMonth('date', '=', $request->month);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$d = $d->whereIn('dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$d = $d->where('dealer_id', '=', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$d = $d->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$d = $d->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->sa) && $request->sa != 'all') {
|
if(isset($request->sa) && $request->sa != 'all') {
|
||||||
@@ -296,40 +472,80 @@ class ReportController extends Controller
|
|||||||
|
|
||||||
$sa_names = json_encode($sa_names);
|
$sa_names = json_encode($sa_names);
|
||||||
$trx_data = json_encode(array_values($trx_data));
|
$trx_data = json_encode(array_values($trx_data));
|
||||||
// dd($trx_data);
|
|
||||||
$work_count = count($works);
|
$work_count = count($works);
|
||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
$dealer_id = $request->dealer;
|
$dealer_id = $request->dealer;
|
||||||
$sa_id = $request->sa;
|
$sa_id = $request->sa;
|
||||||
$dealers = Dealer::all();
|
|
||||||
$sas = User::where('role_id', 4)->get();
|
|
||||||
|
|
||||||
|
|
||||||
return view('back.report.transaction_sa', compact('sas', 'dealers', 'dealer_id', 'sa_id', 'month', 'trxs', 'works', 'work_count', 'sa_names', 'trx_data'));
|
return view('back.report.transaction_sa', compact('sas', 'dealer_datas', 'dealer_id', 'sa_id', 'month', 'trxs', 'works', 'work_count', 'sa_names', 'trx_data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sa_work_trx(Request $request) {
|
public function sa_work_trx(Request $request) {
|
||||||
$sa_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request) {
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sa_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$q->whereIn('dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$q = $q->where('dealer_id', '=', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
||||||
$q = $q->where('user_sa_id', '=', $request->sa_filter);
|
$q->where('user_sa_id', '=', $request->sa_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
})->leftJoin('transactions AS t', function($q) use($request) {
|
})->leftJoin('transactions AS t', function($q) use($request, $allowedDealers) {
|
||||||
$q->on('t.work_id', '=', 'works.id');
|
$q->on('t.work_id', '=', 'works.id');
|
||||||
$q->on(DB::raw('MONTH(t.date)'), '=', DB::raw($request->month));
|
$q->on(DB::raw('MONTH(t.date)'), '=', DB::raw($request->month));
|
||||||
$q->on(DB::raw('YEAR(t.date)'), '=', DB::raw(date('Y')));
|
$q->on(DB::raw('YEAR(t.date)'), '=', DB::raw(date('Y')));
|
||||||
$q->on('t.user_sa_id', '=', DB::raw($request->sa));
|
$q->on('t.user_sa_id', '=', DB::raw($request->sa));
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$q->whereIn('t.dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$q->on('t.dealer_id', '=', DB::raw($request->dealer));
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$q->on('t.dealer_id', '=', DB::raw($request->dealer));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$q->on('t.dealer_id', '=', DB::raw($request->dealer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
||||||
$q->on('t.user_sa_id', '=', DB::raw($request->sa_filter));
|
$q->on('t.user_sa_id', '=', DB::raw($request->sa_filter));
|
||||||
@@ -351,13 +567,41 @@ class ReportController extends Controller
|
|||||||
$request['sa'] = 'all';
|
$request['sa'] = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sas = User::where('role_id', 4)->whereHas('sa_transactions', function($q) use($request) {
|
// Get dealers based on user role
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sas = User::where('role_id', 4)->whereHas('sa_transactions', function($q) use($request, $allowedDealers) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$q->whereIn('dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||||
$q->where('dealer_id', '=', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$q->where('dealer_id', '=', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,10 +627,22 @@ class ReportController extends Controller
|
|||||||
$request['year'] = date('Y');
|
$request['year'] = date('Y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||||
|
$dealer_datas = Dealer::all();
|
||||||
|
} else if($role) {
|
||||||
|
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealer_datas = collect();
|
||||||
|
}
|
||||||
|
|
||||||
$year = $request->year;
|
$year = $request->year;
|
||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
$dealer = $request->dealer;
|
$dealer = $request->dealer;
|
||||||
$dealer_datas = Dealer::all();
|
|
||||||
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
||||||
return view('dashboard', compact('month', 'ajax_url', 'dealer', 'dealer_datas', 'year'));
|
return view('dashboard', compact('month', 'ajax_url', 'dealer', 'dealer_datas', 'year'));
|
||||||
}
|
}
|
||||||
@@ -396,9 +652,30 @@ class ReportController extends Controller
|
|||||||
$menu = Menu::where('link', 'report.transaction')->first();
|
$menu = Menu::where('link', 'report.transaction')->first();
|
||||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||||
|
|
||||||
$sas = User::where('role_id', 4)->get();
|
$current_user = Auth::user();
|
||||||
$mechanics = User::where('role_id', 3)->get();
|
$current_role = Role::with(['dealers' => function($query) {
|
||||||
$dealers = Dealer::all();
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($current_user->role_id);
|
||||||
|
|
||||||
|
// Get dealers based on user role
|
||||||
|
if($current_role && $this->isAdminRole($current_role) && $current_role->dealers->count() == 0) {
|
||||||
|
$dealers = Dealer::all();
|
||||||
|
} else if($current_role) {
|
||||||
|
$dealers = $current_role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$dealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SA users based on dealer access
|
||||||
|
if($dealers->count() > 0) {
|
||||||
|
$dealerIds = $dealers->pluck('id')->toArray();
|
||||||
|
$sas = User::where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||||
|
$mechanics = User::where('role_id', 3)->whereIn('dealer_id', $dealerIds)->get();
|
||||||
|
} else {
|
||||||
|
$sas = User::where('role_id', 4)->get();
|
||||||
|
$mechanics = User::where('role_id', 3)->get();
|
||||||
|
}
|
||||||
|
|
||||||
$works = Work::all();
|
$works = Work::all();
|
||||||
|
|
||||||
return view('back.report.transaction', compact('sas', 'mechanics', 'dealers', 'works'));
|
return view('back.report.transaction', compact('sas', 'mechanics', 'dealers', 'works'));
|
||||||
@@ -410,12 +687,50 @@ class ReportController extends Controller
|
|||||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||||
|
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
|
// Get dealers based on user role
|
||||||
|
$current_user = Auth::user();
|
||||||
|
$current_role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($current_user->role_id);
|
||||||
|
|
||||||
|
if($current_role && $this->isAdminRole($current_role) && $current_role->dealers->count() == 0) {
|
||||||
|
$allowedDealers = Dealer::all();
|
||||||
|
} else if($current_role) {
|
||||||
|
$allowedDealers = $current_role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
} else {
|
||||||
|
$allowedDealers = collect();
|
||||||
|
}
|
||||||
|
|
||||||
$data = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
|
$data = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
|
||||||
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||||
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
||||||
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
|
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
|
||||||
->select('transactions.id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk', 'transactions.dealer_id', 'd.name as dealer_name');
|
->leftJoin('prechecks as pre', 'pre.transaction_id', '=', 'transactions.id')
|
||||||
|
->leftJoin('postchecks as post', 'post.transaction_id', '=', 'transactions.id')
|
||||||
|
->select(
|
||||||
|
'transactions.id',
|
||||||
|
'transactions.status',
|
||||||
|
'users.name as username',
|
||||||
|
'sa.name as sa_name',
|
||||||
|
'cat.name as category_name',
|
||||||
|
'w.name as workname',
|
||||||
|
'transactions.qty as qty',
|
||||||
|
'transactions.date as date',
|
||||||
|
'transactions.police_number as police_number',
|
||||||
|
'transactions.warranty as warranty',
|
||||||
|
'transactions.spk as spk',
|
||||||
|
'd.name as dealer_name',
|
||||||
|
DB::raw('pre.id as precheck_id'),
|
||||||
|
DB::raw('post.id as postcheck_id')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by allowed dealers based on user role
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
|
||||||
|
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
$data->whereIn('transactions.dealer_id', $dealerIds);
|
||||||
|
}
|
||||||
|
|
||||||
if(isset($request->date_start)) {
|
if(isset($request->date_start)) {
|
||||||
$data->where('transactions.date', '>=', $request->date_start);
|
$data->where('transactions.date', '>=', $request->date_start);
|
||||||
@@ -434,28 +749,82 @@ class ReportController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->dealer)) {
|
if(isset($request->dealer)) {
|
||||||
$data->where('transactions.dealer_id', $request->dealer);
|
// Validate that the requested dealer is allowed for this user
|
||||||
|
if($allowedDealers->count() > 0) {
|
||||||
|
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||||
|
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||||
|
$data->where('transactions.dealer_id', $request->dealer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$data->where('transactions.dealer_id', $request->dealer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data->orderBy('date', 'DESC');
|
$data->orderBy('date', 'DESC');
|
||||||
return DataTables::of($data)->addIndexColumn()
|
return DataTables::of($data)->addIndexColumn()
|
||||||
->addColumn('action', function($row) use ($menu) {
|
->addColumn('action', function($row) use ($menu) {
|
||||||
$btn = '<div class="d-flex justify-content-center">';
|
$btn = '<div class="d-flex justify-content-center align-items-center flex-wrap">';
|
||||||
|
|
||||||
if($row->status == 1) {
|
// Jika status closed
|
||||||
if(Gate::allows('delete', $menu)) {
|
if ($row->status == 1) {
|
||||||
$btn .= ' <button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
|
if (Gate::allows('delete', $menu)) {
|
||||||
}
|
$btn .= '<button class="btn btn-danger btn-sm font-weight-bold mr-2 mt-2"
|
||||||
$btn .= '<span class="badge badge-success">Closed</span>';
|
data-action="'. route('report.transaction.destroy', $row->id) .'"
|
||||||
}else{
|
id="destroyTransaction'. $row->id .'"
|
||||||
if(Gate::allows('delete', $menu)) {
|
onclick="destroyTransaction('. $row->id .')">
|
||||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
|
Hapus
|
||||||
|
</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Gate::allows('update', $menu)) {
|
// Badge Closed rapi
|
||||||
$btn .= '<button class="btn btn-info btn-sm btn-bold mr-2" data-url="'. route('report.transaction.edit', $row->id) .'" data-action="'. route('report.transaction.update', $row->id) .'" onclick="editTransaction('. $row->id .')" id="editTransaction'. $row->id .'"> Edit </button>
|
$btn .= '<span class="btn btn-success btn-sm font-weight-bold px-3 py-2 mr-2 mt-2 disabled"
|
||||||
<button class="btn btn-warning btn-sm btn-bold" id="closeTransaction'. $row->id .'" data-url="'. route('report.transaction.close', $row->id) .'" onclick="closeTransaction('. $row->id .')"> Close </button>';
|
style="pointer-events: none; cursor: default;">
|
||||||
|
Closed
|
||||||
|
</span>';
|
||||||
|
} else {
|
||||||
|
if (Gate::allows('delete', $menu)) {
|
||||||
|
$btn .= '<button class="btn btn-danger btn-sm font-weight-bold mr-2 mt-2"
|
||||||
|
data-action="'. route('report.transaction.destroy', $row->id) .'"
|
||||||
|
id="destroyTransaction'. $row->id .'"
|
||||||
|
onclick="destroyTransaction('. $row->id .')">
|
||||||
|
Hapus
|
||||||
|
</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Gate::allows('update', $menu)) {
|
||||||
|
$btn .= '<button class="btn btn-info btn-sm font-weight-bold mr-2 mt-2"
|
||||||
|
data-url="'. route('report.transaction.edit', $row->id) .'"
|
||||||
|
data-action="'. route('report.transaction.update', $row->id) .'"
|
||||||
|
onclick="editTransaction('. $row->id .')"
|
||||||
|
id="editTransaction'. $row->id .'">
|
||||||
|
Edit
|
||||||
|
</button>';
|
||||||
|
|
||||||
|
$btn .= '<button class="btn btn-warning btn-sm font-weight-bold mr-2 mt-2"
|
||||||
|
id="closeTransaction'. $row->id .'"
|
||||||
|
data-url="'. route('report.transaction.close', $row->id) .'"
|
||||||
|
onclick="closeTransaction('. $row->id .')">
|
||||||
|
Close
|
||||||
|
</button>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($row->precheck_id) {
|
||||||
|
$btn .= '<button class="btn btn-primary btn-sm font-weight-bold action-print mr-2 mt-2"
|
||||||
|
data-type="precheck"
|
||||||
|
data-id="'. $row->id .'"
|
||||||
|
data-url="'. route('report.transaction.precheck.print', $row->id) .'">
|
||||||
|
Pre Check
|
||||||
|
</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($row->postcheck_id) {
|
||||||
|
$btn .= '<button class="btn btn-success btn-sm font-weight-bold action-print mr-2 mt-2"
|
||||||
|
data-type="postcheck"
|
||||||
|
data-id="'. $row->id .'"
|
||||||
|
data-url="'. route('report.transaction.postcheck.print', $row->id) .'">
|
||||||
|
Post Check
|
||||||
|
</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$btn .= '</div>';
|
$btn .= '</div>';
|
||||||
@@ -565,4 +934,34 @@ class ReportController extends Controller
|
|||||||
|
|
||||||
return response()->json($response);
|
return response()->json($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if role is admin type
|
||||||
|
*/
|
||||||
|
private function isAdminRole($role)
|
||||||
|
{
|
||||||
|
if (!$role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define admin role names that should have access to all dealers
|
||||||
|
$adminRoleNames = [
|
||||||
|
'admin'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if role name contains admin keywords (but not "area")
|
||||||
|
$roleName = strtolower(trim($role->name));
|
||||||
|
foreach ($adminRoleNames as $adminName) {
|
||||||
|
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role with "area" in name should use pivot dealers, not all dealers
|
||||||
|
if (strpos($roleName, 'area') !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
app/Http/Controllers/Reports/ReportStockProductsController.php
Normal file
103
app/Http/Controllers/Reports/ReportStockProductsController.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Reports;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Menu;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Dealer;
|
||||||
|
use App\Models\Stock;
|
||||||
|
use App\Models\StockLog;
|
||||||
|
use App\Services\StockReportService;
|
||||||
|
use App\Exports\StockProductsExport;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Yajra\DataTables\DataTables;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ReportStockProductsController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||||
|
abort_if(!Gate::allows('view', $menu), 403);
|
||||||
|
|
||||||
|
return view('reports.stock-products');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(Request $request)
|
||||||
|
{
|
||||||
|
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||||
|
abort_if(!Gate::allows('view', $menu), 403);
|
||||||
|
|
||||||
|
if ($request->ajax()) {
|
||||||
|
$filterDate = $request->get('filter_date');
|
||||||
|
|
||||||
|
$stockService = new StockReportService();
|
||||||
|
$reportData = $stockService->getOptimizedStockReportData($filterDate);
|
||||||
|
|
||||||
|
return DataTables::of($reportData['data'])
|
||||||
|
->addIndexColumn()
|
||||||
|
->addColumn('product_info', function($row) {
|
||||||
|
return "<strong>{$row['product_name']}</strong><br><small class='text-muted'>{$row['product_code']}</small>";
|
||||||
|
})
|
||||||
|
->addColumn('total_stock', function($row) {
|
||||||
|
return number_format($row['total_stock'], 2);
|
||||||
|
})
|
||||||
|
->rawColumns(['product_info'])
|
||||||
|
->make(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Invalid request'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDealers()
|
||||||
|
{
|
||||||
|
$stockService = new StockReportService();
|
||||||
|
$dealers = $stockService->getDealersBasedOnUserRole();
|
||||||
|
return response()->json($dealers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||||
|
abort_if(!Gate::allows('view', $menu), 403);
|
||||||
|
|
||||||
|
$filterDate = $request->get('filter_date');
|
||||||
|
|
||||||
|
$stockService = new StockReportService();
|
||||||
|
$reportData = $stockService->getOptimizedStockReportData($filterDate);
|
||||||
|
|
||||||
|
// Validate report data
|
||||||
|
if (!isset($reportData['data']) || !isset($reportData['dealers'])) {
|
||||||
|
throw new \Exception('Invalid report data structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: Log dealer names to identify problematic characters
|
||||||
|
Log::info('Export data validation', [
|
||||||
|
'data_count' => count($reportData['data']),
|
||||||
|
'dealers_count' => count($reportData['dealers']),
|
||||||
|
'dealer_names' => $reportData['dealers']->pluck('name')->toArray(),
|
||||||
|
'first_data_row' => isset($reportData['data'][0]) ? array_keys($reportData['data'][0]) : []
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fileName = 'laporan_stok_produk_' . ($filterDate ?: date('Y-m-d')) . '.xlsx';
|
||||||
|
|
||||||
|
return Excel::download(new StockProductsExport($reportData), $fileName);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Export error: ' . $e->getMessage(), [
|
||||||
|
'filter_date' => $request->get('filter_date'),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'Gagal mengexport data: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
329
app/Http/Controllers/Reports/ReportTechniciansController.php
Normal file
329
app/Http/Controllers/Reports/ReportTechniciansController.php
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Reports;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Menu;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Services\TechnicianReportService;
|
||||||
|
use App\Exports\TechnicianReportExport;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Illuminate\Support\Facades\DB; // Added DB facade
|
||||||
|
use App\Models\Dealer; // Added Dealer model
|
||||||
|
|
||||||
|
class ReportTechniciansController extends Controller
|
||||||
|
{
|
||||||
|
protected $technicianReportService;
|
||||||
|
|
||||||
|
public function __construct(TechnicianReportService $technicianReportService)
|
||||||
|
{
|
||||||
|
$this->technicianReportService = $technicianReportService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$menu = Menu::where('link','reports.technician.index')->first();
|
||||||
|
abort_if(!Gate::allows('view', $menu), 403);
|
||||||
|
|
||||||
|
return view('reports.technician');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dealers for filter dropdown
|
||||||
|
*/
|
||||||
|
public function getDealers()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
Log::info('Controller: No authenticated user found');
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'User tidak terautentikasi'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Controller: Getting dealers for user:', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_name' => $user->name,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dealers = $this->technicianReportService->getDealers();
|
||||||
|
$defaultDealer = $this->technicianReportService->getDefaultDealer();
|
||||||
|
|
||||||
|
Log::info('Controller: Service returned dealers:', [
|
||||||
|
'dealers_count' => $dealers->count(),
|
||||||
|
'dealers' => $dealers->toArray(),
|
||||||
|
'default_dealer' => $defaultDealer ? $defaultDealer->toArray() : null,
|
||||||
|
'default_dealer_id' => $defaultDealer ? $defaultDealer->id : null
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if default dealer exists in dealers list
|
||||||
|
if ($defaultDealer && $dealers->count() > 0) {
|
||||||
|
$defaultDealerExists = $dealers->contains('id', $defaultDealer->id);
|
||||||
|
Log::info('Controller: Default dealer validation:', [
|
||||||
|
'default_dealer_id' => $defaultDealer->id,
|
||||||
|
'default_dealer_exists_in_list' => $defaultDealerExists,
|
||||||
|
'available_dealer_ids' => $dealers->pluck('id')->toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If default dealer doesn't exist in list, use first dealer from list
|
||||||
|
if (!$defaultDealerExists) {
|
||||||
|
Log::info('Controller: Default dealer not in list, using first dealer from list');
|
||||||
|
$defaultDealer = $dealers->first();
|
||||||
|
Log::info('Controller: New default dealer:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||||
|
}
|
||||||
|
} else if ($defaultDealer === null && $dealers->count() > 0) {
|
||||||
|
// Admin without default dealer - no need to set default
|
||||||
|
Log::info('Controller: Admin without default dealer, no default will be set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $dealers,
|
||||||
|
'default_dealer' => $defaultDealer ? $defaultDealer->id : null
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Controller: Error getting dealers: ' . $e->getMessage(), [
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Gagal mengambil data dealer: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get technician report data for DataTable
|
||||||
|
*/
|
||||||
|
public function getData(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dealerId = $request->input('dealer_id');
|
||||||
|
$startDate = $request->input('start_date');
|
||||||
|
$endDate = $request->input('end_date');
|
||||||
|
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'User tidak terautentikasi'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Requesting technician report data:', [
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||||
|
$dealerId,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info('Technician report data response:', [
|
||||||
|
'data_count' => count($reportData['data']),
|
||||||
|
'mechanics_count' => $reportData['mechanics']->count(),
|
||||||
|
'works_count' => $reportData['works']->count(),
|
||||||
|
'mechanics' => $reportData['mechanics']->map(function($mechanic) {
|
||||||
|
return [
|
||||||
|
'id' => $mechanic->id,
|
||||||
|
'name' => $mechanic->name,
|
||||||
|
'role_id' => $mechanic->role_id
|
||||||
|
];
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $reportData['data'],
|
||||||
|
'mechanics' => $reportData['mechanics'],
|
||||||
|
'works' => $reportData['works']
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error getting technician report data: ' . $e->getMessage(), [
|
||||||
|
'dealer_id' => $request->input('dealer_id'),
|
||||||
|
'start_date' => $request->input('start_date'),
|
||||||
|
'end_date' => $request->input('end_date'),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Gagal mengambil data laporan teknisi: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get technician report data for Yajra DataTable
|
||||||
|
*/
|
||||||
|
public function getDataTable(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dealerId = $request->input('dealer_id');
|
||||||
|
$startDate = $request->input('start_date');
|
||||||
|
$endDate = $request->input('end_date');
|
||||||
|
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'User tidak terautentikasi'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Requesting technician report data for DataTable:', [
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reportData = $this->technicianReportService->getTechnicianReportDataForDataTable(
|
||||||
|
$dealerId,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return $reportData;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error getting technician report data for DataTable: ' . $e->getMessage(), [
|
||||||
|
'dealer_id' => $request->input('dealer_id'),
|
||||||
|
'start_date' => $request->input('start_date'),
|
||||||
|
'end_date' => $request->input('end_date'),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'Gagal mengambil data laporan teknisi: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export technician report to Excel
|
||||||
|
*/
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dealerId = $request->input('dealer_id');
|
||||||
|
$startDate = $request->input('start_date');
|
||||||
|
$endDate = $request->input('end_date');
|
||||||
|
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'User tidak terautentikasi'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Exporting technician report', [
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Validate dealer access for export
|
||||||
|
if ($dealerId) {
|
||||||
|
// User is trying to export specific dealer
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
// User has specific dealer_id, check if they can access the requested dealer
|
||||||
|
if ($user->dealer_id != $dealerId) {
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->role_id) {
|
||||||
|
// User has role, check if they can access the requested dealer
|
||||||
|
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User is trying to export "Semua Dealer" - check if they have permission
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||||
|
if ($role) {
|
||||||
|
// Check if role is admin type
|
||||||
|
$technicianReportService = new \App\Services\TechnicianReportService();
|
||||||
|
if ($technicianReportService->isAdminRole($role)) {
|
||||||
|
// Admin can export all dealers
|
||||||
|
Log::info('Admin user exporting all dealers');
|
||||||
|
} else {
|
||||||
|
// Non-admin with pivot dealers - can only export pivot dealers
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
Log::info('User with pivot dealers exporting pivot dealers only');
|
||||||
|
} else {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Anda tidak memiliki akses untuk export data semua dealer'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->dealer_id) {
|
||||||
|
// User with specific dealer_id cannot export all dealers
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Anda hanya dapat export data dealer Anda sendiri'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Excel::download(new TechnicianReportExport($dealerId, $startDate, $endDate), 'laporan_teknisi_' . date('Y-m-d') . '.xlsx');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error exporting technician report: ' . $e->getMessage(), [
|
||||||
|
'dealer_id' => $request->input('dealer_id'),
|
||||||
|
'start_date' => $request->input('start_date'),
|
||||||
|
'end_date' => $request->input('end_date')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Gagal export laporan: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Models\Menu;
|
|||||||
use App\Models\Privilege;
|
use App\Models\Privilege;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Dealer;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
@@ -14,10 +15,11 @@ class RolePrivilegeController extends Controller
|
|||||||
public function index() {
|
public function index() {
|
||||||
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||||
$roles = Role::all();
|
$roles = Role::with('dealers')->get();
|
||||||
$menus = Menu::all();
|
$menus = Menu::all();
|
||||||
$users = User::all();
|
$users = User::all();
|
||||||
return view('back.roleprivileges', compact('roles', 'users', 'menus'));
|
$dealers = Dealer::all();
|
||||||
|
return view('back.roleprivileges', compact('roles', 'users', 'menus', 'dealers'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request) {
|
public function store(Request $request) {
|
||||||
@@ -117,4 +119,36 @@ class RolePrivilegeController extends Controller
|
|||||||
User::where('role_id', $id)->update(['role_id' => 0]);
|
User::where('role_id', $id)->update(['role_id' => 0]);
|
||||||
return redirect()->back()->with('success', 'Berhasil Hapus Role');
|
return redirect()->back()->with('success', 'Berhasil Hapus Role');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assignDealer(Request $request, $id) {
|
||||||
|
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||||
|
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'dealers' => 'required|array',
|
||||||
|
'dealers.*' => 'exists:dealers,id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
|
||||||
|
// Sync dealers (this will replace existing assignments)
|
||||||
|
$role->dealers()->sync($request->dealers);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Berhasil assign dealer ke role'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAssignedDealers($id) {
|
||||||
|
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||||
|
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||||
|
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
$assignedDealers = $role->dealers()->pluck('dealers.id')->toArray();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'assignedDealers' => $assignedDealers
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ use Illuminate\Support\Carbon;
|
|||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use App\Models\Precheck;
|
||||||
|
use App\Models\Postcheck;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class TransactionController extends Controller
|
class TransactionController extends Controller
|
||||||
@@ -49,7 +52,28 @@ class TransactionController extends Controller
|
|||||||
->where('active', true)
|
->where('active', true)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products'));
|
// Get KPI data for current user using KPI service
|
||||||
|
$kpiService = app(\App\Services\KpiService::class);
|
||||||
|
|
||||||
|
// Auto-calculate current month KPI achievement including claimed transactions
|
||||||
|
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||||
|
|
||||||
|
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||||
|
|
||||||
|
// Get current month period name
|
||||||
|
$currentMonthName = now()->translatedFormat('F Y');
|
||||||
|
|
||||||
|
$kpiData = [
|
||||||
|
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||||
|
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||||
|
'percentage' => $kpiSummary['current_percentage'],
|
||||||
|
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||||
|
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||||
|
'period' => $currentMonthName,
|
||||||
|
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products', 'kpiData'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function workcategory($category_id)
|
public function workcategory($category_id)
|
||||||
@@ -81,37 +105,60 @@ class TransactionController extends Controller
|
|||||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||||
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
||||||
->select('transactions.id as transaction_id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk')
|
->select('transactions.id as transaction_id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk')
|
||||||
|
->whereNull('transactions.deleted_at')
|
||||||
->where('users.dealer_id', Auth::user()->dealer_id);
|
->where('users.dealer_id', Auth::user()->dealer_id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$transaction_works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) {
|
$transaction_works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) {
|
||||||
return $q->whereDate('date', '=', date('Y-m-d'))->where('dealer_id', Auth::user()->dealer_id);
|
return $q->whereNull('deleted_at')->whereDate('date', '=', date('Y-m-d'))->where('dealer_id', Auth::user()->dealer_id);
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
$tm1 = [];
|
$tm1 = [];
|
||||||
foreach($transaction_works as $index => $work) {
|
foreach($transaction_works as $index => $work) {
|
||||||
$transaction_sas = Transaction::leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
$transaction_sas = Transaction::leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||||
->select(DB::raw('SUM(transactions.qty) as qty'), 'sa.name as sa_name')
|
->select(DB::raw('SUM(transactions.qty) as qty'), 'sa.name as sa_name')
|
||||||
|
->whereNull('transactions.deleted_at')
|
||||||
->where('sa.dealer_id', Auth::user()->dealer_id)
|
->where('sa.dealer_id', Auth::user()->dealer_id)
|
||||||
->where('work_id', $work->id)
|
->where('work_id', $work->id)
|
||||||
->whereDate('transactions.date', '=', date('Y-m-d'))->groupBy('transactions.user_sa_id')->get();
|
->whereDate('transactions.date', '=', date('Y-m-d'))->groupBy('transactions.user_sa_id')->get();
|
||||||
|
|
||||||
|
// Initialize data array for this work
|
||||||
|
$tm1[$work['shortname']]['data'] = [];
|
||||||
|
$daily_total = 0;
|
||||||
|
|
||||||
foreach($transaction_sas as $sa) {
|
foreach($transaction_sas as $sa) {
|
||||||
$tm1[$work['shortname']]['data'][] = $sa['sa_name'].":".$sa['qty'];
|
$tm1[$work['shortname']]['data'][] = $sa['sa_name'].":".$sa['qty'];
|
||||||
|
$daily_total += $sa['qty'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$month_share_data = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', Auth::user()->dealer->id)->whereMonth('date', date('m'))->whereYear('date', date('Y'))->where('work_id', $work->id)->groupBy('user_sa_id')->get();
|
// Add daily total even if no data
|
||||||
$tm1[$work['shortname']]['total_title'] = "*[PERIODE 1 - ". Carbon::now()->translatedFormat('d F Y') ."]*\n\n";
|
if (empty($tm1[$work['shortname']]['data'])) {
|
||||||
|
$tm1[$work['shortname']]['data'][] = "Tidak ada data:0";
|
||||||
$sum_month_share_trx = 0;
|
|
||||||
$tm_month = [];
|
|
||||||
foreach($month_share_data as $m_trx) {
|
|
||||||
$tm_month[] = $m_trx->name.":".$m_trx->qty." Unit\n";
|
|
||||||
$sum_month_share_trx += $m_trx->qty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$tm1[$work['shortname']]['total_body'] = $tm_month;
|
// Remove monthly data section since this is daily report
|
||||||
$tm1[$work['shortname']]['total_total'] = "*TOTAL : ". $sum_month_share_trx." Unit*";
|
// $month_share_data = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')
|
||||||
|
// ->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')
|
||||||
|
// ->where('transactions.dealer_id', Auth::user()->dealer->id)
|
||||||
|
// ->whereMonth('date', date('m'))
|
||||||
|
// ->whereYear('date', date('Y'))
|
||||||
|
// ->where('work_id', $work->id)
|
||||||
|
// ->groupBy('user_sa_id')
|
||||||
|
// ->get();
|
||||||
|
|
||||||
|
// Remove the period title since this is for daily report, not monthly
|
||||||
|
// $tm1[$work['shortname']]['total_title'] = "*[PERIODE 1 - ". Carbon::now()->translatedFormat('d F Y') ."]*\n\n";
|
||||||
|
|
||||||
|
// $sum_month_share_trx = 0;
|
||||||
|
// $tm_month = [];
|
||||||
|
// foreach($month_share_data as $m_trx) {
|
||||||
|
// $tm_month[] = $m_trx->name.":".$m_trx->qty." Unit\n";
|
||||||
|
// $sum_month_share_trx += $m_trx->qty;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $tm1[$work['shortname']]['total_body'] = $tm_month;
|
||||||
|
// $tm1[$work['shortname']]['total_total'] = "*TOTAL : ". $sum_month_share_trx." Unit*";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($request->date_start)) {
|
if(isset($request->date_start)) {
|
||||||
@@ -135,7 +182,29 @@ class TransactionController extends Controller
|
|||||||
$sas = User::where('role_id', 4)->get();
|
$sas = User::where('role_id', 4)->get();
|
||||||
$dealers = Dealer::all();
|
$dealers = Dealer::all();
|
||||||
$works = Work::all();
|
$works = Work::all();
|
||||||
return view('transaction.lists', compact('transaction_dealers', 'transaction_mechanics', 'mechanic', 'sas', 'dealers', 'works', 'date_start', 'date_end'));
|
|
||||||
|
// Get KPI data for current user using KPI service
|
||||||
|
$kpiService = app(\App\Services\KpiService::class);
|
||||||
|
|
||||||
|
// Auto-calculate current month KPI achievement including claimed transactions
|
||||||
|
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||||
|
|
||||||
|
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||||
|
|
||||||
|
// Get current month period name
|
||||||
|
$currentMonthName = now()->translatedFormat('F Y');
|
||||||
|
|
||||||
|
$kpiData = [
|
||||||
|
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||||
|
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||||
|
'percentage' => $kpiSummary['current_percentage'],
|
||||||
|
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||||
|
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||||
|
'period' => $currentMonthName,
|
||||||
|
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('transaction.lists', compact('transaction_dealers', 'transaction_mechanics', 'mechanic', 'sas', 'dealers', 'works', 'date_start', 'date_end', 'kpiData'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cmp($a, $b){
|
public function cmp($a, $b){
|
||||||
@@ -169,7 +238,7 @@ class TransactionController extends Controller
|
|||||||
$mechanic = User::leftJoin('dealers as d', 'd.id', '=', 'users.dealer_id')
|
$mechanic = User::leftJoin('dealers as d', 'd.id', '=', 'users.dealer_id')
|
||||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||||
->where('users.id', Auth::user()->id)->first();
|
->where('users.id', Auth::user()->id)->first();
|
||||||
$d = Transaction::leftJoin('works as w', 'w.id', '=', 'transactions.work_id')->select('transactions.*', 'w.name as work_name', 'w.shortname as shortname')->where('dealer_id', $id);
|
$d = Transaction::leftJoin('works as w', 'w.id', '=', 'transactions.work_id')->select('transactions.*', 'w.name as work_name', 'w.shortname as shortname')->whereNull('transactions.deleted_at')->where('dealer_id', $id);
|
||||||
|
|
||||||
|
|
||||||
if(isset($request->date_start)) {
|
if(isset($request->date_start)) {
|
||||||
@@ -316,13 +385,13 @@ class TransactionController extends Controller
|
|||||||
$id = Auth::user()->dealer_id;
|
$id = Auth::user()->dealer_id;
|
||||||
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||||
}
|
}
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||||
}
|
}
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
@@ -331,7 +400,7 @@ class TransactionController extends Controller
|
|||||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||||
->where('users.id', Auth::user()->id)->first();
|
->where('users.id', Auth::user()->id)->first();
|
||||||
|
|
||||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', date('Y'))->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->whereNull('deleted_at')->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', date('Y'))->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||||
$dates = $this->array_value_recursive('date', $dates);
|
$dates = $this->array_value_recursive('date', $dates);
|
||||||
|
|
||||||
$month_trxs = [];
|
$month_trxs = [];
|
||||||
@@ -342,7 +411,7 @@ class TransactionController extends Controller
|
|||||||
$prev_mth = explode('-', $prev_mth_start);
|
$prev_mth = explode('-', $prev_mth_start);
|
||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||||
|
|
||||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
$yesterday_month_trx = Transaction::whereNull('deleted_at')->where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||||
|
|
||||||
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
||||||
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
||||||
@@ -356,7 +425,7 @@ class TransactionController extends Controller
|
|||||||
$date_works = [];
|
$date_works = [];
|
||||||
$share_works = [];
|
$share_works = [];
|
||||||
foreach ($works as $key2 => $work) {
|
foreach ($works as $key2 => $work) {
|
||||||
$d = Transaction::where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
$d = Transaction::whereNull('deleted_at')->where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||||
|
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||||
@@ -399,7 +468,7 @@ class TransactionController extends Controller
|
|||||||
foreach($sas as $key => $sa) {
|
foreach($sas as $key => $sa) {
|
||||||
$sa_works = [];
|
$sa_works = [];
|
||||||
foreach ($works as $key2 => $work) {
|
foreach ($works as $key2 => $work) {
|
||||||
$d = Transaction::where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
$d = Transaction::whereNull('deleted_at')->where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||||
|
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||||
@@ -453,7 +522,6 @@ class TransactionController extends Controller
|
|||||||
$works_count = count($works);
|
$works_count = count($works);
|
||||||
$share = $month_trxs;
|
$share = $month_trxs;
|
||||||
$month = $request->month;
|
$month = $request->month;
|
||||||
dd($share);
|
|
||||||
|
|
||||||
return view('transaction.recap', compact('month_trxs_total', 'yesterday_month_trxs_total', 'month', 'trx_data', 'sa_names', 'works', 'works_count', 'trxs', 'month_trxs','dealer', 'share', 'mechanic'));
|
return view('transaction.recap', compact('month_trxs_total', 'yesterday_month_trxs_total', 'month', 'trx_data', 'sa_names', 'works', 'works_count', 'trxs', 'month_trxs','dealer', 'share', 'mechanic'));
|
||||||
}
|
}
|
||||||
@@ -471,13 +539,13 @@ class TransactionController extends Controller
|
|||||||
$id = Auth::user()->dealer_id;
|
$id = Auth::user()->dealer_id;
|
||||||
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||||
}
|
}
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||||
}
|
}
|
||||||
})->get();
|
})->get();
|
||||||
|
|
||||||
@@ -486,7 +554,7 @@ class TransactionController extends Controller
|
|||||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||||
->where('users.id', Auth::user()->id)->first();
|
->where('users.id', Auth::user()->id)->first();
|
||||||
|
|
||||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', $request->year)->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->whereNull('deleted_at')->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', $request->year)->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||||
// print_r($dates);die;
|
// print_r($dates);die;
|
||||||
$dates = $this->array_value_recursive('date', $dates);
|
$dates = $this->array_value_recursive('date', $dates);
|
||||||
|
|
||||||
@@ -502,8 +570,7 @@ class TransactionController extends Controller
|
|||||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
||||||
}
|
}
|
||||||
|
|
||||||
// dd($prev_mth_end);
|
$yesterday_month_trx = Transaction::whereNull('deleted_at')->where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
|
||||||
|
|
||||||
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
||||||
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
||||||
@@ -516,7 +583,7 @@ class TransactionController extends Controller
|
|||||||
$date_works = [];
|
$date_works = [];
|
||||||
$share_works = [];
|
$share_works = [];
|
||||||
foreach ($works as $key2 => $work) {
|
foreach ($works as $key2 => $work) {
|
||||||
$d = Transaction::where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
$d = Transaction::whereNull('deleted_at')->where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||||
|
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
||||||
@@ -556,14 +623,14 @@ class TransactionController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this_month_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', $id)->whereMonth('date', date('m'))->whereYear('date', $request->year)->groupBy('user_sa_id')->get();
|
$this_month_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->whereNull('transactions.deleted_at')->where('transactions.dealer_id', $id)->whereMonth('date', date('m'))->whereYear('date', $request->year)->groupBy('user_sa_id')->get();
|
||||||
$today_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', $id)->whereDate('date', date('Y-m-d'))->groupBy('user_sa_id')->get();
|
$today_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->whereNull('transactions.deleted_at')->where('transactions.dealer_id', $id)->whereDate('date', date('Y-m-d'))->groupBy('user_sa_id')->get();
|
||||||
|
|
||||||
$trxs = [];
|
$trxs = [];
|
||||||
foreach($sas as $key => $sa) {
|
foreach($sas as $key => $sa) {
|
||||||
$sa_works = [];
|
$sa_works = [];
|
||||||
foreach ($works as $key2 => $work) {
|
foreach ($works as $key2 => $work) {
|
||||||
$d = Transaction::where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
$d = Transaction::whereNull('deleted_at')->where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||||
|
|
||||||
if(isset($request->month)) {
|
if(isset($request->month)) {
|
||||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
||||||
@@ -612,15 +679,12 @@ class TransactionController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dd($works);
|
|
||||||
// dd([$month_trxs_total, $yesterday_month_trxs_total]);
|
|
||||||
$final_month_trxs_total = [];
|
$final_month_trxs_total = [];
|
||||||
$final_yesterday_month_trxs_total = [];
|
$final_yesterday_month_trxs_total = [];
|
||||||
foreach($works as $work1) {
|
foreach($works as $work1) {
|
||||||
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
|
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
|
||||||
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
|
||||||
}
|
}
|
||||||
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
|
|
||||||
$month_trxs_total = array_values($final_month_trxs_total);
|
$month_trxs_total = array_values($final_month_trxs_total);
|
||||||
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
||||||
|
|
||||||
@@ -875,9 +939,11 @@ class TransactionController extends Controller
|
|||||||
"warranty" => $request->warranty,
|
"warranty" => $request->warranty,
|
||||||
"user_sa_id" => $request->user_sa_id,
|
"user_sa_id" => $request->user_sa_id,
|
||||||
"date" => $request->date,
|
"date" => $request->date,
|
||||||
"status" => 'completed', // Mark as completed to trigger stock reduction
|
"status" => 0, // pending (0) - Mark as pending initially
|
||||||
"created_at" => date('Y-m-d H:i:s'),
|
"created_at" => date('Y-m-d H:i:s'),
|
||||||
"updated_at" => date('Y-m-d H:i:s')
|
"updated_at" => date('Y-m-d H:i:s'),
|
||||||
|
"claimed_at" => Auth::user()->role_id == 3 ? now() : null,
|
||||||
|
"claimed_by" => Auth::user()->role_id == 3 ? Auth::user()->id : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
$data[] = $transactionData;
|
$data[] = $transactionData;
|
||||||
@@ -898,6 +964,10 @@ class TransactionController extends Controller
|
|||||||
$this->stockService->reduceStockForTransaction($transaction);
|
$this->stockService->reduceStockForTransaction($transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate KPI achievement after creating transactions
|
||||||
|
$kpiService = app(\App\Services\KpiService::class);
|
||||||
|
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
||||||
|
|
||||||
@@ -922,19 +992,31 @@ class TransactionController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
Transaction::find($id)->update([
|
$request->validate([
|
||||||
|
'spk' => 'required|string|max:255',
|
||||||
|
'date' => 'required|date',
|
||||||
|
'police_number' => 'required|string|max:255',
|
||||||
|
'work_id' => 'required|exists:works,id',
|
||||||
|
'qty' => 'required|integer|min:1',
|
||||||
|
'warranty' => 'required|in:0,1',
|
||||||
|
'user_sa_id' => 'required|exists:users,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transaction = Transaction::findOrFail($id);
|
||||||
|
|
||||||
|
$transaction->update([
|
||||||
"spk" => $request->spk,
|
"spk" => $request->spk,
|
||||||
"date" => $request->date,
|
"date" => $request->date,
|
||||||
"police_number" => $request->police_number,
|
"police_number" => $request->police_number,
|
||||||
"work_id" => $request->work_id,
|
"work_id" => $request->work_id,
|
||||||
"qty" => $request->qty,
|
"qty" => $request->qty,
|
||||||
"warranty" => $request->warranty,
|
"warranty" => $request->warranty,
|
||||||
"user_sa_id" => $request->sa_id,
|
"user_sa_id" => $request->user_sa_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = [
|
$response = [
|
||||||
"status" => 200,
|
"status" => 200,
|
||||||
"message" => "Data updated successfully"
|
"message" => "Transaksi berhasil diperbarui"
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json($response);
|
return response()->json($response);
|
||||||
@@ -997,4 +1079,316 @@ class TransactionController extends Controller
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get claim transactions for DataTable - Only for mechanics
|
||||||
|
*/
|
||||||
|
public function getClaimTransactions(Request $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'dealer_id' => 'required|exists:dealers,id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$query = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
|
||||||
|
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||||
|
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||||
|
->select([
|
||||||
|
'transactions.id',
|
||||||
|
'transactions.date',
|
||||||
|
'transactions.spk',
|
||||||
|
'transactions.police_number',
|
||||||
|
'transactions.qty',
|
||||||
|
'transactions.status',
|
||||||
|
'transactions.claimed_at',
|
||||||
|
'transactions.claimed_by',
|
||||||
|
'w.name as work_name',
|
||||||
|
'sa.name as sa_name',
|
||||||
|
'users.name as mechanic_name'
|
||||||
|
])
|
||||||
|
->whereNull('transactions.deleted_at')
|
||||||
|
->where('transactions.dealer_id', $request->dealer_id)
|
||||||
|
->whereIn('transactions.status', [0, 1]) // Only pending and completed transactions
|
||||||
|
->orderBy('transactions.date', 'desc');
|
||||||
|
|
||||||
|
// Handle DataTables server-side processing
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
if ($request->has('search') && !empty($request->search['value'])) {
|
||||||
|
$searchValue = $request->search['value'];
|
||||||
|
$query->where(function($q) use ($searchValue) {
|
||||||
|
$q->where('transactions.spk', 'like', "%{$searchValue}%")
|
||||||
|
->orWhere('transactions.police_number', 'like', "%{$searchValue}%")
|
||||||
|
->orWhere('w.name', 'like', "%{$searchValue}%")
|
||||||
|
->orWhere('sa.name', 'like', "%{$searchValue}%")
|
||||||
|
->orWhere('users.name', 'like', "%{$searchValue}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredTotal = $query->count();
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$start = $request->input('start', 0);
|
||||||
|
$length = $request->input('length', 15);
|
||||||
|
$query->skip($start)->take($length);
|
||||||
|
|
||||||
|
$transactions = $query->get();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach ($transactions as $transaction) {
|
||||||
|
$data[] = [
|
||||||
|
'id' => $transaction->id,
|
||||||
|
'date' => date('d/m/Y', strtotime($transaction->date)),
|
||||||
|
'spk' => $transaction->spk,
|
||||||
|
'police_number' => $transaction->police_number,
|
||||||
|
'work_name' => $transaction->work_name,
|
||||||
|
'qty' => number_format($transaction->qty),
|
||||||
|
'sa_name' => $transaction->sa_name,
|
||||||
|
'status' => $this->getStatusBadge($transaction->status),
|
||||||
|
'action' => $this->getActionButtons($transaction),
|
||||||
|
'action_precheck' => $this->getActionButtonsPrecheck($transaction),
|
||||||
|
'action_postcheck' => $this->getActionButtonsPostcheck($transaction),
|
||||||
|
'claimed_at' => $transaction->claimed_at,
|
||||||
|
'claimed_by' => $transaction->claimed_by
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => intval($request->input('draw')),
|
||||||
|
'recordsTotal' => $total,
|
||||||
|
'recordsFiltered' => $filteredTotal,
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'Error fetching claim transactions: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge HTML
|
||||||
|
*/
|
||||||
|
private function getStatusBadge($status)
|
||||||
|
{
|
||||||
|
switch ($status) {
|
||||||
|
case 0: // pending
|
||||||
|
return '<span class="badge badge-warning">Menunggu</span>';
|
||||||
|
case 1: // completed
|
||||||
|
return '<span class="badge badge-success">Closed</span>';
|
||||||
|
default:
|
||||||
|
return '<span class="badge badge-secondary">Tidak Diketahui</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim a transaction - Only for mechanics
|
||||||
|
*/
|
||||||
|
public function claim($id)
|
||||||
|
{
|
||||||
|
// Only allow mechanics to claim transactions
|
||||||
|
if (Auth::user()->role_id != 3) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 403,
|
||||||
|
'message' => 'Hanya mekanik yang dapat mengklaim pekerjaan'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$transaction = Transaction::whereNull('deleted_at')->find($id);
|
||||||
|
|
||||||
|
if (!$transaction) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 404,
|
||||||
|
'message' => 'Transaksi tidak ditemukan'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transaction belongs to current user's dealer
|
||||||
|
if ($transaction->dealer_id !== Auth::user()->dealer_id) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 403,
|
||||||
|
'message' => 'Anda tidak memiliki akses ke transaksi ini'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transaction can be claimed (pending or completed)
|
||||||
|
if (!in_array($transaction->status, [0, 1])) { // pending (0) and completed (1)
|
||||||
|
return response()->json([
|
||||||
|
'status' => 400,
|
||||||
|
'message' => 'Hanya transaksi yang menunggu atau sudah selesai yang dapat diklaim'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transaction is already claimed
|
||||||
|
if (!empty($transaction->claimed_at) || !empty($transaction->claimed_by)) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 400,
|
||||||
|
'message' => 'Transaksi ini sudah diklaim sebelumnya'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transaction with claim information
|
||||||
|
$transaction->update([
|
||||||
|
'claimed_at' => now(),
|
||||||
|
'claimed_by' => Auth::user()->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Only recalculate KPI if this is a manual claim (not auto-claimed during creation)
|
||||||
|
// Auto-claimed transactions during creation already have KPI calculated in store method
|
||||||
|
$kpiService = app(\App\Services\KpiService::class);
|
||||||
|
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'message' => 'Pekerjaan berhasil diklaim'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Gagal mengklaim pekerjaan: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get action buttons HTML for claim transactions - Only for mechanics
|
||||||
|
*/
|
||||||
|
private function getActionButtons($transaction)
|
||||||
|
{
|
||||||
|
$buttons = '';
|
||||||
|
|
||||||
|
// Edit button - show for all users (not just mechanics)
|
||||||
|
$buttons .= '<button class="btn btn-sm btn-warning mr-1"
|
||||||
|
data-action="' . route('transaction.update', $transaction->id) . '"
|
||||||
|
data-url="' . route('transaction.edit', $transaction->id) . '"
|
||||||
|
onclick="editTransaction(' . $transaction->id . ')"
|
||||||
|
id="editTransaction' . $transaction->id . '"
|
||||||
|
title="Edit Transaksi"
|
||||||
|
style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>';
|
||||||
|
|
||||||
|
// Delete button - show for all users
|
||||||
|
$buttons .= '<button class="btn btn-sm btn-danger mr-1"
|
||||||
|
onclick="deleteTransaction(' . $transaction->id . ')"
|
||||||
|
title="Hapus Transaksi"
|
||||||
|
style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-trash"></i> Hapus
|
||||||
|
</button>';
|
||||||
|
|
||||||
|
// Claim button - show only if not claimed yet
|
||||||
|
if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) {
|
||||||
|
$buttons .= '<button class="btn btn-sm btn-success mr-1" onclick="claimTransaction(' . $transaction->id . ')" title="Klaim Pekerjaan" style="font-size: 11px; padding: 4px 8px;">';
|
||||||
|
$buttons .= '<i class="fas fa-hand-paper"></i> Klaim';
|
||||||
|
$buttons .= '</button>';
|
||||||
|
} else {
|
||||||
|
if ($transaction->claimed_by == Auth::user()->id) {
|
||||||
|
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
|
||||||
|
$postcheck = Postcheck::where('transaction_id', $transaction->id)->first();
|
||||||
|
|
||||||
|
if ($precheck && $postcheck) {
|
||||||
|
$buttons .= '<span class="badge badge-success" style="font-size: 10px;"><i class="fas fa-check"></i> Selesai</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$buttons .= '<span class="badge badge-info" style="font-size: 10px;"><i class="fas fa-check-circle"></i> Sudah Diklaim</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getActionButtonsPrecheck($transaction)
|
||||||
|
{
|
||||||
|
$buttons = '';
|
||||||
|
|
||||||
|
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
|
||||||
|
|
||||||
|
if ($precheck) {
|
||||||
|
$buttons .= '<a href="' . route('prechecks.edit', [$transaction->id, $precheck->id]) . '"
|
||||||
|
class="btn btn-sm btn-warning mr-1" title="Edit Precheck" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>';
|
||||||
|
$buttons .= '<a href="' . route('prechecks.print', $transaction->id) . '"
|
||||||
|
class="btn btn-sm btn-primary mr-1" title="Lihat Precheck" target="_blank" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-eye"></i> Lihat
|
||||||
|
</a>';
|
||||||
|
} else {
|
||||||
|
if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) {
|
||||||
|
$buttons .= '<span class="badge badge-danger" style="font-size: 10px;">Transaksi Belum Diklaim</span>';
|
||||||
|
}else{
|
||||||
|
$buttons .= '<a href="' . route('prechecks.create', $transaction->id) . '"
|
||||||
|
class="btn btn-sm btn-success mr-1" title="Tambah Precheck" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-plus"></i> Tambah
|
||||||
|
</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getActionButtonsPostcheck($transaction)
|
||||||
|
{
|
||||||
|
$buttons = '';
|
||||||
|
$postcheck = Postcheck::where('transaction_id', $transaction->id)->first();
|
||||||
|
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
|
||||||
|
|
||||||
|
if($precheck){
|
||||||
|
if ($postcheck) {
|
||||||
|
$buttons .= '<a href="' . route('postchecks.edit', [$transaction->id, $postcheck->id]) . '"
|
||||||
|
class="btn btn-sm btn-warning mr-1" title="Edit Postcheck" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>';
|
||||||
|
$buttons .= '<a href="' . route('postchecks.print', $transaction->id) . '"
|
||||||
|
class="btn btn-sm btn-primary mr-1" title="Lihat Postcheck" target="_blank" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-eye"></i> Lihat
|
||||||
|
</a>';
|
||||||
|
} else {
|
||||||
|
$buttons .= '<a href="' . route('postchecks.create', $transaction->id) . '"
|
||||||
|
class="btn btn-sm btn-success mr-1" title="Tambah Postcheck" style="font-size: 11px; padding: 4px 8px;">
|
||||||
|
<i class="fas fa-plus"></i> Tambah
|
||||||
|
</a>';
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$buttons .= '<span class="badge badge-danger" style="font-size: 10px;">Precheck Belum Disimpan</span>';
|
||||||
|
}
|
||||||
|
return $buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI data for AJAX refresh
|
||||||
|
*/
|
||||||
|
public function getKpiData()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kpiService = app(\App\Services\KpiService::class);
|
||||||
|
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||||
|
|
||||||
|
$currentMonthName = now()->translatedFormat('F Y');
|
||||||
|
|
||||||
|
$kpiData = [
|
||||||
|
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||||
|
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||||
|
'percentage' => $kpiSummary['current_percentage'],
|
||||||
|
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||||
|
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||||
|
'period' => $currentMonthName,
|
||||||
|
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $kpiData
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error fetching KPI data: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
277
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
277
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Transactions;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Postcheck;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class PostchecksController extends Controller
|
||||||
|
{
|
||||||
|
public function create(Transaction $transaction)
|
||||||
|
{
|
||||||
|
$acConditions = Postcheck::getAcConditionOptions();
|
||||||
|
$blowerConditions = Postcheck::getBlowerConditionOptions();
|
||||||
|
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
|
||||||
|
$compressorConditions = Postcheck::getCompressorConditionOptions();
|
||||||
|
|
||||||
|
return view('transaction.postchecks.create', compact(
|
||||||
|
'transaction',
|
||||||
|
'acConditions',
|
||||||
|
'blowerConditions',
|
||||||
|
'evaporatorConditions',
|
||||||
|
'compressorConditions'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Transaction $transaction)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'kilometer' => 'required|numeric|min:0',
|
||||||
|
'pressure_high' => 'required|numeric|min:0',
|
||||||
|
'pressure_low' => 'nullable|numeric|min:0',
|
||||||
|
'cabin_temperature' => 'nullable|numeric',
|
||||||
|
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'ac_condition' => 'nullable|in:' . implode(',', Postcheck::getAcConditionOptions()),
|
||||||
|
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()),
|
||||||
|
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()),
|
||||||
|
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
|
||||||
|
'postcheck_notes' => 'nullable|string',
|
||||||
|
'front_image' => 'required|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'postcheck_by' => auth()->id(),
|
||||||
|
'postcheck_at' => now(),
|
||||||
|
'police_number' => $transaction->police_number,
|
||||||
|
'spk_number' => $transaction->spk,
|
||||||
|
'kilometer' => $request->kilometer,
|
||||||
|
'pressure_high' => $request->pressure_high,
|
||||||
|
'pressure_low' => $request->pressure_low,
|
||||||
|
'cabin_temperature' => $request->cabin_temperature,
|
||||||
|
'ac_condition' => $request->ac_condition,
|
||||||
|
'blower_condition' => $request->blower_condition,
|
||||||
|
'evaporator_condition' => $request->evaporator_condition,
|
||||||
|
'compressor_condition' => $request->compressor_condition,
|
||||||
|
'postcheck_notes' => $request->postcheck_notes,
|
||||||
|
];
|
||||||
|
// Handle file uploads securely
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
$storedPath = $this->processImageUpload($request, $field, $transaction);
|
||||||
|
if ($storedPath) {
|
||||||
|
$data[$field] = $storedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Postcheck::create($data);
|
||||||
|
return redirect()->route('transaction')->with('success', 'Postcheck berhasil disimpan');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Postcheck creation failed: ' . $e->getMessage());
|
||||||
|
return back()->withErrors(['error' => 'Gagal menyimpan data postcheck. Silakan coba lagi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Transaction $transaction, Postcheck $postcheck)
|
||||||
|
{
|
||||||
|
$acConditions = Postcheck::getAcConditionOptions();
|
||||||
|
$blowerConditions = Postcheck::getBlowerConditionOptions();
|
||||||
|
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
|
||||||
|
$compressorConditions = Postcheck::getCompressorConditionOptions();
|
||||||
|
|
||||||
|
return view('transaction.postchecks.edit', compact(
|
||||||
|
'transaction',
|
||||||
|
'postcheck',
|
||||||
|
'acConditions',
|
||||||
|
'blowerConditions',
|
||||||
|
'evaporatorConditions',
|
||||||
|
'compressorConditions'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Transaction $transaction, Postcheck $postcheck)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'kilometer' => 'required|numeric|min:0',
|
||||||
|
'pressure_high' => 'required|numeric|min:0',
|
||||||
|
'pressure_low' => 'nullable|numeric|min:0',
|
||||||
|
'cabin_temperature' => 'nullable|numeric',
|
||||||
|
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'ac_condition' => 'nullable|in:' . implode(',', Postcheck::getAcConditionOptions()),
|
||||||
|
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()),
|
||||||
|
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()),
|
||||||
|
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
|
||||||
|
'postcheck_notes' => 'nullable|string',
|
||||||
|
'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateData = [
|
||||||
|
'kilometer' => $request->kilometer,
|
||||||
|
'pressure_high' => $request->pressure_high,
|
||||||
|
'pressure_low' => $request->pressure_low,
|
||||||
|
'cabin_temperature' => $request->cabin_temperature,
|
||||||
|
'ac_condition' => $request->ac_condition,
|
||||||
|
'blower_condition' => $request->blower_condition,
|
||||||
|
'evaporator_condition' => $request->evaporator_condition,
|
||||||
|
'compressor_condition' => $request->compressor_condition,
|
||||||
|
'postcheck_notes' => $request->postcheck_notes,
|
||||||
|
];
|
||||||
|
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
$newPath = $this->processImageUpload($request, $field, $transaction);
|
||||||
|
if ($newPath) {
|
||||||
|
// delete old file if exists
|
||||||
|
if ($postcheck->{$field}) {
|
||||||
|
$this->deleteIfExists($postcheck->{$field});
|
||||||
|
}
|
||||||
|
$updateData[$field] = $newPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$postcheck->update($updateData);
|
||||||
|
return redirect()->route('transaction')->with('success', 'Postcheck berhasil diperbarui');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Postcheck update failed: ' . $e->getMessage());
|
||||||
|
return back()->withErrors(['error' => 'Gagal memperbarui data postcheck. Silakan coba lagi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print($transaction_id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$postcheck = Postcheck::where('transaction_id', $transaction_id)->firstOrFail();
|
||||||
|
|
||||||
|
return view('transaction.postchecks.print', compact('postcheck'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error printing postcheck: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Gagal membuka halaman print postcheck.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the base storage directory exists
|
||||||
|
*/
|
||||||
|
private function ensureStorageDirectoryExists()
|
||||||
|
{
|
||||||
|
$storagePath = storage_path('app/public');
|
||||||
|
|
||||||
|
if (!is_dir($storagePath)) {
|
||||||
|
if (!mkdir($storagePath, 0755, true)) {
|
||||||
|
Log::error('Failed to create storage directory: ' . $storagePath);
|
||||||
|
throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.');
|
||||||
|
}
|
||||||
|
Log::info('Created storage directory: ' . $storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if directory is writable
|
||||||
|
if (!is_writable($storagePath)) {
|
||||||
|
Log::error('Storage directory is not writable: ' . $storagePath);
|
||||||
|
throw new \Exception(
|
||||||
|
'Storage directory is not writable: ' . $storagePath . '. ' .
|
||||||
|
'Please run one of these commands from your project root: ' .
|
||||||
|
'1) php fix_permissions.php ' .
|
||||||
|
'2) chmod -R 775 storage/ ' .
|
||||||
|
'3) mkdir -p storage/app/public/transactions/{transaction_id}/postcheck'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can create subdirectories
|
||||||
|
$testDir = $storagePath . '/test_' . time();
|
||||||
|
if (!mkdir($testDir, 0755, true)) {
|
||||||
|
Log::error('Cannot create subdirectories in storage: ' . $storagePath);
|
||||||
|
throw new \Exception(
|
||||||
|
'Cannot create subdirectories in storage. ' .
|
||||||
|
'Please check permissions and run: php fix_permissions.php'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up test directory
|
||||||
|
rmdir($testDir);
|
||||||
|
Log::info('Storage directory is properly configured: ' . $storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Securely process image upload to prevent RCE.
|
||||||
|
* - Only allows jpeg and png
|
||||||
|
* - Generates safe filename
|
||||||
|
* - Validates actual image content using getimagesize
|
||||||
|
*/
|
||||||
|
private function processImageUpload(Request $request, string $field, Transaction $transaction): ?string
|
||||||
|
{
|
||||||
|
if (!($request->hasFile($field) && $request->file($field)->isValid())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file($field);
|
||||||
|
|
||||||
|
// Double-check mime type from PHP, disallow svg/gif
|
||||||
|
$allowedMimes = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
|
||||||
|
$mime = $file->getMimeType();
|
||||||
|
if (!array_key_exists($mime, $allowedMimes)) {
|
||||||
|
throw new \RuntimeException('Tipe file tidak diperbolehkan');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's a real image by reading dimensions
|
||||||
|
$imageInfo = @getimagesize($file->getRealPath());
|
||||||
|
if ($imageInfo === false) {
|
||||||
|
throw new \RuntimeException('File bukan gambar yang valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare directory
|
||||||
|
$directory = 'transactions/' . $transaction->id . '/postcheck';
|
||||||
|
$this->ensureStorageDirectoryExists();
|
||||||
|
if (!Storage::disk('public')->exists('transactions')) {
|
||||||
|
Storage::disk('public')->makeDirectory('transactions', 0755, true);
|
||||||
|
}
|
||||||
|
if (!Storage::disk('public')->exists('transactions/' . $transaction->id)) {
|
||||||
|
Storage::disk('public')->makeDirectory('transactions/' . $transaction->id, 0755, true);
|
||||||
|
}
|
||||||
|
if (!Storage::disk('public')->exists($directory)) {
|
||||||
|
Storage::disk('public')->makeDirectory($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe filename
|
||||||
|
$ext = $allowedMimes[$mime];
|
||||||
|
$filename = time() . '_' . bin2hex(random_bytes(6)) . '_' . $transaction->id . '_' . $field . '.' . $ext;
|
||||||
|
|
||||||
|
// Store
|
||||||
|
$path = $file->storeAs($directory, $filename, 'public');
|
||||||
|
Log::info('Secure image stored', ['field' => $field, 'path' => $path]);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from public storage if it exists
|
||||||
|
*/
|
||||||
|
private function deleteIfExists(string $path): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($path && Storage::disk('public')->exists($path)) {
|
||||||
|
Storage::disk('public')->delete($path);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Failed to delete old image', ['path' => $path, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
479
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
479
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Transactions;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Precheck;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class PrechecksController extends Controller
|
||||||
|
{
|
||||||
|
public function create(Transaction $transaction)
|
||||||
|
{
|
||||||
|
$acConditions = Precheck::getAcConditionOptions();
|
||||||
|
$blowerConditions = Precheck::getBlowerConditionOptions();
|
||||||
|
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
|
||||||
|
$compressorConditions = Precheck::getCompressorConditionOptions();
|
||||||
|
|
||||||
|
return view('transaction.prechecks.create', compact(
|
||||||
|
'transaction',
|
||||||
|
'acConditions',
|
||||||
|
'blowerConditions',
|
||||||
|
'evaporatorConditions',
|
||||||
|
'compressorConditions'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Transaction $transaction)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'kilometer' => 'required|numeric|min:0',
|
||||||
|
'pressure_high' => 'required|numeric|min:0',
|
||||||
|
'pressure_low' => 'nullable|numeric|min:0',
|
||||||
|
'cabin_temperature' => 'nullable|numeric',
|
||||||
|
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()),
|
||||||
|
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()),
|
||||||
|
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()),
|
||||||
|
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
|
||||||
|
'precheck_notes' => 'nullable|string',
|
||||||
|
'front_image' => 'required|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'precheck_by' => auth()->id(),
|
||||||
|
'precheck_at' => now(),
|
||||||
|
'police_number' => $transaction->police_number,
|
||||||
|
'spk_number' => $transaction->spk,
|
||||||
|
'kilometer' => $request->kilometer,
|
||||||
|
'pressure_high' => $request->pressure_high,
|
||||||
|
'pressure_low' => $request->pressure_low,
|
||||||
|
'cabin_temperature' => $request->cabin_temperature,
|
||||||
|
'ac_condition' => $request->ac_condition,
|
||||||
|
'blower_condition' => $request->blower_condition,
|
||||||
|
'evaporator_condition' => $request->evaporator_condition,
|
||||||
|
'compressor_condition' => $request->compressor_condition,
|
||||||
|
'precheck_notes' => $request->precheck_notes,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle file uploads
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
if ($request->hasFile($field) && $request->file($field)->isValid()) {
|
||||||
|
try {
|
||||||
|
$file = $request->file($field);
|
||||||
|
|
||||||
|
// Enhanced security validation
|
||||||
|
if (!$this->isValidImageFile($file)) {
|
||||||
|
return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename with transaction ID
|
||||||
|
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
// Create directory path: transactions/{transaction_id}/precheck/
|
||||||
|
$directory = 'transactions/' . $transaction->id . '/precheck';
|
||||||
|
|
||||||
|
// Ensure base storage directory exists
|
||||||
|
$this->ensureStorageDirectoryExists();
|
||||||
|
|
||||||
|
// Ensure transactions directory exists
|
||||||
|
if (!Storage::disk('public')->exists('transactions')) {
|
||||||
|
Storage::disk('public')->makeDirectory('transactions', 0755, true);
|
||||||
|
Log::info('Created transactions directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure transaction ID directory exists
|
||||||
|
$transactionDir = 'transactions/' . $transaction->id;
|
||||||
|
if (!Storage::disk('public')->exists($transactionDir)) {
|
||||||
|
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
|
||||||
|
Log::info('Created transaction directory: ' . $transactionDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure precheck directory exists
|
||||||
|
if (!Storage::disk('public')->exists($directory)) {
|
||||||
|
Storage::disk('public')->makeDirectory($directory, 0755, true);
|
||||||
|
Log::info('Created precheck directory: ' . $directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store file in organized directory structure
|
||||||
|
$path = $file->storeAs($directory, $filename, 'public');
|
||||||
|
|
||||||
|
// Store file path
|
||||||
|
$data[$field] = $path;
|
||||||
|
|
||||||
|
// Store metadata
|
||||||
|
$data[$field . '_metadata'] = [
|
||||||
|
'original_name' => $file->getClientOriginalName(),
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'mime_type' => $file->getMimeType(),
|
||||||
|
'uploaded_at' => now()->toISOString(),
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'filename' => $filename,
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::info('File uploaded successfully: ' . $path);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error for debugging
|
||||||
|
Log::error('File upload failed: ' . $e->getMessage(), [
|
||||||
|
'field' => $field,
|
||||||
|
'file' => $file->getClientOriginalName(),
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Precheck::create($data);
|
||||||
|
return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Precheck creation failed: ' . $e->getMessage());
|
||||||
|
return back()->withErrors(['error' => 'Gagal menyimpan data precheck. Silakan coba lagi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Transaction $transaction, Precheck $precheck)
|
||||||
|
{
|
||||||
|
|
||||||
|
$acConditions = Precheck::getAcConditionOptions();
|
||||||
|
$blowerConditions = Precheck::getBlowerConditionOptions();
|
||||||
|
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
|
||||||
|
$compressorConditions = Precheck::getCompressorConditionOptions();
|
||||||
|
|
||||||
|
return view('transaction.prechecks.edit', compact(
|
||||||
|
'transaction',
|
||||||
|
'precheck',
|
||||||
|
'acConditions',
|
||||||
|
'blowerConditions',
|
||||||
|
'evaporatorConditions',
|
||||||
|
'compressorConditions'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Transaction $transaction, Precheck $precheck)
|
||||||
|
{
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'kilometer' => 'required|numeric|min:0',
|
||||||
|
'pressure_high' => 'required|numeric|min:0',
|
||||||
|
'pressure_low' => 'nullable|numeric|min:0',
|
||||||
|
'cabin_temperature' => 'nullable|numeric',
|
||||||
|
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()),
|
||||||
|
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()),
|
||||||
|
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()),
|
||||||
|
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
|
||||||
|
'precheck_notes' => 'nullable|string',
|
||||||
|
'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'kilometer' => $request->kilometer,
|
||||||
|
'pressure_high' => $request->pressure_high,
|
||||||
|
'pressure_low' => $request->pressure_low,
|
||||||
|
'cabin_temperature' => $request->cabin_temperature,
|
||||||
|
'ac_condition' => $request->ac_condition,
|
||||||
|
'blower_condition' => $request->blower_condition,
|
||||||
|
'evaporator_condition' => $request->evaporator_condition,
|
||||||
|
'compressor_condition' => $request->compressor_condition,
|
||||||
|
'precheck_notes' => $request->precheck_notes,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle file uploads with security validation
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
if ($request->hasFile($field) && $request->file($field)->isValid()) {
|
||||||
|
try {
|
||||||
|
$file = $request->file($field);
|
||||||
|
|
||||||
|
// Enhanced security validation
|
||||||
|
if (!$this->isValidImageFile($file)) {
|
||||||
|
return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename with transaction ID
|
||||||
|
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
// Create directory path: transactions/{transaction_id}/precheck/
|
||||||
|
$directory = 'transactions/' . $transaction->id . '/precheck';
|
||||||
|
|
||||||
|
// Ensure base storage directory exists
|
||||||
|
$this->ensureStorageDirectoryExists();
|
||||||
|
|
||||||
|
// Ensure transactions directory exists
|
||||||
|
if (!Storage::disk('public')->exists('transactions')) {
|
||||||
|
Storage::disk('public')->makeDirectory('transactions', 0755, true);
|
||||||
|
Log::info('Created transactions directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure transaction ID directory exists
|
||||||
|
$transactionDir = 'transactions/' . $transaction->id;
|
||||||
|
if (!Storage::disk('public')->exists($transactionDir)) {
|
||||||
|
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
|
||||||
|
Log::info('Created transaction directory: ' . $transactionDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure precheck directory exists
|
||||||
|
if (!Storage::disk('public')->exists($directory)) {
|
||||||
|
Storage::disk('public')->makeDirectory($directory, 0755, true);
|
||||||
|
Log::info('Created precheck directory: ' . $directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old file if exists
|
||||||
|
if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) {
|
||||||
|
Storage::disk('public')->delete($precheck->$field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store file in organized directory structure
|
||||||
|
$path = $file->storeAs($directory, $filename, 'public');
|
||||||
|
|
||||||
|
// Store file path
|
||||||
|
$data[$field] = $path;
|
||||||
|
|
||||||
|
// Store metadata
|
||||||
|
$data[$field . '_metadata'] = [
|
||||||
|
'original_name' => $file->getClientOriginalName(),
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'mime_type' => $file->getMimeType(),
|
||||||
|
'uploaded_at' => now()->toISOString(),
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'filename' => $filename,
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::info('File uploaded successfully: ' . $path);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error for debugging
|
||||||
|
Log::error('File upload failed: ' . $e->getMessage(), [
|
||||||
|
'field' => $field,
|
||||||
|
'file' => $file->getClientOriginalName(),
|
||||||
|
'transaction_id' => $transaction->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$precheck->update($data);
|
||||||
|
return redirect()->route('transaction')->with('success', 'Precheck berhasil diperbarui');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Precheck update failed: ' . $e->getMessage());
|
||||||
|
return back()->withErrors(['error' => 'Gagal memperbarui data precheck. Silakan coba lagi.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print($transaction_id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$precheck = Precheck::where('transaction_id', $transaction_id)->firstOrFail();
|
||||||
|
return view('transaction.prechecks.print', compact('precheck'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error printing precheck: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Gagal membuka halaman print precheck.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the base storage directory exists
|
||||||
|
*/
|
||||||
|
private function ensureStorageDirectoryExists()
|
||||||
|
{
|
||||||
|
$storagePath = storage_path('app/public');
|
||||||
|
|
||||||
|
if (!is_dir($storagePath)) {
|
||||||
|
if (!mkdir($storagePath, 0755, true)) {
|
||||||
|
Log::error('Failed to create storage directory: ' . $storagePath);
|
||||||
|
throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.');
|
||||||
|
}
|
||||||
|
Log::info('Created storage directory: ' . $storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if directory is writable
|
||||||
|
if (!is_writable($storagePath)) {
|
||||||
|
Log::error('Storage directory is not writable: ' . $storagePath);
|
||||||
|
throw new \Exception(
|
||||||
|
'Storage directory is not writable: ' . $storagePath . '. ' .
|
||||||
|
'Please run one of these commands from your project root: ' .
|
||||||
|
'1) php fix_permissions.php ' .
|
||||||
|
'2) chmod -R 775 storage/ ' .
|
||||||
|
'3) mkdir -p storage/app/public/transactions/{transaction_id}/precheck'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can create subdirectories
|
||||||
|
$testDir = $storagePath . '/test_' . time();
|
||||||
|
if (!mkdir($testDir, 0755, true)) {
|
||||||
|
Log::error('Cannot create subdirectories in storage: ' . $storagePath);
|
||||||
|
throw new \Exception(
|
||||||
|
'Cannot create subdirectories in storage. ' .
|
||||||
|
'Please check permissions and run: php fix_permissions.php'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up test directory
|
||||||
|
rmdir($testDir);
|
||||||
|
Log::info('Storage directory is properly configured: ' . $storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced security validation for image files to prevent RCE attacks
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\UploadedFile $file
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isValidImageFile($file)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 1. Check file extension (whitelist approach)
|
||||||
|
$allowedExtensions = ['jpg', 'jpeg', 'png'];
|
||||||
|
$extension = strtolower($file->getClientOriginalExtension());
|
||||||
|
|
||||||
|
if (!in_array($extension, $allowedExtensions)) {
|
||||||
|
Log::warning('Invalid file extension: ' . $extension, [
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check MIME type
|
||||||
|
$allowedMimeTypes = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/png'
|
||||||
|
];
|
||||||
|
|
||||||
|
$mimeType = $file->getMimeType();
|
||||||
|
if (!in_array($mimeType, $allowedMimeTypes)) {
|
||||||
|
Log::warning('Invalid MIME type: ' . $mimeType, [
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify file is actually an image using getimagesize
|
||||||
|
$imageInfo = @getimagesize($file->getPathname());
|
||||||
|
if ($imageInfo === false) {
|
||||||
|
Log::warning('File is not a valid image: ' . $file->getClientOriginalName(), [
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check image dimensions (prevent extremely large images)
|
||||||
|
$maxWidth = 5000;
|
||||||
|
$maxHeight = 5000;
|
||||||
|
if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) {
|
||||||
|
Log::warning('Image dimensions too large: ' . $imageInfo[0] . 'x' . $imageInfo[1], [
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check file size (max 20MB)
|
||||||
|
$maxSize = 20 * 1024 * 1024; // 20MB
|
||||||
|
if ($file->getSize() > $maxSize) {
|
||||||
|
Log::warning('File size too large: ' . $file->getSize(), [
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Check for suspicious content in filename
|
||||||
|
$filename = $file->getClientOriginalName();
|
||||||
|
$suspiciousPatterns = [
|
||||||
|
'<?php', '<?=', '<script', 'javascript:', 'data:', 'vbscript:',
|
||||||
|
'..', '~', '$', '`', '|', '&', ';', '(', ')', '{', '}',
|
||||||
|
'exec', 'system', 'shell_exec', 'passthru', 'eval'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($suspiciousPatterns as $pattern) {
|
||||||
|
if (stripos($filename, $pattern) !== false) {
|
||||||
|
Log::warning('Suspicious filename pattern detected: ' . $pattern, [
|
||||||
|
'filename' => $filename,
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Additional security: Check file header (magic bytes)
|
||||||
|
$handle = fopen($file->getPathname(), 'rb');
|
||||||
|
if ($handle) {
|
||||||
|
$header = fread($handle, 8);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Check for valid image headers
|
||||||
|
$validHeaders = [
|
||||||
|
"\xFF\xD8\xFF", // JPEG
|
||||||
|
"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", // PNG
|
||||||
|
];
|
||||||
|
|
||||||
|
$isValidHeader = false;
|
||||||
|
foreach ($validHeaders as $validHeader) {
|
||||||
|
if (substr($header, 0, strlen($validHeader)) === $validHeader) {
|
||||||
|
$isValidHeader = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isValidHeader) {
|
||||||
|
Log::warning('Invalid file header detected', [
|
||||||
|
'filename' => $filename,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'header' => bin2hex($header)
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('File validation passed', [
|
||||||
|
'filename' => $filename,
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'user_id' => auth()->id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('File validation error: ' . $e->getMessage(), [
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ class OpnamesController extends Controller
|
|||||||
$dealers = Dealer::all();
|
$dealers = Dealer::all();
|
||||||
if($request->ajax()){
|
if($request->ajax()){
|
||||||
$data = Opname::query()
|
$data = Opname::query()
|
||||||
->with('user','dealer')
|
->with(['user','dealer', 'details.product'])
|
||||||
->orderBy('created_at', 'desc');
|
->orderBy('created_at', 'desc');
|
||||||
|
|
||||||
// Filter berdasarkan dealer yang dipilih
|
// Filter berdasarkan dealer yang dipilih
|
||||||
@@ -76,6 +76,46 @@ class OpnamesController extends Controller
|
|||||||
|
|
||||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||||
})
|
})
|
||||||
|
->addColumn('stock_info', function ($row) {
|
||||||
|
// Use eager loaded details
|
||||||
|
$details = $row->details;
|
||||||
|
|
||||||
|
if ($details->isEmpty()) {
|
||||||
|
return '<span class="text-muted">Tidak ada data</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalProducts = $details->count();
|
||||||
|
$matchingProducts = $details->where('difference', 0)->count();
|
||||||
|
$differentProducts = $totalProducts - $matchingProducts;
|
||||||
|
|
||||||
|
$info = [];
|
||||||
|
|
||||||
|
if ($matchingProducts > 0) {
|
||||||
|
$info[] = "<span class='text-success'><i class='fa fa-check-circle'></i> {$matchingProducts} sesuai</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($differentProducts > 0) {
|
||||||
|
// Get more details about differences
|
||||||
|
$positiveDiff = $details->where('difference', '>', 0)->count();
|
||||||
|
$negativeDiff = $details->where('difference', '<', 0)->count();
|
||||||
|
|
||||||
|
$diffInfo = [];
|
||||||
|
if ($positiveDiff > 0) {
|
||||||
|
$diffInfo[] = "+{$positiveDiff}";
|
||||||
|
}
|
||||||
|
if ($negativeDiff > 0) {
|
||||||
|
$diffInfo[] = "-{$negativeDiff}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$diffText = implode(', ', $diffInfo);
|
||||||
|
$info[] = "<span class='text-danger'><i class='fa fa-exclamation-triangle'></i> {$differentProducts} selisih ({$diffText})</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add total products info
|
||||||
|
$info[] = "<small class='text-muted'>(Total: {$totalProducts} produk)</small>";
|
||||||
|
|
||||||
|
return '<div class="stock-info-cell">' . implode('<br>', $info) . '</div>';
|
||||||
|
})
|
||||||
->addColumn('action', function ($row) use ($menu) {
|
->addColumn('action', function ($row) use ($menu) {
|
||||||
$btn = '<div class="d-flex">';
|
$btn = '<div class="d-flex">';
|
||||||
|
|
||||||
@@ -86,7 +126,7 @@ class OpnamesController extends Controller
|
|||||||
|
|
||||||
return $btn;
|
return $btn;
|
||||||
})
|
})
|
||||||
->rawColumns(['action', 'status'])
|
->rawColumns(['action', 'status', 'stock_info'])
|
||||||
->make(true);
|
->make(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +164,7 @@ class OpnamesController extends Controller
|
|||||||
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
||||||
|
|
||||||
if ($isTransactionForm) {
|
if ($isTransactionForm) {
|
||||||
// Custom validation for transaction form
|
// Simplified validation for transaction form
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'dealer_id' => 'required|exists:dealers,id',
|
'dealer_id' => 'required|exists:dealers,id',
|
||||||
'user_id' => 'required|exists:users,id',
|
'user_id' => 'required|exists:users,id',
|
||||||
@@ -140,7 +180,7 @@ class OpnamesController extends Controller
|
|||||||
'system_stock' => 'required|array',
|
'system_stock' => 'required|array',
|
||||||
'system_stock.*' => 'required|numeric|min:0',
|
'system_stock.*' => 'required|numeric|min:0',
|
||||||
'physical_stock' => 'required|array',
|
'physical_stock' => 'required|array',
|
||||||
'physical_stock.*' => 'required|numeric|min:0'
|
'physical_stock.*' => 'nullable|numeric|min:0'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process transaction form data with proper date parsing
|
// Process transaction form data with proper date parsing
|
||||||
@@ -199,19 +239,11 @@ class OpnamesController extends Controller
|
|||||||
$physicalStocks = $request->physical_quantity;
|
$physicalStocks = $request->physical_quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Validasi minimal ada produk yang diisi (termasuk nilai 0)
|
// 2. Simplified validation - all products are valid, set defaults for empty physical stocks
|
||||||
$validProductIds = array_filter($productIds);
|
$validProductIds = array_filter($productIds);
|
||||||
$validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; });
|
|
||||||
$validPhysicalStocks = array_filter($physicalStocks, function($value) {
|
|
||||||
return $value !== null && $value !== '' && is_numeric($value);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($validProductIds) || count($validProductIds) === 0) {
|
if (empty($validProductIds) || count($validProductIds) === 0) {
|
||||||
throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.');
|
throw new \Exception('Minimal harus ada satu produk untuk opname.');
|
||||||
}
|
|
||||||
|
|
||||||
if (count($validPhysicalStocks) === 0) {
|
|
||||||
throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validasi duplikasi produk
|
// 3. Validasi duplikasi produk
|
||||||
@@ -283,19 +315,14 @@ class OpnamesController extends Controller
|
|||||||
foreach ($productIds as $index => $productId) {
|
foreach ($productIds as $index => $productId) {
|
||||||
if (!$productId) continue;
|
if (!$productId) continue;
|
||||||
|
|
||||||
// Skip only if physical stock is truly not provided (empty string or null)
|
// Set default value to 0 if physical stock is empty or invalid
|
||||||
// Accept 0 as valid input
|
$physicalStockValue = $physicalStocks[$index] ?? null;
|
||||||
if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) {
|
if ($physicalStockValue === '' || $physicalStockValue === null || !is_numeric($physicalStockValue)) {
|
||||||
continue;
|
$physicalStockValue = 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that physical stock is numeric (including 0)
|
|
||||||
if (!is_numeric($physicalStocks[$index])) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||||
$physicalStock = floatval($physicalStocks[$index]);
|
$physicalStock = floatval($physicalStockValue);
|
||||||
$difference = $physicalStock - $systemStock;
|
$difference = $physicalStock - $systemStock;
|
||||||
|
|
||||||
$processedCount++;
|
$processedCount++;
|
||||||
@@ -337,7 +364,7 @@ class OpnamesController extends Controller
|
|||||||
|
|
||||||
// Validate we have at least one detail to insert
|
// Validate we have at least one detail to insert
|
||||||
if (empty($details)) {
|
if (empty($details)) {
|
||||||
throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.');
|
throw new \Exception('Tidak ada data produk yang valid untuk diproses.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk insert untuk performa lebih baik
|
// Bulk insert untuk performa lebih baik
|
||||||
@@ -371,13 +398,13 @@ class OpnamesController extends Controller
|
|||||||
// Redirect back to transaction page with success message and tab indicator
|
// Redirect back to transaction page with success message and tab indicator
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('transaction')
|
->route('transaction')
|
||||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.")
|
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.")
|
||||||
->with('active_tab', 'opname');
|
->with('active_tab', 'opname');
|
||||||
} else {
|
} else {
|
||||||
// Redirect to opname index for regular form
|
// Redirect to opname index for regular form
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('opnames.index')
|
->route('opnames.index')
|
||||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ class WorkController extends Controller
|
|||||||
</a>';
|
</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Prices Button
|
||||||
|
if(Gate::allows('view', $menu)) {
|
||||||
|
$btn .= '<a href="'. route('work.set-prices', ['work' => $row->work_id]) .'" class="btn btn-primary btn-sm" title="Set Harga per Dealer">
|
||||||
|
Harga
|
||||||
|
</a>';
|
||||||
|
}
|
||||||
|
|
||||||
if(Gate::allows('update', $menu)) {
|
if(Gate::allows('update', $menu)) {
|
||||||
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
||||||
Edit
|
Edit
|
||||||
@@ -157,4 +164,20 @@ class WorkController extends Controller
|
|||||||
|
|
||||||
return response()->json($response);
|
return response()->json($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for setting prices per dealer for a specific work.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Work $work
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function showPrices(Work $work)
|
||||||
|
{
|
||||||
|
$menu = Menu::where('link', 'work.index')->first();
|
||||||
|
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||||
|
|
||||||
|
$dealers = \App\Models\Dealer::all();
|
||||||
|
|
||||||
|
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Work;
|
||||||
|
use App\Models\Dealer;
|
||||||
|
use App\Models\WorkDealerPrice;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Yajra\DataTables\DataTables;
|
||||||
|
|
||||||
|
class WorkDealerPriceController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of work prices for a specific work
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Work $work)
|
||||||
|
{
|
||||||
|
if ($request->ajax()) {
|
||||||
|
$data = WorkDealerPrice::with(['dealer'])
|
||||||
|
->where('work_id', $work->id)
|
||||||
|
->select('work_dealer_prices.*');
|
||||||
|
|
||||||
|
return DataTables::of($data)
|
||||||
|
->addIndexColumn()
|
||||||
|
->addColumn('dealer_name', function($row) {
|
||||||
|
return $row->dealer->name;
|
||||||
|
})
|
||||||
|
->addColumn('formatted_price', function($row) {
|
||||||
|
return $row->formatted_price;
|
||||||
|
})
|
||||||
|
->addColumn('action', function($row) {
|
||||||
|
$btn = '<div class="d-flex flex-row gap-1">';
|
||||||
|
$btn .= '<button class="btn btn-warning btn-sm" onclick="editPrice(' . $row->id . ')" title="Edit Harga">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</button>';
|
||||||
|
$btn .= '<button class="btn btn-danger btn-sm" onclick="deletePrice(' . $row->id . ')" title="Hapus Harga">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>';
|
||||||
|
$btn .= '</div>';
|
||||||
|
return $btn;
|
||||||
|
})
|
||||||
|
->rawColumns(['action'])
|
||||||
|
->make(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dealers = Dealer::all();
|
||||||
|
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created price
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'work_id' => 'required|exists:works,id',
|
||||||
|
'dealer_id' => 'required|exists:dealers,id',
|
||||||
|
'price' => 'required|numeric|min:0',
|
||||||
|
'currency' => 'required|string|max:3',
|
||||||
|
'is_active' => 'nullable|in:0,1',
|
||||||
|
], [
|
||||||
|
'work_id.required' => 'ID pekerjaan harus diisi',
|
||||||
|
'work_id.exists' => 'Pekerjaan tidak ditemukan',
|
||||||
|
'dealer_id.required' => 'ID dealer harus diisi',
|
||||||
|
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||||
|
'price.required' => 'Harga harus diisi',
|
||||||
|
'price.numeric' => 'Harga harus berupa angka',
|
||||||
|
'price.min' => 'Harga minimal 0',
|
||||||
|
'currency.required' => 'Mata uang harus diisi',
|
||||||
|
'currency.max' => 'Mata uang maksimal 3 karakter',
|
||||||
|
'is_active.in' => 'Status aktif harus 0 atau 1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if price already exists for this work-dealer combination (including soft deleted)
|
||||||
|
$existingPrice = WorkDealerPrice::withTrashed()
|
||||||
|
->where('work_id', $request->work_id)
|
||||||
|
->where('dealer_id', $request->dealer_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Also check for active records to prevent duplicates
|
||||||
|
$activePrice = WorkDealerPrice::where('work_id', $request->work_id)
|
||||||
|
->where('dealer_id', $request->dealer_id)
|
||||||
|
->where('id', '!=', $existingPrice ? $existingPrice->id : 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($activePrice) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 422,
|
||||||
|
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use database transaction to prevent race conditions
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
|
||||||
|
if ($existingPrice) {
|
||||||
|
if ($existingPrice->trashed()) {
|
||||||
|
// Restore soft deleted record and update
|
||||||
|
$existingPrice->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing price
|
||||||
|
$existingPrice->update([
|
||||||
|
'price' => $request->price,
|
||||||
|
'currency' => $request->currency,
|
||||||
|
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||||
|
]);
|
||||||
|
$price = $existingPrice;
|
||||||
|
|
||||||
|
$message = 'Harga berhasil diperbarui';
|
||||||
|
} else {
|
||||||
|
// Create new price
|
||||||
|
$price = WorkDealerPrice::create([
|
||||||
|
'work_id' => $request->work_id,
|
||||||
|
'dealer_id' => $request->dealer_id,
|
||||||
|
'price' => $request->price,
|
||||||
|
'currency' => $request->currency,
|
||||||
|
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = 'Harga berhasil disimpan';
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'data' => $price,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 422,
|
||||||
|
'message' => 'Validasi gagal',
|
||||||
|
'errors' => $e->errors()
|
||||||
|
], 422);
|
||||||
|
} catch (\Illuminate\Database\QueryException $e) {
|
||||||
|
// Handle unique constraint violation
|
||||||
|
if ($e->getCode() == 23000) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 422,
|
||||||
|
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Terjadi kesalahan database: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified price
|
||||||
|
*/
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$price = WorkDealerPrice::with(['work', 'dealer'])->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'data' => $price,
|
||||||
|
'message' => 'Data harga berhasil diambil'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified price
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'price' => 'required|numeric|min:0',
|
||||||
|
'currency' => 'required|string|max:3',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$price = WorkDealerPrice::findOrFail($id);
|
||||||
|
$price->update($request->all());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'message' => 'Harga berhasil diperbarui'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified price
|
||||||
|
*/
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$price = WorkDealerPrice::findOrFail($id);
|
||||||
|
$price->delete(); // Soft delete
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'message' => 'Harga berhasil dihapus'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Terjadi kesalahan saat menghapus harga: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for specific work and dealer
|
||||||
|
*/
|
||||||
|
public function getPrice(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'work_id' => 'required|exists:works,id',
|
||||||
|
'dealer_id' => 'required|exists:dealers,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$price = WorkDealerPrice::getPriceForWorkAndDealer(
|
||||||
|
$request->work_id,
|
||||||
|
$request->dealer_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'data' => $price,
|
||||||
|
'message' => $price ? 'Harga ditemukan' : 'Harga tidak ditemukan'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle status of a price
|
||||||
|
*/
|
||||||
|
public function toggleStatus(Request $request, Work $work)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'dealer_id' => 'required|exists:dealers,id',
|
||||||
|
'is_active' => 'required|in:0,1,true,false',
|
||||||
|
], [
|
||||||
|
'dealer_id.required' => 'ID dealer harus diisi',
|
||||||
|
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||||
|
'is_active.required' => 'Status aktif harus diisi',
|
||||||
|
'is_active.in' => 'Status aktif harus 0, 1, true, atau false',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert string values to boolean
|
||||||
|
$isActive = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
|
||||||
|
// Find existing price (including soft deleted)
|
||||||
|
$existingPrice = WorkDealerPrice::withTrashed()
|
||||||
|
->where('work_id', $work->id)
|
||||||
|
->where('dealer_id', $request->dealer_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$existingPrice) {
|
||||||
|
// Create new record with default price 0 if no record exists
|
||||||
|
$existingPrice = WorkDealerPrice::create([
|
||||||
|
'work_id' => $work->id,
|
||||||
|
'dealer_id' => $request->dealer_id,
|
||||||
|
'price' => 0,
|
||||||
|
'currency' => 'IDR',
|
||||||
|
'is_active' => $isActive,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Restore if soft deleted
|
||||||
|
if ($existingPrice->trashed()) {
|
||||||
|
$existingPrice->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
$existingPrice->update([
|
||||||
|
'is_active' => $isActive
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'data' => $existingPrice,
|
||||||
|
'message' => 'Status berhasil diubah menjadi ' . ($isActive ? 'Aktif' : 'Nonaktif')
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 422,
|
||||||
|
'message' => 'Validasi gagal',
|
||||||
|
'errors' => $e->errors()
|
||||||
|
], 422);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk create prices for a work
|
||||||
|
*/
|
||||||
|
public function bulkCreate(Request $request, Work $work)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'prices' => 'required|array',
|
||||||
|
'prices.*.dealer_id' => 'required|exists:dealers,id',
|
||||||
|
'prices.*.price' => 'required|numeric|min:0',
|
||||||
|
'prices.*.currency' => 'required|string|max:3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
foreach ($request->prices as $priceData) {
|
||||||
|
// Check if price already exists
|
||||||
|
$existingPrice = WorkDealerPrice::where('work_id', $work->id)
|
||||||
|
->where('dealer_id', $priceData['dealer_id'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingPrice) {
|
||||||
|
// Update existing price
|
||||||
|
$existingPrice->update([
|
||||||
|
'price' => $priceData['price'],
|
||||||
|
'currency' => $priceData['currency'],
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Create new price
|
||||||
|
WorkDealerPrice::create([
|
||||||
|
'work_id' => $work->id,
|
||||||
|
'dealer_id' => $priceData['dealer_id'],
|
||||||
|
'price' => $priceData['price'],
|
||||||
|
'currency' => $priceData['currency'],
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 200,
|
||||||
|
'message' => 'Harga berhasil disimpan'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
return response()->json([
|
||||||
|
'status' => 500,
|
||||||
|
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ class adminRole
|
|||||||
{
|
{
|
||||||
// check if user can access admin area
|
// check if user can access admin area
|
||||||
$user = Privilege::join('menus AS m', 'm.id', '=', 'privileges.menu_id')->where('m.link', 'adminarea')->where('role_id', Auth::user()->role_id)->where('view', 1)->get();
|
$user = Privilege::join('menus AS m', 'm.id', '=', 'privileges.menu_id')->where('m.link', 'adminarea')->where('role_id', Auth::user()->role_id)->where('view', 1)->get();
|
||||||
// dd($user);
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|||||||
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\KPI;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class StoreKpiTargetRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => 'required|exists:users,id',
|
||||||
|
'target_value' => 'required|integer|min:1',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id.required' => 'Mekanik harus dipilih',
|
||||||
|
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
|
||||||
|
'target_value.required' => 'Target nilai harus diisi',
|
||||||
|
'target_value.integer' => 'Target nilai harus berupa angka',
|
||||||
|
'target_value.min' => 'Target nilai minimal 1',
|
||||||
|
'description.max' => 'Deskripsi maksimal 1000 karakter',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the data for validation.
|
||||||
|
*/
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'is_active' => $this->boolean('is_active', true)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\KPI;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class UpdateKpiTargetRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => 'required|exists:users,id',
|
||||||
|
'target_value' => 'required|integer|min:1',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id.required' => 'Mekanik harus dipilih',
|
||||||
|
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
|
||||||
|
'target_value.required' => 'Target nilai harus diisi',
|
||||||
|
'target_value.integer' => 'Target nilai harus berupa angka',
|
||||||
|
'target_value.min' => 'Target nilai minimal 1',
|
||||||
|
'description.max' => 'Deskripsi maksimal 1000 karakter',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the data for validation.
|
||||||
|
*/
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'is_active' => $this->boolean('is_active', true)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -48,4 +48,43 @@ class Dealer extends Model
|
|||||||
->withPivot('quantity')
|
->withPivot('quantity')
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all work prices for this dealer
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function workPrices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WorkDealerPrice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for specific work
|
||||||
|
*
|
||||||
|
* @param int $workId
|
||||||
|
* @return WorkDealerPrice|null
|
||||||
|
*/
|
||||||
|
public function getPriceForWork($workId)
|
||||||
|
{
|
||||||
|
return $this->workPrices()
|
||||||
|
->where('work_id', $workId)
|
||||||
|
->active()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active work prices for this dealer
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function activeWorkPrices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function roles()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Role::class, 'role_dealer');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
168
app/Models/KpiAchievement.php
Normal file
168
app/Models/KpiAchievement.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class KpiAchievement extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'kpi_target_id',
|
||||||
|
'target_value',
|
||||||
|
'actual_value',
|
||||||
|
'achievement_percentage',
|
||||||
|
'year',
|
||||||
|
'month',
|
||||||
|
'notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'achievement_percentage' => 'decimal:2',
|
||||||
|
'year' => 'integer',
|
||||||
|
'month' => 'integer'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'actual_value' => 0,
|
||||||
|
'achievement_percentage' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the achievement
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the KPI target for this achievement
|
||||||
|
*/
|
||||||
|
public function kpiTarget(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(KpiTarget::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get achievements for specific year and month
|
||||||
|
*/
|
||||||
|
public function scopeForPeriod($query, $year, $month)
|
||||||
|
{
|
||||||
|
return $query->where('year', $year)->where('month', $month);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get achievements for current month
|
||||||
|
*/
|
||||||
|
public function scopeCurrentMonth($query)
|
||||||
|
{
|
||||||
|
return $query->where('year', now()->year)->where('month', now()->month);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get achievements within year range
|
||||||
|
*/
|
||||||
|
public function scopeWithinYearRange($query, $startYear, $endYear)
|
||||||
|
{
|
||||||
|
return $query->whereBetween('year', [$startYear, $endYear]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get achievements for specific user
|
||||||
|
*/
|
||||||
|
public function scopeForUser($query, $userId)
|
||||||
|
{
|
||||||
|
return $query->where('user_id', $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get achievement status
|
||||||
|
*/
|
||||||
|
public function getStatusAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->achievement_percentage >= 100) {
|
||||||
|
return 'exceeded';
|
||||||
|
} elseif ($this->achievement_percentage >= 80) {
|
||||||
|
return 'good';
|
||||||
|
} elseif ($this->achievement_percentage >= 60) {
|
||||||
|
return 'fair';
|
||||||
|
} else {
|
||||||
|
return 'poor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status color for display
|
||||||
|
*/
|
||||||
|
public function getStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->status) {
|
||||||
|
'exceeded' => 'success',
|
||||||
|
'good' => 'info',
|
||||||
|
'fair' => 'warning',
|
||||||
|
'poor' => 'danger',
|
||||||
|
default => 'secondary'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get period display name (e.g., "Januari 2024")
|
||||||
|
*/
|
||||||
|
public function getPeriodDisplayName(): string
|
||||||
|
{
|
||||||
|
$monthNames = [
|
||||||
|
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||||
|
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||||
|
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $monthNames[$this->month] . ' ' . $this->year;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get period start date
|
||||||
|
*/
|
||||||
|
public function getPeriodStartDate(): Carbon
|
||||||
|
{
|
||||||
|
return Carbon::createFromDate($this->year, $this->month, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get period end date
|
||||||
|
*/
|
||||||
|
public function getPeriodEndDate(): Carbon
|
||||||
|
{
|
||||||
|
return Carbon::createFromDate($this->year, $this->month, 1)->endOfMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get target value (from stored value or from relation)
|
||||||
|
*/
|
||||||
|
public function getTargetValueAttribute(): int
|
||||||
|
{
|
||||||
|
// Return stored target value if available, otherwise get from relation
|
||||||
|
return $this->target_value ?? $this->kpiTarget?->target_value ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current target value from relation (for comparison)
|
||||||
|
*/
|
||||||
|
public function getCurrentTargetValueAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->kpiTarget?->target_value ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if stored target value differs from current target value
|
||||||
|
*/
|
||||||
|
public function hasTargetValueChanged(): bool
|
||||||
|
{
|
||||||
|
return $this->target_value !== $this->current_target_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Models/KpiTarget.php
Normal file
61
app/Models/KpiTarget.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class KpiTarget extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'target_value',
|
||||||
|
'is_active',
|
||||||
|
'description'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'is_active' => true
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the KPI target
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the achievements for this target
|
||||||
|
*/
|
||||||
|
public function achievements(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(KpiAchievement::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get active targets
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if target is currently active
|
||||||
|
*/
|
||||||
|
public function isCurrentlyActive(): bool
|
||||||
|
{
|
||||||
|
return $this->is_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
210
app/Models/Postcheck.php
Normal file
210
app/Models/Postcheck.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Postcheck extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'transaction_id',
|
||||||
|
'postcheck_by',
|
||||||
|
'postcheck_at',
|
||||||
|
'police_number',
|
||||||
|
'spk_number',
|
||||||
|
'front_image',
|
||||||
|
'front_image_metadata',
|
||||||
|
'kilometer',
|
||||||
|
'pressure_high',
|
||||||
|
'pressure_low',
|
||||||
|
'cabin_temperature',
|
||||||
|
'cabin_temperature_image',
|
||||||
|
'cabin_temperature_image_metadata',
|
||||||
|
'ac_condition',
|
||||||
|
'ac_image',
|
||||||
|
'ac_image_metadata',
|
||||||
|
'blower_condition',
|
||||||
|
'blower_image',
|
||||||
|
'blower_image_metadata',
|
||||||
|
'evaporator_condition',
|
||||||
|
'evaporator_image',
|
||||||
|
'evaporator_image_metadata',
|
||||||
|
'compressor_condition',
|
||||||
|
'postcheck_notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'postcheck_at' => 'datetime',
|
||||||
|
'kilometer' => 'decimal:2',
|
||||||
|
'pressure_high' => 'decimal:2',
|
||||||
|
'pressure_low' => 'decimal:2',
|
||||||
|
'cabin_temperature' => 'decimal:2',
|
||||||
|
'front_image_metadata' => 'array',
|
||||||
|
'cabin_temperature_image_metadata' => 'array',
|
||||||
|
'ac_image_metadata' => 'array',
|
||||||
|
'blower_image_metadata' => 'array',
|
||||||
|
'evaporator_image_metadata' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the transaction associated with the Postcheck
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function transaction()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Transaction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who performed the postcheck
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function postcheckBy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'postcheck_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get front image URL
|
||||||
|
*/
|
||||||
|
public function getFrontImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->front_image ? asset('storage/' . ltrim($this->front_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cabin temperature image URL
|
||||||
|
*/
|
||||||
|
public function getCabinTemperatureImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->cabin_temperature_image ? asset('storage/' . ltrim($this->cabin_temperature_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AC image URL
|
||||||
|
*/
|
||||||
|
public function getAcImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->ac_image ? asset('storage/' . ltrim($this->ac_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blower image URL
|
||||||
|
*/
|
||||||
|
public function getBlowerImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->blower_image ? asset('storage/' . ltrim($this->blower_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get evaporator image URL
|
||||||
|
*/
|
||||||
|
public function getEvaporatorImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->evaporator_image ? asset('storage/' . ltrim($this->evaporator_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete associated files when model is deleted
|
||||||
|
*/
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::deleting(function ($postcheck) {
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
if ($postcheck->$field && Storage::disk('public')->exists($postcheck->$field)) {
|
||||||
|
Storage::disk('public')->delete($postcheck->$field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the AC condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAcConditionOptions()
|
||||||
|
{
|
||||||
|
return ['sudah dikerjakan', 'sudah diganti'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the blower condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getBlowerConditionOptions()
|
||||||
|
{
|
||||||
|
return ['sudah dibersihkan atau dicuci', 'sudah diganti'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the evaporator condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getEvaporatorConditionOptions()
|
||||||
|
{
|
||||||
|
return ['sudah dikerjakan', 'sudah diganti'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the compressor condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getCompressorConditionOptions()
|
||||||
|
{
|
||||||
|
return ['sudah dikerjakan', 'sudah diganti'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by transaction
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param int $transactionId
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByTransaction($query, $transactionId)
|
||||||
|
{
|
||||||
|
return $query->where('transaction_id', $transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by user who performed postcheck
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param int $userId
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByUser($query, $userId)
|
||||||
|
{
|
||||||
|
return $query->where('postcheck_by', $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by date range
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $startDate
|
||||||
|
* @param string $endDate
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByDateRange($query, $startDate, $endDate)
|
||||||
|
{
|
||||||
|
return $query->whereBetween('postcheck_at', [$startDate, $endDate]);
|
||||||
|
}
|
||||||
|
}
|
||||||
210
app/Models/Precheck.php
Normal file
210
app/Models/Precheck.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Precheck extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'transaction_id',
|
||||||
|
'precheck_by',
|
||||||
|
'precheck_at',
|
||||||
|
'police_number',
|
||||||
|
'spk_number',
|
||||||
|
'front_image',
|
||||||
|
'front_image_metadata',
|
||||||
|
'kilometer',
|
||||||
|
'pressure_high',
|
||||||
|
'pressure_low',
|
||||||
|
'cabin_temperature',
|
||||||
|
'cabin_temperature_image',
|
||||||
|
'cabin_temperature_image_metadata',
|
||||||
|
'ac_condition',
|
||||||
|
'ac_image',
|
||||||
|
'ac_image_metadata',
|
||||||
|
'blower_condition',
|
||||||
|
'blower_image',
|
||||||
|
'blower_image_metadata',
|
||||||
|
'evaporator_condition',
|
||||||
|
'evaporator_image',
|
||||||
|
'evaporator_image_metadata',
|
||||||
|
'compressor_condition',
|
||||||
|
'precheck_notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'precheck_at' => 'datetime',
|
||||||
|
'kilometer' => 'decimal:2',
|
||||||
|
'pressure_high' => 'decimal:2',
|
||||||
|
'pressure_low' => 'decimal:2',
|
||||||
|
'cabin_temperature' => 'decimal:2',
|
||||||
|
'front_image_metadata' => 'array',
|
||||||
|
'cabin_temperature_image_metadata' => 'array',
|
||||||
|
'ac_image_metadata' => 'array',
|
||||||
|
'blower_image_metadata' => 'array',
|
||||||
|
'evaporator_image_metadata' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the transaction associated with the Precheck
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function transaction()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Transaction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who performed the precheck
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function precheckBy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'precheck_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get front image URL
|
||||||
|
*/
|
||||||
|
public function getFrontImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->front_image ? asset('storage/' . ltrim($this->front_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cabin temperature image URL
|
||||||
|
*/
|
||||||
|
public function getCabinTemperatureImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->cabin_temperature_image ? asset('storage/' . ltrim($this->cabin_temperature_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AC image URL
|
||||||
|
*/
|
||||||
|
public function getAcImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->ac_image ? asset('storage/' . ltrim($this->ac_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blower image URL
|
||||||
|
*/
|
||||||
|
public function getBlowerImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->blower_image ? asset('storage/' . ltrim($this->blower_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get evaporator image URL
|
||||||
|
*/
|
||||||
|
public function getEvaporatorImageUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->evaporator_image ? asset('storage/' . ltrim($this->evaporator_image, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete associated files when model is deleted
|
||||||
|
*/
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::deleting(function ($precheck) {
|
||||||
|
$imageFields = [
|
||||||
|
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||||
|
'blower_image', 'evaporator_image'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imageFields as $field) {
|
||||||
|
if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) {
|
||||||
|
Storage::disk('public')->delete($precheck->$field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the AC condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAcConditionOptions()
|
||||||
|
{
|
||||||
|
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the blower condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getBlowerConditionOptions()
|
||||||
|
{
|
||||||
|
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the evaporator condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getEvaporatorConditionOptions()
|
||||||
|
{
|
||||||
|
return ['kotor', 'berlendir', 'bocor', 'bersih'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the compressor condition options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getCompressorConditionOptions()
|
||||||
|
{
|
||||||
|
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by transaction
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param int $transactionId
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByTransaction($query, $transactionId)
|
||||||
|
{
|
||||||
|
return $query->where('transaction_id', $transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by user who performed precheck
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param int $userId
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByUser($query, $userId)
|
||||||
|
{
|
||||||
|
return $query->where('precheck_by', $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by date range
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $startDate
|
||||||
|
* @param string $endDate
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByDateRange($query, $startDate, $endDate)
|
||||||
|
{
|
||||||
|
return $query->whereBetween('precheck_at', [$startDate, $endDate]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class Role extends Model
|
class Role extends Model
|
||||||
{
|
{
|
||||||
@@ -11,4 +12,19 @@ class Role extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name'
|
'name'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function dealers()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Dealer::class, 'role_dealer');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasDealer($dealerId)
|
||||||
|
{
|
||||||
|
return $this->dealers()->where('dealers.id', $dealerId)->whereNull('dealers.deleted_at')->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ class Transaction extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id"
|
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id",
|
||||||
|
"claimed_at", "claimed_by"
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'claimed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,4 +57,24 @@ class Transaction extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'user_sa_id');
|
return $this->belongsTo(User::class, 'user_sa_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the precheck associated with the transaction
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
|
*/
|
||||||
|
public function precheck()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Precheck::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the postcheck associated with the transaction
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
|
*/
|
||||||
|
public function postcheck()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Postcheck::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,4 +132,193 @@ class User extends Authenticatable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all KPI targets for the User
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function kpiTargets()
|
||||||
|
{
|
||||||
|
return $this->hasMany(KpiTarget::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all KPI achievements for the User
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function kpiAchievements()
|
||||||
|
{
|
||||||
|
return $this->hasMany(KpiAchievement::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is mechanic
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isMechanic()
|
||||||
|
{
|
||||||
|
return $this->hasRole('mechanic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current KPI target (no longer filtered by year/month)
|
||||||
|
*
|
||||||
|
* @return KpiTarget|null
|
||||||
|
*/
|
||||||
|
public function getCurrentKpiTarget()
|
||||||
|
{
|
||||||
|
return $this->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI achievement for specific year and month
|
||||||
|
*
|
||||||
|
* @param int $year
|
||||||
|
* @param int $month
|
||||||
|
* @return KpiAchievement|null
|
||||||
|
*/
|
||||||
|
public function getKpiAchievement($year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
return $this->kpiAchievements()
|
||||||
|
->where('year', $year)
|
||||||
|
->where('month', $month)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accessibleDealers()
|
||||||
|
{
|
||||||
|
if (!$this->role_id) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load role with dealers
|
||||||
|
if (!$this->relationLoaded('role')) {
|
||||||
|
$this->load('role.dealers');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has specific dealer_id, check if role allows access
|
||||||
|
if ($this->dealer_id) {
|
||||||
|
if ($this->role && $this->role->hasDealer($this->dealer_id)) {
|
||||||
|
return Dealer::where('id', $this->dealer_id)->get();
|
||||||
|
}
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific dealer_id, return all dealers accessible by role
|
||||||
|
return $this->role ? $this->role->dealers : collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canAccessDealer($dealerId)
|
||||||
|
{
|
||||||
|
if (!$this->role_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load role with dealers
|
||||||
|
if (!$this->relationLoaded('role')) {
|
||||||
|
$this->load('role.dealers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->role && $this->role->hasDealer($dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrimaryDealer()
|
||||||
|
{
|
||||||
|
if ($this->dealer_id && $this->canAccessDealer($this->dealer_id)) {
|
||||||
|
return $this->dealer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all accessible menus for a specific role
|
||||||
|
*
|
||||||
|
* @param int $roleId
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
public static function getAccessibleMenus($roleId)
|
||||||
|
{
|
||||||
|
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||||
|
->where('privileges.role_id', $roleId)
|
||||||
|
->where('privileges.view', 1)
|
||||||
|
->select('menus.*', 'privileges.view', 'privileges.create', 'privileges.update', 'privileges.delete')
|
||||||
|
->orderBy('menus.id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get accessible menus for current user
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
public function getMyAccessibleMenus()
|
||||||
|
{
|
||||||
|
if (!$this->role_id) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
return self::getAccessibleMenus($this->role_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user can access specific menu
|
||||||
|
*
|
||||||
|
* @param string $menuLink
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canAccessMenu($menuLink)
|
||||||
|
{
|
||||||
|
if (!$this->role_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||||
|
->where('privileges.role_id', $this->role_id)
|
||||||
|
->where('menus.link', $menuLink)
|
||||||
|
->where('privileges.view', 1)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if role can access specific menu (static method)
|
||||||
|
*
|
||||||
|
* @param int $roleId
|
||||||
|
* @param string $menuLink
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function roleCanAccessMenu($roleId, $menuLink)
|
||||||
|
{
|
||||||
|
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||||
|
->where('privileges.role_id', $roleId)
|
||||||
|
->where('menus.link', $menuLink)
|
||||||
|
->where('privileges.view', 1)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all prechecks performed by this user
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function prechecks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Precheck::class, 'precheck_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all postchecks performed by this user
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function postchecks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Postcheck::class, 'postcheck_by');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,52 @@ class Work extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Category::class);
|
return $this->belongsTo(Category::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all dealer prices for this work
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function dealerPrices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WorkDealerPrice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for specific dealer
|
||||||
|
*
|
||||||
|
* @param int $dealerId
|
||||||
|
* @return WorkDealerPrice|null
|
||||||
|
*/
|
||||||
|
public function getPriceForDealer($dealerId)
|
||||||
|
{
|
||||||
|
return $this->dealerPrices()
|
||||||
|
->where('dealer_id', $dealerId)
|
||||||
|
->active()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for specific dealer (including soft deleted)
|
||||||
|
*
|
||||||
|
* @param int $dealerId
|
||||||
|
* @return WorkDealerPrice|null
|
||||||
|
*/
|
||||||
|
public function getPriceForDealerWithTrashed($dealerId)
|
||||||
|
{
|
||||||
|
return $this->dealerPrices()
|
||||||
|
->withTrashed()
|
||||||
|
->where('dealer_id', $dealerId)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active prices for this work
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function activeDealerPrices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
app/Models/WorkDealerPrice.php
Normal file
81
app/Models/WorkDealerPrice.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class WorkDealerPrice extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'work_id',
|
||||||
|
'dealer_id',
|
||||||
|
'price',
|
||||||
|
'currency',
|
||||||
|
'is_active'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'price' => 'decimal:2',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the work associated with the price
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function work()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Work::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dealer associated with the price
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function dealer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Dealer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get only active prices
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted price with currency
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFormattedPriceAttribute()
|
||||||
|
{
|
||||||
|
return number_format($this->price, 0, ',', '.') . ' ' . $this->currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for specific work and dealer
|
||||||
|
*
|
||||||
|
* @param int $workId
|
||||||
|
* @param int $dealerId
|
||||||
|
* @return WorkDealerPrice|null
|
||||||
|
*/
|
||||||
|
public static function getPriceForWorkAndDealer($workId, $dealerId)
|
||||||
|
{
|
||||||
|
return static::where('work_id', $workId)
|
||||||
|
->where('dealer_id', $dealerId)
|
||||||
|
->active()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
Carbon::setLocale('id');
|
Carbon::setLocale('id');
|
||||||
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*', 'warehouse_management.*'], function ($view) {
|
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*', 'warehouse_management.*', 'reports.*', 'kpi.*'], function ($view) {
|
||||||
$menuQuery = Menu::all();
|
$menuQuery = Menu::all();
|
||||||
$menus = [];
|
$menus = [];
|
||||||
foreach($menuQuery as $menu) {
|
foreach($menuQuery as $menu) {
|
||||||
|
|||||||
454
app/Services/KpiService.php
Normal file
454
app/Services/KpiService.php
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use App\Models\KpiTarget;
|
||||||
|
use App\Models\KpiAchievement;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class KpiService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate KPI achievement for a user
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $year
|
||||||
|
* @param int $month
|
||||||
|
* @return KpiAchievement|null
|
||||||
|
*/
|
||||||
|
public function calculateKpiAchievement(User $user, $year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
// Get current KPI target (no longer filtered by year/month)
|
||||||
|
$kpiTarget = $user->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$kpiTarget) {
|
||||||
|
Log::info("No KPI target found for user {$user->id}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual value based on month
|
||||||
|
$actualValue = $this->getActualWorkCount($user, $year, $month);
|
||||||
|
|
||||||
|
// Calculate percentage
|
||||||
|
$achievementPercentage = $kpiTarget->target_value > 0
|
||||||
|
? ($actualValue / $kpiTarget->target_value) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Save or update achievement with target value stored directly
|
||||||
|
return KpiAchievement::updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kpi_target_id' => $kpiTarget->id,
|
||||||
|
'target_value' => $kpiTarget->target_value, // Store target value directly for historical tracking
|
||||||
|
'actual_value' => $actualValue,
|
||||||
|
'achievement_percentage' => $achievementPercentage
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get actual work count for a user in specific month
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $year
|
||||||
|
* @param int $month
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function getActualWorkCount(User $user, $year, $month)
|
||||||
|
{
|
||||||
|
return Transaction::where('user_id', $user->id)
|
||||||
|
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||||
|
->whereYear('date', $year)
|
||||||
|
->whereMonth('date', $month)
|
||||||
|
->sum('qty');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate KPI report for a user
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int|null $year
|
||||||
|
* @param int|null $month
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function generateKpiReport(User $user, $year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
$achievements = $user->kpiAchievements()
|
||||||
|
->where('year', $year)
|
||||||
|
->where('month', $month)
|
||||||
|
->orderBy('month')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$target = $user->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'user' => $user,
|
||||||
|
'target' => $target,
|
||||||
|
'achievements' => $achievements,
|
||||||
|
'summary' => $this->calculateSummary($achievements),
|
||||||
|
'period' => [
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'period_name' => $this->getMonthName($month) . ' ' . $year
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate summary statistics for achievements
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Collection $achievements
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function calculateSummary($achievements)
|
||||||
|
{
|
||||||
|
if ($achievements->isEmpty()) {
|
||||||
|
return [
|
||||||
|
'total_target' => 0,
|
||||||
|
'total_actual' => 0,
|
||||||
|
'average_achievement' => 0,
|
||||||
|
'best_period' => null,
|
||||||
|
'worst_period' => null,
|
||||||
|
'total_periods' => 0,
|
||||||
|
'achievement_rate' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalTarget = $achievements->sum('target_value');
|
||||||
|
$totalActual = $achievements->sum('actual_value');
|
||||||
|
$averageAchievement = $achievements->avg('achievement_percentage');
|
||||||
|
$totalPeriods = $achievements->count();
|
||||||
|
$achievementRate = $totalPeriods > 0 ? ($achievements->where('achievement_percentage', '>=', 100)->count() / $totalPeriods) * 100 : 0;
|
||||||
|
|
||||||
|
$bestPeriod = $achievements->sortByDesc('achievement_percentage')->first();
|
||||||
|
$worstPeriod = $achievements->sortBy('achievement_percentage')->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_target' => $totalTarget,
|
||||||
|
'total_actual' => $totalActual,
|
||||||
|
'average_achievement' => round($averageAchievement, 2),
|
||||||
|
'best_period' => $bestPeriod,
|
||||||
|
'worst_period' => $worstPeriod,
|
||||||
|
'total_periods' => $totalPeriods,
|
||||||
|
'achievement_rate' => round($achievementRate, 2)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI statistics for all mechanics
|
||||||
|
*
|
||||||
|
* @param int|null $year
|
||||||
|
* @param int|null $month
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getMechanicsKpiStats($year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
$mechanics = User::whereHas('role', function($query) {
|
||||||
|
$query->where('name', 'mechanic');
|
||||||
|
})->get();
|
||||||
|
|
||||||
|
$stats = [];
|
||||||
|
foreach ($mechanics as $mechanic) {
|
||||||
|
$report = $this->generateKpiReport($mechanic, $year, $month);
|
||||||
|
$stats[] = [
|
||||||
|
'user' => $mechanic,
|
||||||
|
'summary' => $report['summary'],
|
||||||
|
'target' => $report['target']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-calculate KPI achievements for all mechanics
|
||||||
|
*
|
||||||
|
* @param int|null $year
|
||||||
|
* @param int|null $month
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function autoCalculateAllMechanics($year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
$mechanics = User::whereHas('role', function($query) {
|
||||||
|
$query->where('name', 'mechanic');
|
||||||
|
})->get();
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($mechanics as $mechanic) {
|
||||||
|
try {
|
||||||
|
$achievement = $this->calculateKpiAchievement($mechanic, $year, $month);
|
||||||
|
$results[] = [
|
||||||
|
'user_id' => $mechanic->id,
|
||||||
|
'user_name' => $mechanic->name,
|
||||||
|
'success' => true,
|
||||||
|
'achievement' => $achievement
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to calculate KPI for user {$mechanic->id}: " . $e->getMessage());
|
||||||
|
$results[] = [
|
||||||
|
'user_id' => $mechanic->id,
|
||||||
|
'user_name' => $mechanic->name,
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI trend data for chart
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $months
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getKpiTrendData(User $user, $months = 12)
|
||||||
|
{
|
||||||
|
$endDate = now();
|
||||||
|
$startDate = $endDate->copy()->subMonths($months);
|
||||||
|
|
||||||
|
$achievements = $user->kpiAchievements()
|
||||||
|
->where(function($query) use ($startDate, $endDate) {
|
||||||
|
$query->where(function($q) use ($startDate, $endDate) {
|
||||||
|
$q->where('year', '>', $startDate->year)
|
||||||
|
->orWhere(function($subQ) use ($startDate, $endDate) {
|
||||||
|
$subQ->where('year', $startDate->year)
|
||||||
|
->where('month', '>=', $startDate->month);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->where(function($q) use ($endDate) {
|
||||||
|
$q->where('year', '<', $endDate->year)
|
||||||
|
->orWhere(function($subQ) use ($endDate) {
|
||||||
|
$subQ->where('year', $endDate->year)
|
||||||
|
->where('month', '<=', $endDate->month);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('year')
|
||||||
|
->orderBy('month')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$trendData = [];
|
||||||
|
foreach ($achievements as $achievement) {
|
||||||
|
$trendData[] = [
|
||||||
|
'period' => $achievement->getPeriodDisplayName(),
|
||||||
|
'target' => $achievement->target_value,
|
||||||
|
'actual' => $achievement->actual_value,
|
||||||
|
'percentage' => $achievement->achievement_percentage,
|
||||||
|
'status' => $achievement->status
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trendData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month name in Indonesian
|
||||||
|
*
|
||||||
|
* @param int $month
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getMonthName($month)
|
||||||
|
{
|
||||||
|
$monthNames = [
|
||||||
|
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||||
|
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||||
|
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $monthNames[$month] ?? 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI summary for dashboard
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getKpiSummary(User $user)
|
||||||
|
{
|
||||||
|
$currentYear = now()->year;
|
||||||
|
$currentMonth = now()->month;
|
||||||
|
|
||||||
|
// Get current month achievement
|
||||||
|
$currentAchievement = $user->kpiAchievements()
|
||||||
|
->where('year', $currentYear)
|
||||||
|
->where('month', $currentMonth)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Get current month target (no longer filtered by year/month)
|
||||||
|
$currentTarget = $user->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Get last 6 months achievements
|
||||||
|
$recentAchievements = $user->kpiAchievements()
|
||||||
|
->where(function($query) use ($currentYear, $currentMonth) {
|
||||||
|
$query->where('year', '>', $currentYear - 1)
|
||||||
|
->orWhere(function($q) use ($currentYear, $currentMonth) {
|
||||||
|
$q->where('year', $currentYear)
|
||||||
|
->where('month', '>=', max(1, $currentMonth - 5));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('year', 'desc')
|
||||||
|
->orderBy('month', 'desc')
|
||||||
|
->limit(6)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'current_achievement' => $currentAchievement,
|
||||||
|
'current_target' => $currentTarget,
|
||||||
|
'recent_achievements' => $recentAchievements,
|
||||||
|
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
|
||||||
|
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get claimed transactions count for a mechanic
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int|null $year
|
||||||
|
* @param int|null $month
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getClaimedTransactionsCount(User $user, $year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
return Transaction::where('claimed_by', $user->id)
|
||||||
|
->whereNotNull('claimed_at')
|
||||||
|
->whereYear('claimed_at', $year)
|
||||||
|
->whereMonth('claimed_at', $month)
|
||||||
|
->sum('qty');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate KPI achievement including claimed transactions
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $year
|
||||||
|
* @param int $month
|
||||||
|
* @return KpiAchievement|null
|
||||||
|
*/
|
||||||
|
public function calculateKpiAchievementWithClaims(User $user, $year = null, $month = null)
|
||||||
|
{
|
||||||
|
$year = $year ?? now()->year;
|
||||||
|
$month = $month ?? now()->month;
|
||||||
|
|
||||||
|
// Get current KPI target
|
||||||
|
$kpiTarget = $user->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$kpiTarget) {
|
||||||
|
Log::info("No KPI target found for user {$user->id}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual value including claimed transactions
|
||||||
|
$actualValue = $this->getActualWorkCountWithClaims($user, $year, $month);
|
||||||
|
|
||||||
|
// Calculate percentage
|
||||||
|
$achievementPercentage = $kpiTarget->target_value > 0
|
||||||
|
? ($actualValue / $kpiTarget->target_value) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Save or update achievement
|
||||||
|
return KpiAchievement::updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'kpi_target_id' => $kpiTarget->id,
|
||||||
|
'target_value' => $kpiTarget->target_value,
|
||||||
|
'actual_value' => $actualValue,
|
||||||
|
'achievement_percentage' => $achievementPercentage
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get actual work count including claimed transactions
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $year
|
||||||
|
* @param int $month
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function getActualWorkCountWithClaims(User $user, $year, $month)
|
||||||
|
{
|
||||||
|
// Get transactions created by the user (including pending and completed)
|
||||||
|
$createdTransactions = Transaction::where('user_id', $user->id)
|
||||||
|
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||||
|
->whereYear('date', $year)
|
||||||
|
->whereMonth('date', $month)
|
||||||
|
->sum('qty');
|
||||||
|
|
||||||
|
// Get transactions claimed by the user (excluding those created by the same user to avoid double counting)
|
||||||
|
$claimedTransactions = Transaction::where('claimed_by', $user->id)
|
||||||
|
->whereNotNull('claimed_at')
|
||||||
|
->where('user_id', '!=', $user->id) // Exclude transactions created by the same user
|
||||||
|
->whereYear('claimed_at', $year)
|
||||||
|
->whereMonth('claimed_at', $month)
|
||||||
|
->sum('qty');
|
||||||
|
|
||||||
|
return $createdTransactions + $claimedTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get KPI summary including claimed transactions
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getKpiSummaryWithClaims(User $user)
|
||||||
|
{
|
||||||
|
$currentYear = now()->year;
|
||||||
|
$currentMonth = now()->month;
|
||||||
|
|
||||||
|
// Calculate current month achievement including claims
|
||||||
|
$currentAchievement = $this->calculateKpiAchievementWithClaims($user, $currentYear, $currentMonth);
|
||||||
|
|
||||||
|
// Get current month target
|
||||||
|
$currentTarget = $user->kpiTargets()
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'current_achievement' => $currentAchievement,
|
||||||
|
'current_target' => $currentTarget,
|
||||||
|
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
|
||||||
|
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
292
app/Services/StockReportService.php
Normal file
292
app/Services/StockReportService.php
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Dealer;
|
||||||
|
use App\Models\Stock;
|
||||||
|
use App\Models\StockLog;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class StockReportService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get stock report data for all products and dealers on a specific date
|
||||||
|
*/
|
||||||
|
public function getStockReportData($targetDate = null)
|
||||||
|
{
|
||||||
|
$targetDate = $targetDate ? Carbon::parse($targetDate) : now();
|
||||||
|
|
||||||
|
// Get dealers based on user role
|
||||||
|
$dealers = $this->getDealersBasedOnUserRole();
|
||||||
|
|
||||||
|
// Get all active products
|
||||||
|
$products = Product::where('active', true)
|
||||||
|
->with(['category'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$row = [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'product_code' => $product->code,
|
||||||
|
'product_name' => $product->name,
|
||||||
|
'category_name' => $product->category ? $product->category->name : '-',
|
||||||
|
'unit' => $product->unit ?? '-',
|
||||||
|
'total_stock' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate stock for each dealer on the target date
|
||||||
|
foreach ($dealers as $dealer) {
|
||||||
|
$stockOnDate = $this->getStockOnDate($product->id, $dealer->id, $targetDate);
|
||||||
|
$row["dealer_{$dealer->id}"] = $stockOnDate;
|
||||||
|
$row['total_stock'] += $stockOnDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'dealers' => $dealers
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stock quantity for a specific product and dealer on a given date
|
||||||
|
*/
|
||||||
|
public function getStockOnDate($productId, $dealerId, $targetDate)
|
||||||
|
{
|
||||||
|
// Get the latest stock log entry before or on the target date
|
||||||
|
$latestStockLog = StockLog::whereHas('stock', function($query) use ($productId, $dealerId) {
|
||||||
|
$query->where('product_id', $productId)
|
||||||
|
->where('dealer_id', $dealerId);
|
||||||
|
})
|
||||||
|
->where('created_at', '<=', $targetDate->endOfDay())
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($latestStockLog) {
|
||||||
|
// Return the new_quantity from the latest log entry
|
||||||
|
return $latestStockLog->new_quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no stock log found, check if there's a current stock record
|
||||||
|
$currentStock = Stock::where('product_id', $productId)
|
||||||
|
->where('dealer_id', $dealerId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($currentStock) {
|
||||||
|
// Check if the stock was created before or on the target date
|
||||||
|
if ($currentStock->created_at <= $targetDate) {
|
||||||
|
return $currentStock->quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No stock data available for this date
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get optimized stock data using a single query approach
|
||||||
|
*/
|
||||||
|
public function getOptimizedStockReportData($targetDate = null)
|
||||||
|
{
|
||||||
|
$targetDate = $targetDate ? Carbon::parse($targetDate) : now();
|
||||||
|
|
||||||
|
// Get dealers based on user role
|
||||||
|
$dealers = $this->getDealersBasedOnUserRole();
|
||||||
|
|
||||||
|
// Get all active products with their stock data
|
||||||
|
$products = Product::where('active', true)
|
||||||
|
->with(['category', 'stocks.dealer'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$row = [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'product_code' => $product->code,
|
||||||
|
'product_name' => $product->name,
|
||||||
|
'category_name' => $product->category ? $product->category->name : '-',
|
||||||
|
'unit' => $product->unit ?? '-',
|
||||||
|
'total_stock' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate stock for each dealer on the target date
|
||||||
|
foreach ($dealers as $dealer) {
|
||||||
|
$stockOnDate = $this->getOptimizedStockOnDate($product->id, $dealer->id, $targetDate);
|
||||||
|
$row["dealer_{$dealer->id}"] = $stockOnDate;
|
||||||
|
$row['total_stock'] += $stockOnDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'dealers' => $dealers
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dealers based on logged-in user's role
|
||||||
|
*/
|
||||||
|
public function getDealersBasedOnUserRole()
|
||||||
|
{
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
Log::warning('No authenticated user found, returning all dealers');
|
||||||
|
return Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting dealers for user:', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If user has role, check role type and dealer access
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if ($role) {
|
||||||
|
// Check if role is admin type
|
||||||
|
if ($this->isAdminRole($role)) {
|
||||||
|
// Admin role - check if has pivot dealers
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
// Admin with pivot dealers - return pivot dealers only
|
||||||
|
Log::info('Admin role with pivot dealers, returning pivot dealers only');
|
||||||
|
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
Log::info('Returning pivot dealers for admin:', $dealers->pluck('name')->toArray());
|
||||||
|
return $dealers;
|
||||||
|
} else {
|
||||||
|
// Admin without pivot dealers - return all dealers
|
||||||
|
Log::info('Admin role without pivot dealers, returning all dealers');
|
||||||
|
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||||
|
Log::info('Returning all dealers for admin:', $allDealers->pluck('name')->toArray());
|
||||||
|
return $allDealers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-admin role - return dealers from role pivot
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
Log::info('Non-admin role with dealers, returning role dealers');
|
||||||
|
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||||
|
Log::info('Returning dealers from role:', $dealers->pluck('name')->toArray());
|
||||||
|
return $dealers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has specific dealer_id but no role dealers, check if they can access their dealer_id
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
Log::info('User has specific dealer_id:', ['dealer_id' => $user->dealer_id]);
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||||
|
Log::info('User can access their dealer_id, returning single dealer');
|
||||||
|
$dealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->orderBy('name')->get();
|
||||||
|
Log::info('Returning dealer:', $dealer->pluck('name')->toArray());
|
||||||
|
return $dealer;
|
||||||
|
} else {
|
||||||
|
Log::info('User cannot access their dealer_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log::info('User has dealer_id but no role or no access, returning empty');
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return all dealers if no restrictions
|
||||||
|
Log::info('No restrictions found, returning all dealers');
|
||||||
|
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||||
|
Log::info('Returning all dealers:', $allDealers->pluck('name')->toArray());
|
||||||
|
return $allDealers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if role is admin type (should show all dealers if no pivot)
|
||||||
|
*/
|
||||||
|
private function isAdminRole($role)
|
||||||
|
{
|
||||||
|
// Define admin role names that should have access to all dealers
|
||||||
|
$adminRoleNames = [
|
||||||
|
'admin'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if role name contains admin keywords (but not "area")
|
||||||
|
$roleName = strtolower(trim($role->name));
|
||||||
|
foreach ($adminRoleNames as $adminName) {
|
||||||
|
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||||
|
Log::info('Role identified as admin type:', ['role_name' => $role->name]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role with "area" in name should use pivot dealers, not all dealers
|
||||||
|
if (strpos($roleName, 'area') !== false) {
|
||||||
|
Log::info('Role contains "area", treating as area role (use pivot dealers):', ['role_name' => $role->name]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Role is not admin type:', ['role_name' => $role->name]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized method to get stock on date using subquery
|
||||||
|
*/
|
||||||
|
private function getOptimizedStockOnDate($productId, $dealerId, $targetDate)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Use a subquery to get the latest stock log entry efficiently
|
||||||
|
$latestStockLog = DB::table('stock_logs')
|
||||||
|
->join('stocks', 'stock_logs.stock_id', '=', 'stocks.id')
|
||||||
|
->where('stocks.product_id', $productId)
|
||||||
|
->where('stocks.dealer_id', $dealerId)
|
||||||
|
->where('stock_logs.created_at', '<=', $targetDate->endOfDay())
|
||||||
|
->orderBy('stock_logs.created_at', 'desc')
|
||||||
|
->select('stock_logs.new_quantity')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($latestStockLog) {
|
||||||
|
return $latestStockLog->new_quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no stock log found, check current stock
|
||||||
|
$currentStock = Stock::where('product_id', $productId)
|
||||||
|
->where('dealer_id', $dealerId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($currentStock && $currentStock->created_at <= $targetDate) {
|
||||||
|
return $currentStock->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error and return 0
|
||||||
|
Log::error('Error getting stock on date: ' . $e->getMessage(), [
|
||||||
|
'product_id' => $productId,
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'target_date' => $targetDate
|
||||||
|
]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
798
app/Services/TechnicianReportService.php
Normal file
798
app/Services/TechnicianReportService.php
Normal file
@@ -0,0 +1,798 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Work;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use App\Models\Dealer;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TechnicianReportService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get technician report data for all works and mechanics on a specific date range
|
||||||
|
*/
|
||||||
|
public function getTechnicianReportData($dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return [
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dealer access
|
||||||
|
if ($dealerId) {
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
// User has specific dealer_id, check if they can access the requested dealer
|
||||||
|
if ($user->dealer_id != $dealerId) {
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return [
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User has dealer_id but no role, can only access their dealer
|
||||||
|
return [
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->role_id) {
|
||||||
|
// User has role, check if they can access the requested dealer
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return [
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting technician report data', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get all works with category in single query
|
||||||
|
$works = Work::with(['category'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get mechanics based on dealer and role access
|
||||||
|
$mechanics = $this->getMechanicsByDealer($dealerId);
|
||||||
|
|
||||||
|
Log::info('Mechanics found for report:', [
|
||||||
|
'count' => $mechanics->count(),
|
||||||
|
'dealer_id_filter' => $dealerId,
|
||||||
|
'mechanics' => $mechanics->map(function($mechanic) {
|
||||||
|
$roleName = 'Unknown';
|
||||||
|
if ($mechanic->role) {
|
||||||
|
$roleName = is_string($mechanic->role) ? $mechanic->role : $mechanic->role->name;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $mechanic->id,
|
||||||
|
'name' => $mechanic->name,
|
||||||
|
'role_id' => $mechanic->role_id,
|
||||||
|
'role_name' => $roleName,
|
||||||
|
'dealer_id' => $mechanic->dealer_id
|
||||||
|
];
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get all transaction data in single optimized query
|
||||||
|
$transactions = $this->getOptimizedTransactionData($dealerId, $startDate, $endDate, $mechanics->pluck('id'), $works->pluck('id'));
|
||||||
|
|
||||||
|
Log::info('Transaction data:', [
|
||||||
|
'transaction_count' => count($transactions),
|
||||||
|
'sample_transactions' => array_slice($transactions, 0, 5, true),
|
||||||
|
'dealer_id_filter' => $dealerId,
|
||||||
|
'is_admin_with_pivot' => $user->role_id ? (function() use ($user) {
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
return $role && $this->isAdminRole($role) && $role->dealers->count() > 0;
|
||||||
|
})() : false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($works as $work) {
|
||||||
|
$row = [
|
||||||
|
'work_id' => $work->id,
|
||||||
|
'work_name' => $work->name,
|
||||||
|
'work_code' => $work->shortname,
|
||||||
|
'category_name' => $work->category ? $work->category->name : '-',
|
||||||
|
'total_tickets' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate totals for each mechanic
|
||||||
|
foreach ($mechanics as $mechanic) {
|
||||||
|
$key = $work->id . '_' . $mechanic->id;
|
||||||
|
$mechanicData = $transactions[$key] ?? ['total' => 0, 'completed' => 0, 'pending' => 0];
|
||||||
|
|
||||||
|
$row["mechanic_{$mechanic->id}_total"] = $mechanicData['total'];
|
||||||
|
|
||||||
|
// Add to totals
|
||||||
|
$row['total_tickets'] += $mechanicData['total'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Final data prepared:', [
|
||||||
|
'data_count' => count($data),
|
||||||
|
'sample_data' => array_slice($data, 0, 2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'mechanics' => $mechanics,
|
||||||
|
'works' => $works
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in getTechnicianReportData: ' . $e->getMessage(), [
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return empty data structure but with proper format
|
||||||
|
return [
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get optimized transaction data in single query
|
||||||
|
*/
|
||||||
|
private function getOptimizedTransactionData($dealerId = null, $startDate = null, $endDate = null, $mechanicIds = null, $workIds = null)
|
||||||
|
{
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dealer access
|
||||||
|
if ($dealerId) {
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
// User has specific dealer_id, check if they can access the requested dealer
|
||||||
|
if ($user->dealer_id != $dealerId) {
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User has dealer_id but no role, can only access their dealer
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->role_id) {
|
||||||
|
// User has role, check if they can access the requested dealer
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting optimized transaction data', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = Transaction::select(
|
||||||
|
'work_id',
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
DB::raw('COUNT(*) as count')
|
||||||
|
)
|
||||||
|
->groupBy('work_id', 'user_id', 'status');
|
||||||
|
|
||||||
|
if ($dealerId) {
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
} else if ($user->role_id) {
|
||||||
|
// Check if admin with pivot dealers and "Semua Dealer" selected
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if ($role && $this->isAdminRole($role) && $role->dealers->count() > 0) {
|
||||||
|
// Admin with pivot dealers and "Semua Dealer" selected - filter by pivot dealers
|
||||||
|
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||||
|
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||||
|
Log::info('Admin with pivot dealers, filtering transactions by pivot dealer IDs:', $accessibleDealerIds->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$query->where('date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate) {
|
||||||
|
$query->where('date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mechanicIds && $mechanicIds->count() > 0) {
|
||||||
|
$query->whereIn('user_id', $mechanicIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workIds && $workIds->count() > 0) {
|
||||||
|
$query->whereIn('work_id', $workIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove index hint that doesn't exist
|
||||||
|
$results = $query->get();
|
||||||
|
|
||||||
|
Log::info('Transaction query results', [
|
||||||
|
'results_count' => $results->count()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Organize data by work_id_user_id key
|
||||||
|
$organizedData = [];
|
||||||
|
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$key = $result->work_id . '_' . $result->user_id;
|
||||||
|
|
||||||
|
if (!isset($organizedData[$key])) {
|
||||||
|
$organizedData[$key] = [
|
||||||
|
'total' => 0,
|
||||||
|
'completed' => 0,
|
||||||
|
'pending' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizedData[$key]['total'] += $result->count;
|
||||||
|
|
||||||
|
if ($result->status == 1) {
|
||||||
|
$organizedData[$key]['completed'] += $result->count;
|
||||||
|
} else {
|
||||||
|
$organizedData[$key]['pending'] += $result->count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $organizedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||||
|
*/
|
||||||
|
private function getTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
$query = Transaction::where('work_id', $workId)
|
||||||
|
->where('user_id', $mechanicId);
|
||||||
|
|
||||||
|
if ($dealerId) {
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$query->where('date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate) {
|
||||||
|
$query->where('date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get completed ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||||
|
*/
|
||||||
|
private function getCompletedTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
$query = Transaction::where('work_id', $workId)
|
||||||
|
->where('user_id', $mechanicId)
|
||||||
|
->where('status', 1); // Assuming status 1 is completed
|
||||||
|
|
||||||
|
if ($dealerId) {
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$query->where('date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate) {
|
||||||
|
$query->where('date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||||
|
*/
|
||||||
|
private function getPendingTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
$query = Transaction::where('work_id', $workId)
|
||||||
|
->where('user_id', $mechanicId)
|
||||||
|
->where('status', 0); // Assuming status 0 is pending
|
||||||
|
|
||||||
|
if ($dealerId) {
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate) {
|
||||||
|
$query->where('date', '>=', $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate) {
|
||||||
|
$query->where('date', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all dealers for filter
|
||||||
|
*/
|
||||||
|
public function getDealers()
|
||||||
|
{
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
Log::info('No authenticated user found');
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting dealers for user:', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_name' => $user->name,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If user has role, check role type and dealer access
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
Log::info('Role details:', [
|
||||||
|
'role_id' => $role ? $role->id : null,
|
||||||
|
'role_name' => $role ? $role->name : null,
|
||||||
|
'role_dealers_count' => $role ? $role->dealers->count() : 0,
|
||||||
|
'role_dealers' => $role ? $role->dealers->pluck('id', 'name')->toArray() : []
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($role) {
|
||||||
|
// Check if role is admin type
|
||||||
|
if ($this->isAdminRole($role)) {
|
||||||
|
// Admin role - check if has pivot dealers
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
// Admin with pivot dealers - return pivot dealers (for "Semua Dealer" option)
|
||||||
|
Log::info('Admin role with pivot dealers, returning pivot dealers');
|
||||||
|
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get(['dealers.id', 'dealers.name', 'dealers.dealer_code']);
|
||||||
|
Log::info('Returning pivot dealers for admin:', $dealers->toArray());
|
||||||
|
return $dealers;
|
||||||
|
} else {
|
||||||
|
// Admin without pivot dealers - return all dealers
|
||||||
|
Log::info('Admin role without pivot dealers, returning all dealers');
|
||||||
|
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||||
|
Log::info('Returning all dealers for admin:', $allDealers->toArray());
|
||||||
|
return $allDealers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role has dealer relationship (tampilkan dealer berdasarkan pivot)
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
Log::info('Role has dealers relationship, returning role dealers');
|
||||||
|
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get(['dealers.id', 'dealers.name', 'dealers.dealer_code']);
|
||||||
|
Log::info('Returning dealers from role:', $dealers->toArray());
|
||||||
|
return $dealers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has specific dealer_id but no role dealers, check if they can access their dealer_id
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
Log::info('User has specific dealer_id:', ['dealer_id' => $user->dealer_id]);
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||||
|
Log::info('User can access their dealer_id, returning single dealer');
|
||||||
|
$dealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||||
|
Log::info('Returning dealer:', $dealer->toArray());
|
||||||
|
return $dealer;
|
||||||
|
} else {
|
||||||
|
Log::info('User cannot access their dealer_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log::info('User has dealer_id but no role or no access, returning empty');
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return all dealers if no restrictions
|
||||||
|
Log::info('No restrictions found, returning all dealers');
|
||||||
|
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||||
|
Log::info('Returning all dealers:', $allDealers->toArray());
|
||||||
|
return $allDealers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if role is admin type (should show all dealers)
|
||||||
|
*/
|
||||||
|
public function isAdminRole($role)
|
||||||
|
{
|
||||||
|
// Define admin role names that should have access to all dealers
|
||||||
|
$adminRoleNames = [
|
||||||
|
'admin'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if role name contains admin keywords (but not "area")
|
||||||
|
$roleName = strtolower(trim($role->name));
|
||||||
|
foreach ($adminRoleNames as $adminName) {
|
||||||
|
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||||
|
Log::info('Role identified as admin type:', ['role_name' => $role->name]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if role has no dealer restrictions (no pivot relationships)
|
||||||
|
// This means role can access all dealers
|
||||||
|
if ($role->dealers->count() === 0) {
|
||||||
|
Log::info('Role has no dealer restrictions, treating as admin type:', ['role_name' => $role->name]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role with "area" in name should use pivot dealers, not all dealers
|
||||||
|
if (strpos($roleName, 'area') !== false) {
|
||||||
|
Log::info('Role contains "area", treating as area role (use pivot dealers):', ['role_name' => $role->name]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Role is not admin type:', ['role_name' => $role->name]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default dealer for filter (berbasis user role)
|
||||||
|
*/
|
||||||
|
public function getDefaultDealer()
|
||||||
|
{
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting default dealer for user:', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If user has role, check role type and dealer access
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if ($role) {
|
||||||
|
// Check if role is admin type
|
||||||
|
if ($this->isAdminRole($role)) {
|
||||||
|
// Admin role - check if has pivot dealers
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
// Admin with pivot dealers - return first dealer from pivot
|
||||||
|
Log::info('Admin role with pivot dealers, returning first dealer from pivot');
|
||||||
|
$defaultDealer = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->first();
|
||||||
|
Log::info('Default dealer for admin with pivot:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||||
|
return $defaultDealer;
|
||||||
|
} else {
|
||||||
|
// Admin without pivot dealers - no default dealer (show all dealers without selection)
|
||||||
|
Log::info('Admin role without pivot dealers, no default dealer');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role has dealer relationship (return first dealer from role dealers)
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
Log::info('Role has dealers relationship, returning first dealer from role dealers');
|
||||||
|
$defaultDealer = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->first();
|
||||||
|
Log::info('Default dealer from role dealers:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||||
|
return $defaultDealer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has specific dealer_id, check if they can access it
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||||
|
$defaultDealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->first();
|
||||||
|
Log::info('User dealer found:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||||
|
return $defaultDealer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no default dealer
|
||||||
|
Log::info('No default dealer found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mechanics for a specific dealer
|
||||||
|
*/
|
||||||
|
public function getMechanicsByDealer($dealerId = null)
|
||||||
|
{
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting mechanics by dealer:', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_role_id' => $user->role_id,
|
||||||
|
'user_dealer_id' => $user->dealer_id,
|
||||||
|
'requested_dealer_id' => $dealerId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = User::with('role')->whereHas('role', function($query) {
|
||||||
|
$query->where('name', 'mechanic');
|
||||||
|
});
|
||||||
|
|
||||||
|
// If user has role, check role type and dealer access
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with(['dealers' => function($query) {
|
||||||
|
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||||
|
}])->find($user->role_id);
|
||||||
|
|
||||||
|
if ($role) {
|
||||||
|
// Check if role is admin type
|
||||||
|
if ($this->isAdminRole($role)) {
|
||||||
|
// Admin role - check if has pivot dealers
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
// Admin with pivot dealers
|
||||||
|
if ($dealerId) {
|
||||||
|
// Specific dealer selected - get mechanics from that dealer
|
||||||
|
Log::info('Admin with pivot dealers, specific dealer selected:', ['dealer_id' => $dealerId]);
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
} else {
|
||||||
|
// "Semua Dealer" selected - get mechanics from all pivot dealers
|
||||||
|
Log::info('Admin with pivot dealers, "Semua Dealer" selected, getting mechanics from all pivot dealers');
|
||||||
|
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||||
|
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||||
|
Log::info('Accessible dealer IDs for admin:', $accessibleDealerIds->toArray());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Admin without pivot dealers - can access all dealers
|
||||||
|
Log::info('Admin without pivot dealers, can access mechanics from all dealers');
|
||||||
|
if ($dealerId) {
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
}
|
||||||
|
// If no dealer_id, show all mechanics (no additional filtering)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Role has dealer relationship (filter by accessible dealers)
|
||||||
|
if ($role->dealers->count() > 0) {
|
||||||
|
Log::info('Role has dealers relationship, filtering mechanics by accessible dealers');
|
||||||
|
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||||
|
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||||
|
Log::info('Accessible dealer IDs:', $accessibleDealerIds->toArray());
|
||||||
|
} else {
|
||||||
|
Log::info('Role has no dealers, returning empty');
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->dealer_id) {
|
||||||
|
// User has specific dealer_id but no role, can only access their dealer
|
||||||
|
Log::info('User has dealer_id but no role, can only access their dealer');
|
||||||
|
$query->where('dealer_id', $user->dealer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply dealer filter if provided (for non-admin roles)
|
||||||
|
if ($dealerId && !$this->isAdminRole($role ?? null)) {
|
||||||
|
Log::info('Applying dealer filter for non-admin role:', ['dealer_id' => $dealerId]);
|
||||||
|
$query->where('dealer_id', $dealerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mechanics = $query->orderBy('name')->get(['id', 'name', 'dealer_id']);
|
||||||
|
|
||||||
|
Log::info('Mechanics found:', [
|
||||||
|
'count' => $mechanics->count(),
|
||||||
|
'mechanics' => $mechanics->map(function($mechanic) {
|
||||||
|
return [
|
||||||
|
'id' => $mechanic->id,
|
||||||
|
'name' => $mechanic->name,
|
||||||
|
'dealer_id' => $mechanic->dealer_id
|
||||||
|
];
|
||||||
|
})->toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $mechanics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get technician report data for Yajra DataTable
|
||||||
|
*/
|
||||||
|
public function getTechnicianReportDataForDataTable($dealerId = null, $startDate = null, $endDate = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Get current authenticated user
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dealer access
|
||||||
|
if ($dealerId) {
|
||||||
|
if ($user->dealer_id) {
|
||||||
|
// User has specific dealer_id, check if they can access the requested dealer
|
||||||
|
if ($user->dealer_id != $dealerId) {
|
||||||
|
if ($user->role_id) {
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User has dealer_id but no role, can only access their dealer
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($user->role_id) {
|
||||||
|
// User has role, check if they can access the requested dealer
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
if (!$role || !$role->hasDealer($dealerId)) {
|
||||||
|
// User doesn't have access to this dealer
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Getting technician report data for DataTable', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get all works with category
|
||||||
|
$works = Work::with(['category'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get mechanics based on dealer and role access
|
||||||
|
$mechanics = $this->getMechanicsByDealer($dealerId);
|
||||||
|
|
||||||
|
// Get transaction data
|
||||||
|
$transactions = $this->getOptimizedTransactionData($dealerId, $startDate, $endDate, $mechanics->pluck('id'), $works->pluck('id'));
|
||||||
|
|
||||||
|
Log::info('Transaction data for DataTable:', [
|
||||||
|
'transaction_count' => count($transactions),
|
||||||
|
'dealer_id_filter' => $dealerId,
|
||||||
|
'is_admin_with_pivot' => $user->role_id ? (function() use ($user) {
|
||||||
|
$role = Role::with('dealers')->find($user->role_id);
|
||||||
|
return $role && $this->isAdminRole($role) && $role->dealers->count() > 0;
|
||||||
|
})() : false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($works as $work) {
|
||||||
|
$row = [
|
||||||
|
'DT_RowIndex' => count($data) + 1,
|
||||||
|
'work_name' => $work->name,
|
||||||
|
'work_code' => $work->shortname,
|
||||||
|
'category_name' => $work->category ? $work->category->name : '-'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add mechanic columns
|
||||||
|
foreach ($mechanics as $mechanic) {
|
||||||
|
$key = $work->id . '_' . $mechanic->id;
|
||||||
|
$mechanicData = $transactions[$key] ?? ['total' => 0, 'completed' => 0, 'pending' => 0];
|
||||||
|
|
||||||
|
$row["mechanic_{$mechanic->id}_total"] = $mechanicData['total'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('DataTable response prepared', [
|
||||||
|
'data_count' => count($data),
|
||||||
|
'mechanics_count' => $mechanics->count(),
|
||||||
|
'works_count' => $works->count()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create DataTable response
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => count($data),
|
||||||
|
'recordsFiltered' => count($data),
|
||||||
|
'data' => $data,
|
||||||
|
'mechanics' => $mechanics,
|
||||||
|
'works' => $works
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in getTechnicianReportDataForDataTable: ' . $e->getMessage(), [
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => request()->input('draw', 1),
|
||||||
|
'recordsTotal' => 0,
|
||||||
|
'recordsFiltered' => 0,
|
||||||
|
'data' => [],
|
||||||
|
'mechanics' => collect(),
|
||||||
|
'works' => collect()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Advanced Database Backup Script for CKB Docker
|
|
||||||
# ===============================================
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
CONTAINER_NAME="ckb-mysql-dev"
|
|
||||||
DB_NAME="ckb_db"
|
|
||||||
DB_USER="root"
|
|
||||||
DB_PASSWORD="root"
|
|
||||||
BACKUP_DIR="./backups"
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
RETENTION_DAYS=30 # Keep backups for 30 days
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Functions
|
|
||||||
print_header() {
|
|
||||||
echo -e "${BLUE}================================================${NC}"
|
|
||||||
echo -e "${BLUE} CKB Database Advanced Backup Tool${NC}"
|
|
||||||
echo -e "${BLUE}================================================${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}✅ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}❌ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_info() {
|
|
||||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_container() {
|
|
||||||
if ! docker ps | grep -q $CONTAINER_NAME; then
|
|
||||||
print_error "Container $CONTAINER_NAME is not running!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_success "Container $CONTAINER_NAME is running"
|
|
||||||
}
|
|
||||||
|
|
||||||
create_backup_dir() {
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
print_info "Backup directory: $BACKUP_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_old_backups() {
|
|
||||||
print_info "Cleaning up backups older than $RETENTION_DAYS days..."
|
|
||||||
|
|
||||||
# Find and delete old backup files
|
|
||||||
old_files=$(find $BACKUP_DIR -name "ckb_backup_*.sql*" -mtime +$RETENTION_DAYS 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ -z "$old_files" ]]; then
|
|
||||||
print_info "No old backup files found"
|
|
||||||
else
|
|
||||||
echo "$old_files" | while read file; do
|
|
||||||
rm -f "$file"
|
|
||||||
print_warning "Deleted old backup: $(basename "$file")"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
backup_full() {
|
|
||||||
local backup_file="$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
|
||||||
print_info "Creating full backup..."
|
|
||||||
|
|
||||||
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--add-drop-table \
|
|
||||||
$DB_NAME > "$backup_file"; then
|
|
||||||
|
|
||||||
print_success "Full backup created: $backup_file"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "Full backup failed!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
backup_structure_only() {
|
|
||||||
local backup_file="$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
|
||||||
print_info "Creating structure-only backup..."
|
|
||||||
|
|
||||||
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
|
||||||
--no-data \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
$DB_NAME > "$backup_file"; then
|
|
||||||
|
|
||||||
print_success "Structure backup created: $backup_file"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "Structure backup failed!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
backup_data_only() {
|
|
||||||
local backup_file="$BACKUP_DIR/ckb_data_${DATE}.sql"
|
|
||||||
print_info "Creating data-only backup..."
|
|
||||||
|
|
||||||
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD \
|
|
||||||
--no-create-info \
|
|
||||||
--single-transaction \
|
|
||||||
$DB_NAME > "$backup_file"; then
|
|
||||||
|
|
||||||
print_success "Data backup created: $backup_file"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "Data backup failed!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
compress_backup() {
|
|
||||||
local file="$1"
|
|
||||||
if [[ -f "$file" ]]; then
|
|
||||||
print_info "Compressing backup..."
|
|
||||||
if gzip "$file"; then
|
|
||||||
print_success "Backup compressed: $file.gz"
|
|
||||||
|
|
||||||
# Show compression ratio
|
|
||||||
original_size=$(stat -c%s "$file" 2>/dev/null || echo "0")
|
|
||||||
compressed_size=$(stat -c%s "$file.gz" 2>/dev/null || echo "0")
|
|
||||||
if [[ $original_size -gt 0 ]]; then
|
|
||||||
ratio=$(echo "scale=1; (1 - $compressed_size/$original_size) * 100" | bc -l 2>/dev/null || echo "0")
|
|
||||||
print_info "Compression ratio: ${ratio}%"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_error "Compression failed!"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
show_backup_stats() {
|
|
||||||
print_info "Backup Statistics:"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
|
|
||||||
# Count backup files
|
|
||||||
total_backups=$(ls -1 $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | wc -l)
|
|
||||||
total_size=$(du -sh $BACKUP_DIR 2>/dev/null | cut -f1)
|
|
||||||
|
|
||||||
echo "Total backups: $total_backups"
|
|
||||||
echo "Total size: $total_size"
|
|
||||||
echo "Newest backup: $(ls -1t $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | head -1 | xargs basename 2>/dev/null || echo "None")"
|
|
||||||
echo "Oldest backup: $(ls -1tr $BACKUP_DIR/ckb_backup_*.sql* 2>/dev/null | head -1 | xargs basename 2>/dev/null || echo "None")"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main Script
|
|
||||||
print_header
|
|
||||||
|
|
||||||
echo "Backup Options:"
|
|
||||||
echo "1. Full backup (structure + data)"
|
|
||||||
echo "2. Structure only backup"
|
|
||||||
echo "3. Data only backup"
|
|
||||||
echo "4. All backup types"
|
|
||||||
echo "5. Show backup statistics"
|
|
||||||
echo "6. Cleanup old backups"
|
|
||||||
echo "7. Exit"
|
|
||||||
echo
|
|
||||||
|
|
||||||
read -p "Choose option (1-7): " -n 1 -r option
|
|
||||||
echo
|
|
||||||
|
|
||||||
case $option in
|
|
||||||
1)
|
|
||||||
check_container
|
|
||||||
create_backup_dir
|
|
||||||
if backup_full; then
|
|
||||||
backup_file="$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
|
||||||
|
|
||||||
read -p "Compress backup? (y/n): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
compress_backup "$backup_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cleanup_old_backups
|
|
||||||
show_backup_stats
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
check_container
|
|
||||||
create_backup_dir
|
|
||||||
if backup_structure_only; then
|
|
||||||
backup_file="$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
|
||||||
|
|
||||||
read -p "Compress backup? (y/n): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
compress_backup "$backup_file"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
check_container
|
|
||||||
create_backup_dir
|
|
||||||
if backup_data_only; then
|
|
||||||
backup_file="$BACKUP_DIR/ckb_data_${DATE}.sql"
|
|
||||||
|
|
||||||
read -p "Compress backup? (y/n): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
compress_backup "$backup_file"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
check_container
|
|
||||||
create_backup_dir
|
|
||||||
|
|
||||||
print_info "Creating all backup types..."
|
|
||||||
|
|
||||||
backup_full && compress_backup "$BACKUP_DIR/ckb_backup_${DATE}.sql"
|
|
||||||
backup_structure_only && compress_backup "$BACKUP_DIR/ckb_structure_${DATE}.sql"
|
|
||||||
backup_data_only && compress_backup "$BACKUP_DIR/ckb_data_${DATE}.sql"
|
|
||||||
|
|
||||||
cleanup_old_backups
|
|
||||||
show_backup_stats
|
|
||||||
;;
|
|
||||||
5)
|
|
||||||
create_backup_dir
|
|
||||||
show_backup_stats
|
|
||||||
;;
|
|
||||||
6)
|
|
||||||
create_backup_dir
|
|
||||||
cleanup_old_backups
|
|
||||||
show_backup_stats
|
|
||||||
;;
|
|
||||||
7)
|
|
||||||
print_info "Goodbye!"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_error "Invalid option!"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
print_success "Backup process completed!"
|
|
||||||
48
backup_db.sh
48
backup_db.sh
@@ -1,48 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
CONTAINER_NAME="ckb-mysql-dev"
|
|
||||||
DB_NAME="ckb_db"
|
|
||||||
DB_USER="root"
|
|
||||||
DB_PASSWORD="root"
|
|
||||||
BACKUP_DIR="./backups"
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
BACKUP_FILE="ckb_backup_${DATE}.sql"
|
|
||||||
|
|
||||||
# Create backup directory if it doesn't exist
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Check if container is running
|
|
||||||
if ! docker ps | grep -q $CONTAINER_NAME; then
|
|
||||||
echo "Error: Container $CONTAINER_NAME is not running!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting database backup..."
|
|
||||||
echo "Container: $CONTAINER_NAME"
|
|
||||||
echo "Database: $DB_NAME"
|
|
||||||
echo "Backup file: $BACKUP_DIR/$BACKUP_FILE"
|
|
||||||
|
|
||||||
# Create backup
|
|
||||||
if docker exec $CONTAINER_NAME mysqldump -u $DB_USER -p$DB_PASSWORD $DB_NAME > "$BACKUP_DIR/$BACKUP_FILE"; then
|
|
||||||
echo "✅ Backup completed successfully!"
|
|
||||||
echo "📁 File saved: $BACKUP_DIR/$BACKUP_FILE"
|
|
||||||
|
|
||||||
# Show file size
|
|
||||||
FILE_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)
|
|
||||||
echo "📊 File size: $FILE_SIZE"
|
|
||||||
|
|
||||||
# Optional: Compress the backup
|
|
||||||
read -p "Do you want to compress the backup? (y/n): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
gzip "$BACKUP_DIR/$BACKUP_FILE"
|
|
||||||
echo "🗜️ Backup compressed: $BACKUP_DIR/$BACKUP_FILE.gz"
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "❌ Backup failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🎉 Backup process completed!"
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateKpiTargetsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('kpi_targets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('target_value');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk mencegah duplikasi target aktif per user (satu target aktif per user)
|
||||||
|
$table->unique(['user_id', 'is_active'], 'kpi_targets_user_active_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('kpi_targets');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateKpiAchievementsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('kpi_achievements', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('kpi_target_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('target_value'); // Menyimpan target value secara langsung untuk historical tracking
|
||||||
|
$table->integer('actual_value')->default(0);
|
||||||
|
$table->decimal('achievement_percentage', 5, 2)->default(0);
|
||||||
|
$table->integer('year');
|
||||||
|
$table->integer('month');
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk mencegah duplikasi achievement per user per bulan
|
||||||
|
// Note: Tidak menggunakan kpi_target_id karena target sekarang permanen per user
|
||||||
|
$table->unique(['user_id', 'year', 'month']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('kpi_achievements');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddClaimedColumnsToTransactionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->timestamp('claimed_at')->nullable()->after('status');
|
||||||
|
$table->unsignedBigInteger('claimed_by')->nullable()->after('claimed_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['claimed_at', 'claimed_by']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateWorkDealerPricesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('work_dealer_prices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('work_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('dealer_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->decimal('price', 15, 2)->default(0.00);
|
||||||
|
$table->string('currency', 3)->default('IDR');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->unique(['work_id', 'dealer_id']);
|
||||||
|
$table->index(['dealer_id', 'is_active']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('work_dealer_prices');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateRoleDealerTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('role_dealer', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('role_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('dealer_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['role_id', 'dealer_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('role_dealer');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreatePrechecksTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('prechecks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('precheck_by')->constrained('users')->onDelete('cascade');
|
||||||
|
$table->timestamp('precheck_at')->nullable();
|
||||||
|
$table->string('police_number');
|
||||||
|
$table->string('spk_number');
|
||||||
|
$table->string('front_image', 255)->nullable();
|
||||||
|
$table->json('front_image_metadata')->nullable();
|
||||||
|
$table->decimal('kilometer', 10, 2);
|
||||||
|
$table->decimal('pressure_high', 10, 2);
|
||||||
|
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||||
|
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||||
|
$table->string('cabin_temperature_image', 255)->nullable();
|
||||||
|
$table->json('cabin_temperature_image_metadata')->nullable();
|
||||||
|
$table->enum('ac_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||||
|
$table->string('ac_image', 255)->nullable();
|
||||||
|
$table->json('ac_image_metadata')->nullable();
|
||||||
|
$table->enum('blower_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||||
|
$table->string('blower_image', 255)->nullable();
|
||||||
|
$table->json('blower_image_metadata')->nullable();
|
||||||
|
$table->enum('evaporator_condition', ['kotor', 'berlendir', 'bocor', 'bersih'])->nullable();
|
||||||
|
$table->string('evaporator_image', 255)->nullable();
|
||||||
|
$table->json('evaporator_image_metadata')->nullable();
|
||||||
|
$table->enum('compressor_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||||
|
$table->text('precheck_notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['transaction_id']);
|
||||||
|
$table->index(['precheck_by']);
|
||||||
|
$table->index(['precheck_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('prechecks');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreatePostchecksTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('postchecks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('postcheck_by')->constrained('users')->onDelete('cascade');
|
||||||
|
$table->timestamp('postcheck_at')->nullable();
|
||||||
|
$table->string('police_number');
|
||||||
|
$table->string('spk_number');
|
||||||
|
$table->string('front_image', 255)->nullable();
|
||||||
|
$table->json('front_image_metadata')->nullable();
|
||||||
|
$table->decimal('kilometer', 10, 2);
|
||||||
|
$table->decimal('pressure_high', 10, 2);
|
||||||
|
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||||
|
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||||
|
$table->string('cabin_temperature_image', 255)->nullable();
|
||||||
|
$table->json('cabin_temperature_image_metadata')->nullable();
|
||||||
|
$table->enum('ac_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||||
|
$table->string('ac_image', 255)->nullable();
|
||||||
|
$table->json('ac_image_metadata')->nullable();
|
||||||
|
$table->enum('blower_condition', ['sudah dibersihkan atau dicuci', 'sudah diganti'])->nullable();
|
||||||
|
$table->string('blower_image', 255)->nullable();
|
||||||
|
$table->json('blower_image_metadata')->nullable();
|
||||||
|
$table->enum('evaporator_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||||
|
$table->string('evaporator_image', 255)->nullable();
|
||||||
|
$table->json('evaporator_image_metadata')->nullable();
|
||||||
|
$table->enum('compressor_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||||
|
$table->text('postcheck_notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['transaction_id']);
|
||||||
|
$table->index(['postcheck_by']);
|
||||||
|
$table->index(['postcheck_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('postchecks');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,18 @@ class MenuSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'name' => 'Histori Stock',
|
'name' => 'Histori Stock',
|
||||||
'link' => 'stock-audit.index'
|
'link' => 'stock-audit.index'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Target',
|
||||||
|
'link' => 'kpi.targets.index'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Stock Produk',
|
||||||
|
'link' => 'reports.stock-product.index'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Teknisi',
|
||||||
|
'link' => 'reports.technician.index'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -47,5 +59,7 @@ class MenuSeeder extends Seeder
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Menu::whereIn('link', ['targets.index','product-categories.index'])->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# CKB Asset Debugging Script
|
|
||||||
echo "🔍 CKB Asset Debugging..."
|
|
||||||
|
|
||||||
echo "🔧 Checking APP_URL configuration:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan config:show app.url
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📁 Checking public directory structure:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app ls -la /var/www/html/public/
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📁 Checking CSS files:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app ls -la /var/www/html/public/css/
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📁 Checking JS files:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app ls -la /var/www/html/public/js/
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🌐 Testing CSS file accessibility:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app curl -I http://localhost/css/app.css
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🌐 Testing JS file accessibility:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app curl -I http://localhost/js/app.js
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📝 Checking nginx error logs:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec nginx-proxy tail -20 /var/log/nginx/error.log
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📝 Checking app nginx error logs:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app tail -20 /var/log/nginx/error.log
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🔧 Checking nginx configuration:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app nginx -t
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🔧 Checking proxy nginx configuration:"
|
|
||||||
docker-compose -f docker-compose.prod.yml exec nginx-proxy nginx -t
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📊 Container status:"
|
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🌐 Testing external access to CSS:"
|
|
||||||
echo "Try accessing: http://localhost:8082/css/app.css"
|
|
||||||
curl -I http://localhost:8082/css/app.css
|
|
||||||
105
deploy-ckb.sh
105
deploy-ckb.sh
@@ -1,105 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# CKB Application Deployment Script
|
|
||||||
# This script sets up SSL certificate and deploys the CKB application
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== CKB Application Deployment Script ==="
|
|
||||||
echo "Domain: bengkel.digitaloasis.xyz"
|
|
||||||
echo "Port: 8082"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${GREEN}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if running as root
|
|
||||||
if [[ $EUID -eq 0 ]]; then
|
|
||||||
print_error "This script should not be run as root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if we're in the correct directory
|
|
||||||
if [ ! -f "docker-compose.prod.yml" ]; then
|
|
||||||
print_error "Please run this script from the CKB application directory (/var/www/ckb)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_status "Starting CKB application deployment..."
|
|
||||||
|
|
||||||
# Step 1: Stop existing containers
|
|
||||||
print_status "Stopping existing containers..."
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
|
|
||||||
# Step 2: Build and start containers
|
|
||||||
print_status "Building and starting containers..."
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
|
|
||||||
# Step 3: Wait for containers to be ready
|
|
||||||
print_status "Waiting for containers to be ready..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Step 4: Check if containers are running
|
|
||||||
print_status "Checking container status..."
|
|
||||||
if docker ps | grep -q "ckb-laravel-app"; then
|
|
||||||
print_status "CKB Laravel app is running"
|
|
||||||
else
|
|
||||||
print_error "CKB Laravel app is not running"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if docker ps | grep -q "ckb-nginx-proxy"; then
|
|
||||||
print_status "CKB Nginx proxy is running"
|
|
||||||
else
|
|
||||||
print_error "CKB Nginx proxy is not running"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 5: Check if port 8082 is accessible
|
|
||||||
print_status "Checking if port 8082 is accessible..."
|
|
||||||
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8082 | grep -q "200\|301\|302"; then
|
|
||||||
print_status "Port 8082 is accessible"
|
|
||||||
else
|
|
||||||
print_warning "Port 8082 might not be accessible yet, waiting..."
|
|
||||||
sleep 5
|
|
||||||
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8082 | grep -q "200\|301\|302"; then
|
|
||||||
print_status "Port 8082 is now accessible"
|
|
||||||
else
|
|
||||||
print_error "Port 8082 is not accessible"
|
|
||||||
print_status "Checking container logs..."
|
|
||||||
docker-compose -f docker-compose.prod.yml logs ckb-nginx-proxy
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_status "CKB application deployment completed successfully!"
|
|
||||||
echo ""
|
|
||||||
print_status "Next steps:"
|
|
||||||
echo "1. Configure Nginx reverse proxy on the main server:"
|
|
||||||
echo " sudo cp nginx-ckb-reverse-proxy.conf /etc/nginx/sites-available/bengkel.digitaloasis.xyz"
|
|
||||||
echo " sudo ln -s /etc/nginx/sites-available/bengkel.digitaloasis.xyz /etc/nginx/sites-enabled/"
|
|
||||||
echo ""
|
|
||||||
echo "2. Generate SSL certificate:"
|
|
||||||
echo " sudo certbot certonly --webroot --webroot-path=/var/www/html --email admin@digitaloasis.xyz --agree-tos --no-eff-email -d bengkel.digitaloasis.xyz -d www.bengkel.digitaloasis.xyz"
|
|
||||||
echo ""
|
|
||||||
echo "3. Test and reload Nginx:"
|
|
||||||
echo " sudo nginx -t"
|
|
||||||
echo " sudo systemctl reload nginx"
|
|
||||||
echo ""
|
|
||||||
print_status "Application will be accessible at: https://bengkel.digitaloasis.xyz"
|
|
||||||
61
deploy.sh
61
deploy.sh
@@ -1,61 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# CKB Production Deployment Script
|
|
||||||
echo "🚀 Starting CKB Production Deployment..."
|
|
||||||
|
|
||||||
# Stop existing containers
|
|
||||||
echo "📦 Stopping existing containers..."
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
|
|
||||||
# Remove old images to force rebuild
|
|
||||||
echo "🗑️ Removing old images..."
|
|
||||||
docker image prune -f
|
|
||||||
docker rmi ckb-app-prod 2>/dev/null || true
|
|
||||||
|
|
||||||
# Build and start containers
|
|
||||||
echo "🔨 Building and starting containers..."
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
|
|
||||||
# Wait for containers to be ready
|
|
||||||
echo "⏳ Waiting for containers to be ready..."
|
|
||||||
sleep 30
|
|
||||||
|
|
||||||
# Clear Laravel caches first
|
|
||||||
echo "🧹 Clearing Laravel caches..."
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan config:clear
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan route:clear
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan view:clear
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan cache:clear
|
|
||||||
|
|
||||||
# Run Laravel optimizations
|
|
||||||
echo "⚡ Running Laravel optimizations..."
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan config:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan route:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan view:cache
|
|
||||||
|
|
||||||
# Run migrations (if needed)
|
|
||||||
echo "🗄️ Running database migrations..."
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app php artisan migrate --force
|
|
||||||
|
|
||||||
# Set proper permissions
|
|
||||||
echo "🔐 Setting proper permissions..."
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app chown -R www-data:www-data /var/www/html/storage
|
|
||||||
docker-compose -f docker-compose.prod.yml exec -T app chown -R www-data:www-data /var/www/html/bootstrap/cache
|
|
||||||
|
|
||||||
# Show container status
|
|
||||||
echo "📊 Container status:"
|
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
|
||||||
|
|
||||||
# Show logs for debugging
|
|
||||||
echo "📝 Recent logs:"
|
|
||||||
docker-compose -f docker-compose.prod.yml logs --tail=20
|
|
||||||
|
|
||||||
echo "✅ Deployment completed!"
|
|
||||||
echo "🌐 Application should be available at: http://localhost:8082"
|
|
||||||
echo ""
|
|
||||||
echo "🔍 Testing asset URLs:"
|
|
||||||
echo "CSS: http://localhost:8082/assets/css/app.bundle.min.css"
|
|
||||||
echo "JS: http://localhost:8082/assets/js/app.bundle.min.js"
|
|
||||||
echo ""
|
|
||||||
echo "To check logs: docker-compose -f docker-compose.prod.yml logs -f"
|
|
||||||
echo "To check app logs: docker-compose -f docker-compose.prod.yml logs -f app"
|
|
||||||
100
dev-restart.sh
100
dev-restart.sh
@@ -1,100 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Development Restart Script
|
|
||||||
# Usage: ./dev-restart.sh [cache|config|routes|all|container]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
ACTION=${1:-cache}
|
|
||||||
|
|
||||||
print_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
case $ACTION in
|
|
||||||
cache)
|
|
||||||
print_info "Clearing Laravel cache..."
|
|
||||||
docker-compose exec app php artisan cache:clear
|
|
||||||
print_success "Cache cleared!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
config)
|
|
||||||
print_info "Clearing configuration cache..."
|
|
||||||
docker-compose exec app php artisan config:clear
|
|
||||||
print_success "Config cache cleared!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
routes)
|
|
||||||
print_info "Clearing route cache..."
|
|
||||||
docker-compose exec app php artisan route:clear
|
|
||||||
print_success "Route cache cleared!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
all)
|
|
||||||
print_info "Clearing all Laravel caches..."
|
|
||||||
docker-compose exec app php artisan optimize:clear
|
|
||||||
print_success "All caches cleared!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
container)
|
|
||||||
print_info "Restarting app container..."
|
|
||||||
docker-compose restart app
|
|
||||||
print_success "Container restarted!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
build)
|
|
||||||
print_info "Rebuilding app container..."
|
|
||||||
docker-compose up -d --build app
|
|
||||||
print_success "Container rebuilt!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
migrate)
|
|
||||||
print_info "Running database migrations..."
|
|
||||||
docker-compose exec app php artisan migrate
|
|
||||||
print_success "Migrations completed!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
composer)
|
|
||||||
print_info "Installing/updating composer dependencies..."
|
|
||||||
docker-compose exec app composer install --optimize-autoloader
|
|
||||||
print_success "Composer dependencies updated!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
npm)
|
|
||||||
print_info "Installing npm dependencies and building assets..."
|
|
||||||
docker-compose exec app npm install
|
|
||||||
docker-compose exec app npm run dev
|
|
||||||
print_success "Frontend assets built!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Development Restart Script"
|
|
||||||
echo "Usage: $0 [cache|config|routes|all|container|build|migrate|composer|npm]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " cache - Clear application cache only"
|
|
||||||
echo " config - Clear configuration cache"
|
|
||||||
echo " routes - Clear route cache"
|
|
||||||
echo " all - Clear all Laravel caches"
|
|
||||||
echo " container - Restart app container"
|
|
||||||
echo " build - Rebuild app container"
|
|
||||||
echo " migrate - Run database migrations"
|
|
||||||
echo " composer - Update composer dependencies"
|
|
||||||
echo " npm - Update npm and build assets"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
- ./storage:/var/www/html/storage
|
- ./storage:/var/www/html/storage
|
||||||
- ./bootstrap/cache:/var/www/html/bootstrap/cache
|
- ./bootstrap/cache:/var/www/html/bootstrap/cache
|
||||||
- ./docker/php.ini:/usr/local/etc/php/conf.d/local.ini
|
- ./docker/php.ini:/usr/local/etc/php/conf.d/local.ini
|
||||||
|
- ./.env:/var/www/html/.env
|
||||||
- ckb_storage_logs:/var/www/html/storage/logs
|
- ckb_storage_logs:/var/www/html/storage/logs
|
||||||
- ckb_storage_cache:/var/www/html/storage/framework
|
- ckb_storage_cache:/var/www/html/storage/framework
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk deploy CKB Laravel Application ke production dengan domain bengkel.digitaloasis.xyz
|
|
||||||
# Usage: ./docker-deploy-prod.sh [build|deploy|ssl|status]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
DOMAIN="bengkel.digitaloasis.xyz"
|
|
||||||
EMAIL="admin@digitaloasis.xyz"
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
ENV_FILE=".env"
|
|
||||||
|
|
||||||
# Default action
|
|
||||||
ACTION="deploy"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
build)
|
|
||||||
ACTION="build"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
deploy)
|
|
||||||
ACTION="deploy"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
ssl)
|
|
||||||
ACTION="ssl"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
ACTION="status"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [build|deploy|ssl|status]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check prerequisites
|
|
||||||
check_prerequisites() {
|
|
||||||
print_status "Checking prerequisites..."
|
|
||||||
|
|
||||||
# Check Docker
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
print_error "Docker is not running. Please start Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Docker Compose
|
|
||||||
if ! docker-compose --version > /dev/null 2>&1; then
|
|
||||||
print_error "Docker Compose is not installed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if .env file exists
|
|
||||||
if [[ ! -f $ENV_FILE ]]; then
|
|
||||||
print_warning "Environment file not found. Creating from production template..."
|
|
||||||
if [[ -f docker/env.example.production ]]; then
|
|
||||||
cp docker/env.example.production $ENV_FILE
|
|
||||||
print_warning "⚠️ IMPORTANT: Edit $ENV_FILE and change all CHANGE_THIS_* passwords before continuing!"
|
|
||||||
print_status "Production template copied. Please configure:"
|
|
||||||
echo " - DB_PASSWORD and DB_ROOT_PASSWORD"
|
|
||||||
echo " - REDIS_PASSWORD"
|
|
||||||
echo " - MAIL_* settings"
|
|
||||||
echo " - AWS_* settings (if using S3)"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
print_error "Production environment template not found: docker/env.example.production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Prerequisites check passed!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup production environment
|
|
||||||
setup_production_env() {
|
|
||||||
print_status "Setting up production environment variables..."
|
|
||||||
|
|
||||||
# Update .env for production
|
|
||||||
sed -i "s|APP_ENV=.*|APP_ENV=production|g" $ENV_FILE
|
|
||||||
sed -i "s|APP_DEBUG=.*|APP_DEBUG=false|g" $ENV_FILE
|
|
||||||
sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|g" $ENV_FILE
|
|
||||||
|
|
||||||
# Check if database credentials are set
|
|
||||||
if grep -q "DB_PASSWORD=password" $ENV_FILE; then
|
|
||||||
print_warning "Please update database credentials in $ENV_FILE for production!"
|
|
||||||
print_warning "Current settings are for development only."
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Production environment configured!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to build containers
|
|
||||||
build_containers() {
|
|
||||||
print_status "Building production containers..."
|
|
||||||
|
|
||||||
# Pull latest images
|
|
||||||
docker-compose -f $COMPOSE_FILE pull
|
|
||||||
|
|
||||||
# Build application container
|
|
||||||
docker-compose -f $COMPOSE_FILE build --no-cache app
|
|
||||||
|
|
||||||
print_success "Containers built successfully!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to deploy application
|
|
||||||
deploy_application() {
|
|
||||||
print_status "Deploying CKB Laravel Application to production..."
|
|
||||||
|
|
||||||
# Stop existing containers
|
|
||||||
print_status "Stopping existing containers..."
|
|
||||||
docker-compose -f $COMPOSE_FILE down || true
|
|
||||||
|
|
||||||
# Start database and redis first
|
|
||||||
print_status "Starting database and Redis..."
|
|
||||||
docker-compose -f $COMPOSE_FILE up -d db redis
|
|
||||||
|
|
||||||
# Wait for database to be ready
|
|
||||||
print_status "Waiting for database to be ready..."
|
|
||||||
sleep 20
|
|
||||||
|
|
||||||
# Start application
|
|
||||||
print_status "Starting application..."
|
|
||||||
docker-compose -f $COMPOSE_FILE up -d app
|
|
||||||
|
|
||||||
# Wait for application to be ready
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Run Laravel setup commands
|
|
||||||
print_status "Running Laravel setup commands..."
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan key:generate --force || true
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan migrate --force
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan config:cache
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan route:cache
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan view:cache
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan storage:link || true
|
|
||||||
|
|
||||||
# Start nginx proxy
|
|
||||||
print_status "Starting nginx proxy..."
|
|
||||||
docker-compose -f $COMPOSE_FILE up -d nginx-proxy
|
|
||||||
|
|
||||||
print_success "Application deployed successfully!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup SSL
|
|
||||||
setup_ssl() {
|
|
||||||
print_status "Setting up SSL certificate..."
|
|
||||||
|
|
||||||
if [[ -f docker-ssl-setup.sh ]]; then
|
|
||||||
chmod +x docker-ssl-setup.sh
|
|
||||||
./docker-ssl-setup.sh
|
|
||||||
else
|
|
||||||
print_error "SSL setup script not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show deployment status
|
|
||||||
show_status() {
|
|
||||||
print_status "CKB Production Deployment Status"
|
|
||||||
echo "================================================"
|
|
||||||
|
|
||||||
# Show container status
|
|
||||||
print_status "Container Status:"
|
|
||||||
docker-compose -f $COMPOSE_FILE ps
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show application health
|
|
||||||
print_status "Application Health:"
|
|
||||||
if curl -s --max-time 10 http://localhost/health > /dev/null 2>&1; then
|
|
||||||
print_success "✅ Application is responding on HTTP"
|
|
||||||
else
|
|
||||||
print_warning "❌ Application not responding on HTTP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -s --max-time 10 https://$DOMAIN/health > /dev/null 2>&1; then
|
|
||||||
print_success "✅ Application is responding on HTTPS"
|
|
||||||
else
|
|
||||||
print_warning "❌ Application not responding on HTTPS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show SSL certificate status
|
|
||||||
print_status "SSL Certificate Status:"
|
|
||||||
if openssl s_client -connect $DOMAIN:443 -servername $DOMAIN < /dev/null 2>/dev/null | openssl x509 -noout -dates 2>/dev/null; then
|
|
||||||
print_success "✅ SSL certificate is active"
|
|
||||||
else
|
|
||||||
print_warning "❌ SSL certificate not found or invalid"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show disk usage
|
|
||||||
print_status "Docker Disk Usage:"
|
|
||||||
docker system df
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show logs summary
|
|
||||||
print_status "Recent Application Logs:"
|
|
||||||
docker-compose -f $COMPOSE_FILE logs --tail=10 app || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "Access URLs:"
|
|
||||||
echo " 🌐 Application: https://$DOMAIN"
|
|
||||||
echo " 🔍 Health Check: https://$DOMAIN/health"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
print_status "Useful Commands:"
|
|
||||||
echo " - View logs: docker-compose -f $COMPOSE_FILE logs -f [service]"
|
|
||||||
echo " - Enter container: docker-compose -f $COMPOSE_FILE exec app bash"
|
|
||||||
echo " - Update SSL: ./docker-ssl-setup.sh"
|
|
||||||
echo " - Restart app: docker-compose -f $COMPOSE_FILE restart app"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to backup before deployment
|
|
||||||
backup_before_deploy() {
|
|
||||||
print_status "Creating backup before deployment..."
|
|
||||||
|
|
||||||
BACKUP_DIR="backups/$(date +%Y%m%d_%H%M%S)"
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Backup database
|
|
||||||
if docker-compose -f $COMPOSE_FILE ps db | grep -q "Up"; then
|
|
||||||
print_status "Backing up database..."
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T db mysqldump -u root -p"${DB_ROOT_PASSWORD:-rootpassword}" "${DB_DATABASE:-ckb_production}" > "$BACKUP_DIR/database.sql"
|
|
||||||
print_success "Database backed up to $BACKUP_DIR/database.sql"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backup storage
|
|
||||||
if [[ -d storage ]]; then
|
|
||||||
print_status "Backing up storage directory..."
|
|
||||||
tar -czf "$BACKUP_DIR/storage.tar.gz" storage/
|
|
||||||
print_success "Storage backed up to $BACKUP_DIR/storage.tar.gz"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backup environment
|
|
||||||
if [[ -f $ENV_FILE ]]; then
|
|
||||||
cp $ENV_FILE "$BACKUP_DIR/env.backup"
|
|
||||||
print_success "Environment backed up to $BACKUP_DIR/env.backup"
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Backup completed in $BACKUP_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to optimize for production
|
|
||||||
optimize_production() {
|
|
||||||
print_status "Optimizing application for production..."
|
|
||||||
|
|
||||||
# Laravel optimizations
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app composer install --optimize-autoloader --no-dev --no-interaction
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan config:cache
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan route:cache
|
|
||||||
docker-compose -f $COMPOSE_FILE exec -T app php artisan view:cache
|
|
||||||
|
|
||||||
# Clean up Docker
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
print_success "Production optimization completed!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🚀 CKB Production Deployment Script"
|
|
||||||
print_status "Domain: $DOMAIN"
|
|
||||||
print_status "Action: $ACTION"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_prerequisites
|
|
||||||
|
|
||||||
case $ACTION in
|
|
||||||
build)
|
|
||||||
print_status "Building containers only..."
|
|
||||||
build_containers
|
|
||||||
print_success "✅ Build completed!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
deploy)
|
|
||||||
print_status "Full deployment process..."
|
|
||||||
backup_before_deploy
|
|
||||||
setup_production_env
|
|
||||||
build_containers
|
|
||||||
deploy_application
|
|
||||||
optimize_production
|
|
||||||
print_success "✅ Deployment completed!"
|
|
||||||
echo ""
|
|
||||||
print_status "Next steps:"
|
|
||||||
echo "1. Setup SSL certificate: ./docker-deploy-prod.sh ssl"
|
|
||||||
echo "2. Check status: ./docker-deploy-prod.sh status"
|
|
||||||
;;
|
|
||||||
|
|
||||||
ssl)
|
|
||||||
print_status "Setting up SSL certificate..."
|
|
||||||
setup_ssl
|
|
||||||
print_success "✅ SSL setup completed!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
status)
|
|
||||||
show_status
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "✅ Production deployment script completed!"
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk memperbaiki permission Laravel storage di Docker container
|
|
||||||
# Usage: ./docker-fix-permissions.sh [dev|prod]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [dev|prod]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if containers are running
|
|
||||||
check_containers() {
|
|
||||||
local compose_file=""
|
|
||||||
local app_container=""
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
compose_file="docker-compose.yml"
|
|
||||||
app_container="ckb-app-dev"
|
|
||||||
else
|
|
||||||
compose_file="docker-compose.prod.yml"
|
|
||||||
app_container="ckb-app-prod"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker-compose -f "$compose_file" ps | grep -q "$app_container.*Up"; then
|
|
||||||
print_error "App container is not running!"
|
|
||||||
print_status "Please start the containers first:"
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
echo " ./docker-start.sh dev up"
|
|
||||||
else
|
|
||||||
echo " ./docker-start.sh prod up"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
COMPOSE_FILE="$compose_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to create necessary directories
|
|
||||||
create_directories() {
|
|
||||||
print_status "Creating necessary Laravel directories..."
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/storage/logs
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/storage/framework/cache
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/storage/framework/sessions
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/storage/framework/views
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/storage/app/public
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app mkdir -p /var/www/html/bootstrap/cache
|
|
||||||
|
|
||||||
print_success "Directories created!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to fix ownership
|
|
||||||
fix_ownership() {
|
|
||||||
print_status "Fixing ownership to www-data:www-data..."
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chown -R www-data:www-data /var/www/html/storage
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chown -R www-data:www-data /var/www/html/bootstrap/cache
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chown -R www-data:www-data /var/www/html/public
|
|
||||||
|
|
||||||
print_success "Ownership fixed!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to fix permissions
|
|
||||||
fix_permissions() {
|
|
||||||
print_status "Setting proper permissions (775)..."
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chmod -R 775 /var/www/html/storage
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chmod -R 775 /var/www/html/bootstrap/cache
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app chmod -R 755 /var/www/html/public
|
|
||||||
|
|
||||||
print_success "Permissions set!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to create .gitkeep files
|
|
||||||
create_gitkeep() {
|
|
||||||
print_status "Creating .gitkeep files for empty directories..."
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app touch /var/www/html/storage/logs/.gitkeep
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app touch /var/www/html/storage/framework/cache/.gitkeep
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app touch /var/www/html/storage/framework/sessions/.gitkeep
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app touch /var/www/html/storage/framework/views/.gitkeep
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app touch /var/www/html/storage/app/.gitkeep
|
|
||||||
|
|
||||||
print_success ".gitkeep files created!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test Laravel logging
|
|
||||||
test_logging() {
|
|
||||||
print_status "Testing Laravel logging..."
|
|
||||||
|
|
||||||
# Try to write to Laravel log
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec app php -r "file_put_contents('/var/www/html/storage/logs/laravel.log', 'Test log entry: ' . date('Y-m-d H:i:s') . PHP_EOL, FILE_APPEND | LOCK_EX);"; then
|
|
||||||
print_success "Laravel logging test passed!"
|
|
||||||
else
|
|
||||||
print_error "Laravel logging test failed!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test Laravel cache
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec app php artisan cache:clear > /dev/null 2>&1; then
|
|
||||||
print_success "Laravel cache test passed!"
|
|
||||||
else
|
|
||||||
print_warning "Laravel cache test failed (might be normal if no cache driver configured)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to create storage link
|
|
||||||
create_storage_link() {
|
|
||||||
print_status "Creating storage symbolic link..."
|
|
||||||
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec app php artisan storage:link > /dev/null 2>&1; then
|
|
||||||
print_success "Storage link created!"
|
|
||||||
else
|
|
||||||
print_warning "Storage link creation failed (might already exist)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show current permissions
|
|
||||||
show_permissions() {
|
|
||||||
print_status "Current storage permissions:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app ls -la /var/www/html/storage/
|
|
||||||
echo ""
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app ls -la /var/www/html/storage/logs/ || true
|
|
||||||
echo ""
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app ls -la /var/www/html/bootstrap/cache/ || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show troubleshooting tips
|
|
||||||
show_tips() {
|
|
||||||
echo ""
|
|
||||||
print_status "🔧 Troubleshooting Tips:"
|
|
||||||
echo ""
|
|
||||||
echo "1. If you still get permission errors, try rebuilding containers:"
|
|
||||||
echo " ./docker-rebuild.sh $ENVIRONMENT"
|
|
||||||
echo ""
|
|
||||||
echo "2. For persistent permission issues, add this to your docker-compose volumes:"
|
|
||||||
echo " - ./storage:/var/www/html/storage"
|
|
||||||
echo ""
|
|
||||||
echo "3. Check Laravel .env file for correct LOG_CHANNEL setting:"
|
|
||||||
echo " LOG_CHANNEL=stack"
|
|
||||||
echo ""
|
|
||||||
echo "4. Monitor logs for more errors:"
|
|
||||||
echo " docker-compose -f $COMPOSE_FILE logs -f app"
|
|
||||||
echo ""
|
|
||||||
echo "5. Test Laravel application:"
|
|
||||||
echo " docker-compose -f $COMPOSE_FILE exec app php artisan tinker"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
print_status "🔧 Laravel Storage Permission Fix"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_containers
|
|
||||||
|
|
||||||
# Show current state
|
|
||||||
print_status "Before fixing - Current permissions:"
|
|
||||||
show_permissions
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "Fixing permissions..."
|
|
||||||
|
|
||||||
# Execute fix process
|
|
||||||
create_directories
|
|
||||||
fix_ownership
|
|
||||||
fix_permissions
|
|
||||||
create_gitkeep
|
|
||||||
create_storage_link
|
|
||||||
test_logging
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "After fixing - Updated permissions:"
|
|
||||||
show_permissions
|
|
||||||
|
|
||||||
# Show final status
|
|
||||||
echo ""
|
|
||||||
print_success "✅ Permission fix completed!"
|
|
||||||
|
|
||||||
# Show troubleshooting tips
|
|
||||||
show_tips
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk mengimport database backup ke MySQL Docker container
|
|
||||||
# Usage: ./docker-import-db.sh [dev|prod] [database_file.sql]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
SQL_FILE="ckb.sql"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*.sql)
|
|
||||||
SQL_FILE="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [dev|prod] [database_file.sql]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if file exists
|
|
||||||
check_sql_file() {
|
|
||||||
if [[ ! -f "$SQL_FILE" ]]; then
|
|
||||||
print_error "SQL file '$SQL_FILE' not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_status "Found SQL file: $SQL_FILE ($(du -h "$SQL_FILE" | cut -f1))"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if containers are running
|
|
||||||
check_containers() {
|
|
||||||
local compose_file=""
|
|
||||||
local db_container=""
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
compose_file="docker-compose.yml"
|
|
||||||
db_container="ckb-mysql"
|
|
||||||
else
|
|
||||||
compose_file="docker-compose.prod.yml"
|
|
||||||
db_container="ckb-mysql-prod"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker-compose -f "$compose_file" ps | grep -q "$db_container.*Up"; then
|
|
||||||
print_error "Database container is not running!"
|
|
||||||
print_status "Please start the containers first:"
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
echo " ./docker-start.sh dev up"
|
|
||||||
else
|
|
||||||
echo " ./docker-start.sh prod up"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to get database credentials
|
|
||||||
get_db_credentials() {
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
DB_HOST="ckb-mysql"
|
|
||||||
DB_NAME="ckb_db"
|
|
||||||
DB_USER="root"
|
|
||||||
DB_PASSWORD="root"
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
else
|
|
||||||
DB_HOST="ckb-mysql-prod"
|
|
||||||
DB_NAME="ckb_production"
|
|
||||||
DB_USER="root"
|
|
||||||
# For production, we should read from environment or prompt
|
|
||||||
if [[ -f .env.production ]]; then
|
|
||||||
DB_PASSWORD=$(grep DB_ROOT_PASSWORD .env.production | cut -d '=' -f2)
|
|
||||||
else
|
|
||||||
read -s -p "Enter MySQL root password for production: " DB_PASSWORD
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to backup existing database
|
|
||||||
backup_existing_db() {
|
|
||||||
local backup_file="backup_before_import_$(date +%Y%m%d_%H%M%S).sql"
|
|
||||||
|
|
||||||
print_status "Creating backup of existing database..."
|
|
||||||
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T db mysqldump -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "$backup_file" 2>/dev/null; then
|
|
||||||
print_success "Existing database backed up to: $backup_file"
|
|
||||||
else
|
|
||||||
print_warning "Could not backup existing database (it might be empty)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to import database
|
|
||||||
import_database() {
|
|
||||||
print_status "Importing database from $SQL_FILE..."
|
|
||||||
print_status "This may take a while for large files..."
|
|
||||||
|
|
||||||
# Drop and recreate database to ensure clean import
|
|
||||||
print_status "Recreating database..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T db mysql -u "$DB_USER" -p"$DB_PASSWORD" -e "DROP DATABASE IF EXISTS $DB_NAME;"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T db mysql -u "$DB_USER" -p"$DB_PASSWORD" -e "CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
|
||||||
|
|
||||||
# Import the SQL file
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T db mysql -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" < "$SQL_FILE"; then
|
|
||||||
print_success "Database imported successfully!"
|
|
||||||
else
|
|
||||||
print_error "Failed to import database!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to run Laravel migrations (if needed)
|
|
||||||
run_migrations() {
|
|
||||||
print_status "Checking if Laravel migrations need to be run..."
|
|
||||||
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec app php artisan migrate:status > /dev/null 2>&1; then
|
|
||||||
print_status "Running any pending migrations..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app php artisan migrate --force
|
|
||||||
else
|
|
||||||
print_warning "Could not check migration status. Skipping migrations."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to clear Laravel cache
|
|
||||||
clear_cache() {
|
|
||||||
print_status "Clearing Laravel cache..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app php artisan cache:clear || true
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app php artisan config:clear || true
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec app php artisan view:clear || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
print_status "Database Import Script for CKB Laravel Application"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
print_status "SQL File: $SQL_FILE"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_sql_file
|
|
||||||
get_db_credentials
|
|
||||||
check_containers
|
|
||||||
|
|
||||||
# Ask for confirmation
|
|
||||||
echo ""
|
|
||||||
print_warning "This will replace the existing database in $ENVIRONMENT environment!"
|
|
||||||
read -p "Are you sure you want to continue? (y/N): " -n 1 -r
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
print_status "Import cancelled."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Execute import
|
|
||||||
backup_existing_db
|
|
||||||
import_database
|
|
||||||
|
|
||||||
# Post-import tasks
|
|
||||||
print_status "Running post-import tasks..."
|
|
||||||
run_migrations
|
|
||||||
clear_cache
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "Database import completed successfully!"
|
|
||||||
print_status "Database: $DB_NAME"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
echo ""
|
|
||||||
print_status "You can now access your application at:"
|
|
||||||
echo " - Web App: http://localhost:8000"
|
|
||||||
echo " - phpMyAdmin: http://localhost:8080"
|
|
||||||
fi
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Quick Setup Script untuk CKB Laravel Application dengan Auto Import Database
|
|
||||||
# Usage: ./docker-quick-setup.sh [dev|prod]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [dev|prod]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if Docker is running
|
|
||||||
check_docker() {
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
print_error "Docker is not running. Please start Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup environment file
|
|
||||||
setup_env() {
|
|
||||||
if [[ ! -f .env ]]; then
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
if [[ -f docker/env.example.local ]]; then
|
|
||||||
print_status "Setting up local development environment file..."
|
|
||||||
cp docker/env.example.local .env
|
|
||||||
print_success "Local environment file created: .env"
|
|
||||||
else
|
|
||||||
print_error "Local environment template not found: docker/env.example.local"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ -f docker/env.example.production ]]; then
|
|
||||||
print_status "Setting up production environment file..."
|
|
||||||
cp docker/env.example.production .env
|
|
||||||
print_success "Production environment file created: .env"
|
|
||||||
print_warning "⚠️ IMPORTANT: Edit .env and change all CHANGE_THIS_* passwords!"
|
|
||||||
else
|
|
||||||
print_error "Production environment template not found: docker/env.example.production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_status "Environment file already exists: .env"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if database file exists
|
|
||||||
check_db_file() {
|
|
||||||
if [[ ! -f ckb.sql ]]; then
|
|
||||||
print_error "Database backup file 'ckb.sql' not found!"
|
|
||||||
print_status "Please make sure you have the ckb.sql file in the project root."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_status "Found database backup: ckb.sql ($(du -h ckb.sql | cut -f1))"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to start containers
|
|
||||||
start_containers() {
|
|
||||||
print_status "Starting Docker containers for $ENVIRONMENT environment..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
print_status "This will start development environment with:"
|
|
||||||
echo " - MySQL with auto-import from ckb.sql"
|
|
||||||
echo " - Redis for caching"
|
|
||||||
echo " - phpMyAdmin for database management"
|
|
||||||
echo " - MailHog for email testing"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
print_success "Development containers started!"
|
|
||||||
echo ""
|
|
||||||
print_status "Services are starting up... Please wait..."
|
|
||||||
|
|
||||||
# Wait for MySQL to be ready
|
|
||||||
print_status "Waiting for MySQL to be ready..."
|
|
||||||
sleep 20
|
|
||||||
|
|
||||||
# Wait a bit more for MySQL to be fully ready
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Check if database was imported automatically
|
|
||||||
if docker-compose exec -T db mysql -u root -proot -e "USE ckb_db; SHOW TABLES;" > /dev/null 2>&1; then
|
|
||||||
table_count=$(docker-compose exec -T db mysql -u root -proot -e "USE ckb_db; SHOW TABLES;" 2>/dev/null | wc -l)
|
|
||||||
if [[ $table_count -gt 1 ]]; then
|
|
||||||
print_success "Database automatically imported from ckb.sql!"
|
|
||||||
else
|
|
||||||
print_warning "Database not imported automatically. Running manual import..."
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_warning "Database not accessible. Running manual import..."
|
|
||||||
sleep 15
|
|
||||||
./docker-import-db.sh dev
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
print_status "Starting production environment..."
|
|
||||||
if [[ ! -f .env.production ]]; then
|
|
||||||
print_warning "Creating production environment file..."
|
|
||||||
cp docker/env.example .env.production
|
|
||||||
print_warning "Please edit .env.production with your production settings!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
print_success "Production containers started!"
|
|
||||||
|
|
||||||
sleep 15
|
|
||||||
print_status "Importing database for production..."
|
|
||||||
./docker-import-db.sh prod
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to generate application key
|
|
||||||
generate_app_key() {
|
|
||||||
print_status "Generating Laravel application key..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose exec app php artisan key:generate
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan key:generate
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to run Laravel setup commands
|
|
||||||
setup_laravel() {
|
|
||||||
print_status "Setting up Laravel application..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
COMPOSE_CMD="docker-compose exec app"
|
|
||||||
else
|
|
||||||
COMPOSE_CMD="docker-compose -f docker-compose.prod.yml exec app"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clear caches
|
|
||||||
$COMPOSE_CMD php artisan cache:clear || true
|
|
||||||
$COMPOSE_CMD php artisan config:clear || true
|
|
||||||
$COMPOSE_CMD php artisan view:clear || true
|
|
||||||
|
|
||||||
# Set up storage links
|
|
||||||
$COMPOSE_CMD php artisan storage:link || true
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "prod" ]]; then
|
|
||||||
print_status "Optimizing for production..."
|
|
||||||
$COMPOSE_CMD php artisan config:cache
|
|
||||||
$COMPOSE_CMD php artisan route:cache
|
|
||||||
$COMPOSE_CMD php artisan view:cache
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show access information
|
|
||||||
show_access_info() {
|
|
||||||
echo ""
|
|
||||||
print_success "🎉 CKB Laravel Application is now ready!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
print_status "Development Environment Access:"
|
|
||||||
echo " 🌐 Web Application: http://localhost:8000"
|
|
||||||
echo " 📊 phpMyAdmin: http://localhost:8080"
|
|
||||||
echo " - Server: db"
|
|
||||||
echo " - Username: root"
|
|
||||||
echo " - Password: root"
|
|
||||||
echo " - Database: ckb_db"
|
|
||||||
echo ""
|
|
||||||
echo " 📧 MailHog (Email Testing): http://localhost:8025"
|
|
||||||
echo " 🗄️ MySQL Direct: localhost:3306"
|
|
||||||
echo " 🔴 Redis: localhost:6379"
|
|
||||||
echo ""
|
|
||||||
print_status "Useful Commands:"
|
|
||||||
echo " - View logs: docker-compose logs -f"
|
|
||||||
echo " - Access container: docker-compose exec app bash"
|
|
||||||
echo " - Laravel commands: docker-compose exec app php artisan [command]"
|
|
||||||
echo " - Stop containers: docker-compose down"
|
|
||||||
else
|
|
||||||
print_status "Production Environment Access:"
|
|
||||||
echo " 🌐 Web Application: http://localhost (port 80)"
|
|
||||||
echo " 🗄️ Database: localhost:3306"
|
|
||||||
echo ""
|
|
||||||
print_status "Useful Commands:"
|
|
||||||
echo " - View logs: docker-compose -f docker-compose.prod.yml logs -f"
|
|
||||||
echo " - Access container: docker-compose -f docker-compose.prod.yml exec app bash"
|
|
||||||
echo " - Stop containers: docker-compose -f docker-compose.prod.yml down"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "Database has been imported from ckb.sql successfully!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🚀 CKB Laravel Application Quick Setup"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_docker
|
|
||||||
check_db_file
|
|
||||||
|
|
||||||
# Setup process
|
|
||||||
setup_env
|
|
||||||
start_containers
|
|
||||||
generate_app_key
|
|
||||||
setup_laravel
|
|
||||||
|
|
||||||
# Show final information
|
|
||||||
show_access_info
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "✅ Quick setup completed successfully!"
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk rebuild Docker containers dari scratch
|
|
||||||
# Usage: ./docker-rebuild.sh [dev|prod]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [dev|prod]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if Docker is running
|
|
||||||
check_docker() {
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
print_error "Docker is not running. Please start Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to clean up existing containers and images
|
|
||||||
cleanup_containers() {
|
|
||||||
print_status "Cleaning up existing containers and images..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
APP_CONTAINER="ckb-app-dev"
|
|
||||||
else
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
APP_CONTAINER="ckb-app-prod"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop and remove containers
|
|
||||||
print_status "Stopping containers..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" down || true
|
|
||||||
|
|
||||||
# Remove specific containers if they exist
|
|
||||||
if docker ps -a --format "table {{.Names}}" | grep -q "$APP_CONTAINER"; then
|
|
||||||
print_status "Removing existing app container..."
|
|
||||||
docker rm -f "$APP_CONTAINER" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove images related to this project
|
|
||||||
print_status "Removing existing images..."
|
|
||||||
docker images | grep "ckb" | awk '{print $3}' | xargs docker rmi -f || true
|
|
||||||
|
|
||||||
print_success "Cleanup completed!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to prune Docker system
|
|
||||||
prune_docker() {
|
|
||||||
print_status "Pruning Docker system to free up space..."
|
|
||||||
|
|
||||||
# Remove unused containers, networks, images
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# Remove unused volumes (be careful with this)
|
|
||||||
print_warning "Removing unused Docker volumes..."
|
|
||||||
docker volume prune -f
|
|
||||||
|
|
||||||
print_success "Docker system pruned!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to build containers
|
|
||||||
build_containers() {
|
|
||||||
print_status "Building containers for $ENVIRONMENT environment..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
print_status "Building development container..."
|
|
||||||
docker-compose build --no-cache --pull
|
|
||||||
print_success "Development container built successfully!"
|
|
||||||
else
|
|
||||||
print_status "Building production container..."
|
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache --pull
|
|
||||||
print_success "Production container built successfully!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test build
|
|
||||||
test_build() {
|
|
||||||
print_status "Testing the build..."
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
else
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start containers to test
|
|
||||||
print_status "Starting containers for testing..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" up -d
|
|
||||||
|
|
||||||
# Wait for containers to be ready
|
|
||||||
print_status "Waiting for containers to be ready..."
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Test PHP version and extensions
|
|
||||||
print_status "Testing PHP and extensions..."
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php -v; then
|
|
||||||
print_success "PHP is working!"
|
|
||||||
else
|
|
||||||
print_error "PHP test failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test PHP extensions
|
|
||||||
print_status "Checking PHP extensions..."
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T app php -m | grep -E "(curl|gd|zip|dom|mysql|redis)" || true
|
|
||||||
|
|
||||||
# Test Redis connection
|
|
||||||
print_status "Testing Redis connection..."
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php -r "try { \$redis = new Redis(); \$redis->connect('redis', 6379); echo 'OK'; } catch (Exception \$e) { echo 'FAILED'; }" | grep -q "OK"; then
|
|
||||||
print_success "Redis connection test passed!"
|
|
||||||
else
|
|
||||||
print_warning "Redis connection test failed!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test Laravel
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php artisan --version; then
|
|
||||||
print_success "Laravel is working!"
|
|
||||||
else
|
|
||||||
print_error "Laravel test failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Build test completed successfully!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show next steps
|
|
||||||
show_next_steps() {
|
|
||||||
echo ""
|
|
||||||
print_success "🎉 Rebuild completed successfully!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
print_status "Development environment is ready!"
|
|
||||||
echo ""
|
|
||||||
print_status "Next steps:"
|
|
||||||
echo " 1. Import your database:"
|
|
||||||
echo " ./docker-import-db.sh dev"
|
|
||||||
echo ""
|
|
||||||
echo " 2. Access your application:"
|
|
||||||
echo " - Web App: http://localhost:8000"
|
|
||||||
echo " - phpMyAdmin: http://localhost:8080"
|
|
||||||
echo ""
|
|
||||||
echo " 3. Or use quick setup:"
|
|
||||||
echo " ./docker-quick-setup.sh dev"
|
|
||||||
else
|
|
||||||
print_status "Production environment is ready!"
|
|
||||||
echo ""
|
|
||||||
print_status "Next steps:"
|
|
||||||
echo " 1. Import your database:"
|
|
||||||
echo " ./docker-import-db.sh prod"
|
|
||||||
echo ""
|
|
||||||
echo " 2. Access your application:"
|
|
||||||
echo " - Web App: http://localhost"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🔄 Docker Clean Rebuild Script"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Ask for confirmation
|
|
||||||
print_warning "This will remove all existing containers, images, and volumes!"
|
|
||||||
print_warning "Any data not backed up will be lost!"
|
|
||||||
read -p "Are you sure you want to continue? (y/N): " -n 1 -r
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
print_status "Rebuild cancelled."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_docker
|
|
||||||
|
|
||||||
# Execute rebuild process
|
|
||||||
cleanup_containers
|
|
||||||
prune_docker
|
|
||||||
build_containers
|
|
||||||
test_build
|
|
||||||
|
|
||||||
# Show final information
|
|
||||||
show_next_steps
|
|
||||||
|
|
||||||
print_success "✅ Clean rebuild completed successfully!"
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk setup environment file
|
|
||||||
# Usage: ./docker-setup-env.sh [local|production]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default environment
|
|
||||||
ENVIRONMENT="local"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
local|dev|development)
|
|
||||||
ENVIRONMENT="local"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production)
|
|
||||||
ENVIRONMENT="production"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [local|production]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to backup existing .env
|
|
||||||
backup_existing_env() {
|
|
||||||
if [[ -f .env ]]; then
|
|
||||||
local backup_name=".env.backup.$(date +%Y%m%d_%H%M%S)"
|
|
||||||
print_status "Backing up existing .env to $backup_name"
|
|
||||||
cp .env "$backup_name"
|
|
||||||
print_success "Backup created: $backup_name"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup local environment
|
|
||||||
setup_local_env() {
|
|
||||||
print_status "Setting up LOCAL development environment..."
|
|
||||||
|
|
||||||
if [[ -f docker/env.example.local ]]; then
|
|
||||||
backup_existing_env
|
|
||||||
cp docker/env.example.local .env
|
|
||||||
print_success "✅ Local environment file created!"
|
|
||||||
echo ""
|
|
||||||
print_status "Local Development Configuration:"
|
|
||||||
echo " 🌐 App URL: http://localhost:8000"
|
|
||||||
echo " 🗄️ Database: ckb_db (MySQL)"
|
|
||||||
echo " 📧 Mail: MailHog (http://localhost:8025)"
|
|
||||||
echo " 🔴 Redis: Session & Cache"
|
|
||||||
echo " 🐛 Debug: Enabled"
|
|
||||||
echo ""
|
|
||||||
print_status "Services will be available at:"
|
|
||||||
echo " - Web App: http://localhost:8000"
|
|
||||||
echo " - phpMyAdmin: http://localhost:8080"
|
|
||||||
echo " - MailHog: http://localhost:8025"
|
|
||||||
echo ""
|
|
||||||
print_success "Ready for local development! Run: ./docker-quick-setup.sh dev"
|
|
||||||
else
|
|
||||||
print_error "Local environment template not found: docker/env.example.local"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup production environment
|
|
||||||
setup_production_env() {
|
|
||||||
print_status "Setting up PRODUCTION environment..."
|
|
||||||
|
|
||||||
if [[ -f docker/env.example.production ]]; then
|
|
||||||
backup_existing_env
|
|
||||||
cp docker/env.example.production .env
|
|
||||||
print_success "✅ Production environment file created!"
|
|
||||||
echo ""
|
|
||||||
print_warning "🚨 SECURITY CONFIGURATION REQUIRED!"
|
|
||||||
echo ""
|
|
||||||
print_status "You MUST change these settings in .env file:"
|
|
||||||
echo " 🔐 DB_PASSWORD=CHANGE_THIS_SECURE_PASSWORD"
|
|
||||||
echo " 🔐 DB_ROOT_PASSWORD=CHANGE_THIS_ROOT_PASSWORD"
|
|
||||||
echo " 🔐 REDIS_PASSWORD=CHANGE_THIS_REDIS_PASSWORD"
|
|
||||||
echo ""
|
|
||||||
print_status "Optional but recommended configurations:"
|
|
||||||
echo " 📧 MAIL_HOST, MAIL_USERNAME, MAIL_PASSWORD"
|
|
||||||
echo " ☁️ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (for S3)"
|
|
||||||
echo " 📡 PUSHER_* settings (for real-time features)"
|
|
||||||
echo ""
|
|
||||||
print_status "Production Configuration:"
|
|
||||||
echo " 🌐 App URL: https://bengkel.digitaloasis.xyz"
|
|
||||||
echo " 🗄️ Database: ckb_production (MySQL)"
|
|
||||||
echo " 📧 Mail: SMTP (configure in .env)"
|
|
||||||
echo " 🔴 Redis: Session, Cache & Queue"
|
|
||||||
echo " 🐛 Debug: Disabled"
|
|
||||||
echo " 🔒 SSL: Let's Encrypt"
|
|
||||||
echo ""
|
|
||||||
print_warning "Next steps:"
|
|
||||||
echo "1. Edit .env file and change all CHANGE_THIS_* values"
|
|
||||||
echo "2. Run: ./docker-deploy-prod.sh deploy"
|
|
||||||
echo "3. Run: ./docker-deploy-prod.sh ssl"
|
|
||||||
else
|
|
||||||
print_error "Production environment template not found: docker/env.example.production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show current environment info
|
|
||||||
show_current_env() {
|
|
||||||
if [[ -f .env ]]; then
|
|
||||||
print_status "Current Environment Information:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Detect environment type
|
|
||||||
local app_env=$(grep "^APP_ENV=" .env | cut -d '=' -f2)
|
|
||||||
local app_url=$(grep "^APP_URL=" .env | cut -d '=' -f2)
|
|
||||||
local app_debug=$(grep "^APP_DEBUG=" .env | cut -d '=' -f2)
|
|
||||||
local db_host=$(grep "^DB_HOST=" .env | cut -d '=' -f2)
|
|
||||||
local db_name=$(grep "^DB_DATABASE=" .env | cut -d '=' -f2)
|
|
||||||
|
|
||||||
echo " Environment: $app_env"
|
|
||||||
echo " App URL: $app_url"
|
|
||||||
echo " Debug Mode: $app_debug"
|
|
||||||
echo " Database Host: $db_host"
|
|
||||||
echo " Database Name: $db_name"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check for security issues in production
|
|
||||||
if [[ "$app_env" == "production" ]]; then
|
|
||||||
print_status "Security Check:"
|
|
||||||
if grep -q "CHANGE_THIS" .env; then
|
|
||||||
print_error "❌ Found CHANGE_THIS_* values in production .env!"
|
|
||||||
print_warning "Please update all CHANGE_THIS_* values with secure passwords."
|
|
||||||
else
|
|
||||||
print_success "✅ No CHANGE_THIS_* values found."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$app_debug" == "true" ]]; then
|
|
||||||
print_error "❌ Debug mode is enabled in production!"
|
|
||||||
print_warning "Set APP_DEBUG=false for production."
|
|
||||||
else
|
|
||||||
print_success "✅ Debug mode is disabled."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_status "No .env file found."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to validate environment file
|
|
||||||
validate_env() {
|
|
||||||
if [[ ! -f .env ]]; then
|
|
||||||
print_error "No .env file found!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_status "Validating environment file..."
|
|
||||||
|
|
||||||
# Required variables
|
|
||||||
local required_vars=("APP_NAME" "APP_ENV" "APP_URL" "DB_HOST" "DB_DATABASE" "DB_USERNAME" "DB_PASSWORD")
|
|
||||||
local missing_vars=()
|
|
||||||
|
|
||||||
for var in "${required_vars[@]}"; do
|
|
||||||
if ! grep -q "^${var}=" .env; then
|
|
||||||
missing_vars+=("$var")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#missing_vars[@]} -gt 0 ]]; then
|
|
||||||
print_error "Missing required environment variables:"
|
|
||||||
for var in "${missing_vars[@]}"; do
|
|
||||||
echo " - $var"
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "✅ Environment file validation passed!"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🔧 CKB Environment Setup Helper"
|
|
||||||
print_status "Target Environment: $ENVIRONMENT"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
case $ENVIRONMENT in
|
|
||||||
local)
|
|
||||||
setup_local_env
|
|
||||||
;;
|
|
||||||
production)
|
|
||||||
setup_production_env
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "Environment file setup completed!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show current environment info
|
|
||||||
show_current_env
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_status "Available commands:"
|
|
||||||
echo " - Show current env: ./docker-setup-env.sh"
|
|
||||||
echo " - Setup local: ./docker-setup-env.sh local"
|
|
||||||
echo " - Setup production: ./docker-setup-env.sh production"
|
|
||||||
echo " - Quick local setup: ./docker-quick-setup.sh dev"
|
|
||||||
echo " - Production deploy: ./docker-deploy-prod.sh deploy"
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk setup SSL certificate dengan Let's Encrypt untuk domain bengkel.digitaloasis.xyz
|
|
||||||
# Usage: ./docker-ssl-setup.sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
DOMAIN="bengkel.digitaloasis.xyz"
|
|
||||||
WWW_DOMAIN="www.bengkel.digitaloasis.xyz"
|
|
||||||
EMAIL="admin@digitaloasis.xyz"
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if Docker is running
|
|
||||||
check_docker() {
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
print_error "Docker is not running. Please start Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if domain is pointing to this server
|
|
||||||
check_domain() {
|
|
||||||
print_status "Checking if domain $DOMAIN is pointing to this server..."
|
|
||||||
|
|
||||||
# Get current server IP
|
|
||||||
SERVER_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "Unable to detect")
|
|
||||||
|
|
||||||
# Get domain IP
|
|
||||||
DOMAIN_IP=$(dig +short $DOMAIN | head -n1)
|
|
||||||
|
|
||||||
print_status "Server IP: $SERVER_IP"
|
|
||||||
print_status "Domain IP: $DOMAIN_IP"
|
|
||||||
|
|
||||||
if [[ "$SERVER_IP" != "$DOMAIN_IP" ]]; then
|
|
||||||
print_warning "Domain might not be pointing to this server!"
|
|
||||||
print_warning "Please make sure DNS is configured correctly before proceeding."
|
|
||||||
read -p "Continue anyway? (y/N): " -n 1 -r
|
|
||||||
echo ""
|
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
print_status "SSL setup cancelled."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_success "Domain is correctly pointing to this server!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to create temporary nginx config for initial certificate
|
|
||||||
create_temp_nginx() {
|
|
||||||
print_status "Creating temporary nginx configuration for initial certificate..."
|
|
||||||
|
|
||||||
cat > docker/nginx-temp.conf << 'EOF'
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name bengkel.digitaloasis.xyz www.bengkel.digitaloasis.xyz;
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 200 'SSL setup in progress...';
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to start nginx with temporary config
|
|
||||||
start_temp_nginx() {
|
|
||||||
print_status "Starting nginx with temporary configuration..."
|
|
||||||
|
|
||||||
# Update docker-compose to use temporary config
|
|
||||||
sed -i 's|nginx-proxy.conf|nginx-temp.conf|g' $COMPOSE_FILE
|
|
||||||
|
|
||||||
# Start nginx-proxy
|
|
||||||
docker-compose -f $COMPOSE_FILE up -d nginx-proxy
|
|
||||||
|
|
||||||
# Wait for nginx to be ready
|
|
||||||
sleep 10
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to obtain SSL certificate
|
|
||||||
obtain_certificate() {
|
|
||||||
print_status "Obtaining SSL certificate from Let's Encrypt..."
|
|
||||||
|
|
||||||
# Run certbot
|
|
||||||
docker-compose -f $COMPOSE_FILE run --rm certbot certonly \
|
|
||||||
--webroot \
|
|
||||||
--webroot-path=/var/www/certbot \
|
|
||||||
--email $EMAIL \
|
|
||||||
--agree-tos \
|
|
||||||
--no-eff-email \
|
|
||||||
--force-renewal \
|
|
||||||
-d $DOMAIN \
|
|
||||||
-d $WWW_DOMAIN
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
print_success "SSL certificate obtained successfully!"
|
|
||||||
else
|
|
||||||
print_error "Failed to obtain SSL certificate!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup certificate files
|
|
||||||
setup_certificate_files() {
|
|
||||||
print_status "Setting up certificate files for nginx..."
|
|
||||||
|
|
||||||
# Copy certificates to nginx ssl directory
|
|
||||||
docker run --rm \
|
|
||||||
-v ckb_ssl_certificates:/source \
|
|
||||||
-v ckb_ssl_certificates:/target \
|
|
||||||
alpine sh -c "
|
|
||||||
mkdir -p /target/live/$DOMAIN
|
|
||||||
cp -L /source/live/$DOMAIN/fullchain.pem /target/fullchain.pem
|
|
||||||
cp -L /source/live/$DOMAIN/privkey.pem /target/privkey.pem
|
|
||||||
chmod 644 /target/fullchain.pem
|
|
||||||
chmod 600 /target/privkey.pem
|
|
||||||
"
|
|
||||||
|
|
||||||
print_success "Certificate files setup completed!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to restore production nginx config
|
|
||||||
restore_production_config() {
|
|
||||||
print_status "Restoring production nginx configuration..."
|
|
||||||
|
|
||||||
# Restore original config
|
|
||||||
sed -i 's|nginx-temp.conf|nginx-proxy.conf|g' $COMPOSE_FILE
|
|
||||||
|
|
||||||
# Restart nginx with SSL configuration
|
|
||||||
docker-compose -f $COMPOSE_FILE up -d nginx-proxy
|
|
||||||
|
|
||||||
print_success "Production nginx configuration restored!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test SSL certificate
|
|
||||||
test_ssl() {
|
|
||||||
print_status "Testing SSL certificate..."
|
|
||||||
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Test HTTPS connection
|
|
||||||
if curl -s --max-time 10 https://$DOMAIN > /dev/null; then
|
|
||||||
print_success "HTTPS is working correctly!"
|
|
||||||
else
|
|
||||||
print_warning "HTTPS test failed. Please check the configuration."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test certificate validity
|
|
||||||
if openssl s_client -connect $DOMAIN:443 -servername $DOMAIN < /dev/null 2>/dev/null | openssl x509 -noout -dates; then
|
|
||||||
print_success "Certificate information retrieved successfully!"
|
|
||||||
else
|
|
||||||
print_warning "Could not retrieve certificate information."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup certificate renewal
|
|
||||||
setup_renewal() {
|
|
||||||
print_status "Setting up automatic certificate renewal..."
|
|
||||||
|
|
||||||
# Create renewal script
|
|
||||||
cat > docker-ssl-renew.sh << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# SSL Certificate Renewal Script
|
|
||||||
# Add this to crontab: 0 12 * * * /path/to/docker-ssl-renew.sh
|
|
||||||
|
|
||||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew --quiet
|
|
||||||
|
|
||||||
# Reload nginx if certificate was renewed
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
# Copy renewed certificates
|
|
||||||
docker run --rm \
|
|
||||||
-v ckb_ssl_certificates:/source \
|
|
||||||
-v ckb_ssl_certificates:/target \
|
|
||||||
alpine sh -c "
|
|
||||||
cp -L /source/live/bengkel.digitaloasis.xyz/fullchain.pem /target/fullchain.pem
|
|
||||||
cp -L /source/live/bengkel.digitaloasis.xyz/privkey.pem /target/privkey.pem
|
|
||||||
"
|
|
||||||
|
|
||||||
# Reload nginx
|
|
||||||
docker-compose -f docker-compose.prod.yml exec nginx-proxy nginx -s reload
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x docker-ssl-renew.sh
|
|
||||||
|
|
||||||
print_success "Certificate renewal script created: docker-ssl-renew.sh"
|
|
||||||
print_status "To setup automatic renewal, add this to crontab:"
|
|
||||||
echo "0 12 * * * $(pwd)/docker-ssl-renew.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show final information
|
|
||||||
show_final_info() {
|
|
||||||
echo ""
|
|
||||||
print_success "🎉 SSL setup completed successfully!"
|
|
||||||
echo ""
|
|
||||||
print_status "Your application is now available at:"
|
|
||||||
echo " 🌐 https://bengkel.digitaloasis.xyz"
|
|
||||||
echo " 🌐 https://www.bengkel.digitaloasis.xyz"
|
|
||||||
echo ""
|
|
||||||
print_status "SSL Certificate Information:"
|
|
||||||
echo " 📅 Domain: $DOMAIN, $WWW_DOMAIN"
|
|
||||||
echo " 📧 Email: $EMAIL"
|
|
||||||
echo " 🔄 Auto-renewal: Setup docker-ssl-renew.sh in crontab"
|
|
||||||
echo ""
|
|
||||||
print_status "Useful Commands:"
|
|
||||||
echo " - Check certificate: openssl s_client -connect $DOMAIN:443 -servername $DOMAIN"
|
|
||||||
echo " - Renew certificate: ./docker-ssl-renew.sh"
|
|
||||||
echo " - View logs: docker-compose -f $COMPOSE_FILE logs nginx-proxy"
|
|
||||||
echo " - Test renewal: docker-compose -f $COMPOSE_FILE run --rm certbot renew --dry-run"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🔒 SSL Certificate Setup for CKB Production"
|
|
||||||
print_status "Domain: $DOMAIN"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_docker
|
|
||||||
check_domain
|
|
||||||
|
|
||||||
# Ask for confirmation
|
|
||||||
print_warning "This will setup SSL certificate for $DOMAIN"
|
|
||||||
print_status "Make sure your application is not currently running."
|
|
||||||
read -p "Continue with SSL setup? (y/N): " -n 1 -r
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
print_status "SSL setup cancelled."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Execute SSL setup
|
|
||||||
print_status "Starting SSL certificate setup process..."
|
|
||||||
|
|
||||||
create_temp_nginx
|
|
||||||
start_temp_nginx
|
|
||||||
obtain_certificate
|
|
||||||
setup_certificate_files
|
|
||||||
restore_production_config
|
|
||||||
test_ssl
|
|
||||||
setup_renewal
|
|
||||||
|
|
||||||
# Show final information
|
|
||||||
show_final_info
|
|
||||||
|
|
||||||
print_success "✅ SSL setup completed successfully!"
|
|
||||||
218
docker-start.sh
218
docker-start.sh
@@ -1,218 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk menjalankan CKB Laravel Application dengan Docker
|
|
||||||
# Usage: ./docker-start.sh [dev|prod] [up|down|build|logs]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
ACTION="up"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
up|start)
|
|
||||||
ACTION="up"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
down|stop)
|
|
||||||
ACTION="down"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
ACTION="build"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
ACTION="logs"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
ACTION="restart"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if Docker is running
|
|
||||||
check_docker() {
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
print_error "Docker is not running. Please start Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to setup environment file
|
|
||||||
setup_env() {
|
|
||||||
if [[ ! -f .env ]]; then
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
if [[ -f docker/env.example.local ]]; then
|
|
||||||
print_status "Copying local environment file..."
|
|
||||||
cp docker/env.example.local .env
|
|
||||||
print_success "Local development environment configured"
|
|
||||||
else
|
|
||||||
print_error "Local environment template not found: docker/env.example.local"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ -f docker/env.example.production ]]; then
|
|
||||||
print_status "Copying production environment file..."
|
|
||||||
cp docker/env.example.production .env
|
|
||||||
print_warning "⚠️ IMPORTANT: Edit .env and change all CHANGE_THIS_* passwords!"
|
|
||||||
print_warning "Please configure production settings before continuing"
|
|
||||||
else
|
|
||||||
print_error "Production environment template not found: docker/env.example.production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to generate application key
|
|
||||||
generate_key() {
|
|
||||||
if ! grep -q "APP_KEY=base64:" .env; then
|
|
||||||
print_status "Generating Laravel application key..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose exec app php artisan key:generate || true
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan key:generate || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to run migrations
|
|
||||||
run_migrations() {
|
|
||||||
print_status "Running database migrations..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose exec app php artisan migrate --force
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan migrate --force
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to optimize for production
|
|
||||||
optimize_production() {
|
|
||||||
if [[ $ENVIRONMENT == "prod" ]]; then
|
|
||||||
print_status "Optimizing for production..."
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan config:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan route:cache
|
|
||||||
docker-compose -f docker-compose.prod.yml exec app php artisan view:cache
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
print_status "Starting CKB Laravel Application with Docker"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
print_status "Action: $ACTION"
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_docker
|
|
||||||
|
|
||||||
case $ACTION in
|
|
||||||
up|start)
|
|
||||||
setup_env
|
|
||||||
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
print_status "Starting development environment..."
|
|
||||||
docker-compose up -d --build
|
|
||||||
print_success "Development environment started!"
|
|
||||||
echo ""
|
|
||||||
print_status "Access your application at:"
|
|
||||||
echo " - Web App: http://localhost:8000"
|
|
||||||
echo " - phpMyAdmin: http://localhost:8080"
|
|
||||||
echo " - MailHog: http://localhost:8025"
|
|
||||||
else
|
|
||||||
print_status "Starting production environment..."
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
print_success "Production environment started!"
|
|
||||||
echo ""
|
|
||||||
print_status "Application is running on port 80"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wait for containers to be ready
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
generate_key
|
|
||||||
run_migrations
|
|
||||||
optimize_production
|
|
||||||
;;
|
|
||||||
|
|
||||||
down|stop)
|
|
||||||
print_status "Stopping containers..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose down
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
fi
|
|
||||||
print_success "Containers stopped!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
build)
|
|
||||||
print_status "Building containers..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose build --no-cache
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
|
||||||
fi
|
|
||||||
print_success "Build completed!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
logs)
|
|
||||||
print_status "Showing logs..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose logs -f
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
restart)
|
|
||||||
print_status "Restarting containers..."
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
docker-compose restart
|
|
||||||
else
|
|
||||||
docker-compose -f docker-compose.prod.yml restart
|
|
||||||
fi
|
|
||||||
print_success "Containers restarted!"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "Operation completed successfully!"
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script untuk test Redis functionality di Docker environment
|
|
||||||
# Usage: ./docker-test-redis.sh [dev|prod]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
dev|development)
|
|
||||||
ENVIRONMENT="dev"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
prod|production|staging)
|
|
||||||
ENVIRONMENT="prod"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
echo "Usage: $0 [dev|prod]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to get compose file
|
|
||||||
get_compose_file() {
|
|
||||||
if [[ $ENVIRONMENT == "dev" ]]; then
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
else
|
|
||||||
COMPOSE_FILE="docker-compose.prod.yml"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if containers are running
|
|
||||||
check_containers() {
|
|
||||||
if ! docker-compose -f "$COMPOSE_FILE" ps | grep -q "redis.*Up"; then
|
|
||||||
print_error "Redis container is not running!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker-compose -f "$COMPOSE_FILE" ps | grep -q "app.*Up"; then
|
|
||||||
print_error "App container is not running!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test PHP Redis extension
|
|
||||||
test_redis_extension() {
|
|
||||||
print_status "Testing PHP Redis extension..."
|
|
||||||
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php -m | grep -q "redis"; then
|
|
||||||
print_success "PHP Redis extension is installed"
|
|
||||||
else
|
|
||||||
print_error "PHP Redis extension is NOT installed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test Redis server connection
|
|
||||||
test_redis_connection() {
|
|
||||||
print_status "Testing Redis server connection..."
|
|
||||||
|
|
||||||
# Test direct connection to Redis container
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T redis redis-cli ping | grep -q "PONG"; then
|
|
||||||
print_success "Redis server is responding"
|
|
||||||
else
|
|
||||||
print_error "Redis server is not responding"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test connection from PHP
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php -r "
|
|
||||||
try {
|
|
||||||
\$redis = new Redis();
|
|
||||||
\$redis->connect('redis', 6379);
|
|
||||||
echo 'OK';
|
|
||||||
} catch (Exception \$e) {
|
|
||||||
echo 'FAILED: ' . \$e->getMessage();
|
|
||||||
}
|
|
||||||
" | grep -q "OK"; then
|
|
||||||
print_success "PHP Redis connection working"
|
|
||||||
else
|
|
||||||
print_error "PHP Redis connection failed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test Laravel cache with Redis
|
|
||||||
test_laravel_cache() {
|
|
||||||
print_status "Testing Laravel cache with Redis..."
|
|
||||||
|
|
||||||
# Test cache clear
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php artisan cache:clear > /dev/null 2>&1; then
|
|
||||||
print_success "Laravel cache clear working"
|
|
||||||
else
|
|
||||||
print_warning "Laravel cache clear failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test cache set/get
|
|
||||||
local test_key="test_$(date +%s)"
|
|
||||||
local test_value="redis_test_value"
|
|
||||||
|
|
||||||
if docker-compose -f "$COMPOSE_FILE" exec -T app php artisan tinker --execute="
|
|
||||||
Cache::put('$test_key', '$test_value', 60);
|
|
||||||
echo Cache::get('$test_key');
|
|
||||||
" | grep -q "$test_value"; then
|
|
||||||
print_success "Laravel cache operations working"
|
|
||||||
else
|
|
||||||
print_error "Laravel cache operations failed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test Redis session storage
|
|
||||||
test_redis_sessions() {
|
|
||||||
print_status "Testing Redis session configuration..."
|
|
||||||
|
|
||||||
# Check session driver in config
|
|
||||||
local session_driver=$(docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo config('session.driver');")
|
|
||||||
|
|
||||||
if [[ "$session_driver" == "redis" ]]; then
|
|
||||||
print_success "Laravel sessions configured to use Redis"
|
|
||||||
else
|
|
||||||
print_warning "Laravel sessions not using Redis (current: $session_driver)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test Redis queue configuration
|
|
||||||
test_redis_queue() {
|
|
||||||
print_status "Testing Redis queue configuration..."
|
|
||||||
|
|
||||||
# Check queue driver in config
|
|
||||||
local queue_driver=$(docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo config('queue.default');")
|
|
||||||
|
|
||||||
if [[ "$queue_driver" == "redis" ]]; then
|
|
||||||
print_success "Laravel queue configured to use Redis"
|
|
||||||
else
|
|
||||||
print_warning "Laravel queue not using Redis (current: $queue_driver)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show Redis info
|
|
||||||
show_redis_info() {
|
|
||||||
print_status "Redis server information:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec redis redis-cli info server | head -10
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
print_status "Redis memory usage:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec redis redis-cli info memory | grep used_memory_human
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
print_status "Redis connected clients:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec redis redis-cli info clients | grep connected_clients
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to show Laravel configuration
|
|
||||||
show_laravel_config() {
|
|
||||||
print_status "Laravel Redis configuration:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
print_status "Cache driver:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo 'CACHE_DRIVER=' . config('cache.default') . PHP_EOL;"
|
|
||||||
|
|
||||||
print_status "Session driver:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo 'SESSION_DRIVER=' . config('session.driver') . PHP_EOL;"
|
|
||||||
|
|
||||||
print_status "Queue driver:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo 'QUEUE_CONNECTION=' . config('queue.default') . PHP_EOL;"
|
|
||||||
|
|
||||||
print_status "Redis host:"
|
|
||||||
docker-compose -f "$COMPOSE_FILE" exec -T app php -r "echo 'REDIS_HOST=' . config('database.redis.default.host') . PHP_EOL;"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================"
|
|
||||||
print_status "🔴 Redis Functionality Test"
|
|
||||||
print_status "Environment: $ENVIRONMENT"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Get compose file
|
|
||||||
get_compose_file
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
check_containers
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
print_status "Running Redis tests..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
test_redis_extension && echo ""
|
|
||||||
test_redis_connection && echo ""
|
|
||||||
test_laravel_cache && echo ""
|
|
||||||
test_redis_sessions && echo ""
|
|
||||||
test_redis_queue && echo ""
|
|
||||||
|
|
||||||
# Show information
|
|
||||||
show_redis_info
|
|
||||||
echo ""
|
|
||||||
show_laravel_config
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "✅ Redis functionality test completed!"
|
|
||||||
|
|
||||||
print_status "🔧 Troubleshooting commands:"
|
|
||||||
echo " - Redis logs: docker-compose -f $COMPOSE_FILE logs redis"
|
|
||||||
echo " - App logs: docker-compose -f $COMPOSE_FILE logs app"
|
|
||||||
echo " - Redis CLI: docker-compose -f $COMPOSE_FILE exec redis redis-cli"
|
|
||||||
echo " - Test cache: docker-compose -f $COMPOSE_FILE exec app php artisan cache:clear"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
APP_NAME="CKB Bengkel System"
|
APP_NAME="CKB Bengkel System"
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
APP_KEY=
|
APP_KEY=base64:iuB/lNzV+lCffDcelFxtvXZJSk+04oiK2XahPxSuOSw=
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_URL=http://bengkel.digitaloasis.xyz:8082
|
APP_URL=http://bengkel.digitaloasis.xyz:8082
|
||||||
|
|
||||||
@@ -10,16 +10,16 @@ LOG_LEVEL=error
|
|||||||
# Database Configuration for Production
|
# Database Configuration for Production
|
||||||
# IMPORTANT: Change these credentials for security!
|
# IMPORTANT: Change these credentials for security!
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=db
|
DB_HOST=ckb-mariadb
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=ckb_production
|
DB_DATABASE=ckb_production
|
||||||
DB_USERNAME=ckb_user
|
DB_USERNAME=ckb_user
|
||||||
DB_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
DB_PASSWORD=890xVn8nWJO
|
||||||
DB_ROOT_PASSWORD=CHANGE_THIS_ROOT_PASSWORD
|
DB_ROOT_PASSWORD=890xVn8nWJO
|
||||||
|
|
||||||
# Redis Configuration for Production
|
# Redis Configuration for Production
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=ckb-redis
|
||||||
REDIS_PASSWORD=CHANGE_THIS_REDIS_PASSWORD
|
REDIS_PASSWORD=890xVn8nWJO
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
BROADCAST_DRIVER=redis
|
BROADCAST_DRIVER=redis
|
||||||
@@ -34,8 +34,8 @@ SESSION_LIFETIME=120
|
|||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=smtp
|
||||||
MAIL_HOST=smtp.gmail.com
|
MAIL_HOST=smtp.gmail.com
|
||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USERNAME=your-email@gmail.com
|
MAIL_USERNAME=arifaldiosdevelopment@gmail.com
|
||||||
MAIL_PASSWORD=your-app-password
|
MAIL_PASSWORD=Arifal@1998
|
||||||
MAIL_ENCRYPTION=tls
|
MAIL_ENCRYPTION=tls
|
||||||
MAIL_FROM_ADDRESS=noreply@bengkel.digitaloasis.xyz
|
MAIL_FROM_ADDRESS=noreply@bengkel.digitaloasis.xyz
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
# Audit Histori Stock
|
|
||||||
|
|
||||||
## Deskripsi
|
|
||||||
|
|
||||||
Fitur Audit Histori Stock memungkinkan untuk melacak semua perubahan stock yang terjadi di sistem. Setiap kali ada perubahan stock (penambahan, pengurangan, penyesuaian), sistem akan mencatat detail perubahan tersebut untuk keperluan audit.
|
|
||||||
|
|
||||||
## Fitur Utama
|
|
||||||
|
|
||||||
### 1. Tracking Otomatis
|
|
||||||
|
|
||||||
- Sistem otomatis mencatat setiap perubahan stock
|
|
||||||
- Mencatat stock sebelum dan sesudah perubahan
|
|
||||||
- Mencatat sumber perubahan (mutasi, opname, dll)
|
|
||||||
- Mencatat user yang melakukan perubahan
|
|
||||||
- Mencatat timestamp perubahan
|
|
||||||
|
|
||||||
### 2. Filter dan Pencarian
|
|
||||||
|
|
||||||
- Filter berdasarkan dealer
|
|
||||||
- Filter berdasarkan produk
|
|
||||||
- Filter berdasarkan jenis perubahan
|
|
||||||
- Filter berdasarkan tanggal
|
|
||||||
- Pencarian realtime pada semua kolom
|
|
||||||
|
|
||||||
### 3. Detail Audit
|
|
||||||
|
|
||||||
- Informasi lengkap perubahan stock
|
|
||||||
- Detail sumber perubahan (mutasi/opname)
|
|
||||||
- History user yang melakukan aksi
|
|
||||||
- Catatan dan keterangan perubahan
|
|
||||||
|
|
||||||
### 4. Export Data
|
|
||||||
|
|
||||||
- Export ke Excel
|
|
||||||
- Export ke PDF
|
|
||||||
- Data yang diekspor dapat disesuaikan
|
|
||||||
|
|
||||||
## Jenis Perubahan Stock
|
|
||||||
|
|
||||||
### 1. Penambahan (Increase)
|
|
||||||
|
|
||||||
- Stock bertambah dari transaksi
|
|
||||||
- Biasanya dari mutasi masuk atau opname correction
|
|
||||||
|
|
||||||
### 2. Pengurangan (Decrease)
|
|
||||||
|
|
||||||
- Stock berkurang dari transaksi
|
|
||||||
- Biasanya dari mutasi keluar atau penjualan
|
|
||||||
|
|
||||||
### 3. Penyesuaian (Adjustment)
|
|
||||||
|
|
||||||
- Penyesuaian stock dari opname
|
|
||||||
- Koreksi stock manual
|
|
||||||
|
|
||||||
### 4. Tidak Ada Perubahan (No Change)
|
|
||||||
|
|
||||||
- Record dibuat tapi tidak ada perubahan quantity
|
|
||||||
- Biasanya untuk tracking purpose
|
|
||||||
|
|
||||||
## Cara Menggunakan
|
|
||||||
|
|
||||||
### 1. Akses Menu
|
|
||||||
|
|
||||||
```
|
|
||||||
Warehouse -> Stock Audit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Menggunakan Filter
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Filter dealer
|
|
||||||
$("#filter-dealer").val("Nama Dealer");
|
|
||||||
|
|
||||||
// Filter produk
|
|
||||||
$("#filter-product").val("Nama Produk");
|
|
||||||
|
|
||||||
// Filter jenis perubahan
|
|
||||||
$("#filter-change-type").val("increase"); // increase, decrease, adjustment, no_change
|
|
||||||
|
|
||||||
// Filter tanggal
|
|
||||||
$("#filter-date").val("2024-01-15");
|
|
||||||
|
|
||||||
// Reset semua filter
|
|
||||||
$("#reset-filters").click();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Melihat Detail
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Klik tombol Detail pada baris data
|
|
||||||
showAuditDetail(stockLogId);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup dan Instalasi
|
|
||||||
|
|
||||||
### 1. Setup Menu dan Privileges
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan setup:stock-audit-menu
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Atau Menggunakan Seeder
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan db:seed --class=StockAuditMenuSeeder
|
|
||||||
```
|
|
||||||
|
|
||||||
## Struktur Data
|
|
||||||
|
|
||||||
### Model yang Terlibat
|
|
||||||
|
|
||||||
- **StockLog**: Record audit perubahan stock
|
|
||||||
- **Stock**: Data stock utama
|
|
||||||
- **Product**: Data produk
|
|
||||||
- **Dealer**: Data dealer
|
|
||||||
- **User**: Data user
|
|
||||||
- **Mutation**: Data mutasi stock
|
|
||||||
- **StockOpname**: Data opname stock
|
|
||||||
|
|
||||||
### Relasi Database
|
|
||||||
|
|
||||||
```php
|
|
||||||
StockLog belongsTo Stock
|
|
||||||
StockLog belongsTo User
|
|
||||||
StockLog morphTo Source (Mutation, StockOpname, etc)
|
|
||||||
Stock belongsTo Product
|
|
||||||
Stock belongsTo Dealer
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### 1. Index (List Data)
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /warehouse/stock-audit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Detail Audit
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /warehouse/stock-audit/{stockLog}/detail
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kustomisasi
|
|
||||||
|
|
||||||
### 1. Menambah Jenis Perubahan
|
|
||||||
|
|
||||||
Edit enum `StockChangeType`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// app/Enums/StockChangeType.php
|
|
||||||
case NEW_TYPE = 'new_type';
|
|
||||||
|
|
||||||
public function label(): string
|
|
||||||
{
|
|
||||||
return match($this) {
|
|
||||||
// ... existing cases
|
|
||||||
self::NEW_TYPE => 'Label Baru',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Menambah Filter Custom
|
|
||||||
|
|
||||||
Edit controller dan view untuk menambah filter baru:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Controller
|
|
||||||
->filterColumn('new_field', function($query, $keyword) {
|
|
||||||
$query->where('new_field', 'like', "%{$keyword}%");
|
|
||||||
})
|
|
||||||
|
|
||||||
// View
|
|
||||||
<select class="form-select" id="filter-new-field">
|
|
||||||
<option value="">Semua</option>
|
|
||||||
// ... options
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Kustomisasi Export
|
|
||||||
|
|
||||||
Edit DataTables buttons untuk menyesuaikan kolom export:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
exportOptions: {
|
|
||||||
columns: [1, 2, 3, 4, 5, 6, 7, 8]; // Sesuaikan kolom yang ingin diekspor
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### 1. Menu Tidak Muncul
|
|
||||||
|
|
||||||
- Pastikan menu sudah di-setup dengan benar
|
|
||||||
- Cek privileges user untuk menu stock-audit.index
|
|
||||||
- Cek role user memiliki akses view = 1
|
|
||||||
|
|
||||||
### 2. Data Tidak Muncul
|
|
||||||
|
|
||||||
- Cek apakah ada data StockLog di database
|
|
||||||
- Cek filter yang aktive
|
|
||||||
- Cek permission user untuk melihat data dealer tertentu
|
|
||||||
|
|
||||||
### 3. Detail Tidak Loading
|
|
||||||
|
|
||||||
- Cek URL endpoint `/warehouse/stock-audit/{id}/detail`
|
|
||||||
- Cek network tab di browser untuk error response
|
|
||||||
- Cek log Laravel untuk error detail
|
|
||||||
|
|
||||||
## Keamanan
|
|
||||||
|
|
||||||
### 1. Filter Berdasarkan Role
|
|
||||||
|
|
||||||
- User dengan `dealer_id` hanya melihat data dealer mereka
|
|
||||||
- Admin dapat melihat semua data
|
|
||||||
|
|
||||||
### 2. View-Only Access
|
|
||||||
|
|
||||||
- Menu ini adalah read-only
|
|
||||||
- Tidak ada aksi create, update, atau delete
|
|
||||||
- Hanya viewing dan export yang diizinkan
|
|
||||||
|
|
||||||
### 3. Audit Trail
|
|
||||||
|
|
||||||
- Setiap akses audit log dapat di-track
|
|
||||||
- User activity dapat dimonitor
|
|
||||||
- Data tidak dapat dimanipulasi
|
|
||||||
|
|
||||||
## Performance Tips
|
|
||||||
|
|
||||||
### 1. Index Database
|
|
||||||
|
|
||||||
Pastikan ada index pada kolom yang sering difilter:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Index untuk performance
|
|
||||||
CREATE INDEX idx_stock_logs_created_at ON stock_logs(created_at);
|
|
||||||
CREATE INDEX idx_stock_logs_change_type ON stock_logs(change_type);
|
|
||||||
CREATE INDEX idx_stock_logs_stock_id ON stock_logs(stock_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Pagination
|
|
||||||
|
|
||||||
- DataTables menggunakan server-side processing
|
|
||||||
- Default page length: 25 records
|
|
||||||
- Dapat disesuaikan sesuai kebutuhan
|
|
||||||
|
|
||||||
### 3. Caching
|
|
||||||
|
|
||||||
Jika data sangat besar, pertimbangkan untuk menambah caching:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Cache dealer dan product data
|
|
||||||
$dealers = Cache::remember('dealers_for_audit', 3600, function () {
|
|
||||||
return Dealer::all();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
# Work Products & Stock Management System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Sistem ini memungkinkan setiap pekerjaan (work) memiliki relasi dengan banyak produk (products) dan otomatis mengurangi stock di dealer ketika transaksi pekerjaan dilakukan.
|
|
||||||
|
|
||||||
## Fitur Utama
|
|
||||||
|
|
||||||
### 1. Work Products Management
|
|
||||||
|
|
||||||
- Setiap pekerjaan dapat dikonfigurasi untuk memerlukan produk tertentu
|
|
||||||
- Admin dapat mengatur jumlah (quantity) produk yang dibutuhkan per pekerjaan
|
|
||||||
- Mendukung catatan/notes untuk setiap produk
|
|
||||||
|
|
||||||
### 2. Automatic Stock Reduction
|
|
||||||
|
|
||||||
- Stock otomatis dikurangi ketika transaksi pekerjaan dibuat
|
|
||||||
- Validasi stock tersedia sebelum transaksi disimpan
|
|
||||||
- Stock dikembalikan ketika transaksi dihapus
|
|
||||||
|
|
||||||
### 3. Stock Validation & Warning
|
|
||||||
|
|
||||||
- Real-time checking stock availability saat memilih pekerjaan
|
|
||||||
- Warning ketika stock tidak mencukupi
|
|
||||||
- Konfirmasi user sebelum melanjutkan dengan stock negatif
|
|
||||||
|
|
||||||
### 4. Stock Prediction
|
|
||||||
|
|
||||||
- Melihat prediksi penggunaan stock untuk pekerjaan tertentu
|
|
||||||
- Kalkulasi berdasarkan quantity pekerjaan yang akan dilakukan
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Tabel `work_products`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE work_products (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
work_id BIGINT NOT NULL,
|
|
||||||
product_id BIGINT NOT NULL,
|
|
||||||
quantity_required DECIMAL(10,2) DEFAULT 1.00,
|
|
||||||
notes TEXT NULL,
|
|
||||||
created_at TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP,
|
|
||||||
UNIQUE KEY unique_work_product (work_id, product_id),
|
|
||||||
FOREIGN KEY (work_id) REFERENCES works(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Model Relationships
|
|
||||||
|
|
||||||
#### Work Model
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Relasi many-to-many dengan Product
|
|
||||||
public function products()
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(Product::class, 'work_products')
|
|
||||||
->withPivot('quantity_required', 'notes')
|
|
||||||
->withTimestamps();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relasi one-to-many dengan WorkProduct
|
|
||||||
public function workProducts()
|
|
||||||
{
|
|
||||||
return $this->hasMany(WorkProduct::class);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Product Model
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Relasi many-to-many dengan Work
|
|
||||||
public function works()
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(Work::class, 'work_products')
|
|
||||||
->withPivot('quantity_required', 'notes')
|
|
||||||
->withTimestamps();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Work Products Management
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /admin/work/{work}/products - List work products
|
|
||||||
POST /admin/work/{work}/products - Add product to work
|
|
||||||
GET /admin/work/{work}/products/{id} - Show work product
|
|
||||||
PUT /admin/work/{work}/products/{id} - Update work product
|
|
||||||
DELETE /admin/work/{work}/products/{id} - Remove product from work
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stock Operations
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /transaction/check-stock - Check stock availability
|
|
||||||
GET /transaction/stock-prediction - Get stock usage prediction
|
|
||||||
GET /admin/work/{work}/stock-prediction - Get work stock prediction
|
|
||||||
```
|
|
||||||
|
|
||||||
## StockService Methods
|
|
||||||
|
|
||||||
### `checkStockAvailability($workId, $dealerId, $workQuantity)`
|
|
||||||
|
|
||||||
Mengecek apakah dealer memiliki stock yang cukup untuk pekerjaan tertentu.
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
- `$workId`: ID pekerjaan
|
|
||||||
- `$dealerId`: ID dealer
|
|
||||||
- `$workQuantity`: Jumlah pekerjaan yang akan dilakukan
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
|
|
||||||
```php
|
|
||||||
[
|
|
||||||
'available' => bool,
|
|
||||||
'message' => string,
|
|
||||||
'details' => [
|
|
||||||
[
|
|
||||||
'product_id' => int,
|
|
||||||
'product_name' => string,
|
|
||||||
'required_quantity' => float,
|
|
||||||
'available_stock' => float,
|
|
||||||
'is_available' => bool
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `reduceStockForTransaction($transaction)`
|
|
||||||
|
|
||||||
Mengurangi stock otomatis berdasarkan transaksi pekerjaan.
|
|
||||||
|
|
||||||
### `restoreStockForTransaction($transaction)`
|
|
||||||
|
|
||||||
Mengembalikan stock ketika transaksi dibatalkan/dihapus.
|
|
||||||
|
|
||||||
### `getStockUsagePrediction($workId, $quantity)`
|
|
||||||
|
|
||||||
Mendapatkan prediksi penggunaan stock untuk pekerjaan.
|
|
||||||
|
|
||||||
## User Interface
|
|
||||||
|
|
||||||
### 1. Work Products Management
|
|
||||||
|
|
||||||
- Akses melalui: **Admin Panel > Master > Pekerjaan > [Pilih Pekerjaan] > Tombol "Produk"**
|
|
||||||
- Fitur:
|
|
||||||
- Tambah/edit/hapus produk yang diperlukan
|
|
||||||
- Set quantity required per produk
|
|
||||||
- Tambah catatan untuk produk
|
|
||||||
- Preview prediksi penggunaan stock
|
|
||||||
|
|
||||||
### 2. Transaction Form dengan Stock Warning
|
|
||||||
|
|
||||||
- Real-time stock checking saat memilih pekerjaan
|
|
||||||
- Warning visual ketika stock tidak mencukupi
|
|
||||||
- Konfirmasi sebelum submit dengan stock negatif
|
|
||||||
|
|
||||||
### 3. Stock Prediction Modal
|
|
||||||
|
|
||||||
- Kalkulasi total produk yang dibutuhkan
|
|
||||||
- Informasi per produk dengan quantity dan satuan
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### 1. Mengatur Produk untuk Pekerjaan "Service Rutin"
|
|
||||||
|
|
||||||
1. Masuk ke Admin Panel > Master > Pekerjaan
|
|
||||||
2. Klik tombol "Produk" pada pekerjaan "Service Rutin"
|
|
||||||
3. Klik "Tambah Produk"
|
|
||||||
4. Pilih produk "Oli Mesin", set quantity 4, notes "4 liter untuk ganti oli"
|
|
||||||
5. Tambah produk "Filter Oli", set quantity 1, notes "Filter standar"
|
|
||||||
6. Simpan
|
|
||||||
|
|
||||||
### 2. Membuat Transaksi dengan Stock Warning
|
|
||||||
|
|
||||||
1. Pada form transaksi, pilih pekerjaan "Service Rutin"
|
|
||||||
2. Set quantity 2 (untuk 2 kendaraan)
|
|
||||||
3. Sistem akan menampilkan warning jika stock tidak cukup:
|
|
||||||
- Oli Mesin: Butuh 8 liter, Tersedia 5 liter
|
|
||||||
- Filter Oli: Butuh 2 unit, Tersedia 3 unit
|
|
||||||
4. User dapat memilih untuk melanjutkan atau membatalkan
|
|
||||||
|
|
||||||
### 3. Melihat Prediksi Stock
|
|
||||||
|
|
||||||
1. Di halaman Work Products, klik "Prediksi Stock"
|
|
||||||
2. Set jumlah pekerjaan (misal: 5)
|
|
||||||
3. Sistem menampilkan:
|
|
||||||
- Oli Mesin: 4 liter/pekerjaan × 5 = 20 liter total
|
|
||||||
- Filter Oli: 1 unit/pekerjaan × 5 = 5 unit total
|
|
||||||
|
|
||||||
## Stock Flow Process
|
|
||||||
|
|
||||||
### Saat Transaksi Dibuat:
|
|
||||||
|
|
||||||
1. User memilih pekerjaan dan quantity
|
|
||||||
2. Sistem check stock availability
|
|
||||||
3. Jika stock tidak cukup, tampilkan warning
|
|
||||||
4. User konfirmasi untuk melanjutkan
|
|
||||||
5. Transaksi disimpan dengan status 'completed'
|
|
||||||
6. Stock otomatis dikurangi sesuai konfigurasi work products
|
|
||||||
|
|
||||||
### Saat Transaksi Dihapus:
|
|
||||||
|
|
||||||
1. Sistem ambil data transaksi
|
|
||||||
2. Kembalikan stock sesuai dengan produk yang digunakan
|
|
||||||
3. Catat dalam stock log
|
|
||||||
4. Hapus transaksi
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Stock Tidak Mencukupi:
|
|
||||||
|
|
||||||
- Tampilkan warning dengan detail produk
|
|
||||||
- Izinkan user untuk melanjutkan dengan konfirmasi
|
|
||||||
- Stock boleh menjadi negatif (business rule)
|
|
||||||
|
|
||||||
### Product Tidak Dikonfigurasi:
|
|
||||||
|
|
||||||
- Jika pekerjaan belum dikonfigurasi produknya, tidak ada pengurangan stock
|
|
||||||
- Transaksi tetap bisa dibuat normal
|
|
||||||
|
|
||||||
### Database Transaction:
|
|
||||||
|
|
||||||
- Semua operasi stock menggunakan database transaction
|
|
||||||
- Rollback otomatis jika ada error
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Konfigurasi Work Products
|
|
||||||
|
|
||||||
- Set quantity required yang akurat
|
|
||||||
- Gunakan notes untuk informasi tambahan
|
|
||||||
- Review berkala konfigurasi produk
|
|
||||||
|
|
||||||
### 2. Stock Management
|
|
||||||
|
|
||||||
- Monitor stock levels secara berkala
|
|
||||||
- Set minimum stock alerts
|
|
||||||
- Koordinasi dengan procurement team
|
|
||||||
|
|
||||||
### 3. Training User
|
|
||||||
|
|
||||||
- Berikan training tentang stock warnings
|
|
||||||
- Edukasi tentang impact stock negatif
|
|
||||||
- Prosedur escalation jika stock habis
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Stock Tidak Berkurang Otomatis:
|
|
||||||
|
|
||||||
1. Cek konfigurasi work products
|
|
||||||
2. Pastikan produk memiliki stock record di dealer
|
|
||||||
3. Check error logs
|
|
||||||
|
|
||||||
### Error Saat Submit Transaksi:
|
|
||||||
|
|
||||||
1. Refresh halaman dan coba lagi
|
|
||||||
2. Check koneksi internet
|
|
||||||
3. Contact admin jika masih error
|
|
||||||
|
|
||||||
### Stock Calculation Salah:
|
|
||||||
|
|
||||||
1. Review konfigurasi quantity di work products
|
|
||||||
2. Check apakah ada duplikasi produk
|
|
||||||
3. Verify stock log untuk audit trail
|
|
||||||
|
|
||||||
## Monitoring & Reporting
|
|
||||||
|
|
||||||
### Stock Logs
|
|
||||||
|
|
||||||
Semua perubahan stock tercatat dalam `stock_logs` table dengan informasi:
|
|
||||||
|
|
||||||
- Source transaction
|
|
||||||
- Previous quantity
|
|
||||||
- New quantity
|
|
||||||
- Change amount
|
|
||||||
- Timestamp
|
|
||||||
- User who made the change
|
|
||||||
|
|
||||||
### Reports Available
|
|
||||||
|
|
||||||
- Stock usage by work type
|
|
||||||
- Stock movement history
|
|
||||||
- Negative stock alerts
|
|
||||||
- Product consumption analysis
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Automated Stock Alerts**: Email notifications ketika stock di bawah minimum
|
|
||||||
2. **Batch Operations**: Update multiple work products sekaligus
|
|
||||||
3. **Stock Forecasting**: Prediksi kebutuhan stock berdasarkan historical data
|
|
||||||
4. **Mobile Interface**: Mobile-friendly interface untuk stock checking
|
|
||||||
5. **Integration**: Integration dengan sistem procurement/inventory external
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "🔧 Fixing file permissions and ownership for Docker development..."
|
|
||||||
|
|
||||||
# Stop containers first
|
|
||||||
echo "🛑 Stopping containers..."
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Fix ownership - change back to current user for development
|
|
||||||
echo "👤 Fixing file ownership..."
|
|
||||||
sudo chown -R $USER:$USER .
|
|
||||||
|
|
||||||
# Set proper permissions for Laravel
|
|
||||||
echo "🔐 Setting Laravel permissions..."
|
|
||||||
chmod -R 755 .
|
|
||||||
chmod -R 775 storage bootstrap/cache
|
|
||||||
chmod 644 .env
|
|
||||||
|
|
||||||
# Ensure public directory is readable
|
|
||||||
chmod -R 755 public
|
|
||||||
|
|
||||||
# Fix specific file permissions
|
|
||||||
chmod 644 public/index.php
|
|
||||||
chmod 644 artisan
|
|
||||||
chmod +x artisan
|
|
||||||
|
|
||||||
echo "📋 Current ownership:"
|
|
||||||
ls -la public/index.php
|
|
||||||
ls -la .env
|
|
||||||
|
|
||||||
# Restart containers
|
|
||||||
echo "🚀 Starting containers..."
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Wait for containers to be ready
|
|
||||||
echo "⏳ Waiting for containers..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Test inside container
|
|
||||||
echo "🧪 Testing file access in container..."
|
|
||||||
docker exec ckb-app-dev ls -la /var/www/html/public/index.php
|
|
||||||
|
|
||||||
# Test HTTP access
|
|
||||||
echo "🌐 Testing HTTP access..."
|
|
||||||
sleep 5
|
|
||||||
curl -I http://localhost:8000
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Permission fix completed!"
|
|
||||||
echo ""
|
|
||||||
echo "If still having issues, try:"
|
|
||||||
echo "1. Check container logs: docker logs ckb-app-dev"
|
|
||||||
echo "2. Test PHP directly: docker exec ckb-app-dev php /var/www/html/public/index.php"
|
|
||||||
echo "3. Check nginx config: docker exec ckb-app-dev nginx -t"
|
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
# MIME Types for fonts
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
AddType font/ttf .ttf
|
||||||
|
AddType font/woff .woff
|
||||||
|
AddType font/woff2 .woff2
|
||||||
|
AddType font/eot .eot
|
||||||
|
AddType font/otf .otf
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
<IfModule mod_negotiation.c>
|
<IfModule mod_negotiation.c>
|
||||||
Options -MultiViews -Indexes
|
Options -MultiViews -Indexes
|
||||||
|
|||||||
11546
public/css/app.css
11546
public/css/app.css
File diff suppressed because one or more lines are too long
1211
public/js/app.js
1211
public/js/app.js
File diff suppressed because one or more lines are too long
@@ -1,63 +1,67 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function moneyFormat(n, currency) {
|
function moneyFormat(n, currency) {
|
||||||
n = (n != null) ? n : 0;
|
n = n != null ? n : 0;
|
||||||
var v = parseFloat(n).toFixed(0);
|
var v = parseFloat(n).toFixed(0);
|
||||||
return currency + " " + v.replace(/./g, function (c, i, a) {
|
return (
|
||||||
return i > 0 && c !== "," && (a.length - i) % 3 === 0 ? "." + c : c;
|
currency +
|
||||||
});
|
" " +
|
||||||
|
v.replace(/./g, function (c, i, a) {
|
||||||
|
return i > 0 && c !== "," && (a.length - i) % 3 === 0 ? "." + c : c;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var KTToastr = function () {
|
var KTToastr = (function () {
|
||||||
var successToastr = function (msg, title) {
|
var successToastr = function (msg, title) {
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
"closeButton": true,
|
closeButton: true,
|
||||||
"debug": false,
|
debug: false,
|
||||||
"newestOnTop": false,
|
newestOnTop: false,
|
||||||
"progressBar": false,
|
progressBar: false,
|
||||||
"positionClass": "toast-top-right",
|
positionClass: "toast-top-right",
|
||||||
"preventDuplicates": true,
|
preventDuplicates: true,
|
||||||
"onclick": null,
|
onclick: null,
|
||||||
"showDuration": "500",
|
showDuration: "500",
|
||||||
"hideDuration": "500",
|
hideDuration: "500",
|
||||||
"timeOut": "3000",
|
timeOut: "3000",
|
||||||
"extendedTimeOut": "3000",
|
extendedTimeOut: "3000",
|
||||||
"showEasing": "swing",
|
showEasing: "swing",
|
||||||
"hideEasing": "swing",
|
hideEasing: "swing",
|
||||||
"showMethod": "fadeIn",
|
showMethod: "fadeIn",
|
||||||
"hideMethod": "fadeOut"
|
hideMethod: "fadeOut",
|
||||||
};
|
};
|
||||||
var $toast = toastr["success"](msg, title);
|
var $toast = toastr["success"](msg, title);
|
||||||
|
|
||||||
if (typeof $toast === 'undefined') {
|
if (typeof $toast === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var errorToastr = function (msg, title) {
|
var errorToastr = function (msg, title) {
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
"closeButton": true,
|
closeButton: true,
|
||||||
"debug": false,
|
debug: false,
|
||||||
"newestOnTop": false,
|
newestOnTop: false,
|
||||||
"progressBar": false,
|
progressBar: false,
|
||||||
"positionClass": "toast-top-right",
|
positionClass: "toast-top-right",
|
||||||
"preventDuplicates": true,
|
preventDuplicates: true,
|
||||||
"onclick": null,
|
onclick: null,
|
||||||
"showDuration": "500",
|
showDuration: "500",
|
||||||
"hideDuration": "500",
|
hideDuration: "500",
|
||||||
"timeOut": "3000",
|
timeOut: "3000",
|
||||||
"extendedTimeOut": "3000",
|
extendedTimeOut: "3000",
|
||||||
"showEasing": "swing",
|
showEasing: "swing",
|
||||||
"hideEasing": "swing",
|
hideEasing: "swing",
|
||||||
"showMethod": "fadeIn",
|
showMethod: "fadeIn",
|
||||||
"hideMethod": "fadeOut"
|
hideMethod: "fadeOut",
|
||||||
};
|
};
|
||||||
var $toast = toastr["error"](msg, title);
|
var $toast = toastr["error"](msg, title);
|
||||||
|
|
||||||
if (typeof $toast === 'undefined') {
|
if (typeof $toast === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initSuccess: function (msg, title) {
|
initSuccess: function (msg, title) {
|
||||||
@@ -65,11 +69,29 @@ var KTToastr = function () {
|
|||||||
},
|
},
|
||||||
initError: function (msg, title) {
|
initError: function (msg, title) {
|
||||||
errorToastr(msg, title);
|
errorToastr(msg, title);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}();
|
})();
|
||||||
|
|
||||||
jQuery(document).ready(function () {
|
jQuery(document).ready(function () {
|
||||||
var li = $('.kt-menu__item--active');
|
var li = $(".kt-menu__item--active");
|
||||||
li.closest('li.kt-menu__item--submenu').addClass('kt-menu__item--open');
|
li.closest("li.kt-menu__item--submenu").addClass("kt-menu__item--open");
|
||||||
|
|
||||||
|
// Initialize Select2 globally
|
||||||
|
if (typeof $.fn.select2 !== "undefined") {
|
||||||
|
$(".select2").select2({
|
||||||
|
theme: "bootstrap4",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize DatePicker globally
|
||||||
|
if (typeof $.fn.datepicker !== "undefined") {
|
||||||
|
$(".datepicker").datepicker({
|
||||||
|
format: "yyyy-mm-dd",
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true,
|
||||||
|
orientation: "bottom auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
655
public/js/pages/back/master/work-prices.js
Normal file
655
public/js/pages/back/master/work-prices.js
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Class definition
|
||||||
|
var WorkPrices = (function () {
|
||||||
|
// Private variables
|
||||||
|
var workId;
|
||||||
|
var dealersTable;
|
||||||
|
var saveTimeout = {}; // For debouncing save requests
|
||||||
|
|
||||||
|
// Loading overlay functions
|
||||||
|
var showLoadingOverlay = function (message) {
|
||||||
|
if ($("#loading-overlay").length === 0) {
|
||||||
|
$("body").append(`
|
||||||
|
<div id="loading-overlay" style="
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
">
|
||||||
|
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-light mt-3" style="font-size: 1.1rem; font-weight: 500;">${message}</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
$("#loading-overlay").show();
|
||||||
|
$("#loading-overlay .text-light:last").text(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var hideLoadingOverlay = function () {
|
||||||
|
$("#loading-overlay").hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Private functions
|
||||||
|
var initTable = function () {
|
||||||
|
dealersTable = $("#dealers-table").DataTable({
|
||||||
|
pageLength: -1, // Show all records
|
||||||
|
lengthChange: false, // Hide length change dropdown
|
||||||
|
searching: true,
|
||||||
|
ordering: true,
|
||||||
|
info: false, // Hide "Showing X of Y entries"
|
||||||
|
responsive: true,
|
||||||
|
dom: '<"top"f>rt<"bottom"p>', // Only show search and pagination
|
||||||
|
paging: false, // Disable pagination
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initEvents = function () {
|
||||||
|
// Get work ID from URL
|
||||||
|
var pathArray = window.location.pathname.split("/");
|
||||||
|
workId = pathArray[pathArray.length - 2]; // work/{id}/set-prices
|
||||||
|
|
||||||
|
// Save single price with debouncing
|
||||||
|
$(document).on("click", ".save-single", function () {
|
||||||
|
var dealerId = $(this).data("dealer-id");
|
||||||
|
|
||||||
|
// Clear existing timeout for this dealer
|
||||||
|
if (saveTimeout[dealerId]) {
|
||||||
|
clearTimeout(saveTimeout[dealerId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout to prevent rapid clicks
|
||||||
|
saveTimeout[dealerId] = setTimeout(function () {
|
||||||
|
saveSinglePrice(dealerId);
|
||||||
|
}, 300); // 300ms delay
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete price
|
||||||
|
$(document).on("click", ".delete-price", function () {
|
||||||
|
var priceId = $(this).data("price-id");
|
||||||
|
var dealerId = $(this).data("dealer-id");
|
||||||
|
deletePrice(priceId, dealerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save all prices
|
||||||
|
$("#btn-save-all").on("click", function () {
|
||||||
|
saveAllPrices();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status toggle
|
||||||
|
$(document).on("change", ".status-input", function () {
|
||||||
|
var dealerId = $(this).data("dealer-id");
|
||||||
|
var isChecked = $(this).is(":checked");
|
||||||
|
var label = $(this).siblings("label");
|
||||||
|
var checkbox = $(this);
|
||||||
|
|
||||||
|
// Update visual immediately
|
||||||
|
if (isChecked) {
|
||||||
|
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
.text("Nonaktif")
|
||||||
|
.removeClass("active")
|
||||||
|
.addClass("inactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send AJAX request to update database
|
||||||
|
toggleStatus(dealerId, isChecked, checkbox, label);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format price input with thousand separator
|
||||||
|
$(document).on("input", ".price-input", function () {
|
||||||
|
var input = $(this);
|
||||||
|
var value = input.val().replace(/[^\d]/g, "");
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
input.val("0");
|
||||||
|
} else {
|
||||||
|
var numValue = parseInt(value);
|
||||||
|
input.val(numValue.toLocaleString("id-ID"));
|
||||||
|
// Don't update original value here - let it be updated only when saving
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format price inputs on page load
|
||||||
|
$(".price-input").each(function () {
|
||||||
|
var input = $(this);
|
||||||
|
var value = input.val();
|
||||||
|
if (value && value !== "0") {
|
||||||
|
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||||
|
input.val(numValue.toLocaleString("id-ID"));
|
||||||
|
// Store the original numeric value for comparison
|
||||||
|
input.data("original-value", numValue.toString());
|
||||||
|
console.log(
|
||||||
|
"Initialized price for dealer",
|
||||||
|
input.attr("name").replace("price_", ""),
|
||||||
|
":",
|
||||||
|
numValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var saveSinglePrice = function (dealerId) {
|
||||||
|
// Prevent multiple clicks
|
||||||
|
var saveButton = $('.save-single[data-dealer-id="' + dealerId + '"]');
|
||||||
|
if (saveButton.hasClass("loading")) {
|
||||||
|
return; // Already processing
|
||||||
|
}
|
||||||
|
|
||||||
|
var priceInput = $('input[name="price_' + dealerId + '"]');
|
||||||
|
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||||
|
var price = priceInput.val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||||
|
var isActive = statusInput.is(":checked");
|
||||||
|
|
||||||
|
if (!price || parseInt(price) <= 0) {
|
||||||
|
toastr.error("Harga harus lebih dari 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original price from data attribute (without separator)
|
||||||
|
var originalPrice = priceInput.data("original-value") || "0";
|
||||||
|
var currentPrice = parseInt(price);
|
||||||
|
var originalPriceInt = parseInt(originalPrice);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Debug - Original price:",
|
||||||
|
originalPriceInt,
|
||||||
|
"Current price:",
|
||||||
|
currentPrice
|
||||||
|
);
|
||||||
|
|
||||||
|
// If price hasn't actually changed, don't update
|
||||||
|
if (currentPrice === originalPriceInt && originalPrice !== "0") {
|
||||||
|
toastr.info("Harga tidak berubah, tidak perlu update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If price has changed, update original value for next comparison
|
||||||
|
if (currentPrice !== originalPriceInt) {
|
||||||
|
priceInput.data("original-value", currentPrice.toString());
|
||||||
|
console.log(
|
||||||
|
"Price changed from",
|
||||||
|
originalPriceInt,
|
||||||
|
"to",
|
||||||
|
currentPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable button and show loading state
|
||||||
|
saveButton.addClass("loading").prop("disabled", true);
|
||||||
|
var originalText = saveButton.text();
|
||||||
|
saveButton.text("Menyimpan...");
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
work_id: parseInt(workId),
|
||||||
|
dealer_id: parseInt(dealerId),
|
||||||
|
price: currentPrice, // Use the validated price
|
||||||
|
currency: "IDR",
|
||||||
|
is_active: isActive ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug: Log the data being sent
|
||||||
|
console.log("Sending data:", data);
|
||||||
|
console.log("Original price:", originalPriceInt);
|
||||||
|
console.log("Current price:", currentPrice);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/work/" + workId + "/prices",
|
||||||
|
method: "POST",
|
||||||
|
data: data,
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||||
|
},
|
||||||
|
beforeSend: function () {
|
||||||
|
console.log("Sending AJAX request with data:", data);
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
// Re-enable button
|
||||||
|
saveButton
|
||||||
|
.removeClass("loading")
|
||||||
|
.prop("disabled", false)
|
||||||
|
.text(originalText);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
toastr.success(response.message);
|
||||||
|
// Update UI
|
||||||
|
updateRowAfterSave(dealerId, response.data);
|
||||||
|
|
||||||
|
// Ensure consistent formatting after update
|
||||||
|
var updatedPrice = priceInput.val().replace(/[^\d]/g, "");
|
||||||
|
if (updatedPrice && updatedPrice !== "0") {
|
||||||
|
var formattedPrice =
|
||||||
|
parseInt(updatedPrice).toLocaleString("id-ID");
|
||||||
|
priceInput.val(formattedPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show brief loading message
|
||||||
|
toastr.info(
|
||||||
|
"Data berhasil disimpan, memperbarui tampilan..."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toastr.error(response.message || "Terjadi kesalahan");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
// Re-enable button
|
||||||
|
saveButton
|
||||||
|
.removeClass("loading")
|
||||||
|
.prop("disabled", false)
|
||||||
|
.text(originalText);
|
||||||
|
|
||||||
|
var message = "Terjadi kesalahan";
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
if (xhr.responseJSON.message) {
|
||||||
|
message = xhr.responseJSON.message;
|
||||||
|
}
|
||||||
|
if (xhr.responseJSON.errors) {
|
||||||
|
// Show validation errors
|
||||||
|
var errorMessages = [];
|
||||||
|
for (var field in xhr.responseJSON.errors) {
|
||||||
|
var fieldName = field;
|
||||||
|
switch (field) {
|
||||||
|
case "work_id":
|
||||||
|
fieldName = "ID Pekerjaan";
|
||||||
|
break;
|
||||||
|
case "dealer_id":
|
||||||
|
fieldName = "ID Dealer";
|
||||||
|
break;
|
||||||
|
case "price":
|
||||||
|
fieldName = "Harga";
|
||||||
|
break;
|
||||||
|
case "currency":
|
||||||
|
fieldName = "Mata Uang";
|
||||||
|
break;
|
||||||
|
case "is_active":
|
||||||
|
fieldName = "Status Aktif";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
errorMessages.push(
|
||||||
|
fieldName +
|
||||||
|
": " +
|
||||||
|
xhr.responseJSON.errors[field][0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
message = errorMessages.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toastr.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var saveAllPrices = function () {
|
||||||
|
// Prevent multiple clicks
|
||||||
|
var saveAllButton = $("#btn-save-all");
|
||||||
|
if (saveAllButton.hasClass("loading")) {
|
||||||
|
return; // Already processing
|
||||||
|
}
|
||||||
|
|
||||||
|
var prices = [];
|
||||||
|
var hasValidPrice = false;
|
||||||
|
|
||||||
|
$(".price-input").each(function () {
|
||||||
|
var dealerId = $(this).attr("name").replace("price_", "");
|
||||||
|
var price = $(this).val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||||
|
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||||
|
var isActive = statusInput.is(":checked");
|
||||||
|
|
||||||
|
if (price && parseInt(price) > 0) {
|
||||||
|
hasValidPrice = true;
|
||||||
|
prices.push({
|
||||||
|
dealer_id: dealerId,
|
||||||
|
price: parseInt(price),
|
||||||
|
currency: "IDR",
|
||||||
|
is_active: isActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasValidPrice) {
|
||||||
|
toastr.error("Minimal satu dealer harus memiliki harga yang valid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable button and show loading state
|
||||||
|
saveAllButton.addClass("loading").prop("disabled", true);
|
||||||
|
var originalText = saveAllButton.text();
|
||||||
|
saveAllButton.text("Menyimpan...");
|
||||||
|
|
||||||
|
// Show confirmation
|
||||||
|
$("#confirmMessage").text(
|
||||||
|
"Apakah Anda yakin ingin menyimpan semua harga?"
|
||||||
|
);
|
||||||
|
$("#confirmModal").modal("show");
|
||||||
|
|
||||||
|
$("#confirmAction")
|
||||||
|
.off("click")
|
||||||
|
.on("click", function () {
|
||||||
|
$("#confirmModal").modal("hide");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/work/" + workId + "/prices/bulk",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
prices: prices,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||||
|
"content"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
// Re-enable button
|
||||||
|
saveAllButton
|
||||||
|
.removeClass("loading")
|
||||||
|
.prop("disabled", false)
|
||||||
|
.text(originalText);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
toastr.success(response.message);
|
||||||
|
|
||||||
|
// Show loading overlay
|
||||||
|
showLoadingOverlay("Memperbarui data...");
|
||||||
|
|
||||||
|
// Reload page to update all data
|
||||||
|
setTimeout(function () {
|
||||||
|
location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
toastr.error(
|
||||||
|
response.message || "Terjadi kesalahan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
// Re-enable button
|
||||||
|
saveAllButton
|
||||||
|
.removeClass("loading")
|
||||||
|
.prop("disabled", false)
|
||||||
|
.text(originalText);
|
||||||
|
|
||||||
|
var message = "Terjadi kesalahan";
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||||
|
message = xhr.responseJSON.message;
|
||||||
|
}
|
||||||
|
toastr.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var deletePrice = function (priceId, dealerId) {
|
||||||
|
$("#confirmMessage").text(
|
||||||
|
"Apakah Anda yakin ingin menghapus harga ini? Harga yang dihapus dapat dipulihkan dengan menyimpan ulang."
|
||||||
|
);
|
||||||
|
$("#confirmModal").modal("show");
|
||||||
|
|
||||||
|
$("#confirmAction")
|
||||||
|
.off("click")
|
||||||
|
.on("click", function () {
|
||||||
|
$("#confirmModal").modal("hide");
|
||||||
|
|
||||||
|
// Show loading overlay
|
||||||
|
showLoadingOverlay("Menghapus harga...");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/work/" + workId + "/prices/" + priceId,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||||
|
"content"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
hideLoadingOverlay();
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
toastr.success(response.message);
|
||||||
|
// Reset the row
|
||||||
|
resetRow(dealerId);
|
||||||
|
} else {
|
||||||
|
toastr.error(
|
||||||
|
response.message || "Terjadi kesalahan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
hideLoadingOverlay();
|
||||||
|
|
||||||
|
var message = "Terjadi kesalahan";
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||||
|
message = xhr.responseJSON.message;
|
||||||
|
}
|
||||||
|
toastr.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle delete button click
|
||||||
|
$(document).on("click", ".delete-price", function () {
|
||||||
|
var priceId = $(this).data("price-id");
|
||||||
|
var dealerId = $(this).data("dealer-id");
|
||||||
|
deletePrice(priceId, dealerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var updateRowAfterSave = function (dealerId, data) {
|
||||||
|
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||||
|
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||||
|
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||||
|
var label = statusInput.siblings("label").find(".status-text");
|
||||||
|
var actionCell = row.find("td:last");
|
||||||
|
|
||||||
|
// Update price input if data contains price
|
||||||
|
if (data.price !== undefined) {
|
||||||
|
// Only update if the price actually changed
|
||||||
|
var currentDisplayValue = priceInput.val().replace(/[^\d]/g, "");
|
||||||
|
var newPrice = parseInt(data.price);
|
||||||
|
|
||||||
|
if (parseInt(currentDisplayValue) !== newPrice) {
|
||||||
|
priceInput.val(newPrice.toLocaleString("id-ID"));
|
||||||
|
// Update the original value for future comparisons
|
||||||
|
priceInput.data("original-value", newPrice.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a new record (price = 0), update the save button
|
||||||
|
if (data.price === 0) {
|
||||||
|
actionCell
|
||||||
|
.find(".save-single")
|
||||||
|
.text("Simpan")
|
||||||
|
.removeClass("btn-warning")
|
||||||
|
.addClass("btn-success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status if data contains is_active
|
||||||
|
if (data.is_active !== undefined) {
|
||||||
|
statusInput.prop("checked", data.is_active);
|
||||||
|
if (data.is_active) {
|
||||||
|
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
.text("Nonaktif")
|
||||||
|
.removeClass("active")
|
||||||
|
.addClass("inactive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update save button if this is a new price save (not just status toggle)
|
||||||
|
if (data.price !== undefined) {
|
||||||
|
actionCell
|
||||||
|
.find(".save-single")
|
||||||
|
.text("Update")
|
||||||
|
.removeClass("btn-success")
|
||||||
|
.addClass("btn-warning");
|
||||||
|
|
||||||
|
// Update delete button
|
||||||
|
if (actionCell.find(".delete-price").length === 0) {
|
||||||
|
// Add delete button if it doesn't exist
|
||||||
|
var deleteBtn =
|
||||||
|
'<button type="button" class="btn btn-sm btn-danger delete-price" data-price-id="' +
|
||||||
|
data.id +
|
||||||
|
'" data-dealer-id="' +
|
||||||
|
dealerId +
|
||||||
|
'" title="Hapus Harga">Hapus</button>';
|
||||||
|
actionCell.find(".d-flex.flex-row.gap-1").append(deleteBtn);
|
||||||
|
} else {
|
||||||
|
// Update existing delete button with new price ID
|
||||||
|
actionCell.find(".delete-price").attr("data-price-id", data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var toggleStatus = function (dealerId, isActive, checkbox, label) {
|
||||||
|
var data = {
|
||||||
|
dealer_id: parseInt(dealerId),
|
||||||
|
is_active: isActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/work/" + workId + "/prices/toggle-status",
|
||||||
|
method: "POST",
|
||||||
|
data: data,
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||||
|
},
|
||||||
|
beforeSend: function () {
|
||||||
|
// Show brief loading indicator on checkbox
|
||||||
|
checkbox.prop("disabled", true);
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
// Re-enable checkbox
|
||||||
|
checkbox.prop("disabled", false);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
toastr.success(response.message);
|
||||||
|
|
||||||
|
// Update UI if needed
|
||||||
|
if (response.data) {
|
||||||
|
// If this is a new record, update the row to show save button
|
||||||
|
if (response.data.price === 0) {
|
||||||
|
updateRowAfterSave(dealerId, response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toastr.error(response.message || "Terjadi kesalahan");
|
||||||
|
// Revert checkbox state
|
||||||
|
checkbox.prop("checked", !isActive);
|
||||||
|
if (!isActive) {
|
||||||
|
label.text("Aktif");
|
||||||
|
} else {
|
||||||
|
label.text("Nonaktif");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
// Re-enable checkbox
|
||||||
|
checkbox.prop("disabled", false);
|
||||||
|
|
||||||
|
var message = "Terjadi kesalahan";
|
||||||
|
if (xhr.responseJSON) {
|
||||||
|
if (xhr.responseJSON.message) {
|
||||||
|
message = xhr.responseJSON.message;
|
||||||
|
}
|
||||||
|
if (xhr.responseJSON.errors) {
|
||||||
|
var errorMessages = [];
|
||||||
|
for (var field in xhr.responseJSON.errors) {
|
||||||
|
var fieldName = field;
|
||||||
|
switch (field) {
|
||||||
|
case "dealer_id":
|
||||||
|
fieldName = "ID Dealer";
|
||||||
|
break;
|
||||||
|
case "is_active":
|
||||||
|
fieldName = "Status Aktif";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
errorMessages.push(
|
||||||
|
fieldName +
|
||||||
|
": " +
|
||||||
|
xhr.responseJSON.errors[field][0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
message = errorMessages.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toastr.error(message);
|
||||||
|
|
||||||
|
// Revert checkbox state
|
||||||
|
checkbox.prop("checked", !isActive);
|
||||||
|
if (!isActive) {
|
||||||
|
label.text("Aktif");
|
||||||
|
} else {
|
||||||
|
label.text("Nonaktif");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetRow = function (dealerId) {
|
||||||
|
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||||
|
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||||
|
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||||
|
var label = statusInput.siblings("label").find(".status-text");
|
||||||
|
var actionCell = row.find("td:last");
|
||||||
|
|
||||||
|
// Reset price input
|
||||||
|
priceInput.val("0");
|
||||||
|
|
||||||
|
// Reset status
|
||||||
|
statusInput.prop("checked", false);
|
||||||
|
label.text("Nonaktif").removeClass("active").addClass("inactive");
|
||||||
|
|
||||||
|
// Remove delete button and update save button
|
||||||
|
actionCell.find(".delete-price").remove();
|
||||||
|
actionCell
|
||||||
|
.find(".save-single")
|
||||||
|
.text("Simpan")
|
||||||
|
.removeClass("btn-warning")
|
||||||
|
.addClass("btn-success");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Public methods
|
||||||
|
return {
|
||||||
|
init: function () {
|
||||||
|
initTable();
|
||||||
|
initEvents();
|
||||||
|
// Initialize price formatting on page load
|
||||||
|
setTimeout(function () {
|
||||||
|
$(".price-input").each(function () {
|
||||||
|
var value = $(this).val();
|
||||||
|
if (value && value !== "0") {
|
||||||
|
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
$(this).val(numValue.toLocaleString("id-ID"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Cleanup timeouts on page unload
|
||||||
|
$(window).on("beforeunload", function () {
|
||||||
|
for (var dealerId in saveTimeout) {
|
||||||
|
if (saveTimeout[dealerId]) {
|
||||||
|
clearTimeout(saveTimeout[dealerId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// On document ready
|
||||||
|
jQuery(document).ready(function () {
|
||||||
|
WorkPrices.init();
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
3
public/js/vendor/axios.min.js
vendored
Executable file
3
public/js/vendor/axios.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
4
public/js/vendor/fixedColumns.bootstrap4.min.js
vendored
Normal file
4
public/js/vendor/fixedColumns.bootstrap4.min.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/*! Bootstrap 4 integration for DataTables' FixedColumns
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(t){var d,o;"function"==typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-fixedcolumns"],function(e){return t(e,window,document)}):"object"==typeof exports?(d=require("jquery"),o=function(e,n){n.fn.dataTable||require("datatables.net-bs4")(e,n),n.fn.dataTable.FixedColumns||require("datatables.net-fixedcolumns")(e,n)},"undefined"==typeof window?module.exports=function(e,n){return e=e||window,n=n||d(e),o(e,n),t(n,0,e.document)}:(o(window,d),module.exports=t(d,window,window.document))):t(jQuery,window,document)}(function(e,n,t,d){"use strict";return e.fn.dataTable});
|
||||||
140
public/js/vendor/lodash.min.js
vendored
Executable file
140
public/js/vendor/lodash.min.js
vendored
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Lodash <https://lodash.com/>
|
||||||
|
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
|
||||||
|
* Released under MIT license <https://lodash.com/license>
|
||||||
|
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
||||||
|
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||||
|
*/
|
||||||
|
(function(){function n(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function t(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u<i;){var o=n[u];t(e,o,r(o),n)}return e}function r(n,t){for(var r=-1,e=null==n?0:n.length;++r<e&&t(n[r],r,n)!==!1;);return n}function e(n,t){for(var r=null==n?0:n.length;r--&&t(n[r],r,n)!==!1;);return n}function u(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(!t(n[r],r,n))return!1;
|
||||||
|
return!0}function i(n,t){for(var r=-1,e=null==n?0:n.length,u=0,i=[];++r<e;){var o=n[r];t(o,r,n)&&(i[u++]=o)}return i}function o(n,t){return!!(null==n?0:n.length)&&y(n,t,0)>-1}function f(n,t,r){for(var e=-1,u=null==n?0:n.length;++e<u;)if(r(t,n[e]))return!0;return!1}function c(n,t){for(var r=-1,e=null==n?0:n.length,u=Array(e);++r<e;)u[r]=t(n[r],r,n);return u}function a(n,t){for(var r=-1,e=t.length,u=n.length;++r<e;)n[u+r]=t[r];return n}function l(n,t,r,e){var u=-1,i=null==n?0:n.length;for(e&&i&&(r=n[++u]);++u<i;)r=t(r,n[u],u,n);
|
||||||
|
return r}function s(n,t,r,e){var u=null==n?0:n.length;for(e&&u&&(r=n[--u]);u--;)r=t(r,n[u],u,n);return r}function h(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(t(n[r],r,n))return!0;return!1}function p(n){return n.split("")}function _(n){return n.match($t)||[]}function v(n,t,r){var e;return r(n,function(n,r,u){if(t(n,r,u))return e=r,!1}),e}function g(n,t,r,e){for(var u=n.length,i=r+(e?1:-1);e?i--:++i<u;)if(t(n[i],i,n))return i;return-1}function y(n,t,r){return t===t?Z(n,t,r):g(n,b,r)}function d(n,t,r,e){
|
||||||
|
for(var u=r-1,i=n.length;++u<i;)if(e(n[u],t))return u;return-1}function b(n){return n!==n}function w(n,t){var r=null==n?0:n.length;return r?k(n,t)/r:Cn}function m(n){return function(t){return null==t?X:t[n]}}function x(n){return function(t){return null==n?X:n[t]}}function j(n,t,r,e,u){return u(n,function(n,u,i){r=e?(e=!1,n):t(r,n,u,i)}),r}function A(n,t){var r=n.length;for(n.sort(t);r--;)n[r]=n[r].value;return n}function k(n,t){for(var r,e=-1,u=n.length;++e<u;){var i=t(n[e]);i!==X&&(r=r===X?i:r+i);
|
||||||
|
}return r}function O(n,t){for(var r=-1,e=Array(n);++r<n;)e[r]=t(r);return e}function I(n,t){return c(t,function(t){return[t,n[t]]})}function R(n){return n?n.slice(0,H(n)+1).replace(Lt,""):n}function z(n){return function(t){return n(t)}}function E(n,t){return c(t,function(t){return n[t]})}function S(n,t){return n.has(t)}function W(n,t){for(var r=-1,e=n.length;++r<e&&y(t,n[r],0)>-1;);return r}function L(n,t){for(var r=n.length;r--&&y(t,n[r],0)>-1;);return r}function C(n,t){for(var r=n.length,e=0;r--;)n[r]===t&&++e;
|
||||||
|
return e}function U(n){return"\\"+Yr[n]}function B(n,t){return null==n?X:n[t]}function T(n){return Nr.test(n)}function $(n){return Pr.test(n)}function D(n){for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}function M(n){var t=-1,r=Array(n.size);return n.forEach(function(n,e){r[++t]=[e,n]}),r}function F(n,t){return function(r){return n(t(r))}}function N(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){var o=n[r];o!==t&&o!==cn||(n[r]=cn,i[u++]=r)}return i}function P(n){var t=-1,r=Array(n.size);
|
||||||
|
return n.forEach(function(n){r[++t]=n}),r}function q(n){var t=-1,r=Array(n.size);return n.forEach(function(n){r[++t]=[n,n]}),r}function Z(n,t,r){for(var e=r-1,u=n.length;++e<u;)if(n[e]===t)return e;return-1}function K(n,t,r){for(var e=r+1;e--;)if(n[e]===t)return e;return e}function V(n){return T(n)?J(n):_e(n)}function G(n){return T(n)?Y(n):p(n)}function H(n){for(var t=n.length;t--&&Ct.test(n.charAt(t)););return t}function J(n){for(var t=Mr.lastIndex=0;Mr.test(n);)++t;return t}function Y(n){return n.match(Mr)||[];
|
||||||
|
}function Q(n){return n.match(Fr)||[]}var X,nn="4.17.21",tn=200,rn="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",en="Expected a function",un="Invalid `variable` option passed into `_.template`",on="__lodash_hash_undefined__",fn=500,cn="__lodash_placeholder__",an=1,ln=2,sn=4,hn=1,pn=2,_n=1,vn=2,gn=4,yn=8,dn=16,bn=32,wn=64,mn=128,xn=256,jn=512,An=30,kn="...",On=800,In=16,Rn=1,zn=2,En=3,Sn=1/0,Wn=9007199254740991,Ln=1.7976931348623157e308,Cn=NaN,Un=4294967295,Bn=Un-1,Tn=Un>>>1,$n=[["ary",mn],["bind",_n],["bindKey",vn],["curry",yn],["curryRight",dn],["flip",jn],["partial",bn],["partialRight",wn],["rearg",xn]],Dn="[object Arguments]",Mn="[object Array]",Fn="[object AsyncFunction]",Nn="[object Boolean]",Pn="[object Date]",qn="[object DOMException]",Zn="[object Error]",Kn="[object Function]",Vn="[object GeneratorFunction]",Gn="[object Map]",Hn="[object Number]",Jn="[object Null]",Yn="[object Object]",Qn="[object Promise]",Xn="[object Proxy]",nt="[object RegExp]",tt="[object Set]",rt="[object String]",et="[object Symbol]",ut="[object Undefined]",it="[object WeakMap]",ot="[object WeakSet]",ft="[object ArrayBuffer]",ct="[object DataView]",at="[object Float32Array]",lt="[object Float64Array]",st="[object Int8Array]",ht="[object Int16Array]",pt="[object Int32Array]",_t="[object Uint8Array]",vt="[object Uint8ClampedArray]",gt="[object Uint16Array]",yt="[object Uint32Array]",dt=/\b__p \+= '';/g,bt=/\b(__p \+=) '' \+/g,wt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,mt=/&(?:amp|lt|gt|quot|#39);/g,xt=/[&<>"']/g,jt=RegExp(mt.source),At=RegExp(xt.source),kt=/<%-([\s\S]+?)%>/g,Ot=/<%([\s\S]+?)%>/g,It=/<%=([\s\S]+?)%>/g,Rt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,zt=/^\w*$/,Et=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,St=/[\\^$.*+?()[\]{}|]/g,Wt=RegExp(St.source),Lt=/^\s+/,Ct=/\s/,Ut=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Bt=/\{\n\/\* \[wrapped with (.+)\] \*/,Tt=/,? & /,$t=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Dt=/[()=,{}\[\]\/\s]/,Mt=/\\(\\)?/g,Ft=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Nt=/\w*$/,Pt=/^[-+]0x[0-9a-f]+$/i,qt=/^0b[01]+$/i,Zt=/^\[object .+?Constructor\]$/,Kt=/^0o[0-7]+$/i,Vt=/^(?:0|[1-9]\d*)$/,Gt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Ht=/($^)/,Jt=/['\n\r\u2028\u2029\\]/g,Yt="\\ud800-\\udfff",Qt="\\u0300-\\u036f",Xt="\\ufe20-\\ufe2f",nr="\\u20d0-\\u20ff",tr=Qt+Xt+nr,rr="\\u2700-\\u27bf",er="a-z\\xdf-\\xf6\\xf8-\\xff",ur="\\xac\\xb1\\xd7\\xf7",ir="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",or="\\u2000-\\u206f",fr=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",cr="A-Z\\xc0-\\xd6\\xd8-\\xde",ar="\\ufe0e\\ufe0f",lr=ur+ir+or+fr,sr="['\u2019]",hr="["+Yt+"]",pr="["+lr+"]",_r="["+tr+"]",vr="\\d+",gr="["+rr+"]",yr="["+er+"]",dr="[^"+Yt+lr+vr+rr+er+cr+"]",br="\\ud83c[\\udffb-\\udfff]",wr="(?:"+_r+"|"+br+")",mr="[^"+Yt+"]",xr="(?:\\ud83c[\\udde6-\\uddff]){2}",jr="[\\ud800-\\udbff][\\udc00-\\udfff]",Ar="["+cr+"]",kr="\\u200d",Or="(?:"+yr+"|"+dr+")",Ir="(?:"+Ar+"|"+dr+")",Rr="(?:"+sr+"(?:d|ll|m|re|s|t|ve))?",zr="(?:"+sr+"(?:D|LL|M|RE|S|T|VE))?",Er=wr+"?",Sr="["+ar+"]?",Wr="(?:"+kr+"(?:"+[mr,xr,jr].join("|")+")"+Sr+Er+")*",Lr="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Cr="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Ur=Sr+Er+Wr,Br="(?:"+[gr,xr,jr].join("|")+")"+Ur,Tr="(?:"+[mr+_r+"?",_r,xr,jr,hr].join("|")+")",$r=RegExp(sr,"g"),Dr=RegExp(_r,"g"),Mr=RegExp(br+"(?="+br+")|"+Tr+Ur,"g"),Fr=RegExp([Ar+"?"+yr+"+"+Rr+"(?="+[pr,Ar,"$"].join("|")+")",Ir+"+"+zr+"(?="+[pr,Ar+Or,"$"].join("|")+")",Ar+"?"+Or+"+"+Rr,Ar+"+"+zr,Cr,Lr,vr,Br].join("|"),"g"),Nr=RegExp("["+kr+Yt+tr+ar+"]"),Pr=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,qr=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Zr=-1,Kr={};
|
||||||
|
Kr[at]=Kr[lt]=Kr[st]=Kr[ht]=Kr[pt]=Kr[_t]=Kr[vt]=Kr[gt]=Kr[yt]=!0,Kr[Dn]=Kr[Mn]=Kr[ft]=Kr[Nn]=Kr[ct]=Kr[Pn]=Kr[Zn]=Kr[Kn]=Kr[Gn]=Kr[Hn]=Kr[Yn]=Kr[nt]=Kr[tt]=Kr[rt]=Kr[it]=!1;var Vr={};Vr[Dn]=Vr[Mn]=Vr[ft]=Vr[ct]=Vr[Nn]=Vr[Pn]=Vr[at]=Vr[lt]=Vr[st]=Vr[ht]=Vr[pt]=Vr[Gn]=Vr[Hn]=Vr[Yn]=Vr[nt]=Vr[tt]=Vr[rt]=Vr[et]=Vr[_t]=Vr[vt]=Vr[gt]=Vr[yt]=!0,Vr[Zn]=Vr[Kn]=Vr[it]=!1;var Gr={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a",
|
||||||
|
"\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae",
|
||||||
|
"\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g",
|
||||||
|
"\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O",
|
||||||
|
"\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w",
|
||||||
|
"\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},Hr={"&":"&","<":"<",">":">",'"':""","'":"'"},Jr={"&":"&","<":"<",">":">",""":'"',"'":"'"},Yr={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Qr=parseFloat,Xr=parseInt,ne="object"==typeof global&&global&&global.Object===Object&&global,te="object"==typeof self&&self&&self.Object===Object&&self,re=ne||te||Function("return this")(),ee="object"==typeof exports&&exports&&!exports.nodeType&&exports,ue=ee&&"object"==typeof module&&module&&!module.nodeType&&module,ie=ue&&ue.exports===ee,oe=ie&&ne.process,fe=function(){
|
||||||
|
try{var n=ue&&ue.require&&ue.require("util").types;return n?n:oe&&oe.binding&&oe.binding("util")}catch(n){}}(),ce=fe&&fe.isArrayBuffer,ae=fe&&fe.isDate,le=fe&&fe.isMap,se=fe&&fe.isRegExp,he=fe&&fe.isSet,pe=fe&&fe.isTypedArray,_e=m("length"),ve=x(Gr),ge=x(Hr),ye=x(Jr),de=function p(x){function Z(n){if(cc(n)&&!bh(n)&&!(n instanceof Ct)){if(n instanceof Y)return n;if(bl.call(n,"__wrapped__"))return eo(n)}return new Y(n)}function J(){}function Y(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,
|
||||||
|
this.__index__=0,this.__values__=X}function Ct(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=Un,this.__views__=[]}function $t(){var n=new Ct(this.__wrapped__);return n.__actions__=Tu(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Tu(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Tu(this.__views__),n}function Yt(){if(this.__filtered__){var n=new Ct(this);n.__dir__=-1,
|
||||||
|
n.__filtered__=!0}else n=this.clone(),n.__dir__*=-1;return n}function Qt(){var n=this.__wrapped__.value(),t=this.__dir__,r=bh(n),e=t<0,u=r?n.length:0,i=Oi(0,u,this.__views__),o=i.start,f=i.end,c=f-o,a=e?f:o-1,l=this.__iteratees__,s=l.length,h=0,p=Hl(c,this.__takeCount__);if(!r||!e&&u==c&&p==c)return wu(n,this.__actions__);var _=[];n:for(;c--&&h<p;){a+=t;for(var v=-1,g=n[a];++v<s;){var y=l[v],d=y.iteratee,b=y.type,w=d(g);if(b==zn)g=w;else if(!w){if(b==Rn)continue n;break n}}_[h++]=g}return _}function Xt(n){
|
||||||
|
var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function nr(){this.__data__=is?is(null):{},this.size=0}function tr(n){var t=this.has(n)&&delete this.__data__[n];return this.size-=t?1:0,t}function rr(n){var t=this.__data__;if(is){var r=t[n];return r===on?X:r}return bl.call(t,n)?t[n]:X}function er(n){var t=this.__data__;return is?t[n]!==X:bl.call(t,n)}function ur(n,t){var r=this.__data__;return this.size+=this.has(n)?0:1,r[n]=is&&t===X?on:t,this}function ir(n){
|
||||||
|
var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function or(){this.__data__=[],this.size=0}function fr(n){var t=this.__data__,r=Wr(t,n);return!(r<0)&&(r==t.length-1?t.pop():Ll.call(t,r,1),--this.size,!0)}function cr(n){var t=this.__data__,r=Wr(t,n);return r<0?X:t[r][1]}function ar(n){return Wr(this.__data__,n)>-1}function lr(n,t){var r=this.__data__,e=Wr(r,n);return e<0?(++this.size,r.push([n,t])):r[e][1]=t,this}function sr(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){
|
||||||
|
var e=n[t];this.set(e[0],e[1])}}function hr(){this.size=0,this.__data__={hash:new Xt,map:new(ts||ir),string:new Xt}}function pr(n){var t=xi(this,n).delete(n);return this.size-=t?1:0,t}function _r(n){return xi(this,n).get(n)}function vr(n){return xi(this,n).has(n)}function gr(n,t){var r=xi(this,n),e=r.size;return r.set(n,t),this.size+=r.size==e?0:1,this}function yr(n){var t=-1,r=null==n?0:n.length;for(this.__data__=new sr;++t<r;)this.add(n[t])}function dr(n){return this.__data__.set(n,on),this}function br(n){
|
||||||
|
return this.__data__.has(n)}function wr(n){this.size=(this.__data__=new ir(n)).size}function mr(){this.__data__=new ir,this.size=0}function xr(n){var t=this.__data__,r=t.delete(n);return this.size=t.size,r}function jr(n){return this.__data__.get(n)}function Ar(n){return this.__data__.has(n)}function kr(n,t){var r=this.__data__;if(r instanceof ir){var e=r.__data__;if(!ts||e.length<tn-1)return e.push([n,t]),this.size=++r.size,this;r=this.__data__=new sr(e)}return r.set(n,t),this.size=r.size,this}function Or(n,t){
|
||||||
|
var r=bh(n),e=!r&&dh(n),u=!r&&!e&&mh(n),i=!r&&!e&&!u&&Oh(n),o=r||e||u||i,f=o?O(n.length,hl):[],c=f.length;for(var a in n)!t&&!bl.call(n,a)||o&&("length"==a||u&&("offset"==a||"parent"==a)||i&&("buffer"==a||"byteLength"==a||"byteOffset"==a)||Ci(a,c))||f.push(a);return f}function Ir(n){var t=n.length;return t?n[tu(0,t-1)]:X}function Rr(n,t){return Xi(Tu(n),Mr(t,0,n.length))}function zr(n){return Xi(Tu(n))}function Er(n,t,r){(r===X||Gf(n[t],r))&&(r!==X||t in n)||Br(n,t,r)}function Sr(n,t,r){var e=n[t];
|
||||||
|
bl.call(n,t)&&Gf(e,r)&&(r!==X||t in n)||Br(n,t,r)}function Wr(n,t){for(var r=n.length;r--;)if(Gf(n[r][0],t))return r;return-1}function Lr(n,t,r,e){return ys(n,function(n,u,i){t(e,n,r(n),i)}),e}function Cr(n,t){return n&&$u(t,Pc(t),n)}function Ur(n,t){return n&&$u(t,qc(t),n)}function Br(n,t,r){"__proto__"==t&&Tl?Tl(n,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):n[t]=r}function Tr(n,t){for(var r=-1,e=t.length,u=il(e),i=null==n;++r<e;)u[r]=i?X:Mc(n,t[r]);return u}function Mr(n,t,r){return n===n&&(r!==X&&(n=n<=r?n:r),
|
||||||
|
t!==X&&(n=n>=t?n:t)),n}function Fr(n,t,e,u,i,o){var f,c=t&an,a=t&ln,l=t&sn;if(e&&(f=i?e(n,u,i,o):e(n)),f!==X)return f;if(!fc(n))return n;var s=bh(n);if(s){if(f=zi(n),!c)return Tu(n,f)}else{var h=zs(n),p=h==Kn||h==Vn;if(mh(n))return Iu(n,c);if(h==Yn||h==Dn||p&&!i){if(f=a||p?{}:Ei(n),!c)return a?Mu(n,Ur(f,n)):Du(n,Cr(f,n))}else{if(!Vr[h])return i?n:{};f=Si(n,h,c)}}o||(o=new wr);var _=o.get(n);if(_)return _;o.set(n,f),kh(n)?n.forEach(function(r){f.add(Fr(r,t,e,r,n,o))}):jh(n)&&n.forEach(function(r,u){
|
||||||
|
f.set(u,Fr(r,t,e,u,n,o))});var v=l?a?di:yi:a?qc:Pc,g=s?X:v(n);return r(g||n,function(r,u){g&&(u=r,r=n[u]),Sr(f,u,Fr(r,t,e,u,n,o))}),f}function Nr(n){var t=Pc(n);return function(r){return Pr(r,n,t)}}function Pr(n,t,r){var e=r.length;if(null==n)return!e;for(n=ll(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===X&&!(u in n)||!i(o))return!1}return!0}function Gr(n,t,r){if("function"!=typeof n)throw new pl(en);return Ws(function(){n.apply(X,r)},t)}function Hr(n,t,r,e){var u=-1,i=o,a=!0,l=n.length,s=[],h=t.length;
|
||||||
|
if(!l)return s;r&&(t=c(t,z(r))),e?(i=f,a=!1):t.length>=tn&&(i=S,a=!1,t=new yr(t));n:for(;++u<l;){var p=n[u],_=null==r?p:r(p);if(p=e||0!==p?p:0,a&&_===_){for(var v=h;v--;)if(t[v]===_)continue n;s.push(p)}else i(t,_,e)||s.push(p)}return s}function Jr(n,t){var r=!0;return ys(n,function(n,e,u){return r=!!t(n,e,u)}),r}function Yr(n,t,r){for(var e=-1,u=n.length;++e<u;){var i=n[e],o=t(i);if(null!=o&&(f===X?o===o&&!bc(o):r(o,f)))var f=o,c=i}return c}function ne(n,t,r,e){var u=n.length;for(r=kc(r),r<0&&(r=-r>u?0:u+r),
|
||||||
|
e=e===X||e>u?u:kc(e),e<0&&(e+=u),e=r>e?0:Oc(e);r<e;)n[r++]=t;return n}function te(n,t){var r=[];return ys(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function ee(n,t,r,e,u){var i=-1,o=n.length;for(r||(r=Li),u||(u=[]);++i<o;){var f=n[i];t>0&&r(f)?t>1?ee(f,t-1,r,e,u):a(u,f):e||(u[u.length]=f)}return u}function ue(n,t){return n&&bs(n,t,Pc)}function oe(n,t){return n&&ws(n,t,Pc)}function fe(n,t){return i(t,function(t){return uc(n[t])})}function _e(n,t){t=ku(t,n);for(var r=0,e=t.length;null!=n&&r<e;)n=n[no(t[r++])];
|
||||||
|
return r&&r==e?n:X}function de(n,t,r){var e=t(n);return bh(n)?e:a(e,r(n))}function we(n){return null==n?n===X?ut:Jn:Bl&&Bl in ll(n)?ki(n):Ki(n)}function me(n,t){return n>t}function xe(n,t){return null!=n&&bl.call(n,t)}function je(n,t){return null!=n&&t in ll(n)}function Ae(n,t,r){return n>=Hl(t,r)&&n<Gl(t,r)}function ke(n,t,r){for(var e=r?f:o,u=n[0].length,i=n.length,a=i,l=il(i),s=1/0,h=[];a--;){var p=n[a];a&&t&&(p=c(p,z(t))),s=Hl(p.length,s),l[a]=!r&&(t||u>=120&&p.length>=120)?new yr(a&&p):X}p=n[0];
|
||||||
|
var _=-1,v=l[0];n:for(;++_<u&&h.length<s;){var g=p[_],y=t?t(g):g;if(g=r||0!==g?g:0,!(v?S(v,y):e(h,y,r))){for(a=i;--a;){var d=l[a];if(!(d?S(d,y):e(n[a],y,r)))continue n}v&&v.push(y),h.push(g)}}return h}function Oe(n,t,r,e){return ue(n,function(n,u,i){t(e,r(n),u,i)}),e}function Ie(t,r,e){r=ku(r,t),t=Gi(t,r);var u=null==t?t:t[no(jo(r))];return null==u?X:n(u,t,e)}function Re(n){return cc(n)&&we(n)==Dn}function ze(n){return cc(n)&&we(n)==ft}function Ee(n){return cc(n)&&we(n)==Pn}function Se(n,t,r,e,u){
|
||||||
|
return n===t||(null==n||null==t||!cc(n)&&!cc(t)?n!==n&&t!==t:We(n,t,r,e,Se,u))}function We(n,t,r,e,u,i){var o=bh(n),f=bh(t),c=o?Mn:zs(n),a=f?Mn:zs(t);c=c==Dn?Yn:c,a=a==Dn?Yn:a;var l=c==Yn,s=a==Yn,h=c==a;if(h&&mh(n)){if(!mh(t))return!1;o=!0,l=!1}if(h&&!l)return i||(i=new wr),o||Oh(n)?pi(n,t,r,e,u,i):_i(n,t,c,r,e,u,i);if(!(r&hn)){var p=l&&bl.call(n,"__wrapped__"),_=s&&bl.call(t,"__wrapped__");if(p||_){var v=p?n.value():n,g=_?t.value():t;return i||(i=new wr),u(v,g,r,e,i)}}return!!h&&(i||(i=new wr),vi(n,t,r,e,u,i));
|
||||||
|
}function Le(n){return cc(n)&&zs(n)==Gn}function Ce(n,t,r,e){var u=r.length,i=u,o=!e;if(null==n)return!i;for(n=ll(n);u--;){var f=r[u];if(o&&f[2]?f[1]!==n[f[0]]:!(f[0]in n))return!1}for(;++u<i;){f=r[u];var c=f[0],a=n[c],l=f[1];if(o&&f[2]){if(a===X&&!(c in n))return!1}else{var s=new wr;if(e)var h=e(a,l,c,n,t,s);if(!(h===X?Se(l,a,hn|pn,e,s):h))return!1}}return!0}function Ue(n){return!(!fc(n)||Di(n))&&(uc(n)?kl:Zt).test(to(n))}function Be(n){return cc(n)&&we(n)==nt}function Te(n){return cc(n)&&zs(n)==tt;
|
||||||
|
}function $e(n){return cc(n)&&oc(n.length)&&!!Kr[we(n)]}function De(n){return"function"==typeof n?n:null==n?La:"object"==typeof n?bh(n)?Ze(n[0],n[1]):qe(n):Fa(n)}function Me(n){if(!Mi(n))return Vl(n);var t=[];for(var r in ll(n))bl.call(n,r)&&"constructor"!=r&&t.push(r);return t}function Fe(n){if(!fc(n))return Zi(n);var t=Mi(n),r=[];for(var e in n)("constructor"!=e||!t&&bl.call(n,e))&&r.push(e);return r}function Ne(n,t){return n<t}function Pe(n,t){var r=-1,e=Hf(n)?il(n.length):[];return ys(n,function(n,u,i){
|
||||||
|
e[++r]=t(n,u,i)}),e}function qe(n){var t=ji(n);return 1==t.length&&t[0][2]?Ni(t[0][0],t[0][1]):function(r){return r===n||Ce(r,n,t)}}function Ze(n,t){return Bi(n)&&Fi(t)?Ni(no(n),t):function(r){var e=Mc(r,n);return e===X&&e===t?Nc(r,n):Se(t,e,hn|pn)}}function Ke(n,t,r,e,u){n!==t&&bs(t,function(i,o){if(u||(u=new wr),fc(i))Ve(n,t,o,r,Ke,e,u);else{var f=e?e(Ji(n,o),i,o+"",n,t,u):X;f===X&&(f=i),Er(n,o,f)}},qc)}function Ve(n,t,r,e,u,i,o){var f=Ji(n,r),c=Ji(t,r),a=o.get(c);if(a)return Er(n,r,a),X;var l=i?i(f,c,r+"",n,t,o):X,s=l===X;
|
||||||
|
if(s){var h=bh(c),p=!h&&mh(c),_=!h&&!p&&Oh(c);l=c,h||p||_?bh(f)?l=f:Jf(f)?l=Tu(f):p?(s=!1,l=Iu(c,!0)):_?(s=!1,l=Wu(c,!0)):l=[]:gc(c)||dh(c)?(l=f,dh(f)?l=Rc(f):fc(f)&&!uc(f)||(l=Ei(c))):s=!1}s&&(o.set(c,l),u(l,c,e,i,o),o.delete(c)),Er(n,r,l)}function Ge(n,t){var r=n.length;if(r)return t+=t<0?r:0,Ci(t,r)?n[t]:X}function He(n,t,r){t=t.length?c(t,function(n){return bh(n)?function(t){return _e(t,1===n.length?n[0]:n)}:n}):[La];var e=-1;return t=c(t,z(mi())),A(Pe(n,function(n,r,u){return{criteria:c(t,function(t){
|
||||||
|
return t(n)}),index:++e,value:n}}),function(n,t){return Cu(n,t,r)})}function Je(n,t){return Ye(n,t,function(t,r){return Nc(n,r)})}function Ye(n,t,r){for(var e=-1,u=t.length,i={};++e<u;){var o=t[e],f=_e(n,o);r(f,o)&&fu(i,ku(o,n),f)}return i}function Qe(n){return function(t){return _e(t,n)}}function Xe(n,t,r,e){var u=e?d:y,i=-1,o=t.length,f=n;for(n===t&&(t=Tu(t)),r&&(f=c(n,z(r)));++i<o;)for(var a=0,l=t[i],s=r?r(l):l;(a=u(f,s,a,e))>-1;)f!==n&&Ll.call(f,a,1),Ll.call(n,a,1);return n}function nu(n,t){for(var r=n?t.length:0,e=r-1;r--;){
|
||||||
|
var u=t[r];if(r==e||u!==i){var i=u;Ci(u)?Ll.call(n,u,1):yu(n,u)}}return n}function tu(n,t){return n+Nl(Ql()*(t-n+1))}function ru(n,t,r,e){for(var u=-1,i=Gl(Fl((t-n)/(r||1)),0),o=il(i);i--;)o[e?i:++u]=n,n+=r;return o}function eu(n,t){var r="";if(!n||t<1||t>Wn)return r;do t%2&&(r+=n),t=Nl(t/2),t&&(n+=n);while(t);return r}function uu(n,t){return Ls(Vi(n,t,La),n+"")}function iu(n){return Ir(ra(n))}function ou(n,t){var r=ra(n);return Xi(r,Mr(t,0,r.length))}function fu(n,t,r,e){if(!fc(n))return n;t=ku(t,n);
|
||||||
|
for(var u=-1,i=t.length,o=i-1,f=n;null!=f&&++u<i;){var c=no(t[u]),a=r;if("__proto__"===c||"constructor"===c||"prototype"===c)return n;if(u!=o){var l=f[c];a=e?e(l,c,f):X,a===X&&(a=fc(l)?l:Ci(t[u+1])?[]:{})}Sr(f,c,a),f=f[c]}return n}function cu(n){return Xi(ra(n))}function au(n,t,r){var e=-1,u=n.length;t<0&&(t=-t>u?0:u+t),r=r>u?u:r,r<0&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0;for(var i=il(u);++e<u;)i[e]=n[e+t];return i}function lu(n,t){var r;return ys(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function su(n,t,r){
|
||||||
|
var e=0,u=null==n?e:n.length;if("number"==typeof t&&t===t&&u<=Tn){for(;e<u;){var i=e+u>>>1,o=n[i];null!==o&&!bc(o)&&(r?o<=t:o<t)?e=i+1:u=i}return u}return hu(n,t,La,r)}function hu(n,t,r,e){var u=0,i=null==n?0:n.length;if(0===i)return 0;t=r(t);for(var o=t!==t,f=null===t,c=bc(t),a=t===X;u<i;){var l=Nl((u+i)/2),s=r(n[l]),h=s!==X,p=null===s,_=s===s,v=bc(s);if(o)var g=e||_;else g=a?_&&(e||h):f?_&&h&&(e||!p):c?_&&h&&!p&&(e||!v):!p&&!v&&(e?s<=t:s<t);g?u=l+1:i=l}return Hl(i,Bn)}function pu(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){
|
||||||
|
var o=n[r],f=t?t(o):o;if(!r||!Gf(f,c)){var c=f;i[u++]=0===o?0:o}}return i}function _u(n){return"number"==typeof n?n:bc(n)?Cn:+n}function vu(n){if("string"==typeof n)return n;if(bh(n))return c(n,vu)+"";if(bc(n))return vs?vs.call(n):"";var t=n+"";return"0"==t&&1/n==-Sn?"-0":t}function gu(n,t,r){var e=-1,u=o,i=n.length,c=!0,a=[],l=a;if(r)c=!1,u=f;else if(i>=tn){var s=t?null:ks(n);if(s)return P(s);c=!1,u=S,l=new yr}else l=t?[]:a;n:for(;++e<i;){var h=n[e],p=t?t(h):h;if(h=r||0!==h?h:0,c&&p===p){for(var _=l.length;_--;)if(l[_]===p)continue n;
|
||||||
|
t&&l.push(p),a.push(h)}else u(l,p,r)||(l!==a&&l.push(p),a.push(h))}return a}function yu(n,t){return t=ku(t,n),n=Gi(n,t),null==n||delete n[no(jo(t))]}function du(n,t,r,e){return fu(n,t,r(_e(n,t)),e)}function bu(n,t,r,e){for(var u=n.length,i=e?u:-1;(e?i--:++i<u)&&t(n[i],i,n););return r?au(n,e?0:i,e?i+1:u):au(n,e?i+1:0,e?u:i)}function wu(n,t){var r=n;return r instanceof Ct&&(r=r.value()),l(t,function(n,t){return t.func.apply(t.thisArg,a([n],t.args))},r)}function mu(n,t,r){var e=n.length;if(e<2)return e?gu(n[0]):[];
|
||||||
|
for(var u=-1,i=il(e);++u<e;)for(var o=n[u],f=-1;++f<e;)f!=u&&(i[u]=Hr(i[u]||o,n[f],t,r));return gu(ee(i,1),t,r)}function xu(n,t,r){for(var e=-1,u=n.length,i=t.length,o={};++e<u;){r(o,n[e],e<i?t[e]:X)}return o}function ju(n){return Jf(n)?n:[]}function Au(n){return"function"==typeof n?n:La}function ku(n,t){return bh(n)?n:Bi(n,t)?[n]:Cs(Ec(n))}function Ou(n,t,r){var e=n.length;return r=r===X?e:r,!t&&r>=e?n:au(n,t,r)}function Iu(n,t){if(t)return n.slice();var r=n.length,e=zl?zl(r):new n.constructor(r);
|
||||||
|
return n.copy(e),e}function Ru(n){var t=new n.constructor(n.byteLength);return new Rl(t).set(new Rl(n)),t}function zu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.byteLength)}function Eu(n){var t=new n.constructor(n.source,Nt.exec(n));return t.lastIndex=n.lastIndex,t}function Su(n){return _s?ll(_s.call(n)):{}}function Wu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.length)}function Lu(n,t){if(n!==t){var r=n!==X,e=null===n,u=n===n,i=bc(n),o=t!==X,f=null===t,c=t===t,a=bc(t);
|
||||||
|
if(!f&&!a&&!i&&n>t||i&&o&&c&&!f&&!a||e&&o&&c||!r&&c||!u)return 1;if(!e&&!i&&!a&&n<t||a&&r&&u&&!e&&!i||f&&r&&u||!o&&u||!c)return-1}return 0}function Cu(n,t,r){for(var e=-1,u=n.criteria,i=t.criteria,o=u.length,f=r.length;++e<o;){var c=Lu(u[e],i[e]);if(c){if(e>=f)return c;return c*("desc"==r[e]?-1:1)}}return n.index-t.index}function Uu(n,t,r,e){for(var u=-1,i=n.length,o=r.length,f=-1,c=t.length,a=Gl(i-o,0),l=il(c+a),s=!e;++f<c;)l[f]=t[f];for(;++u<o;)(s||u<i)&&(l[r[u]]=n[u]);for(;a--;)l[f++]=n[u++];return l;
|
||||||
|
}function Bu(n,t,r,e){for(var u=-1,i=n.length,o=-1,f=r.length,c=-1,a=t.length,l=Gl(i-f,0),s=il(l+a),h=!e;++u<l;)s[u]=n[u];for(var p=u;++c<a;)s[p+c]=t[c];for(;++o<f;)(h||u<i)&&(s[p+r[o]]=n[u++]);return s}function Tu(n,t){var r=-1,e=n.length;for(t||(t=il(e));++r<e;)t[r]=n[r];return t}function $u(n,t,r,e){var u=!r;r||(r={});for(var i=-1,o=t.length;++i<o;){var f=t[i],c=e?e(r[f],n[f],f,r,n):X;c===X&&(c=n[f]),u?Br(r,f,c):Sr(r,f,c)}return r}function Du(n,t){return $u(n,Is(n),t)}function Mu(n,t){return $u(n,Rs(n),t);
|
||||||
|
}function Fu(n,r){return function(e,u){var i=bh(e)?t:Lr,o=r?r():{};return i(e,n,mi(u,2),o)}}function Nu(n){return uu(function(t,r){var e=-1,u=r.length,i=u>1?r[u-1]:X,o=u>2?r[2]:X;for(i=n.length>3&&"function"==typeof i?(u--,i):X,o&&Ui(r[0],r[1],o)&&(i=u<3?X:i,u=1),t=ll(t);++e<u;){var f=r[e];f&&n(t,f,e,i)}return t})}function Pu(n,t){return function(r,e){if(null==r)return r;if(!Hf(r))return n(r,e);for(var u=r.length,i=t?u:-1,o=ll(r);(t?i--:++i<u)&&e(o[i],i,o)!==!1;);return r}}function qu(n){return function(t,r,e){
|
||||||
|
for(var u=-1,i=ll(t),o=e(t),f=o.length;f--;){var c=o[n?f:++u];if(r(i[c],c,i)===!1)break}return t}}function Zu(n,t,r){function e(){return(this&&this!==re&&this instanceof e?i:n).apply(u?r:this,arguments)}var u=t&_n,i=Gu(n);return e}function Ku(n){return function(t){t=Ec(t);var r=T(t)?G(t):X,e=r?r[0]:t.charAt(0),u=r?Ou(r,1).join(""):t.slice(1);return e[n]()+u}}function Vu(n){return function(t){return l(Ra(ca(t).replace($r,"")),n,"")}}function Gu(n){return function(){var t=arguments;switch(t.length){
|
||||||
|
case 0:return new n;case 1:return new n(t[0]);case 2:return new n(t[0],t[1]);case 3:return new n(t[0],t[1],t[2]);case 4:return new n(t[0],t[1],t[2],t[3]);case 5:return new n(t[0],t[1],t[2],t[3],t[4]);case 6:return new n(t[0],t[1],t[2],t[3],t[4],t[5]);case 7:return new n(t[0],t[1],t[2],t[3],t[4],t[5],t[6])}var r=gs(n.prototype),e=n.apply(r,t);return fc(e)?e:r}}function Hu(t,r,e){function u(){for(var o=arguments.length,f=il(o),c=o,a=wi(u);c--;)f[c]=arguments[c];var l=o<3&&f[0]!==a&&f[o-1]!==a?[]:N(f,a);
|
||||||
|
return o-=l.length,o<e?oi(t,r,Qu,u.placeholder,X,f,l,X,X,e-o):n(this&&this!==re&&this instanceof u?i:t,this,f)}var i=Gu(t);return u}function Ju(n){return function(t,r,e){var u=ll(t);if(!Hf(t)){var i=mi(r,3);t=Pc(t),r=function(n){return i(u[n],n,u)}}var o=n(t,r,e);return o>-1?u[i?t[o]:o]:X}}function Yu(n){return gi(function(t){var r=t.length,e=r,u=Y.prototype.thru;for(n&&t.reverse();e--;){var i=t[e];if("function"!=typeof i)throw new pl(en);if(u&&!o&&"wrapper"==bi(i))var o=new Y([],!0)}for(e=o?e:r;++e<r;){
|
||||||
|
i=t[e];var f=bi(i),c="wrapper"==f?Os(i):X;o=c&&$i(c[0])&&c[1]==(mn|yn|bn|xn)&&!c[4].length&&1==c[9]?o[bi(c[0])].apply(o,c[3]):1==i.length&&$i(i)?o[f]():o.thru(i)}return function(){var n=arguments,e=n[0];if(o&&1==n.length&&bh(e))return o.plant(e).value();for(var u=0,i=r?t[u].apply(this,n):e;++u<r;)i=t[u].call(this,i);return i}})}function Qu(n,t,r,e,u,i,o,f,c,a){function l(){for(var y=arguments.length,d=il(y),b=y;b--;)d[b]=arguments[b];if(_)var w=wi(l),m=C(d,w);if(e&&(d=Uu(d,e,u,_)),i&&(d=Bu(d,i,o,_)),
|
||||||
|
y-=m,_&&y<a){return oi(n,t,Qu,l.placeholder,r,d,N(d,w),f,c,a-y)}var x=h?r:this,j=p?x[n]:n;return y=d.length,f?d=Hi(d,f):v&&y>1&&d.reverse(),s&&c<y&&(d.length=c),this&&this!==re&&this instanceof l&&(j=g||Gu(j)),j.apply(x,d)}var s=t&mn,h=t&_n,p=t&vn,_=t&(yn|dn),v=t&jn,g=p?X:Gu(n);return l}function Xu(n,t){return function(r,e){return Oe(r,n,t(e),{})}}function ni(n,t){return function(r,e){var u;if(r===X&&e===X)return t;if(r!==X&&(u=r),e!==X){if(u===X)return e;"string"==typeof r||"string"==typeof e?(r=vu(r),
|
||||||
|
e=vu(e)):(r=_u(r),e=_u(e)),u=n(r,e)}return u}}function ti(t){return gi(function(r){return r=c(r,z(mi())),uu(function(e){var u=this;return t(r,function(t){return n(t,u,e)})})})}function ri(n,t){t=t===X?" ":vu(t);var r=t.length;if(r<2)return r?eu(t,n):t;var e=eu(t,Fl(n/V(t)));return T(t)?Ou(G(e),0,n).join(""):e.slice(0,n)}function ei(t,r,e,u){function i(){for(var r=-1,c=arguments.length,a=-1,l=u.length,s=il(l+c),h=this&&this!==re&&this instanceof i?f:t;++a<l;)s[a]=u[a];for(;c--;)s[a++]=arguments[++r];
|
||||||
|
return n(h,o?e:this,s)}var o=r&_n,f=Gu(t);return i}function ui(n){return function(t,r,e){return e&&"number"!=typeof e&&Ui(t,r,e)&&(r=e=X),t=Ac(t),r===X?(r=t,t=0):r=Ac(r),e=e===X?t<r?1:-1:Ac(e),ru(t,r,e,n)}}function ii(n){return function(t,r){return"string"==typeof t&&"string"==typeof r||(t=Ic(t),r=Ic(r)),n(t,r)}}function oi(n,t,r,e,u,i,o,f,c,a){var l=t&yn,s=l?o:X,h=l?X:o,p=l?i:X,_=l?X:i;t|=l?bn:wn,t&=~(l?wn:bn),t&gn||(t&=~(_n|vn));var v=[n,t,u,p,s,_,h,f,c,a],g=r.apply(X,v);return $i(n)&&Ss(g,v),g.placeholder=e,
|
||||||
|
Yi(g,n,t)}function fi(n){var t=al[n];return function(n,r){if(n=Ic(n),r=null==r?0:Hl(kc(r),292),r&&Zl(n)){var e=(Ec(n)+"e").split("e");return e=(Ec(t(e[0]+"e"+(+e[1]+r)))+"e").split("e"),+(e[0]+"e"+(+e[1]-r))}return t(n)}}function ci(n){return function(t){var r=zs(t);return r==Gn?M(t):r==tt?q(t):I(t,n(t))}}function ai(n,t,r,e,u,i,o,f){var c=t&vn;if(!c&&"function"!=typeof n)throw new pl(en);var a=e?e.length:0;if(a||(t&=~(bn|wn),e=u=X),o=o===X?o:Gl(kc(o),0),f=f===X?f:kc(f),a-=u?u.length:0,t&wn){var l=e,s=u;
|
||||||
|
e=u=X}var h=c?X:Os(n),p=[n,t,r,e,u,l,s,i,o,f];if(h&&qi(p,h),n=p[0],t=p[1],r=p[2],e=p[3],u=p[4],f=p[9]=p[9]===X?c?0:n.length:Gl(p[9]-a,0),!f&&t&(yn|dn)&&(t&=~(yn|dn)),t&&t!=_n)_=t==yn||t==dn?Hu(n,t,f):t!=bn&&t!=(_n|bn)||u.length?Qu.apply(X,p):ei(n,t,r,e);else var _=Zu(n,t,r);return Yi((h?ms:Ss)(_,p),n,t)}function li(n,t,r,e){return n===X||Gf(n,gl[r])&&!bl.call(e,r)?t:n}function si(n,t,r,e,u,i){return fc(n)&&fc(t)&&(i.set(t,n),Ke(n,t,X,si,i),i.delete(t)),n}function hi(n){return gc(n)?X:n}function pi(n,t,r,e,u,i){
|
||||||
|
var o=r&hn,f=n.length,c=t.length;if(f!=c&&!(o&&c>f))return!1;var a=i.get(n),l=i.get(t);if(a&&l)return a==t&&l==n;var s=-1,p=!0,_=r&pn?new yr:X;for(i.set(n,t),i.set(t,n);++s<f;){var v=n[s],g=t[s];if(e)var y=o?e(g,v,s,t,n,i):e(v,g,s,n,t,i);if(y!==X){if(y)continue;p=!1;break}if(_){if(!h(t,function(n,t){if(!S(_,t)&&(v===n||u(v,n,r,e,i)))return _.push(t)})){p=!1;break}}else if(v!==g&&!u(v,g,r,e,i)){p=!1;break}}return i.delete(n),i.delete(t),p}function _i(n,t,r,e,u,i,o){switch(r){case ct:if(n.byteLength!=t.byteLength||n.byteOffset!=t.byteOffset)return!1;
|
||||||
|
n=n.buffer,t=t.buffer;case ft:return!(n.byteLength!=t.byteLength||!i(new Rl(n),new Rl(t)));case Nn:case Pn:case Hn:return Gf(+n,+t);case Zn:return n.name==t.name&&n.message==t.message;case nt:case rt:return n==t+"";case Gn:var f=M;case tt:var c=e&hn;if(f||(f=P),n.size!=t.size&&!c)return!1;var a=o.get(n);if(a)return a==t;e|=pn,o.set(n,t);var l=pi(f(n),f(t),e,u,i,o);return o.delete(n),l;case et:if(_s)return _s.call(n)==_s.call(t)}return!1}function vi(n,t,r,e,u,i){var o=r&hn,f=yi(n),c=f.length;if(c!=yi(t).length&&!o)return!1;
|
||||||
|
for(var a=c;a--;){var l=f[a];if(!(o?l in t:bl.call(t,l)))return!1}var s=i.get(n),h=i.get(t);if(s&&h)return s==t&&h==n;var p=!0;i.set(n,t),i.set(t,n);for(var _=o;++a<c;){l=f[a];var v=n[l],g=t[l];if(e)var y=o?e(g,v,l,t,n,i):e(v,g,l,n,t,i);if(!(y===X?v===g||u(v,g,r,e,i):y)){p=!1;break}_||(_="constructor"==l)}if(p&&!_){var d=n.constructor,b=t.constructor;d!=b&&"constructor"in n&&"constructor"in t&&!("function"==typeof d&&d instanceof d&&"function"==typeof b&&b instanceof b)&&(p=!1)}return i.delete(n),
|
||||||
|
i.delete(t),p}function gi(n){return Ls(Vi(n,X,_o),n+"")}function yi(n){return de(n,Pc,Is)}function di(n){return de(n,qc,Rs)}function bi(n){for(var t=n.name+"",r=fs[t],e=bl.call(fs,t)?r.length:0;e--;){var u=r[e],i=u.func;if(null==i||i==n)return u.name}return t}function wi(n){return(bl.call(Z,"placeholder")?Z:n).placeholder}function mi(){var n=Z.iteratee||Ca;return n=n===Ca?De:n,arguments.length?n(arguments[0],arguments[1]):n}function xi(n,t){var r=n.__data__;return Ti(t)?r["string"==typeof t?"string":"hash"]:r.map;
|
||||||
|
}function ji(n){for(var t=Pc(n),r=t.length;r--;){var e=t[r],u=n[e];t[r]=[e,u,Fi(u)]}return t}function Ai(n,t){var r=B(n,t);return Ue(r)?r:X}function ki(n){var t=bl.call(n,Bl),r=n[Bl];try{n[Bl]=X;var e=!0}catch(n){}var u=xl.call(n);return e&&(t?n[Bl]=r:delete n[Bl]),u}function Oi(n,t,r){for(var e=-1,u=r.length;++e<u;){var i=r[e],o=i.size;switch(i.type){case"drop":n+=o;break;case"dropRight":t-=o;break;case"take":t=Hl(t,n+o);break;case"takeRight":n=Gl(n,t-o)}}return{start:n,end:t}}function Ii(n){var t=n.match(Bt);
|
||||||
|
return t?t[1].split(Tt):[]}function Ri(n,t,r){t=ku(t,n);for(var e=-1,u=t.length,i=!1;++e<u;){var o=no(t[e]);if(!(i=null!=n&&r(n,o)))break;n=n[o]}return i||++e!=u?i:(u=null==n?0:n.length,!!u&&oc(u)&&Ci(o,u)&&(bh(n)||dh(n)))}function zi(n){var t=n.length,r=new n.constructor(t);return t&&"string"==typeof n[0]&&bl.call(n,"index")&&(r.index=n.index,r.input=n.input),r}function Ei(n){return"function"!=typeof n.constructor||Mi(n)?{}:gs(El(n))}function Si(n,t,r){var e=n.constructor;switch(t){case ft:return Ru(n);
|
||||||
|
case Nn:case Pn:return new e(+n);case ct:return zu(n,r);case at:case lt:case st:case ht:case pt:case _t:case vt:case gt:case yt:return Wu(n,r);case Gn:return new e;case Hn:case rt:return new e(n);case nt:return Eu(n);case tt:return new e;case et:return Su(n)}}function Wi(n,t){var r=t.length;if(!r)return n;var e=r-1;return t[e]=(r>1?"& ":"")+t[e],t=t.join(r>2?", ":" "),n.replace(Ut,"{\n/* [wrapped with "+t+"] */\n")}function Li(n){return bh(n)||dh(n)||!!(Cl&&n&&n[Cl])}function Ci(n,t){var r=typeof n;
|
||||||
|
return t=null==t?Wn:t,!!t&&("number"==r||"symbol"!=r&&Vt.test(n))&&n>-1&&n%1==0&&n<t}function Ui(n,t,r){if(!fc(r))return!1;var e=typeof t;return!!("number"==e?Hf(r)&&Ci(t,r.length):"string"==e&&t in r)&&Gf(r[t],n)}function Bi(n,t){if(bh(n))return!1;var r=typeof n;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=n&&!bc(n))||(zt.test(n)||!Rt.test(n)||null!=t&&n in ll(t))}function Ti(n){var t=typeof n;return"string"==t||"number"==t||"symbol"==t||"boolean"==t?"__proto__"!==n:null===n}function $i(n){
|
||||||
|
var t=bi(n),r=Z[t];if("function"!=typeof r||!(t in Ct.prototype))return!1;if(n===r)return!0;var e=Os(r);return!!e&&n===e[0]}function Di(n){return!!ml&&ml in n}function Mi(n){var t=n&&n.constructor;return n===("function"==typeof t&&t.prototype||gl)}function Fi(n){return n===n&&!fc(n)}function Ni(n,t){return function(r){return null!=r&&(r[n]===t&&(t!==X||n in ll(r)))}}function Pi(n){var t=Cf(n,function(n){return r.size===fn&&r.clear(),n}),r=t.cache;return t}function qi(n,t){var r=n[1],e=t[1],u=r|e,i=u<(_n|vn|mn),o=e==mn&&r==yn||e==mn&&r==xn&&n[7].length<=t[8]||e==(mn|xn)&&t[7].length<=t[8]&&r==yn;
|
||||||
|
if(!i&&!o)return n;e&_n&&(n[2]=t[2],u|=r&_n?0:gn);var f=t[3];if(f){var c=n[3];n[3]=c?Uu(c,f,t[4]):f,n[4]=c?N(n[3],cn):t[4]}return f=t[5],f&&(c=n[5],n[5]=c?Bu(c,f,t[6]):f,n[6]=c?N(n[5],cn):t[6]),f=t[7],f&&(n[7]=f),e&mn&&(n[8]=null==n[8]?t[8]:Hl(n[8],t[8])),null==n[9]&&(n[9]=t[9]),n[0]=t[0],n[1]=u,n}function Zi(n){var t=[];if(null!=n)for(var r in ll(n))t.push(r);return t}function Ki(n){return xl.call(n)}function Vi(t,r,e){return r=Gl(r===X?t.length-1:r,0),function(){for(var u=arguments,i=-1,o=Gl(u.length-r,0),f=il(o);++i<o;)f[i]=u[r+i];
|
||||||
|
i=-1;for(var c=il(r+1);++i<r;)c[i]=u[i];return c[r]=e(f),n(t,this,c)}}function Gi(n,t){return t.length<2?n:_e(n,au(t,0,-1))}function Hi(n,t){for(var r=n.length,e=Hl(t.length,r),u=Tu(n);e--;){var i=t[e];n[e]=Ci(i,r)?u[i]:X}return n}function Ji(n,t){if(("constructor"!==t||"function"!=typeof n[t])&&"__proto__"!=t)return n[t]}function Yi(n,t,r){var e=t+"";return Ls(n,Wi(e,ro(Ii(e),r)))}function Qi(n){var t=0,r=0;return function(){var e=Jl(),u=In-(e-r);if(r=e,u>0){if(++t>=On)return arguments[0]}else t=0;
|
||||||
|
return n.apply(X,arguments)}}function Xi(n,t){var r=-1,e=n.length,u=e-1;for(t=t===X?e:t;++r<t;){var i=tu(r,u),o=n[i];n[i]=n[r],n[r]=o}return n.length=t,n}function no(n){if("string"==typeof n||bc(n))return n;var t=n+"";return"0"==t&&1/n==-Sn?"-0":t}function to(n){if(null!=n){try{return dl.call(n)}catch(n){}try{return n+""}catch(n){}}return""}function ro(n,t){return r($n,function(r){var e="_."+r[0];t&r[1]&&!o(n,e)&&n.push(e)}),n.sort()}function eo(n){if(n instanceof Ct)return n.clone();var t=new Y(n.__wrapped__,n.__chain__);
|
||||||
|
return t.__actions__=Tu(n.__actions__),t.__index__=n.__index__,t.__values__=n.__values__,t}function uo(n,t,r){t=(r?Ui(n,t,r):t===X)?1:Gl(kc(t),0);var e=null==n?0:n.length;if(!e||t<1)return[];for(var u=0,i=0,o=il(Fl(e/t));u<e;)o[i++]=au(n,u,u+=t);return o}function io(n){for(var t=-1,r=null==n?0:n.length,e=0,u=[];++t<r;){var i=n[t];i&&(u[e++]=i)}return u}function oo(){var n=arguments.length;if(!n)return[];for(var t=il(n-1),r=arguments[0],e=n;e--;)t[e-1]=arguments[e];return a(bh(r)?Tu(r):[r],ee(t,1));
|
||||||
|
}function fo(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),au(n,t<0?0:t,e)):[]}function co(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),t=e-t,au(n,0,t<0?0:t)):[]}function ao(n,t){return n&&n.length?bu(n,mi(t,3),!0,!0):[]}function lo(n,t){return n&&n.length?bu(n,mi(t,3),!0):[]}function so(n,t,r,e){var u=null==n?0:n.length;return u?(r&&"number"!=typeof r&&Ui(n,t,r)&&(r=0,e=u),ne(n,t,r,e)):[]}function ho(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=null==r?0:kc(r);
|
||||||
|
return u<0&&(u=Gl(e+u,0)),g(n,mi(t,3),u)}function po(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e-1;return r!==X&&(u=kc(r),u=r<0?Gl(e+u,0):Hl(u,e-1)),g(n,mi(t,3),u,!0)}function _o(n){return(null==n?0:n.length)?ee(n,1):[]}function vo(n){return(null==n?0:n.length)?ee(n,Sn):[]}function go(n,t){return(null==n?0:n.length)?(t=t===X?1:kc(t),ee(n,t)):[]}function yo(n){for(var t=-1,r=null==n?0:n.length,e={};++t<r;){var u=n[t];e[u[0]]=u[1]}return e}function bo(n){return n&&n.length?n[0]:X}function wo(n,t,r){
|
||||||
|
var e=null==n?0:n.length;if(!e)return-1;var u=null==r?0:kc(r);return u<0&&(u=Gl(e+u,0)),y(n,t,u)}function mo(n){return(null==n?0:n.length)?au(n,0,-1):[]}function xo(n,t){return null==n?"":Kl.call(n,t)}function jo(n){var t=null==n?0:n.length;return t?n[t-1]:X}function Ao(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e;return r!==X&&(u=kc(r),u=u<0?Gl(e+u,0):Hl(u,e-1)),t===t?K(n,t,u):g(n,b,u,!0)}function ko(n,t){return n&&n.length?Ge(n,kc(t)):X}function Oo(n,t){return n&&n.length&&t&&t.length?Xe(n,t):n;
|
||||||
|
}function Io(n,t,r){return n&&n.length&&t&&t.length?Xe(n,t,mi(r,2)):n}function Ro(n,t,r){return n&&n.length&&t&&t.length?Xe(n,t,X,r):n}function zo(n,t){var r=[];if(!n||!n.length)return r;var e=-1,u=[],i=n.length;for(t=mi(t,3);++e<i;){var o=n[e];t(o,e,n)&&(r.push(o),u.push(e))}return nu(n,u),r}function Eo(n){return null==n?n:Xl.call(n)}function So(n,t,r){var e=null==n?0:n.length;return e?(r&&"number"!=typeof r&&Ui(n,t,r)?(t=0,r=e):(t=null==t?0:kc(t),r=r===X?e:kc(r)),au(n,t,r)):[]}function Wo(n,t){
|
||||||
|
return su(n,t)}function Lo(n,t,r){return hu(n,t,mi(r,2))}function Co(n,t){var r=null==n?0:n.length;if(r){var e=su(n,t);if(e<r&&Gf(n[e],t))return e}return-1}function Uo(n,t){return su(n,t,!0)}function Bo(n,t,r){return hu(n,t,mi(r,2),!0)}function To(n,t){if(null==n?0:n.length){var r=su(n,t,!0)-1;if(Gf(n[r],t))return r}return-1}function $o(n){return n&&n.length?pu(n):[]}function Do(n,t){return n&&n.length?pu(n,mi(t,2)):[]}function Mo(n){var t=null==n?0:n.length;return t?au(n,1,t):[]}function Fo(n,t,r){
|
||||||
|
return n&&n.length?(t=r||t===X?1:kc(t),au(n,0,t<0?0:t)):[]}function No(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===X?1:kc(t),t=e-t,au(n,t<0?0:t,e)):[]}function Po(n,t){return n&&n.length?bu(n,mi(t,3),!1,!0):[]}function qo(n,t){return n&&n.length?bu(n,mi(t,3)):[]}function Zo(n){return n&&n.length?gu(n):[]}function Ko(n,t){return n&&n.length?gu(n,mi(t,2)):[]}function Vo(n,t){return t="function"==typeof t?t:X,n&&n.length?gu(n,X,t):[]}function Go(n){if(!n||!n.length)return[];var t=0;return n=i(n,function(n){
|
||||||
|
if(Jf(n))return t=Gl(n.length,t),!0}),O(t,function(t){return c(n,m(t))})}function Ho(t,r){if(!t||!t.length)return[];var e=Go(t);return null==r?e:c(e,function(t){return n(r,X,t)})}function Jo(n,t){return xu(n||[],t||[],Sr)}function Yo(n,t){return xu(n||[],t||[],fu)}function Qo(n){var t=Z(n);return t.__chain__=!0,t}function Xo(n,t){return t(n),n}function nf(n,t){return t(n)}function tf(){return Qo(this)}function rf(){return new Y(this.value(),this.__chain__)}function ef(){this.__values__===X&&(this.__values__=jc(this.value()));
|
||||||
|
var n=this.__index__>=this.__values__.length;return{done:n,value:n?X:this.__values__[this.__index__++]}}function uf(){return this}function of(n){for(var t,r=this;r instanceof J;){var e=eo(r);e.__index__=0,e.__values__=X,t?u.__wrapped__=e:t=e;var u=e;r=r.__wrapped__}return u.__wrapped__=n,t}function ff(){var n=this.__wrapped__;if(n instanceof Ct){var t=n;return this.__actions__.length&&(t=new Ct(this)),t=t.reverse(),t.__actions__.push({func:nf,args:[Eo],thisArg:X}),new Y(t,this.__chain__)}return this.thru(Eo);
|
||||||
|
}function cf(){return wu(this.__wrapped__,this.__actions__)}function af(n,t,r){var e=bh(n)?u:Jr;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function lf(n,t){return(bh(n)?i:te)(n,mi(t,3))}function sf(n,t){return ee(yf(n,t),1)}function hf(n,t){return ee(yf(n,t),Sn)}function pf(n,t,r){return r=r===X?1:kc(r),ee(yf(n,t),r)}function _f(n,t){return(bh(n)?r:ys)(n,mi(t,3))}function vf(n,t){return(bh(n)?e:ds)(n,mi(t,3))}function gf(n,t,r,e){n=Hf(n)?n:ra(n),r=r&&!e?kc(r):0;var u=n.length;return r<0&&(r=Gl(u+r,0)),
|
||||||
|
dc(n)?r<=u&&n.indexOf(t,r)>-1:!!u&&y(n,t,r)>-1}function yf(n,t){return(bh(n)?c:Pe)(n,mi(t,3))}function df(n,t,r,e){return null==n?[]:(bh(t)||(t=null==t?[]:[t]),r=e?X:r,bh(r)||(r=null==r?[]:[r]),He(n,t,r))}function bf(n,t,r){var e=bh(n)?l:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ys)}function wf(n,t,r){var e=bh(n)?s:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ds)}function mf(n,t){return(bh(n)?i:te)(n,Uf(mi(t,3)))}function xf(n){return(bh(n)?Ir:iu)(n)}function jf(n,t,r){return t=(r?Ui(n,t,r):t===X)?1:kc(t),
|
||||||
|
(bh(n)?Rr:ou)(n,t)}function Af(n){return(bh(n)?zr:cu)(n)}function kf(n){if(null==n)return 0;if(Hf(n))return dc(n)?V(n):n.length;var t=zs(n);return t==Gn||t==tt?n.size:Me(n).length}function Of(n,t,r){var e=bh(n)?h:lu;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function If(n,t){if("function"!=typeof t)throw new pl(en);return n=kc(n),function(){if(--n<1)return t.apply(this,arguments)}}function Rf(n,t,r){return t=r?X:t,t=n&&null==t?n.length:t,ai(n,mn,X,X,X,X,t)}function zf(n,t){var r;if("function"!=typeof t)throw new pl(en);
|
||||||
|
return n=kc(n),function(){return--n>0&&(r=t.apply(this,arguments)),n<=1&&(t=X),r}}function Ef(n,t,r){t=r?X:t;var e=ai(n,yn,X,X,X,X,X,t);return e.placeholder=Ef.placeholder,e}function Sf(n,t,r){t=r?X:t;var e=ai(n,dn,X,X,X,X,X,t);return e.placeholder=Sf.placeholder,e}function Wf(n,t,r){function e(t){var r=h,e=p;return h=p=X,d=t,v=n.apply(e,r)}function u(n){return d=n,g=Ws(f,t),b?e(n):v}function i(n){var r=n-y,e=n-d,u=t-r;return w?Hl(u,_-e):u}function o(n){var r=n-y,e=n-d;return y===X||r>=t||r<0||w&&e>=_;
|
||||||
|
}function f(){var n=fh();return o(n)?c(n):(g=Ws(f,i(n)),X)}function c(n){return g=X,m&&h?e(n):(h=p=X,v)}function a(){g!==X&&As(g),d=0,h=y=p=g=X}function l(){return g===X?v:c(fh())}function s(){var n=fh(),r=o(n);if(h=arguments,p=this,y=n,r){if(g===X)return u(y);if(w)return As(g),g=Ws(f,t),e(y)}return g===X&&(g=Ws(f,t)),v}var h,p,_,v,g,y,d=0,b=!1,w=!1,m=!0;if("function"!=typeof n)throw new pl(en);return t=Ic(t)||0,fc(r)&&(b=!!r.leading,w="maxWait"in r,_=w?Gl(Ic(r.maxWait)||0,t):_,m="trailing"in r?!!r.trailing:m),
|
||||||
|
s.cancel=a,s.flush=l,s}function Lf(n){return ai(n,jn)}function Cf(n,t){if("function"!=typeof n||null!=t&&"function"!=typeof t)throw new pl(en);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;if(i.has(u))return i.get(u);var o=n.apply(this,e);return r.cache=i.set(u,o)||i,o};return r.cache=new(Cf.Cache||sr),r}function Uf(n){if("function"!=typeof n)throw new pl(en);return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:
|
||||||
|
return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function Bf(n){return zf(2,n)}function Tf(n,t){if("function"!=typeof n)throw new pl(en);return t=t===X?t:kc(t),uu(n,t)}function $f(t,r){if("function"!=typeof t)throw new pl(en);return r=null==r?0:Gl(kc(r),0),uu(function(e){var u=e[r],i=Ou(e,0,r);return u&&a(i,u),n(t,this,i)})}function Df(n,t,r){var e=!0,u=!0;if("function"!=typeof n)throw new pl(en);return fc(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),
|
||||||
|
Wf(n,t,{leading:e,maxWait:t,trailing:u})}function Mf(n){return Rf(n,1)}function Ff(n,t){return ph(Au(t),n)}function Nf(){if(!arguments.length)return[];var n=arguments[0];return bh(n)?n:[n]}function Pf(n){return Fr(n,sn)}function qf(n,t){return t="function"==typeof t?t:X,Fr(n,sn,t)}function Zf(n){return Fr(n,an|sn)}function Kf(n,t){return t="function"==typeof t?t:X,Fr(n,an|sn,t)}function Vf(n,t){return null==t||Pr(n,t,Pc(t))}function Gf(n,t){return n===t||n!==n&&t!==t}function Hf(n){return null!=n&&oc(n.length)&&!uc(n);
|
||||||
|
}function Jf(n){return cc(n)&&Hf(n)}function Yf(n){return n===!0||n===!1||cc(n)&&we(n)==Nn}function Qf(n){return cc(n)&&1===n.nodeType&&!gc(n)}function Xf(n){if(null==n)return!0;if(Hf(n)&&(bh(n)||"string"==typeof n||"function"==typeof n.splice||mh(n)||Oh(n)||dh(n)))return!n.length;var t=zs(n);if(t==Gn||t==tt)return!n.size;if(Mi(n))return!Me(n).length;for(var r in n)if(bl.call(n,r))return!1;return!0}function nc(n,t){return Se(n,t)}function tc(n,t,r){r="function"==typeof r?r:X;var e=r?r(n,t):X;return e===X?Se(n,t,X,r):!!e;
|
||||||
|
}function rc(n){if(!cc(n))return!1;var t=we(n);return t==Zn||t==qn||"string"==typeof n.message&&"string"==typeof n.name&&!gc(n)}function ec(n){return"number"==typeof n&&Zl(n)}function uc(n){if(!fc(n))return!1;var t=we(n);return t==Kn||t==Vn||t==Fn||t==Xn}function ic(n){return"number"==typeof n&&n==kc(n)}function oc(n){return"number"==typeof n&&n>-1&&n%1==0&&n<=Wn}function fc(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function cc(n){return null!=n&&"object"==typeof n}function ac(n,t){
|
||||||
|
return n===t||Ce(n,t,ji(t))}function lc(n,t,r){return r="function"==typeof r?r:X,Ce(n,t,ji(t),r)}function sc(n){return vc(n)&&n!=+n}function hc(n){if(Es(n))throw new fl(rn);return Ue(n)}function pc(n){return null===n}function _c(n){return null==n}function vc(n){return"number"==typeof n||cc(n)&&we(n)==Hn}function gc(n){if(!cc(n)||we(n)!=Yn)return!1;var t=El(n);if(null===t)return!0;var r=bl.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&dl.call(r)==jl}function yc(n){
|
||||||
|
return ic(n)&&n>=-Wn&&n<=Wn}function dc(n){return"string"==typeof n||!bh(n)&&cc(n)&&we(n)==rt}function bc(n){return"symbol"==typeof n||cc(n)&&we(n)==et}function wc(n){return n===X}function mc(n){return cc(n)&&zs(n)==it}function xc(n){return cc(n)&&we(n)==ot}function jc(n){if(!n)return[];if(Hf(n))return dc(n)?G(n):Tu(n);if(Ul&&n[Ul])return D(n[Ul]());var t=zs(n);return(t==Gn?M:t==tt?P:ra)(n)}function Ac(n){if(!n)return 0===n?n:0;if(n=Ic(n),n===Sn||n===-Sn){return(n<0?-1:1)*Ln}return n===n?n:0}function kc(n){
|
||||||
|
var t=Ac(n),r=t%1;return t===t?r?t-r:t:0}function Oc(n){return n?Mr(kc(n),0,Un):0}function Ic(n){if("number"==typeof n)return n;if(bc(n))return Cn;if(fc(n)){var t="function"==typeof n.valueOf?n.valueOf():n;n=fc(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=R(n);var r=qt.test(n);return r||Kt.test(n)?Xr(n.slice(2),r?2:8):Pt.test(n)?Cn:+n}function Rc(n){return $u(n,qc(n))}function zc(n){return n?Mr(kc(n),-Wn,Wn):0===n?n:0}function Ec(n){return null==n?"":vu(n)}function Sc(n,t){var r=gs(n);return null==t?r:Cr(r,t);
|
||||||
|
}function Wc(n,t){return v(n,mi(t,3),ue)}function Lc(n,t){return v(n,mi(t,3),oe)}function Cc(n,t){return null==n?n:bs(n,mi(t,3),qc)}function Uc(n,t){return null==n?n:ws(n,mi(t,3),qc)}function Bc(n,t){return n&&ue(n,mi(t,3))}function Tc(n,t){return n&&oe(n,mi(t,3))}function $c(n){return null==n?[]:fe(n,Pc(n))}function Dc(n){return null==n?[]:fe(n,qc(n))}function Mc(n,t,r){var e=null==n?X:_e(n,t);return e===X?r:e}function Fc(n,t){return null!=n&&Ri(n,t,xe)}function Nc(n,t){return null!=n&&Ri(n,t,je);
|
||||||
|
}function Pc(n){return Hf(n)?Or(n):Me(n)}function qc(n){return Hf(n)?Or(n,!0):Fe(n)}function Zc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,t(n,e,u),n)}),r}function Kc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,e,t(n,e,u))}),r}function Vc(n,t){return Gc(n,Uf(mi(t)))}function Gc(n,t){if(null==n)return{};var r=c(di(n),function(n){return[n]});return t=mi(t),Ye(n,r,function(n,r){return t(n,r[0])})}function Hc(n,t,r){t=ku(t,n);var e=-1,u=t.length;for(u||(u=1,n=X);++e<u;){var i=null==n?X:n[no(t[e])];
|
||||||
|
i===X&&(e=u,i=r),n=uc(i)?i.call(n):i}return n}function Jc(n,t,r){return null==n?n:fu(n,t,r)}function Yc(n,t,r,e){return e="function"==typeof e?e:X,null==n?n:fu(n,t,r,e)}function Qc(n,t,e){var u=bh(n),i=u||mh(n)||Oh(n);if(t=mi(t,4),null==e){var o=n&&n.constructor;e=i?u?new o:[]:fc(n)&&uc(o)?gs(El(n)):{}}return(i?r:ue)(n,function(n,r,u){return t(e,n,r,u)}),e}function Xc(n,t){return null==n||yu(n,t)}function na(n,t,r){return null==n?n:du(n,t,Au(r))}function ta(n,t,r,e){return e="function"==typeof e?e:X,
|
||||||
|
null==n?n:du(n,t,Au(r),e)}function ra(n){return null==n?[]:E(n,Pc(n))}function ea(n){return null==n?[]:E(n,qc(n))}function ua(n,t,r){return r===X&&(r=t,t=X),r!==X&&(r=Ic(r),r=r===r?r:0),t!==X&&(t=Ic(t),t=t===t?t:0),Mr(Ic(n),t,r)}function ia(n,t,r){return t=Ac(t),r===X?(r=t,t=0):r=Ac(r),n=Ic(n),Ae(n,t,r)}function oa(n,t,r){if(r&&"boolean"!=typeof r&&Ui(n,t,r)&&(t=r=X),r===X&&("boolean"==typeof t?(r=t,t=X):"boolean"==typeof n&&(r=n,n=X)),n===X&&t===X?(n=0,t=1):(n=Ac(n),t===X?(t=n,n=0):t=Ac(t)),n>t){
|
||||||
|
var e=n;n=t,t=e}if(r||n%1||t%1){var u=Ql();return Hl(n+u*(t-n+Qr("1e-"+((u+"").length-1))),t)}return tu(n,t)}function fa(n){return Qh(Ec(n).toLowerCase())}function ca(n){return n=Ec(n),n&&n.replace(Gt,ve).replace(Dr,"")}function aa(n,t,r){n=Ec(n),t=vu(t);var e=n.length;r=r===X?e:Mr(kc(r),0,e);var u=r;return r-=t.length,r>=0&&n.slice(r,u)==t}function la(n){return n=Ec(n),n&&At.test(n)?n.replace(xt,ge):n}function sa(n){return n=Ec(n),n&&Wt.test(n)?n.replace(St,"\\$&"):n}function ha(n,t,r){n=Ec(n),t=kc(t);
|
||||||
|
var e=t?V(n):0;if(!t||e>=t)return n;var u=(t-e)/2;return ri(Nl(u),r)+n+ri(Fl(u),r)}function pa(n,t,r){n=Ec(n),t=kc(t);var e=t?V(n):0;return t&&e<t?n+ri(t-e,r):n}function _a(n,t,r){n=Ec(n),t=kc(t);var e=t?V(n):0;return t&&e<t?ri(t-e,r)+n:n}function va(n,t,r){return r||null==t?t=0:t&&(t=+t),Yl(Ec(n).replace(Lt,""),t||0)}function ga(n,t,r){return t=(r?Ui(n,t,r):t===X)?1:kc(t),eu(Ec(n),t)}function ya(){var n=arguments,t=Ec(n[0]);return n.length<3?t:t.replace(n[1],n[2])}function da(n,t,r){return r&&"number"!=typeof r&&Ui(n,t,r)&&(t=r=X),
|
||||||
|
(r=r===X?Un:r>>>0)?(n=Ec(n),n&&("string"==typeof t||null!=t&&!Ah(t))&&(t=vu(t),!t&&T(n))?Ou(G(n),0,r):n.split(t,r)):[]}function ba(n,t,r){return n=Ec(n),r=null==r?0:Mr(kc(r),0,n.length),t=vu(t),n.slice(r,r+t.length)==t}function wa(n,t,r){var e=Z.templateSettings;r&&Ui(n,t,r)&&(t=X),n=Ec(n),t=Sh({},t,e,li);var u,i,o=Sh({},t.imports,e.imports,li),f=Pc(o),c=E(o,f),a=0,l=t.interpolate||Ht,s="__p += '",h=sl((t.escape||Ht).source+"|"+l.source+"|"+(l===It?Ft:Ht).source+"|"+(t.evaluate||Ht).source+"|$","g"),p="//# sourceURL="+(bl.call(t,"sourceURL")?(t.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Zr+"]")+"\n";
|
||||||
|
n.replace(h,function(t,r,e,o,f,c){return e||(e=o),s+=n.slice(a,c).replace(Jt,U),r&&(u=!0,s+="' +\n__e("+r+") +\n'"),f&&(i=!0,s+="';\n"+f+";\n__p += '"),e&&(s+="' +\n((__t = ("+e+")) == null ? '' : __t) +\n'"),a=c+t.length,t}),s+="';\n";var _=bl.call(t,"variable")&&t.variable;if(_){if(Dt.test(_))throw new fl(un)}else s="with (obj) {\n"+s+"\n}\n";s=(i?s.replace(dt,""):s).replace(bt,"$1").replace(wt,"$1;"),s="function("+(_||"obj")+") {\n"+(_?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(u?", __e = _.escape":"")+(i?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+s+"return __p\n}";
|
||||||
|
var v=Xh(function(){return cl(f,p+"return "+s).apply(X,c)});if(v.source=s,rc(v))throw v;return v}function ma(n){return Ec(n).toLowerCase()}function xa(n){return Ec(n).toUpperCase()}function ja(n,t,r){if(n=Ec(n),n&&(r||t===X))return R(n);if(!n||!(t=vu(t)))return n;var e=G(n),u=G(t);return Ou(e,W(e,u),L(e,u)+1).join("")}function Aa(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.slice(0,H(n)+1);if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,0,L(e,G(t))+1).join("")}function ka(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.replace(Lt,"");
|
||||||
|
if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,W(e,G(t))).join("")}function Oa(n,t){var r=An,e=kn;if(fc(t)){var u="separator"in t?t.separator:u;r="length"in t?kc(t.length):r,e="omission"in t?vu(t.omission):e}n=Ec(n);var i=n.length;if(T(n)){var o=G(n);i=o.length}if(r>=i)return n;var f=r-V(e);if(f<1)return e;var c=o?Ou(o,0,f).join(""):n.slice(0,f);if(u===X)return c+e;if(o&&(f+=c.length-f),Ah(u)){if(n.slice(f).search(u)){var a,l=c;for(u.global||(u=sl(u.source,Ec(Nt.exec(u))+"g")),u.lastIndex=0;a=u.exec(l);)var s=a.index;
|
||||||
|
c=c.slice(0,s===X?f:s)}}else if(n.indexOf(vu(u),f)!=f){var h=c.lastIndexOf(u);h>-1&&(c=c.slice(0,h))}return c+e}function Ia(n){return n=Ec(n),n&&jt.test(n)?n.replace(mt,ye):n}function Ra(n,t,r){return n=Ec(n),t=r?X:t,t===X?$(n)?Q(n):_(n):n.match(t)||[]}function za(t){var r=null==t?0:t.length,e=mi();return t=r?c(t,function(n){if("function"!=typeof n[1])throw new pl(en);return[e(n[0]),n[1]]}):[],uu(function(e){for(var u=-1;++u<r;){var i=t[u];if(n(i[0],this,e))return n(i[1],this,e)}})}function Ea(n){
|
||||||
|
return Nr(Fr(n,an))}function Sa(n){return function(){return n}}function Wa(n,t){return null==n||n!==n?t:n}function La(n){return n}function Ca(n){return De("function"==typeof n?n:Fr(n,an))}function Ua(n){return qe(Fr(n,an))}function Ba(n,t){return Ze(n,Fr(t,an))}function Ta(n,t,e){var u=Pc(t),i=fe(t,u);null!=e||fc(t)&&(i.length||!u.length)||(e=t,t=n,n=this,i=fe(t,Pc(t)));var o=!(fc(e)&&"chain"in e&&!e.chain),f=uc(n);return r(i,function(r){var e=t[r];n[r]=e,f&&(n.prototype[r]=function(){var t=this.__chain__;
|
||||||
|
if(o||t){var r=n(this.__wrapped__);return(r.__actions__=Tu(this.__actions__)).push({func:e,args:arguments,thisArg:n}),r.__chain__=t,r}return e.apply(n,a([this.value()],arguments))})}),n}function $a(){return re._===this&&(re._=Al),this}function Da(){}function Ma(n){return n=kc(n),uu(function(t){return Ge(t,n)})}function Fa(n){return Bi(n)?m(no(n)):Qe(n)}function Na(n){return function(t){return null==n?X:_e(n,t)}}function Pa(){return[]}function qa(){return!1}function Za(){return{}}function Ka(){return"";
|
||||||
|
}function Va(){return!0}function Ga(n,t){if(n=kc(n),n<1||n>Wn)return[];var r=Un,e=Hl(n,Un);t=mi(t),n-=Un;for(var u=O(e,t);++r<n;)t(r);return u}function Ha(n){return bh(n)?c(n,no):bc(n)?[n]:Tu(Cs(Ec(n)))}function Ja(n){var t=++wl;return Ec(n)+t}function Ya(n){return n&&n.length?Yr(n,La,me):X}function Qa(n,t){return n&&n.length?Yr(n,mi(t,2),me):X}function Xa(n){return w(n,La)}function nl(n,t){return w(n,mi(t,2))}function tl(n){return n&&n.length?Yr(n,La,Ne):X}function rl(n,t){return n&&n.length?Yr(n,mi(t,2),Ne):X;
|
||||||
|
}function el(n){return n&&n.length?k(n,La):0}function ul(n,t){return n&&n.length?k(n,mi(t,2)):0}x=null==x?re:be.defaults(re.Object(),x,be.pick(re,qr));var il=x.Array,ol=x.Date,fl=x.Error,cl=x.Function,al=x.Math,ll=x.Object,sl=x.RegExp,hl=x.String,pl=x.TypeError,_l=il.prototype,vl=cl.prototype,gl=ll.prototype,yl=x["__core-js_shared__"],dl=vl.toString,bl=gl.hasOwnProperty,wl=0,ml=function(){var n=/[^.]+$/.exec(yl&&yl.keys&&yl.keys.IE_PROTO||"");return n?"Symbol(src)_1."+n:""}(),xl=gl.toString,jl=dl.call(ll),Al=re._,kl=sl("^"+dl.call(bl).replace(St,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Ol=ie?x.Buffer:X,Il=x.Symbol,Rl=x.Uint8Array,zl=Ol?Ol.allocUnsafe:X,El=F(ll.getPrototypeOf,ll),Sl=ll.create,Wl=gl.propertyIsEnumerable,Ll=_l.splice,Cl=Il?Il.isConcatSpreadable:X,Ul=Il?Il.iterator:X,Bl=Il?Il.toStringTag:X,Tl=function(){
|
||||||
|
try{var n=Ai(ll,"defineProperty");return n({},"",{}),n}catch(n){}}(),$l=x.clearTimeout!==re.clearTimeout&&x.clearTimeout,Dl=ol&&ol.now!==re.Date.now&&ol.now,Ml=x.setTimeout!==re.setTimeout&&x.setTimeout,Fl=al.ceil,Nl=al.floor,Pl=ll.getOwnPropertySymbols,ql=Ol?Ol.isBuffer:X,Zl=x.isFinite,Kl=_l.join,Vl=F(ll.keys,ll),Gl=al.max,Hl=al.min,Jl=ol.now,Yl=x.parseInt,Ql=al.random,Xl=_l.reverse,ns=Ai(x,"DataView"),ts=Ai(x,"Map"),rs=Ai(x,"Promise"),es=Ai(x,"Set"),us=Ai(x,"WeakMap"),is=Ai(ll,"create"),os=us&&new us,fs={},cs=to(ns),as=to(ts),ls=to(rs),ss=to(es),hs=to(us),ps=Il?Il.prototype:X,_s=ps?ps.valueOf:X,vs=ps?ps.toString:X,gs=function(){
|
||||||
|
function n(){}return function(t){if(!fc(t))return{};if(Sl)return Sl(t);n.prototype=t;var r=new n;return n.prototype=X,r}}();Z.templateSettings={escape:kt,evaluate:Ot,interpolate:It,variable:"",imports:{_:Z}},Z.prototype=J.prototype,Z.prototype.constructor=Z,Y.prototype=gs(J.prototype),Y.prototype.constructor=Y,Ct.prototype=gs(J.prototype),Ct.prototype.constructor=Ct,Xt.prototype.clear=nr,Xt.prototype.delete=tr,Xt.prototype.get=rr,Xt.prototype.has=er,Xt.prototype.set=ur,ir.prototype.clear=or,ir.prototype.delete=fr,
|
||||||
|
ir.prototype.get=cr,ir.prototype.has=ar,ir.prototype.set=lr,sr.prototype.clear=hr,sr.prototype.delete=pr,sr.prototype.get=_r,sr.prototype.has=vr,sr.prototype.set=gr,yr.prototype.add=yr.prototype.push=dr,yr.prototype.has=br,wr.prototype.clear=mr,wr.prototype.delete=xr,wr.prototype.get=jr,wr.prototype.has=Ar,wr.prototype.set=kr;var ys=Pu(ue),ds=Pu(oe,!0),bs=qu(),ws=qu(!0),ms=os?function(n,t){return os.set(n,t),n}:La,xs=Tl?function(n,t){return Tl(n,"toString",{configurable:!0,enumerable:!1,value:Sa(t),
|
||||||
|
writable:!0})}:La,js=uu,As=$l||function(n){return re.clearTimeout(n)},ks=es&&1/P(new es([,-0]))[1]==Sn?function(n){return new es(n)}:Da,Os=os?function(n){return os.get(n)}:Da,Is=Pl?function(n){return null==n?[]:(n=ll(n),i(Pl(n),function(t){return Wl.call(n,t)}))}:Pa,Rs=Pl?function(n){for(var t=[];n;)a(t,Is(n)),n=El(n);return t}:Pa,zs=we;(ns&&zs(new ns(new ArrayBuffer(1)))!=ct||ts&&zs(new ts)!=Gn||rs&&zs(rs.resolve())!=Qn||es&&zs(new es)!=tt||us&&zs(new us)!=it)&&(zs=function(n){var t=we(n),r=t==Yn?n.constructor:X,e=r?to(r):"";
|
||||||
|
if(e)switch(e){case cs:return ct;case as:return Gn;case ls:return Qn;case ss:return tt;case hs:return it}return t});var Es=yl?uc:qa,Ss=Qi(ms),Ws=Ml||function(n,t){return re.setTimeout(n,t)},Ls=Qi(xs),Cs=Pi(function(n){var t=[];return 46===n.charCodeAt(0)&&t.push(""),n.replace(Et,function(n,r,e,u){t.push(e?u.replace(Mt,"$1"):r||n)}),t}),Us=uu(function(n,t){return Jf(n)?Hr(n,ee(t,1,Jf,!0)):[]}),Bs=uu(function(n,t){var r=jo(t);return Jf(r)&&(r=X),Jf(n)?Hr(n,ee(t,1,Jf,!0),mi(r,2)):[]}),Ts=uu(function(n,t){
|
||||||
|
var r=jo(t);return Jf(r)&&(r=X),Jf(n)?Hr(n,ee(t,1,Jf,!0),X,r):[]}),$s=uu(function(n){var t=c(n,ju);return t.length&&t[0]===n[0]?ke(t):[]}),Ds=uu(function(n){var t=jo(n),r=c(n,ju);return t===jo(r)?t=X:r.pop(),r.length&&r[0]===n[0]?ke(r,mi(t,2)):[]}),Ms=uu(function(n){var t=jo(n),r=c(n,ju);return t="function"==typeof t?t:X,t&&r.pop(),r.length&&r[0]===n[0]?ke(r,X,t):[]}),Fs=uu(Oo),Ns=gi(function(n,t){var r=null==n?0:n.length,e=Tr(n,t);return nu(n,c(t,function(n){return Ci(n,r)?+n:n}).sort(Lu)),e}),Ps=uu(function(n){
|
||||||
|
return gu(ee(n,1,Jf,!0))}),qs=uu(function(n){var t=jo(n);return Jf(t)&&(t=X),gu(ee(n,1,Jf,!0),mi(t,2))}),Zs=uu(function(n){var t=jo(n);return t="function"==typeof t?t:X,gu(ee(n,1,Jf,!0),X,t)}),Ks=uu(function(n,t){return Jf(n)?Hr(n,t):[]}),Vs=uu(function(n){return mu(i(n,Jf))}),Gs=uu(function(n){var t=jo(n);return Jf(t)&&(t=X),mu(i(n,Jf),mi(t,2))}),Hs=uu(function(n){var t=jo(n);return t="function"==typeof t?t:X,mu(i(n,Jf),X,t)}),Js=uu(Go),Ys=uu(function(n){var t=n.length,r=t>1?n[t-1]:X;return r="function"==typeof r?(n.pop(),
|
||||||
|
r):X,Ho(n,r)}),Qs=gi(function(n){var t=n.length,r=t?n[0]:0,e=this.__wrapped__,u=function(t){return Tr(t,n)};return!(t>1||this.__actions__.length)&&e instanceof Ct&&Ci(r)?(e=e.slice(r,+r+(t?1:0)),e.__actions__.push({func:nf,args:[u],thisArg:X}),new Y(e,this.__chain__).thru(function(n){return t&&!n.length&&n.push(X),n})):this.thru(u)}),Xs=Fu(function(n,t,r){bl.call(n,r)?++n[r]:Br(n,r,1)}),nh=Ju(ho),th=Ju(po),rh=Fu(function(n,t,r){bl.call(n,r)?n[r].push(t):Br(n,r,[t])}),eh=uu(function(t,r,e){var u=-1,i="function"==typeof r,o=Hf(t)?il(t.length):[];
|
||||||
|
return ys(t,function(t){o[++u]=i?n(r,t,e):Ie(t,r,e)}),o}),uh=Fu(function(n,t,r){Br(n,r,t)}),ih=Fu(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),oh=uu(function(n,t){if(null==n)return[];var r=t.length;return r>1&&Ui(n,t[0],t[1])?t=[]:r>2&&Ui(t[0],t[1],t[2])&&(t=[t[0]]),He(n,ee(t,1),[])}),fh=Dl||function(){return re.Date.now()},ch=uu(function(n,t,r){var e=_n;if(r.length){var u=N(r,wi(ch));e|=bn}return ai(n,e,t,r,u)}),ah=uu(function(n,t,r){var e=_n|vn;if(r.length){var u=N(r,wi(ah));e|=bn;
|
||||||
|
}return ai(t,e,n,r,u)}),lh=uu(function(n,t){return Gr(n,1,t)}),sh=uu(function(n,t,r){return Gr(n,Ic(t)||0,r)});Cf.Cache=sr;var hh=js(function(t,r){r=1==r.length&&bh(r[0])?c(r[0],z(mi())):c(ee(r,1),z(mi()));var e=r.length;return uu(function(u){for(var i=-1,o=Hl(u.length,e);++i<o;)u[i]=r[i].call(this,u[i]);return n(t,this,u)})}),ph=uu(function(n,t){return ai(n,bn,X,t,N(t,wi(ph)))}),_h=uu(function(n,t){return ai(n,wn,X,t,N(t,wi(_h)))}),vh=gi(function(n,t){return ai(n,xn,X,X,X,t)}),gh=ii(me),yh=ii(function(n,t){
|
||||||
|
return n>=t}),dh=Re(function(){return arguments}())?Re:function(n){return cc(n)&&bl.call(n,"callee")&&!Wl.call(n,"callee")},bh=il.isArray,wh=ce?z(ce):ze,mh=ql||qa,xh=ae?z(ae):Ee,jh=le?z(le):Le,Ah=se?z(se):Be,kh=he?z(he):Te,Oh=pe?z(pe):$e,Ih=ii(Ne),Rh=ii(function(n,t){return n<=t}),zh=Nu(function(n,t){if(Mi(t)||Hf(t))return $u(t,Pc(t),n),X;for(var r in t)bl.call(t,r)&&Sr(n,r,t[r])}),Eh=Nu(function(n,t){$u(t,qc(t),n)}),Sh=Nu(function(n,t,r,e){$u(t,qc(t),n,e)}),Wh=Nu(function(n,t,r,e){$u(t,Pc(t),n,e);
|
||||||
|
}),Lh=gi(Tr),Ch=uu(function(n,t){n=ll(n);var r=-1,e=t.length,u=e>2?t[2]:X;for(u&&Ui(t[0],t[1],u)&&(e=1);++r<e;)for(var i=t[r],o=qc(i),f=-1,c=o.length;++f<c;){var a=o[f],l=n[a];(l===X||Gf(l,gl[a])&&!bl.call(n,a))&&(n[a]=i[a])}return n}),Uh=uu(function(t){return t.push(X,si),n(Mh,X,t)}),Bh=Xu(function(n,t,r){null!=t&&"function"!=typeof t.toString&&(t=xl.call(t)),n[t]=r},Sa(La)),Th=Xu(function(n,t,r){null!=t&&"function"!=typeof t.toString&&(t=xl.call(t)),bl.call(n,t)?n[t].push(r):n[t]=[r]},mi),$h=uu(Ie),Dh=Nu(function(n,t,r){
|
||||||
|
Ke(n,t,r)}),Mh=Nu(function(n,t,r,e){Ke(n,t,r,e)}),Fh=gi(function(n,t){var r={};if(null==n)return r;var e=!1;t=c(t,function(t){return t=ku(t,n),e||(e=t.length>1),t}),$u(n,di(n),r),e&&(r=Fr(r,an|ln|sn,hi));for(var u=t.length;u--;)yu(r,t[u]);return r}),Nh=gi(function(n,t){return null==n?{}:Je(n,t)}),Ph=ci(Pc),qh=ci(qc),Zh=Vu(function(n,t,r){return t=t.toLowerCase(),n+(r?fa(t):t)}),Kh=Vu(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Vh=Vu(function(n,t,r){return n+(r?" ":"")+t.toLowerCase()}),Gh=Ku("toLowerCase"),Hh=Vu(function(n,t,r){
|
||||||
|
return n+(r?"_":"")+t.toLowerCase()}),Jh=Vu(function(n,t,r){return n+(r?" ":"")+Qh(t)}),Yh=Vu(function(n,t,r){return n+(r?" ":"")+t.toUpperCase()}),Qh=Ku("toUpperCase"),Xh=uu(function(t,r){try{return n(t,X,r)}catch(n){return rc(n)?n:new fl(n)}}),np=gi(function(n,t){return r(t,function(t){t=no(t),Br(n,t,ch(n[t],n))}),n}),tp=Yu(),rp=Yu(!0),ep=uu(function(n,t){return function(r){return Ie(r,n,t)}}),up=uu(function(n,t){return function(r){return Ie(n,r,t)}}),ip=ti(c),op=ti(u),fp=ti(h),cp=ui(),ap=ui(!0),lp=ni(function(n,t){
|
||||||
|
return n+t},0),sp=fi("ceil"),hp=ni(function(n,t){return n/t},1),pp=fi("floor"),_p=ni(function(n,t){return n*t},1),vp=fi("round"),gp=ni(function(n,t){return n-t},0);return Z.after=If,Z.ary=Rf,Z.assign=zh,Z.assignIn=Eh,Z.assignInWith=Sh,Z.assignWith=Wh,Z.at=Lh,Z.before=zf,Z.bind=ch,Z.bindAll=np,Z.bindKey=ah,Z.castArray=Nf,Z.chain=Qo,Z.chunk=uo,Z.compact=io,Z.concat=oo,Z.cond=za,Z.conforms=Ea,Z.constant=Sa,Z.countBy=Xs,Z.create=Sc,Z.curry=Ef,Z.curryRight=Sf,Z.debounce=Wf,Z.defaults=Ch,Z.defaultsDeep=Uh,
|
||||||
|
Z.defer=lh,Z.delay=sh,Z.difference=Us,Z.differenceBy=Bs,Z.differenceWith=Ts,Z.drop=fo,Z.dropRight=co,Z.dropRightWhile=ao,Z.dropWhile=lo,Z.fill=so,Z.filter=lf,Z.flatMap=sf,Z.flatMapDeep=hf,Z.flatMapDepth=pf,Z.flatten=_o,Z.flattenDeep=vo,Z.flattenDepth=go,Z.flip=Lf,Z.flow=tp,Z.flowRight=rp,Z.fromPairs=yo,Z.functions=$c,Z.functionsIn=Dc,Z.groupBy=rh,Z.initial=mo,Z.intersection=$s,Z.intersectionBy=Ds,Z.intersectionWith=Ms,Z.invert=Bh,Z.invertBy=Th,Z.invokeMap=eh,Z.iteratee=Ca,Z.keyBy=uh,Z.keys=Pc,Z.keysIn=qc,
|
||||||
|
Z.map=yf,Z.mapKeys=Zc,Z.mapValues=Kc,Z.matches=Ua,Z.matchesProperty=Ba,Z.memoize=Cf,Z.merge=Dh,Z.mergeWith=Mh,Z.method=ep,Z.methodOf=up,Z.mixin=Ta,Z.negate=Uf,Z.nthArg=Ma,Z.omit=Fh,Z.omitBy=Vc,Z.once=Bf,Z.orderBy=df,Z.over=ip,Z.overArgs=hh,Z.overEvery=op,Z.overSome=fp,Z.partial=ph,Z.partialRight=_h,Z.partition=ih,Z.pick=Nh,Z.pickBy=Gc,Z.property=Fa,Z.propertyOf=Na,Z.pull=Fs,Z.pullAll=Oo,Z.pullAllBy=Io,Z.pullAllWith=Ro,Z.pullAt=Ns,Z.range=cp,Z.rangeRight=ap,Z.rearg=vh,Z.reject=mf,Z.remove=zo,Z.rest=Tf,
|
||||||
|
Z.reverse=Eo,Z.sampleSize=jf,Z.set=Jc,Z.setWith=Yc,Z.shuffle=Af,Z.slice=So,Z.sortBy=oh,Z.sortedUniq=$o,Z.sortedUniqBy=Do,Z.split=da,Z.spread=$f,Z.tail=Mo,Z.take=Fo,Z.takeRight=No,Z.takeRightWhile=Po,Z.takeWhile=qo,Z.tap=Xo,Z.throttle=Df,Z.thru=nf,Z.toArray=jc,Z.toPairs=Ph,Z.toPairsIn=qh,Z.toPath=Ha,Z.toPlainObject=Rc,Z.transform=Qc,Z.unary=Mf,Z.union=Ps,Z.unionBy=qs,Z.unionWith=Zs,Z.uniq=Zo,Z.uniqBy=Ko,Z.uniqWith=Vo,Z.unset=Xc,Z.unzip=Go,Z.unzipWith=Ho,Z.update=na,Z.updateWith=ta,Z.values=ra,Z.valuesIn=ea,
|
||||||
|
Z.without=Ks,Z.words=Ra,Z.wrap=Ff,Z.xor=Vs,Z.xorBy=Gs,Z.xorWith=Hs,Z.zip=Js,Z.zipObject=Jo,Z.zipObjectDeep=Yo,Z.zipWith=Ys,Z.entries=Ph,Z.entriesIn=qh,Z.extend=Eh,Z.extendWith=Sh,Ta(Z,Z),Z.add=lp,Z.attempt=Xh,Z.camelCase=Zh,Z.capitalize=fa,Z.ceil=sp,Z.clamp=ua,Z.clone=Pf,Z.cloneDeep=Zf,Z.cloneDeepWith=Kf,Z.cloneWith=qf,Z.conformsTo=Vf,Z.deburr=ca,Z.defaultTo=Wa,Z.divide=hp,Z.endsWith=aa,Z.eq=Gf,Z.escape=la,Z.escapeRegExp=sa,Z.every=af,Z.find=nh,Z.findIndex=ho,Z.findKey=Wc,Z.findLast=th,Z.findLastIndex=po,
|
||||||
|
Z.findLastKey=Lc,Z.floor=pp,Z.forEach=_f,Z.forEachRight=vf,Z.forIn=Cc,Z.forInRight=Uc,Z.forOwn=Bc,Z.forOwnRight=Tc,Z.get=Mc,Z.gt=gh,Z.gte=yh,Z.has=Fc,Z.hasIn=Nc,Z.head=bo,Z.identity=La,Z.includes=gf,Z.indexOf=wo,Z.inRange=ia,Z.invoke=$h,Z.isArguments=dh,Z.isArray=bh,Z.isArrayBuffer=wh,Z.isArrayLike=Hf,Z.isArrayLikeObject=Jf,Z.isBoolean=Yf,Z.isBuffer=mh,Z.isDate=xh,Z.isElement=Qf,Z.isEmpty=Xf,Z.isEqual=nc,Z.isEqualWith=tc,Z.isError=rc,Z.isFinite=ec,Z.isFunction=uc,Z.isInteger=ic,Z.isLength=oc,Z.isMap=jh,
|
||||||
|
Z.isMatch=ac,Z.isMatchWith=lc,Z.isNaN=sc,Z.isNative=hc,Z.isNil=_c,Z.isNull=pc,Z.isNumber=vc,Z.isObject=fc,Z.isObjectLike=cc,Z.isPlainObject=gc,Z.isRegExp=Ah,Z.isSafeInteger=yc,Z.isSet=kh,Z.isString=dc,Z.isSymbol=bc,Z.isTypedArray=Oh,Z.isUndefined=wc,Z.isWeakMap=mc,Z.isWeakSet=xc,Z.join=xo,Z.kebabCase=Kh,Z.last=jo,Z.lastIndexOf=Ao,Z.lowerCase=Vh,Z.lowerFirst=Gh,Z.lt=Ih,Z.lte=Rh,Z.max=Ya,Z.maxBy=Qa,Z.mean=Xa,Z.meanBy=nl,Z.min=tl,Z.minBy=rl,Z.stubArray=Pa,Z.stubFalse=qa,Z.stubObject=Za,Z.stubString=Ka,
|
||||||
|
Z.stubTrue=Va,Z.multiply=_p,Z.nth=ko,Z.noConflict=$a,Z.noop=Da,Z.now=fh,Z.pad=ha,Z.padEnd=pa,Z.padStart=_a,Z.parseInt=va,Z.random=oa,Z.reduce=bf,Z.reduceRight=wf,Z.repeat=ga,Z.replace=ya,Z.result=Hc,Z.round=vp,Z.runInContext=p,Z.sample=xf,Z.size=kf,Z.snakeCase=Hh,Z.some=Of,Z.sortedIndex=Wo,Z.sortedIndexBy=Lo,Z.sortedIndexOf=Co,Z.sortedLastIndex=Uo,Z.sortedLastIndexBy=Bo,Z.sortedLastIndexOf=To,Z.startCase=Jh,Z.startsWith=ba,Z.subtract=gp,Z.sum=el,Z.sumBy=ul,Z.template=wa,Z.times=Ga,Z.toFinite=Ac,Z.toInteger=kc,
|
||||||
|
Z.toLength=Oc,Z.toLower=ma,Z.toNumber=Ic,Z.toSafeInteger=zc,Z.toString=Ec,Z.toUpper=xa,Z.trim=ja,Z.trimEnd=Aa,Z.trimStart=ka,Z.truncate=Oa,Z.unescape=Ia,Z.uniqueId=Ja,Z.upperCase=Yh,Z.upperFirst=Qh,Z.each=_f,Z.eachRight=vf,Z.first=bo,Ta(Z,function(){var n={};return ue(Z,function(t,r){bl.call(Z.prototype,r)||(n[r]=t)}),n}(),{chain:!1}),Z.VERSION=nn,r(["bind","bindKey","curry","curryRight","partial","partialRight"],function(n){Z[n].placeholder=Z}),r(["drop","take"],function(n,t){Ct.prototype[n]=function(r){
|
||||||
|
r=r===X?1:Gl(kc(r),0);var e=this.__filtered__&&!t?new Ct(this):this.clone();return e.__filtered__?e.__takeCount__=Hl(r,e.__takeCount__):e.__views__.push({size:Hl(r,Un),type:n+(e.__dir__<0?"Right":"")}),e},Ct.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),r(["filter","map","takeWhile"],function(n,t){var r=t+1,e=r==Rn||r==En;Ct.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:mi(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),r(["head","last"],function(n,t){
|
||||||
|
var r="take"+(t?"Right":"");Ct.prototype[n]=function(){return this[r](1).value()[0]}}),r(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Ct.prototype[n]=function(){return this.__filtered__?new Ct(this):this[r](1)}}),Ct.prototype.compact=function(){return this.filter(La)},Ct.prototype.find=function(n){return this.filter(n).head()},Ct.prototype.findLast=function(n){return this.reverse().find(n)},Ct.prototype.invokeMap=uu(function(n,t){return"function"==typeof n?new Ct(this):this.map(function(r){
|
||||||
|
return Ie(r,n,t)})}),Ct.prototype.reject=function(n){return this.filter(Uf(mi(n)))},Ct.prototype.slice=function(n,t){n=kc(n);var r=this;return r.__filtered__&&(n>0||t<0)?new Ct(r):(n<0?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==X&&(t=kc(t),r=t<0?r.dropRight(-t):r.take(t-n)),r)},Ct.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Ct.prototype.toArray=function(){return this.take(Un)},ue(Ct.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=Z[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t);
|
||||||
|
u&&(Z.prototype[t]=function(){var t=this.__wrapped__,o=e?[1]:arguments,f=t instanceof Ct,c=o[0],l=f||bh(t),s=function(n){var t=u.apply(Z,a([n],o));return e&&h?t[0]:t};l&&r&&"function"==typeof c&&1!=c.length&&(f=l=!1);var h=this.__chain__,p=!!this.__actions__.length,_=i&&!h,v=f&&!p;if(!i&&l){t=v?t:new Ct(this);var g=n.apply(t,o);return g.__actions__.push({func:nf,args:[s],thisArg:X}),new Y(g,h)}return _&&v?n.apply(this,o):(g=this.thru(s),_?e?g.value()[0]:g.value():g)})}),r(["pop","push","shift","sort","splice","unshift"],function(n){
|
||||||
|
var t=_l[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);Z.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(bh(u)?u:[],n)}return this[r](function(r){return t.apply(bh(r)?r:[],n)})}}),ue(Ct.prototype,function(n,t){var r=Z[t];if(r){var e=r.name+"";bl.call(fs,e)||(fs[e]=[]),fs[e].push({name:t,func:r})}}),fs[Qu(X,vn).name]=[{name:"wrapper",func:X}],Ct.prototype.clone=$t,Ct.prototype.reverse=Yt,Ct.prototype.value=Qt,Z.prototype.at=Qs,
|
||||||
|
Z.prototype.chain=tf,Z.prototype.commit=rf,Z.prototype.next=ef,Z.prototype.plant=of,Z.prototype.reverse=ff,Z.prototype.toJSON=Z.prototype.valueOf=Z.prototype.value=cf,Z.prototype.first=Z.prototype.head,Ul&&(Z.prototype[Ul]=uf),Z},be=de();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(re._=be,define(function(){return be})):ue?((ue.exports=be)._=be,ee._=be):re._=be}).call(this);
|
||||||
6
public/js/vendor/popper.min.js
vendored
Executable file
6
public/js/vendor/popper.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
|||||||
(()=>{function e(){$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")}))}function a(){$("#date_to").datepicker("remove"),$("#date_to").val(""),e(),$("#date_to").prop("disabled",!0),console.log("End date picker reset and disabled")}$(document).ready((function(){console.log("Opnames index.js loaded"),void 0!==$.fn.DataTable?(console.log("Initializing Select2..."),void 0!==$.fn.select2?$("#dealer_filter").select2({placeholder:"Pilih...",allowClear:!0,width:"100%"}):console.warn("Select2 not available, using regular select"),function(){if(console.log("Initializing datepickers..."),void 0===$.fn.datepicker)return void console.error("Bootstrap Datepicker not available!");$("#date_from").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){var a;console.log("Start date selected:",e.format()),a=e.format(),console.log("Enabling end date picker with min date:",a),$("#date_to").prop("disabled",!1),$("#date_to").datepicker("remove"),$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},startDate:a,endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")})),console.log("End date picker enabled with startDate:",a)})).on("clearDate",(function(e){console.log("Start date cleared"),a()})),e(),$("#date_to").prop("disabled",!0)}(),setTimeout((function(){!function(){console.log("Initializing DataTable..."),$.fn.DataTable.isDataTable("#opnames-table")&&$("#opnames-table").DataTable().destroy();var e=$("#opnames-table").DataTable({processing:!0,serverSide:!0,destroy:!0,ajax:{url:$("#opnames-table").data("url"),type:"GET",data:function(e){return e.dealer_filter=$("#dealer_filter").val(),e.date_from=$("#date_from").val(),e.date_to=$("#date_to").val(),console.log("AJAX data being sent:",{dealer_filter:e.dealer_filter,date_from:e.date_from,date_to:e.date_to}),e},error:function(e,a,t){console.error("DataTables AJAX error:",a,t),console.error("Response:",e.responseText)}},columnDefs:[{targets:0,width:"15%"},{targets:5,width:"15%",className:"text-center"}],columns:[{data:"created_at",name:"created_at",orderable:!0},{data:"opname_date",name:"opname_date",orderable:!0},{data:"dealer_name",name:"dealer.name",orderable:!0},{data:"user_name",name:"user.name",orderable:!0},{data:"status",name:"status",orderable:!0},{data:"action",name:"action",orderable:!1,searchable:!1}],order:[[4,"desc"]],pageLength:10,responsive:!0,ordering:!0,orderMulti:!1});(function(e){$("#kt_search").on("click",(function(){console.log("Filter button clicked");var a=$("#dealer_filter").val(),t=$("#date_from").val(),o=$("#date_to").val();console.log("Filtering with:",{dealer:a,dateFrom:t,dateTo:o}),e.ajax.reload()})),$("#kt_reset").on("click",(function(){console.log("Reset button clicked"),$("#dealer_filter").val(null).trigger("change.select2"),$("#date_from").datepicker("clearDates"),$("#date_to").datepicker("clearDates"),a(),e.ajax.reload()})),$("#date_from, #date_to").on("keypress",(function(e){13===e.which&&$("#kt_search").click()})),$("#dealer_filter").on("change",(function(){console.log("Dealer filter changed:",$(this).val())}))})(e),function(e){e.on("order.dt",(function(){console.log("Order changed:",e.order())})),e.on("processing.dt",(function(e,a,t){t?console.log("DataTable processing started"):console.log("DataTable processing finished")}))}(e)}()}),100)):console.error("DataTables not available!")}))})();
|
(()=>{function e(){$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")}))}function a(){$("#date_to").datepicker("remove"),$("#date_to").val(""),e(),$("#date_to").prop("disabled",!0),console.log("End date picker reset and disabled")}$(document).ready((function(){console.log("Opnames index.js loaded"),void 0!==$.fn.DataTable?(console.log("Initializing Select2..."),void 0!==$.fn.select2?$("#dealer_filter").select2({placeholder:"Pilih...",allowClear:!0,width:"100%"}):console.warn("Select2 not available, using regular select"),function(){if(console.log("Initializing datepickers..."),void 0===$.fn.datepicker)return void console.error("Bootstrap Datepicker not available!");$("#date_from").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){var a;console.log("Start date selected:",e.format()),a=e.format(),console.log("Enabling end date picker with min date:",a),$("#date_to").prop("disabled",!1),$("#date_to").datepicker("remove"),$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},startDate:a,endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")})),console.log("End date picker enabled with startDate:",a)})).on("clearDate",(function(e){console.log("Start date cleared"),a()})),e(),$("#date_to").prop("disabled",!0)}(),setTimeout((function(){!function(){console.log("Initializing DataTable..."),$.fn.DataTable.isDataTable("#opnames-table")&&$("#opnames-table").DataTable().destroy();var e=$("#opnames-table").DataTable({processing:!0,serverSide:!0,destroy:!0,ajax:{url:$("#opnames-table").data("url"),type:"GET",data:function(e){return e.dealer_filter=$("#dealer_filter").val(),e.date_from=$("#date_from").val(),e.date_to=$("#date_to").val(),console.log("AJAX data being sent:",{dealer_filter:e.dealer_filter,date_from:e.date_from,date_to:e.date_to}),e},error:function(e,a,t){console.error("DataTables AJAX error:",a,t),console.error("Response:",e.responseText)}},columnDefs:[{targets:0,width:"15%"},{targets:1,width:"12%"},{targets:2,width:"15%"},{targets:3,width:"12%"},{targets:4,width:"10%"},{targets:5,width:"15%",className:"text-center"},{targets:6,width:"15%",className:"text-center"}],columns:[{data:"created_at",name:"created_at",orderable:!0},{data:"opname_date",name:"opname_date",orderable:!0},{data:"dealer_name",name:"dealer.name",orderable:!0},{data:"user_name",name:"user.name",orderable:!0},{data:"status",name:"status",orderable:!0},{data:"stock_info",name:"stock_info",orderable:!1,searchable:!1},{data:"action",name:"action",orderable:!1,searchable:!1}],order:[[0,"desc"]],pageLength:10,responsive:!0,ordering:!0,orderMulti:!1});(function(e){$("#kt_search").on("click",(function(){console.log("Filter button clicked");var a=$("#dealer_filter").val(),t=$("#date_from").val(),o=$("#date_to").val();console.log("Filtering with:",{dealer:a,dateFrom:t,dateTo:o}),e.ajax.reload()})),$("#kt_reset").on("click",(function(){console.log("Reset button clicked"),$("#dealer_filter").val(null).trigger("change.select2"),$("#date_from").datepicker("clearDates"),$("#date_to").datepicker("clearDates"),a(),e.ajax.reload()})),$("#date_from, #date_to").on("keypress",(function(e){13===e.which&&$("#kt_search").click()})),$("#dealer_filter").on("change",(function(){console.log("Dealer filter changed:",$(this).val())}))})(e),function(e){e.on("order.dt",(function(){console.log("Order changed:",e.order())})),e.on("processing.dt",(function(e,a,t){t?console.log("DataTable processing started"):console.log("DataTable processing finished")}))}(e)}()}),100)):console.error("DataTables not available!")}))})();
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,28 +1,5 @@
|
|||||||
{
|
{
|
||||||
"/js/app.js": "/js/app.js",
|
"/js/app.js": "/js/app.js",
|
||||||
"/js/vendor.js": "/js/vendor.js",
|
"/js/vendor.js": "/js/vendor.js",
|
||||||
"/js/warehouse_management/product_categories/index.js": "/js/warehouse_management/product_categories/index.js",
|
"/css/app.css": "/css/app.css"
|
||||||
"/js/warehouse_management/products/index.js": "/js/warehouse_management/products/index.js",
|
|
||||||
"/js/warehouse_management/opnames/index.js": "/js/warehouse_management/opnames/index.js",
|
|
||||||
"/js/warehouse_management/opnames/create.js": "/js/warehouse_management/opnames/create.js",
|
|
||||||
"/js/warehouse_management/opnames/detail.js": "/js/warehouse_management/opnames/detail.js",
|
|
||||||
"/js/warehouse_management/mutations/index.js": "/js/warehouse_management/mutations/index.js",
|
|
||||||
"/js/warehouse_management/mutations/create.js": "/js/warehouse_management/mutations/create.js",
|
|
||||||
"/js/warehouse_management/stock_audit/index.js": "/js/warehouse_management/stock_audit/index.js",
|
|
||||||
"/css/app.css": "/css/app.css",
|
|
||||||
"/js/vendor/jquery.dataTables.min.js": "/js/vendor/jquery.dataTables.min.js",
|
|
||||||
"/js/vendor/dataTables.bootstrap4.min.js": "/js/vendor/dataTables.bootstrap4.min.js",
|
|
||||||
"/js/vendor/dataTables.fixedColumns.min.js": "/js/vendor/dataTables.fixedColumns.min.js",
|
|
||||||
"/js/vendor/sweetalert2.min.js": "/js/vendor/sweetalert2.min.js",
|
|
||||||
"/js/vendor/chart.umd.js": "/js/vendor/chart.umd.js",
|
|
||||||
"/js/vendor/chartjs-plugin-datalabels.min.js": "/js/vendor/chartjs-plugin-datalabels.min.js",
|
|
||||||
"/css/vendor/dataTables.bootstrap4.min.css": "/css/vendor/dataTables.bootstrap4.min.css",
|
|
||||||
"/css/vendor/fixedColumns.bootstrap4.min.css": "/css/vendor/fixedColumns.bootstrap4.min.css",
|
|
||||||
"/css/vendor/sweetalert2.min.css": "/css/vendor/sweetalert2.min.css",
|
|
||||||
"/js/cdn/dataTables.bootstrap4.min.js": "/js/cdn/dataTables.bootstrap4.min.js",
|
|
||||||
"/js/cdn/dataTables.fixedColumns.min.js": "/js/cdn/dataTables.fixedColumns.min.js",
|
|
||||||
"/js/cdn/jquery.dataTables.min.js": "/js/cdn/jquery.dataTables.min.js",
|
|
||||||
"/css/dataTables.bootstrap4.min.css": "/css/dataTables.bootstrap4.min.css",
|
|
||||||
"/css/fixedColumns.bootstrap4.min.css": "/css/fixedColumns.bootstrap4.min.css",
|
|
||||||
"/css/google-font.css": "/css/google-font.css"
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
quick-fix.sh
33
quick-fix.sh
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "⚡ Quick fix for Docker development..."
|
|
||||||
|
|
||||||
# Quick permission fix without stopping containers
|
|
||||||
echo "🔐 Quick permission fix..."
|
|
||||||
sudo chmod -R 755 public
|
|
||||||
sudo chmod 644 public/index.php
|
|
||||||
|
|
||||||
# Test if container can read the file
|
|
||||||
echo "🧪 Testing file access..."
|
|
||||||
docker exec ckb-app-dev test -r /var/www/html/public/index.php && echo "✅ File is readable" || echo "❌ File not readable"
|
|
||||||
|
|
||||||
# Test PHP execution
|
|
||||||
echo "🐘 Testing PHP execution..."
|
|
||||||
docker exec ckb-app-dev php -v
|
|
||||||
|
|
||||||
# Test Laravel
|
|
||||||
echo "🎯 Testing Laravel..."
|
|
||||||
docker exec ckb-app-dev php /var/www/html/artisan --version
|
|
||||||
|
|
||||||
# Test HTTP
|
|
||||||
echo "🌐 Testing HTTP..."
|
|
||||||
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://localhost:8000
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🔍 Debug info:"
|
|
||||||
echo "Host file: $(ls -la public/index.php)"
|
|
||||||
echo "Container file:"
|
|
||||||
docker exec ckb-app-dev ls -la /var/www/html/public/index.php
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "If still not working, run: chmod +x fix-permissions.sh && ./fix-permissions.sh"
|
|
||||||
@@ -196,8 +196,13 @@ function initializeDataTable() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{ targets: 0, width: "15%" }, // Opname Date column
|
{ targets: 0, width: "15%" }, // Created At column
|
||||||
{ targets: 5, width: "15%", className: "text-center" }, // Action column
|
{ targets: 1, width: "12%" }, // Opname Date column
|
||||||
|
{ targets: 2, width: "15%" }, // Dealer column
|
||||||
|
{ targets: 3, width: "12%" }, // User column
|
||||||
|
{ targets: 4, width: "10%" }, // Status column
|
||||||
|
{ targets: 5, width: "15%", className: "text-center" }, // Stock Info column
|
||||||
|
{ targets: 6, width: "15%", className: "text-center" }, // Action column
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -225,6 +230,12 @@ function initializeDataTable() {
|
|||||||
name: "status",
|
name: "status",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
data: "stock_info",
|
||||||
|
name: "stock_info",
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
data: "action",
|
data: "action",
|
||||||
name: "action",
|
name: "action",
|
||||||
@@ -232,7 +243,7 @@ function initializeDataTable() {
|
|||||||
searchable: false,
|
searchable: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
order: [[4, "desc"]], // Order by created_at desc
|
order: [[0, "desc"]], // Order by created_at desc
|
||||||
pageLength: 10,
|
pageLength: 10,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
ordering: true,
|
ordering: true,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
<title>POS | Login</title>
|
<title>POS | Login</title>
|
||||||
<meta name="description" content="Login page example">
|
<meta name="description" content="CKB POS Login Page">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="base-url" content="{{ url('/') }}">
|
<meta name="base-url" content="{{ url('/') }}">
|
||||||
|
|
||||||
|
|||||||
322
resources/views/back/master/work_prices.blade.php
Normal file
322
resources/views/back/master/work_prices.blade.php
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
/* Action button flex layout */
|
||||||
|
.d-flex.flex-row.gap-1 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 0.25rem !important;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.flex-row.gap-1 .btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.flex-row.gap-1 .btn i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure DataTables doesn't break flex layout */
|
||||||
|
.dataTables_wrapper .dataTables_scrollBody .d-flex {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.d-flex.flex-row.gap-1 {
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 0.125rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.flex-row.gap-1 .btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form control styling */
|
||||||
|
.form-control {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn i {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status text styling */
|
||||||
|
.status-text {
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.active {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.inactive {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input group styling */
|
||||||
|
.input-group-text {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price input styling */
|
||||||
|
.price-input {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-input:focus {
|
||||||
|
border-color: #80bdff;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status label styling */
|
||||||
|
.custom-control-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status label styling */
|
||||||
|
.custom-control-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styling */
|
||||||
|
.alert {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button loading state */
|
||||||
|
.btn.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading overlay */
|
||||||
|
#loading-overlay {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-overlay .spinner-border {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<span class="kt-portlet__head-icon">
|
||||||
|
<i class="kt-font-brand flaticon2-line-chart"></i>
|
||||||
|
</span>
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Set Harga Pekerjaan: <strong>{{ $work->name }}</strong>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-wrapper">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<button type="button" class="btn btn-bold btn-label-brand" id="btn-save-all">
|
||||||
|
Simpan Semua Harga
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<!--begin: Datatable -->
|
||||||
|
<table class="table table-striped table-bordered table-hover table-checkable" id="dealers-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%">No</th>
|
||||||
|
<th width="25%">Nama Dealer</th>
|
||||||
|
<th width="15%">Kode Dealer</th>
|
||||||
|
<th width="20%">Harga (IDR)</th>
|
||||||
|
<th width="10%">Status</th>
|
||||||
|
<th width="15%">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($dealers as $index => $dealer)
|
||||||
|
@php
|
||||||
|
$existingPrice = $work->getPriceForDealer($dealer->id);
|
||||||
|
@endphp
|
||||||
|
<tr data-dealer-id="{{ $dealer->id }}">
|
||||||
|
<td>{{ $index + 1 }}</td>
|
||||||
|
<td>{{ $dealer->name }}</td>
|
||||||
|
<td>{{ $dealer->dealer_code }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Rp</span>
|
||||||
|
</div>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control price-input"
|
||||||
|
name="price_{{ $dealer->id }}"
|
||||||
|
value="{{ $existingPrice ? number_format($existingPrice->price, 0, ',', '.') : '0' }}"
|
||||||
|
placeholder="0"
|
||||||
|
data-dealer-id="{{ $dealer->id }}"
|
||||||
|
data-original-value="{{ $existingPrice ? $existingPrice->price : '0' }}">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox"
|
||||||
|
class="status-input"
|
||||||
|
id="status_{{ $dealer->id }}"
|
||||||
|
name="status_{{ $dealer->id }}"
|
||||||
|
data-dealer-id="{{ $dealer->id }}"
|
||||||
|
{{ $existingPrice && $existingPrice->is_active ? 'checked' : '' }}>
|
||||||
|
<label for="status_{{ $dealer->id }}" style="margin-left: 0.5rem; font-weight: 500;">Aktif</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-row gap-1">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm {{ $existingPrice ? 'btn-warning' : 'btn-success' }} save-single"
|
||||||
|
data-dealer-id="{{ $dealer->id }}"
|
||||||
|
title="{{ $existingPrice ? 'Update Harga' : 'Simpan Harga' }}">
|
||||||
|
{{ $existingPrice ? 'Update' : 'Simpan' }}
|
||||||
|
</button>
|
||||||
|
@if($existingPrice)
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-danger delete-price"
|
||||||
|
data-price-id="{{ $existingPrice->id }}"
|
||||||
|
data-dealer-id="{{ $dealer->id }}"
|
||||||
|
title="Hapus Harga">
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--end: Datatable -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dealers->count() == 0)
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning text-center">
|
||||||
|
<i class="fa fa-exclamation-triangle mr-2"></i>
|
||||||
|
Tidak ada dealer yang tersedia.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--begin::Modal-->
|
||||||
|
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="confirmModalLabel">
|
||||||
|
<i class="fa fa-question-circle mr-2"></i>
|
||||||
|
Konfirmasi
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="confirmMessage"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||||
|
<i class="fa fa-times mr-1"></i>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirmAction">
|
||||||
|
<i class="fa fa-check mr-1"></i>
|
||||||
|
Ya, Lanjutkan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--end::Modal-->
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('javascripts')
|
||||||
|
<script src="{{ url('js/pages/back/master/work-prices.js') }}" type="text/javascript"></script>
|
||||||
|
@endsection
|
||||||
@@ -223,7 +223,14 @@ var table = $('#kt_table').DataTable({
|
|||||||
return `<input type="checkbox" name="selected[]" value="${data}" />`;
|
return `<input type="checkbox" name="selected[]" value="${data}" />`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{data: 'date', name: 'transactions.date'},
|
{
|
||||||
|
data: 'date',
|
||||||
|
name: 'transactions.date',
|
||||||
|
render: function (data, type, row) {
|
||||||
|
if (!data) return '';
|
||||||
|
return data.split(' ')[0]; // ambil bagian sebelum spasi
|
||||||
|
}
|
||||||
|
},
|
||||||
{data: 'dealer_name', name: 'd.name'},
|
{data: 'dealer_name', name: 'd.name'},
|
||||||
{data: 'username', name: 'users.name'},
|
{data: 'username', name: 'users.name'},
|
||||||
{data: 'sa_name', name: 'sa.name'},
|
{data: 'sa_name', name: 'sa.name'},
|
||||||
@@ -415,6 +422,14 @@ jQuery(document).ready(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$(document).on("click", ".action-print", function () {
|
||||||
|
let type = $(this).data("type");
|
||||||
|
let id = $(this).data("id");
|
||||||
|
let url = $(this).data("url");
|
||||||
|
|
||||||
|
window.open(url, "_blank");
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>No</th>
|
<th>No</th>
|
||||||
<th>Nama Role</th>
|
<th>Nama Role</th>
|
||||||
|
<th>Dealer Tambahan</th>
|
||||||
<th>Aksi</th>
|
<th>Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -47,13 +48,30 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ $loop->iteration }}</td>
|
<td>{{ $loop->iteration }}</td>
|
||||||
<td>{{ $role->name }}</td>
|
<td>{{ $role->name }}</td>
|
||||||
|
<td>
|
||||||
|
@if($role->dealers->count() > 0)
|
||||||
|
<div class="dealer-list">
|
||||||
|
@foreach($role->dealers->take(3) as $dealer)
|
||||||
|
<span class="badge badge-info mr-1 mb-1">{{ $dealer->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
@if($role->dealers->count() > 3)
|
||||||
|
<span class="badge badge-secondary">+{{ $role->dealers->count() - 3 }} more</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Tidak ada dealer tambahan untuk role ini</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@can('update', $menus['roleprivileges.index'])
|
@can('update', $menus['roleprivileges.index'])
|
||||||
<button class="btn btn-sm btn-bold btn-warning mr-2" onclick="editRole({{$role->id}})"> Edit</button>
|
<button class="btn btn-sm btn-bold btn-warning mr-2" onclick="editRole({{$role->id}})"> Edit</button>
|
||||||
@endcan
|
@endcan
|
||||||
@can('delete', $menus['roleprivileges.index'])
|
@can('delete', $menus['roleprivileges.index'])
|
||||||
<button class="btn btn-sm btn-bold btn-danger" onclick="deleteRole({{$role->id}}, '{{$role->name}}')">Hapus</button>
|
<button class="btn btn-sm btn-bold btn-danger mr-2" onclick="deleteRole({{$role->id}}, '{{$role->name}}')">Hapus</button>
|
||||||
|
@endcan
|
||||||
|
@can('create', $menus['roleprivileges.index'])
|
||||||
|
<button class="btn btn-sm btn-bold btn-success" onclick="assignDealer({{$role->id}})"> Tambah Dealer</button>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -183,6 +201,141 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="assignDealerModal" tabindex="-1" role="dialog" aria-labelledby="assignDealerModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<form id="assignDealerForm" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="assignDealerModalLabel">
|
||||||
|
Assign Dealer ke Role
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-light border">
|
||||||
|
<strong>Petunjuk:</strong> Pilih dealer yang akan di-assign ke role ini. Dealer yang sudah di-assign akan otomatis tercentang.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">
|
||||||
|
Daftar Dealer
|
||||||
|
</label>
|
||||||
|
<div class="dealer-checkboxes border rounded p-3" style="max-height: 350px; overflow-y: auto;">
|
||||||
|
<div class="row">
|
||||||
|
@foreach ($dealers as $dealer)
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="form-check custom-checkbox">
|
||||||
|
<input class="form-check-input dealer-checkbox" type="checkbox"
|
||||||
|
name="dealers[]" value="{{ $dealer->id }}"
|
||||||
|
id="dealer_{{ $dealer->id }}">
|
||||||
|
<label class="form-check-label d-flex align-items-center" for="dealer_{{ $dealer->id }}">
|
||||||
|
<div class="dealer-info">
|
||||||
|
<div class="dealer-name font-weight-semibold">{{ $dealer->name }}</div>
|
||||||
|
<div class="dealer-code text-muted small">
|
||||||
|
<i class="fas fa-tag mr-1"></i>
|
||||||
|
{{ $dealer->dealer_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm mr-2" onclick="selectAllDealers()">
|
||||||
|
Pilih Semua
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deselectAllDealers()">
|
||||||
|
Hapus Semua
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span id="selectedCount">0</span> dealer dipilih
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||||
|
Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.custom-checkbox .form-check-label {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox .form-check-label:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox .form-check-input:checked + .form-check-label {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-left: 3px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-code {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-checkboxes::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-checkboxes::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-checkboxes::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-checkboxes::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive improvements */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dealer-checkboxes .col-md-6 {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('javascripts')
|
@section('javascripts')
|
||||||
@@ -296,17 +449,145 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assignDealer(roleId) {
|
||||||
|
// Set form action
|
||||||
|
let url = '{{ route("roleprivileges.assignDealer", ":id") }}'.replace(':id', roleId);
|
||||||
|
$("#assignDealerForm").attr("action", url);
|
||||||
|
|
||||||
|
// Reset checkboxes and counter
|
||||||
|
$('.dealer-checkbox').prop('checked', false);
|
||||||
|
updateSelectedCount();
|
||||||
|
|
||||||
|
// Load existing assigned dealers
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("roleprivileges.getAssignedDealers", ":id") }}'.replace(':id', roleId),
|
||||||
|
type: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.assignedDealers) {
|
||||||
|
response.assignedDealers.forEach(function(dealerId) {
|
||||||
|
$(`#dealer_${dealerId}`).prop('checked', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateSelectedCount();
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.log('Error loading assigned dealers');
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#assignDealerModal").modal("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllDealers() {
|
||||||
|
$('.dealer-checkbox').prop('checked', true);
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAllDealers() {
|
||||||
|
$('.dealer-checkbox').prop('checked', false);
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedCount() {
|
||||||
|
const selectedCount = $('.dealer-checkbox:checked').length;
|
||||||
|
$('#selectedCount').text(selectedCount);
|
||||||
|
|
||||||
|
// Update submit button state
|
||||||
|
if (selectedCount > 0) {
|
||||||
|
$('#submitBtn').prop('disabled', false).removeClass('btn-secondary').addClass('btn-primary');
|
||||||
|
} else {
|
||||||
|
$('#submitBtn').prop('disabled', true).removeClass('btn-primary').addClass('btn-secondary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// Add event handlers for modal close buttons
|
// Add event handlers for modal close buttons
|
||||||
$('.close, [data-dismiss="modal"]').on("click", function () {
|
$('.close, [data-dismiss="modal"]').on("click", function () {
|
||||||
$("#roleModal").modal("hide");
|
$("#roleModal").modal("hide");
|
||||||
$("#roleEditModal").modal("hide");
|
$("#roleEditModal").modal("hide");
|
||||||
|
$("#assignDealerModal").modal("hide");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also handle the "Close" button
|
// Also handle the "Close" button
|
||||||
$('.btn-secondary[data-dismiss="modal"]').on("click", function () {
|
$('.btn-secondary[data-dismiss="modal"]').on("click", function () {
|
||||||
$("#roleModal").modal("hide");
|
$("#roleModal").modal("hide");
|
||||||
$("#roleEditModal").modal("hide");
|
$("#roleEditModal").modal("hide");
|
||||||
|
$("#assignDealerModal").modal("hide");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener for dealer checkboxes
|
||||||
|
$(document).on('change', '.dealer-checkbox', function() {
|
||||||
|
updateSelectedCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission for assign dealer
|
||||||
|
$("#assignDealerForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate if at least one dealer is selected
|
||||||
|
const selectedDealers = $('.dealer-checkbox:checked').length;
|
||||||
|
if (selectedDealers === 0) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Peringatan!',
|
||||||
|
text: 'Silakan pilih minimal satu dealer',
|
||||||
|
icon: 'warning',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable submit button and show loading
|
||||||
|
const submitBtn = $('#submitBtn');
|
||||||
|
const originalText = submitBtn.html();
|
||||||
|
submitBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i>Menyimpan...');
|
||||||
|
|
||||||
|
let formData = new FormData(this);
|
||||||
|
let url = $(this).attr("action");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Berhasil!',
|
||||||
|
text: response.message,
|
||||||
|
icon: 'success',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
}).then(() => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error!',
|
||||||
|
text: response.message || 'Terjadi kesalahan',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
let message = 'Terjadi kesalahan';
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||||
|
message = xhr.responseJSON.message;
|
||||||
|
}
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error!',
|
||||||
|
text: message,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
// Re-enable submit button
|
||||||
|
submitBtn.prop('disabled', false).html(originalText);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,22 +24,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kt-portlet__body">
|
<div class="kt-portlet__body">
|
||||||
<!--begin: Datatable -->
|
<div class="table-responsive">
|
||||||
<table class="table table-responsive table-striped table-bordered table-hover table-checkable" id="kt_table">
|
<!--begin: Datatable -->
|
||||||
<thead>
|
<table class="table table-striped table-bordered table-hover table-checkable" id="kt_table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>No</th>
|
<tr>
|
||||||
<th>Dealer</th>
|
<th>No</th>
|
||||||
<th>Role</th>
|
<th>Dealer</th>
|
||||||
<th style="width: 35%;">Nama Pengguna</th>
|
<th>Role</th>
|
||||||
<th>Email</th>
|
<th style="width: 35%;">Nama Pengguna</th>
|
||||||
<th>Aksi</th>
|
<th>Email</th>
|
||||||
</tr>
|
<th>Aksi</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
</tbody>
|
||||||
<!--end: Datatable -->
|
</table>
|
||||||
|
<!--end: Datatable -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
252
resources/views/kpi/targets/create.blade.php
Normal file
252
resources/views/kpi/targets/create.blade.php
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('title', 'Tambah Target KPI')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.select2-container .select2-selection {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container.select2-container--focus .select2-selection {
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(197, 214, 233, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||||
|
padding-left: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit Select2 dropdown height */
|
||||||
|
.select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for Select2 results */
|
||||||
|
.select2-results__option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve Select2 search box */
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Tambah Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||||
|
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||||
|
<option value="">Pilih Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
<option value="{{ $mechanic->id }}" {{ old('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }} ({{ $mechanic->dealer->name ?? 'N/A' }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@if($mechanics->isEmpty())
|
||||||
|
<div class="alert alert-warning mt-2">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Tidak ada mekanik yang ditemukan. Pastikan ada user dengan role "mechanic" di sistem.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Ditemukan {{ $mechanics->count() }} mekanik.
|
||||||
|
@if($mechanics->count() >= 50)
|
||||||
|
Menampilkan 50 mekanik pertama. Gunakan pencarian untuk menemukan mekanik tertentu.
|
||||||
|
@endif
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
|
@error('user_id')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||||
|
value="{{ old('target_value') }}" min="1" required>
|
||||||
|
@error('target_value')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-control-label">Deskripsi</label>
|
||||||
|
<textarea name="description" id="description" class="form-control" rows="3"
|
||||||
|
placeholder="Deskripsi target (opsional)">{{ old('description') }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||||
|
value="1" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="is_active">
|
||||||
|
Target Aktif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Simpan Target
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize Select2 with fallback
|
||||||
|
try {
|
||||||
|
// Initialize Select2 for mechanics with search limit
|
||||||
|
$('#user_id').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: 'Pilih Mekanik',
|
||||||
|
allowClear: true,
|
||||||
|
minimumInputLength: 1,
|
||||||
|
maximumInputLength: 50,
|
||||||
|
maximumResultsForSearch: 10,
|
||||||
|
language: {
|
||||||
|
inputTooShort: function() {
|
||||||
|
return "Masukkan minimal 1 karakter untuk mencari";
|
||||||
|
},
|
||||||
|
inputTooLong: function() {
|
||||||
|
return "Maksimal 50 karakter";
|
||||||
|
},
|
||||||
|
noResults: function() {
|
||||||
|
return "Tidak ada hasil ditemukan";
|
||||||
|
},
|
||||||
|
searching: function() {
|
||||||
|
return "Mencari...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Select2 not available, using regular select');
|
||||||
|
// Fallback: ensure regular select works
|
||||||
|
$('.select2').removeClass('select2').addClass('form-control');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$('#kpi-form').on('submit', function(e) {
|
||||||
|
var isValid = true;
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
$('.text-danger').remove();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$('#user_id').val()) {
|
||||||
|
errors.push('Mekanik harus dipilih');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||||
|
errors.push('Target nilai harus diisi dan minimal 1');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errors.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
263
resources/views/kpi/targets/edit.blade.php
Normal file
263
resources/views/kpi/targets/edit.blade.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('title', 'Edit Target KPI')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.select2-container .select2-selection {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container.select2-container--focus .select2-selection {
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||||
|
padding-left: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure Select2 is visible */
|
||||||
|
.select2-container {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-dropdown {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix Select2 width */
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit Select2 dropdown height */
|
||||||
|
.select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for Select2 results */
|
||||||
|
.select2-results__option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve Select2 search box */
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Edit Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
|
||||||
|
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.update', $target->id) }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||||
|
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||||
|
<option value="">Pilih Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
@php
|
||||||
|
$isSelected = old('user_id', $target->user_id) == $mechanic->id;
|
||||||
|
@endphp
|
||||||
|
<option value="{{ $mechanic->id }}"
|
||||||
|
{{ $isSelected ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
|
||||||
|
@error('user_id')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||||
|
value="{{ old('target_value', $target->target_value) }}" min="1" required>
|
||||||
|
@error('target_value')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-control-label">Deskripsi</label>
|
||||||
|
<textarea name="description" id="description" class="form-control" rows="3"
|
||||||
|
placeholder="Deskripsi target (opsional)">{{ old('description', $target->description) }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||||
|
value="1" {{ old('is_active', $target->is_active) ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="is_active">
|
||||||
|
Target Aktif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update Target
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize Select2 with fallback and delay
|
||||||
|
setTimeout(function() {
|
||||||
|
try {
|
||||||
|
// Initialize Select2 for mechanics with search limit
|
||||||
|
$('#user_id').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: 'Pilih Mekanik',
|
||||||
|
allowClear: true,
|
||||||
|
minimumInputLength: 1,
|
||||||
|
maximumInputLength: 50,
|
||||||
|
maximumResultsForSearch: 10,
|
||||||
|
language: {
|
||||||
|
inputTooShort: function() {
|
||||||
|
return "Masukkan minimal 1 karakter untuk mencari";
|
||||||
|
},
|
||||||
|
inputTooLong: function() {
|
||||||
|
return "Maksimal 50 karakter";
|
||||||
|
},
|
||||||
|
noResults: function() {
|
||||||
|
return "Tidak ada hasil ditemukan";
|
||||||
|
},
|
||||||
|
searching: function() {
|
||||||
|
return "Mencari...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Select2 not available, using regular select');
|
||||||
|
// Fallback: ensure regular select works
|
||||||
|
$('.select2').removeClass('select2').addClass('form-control');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$('#kpi-form').on('submit', function(e) {
|
||||||
|
var isValid = true;
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
$('.text-danger').remove();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$('#user_id').val()) {
|
||||||
|
errors.push('Mekanik harus dipilih');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||||
|
errors.push('Target nilai harus diisi dan minimal 1');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errors.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
209
resources/views/kpi/targets/index.blade.php
Normal file
209
resources/views/kpi/targets/index.blade.php
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Manajemen Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.create') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Tambah Target
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('error') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered" id="kpiTargetsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Mekanik</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($targets as $target)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td>{{ $target->user->name }}</td>
|
||||||
|
<td>{{ number_format($target->target_value) }}</td>
|
||||||
|
<td>
|
||||||
|
@if($target->is_active)
|
||||||
|
<span class="badge badge-success">Aktif</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">Nonaktif</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{{ route('kpi.targets.show', $target->id) }}"
|
||||||
|
class="btn btn-sm btn-info" title="Detail">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('kpi.targets.edit', $target->id) }}"
|
||||||
|
class="btn btn-sm btn-warning" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-{{ $target->is_active ? 'warning' : 'success' }}"
|
||||||
|
onclick="toggleStatus({{ $target->id }})"
|
||||||
|
title="{{ $target->is_active ? 'Nonaktifkan' : 'Aktifkan' }}">
|
||||||
|
<i class="fas fa-{{ $target->is_active ? 'pause' : 'play' }}"></i>
|
||||||
|
</button>
|
||||||
|
<form action="{{ route('kpi.targets.destroy', $target->id) }}"
|
||||||
|
method="POST"
|
||||||
|
style="display: inline;"
|
||||||
|
onsubmit="return confirm('Yakin ingin menghapus target ini?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">Tidak ada data target KPI</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($targets->hasPages())
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $targets->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Modal -->
|
||||||
|
<div class="modal fade" id="filterModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Filter Target KPI</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form action="{{ route('kpi.targets.index') }}" method="GET">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mekanik</label>
|
||||||
|
<select name="user_id" class="form-control">
|
||||||
|
<option value="">Semua Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
<option value="{{ $mechanic->id }}"
|
||||||
|
{{ request('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<select name="is_active" class="form-control">
|
||||||
|
<option value="">Semua Status</option>
|
||||||
|
<option value="1" {{ request('is_active') == '1' ? 'selected' : '' }}>Aktif</option>
|
||||||
|
<option value="0" {{ request('is_active') == '0' ? 'selected' : '' }}>Nonaktif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Filter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize DataTable
|
||||||
|
$('#kpiTargetsTable').DataTable({
|
||||||
|
"pageLength": 25,
|
||||||
|
"order": [[0, "asc"]]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto hide alerts after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
$('.alert').fadeOut('slow');
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleStatus(targetId) {
|
||||||
|
if (confirm('Yakin ingin mengubah status target ini?')) {
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("kpi.targets.toggle-status", ":id") }}'.replace(':id', targetId),
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
_token: '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: response.message,
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Terjadi kesalahan saat mengubah status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
193
resources/views/kpi/targets/show.blade.php
Normal file
193
resources/views/kpi/targets/show.blade.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Detail Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.edit', $target->id) }}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td width="150"><strong>Mekanik</strong></td>
|
||||||
|
<td>: {{ $target->user->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Email</strong></td>
|
||||||
|
<td>: {{ $target->user->email }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Dealer</strong></td>
|
||||||
|
<td>: {{ $target->user->dealer->name ?? 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Target Nilai</strong></td>
|
||||||
|
<td>: {{ number_format($target->target_value) }} Pekerjaan</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Status</strong></td>
|
||||||
|
<td>:
|
||||||
|
@if($target->is_active)
|
||||||
|
<span class="badge badge-success">Aktif</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">Nonaktif</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td width="150"><strong>Jenis Target</strong></td>
|
||||||
|
<td>: <span class="badge badge-info">Target Permanen</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Berlaku Sejak</strong></td>
|
||||||
|
<td>: {{ $target->created_at->format('d/m/Y') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Dibuat</strong></td>
|
||||||
|
<td>: {{ $target->created_at->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Terakhir Update</strong></td>
|
||||||
|
<td>: {{ $target->updated_at->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Total Pencapaian</strong></td>
|
||||||
|
<td>: {{ $target->achievements->count() }} Bulan</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($target->description)
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6><strong>Deskripsi:</strong></h6>
|
||||||
|
<p class="text-muted">{{ $target->description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Achievement History -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5><i class="fas fa-chart-line"></i> Riwayat Pencapaian Bulanan</h5>
|
||||||
|
@if($target->achievements->count() > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Periode</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Aktual</th>
|
||||||
|
<th>Pencapaian</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($target->achievements->sortByDesc('year')->sortByDesc('month') as $achievement)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $achievement->getPeriodDisplayName() }}</td>
|
||||||
|
<td>{{ number_format($achievement->target_value) }}</td>
|
||||||
|
<td>{{ number_format($achievement->actual_value) }}</td>
|
||||||
|
<td>{{ number_format($achievement->achievement_percentage, 1) }}%</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ $achievement->status_color }}">
|
||||||
|
@switch($achievement->status)
|
||||||
|
@case('exceeded')
|
||||||
|
Melebihi Target
|
||||||
|
@break
|
||||||
|
@case('good')
|
||||||
|
Baik
|
||||||
|
@break
|
||||||
|
@case('fair')
|
||||||
|
Cukup
|
||||||
|
@break
|
||||||
|
@case('poor')
|
||||||
|
Kurang
|
||||||
|
@break
|
||||||
|
@default
|
||||||
|
Tidak Diketahui
|
||||||
|
@endswitch
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i> Belum ada data pencapaian untuk target ini.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Statistics -->
|
||||||
|
@if($target->achievements->count() > 0)
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5><i class="fas fa-chart-bar"></i> Statistik Pencapaian</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ $target->achievements->count() }}</h4>
|
||||||
|
<small>Total Pencapaian</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-success text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ $target->achievements->where('achievement_percentage', '>=', 100)->count() }}</h4>
|
||||||
|
<small>Target Tercapai</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-info text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ number_format($target->achievements->avg('achievement_percentage'), 1) }}%</h4>
|
||||||
|
<small>Rata-rata Pencapaian</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-warning text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ number_format($target->achievements->max('achievement_percentage'), 1) }}%</h4>
|
||||||
|
<small>Pencapaian Tertinggi</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -118,7 +118,6 @@ License: You must have a valid license purchased only from themeforest(the above
|
|||||||
|
|
||||||
<!--begin::Common Script -->
|
<!--begin::Common Script -->
|
||||||
<script src="{{ asset('js/vendor.js') }}"></script>
|
<script src="{{ asset('js/vendor.js') }}"></script>
|
||||||
<script src="{{ mix('js/app.js') }}"></script>
|
|
||||||
<script src="{{ asset('js/init.js') }}"></script>
|
<script src="{{ asset('js/init.js') }}"></script>
|
||||||
<!--end::Common Scripts -->
|
<!--end::Common Scripts -->
|
||||||
@yield('javascripts')
|
@yield('javascripts')
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ License: You must have a valid license purchased only from themeforest(the above
|
|||||||
|
|
||||||
<!--begin::Global Theme Styles(used by all pages) -->
|
<!--begin::Global Theme Styles(used by all pages) -->
|
||||||
<link href="{{ url('css/app.bundle.min.css') }}" rel="stylesheet" type="text/css" />
|
<link href="{{ url('css/app.bundle.min.css') }}" rel="stylesheet" type="text/css" />
|
||||||
<link href="{{ url('css/saxmono.ttf') }}" rel="stylesheet" type="text/css" />
|
|
||||||
<!--end::Global Theme Styles -->
|
<!--end::Global Theme Styles -->
|
||||||
|
|
||||||
<!--begin::Global Custom Styles(used by all pages) -->
|
<!--begin::Global Custom Styles(used by all pages) -->
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
@if(Gate::check('view', $menus['user.index']) || Gate::check('view', $menus['roleprivileges.index']))
|
@if(Gate::check('view', $menus['user.index']) || Gate::check('view', $menus['roleprivileges.index']))
|
||||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
<i class="fa fa-users" style="margin-right: 8px; font-size: 14px;"></i>
|
|
||||||
<span>Manajemen Pengguna</span>
|
<span>Manajemen Pengguna</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +76,6 @@
|
|||||||
@if(Gate::check('view', $menus['work.index']) || Gate::check('view', $menus['category.index']) || Gate::check('view', $menus['dealer.index']))
|
@if(Gate::check('view', $menus['work.index']) || Gate::check('view', $menus['category.index']) || Gate::check('view', $menus['dealer.index']))
|
||||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
<i class="fa fa-exchange-alt" style="margin-right: 8px; font-size: 14px;"></i>
|
|
||||||
<span>Manajemen Transaksi</span>
|
<span>Manajemen Transaksi</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +113,6 @@
|
|||||||
@if(Gate::check('view', $menus['products.index']) || Gate::check('view', $menus['product_categories.index']) || Gate::check('view', $menus['mutations.index']) || Gate::check('view', $menus['opnames.index']) || Gate::check('view', $menus['stock-audit.index']))
|
@if(Gate::check('view', $menus['products.index']) || Gate::check('view', $menus['product_categories.index']) || Gate::check('view', $menus['mutations.index']) || Gate::check('view', $menus['opnames.index']) || Gate::check('view', $menus['stock-audit.index']))
|
||||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
<i class="fa fa-warehouse" style="margin-right: 8px; font-size: 14px;"></i>
|
|
||||||
<span>Manajemen Gudang</span>
|
<span>Manajemen Gudang</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +168,6 @@
|
|||||||
@if(Gate::check('view', $menus['report.transaction_sa']) || Gate::check('view', $menus['report.transaction']) || Gate::check('view', $menus['report.transaction_dealer']) || Gate::check('view', $menus['work.index']))
|
@if(Gate::check('view', $menus['report.transaction_sa']) || Gate::check('view', $menus['report.transaction']) || Gate::check('view', $menus['report.transaction_dealer']) || Gate::check('view', $menus['work.index']))
|
||||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
<i class="fa fa-chart-bar" style="margin-right: 8px; font-size: 14px;"></i>
|
|
||||||
<span>Laporan</span>
|
<span>Laporan</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,23 +201,42 @@
|
|||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
@can('view', $menus['work.index'])
|
@can('view', $menus['reports.stock-product.index'])
|
||||||
<li class="kt-menu__item" aria-haspopup="true">
|
<li class="kt-menu__item" aria-haspopup="true">
|
||||||
<a href="{{ route('work.index') }}" class="kt-menu__link">
|
<a href="{{ route('reports.stock-product.index') }}" class="kt-menu__link">
|
||||||
<i class="fa fa-cubes" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
<i class="fa fa-cubes" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||||
<span class="kt-menu__link-text">Stok Produk</span>
|
<span class="kt-menu__link-text">Stok Produk</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
@can('view', $menus['work.index'])
|
@can('view', $menus['reports.technician.index'])
|
||||||
<li class="kt-menu__item" aria-haspopup="true">
|
<li class="kt-menu__item" aria-haspopup="true">
|
||||||
<a href="{{ route('work.index') }}" class="kt-menu__link">
|
<a href="{{ route('reports.technician.index') }}" class="kt-menu__link">
|
||||||
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||||
<span class="kt-menu__link-text">Teknisi</span>
|
<span class="kt-menu__link-text">Teknisi</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
{{-- Section Header - Only show if user has access to any submenu --}}
|
||||||
|
@if(Gate::check('view', $menus['kpi.targets.index']))
|
||||||
|
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||||
|
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
<span>KPI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Submenu Items --}}
|
||||||
|
@can('view', $menus['kpi.targets.index'])
|
||||||
|
<li class="kt-menu__item" aria-haspopup="true">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
||||||
|
<i class="fa fa-bullseye" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||||
|
<span class="kt-menu__link-text">Target</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user