Compare commits
89 Commits
main
...
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 | ||
|
|
9b3889ef1f | ||
|
|
fc98479362 | ||
|
|
38def0dc9c | ||
|
|
e5daafc8f0 | ||
|
|
e96ca0a83c | ||
|
|
c3233ea6b2 | ||
|
|
33502e905d | ||
|
|
41ae7da60e | ||
|
|
334b9acd87 | ||
|
|
0de5bec84a | ||
|
|
82f9d7f466 | ||
|
|
e478dc81bb | ||
|
|
22477b6dab | ||
|
|
b803068d0e | ||
|
|
aa233eb793 | ||
|
|
567e4aa5fc | ||
|
|
9cfb566aee | ||
|
|
3fb598ae4d | ||
|
|
e9566d4c8a | ||
|
|
4517f7efcb | ||
|
|
ec8224760e | ||
|
|
ac55ed1b67 | ||
|
|
6625baf7bd | ||
|
|
2f5eff9e63 | ||
|
|
b2bfd666a7 | ||
|
|
680eb2045a | ||
|
|
ca7a0b941e | ||
|
|
e64cf43390 | ||
|
|
bba37c1720 | ||
|
|
520c0e9885 | ||
|
|
2fa60c583a | ||
|
|
b04b8f88cb | ||
|
|
58578532cc | ||
|
|
1a01efb1b5 | ||
|
|
a5e1348436 | ||
|
|
0b211915f1 | ||
|
|
647aa51187 | ||
|
|
9b25a772a6 | ||
|
|
f92655e3e2 | ||
|
|
84fb7ffb52 | ||
|
|
51079aa567 | ||
|
|
1a2ddb59d4 | ||
|
|
d294bb7876 | ||
|
|
ce0a4718e0 | ||
|
|
ff498cd98f | ||
|
|
8305e44c42 | ||
|
|
215792ce63 | ||
|
|
a881779c4f | ||
|
|
6bf8bc4965 | ||
|
|
59e23ae535 |
71
.dockerignore
Executable file
71
.dockerignore
Executable file
@@ -0,0 +1,71 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Laravel
|
||||
/vendor/
|
||||
.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
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Testing
|
||||
/tests/
|
||||
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
|
||||
0
.editorconfig
Normal file → Executable file
0
.editorconfig
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.gitattributes
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.styleci.yml
Normal file → Executable file
0
.styleci.yml
Normal file → Executable file
84
Dockerfile
Executable file
84
Dockerfile
Executable file
@@ -0,0 +1,84 @@
|
||||
FROM php:8.1-fpm
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libcurl4-openssl-dev \
|
||||
pkg-config \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
libfreetype6-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libxpm-dev \
|
||||
libvpx-dev \
|
||||
supervisor \
|
||||
nginx \
|
||||
nodejs \
|
||||
npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-xpm \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
curl \
|
||||
pdo_mysql \
|
||||
mbstring \
|
||||
exif \
|
||||
pcntl \
|
||||
bcmath \
|
||||
gd \
|
||||
zip \
|
||||
dom \
|
||||
xml
|
||||
|
||||
# Install Redis extension
|
||||
RUN pecl install redis \
|
||||
&& docker-php-ext-enable redis
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Copy only composer files first for caching
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
# Install PHP dependencies (cached if lock file unchanged)
|
||||
RUN composer install --optimize-autoloader --no-dev --no-interaction --no-scripts
|
||||
|
||||
# Now copy the full Laravel application code
|
||||
COPY . .
|
||||
|
||||
# Run composer scripts and install Node dependencies
|
||||
RUN composer run-script post-autoload-dump && \
|
||||
npm install && \
|
||||
npm run production && \
|
||||
php artisan storage:link
|
||||
|
||||
# Set proper permissions (for production only do this once)
|
||||
RUN mkdir -p storage/logs \
|
||||
&& mkdir -p storage/framework/{cache,sessions,views} \
|
||||
&& mkdir -p storage/app/public \
|
||||
&& mkdir -p bootstrap/cache \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 775 storage \
|
||||
&& chmod -R 775 bootstrap/cache \
|
||||
&& chmod -R 755 public \
|
||||
&& chmod -R 777 storage/app/public
|
||||
|
||||
# Nginx config
|
||||
COPY ./docker/nginx.conf /etc/nginx/sites-available/default
|
||||
|
||||
# Supervisor config
|
||||
COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Expose web port
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
76
Dockerfile.dev
Executable file
76
Dockerfile.dev
Executable file
@@ -0,0 +1,76 @@
|
||||
FROM php:8.1-fpm
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libcurl4-openssl-dev \
|
||||
pkg-config \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
libfreetype6-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libxpm-dev \
|
||||
libvpx-dev \
|
||||
supervisor \
|
||||
nginx \
|
||||
vim \
|
||||
nano \
|
||||
htop \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-xpm \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
curl \
|
||||
pdo_mysql \
|
||||
mbstring \
|
||||
exif \
|
||||
pcntl \
|
||||
bcmath \
|
||||
gd \
|
||||
zip \
|
||||
dom \
|
||||
xml
|
||||
|
||||
# Install Redis and Xdebug
|
||||
RUN pecl install redis xdebug \
|
||||
&& docker-php-ext-enable redis xdebug
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Copy dependency files first for better caching
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
# Now copy the entire application code (after composer install)
|
||||
COPY . .
|
||||
|
||||
# Install PHP dependencies (with dev)
|
||||
RUN composer install --no-interaction
|
||||
|
||||
# Set ownership and permissions
|
||||
RUN mkdir -p storage/logs \
|
||||
&& mkdir -p storage/framework/{cache,sessions,views} \
|
||||
&& mkdir -p storage/app \
|
||||
&& mkdir -p bootstrap/cache \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 775 storage bootstrap/cache \
|
||||
&& chmod -R 755 public
|
||||
|
||||
# Copy configs
|
||||
COPY ./docker/nginx.dev.conf /etc/nginx/sites-available/default
|
||||
COPY ./docker/supervisord.dev.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY ./docker/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
|
||||
# Expose web port
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
261
README.md
Normal file → Executable file
261
README.md
Normal file → Executable file
@@ -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">
|
||||
<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>
|
||||
Sistem manajemen bengkel yang dibangun dengan Laravel 8 dan menggunakan JavaScript inline untuk performa optimal.
|
||||
|
||||
## 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).
|
||||
- [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).
|
||||
## 📦 Prerequisites
|
||||
|
||||
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/)**
|
||||
- **[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)**
|
||||
3. **Copy environment file**
|
||||
|
||||
## 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
|
||||
|
||||
0
api_bengkel2/git.zip
Normal file → Executable file
0
api_bengkel2/git.zip
Normal file → Executable file
97
app/Console/Commands/CleanMutationsData.php
Normal file
97
app/Console/Commands/CleanMutationsData.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CleanMutationsData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'mutations:clean {--force : Force cleanup without confirmation}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clean mutations data to allow migration rollback';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('This will delete ALL mutations data. Are you sure?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Delete mutations data in proper order (foreign key constraints)
|
||||
$this->info('Cleaning mutations data...');
|
||||
|
||||
// 1. Delete stock logs related to mutations
|
||||
if (Schema::hasTable('stock_logs')) {
|
||||
$deleted = DB::table('stock_logs')
|
||||
->where('source_type', 'App\\Models\\Mutation')
|
||||
->delete();
|
||||
$this->info("Deleted {$deleted} stock logs related to mutations");
|
||||
}
|
||||
|
||||
// 2. Delete mutation details
|
||||
if (Schema::hasTable('mutation_details')) {
|
||||
$deleted = DB::table('mutation_details')->delete();
|
||||
$this->info("Deleted {$deleted} mutation details");
|
||||
}
|
||||
|
||||
// 3. Delete mutations
|
||||
if (Schema::hasTable('mutations')) {
|
||||
$deleted = DB::table('mutations')->delete();
|
||||
$this->info("Deleted {$deleted} mutations");
|
||||
}
|
||||
|
||||
// 4. Reset auto increment
|
||||
if (Schema::hasTable('mutations')) {
|
||||
DB::statement('ALTER TABLE mutations AUTO_INCREMENT = 1');
|
||||
$this->info('Reset mutations auto increment');
|
||||
}
|
||||
|
||||
if (Schema::hasTable('mutation_details')) {
|
||||
DB::statement('ALTER TABLE mutation_details AUTO_INCREMENT = 1');
|
||||
$this->info('Reset mutation_details auto increment');
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info('✅ Mutations data cleaned successfully!');
|
||||
$this->info('You can now rollback and re-run migrations.');
|
||||
|
||||
return 0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
$this->error('❌ Error cleaning mutations data: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
app/Console/Commands/ClearMutationsCommand.php
Executable file
112
app/Console/Commands/ClearMutationsCommand.php
Executable file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Mutation;
|
||||
use App\Models\MutationDetail;
|
||||
|
||||
class ClearMutationsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'mutations:clear {--force : Force the operation without confirmation}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear all mutations and mutation details, then reset auto increment IDs';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Show warning
|
||||
$this->warn('⚠️ WARNING: This will permanently delete ALL mutations and mutation details!');
|
||||
$this->warn('⚠️ This action cannot be undone!');
|
||||
|
||||
// Check for force flag
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('Are you sure you want to continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Show current counts
|
||||
$mutationCount = Mutation::count();
|
||||
$detailCount = MutationDetail::count();
|
||||
$trashedMutationCount = Mutation::onlyTrashed()->count();
|
||||
|
||||
$this->info("Current data:");
|
||||
$this->info("- Mutations: {$mutationCount}");
|
||||
$this->info("- Mutation Details: {$detailCount}");
|
||||
if ($trashedMutationCount > 0) {
|
||||
$this->info("- Soft Deleted Mutations: {$trashedMutationCount}");
|
||||
}
|
||||
|
||||
if ($mutationCount === 0 && $detailCount === 0 && $trashedMutationCount === 0) {
|
||||
$this->info('No data to clear.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('Starting cleanup process...');
|
||||
|
||||
try {
|
||||
// Delete data within transaction
|
||||
DB::beginTransaction();
|
||||
|
||||
// Delete mutation details first (foreign key constraint)
|
||||
$this->info('🗑️ Deleting mutation details...');
|
||||
MutationDetail::query()->delete();
|
||||
|
||||
// Delete mutations (including soft deleted ones)
|
||||
$this->info('🗑️ Deleting mutations...');
|
||||
Mutation::query()->delete();
|
||||
|
||||
// Force delete soft deleted mutations
|
||||
$this->info('🗑️ Force deleting soft deleted mutations...');
|
||||
Mutation::onlyTrashed()->forceDelete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Reset auto increment outside transaction (DDL operations auto-commit)
|
||||
$this->info('🔄 Resetting mutation_details auto increment...');
|
||||
DB::statement('ALTER TABLE mutation_details AUTO_INCREMENT = 1');
|
||||
|
||||
$this->info('🔄 Resetting mutations auto increment...');
|
||||
DB::statement('ALTER TABLE mutations AUTO_INCREMENT = 1');
|
||||
|
||||
$this->info('✅ Successfully cleared all mutations and reset auto increment!');
|
||||
$this->info('📊 Final counts:');
|
||||
$this->info('- Mutations: ' . Mutation::count());
|
||||
$this->info('- Mutation Details: ' . MutationDetail::count());
|
||||
|
||||
return 0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if (DB::transactionLevel() > 0) {
|
||||
DB::rollback();
|
||||
}
|
||||
$this->error('❌ Failed to clear mutations: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
app/Console/Commands/ClearOpnameData.php
Executable file
93
app/Console/Commands/ClearOpnameData.php
Executable file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
|
||||
class ClearOpnameData extends Command
|
||||
{
|
||||
protected $signature = 'opname:clear {--force : Force clear without confirmation}';
|
||||
protected $description = 'Clear all opname-related data including opnames, details, stocks, logs, and reset all IDs to 1';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('This will delete ALL opname data, stocks, stock logs, and reset ALL IDs to 1. This is irreversible! Are you sure?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Starting complete data cleanup...');
|
||||
|
||||
try {
|
||||
// Disable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
// 1. Clear and reset stock logs
|
||||
if (Schema::hasTable('stock_logs')) {
|
||||
DB::table('stock_logs')->truncate();
|
||||
DB::statement('ALTER TABLE stock_logs AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stock_logs table');
|
||||
}
|
||||
|
||||
// 2. Clear and reset stocks
|
||||
if (Schema::hasTable('stocks')) {
|
||||
DB::table('stocks')->truncate();
|
||||
DB::statement('ALTER TABLE stocks AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stocks table');
|
||||
}
|
||||
|
||||
// 3. Clear and reset opname details
|
||||
if (Schema::hasTable('opname_details')) {
|
||||
DB::table('opname_details')->truncate();
|
||||
DB::statement('ALTER TABLE opname_details AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opname_details table');
|
||||
}
|
||||
|
||||
// 4. Clear and reset opnames
|
||||
if (Schema::hasTable('opnames')) {
|
||||
DB::table('opnames')->truncate();
|
||||
DB::statement('ALTER TABLE opnames AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opnames table');
|
||||
}
|
||||
|
||||
// Re-enable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->info('Successfully cleared all data and reset IDs to 1!');
|
||||
$this->info('Cleared tables:');
|
||||
$this->info('- stock_logs');
|
||||
$this->info('- stocks');
|
||||
$this->info('- opname_details');
|
||||
$this->info('- opnames');
|
||||
|
||||
Log::info('Complete data cleared and IDs reset by command', [
|
||||
'user' => auth()->user() ? auth()->user()->id : 'system',
|
||||
'timestamp' => now(),
|
||||
'tables_cleared' => ['stock_logs', 'stocks', 'opname_details', 'opnames']
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Re-enable foreign key checks if they were disabled
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->error('Error clearing data: ' . $e->getMessage());
|
||||
Log::error('Error in ClearOpnameData command: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return 1; // Return error code
|
||||
}
|
||||
|
||||
return 0; // Return success code
|
||||
}
|
||||
}
|
||||
83
app/Console/Commands/SetupStockAuditMenu.php
Normal file
83
app/Console/Commands/SetupStockAuditMenu.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Models\Privilege;
|
||||
|
||||
class SetupStockAuditMenu extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'setup:stock-audit-menu';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Setup Stock Audit menu and privileges';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Setting up Stock Audit menu...');
|
||||
|
||||
// Check if menu already exists
|
||||
$existingMenu = Menu::where('link', 'stock-audit.index')->first();
|
||||
|
||||
if ($existingMenu) {
|
||||
$this->warn('Stock Audit menu already exists!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create Stock Audit menu
|
||||
$menu = Menu::create([
|
||||
'name' => 'Audit Histori Stock',
|
||||
'link' => 'stock-audit.index',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('Stock Audit menu created with ID: ' . $menu->id);
|
||||
|
||||
// Give all roles access to this menu
|
||||
$roles = Role::all();
|
||||
$privilegeCount = 0;
|
||||
|
||||
foreach($roles as $role) {
|
||||
// Check if privilege already exists
|
||||
$existingPrivilege = Privilege::where('role_id', $role->id)
|
||||
->where('menu_id', $menu->id)
|
||||
->first();
|
||||
|
||||
if (!$existingPrivilege) {
|
||||
Privilege::create([
|
||||
'role_id' => $role->id,
|
||||
'menu_id' => $menu->id,
|
||||
'create' => 0, // Stock audit is view-only
|
||||
'update' => 0, // Stock audit is view-only
|
||||
'delete' => 0, // Stock audit is view-only
|
||||
'view' => 1, // Allow viewing
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
$privilegeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Created {$privilegeCount} privileges for Stock Audit menu.");
|
||||
$this->info('Stock Audit menu setup completed successfully!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
4
app/Console/Kernel.php
Normal file → Executable file
4
app/Console/Kernel.php
Normal file → Executable file
@@ -28,5 +28,9 @@ class Kernel extends ConsoleKernel
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
|
||||
$this->commands = [
|
||||
Commands\ClearOpnameData::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
59
app/Enums/MutationStatus.php
Executable file
59
app/Enums/MutationStatus.php
Executable file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum MutationStatus: string
|
||||
{
|
||||
case SENT = 'sent';
|
||||
case RECEIVED = 'received';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
case CANCELLED = 'cancelled';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SENT => 'Terkirim ke Dealer',
|
||||
self::RECEIVED => 'Diterima Dealer',
|
||||
self::APPROVED => 'Disetujui & Stock Dipindahkan',
|
||||
self::REJECTED => 'Ditolak',
|
||||
self::CANCELLED => 'Dibatalkan',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SENT => 'primary',
|
||||
self::RECEIVED => 'info',
|
||||
self::APPROVED => 'brand',
|
||||
self::REJECTED => 'danger',
|
||||
self::CANCELLED => 'secondary',
|
||||
};
|
||||
}
|
||||
|
||||
public function textColorClass(): string
|
||||
{
|
||||
return match($this->color()) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
}
|
||||
|
||||
public static function getOptions(): array
|
||||
{
|
||||
return [
|
||||
self::SENT->value => self::SENT->label(),
|
||||
self::RECEIVED->value => self::RECEIVED->label(),
|
||||
self::APPROVED->value => self::APPROVED->label(),
|
||||
self::REJECTED->value => self::REJECTED->label(),
|
||||
self::CANCELLED->value => self::CANCELLED->label(),
|
||||
];
|
||||
}
|
||||
}
|
||||
55
app/Enums/OpnameStatus.php
Executable file
55
app/Enums/OpnameStatus.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum OpnameStatus: string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case PENDING = 'pending';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DRAFT => 'Draft',
|
||||
self::PENDING => 'Menunggu Persetujuan',
|
||||
self::APPROVED => 'Disetujui',
|
||||
self::REJECTED => 'Ditolak',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DRAFT => 'warning',
|
||||
self::PENDING => 'info',
|
||||
self::APPROVED => 'success',
|
||||
self::REJECTED => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public function textColorClass(): string
|
||||
{
|
||||
return match($this->color()) {
|
||||
'success' => 'text-success',
|
||||
'warning' => 'text-warning',
|
||||
'danger' => 'text-danger',
|
||||
'info' => 'text-info',
|
||||
'primary' => 'text-primary',
|
||||
'brand' => 'text-primary',
|
||||
'secondary' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
}
|
||||
|
||||
public static function getOptions(): array
|
||||
{
|
||||
return [
|
||||
self::DRAFT->value => self::DRAFT->label(),
|
||||
self::PENDING->value => self::PENDING->label(),
|
||||
self::APPROVED->value => self::APPROVED->label(),
|
||||
self::REJECTED->value => self::REJECTED->label(),
|
||||
];
|
||||
}
|
||||
}
|
||||
21
app/Enums/StockChangeType.php
Executable file
21
app/Enums/StockChangeType.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StockChangeType: string
|
||||
{
|
||||
case INCREASE = 'increase';
|
||||
case DECREASE = 'decrease';
|
||||
case ADJUSTMENT = 'adjustment'; // Untuk kasus dimana quantity sama tapi perlu dicatat
|
||||
case NO_CHANGE = 'no_change'; // Untuk kasus dimana quantity sama dan tidak perlu dicatat
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::INCREASE => 'Penambahan',
|
||||
self::DECREASE => 'Pengurangan',
|
||||
self::ADJUSTMENT => 'Penyesuaian',
|
||||
self::NO_CHANGE => 'Tidak Ada Perubahan'
|
||||
};
|
||||
}
|
||||
}
|
||||
0
app/Exceptions/Handler.php
Normal file → Executable file
0
app/Exceptions/Handler.php
Normal file → Executable file
201
app/Exports/ProductStockDealers.php
Normal file
201
app/Exports/ProductStockDealers.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Product;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||
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;
|
||||
|
||||
class ProductStockDealers implements WithMultipleSheets
|
||||
{
|
||||
public function sheets(): array
|
||||
{
|
||||
$sheets = [];
|
||||
$usedNames = [];
|
||||
|
||||
// Get all dealers with their stock data
|
||||
$dealers = Dealer::with(['stocks.product.category'])->get();
|
||||
|
||||
/** @var Dealer $dealer */
|
||||
foreach ($dealers as $dealer) {
|
||||
$dealerSheet = new DealerStockSheet($dealer);
|
||||
$sheetTitle = $dealerSheet->title();
|
||||
|
||||
// Handle duplicate sheet names
|
||||
$originalTitle = $sheetTitle;
|
||||
$counter = 1;
|
||||
while (in_array($sheetTitle, $usedNames)) {
|
||||
$sheetTitle = substr($originalTitle, 0, 28) . '_' . $counter;
|
||||
$counter++;
|
||||
}
|
||||
$usedNames[] = $sheetTitle;
|
||||
|
||||
// Set the unique title
|
||||
$dealerSheet->setUniqueTitle($sheetTitle);
|
||||
$sheets[] = $dealerSheet;
|
||||
}
|
||||
|
||||
return $sheets;
|
||||
}
|
||||
}
|
||||
|
||||
class DealerStockSheet implements FromCollection, WithTitle, WithHeadings, WithStyles, WithColumnWidths
|
||||
{
|
||||
protected $dealer;
|
||||
protected $uniqueTitle;
|
||||
|
||||
public function __construct(Dealer $dealer)
|
||||
{
|
||||
$this->dealer = $dealer;
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
// Get all products with stock for this dealer
|
||||
$stocks = $this->dealer->stocks()
|
||||
->with(['product.category'])
|
||||
->whereHas('product', function($query) {
|
||||
$query->where('active', true);
|
||||
})
|
||||
->get();
|
||||
|
||||
$data = collect();
|
||||
$no = 1;
|
||||
|
||||
foreach ($stocks as $stock) {
|
||||
$product = $stock->product;
|
||||
$data->push([
|
||||
'no' => $no++,
|
||||
'kode_produk' => $product->code,
|
||||
'nama_produk' => $product->name,
|
||||
'kategori' => $product->category ? $product->category->name : '-',
|
||||
'satuan' => $product->unit ?? '-',
|
||||
'stok' => number_format($stock->quantity, 2)
|
||||
]);
|
||||
}
|
||||
|
||||
// If no stock, add empty row
|
||||
if ($data->isEmpty()) {
|
||||
$data->push([
|
||||
'no' => '-',
|
||||
'kode_produk' => '-',
|
||||
'nama_produk' => 'Tidak ada stok produk',
|
||||
'kategori' => '-',
|
||||
'satuan' => '-',
|
||||
'stok' => '0'
|
||||
]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function setUniqueTitle(string $title): void
|
||||
{
|
||||
$this->uniqueTitle = $title;
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
if (isset($this->uniqueTitle)) {
|
||||
return $this->uniqueTitle;
|
||||
}
|
||||
|
||||
// Clean dealer name for sheet title (remove invalid characters and handle edge cases)
|
||||
$cleanName = $this->dealer->name;
|
||||
|
||||
// Remove parentheses and their contents
|
||||
$cleanName = preg_replace('/\([^)]*\)/', '', $cleanName);
|
||||
|
||||
// Remove dots, commas, and other special characters
|
||||
$cleanName = preg_replace('/[^A-Za-z0-9\-_ ]/', '', $cleanName);
|
||||
|
||||
// Clean up multiple spaces and trim
|
||||
$cleanName = preg_replace('/\s+/', ' ', trim($cleanName));
|
||||
|
||||
// If name is empty after cleaning, use dealer ID
|
||||
if (empty($cleanName)) {
|
||||
$cleanName = 'Dealer_' . $this->dealer->id;
|
||||
}
|
||||
|
||||
// Limit to 31 characters and ensure no leading/trailing spaces
|
||||
$cleanName = trim(substr($cleanName, 0, 31));
|
||||
|
||||
// Ensure it doesn't end with a space (which can cause Excel issues)
|
||||
return rtrim($cleanName);
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'No',
|
||||
'Kode Produk',
|
||||
'Nama Produk',
|
||||
'Kategori',
|
||||
'Satuan',
|
||||
'Stok'
|
||||
];
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet)
|
||||
{
|
||||
// Add dealer info at the top first
|
||||
$sheet->insertNewRowBefore(1, 2);
|
||||
$sheet->setCellValue('A1', 'STOK PRODUK DEALER: ' . strtoupper($this->dealer->name));
|
||||
$sheet->setCellValue('A2', 'Tanggal Export: ' . now()->format('d/m/Y H:i:s'));
|
||||
|
||||
// Merge cells for dealer info
|
||||
$sheet->mergeCells('A1:F1');
|
||||
$sheet->mergeCells('A2:F2');
|
||||
|
||||
$lastRow = $sheet->getHighestRow();
|
||||
|
||||
// Style dealer info
|
||||
$sheet->getStyle('A1:A2')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 12],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER]
|
||||
]);
|
||||
|
||||
// Style headers (row 3 after inserting 2 rows)
|
||||
$sheet->getStyle('A3:F3')->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '4472C4']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]]
|
||||
]);
|
||||
|
||||
// Style data rows if they exist
|
||||
if ($lastRow > 3) {
|
||||
$sheet->getStyle('A4:F' . $lastRow)->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'CCCCCC']]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER]
|
||||
]);
|
||||
|
||||
// Center align specific columns
|
||||
$sheet->getStyle('A4:A' . $lastRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle('E4:F' . $lastRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
return $sheet;
|
||||
}
|
||||
|
||||
public function columnWidths(): array
|
||||
{
|
||||
return [
|
||||
'A' => 8, // No
|
||||
'B' => 15, // Kode Produk
|
||||
'C' => 30, // Nama Produk
|
||||
'D' => 20, // Kategori
|
||||
'E' => 12, // Satuan
|
||||
'F' => 12 // Stok
|
||||
];
|
||||
}
|
||||
}
|
||||
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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
0
app/Exports/TransactionDealerExport.php
Normal file → Executable file
0
app/Exports/TransactionDealerExport.php
Normal file → Executable file
0
app/Exports/TransactionExport.php
Normal file → Executable file
0
app/Exports/TransactionExport.php
Normal file → Executable file
0
app/Exports/TransactionSaExport.php
Normal file → Executable file
0
app/Exports/TransactionSaExport.php
Normal file → Executable file
130
app/Http/Controllers/AdminController.php
Normal file → Executable file
130
app/Http/Controllers/AdminController.php
Normal file → Executable file
@@ -4,10 +4,12 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Models\Work;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
@@ -37,9 +39,22 @@ class AdminController extends Controller
|
||||
$month = $request->month;
|
||||
$dealer = $request->dealer;
|
||||
$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;
|
||||
// dd($ajax_url);
|
||||
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");
|
||||
$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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
$dealer_work_trx = DB::statement($sql);
|
||||
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
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\"))");
|
||||
}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 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($dealer_datas->count() > 0) {
|
||||
$allowedDealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
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::select(DB::raw("EXECUTE stmt"));
|
||||
DB::statement('DEALLOCATE PREPARE stmt');
|
||||
// DD($dealer_work_trx);
|
||||
$theads = ['DEALER'];
|
||||
$dealer_names = [];
|
||||
$dealer_trx = [];
|
||||
@@ -118,7 +163,6 @@ class AdminController extends Controller
|
||||
|
||||
$dealer_names[] = $dealer_work->DEALER;
|
||||
}
|
||||
// dd($dealer_trx);
|
||||
$dealer_trx = array_values($dealer_trx);
|
||||
$dealer = $request->dealer;
|
||||
$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 = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && $year == date('Y')) {
|
||||
// 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{
|
||||
$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 = [];
|
||||
@@ -143,6 +189,11 @@ class AdminController extends Controller
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$prev_month = $prev_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');
|
||||
@@ -160,6 +211,36 @@ class AdminController extends Controller
|
||||
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) {
|
||||
$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)) {
|
||||
@@ -227,13 +308,14 @@ class AdminController extends Controller
|
||||
foreach($works as $work1) {
|
||||
$prev_mth_start = date('Y-m-d', strtotime(date('Y-'. $request->month .'-1')." -1 month"));
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && date('Y') == date('Y')) {
|
||||
// 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{
|
||||
$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');
|
||||
|
||||
if(array_key_exists($work1->id, $prev_month_trxs_total)) {
|
||||
@@ -348,10 +430,12 @@ class AdminController extends Controller
|
||||
foreach($works as $work1) {
|
||||
$prev_mth_start = date('Y-m-d', strtotime(date($request->year.'-'. $request->month .'-1')." -1 month"));
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && $request->year == date('Y')) {
|
||||
// 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{
|
||||
$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');
|
||||
@@ -440,16 +524,12 @@ class AdminController extends Controller
|
||||
|
||||
// $month_trxs_total = array_values($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_yesterday_month_trxs_total = [];
|
||||
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_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);
|
||||
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
||||
$totals = [];
|
||||
|
||||
8
app/Http/Controllers/ApiController.php
Normal file → Executable file
8
app/Http/Controllers/ApiController.php
Normal file → Executable file
@@ -93,7 +93,6 @@ class ApiController extends Controller
|
||||
$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');
|
||||
|
||||
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_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);
|
||||
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
|
||||
|
||||
@@ -287,7 +285,11 @@ class ApiController extends Controller
|
||||
|
||||
public function logout()
|
||||
{
|
||||
Auth::user()->tokens()->delete();
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth('sanctum')->user();
|
||||
if ($user) {
|
||||
$user->tokens()->delete();
|
||||
}
|
||||
return response()->json([
|
||||
'message' => 'Logout success',
|
||||
'status' => true,
|
||||
|
||||
0
app/Http/Controllers/Auth/ConfirmPasswordController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/ConfirmPasswordController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file → Executable file
37
app/Http/Controllers/Auth/LoginController.php
Normal file → Executable file
37
app/Http/Controllers/Auth/LoginController.php
Normal file → Executable file
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
@@ -50,11 +51,39 @@ class LoginController extends Controller
|
||||
*/
|
||||
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 (!$roleId) {
|
||||
// User has no role, redirect to default
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
if ($user != null) {
|
||||
return redirect()->route('dashboard');
|
||||
}else{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
0
app/Http/Controllers/Auth/RegisterController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/RegisterController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/ResetPasswordController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/ResetPasswordController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/VerificationController.php
Normal file → Executable file
0
app/Http/Controllers/Auth/VerificationController.php
Normal file → Executable file
12
app/Http/Controllers/CategoryController.php
Normal file → Executable file
12
app/Http/Controllers/CategoryController.php
Normal file → Executable file
@@ -25,16 +25,16 @@ class CategoryController extends Controller
|
||||
$data = Category::all();
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editCategory'. $row->id .'" data-url="'. route('category.edit', $row->id) .'" data-action="'. route('category.update', $row->id) .'" onclick="editCategory('. $row->id .')"> Edit </button>';
|
||||
}
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('category.destroy', $row->id) .'" id="destroyCategory'. $row->id .'" onclick="destroyCategory('. $row->id .')"> Hapus </button>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editCategory'. $row->id .'" data-url="'. route('category.edit', $row->id) .'" data-action="'. route('category.update', $row->id) .'" onclick="editCategory('. $row->id .')"> Edit </button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
|
||||
0
app/Http/Controllers/Controller.php
Normal file → Executable file
0
app/Http/Controllers/Controller.php
Normal file → Executable file
17
app/Http/Controllers/DealerController.php
Normal file → Executable file
17
app/Http/Controllers/DealerController.php
Normal file → Executable file
@@ -27,27 +27,28 @@ class DealerController extends Controller
|
||||
$data = Dealer::leftJoin('users as u', 'u.id', '=', 'pic')->select('u.name as pic_name', 'dealers.*');
|
||||
return Datatables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex">';
|
||||
if($row->pic != null) {
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>';
|
||||
}
|
||||
}else{
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('dealer.destroy', $row->id) .'" id="destroyDealer'. $row->id .'" onclick="destroyDealer('. $row->id .')"> Hapus </button>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editDealer'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" data-action="'. route('dealer.update', $row->id) .'" onclick="editDealer('. $row->id .')"> Edit </button>
|
||||
<button class="btn btn-success btn-sm btn-bold" data-action="'. route('dealer.picstore', $row->id) .'" id="addPic'. $row->id .'" data-url="'. route('dealer.edit', $row->id) .'" onclick="addPic('. $row->id .')"> Tambahkan PIC </button>';
|
||||
}
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
|
||||
0
app/Http/Controllers/HomeController.php
Normal file → Executable file
0
app/Http/Controllers/HomeController.php
Normal file → Executable file
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
|
||||
]);
|
||||
}
|
||||
}
|
||||
498
app/Http/Controllers/ReportController.php
Normal file → Executable file
498
app/Http/Controllers/ReportController.php
Normal file → Executable file
@@ -16,6 +16,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Models\Role;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
@@ -36,13 +37,41 @@ class ReportController extends Controller
|
||||
$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)) {
|
||||
$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') {
|
||||
$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') {
|
||||
@@ -52,8 +81,27 @@ class ReportController extends Controller
|
||||
return $q;
|
||||
})->orderBy('id', 'ASC')->get();
|
||||
|
||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||
$sa_datas = 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::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;
|
||||
$dealer = $request->dealer;
|
||||
$month = $request->month;
|
||||
@@ -82,8 +130,27 @@ class ReportController extends Controller
|
||||
$request['sa'] = 'all';
|
||||
}
|
||||
|
||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||
$sa_datas = 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::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;
|
||||
$dealer = $request->dealer;
|
||||
@@ -126,11 +193,40 @@ class ReportController extends Controller
|
||||
$sa = $request->sa;
|
||||
$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");
|
||||
$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') {
|
||||
$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') {
|
||||
@@ -139,17 +235,35 @@ class ReportController extends Controller
|
||||
|
||||
$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->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{
|
||||
$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{
|
||||
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{
|
||||
$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');
|
||||
}
|
||||
|
||||
$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)) {
|
||||
$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') {
|
||||
$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') {
|
||||
@@ -232,7 +374,27 @@ class ReportController extends Controller
|
||||
}
|
||||
})->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 = [];
|
||||
foreach($sas as $key => $sa) {
|
||||
@@ -243,9 +405,23 @@ class ReportController extends Controller
|
||||
if(isset($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') {
|
||||
$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') {
|
||||
@@ -296,40 +472,80 @@ class ReportController extends Controller
|
||||
|
||||
$sa_names = json_encode($sa_names);
|
||||
$trx_data = json_encode(array_values($trx_data));
|
||||
// dd($trx_data);
|
||||
$work_count = count($works);
|
||||
$month = $request->month;
|
||||
$dealer_id = $request->dealer;
|
||||
$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) {
|
||||
$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)) {
|
||||
$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') {
|
||||
$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') {
|
||||
$q = $q->where('user_sa_id', '=', $request->sa_filter);
|
||||
$q->where('user_sa_id', '=', $request->sa_filter);
|
||||
}
|
||||
|
||||
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(DB::raw('MONTH(t.date)'), '=', DB::raw($request->month));
|
||||
$q->on(DB::raw('YEAR(t.date)'), '=', DB::raw(date('Y')));
|
||||
$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') {
|
||||
$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') {
|
||||
$q->on('t.user_sa_id', '=', DB::raw($request->sa_filter));
|
||||
@@ -351,13 +567,41 @@ class ReportController extends Controller
|
||||
$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)) {
|
||||
$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') {
|
||||
$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');
|
||||
}
|
||||
|
||||
$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;
|
||||
$month = $request->month;
|
||||
$dealer = $request->dealer;
|
||||
$dealer_datas = Dealer::all();
|
||||
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
||||
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();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$sas = User::where('role_id', 4)->get();
|
||||
$mechanics = User::where('role_id', 3)->get();
|
||||
$dealers = Dealer::all();
|
||||
$current_user = Auth::user();
|
||||
$current_role = Role::with(['dealers' => function($query) {
|
||||
$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();
|
||||
|
||||
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');
|
||||
|
||||
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')
|
||||
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_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('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
||||
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
|
||||
->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)) {
|
||||
$data->where('transactions.date', '>=', $request->date_start);
|
||||
@@ -434,29 +749,86 @@ class ReportController extends Controller
|
||||
}
|
||||
|
||||
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');
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
if($row->status == 1) {
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= ' <button class="btn btn-danger btn-sm btn-bold" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
|
||||
$btn = '<div class="d-flex justify-content-center align-items-center flex-wrap">';
|
||||
|
||||
// Jika status closed
|
||||
if ($row->status == 1) {
|
||||
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>';
|
||||
}
|
||||
$btn .= '<span class="badge badge-success">Closed</span>';
|
||||
}else{
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
|
||||
|
||||
// Badge Closed rapi
|
||||
$btn .= '<span class="btn btn-success btn-sm font-weight-bold px-3 py-2 mr-2 mt-2 disabled"
|
||||
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(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-info btn-sm btn-bold" 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>
|
||||
<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>';
|
||||
|
||||
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>';
|
||||
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
@@ -562,4 +934,34 @@ class ReportController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
38
app/Http/Controllers/RolePrivilegeController.php
Normal file → Executable file
38
app/Http/Controllers/RolePrivilegeController.php
Normal file → Executable file
@@ -6,6 +6,7 @@ use App\Models\Menu;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Dealer;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
@@ -14,10 +15,11 @@ class RolePrivilegeController extends Controller
|
||||
public function index() {
|
||||
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
$roles = Role::all();
|
||||
$roles = Role::with('dealers')->get();
|
||||
$menus = Menu::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) {
|
||||
@@ -117,4 +119,36 @@ class RolePrivilegeController extends Controller
|
||||
User::where('role_id', $id)->update(['role_id' => 0]);
|
||||
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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
797
app/Http/Controllers/TransactionController.php
Normal file → Executable file
797
app/Http/Controllers/TransactionController.php
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
app/Http/Controllers/UserController.php
Normal file → Executable file
12
app/Http/Controllers/UserController.php
Normal file → Executable file
@@ -24,16 +24,16 @@ class UserController extends Controller
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold mr-2" id="editUser'. $row->id .'" data-url="'. route('user.edit', $row->id) .'" data-action="'. route('user.update', $row->id) .'" onclick="editUser('. $row->id .')"> Edit </button>';
|
||||
}
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('user.destroy', $row->id) .'" id="destroyUser'. $row->id .'" onclick="destroyUser('. $row->id .')"> Hapus </button>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editUser'. $row->id .'" data-url="'. route('user.edit', $row->id) .'" data-action="'. route('user.update', $row->id) .'" onclick="editUser('. $row->id .')"> Edit </button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
|
||||
553
app/Http/Controllers/WarehouseManagement/MutationsController.php
Executable file
553
app/Http/Controllers/WarehouseManagement/MutationsController.php
Executable file
@@ -0,0 +1,553 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Mutation;
|
||||
use App\Models\MutationDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Dealer;
|
||||
use App\Enums\MutationStatus;
|
||||
use App\Models\Menu;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class MutationsController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','mutations.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
$dealers = Dealer::all();
|
||||
|
||||
if ($request->ajax()) {
|
||||
// Use a more specific query to avoid join conflicts
|
||||
$data = Mutation::query()
|
||||
->with(['fromDealer', 'toDealer', 'requestedBy.role', 'approvedBy.role', 'receivedBy.role'])
|
||||
->select(['mutations.*']);
|
||||
|
||||
// Filter berdasarkan dealer jika user bukan admin
|
||||
if (auth()->user()->dealer_id) {
|
||||
$data->where(function($query) {
|
||||
$query->where('from_dealer_id', auth()->user()->dealer_id)
|
||||
->orWhere('to_dealer_id', auth()->user()->dealer_id);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter berdasarkan dealer yang dipilih
|
||||
if ($request->filled('dealer_filter')) {
|
||||
$data->where(function($query) use ($request) {
|
||||
$query->where('from_dealer_id', $request->dealer_filter)
|
||||
->orWhere('to_dealer_id', $request->dealer_filter);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter berdasarkan tanggal
|
||||
if ($request->filled('date_from')) {
|
||||
try {
|
||||
$dateFrom = \Carbon\Carbon::parse($request->date_from)->format('Y-m-d');
|
||||
$data->whereDate('mutations.created_at', '>=', $dateFrom);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to original format
|
||||
$data->whereDate('mutations.created_at', '>=', $request->date_from);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('date_to')) {
|
||||
try {
|
||||
$dateTo = \Carbon\Carbon::parse($request->date_to)->format('Y-m-d');
|
||||
$data->whereDate('mutations.created_at', '<=', $dateTo);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to original format
|
||||
$data->whereDate('mutations.created_at', '<=', $request->date_to);
|
||||
}
|
||||
}
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('mutation_number', function($row) {
|
||||
return $row->mutation_number;
|
||||
})
|
||||
->addColumn('from_dealer', function($row) {
|
||||
return $row->fromDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('to_dealer', function($row) {
|
||||
return $row->toDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('requested_by', function($row) {
|
||||
return $row->requestedBy->name ?? '-';
|
||||
})
|
||||
->addColumn('status', function($row) {
|
||||
$status = $row->status instanceof MutationStatus ? $row->status : MutationStatus::from($row->status);
|
||||
$textColorClass = $status->textColorClass();
|
||||
$label = $status->label();
|
||||
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||
})
|
||||
->addColumn('total_items', function($row) {
|
||||
return number_format($row->total_items, 0);
|
||||
})
|
||||
->addColumn('created_at', function($row) {
|
||||
return $row->created_at->format('d M Y, H:i');
|
||||
})
|
||||
->addColumn('action', function($row) {
|
||||
return view('warehouse_management.mutations._action', compact('row'))->render();
|
||||
})
|
||||
// Enhanced filtering
|
||||
->filterColumn('mutation_number', function($query, $keyword) {
|
||||
$query->where('mutations.mutation_number', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('from_dealer', function($query, $keyword) {
|
||||
$query->whereHas('fromDealer', function($q) use ($keyword) {
|
||||
$q->where('name', 'like', "%{$keyword}%");
|
||||
});
|
||||
})
|
||||
->filterColumn('to_dealer', function($query, $keyword) {
|
||||
$query->whereHas('toDealer', function($q) use ($keyword) {
|
||||
$q->where('name', 'like', "%{$keyword}%");
|
||||
});
|
||||
})
|
||||
->filterColumn('requested_by', function($query, $keyword) {
|
||||
$query->whereHas('requestedBy', function($q) use ($keyword) {
|
||||
$q->where('name', 'like', "%{$keyword}%");
|
||||
});
|
||||
})
|
||||
->filterColumn('status', function($query, $keyword) {
|
||||
$query->where('mutations.status', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('created_at', function($query, $keyword) {
|
||||
$query->whereDate('mutations.created_at', 'like', "%{$keyword}%");
|
||||
})
|
||||
// Enhanced ordering - avoid join conflicts by using subqueries
|
||||
->orderColumn('mutation_number', function($query, $order) {
|
||||
$query->orderBy('mutations.mutation_number', $order);
|
||||
})
|
||||
->orderColumn('from_dealer', function($query, $order) {
|
||||
$query->orderBy(
|
||||
DB::raw('(SELECT name FROM dealers WHERE dealers.id = mutations.from_dealer_id)'),
|
||||
$order
|
||||
);
|
||||
})
|
||||
->orderColumn('to_dealer', function($query, $order) {
|
||||
$query->orderBy(
|
||||
DB::raw('(SELECT name FROM dealers WHERE dealers.id = mutations.to_dealer_id)'),
|
||||
$order
|
||||
);
|
||||
})
|
||||
->orderColumn('requested_by', function($query, $order) {
|
||||
$query->orderBy(
|
||||
DB::raw('(SELECT name FROM users WHERE users.id = mutations.requested_by)'),
|
||||
$order
|
||||
);
|
||||
})
|
||||
->orderColumn('total_items', function($query, $order) {
|
||||
$query->orderBy(
|
||||
DB::raw('(SELECT SUM(quantity_requested) FROM mutation_details WHERE mutation_details.mutation_id = mutations.id)'),
|
||||
$order
|
||||
);
|
||||
})
|
||||
->orderColumn('status', function($query, $order) {
|
||||
$query->orderBy('mutations.status', $order);
|
||||
})
|
||||
->orderColumn('created_at', function($query, $order) {
|
||||
$query->orderBy('mutations.created_at', $order);
|
||||
})
|
||||
->rawColumns(['status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('warehouse_management.mutations.index', compact('menu', 'dealers'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$menu = Menu::where('link','mutations.create')->first();
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::with('stocks')->get();
|
||||
|
||||
return view('warehouse_management.mutations.create', compact('menu', 'dealers', 'products'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'from_dealer_id' => 'required|exists:dealers,id',
|
||||
'to_dealer_id' => 'required|exists:dealers,id|different:from_dealer_id',
|
||||
'shipping_notes' => 'nullable|string',
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.product_id' => 'required|exists:products,id',
|
||||
'products.*.quantity_requested' => 'required|numeric|min:0.01'
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Buat mutation record dengan status SENT (langsung terkirim ke dealer tujuan)
|
||||
$mutation = Mutation::create([
|
||||
'from_dealer_id' => $request->from_dealer_id,
|
||||
'to_dealer_id' => $request->to_dealer_id,
|
||||
'status' => MutationStatus::SENT,
|
||||
'requested_by' => auth()->id(),
|
||||
'shipping_notes' => $request->shipping_notes
|
||||
]);
|
||||
|
||||
// Buat mutation details
|
||||
foreach ($request->products as $productData) {
|
||||
MutationDetail::create([
|
||||
'mutation_id' => $mutation->id,
|
||||
'product_id' => $productData['product_id'],
|
||||
'quantity_requested' => $productData['quantity_requested']
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Check if request came from transaction page
|
||||
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
|
||||
return redirect()->back()
|
||||
->with('success', 'Mutasi berhasil dibuat dan terkirim ke dealer tujuan');
|
||||
}
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil dibuat dan terkirim ke dealer tujuan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return back()->withErrors(['error' => 'Gagal membuat mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(Mutation $mutation)
|
||||
{
|
||||
$mutation->load([
|
||||
'fromDealer',
|
||||
'toDealer',
|
||||
'requestedBy.role',
|
||||
'approvedBy.role',
|
||||
'receivedBy.role',
|
||||
'rejectedBy.role',
|
||||
'cancelledBy.role',
|
||||
'mutationDetails.product'
|
||||
]);
|
||||
|
||||
return view('warehouse_management.mutations.show', compact('mutation'));
|
||||
}
|
||||
|
||||
public function receive(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'reception_notes' => 'nullable|string',
|
||||
'products' => 'required|array',
|
||||
'products.*.quantity_approved' => 'required|numeric|min:0',
|
||||
'products.*.notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeReceived()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat diterima dalam status saat ini']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Update product details dengan quantity_approved dan notes
|
||||
if ($request->products) {
|
||||
foreach ($request->products as $detailId => $productData) {
|
||||
$updateData = [];
|
||||
|
||||
// Set quantity_approved
|
||||
if (isset($productData['quantity_approved'])) {
|
||||
$updateData['quantity_approved'] = $productData['quantity_approved'];
|
||||
}
|
||||
|
||||
// Set notes jika ada
|
||||
if (isset($productData['notes']) && !empty($productData['notes'])) {
|
||||
$updateData['notes'] = $productData['notes'];
|
||||
}
|
||||
|
||||
if (!empty($updateData)) {
|
||||
MutationDetail::where('id', $detailId)
|
||||
->where('mutation_id', $mutation->id)
|
||||
->update($updateData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Receive mutation with reception notes
|
||||
$mutation->receive(auth()->id(), $request->reception_notes);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Check user role and redirect accordingly
|
||||
if (!auth()->user()->dealer_id) {
|
||||
// Users without dealer_id are likely admin, redirect to mutations index
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil diterima dan siap untuk disetujui. Stock akan dipindahkan setelah disetujui.');
|
||||
} else {
|
||||
// Dealer users redirect back to transaction page
|
||||
return redirect()->route('transaction')
|
||||
->with('success', 'Mutasi berhasil diterima. Silakan setujui mutasi ini untuk memindahkan stock.')
|
||||
->with('active_tab', 'penerimaan');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return back()->withErrors(['error' => 'Gagal menerima mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function approve(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'approval_notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeApproved()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat disetujui dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
// Approve mutation (stock will move automatically)
|
||||
$mutation->approve(auth()->id(), $request->approval_notes);
|
||||
|
||||
// Check user role and redirect accordingly
|
||||
if (!auth()->user()->dealer_id) {
|
||||
// Admin users redirect to mutations index
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan');
|
||||
} else {
|
||||
// Dealer users
|
||||
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
|
||||
return redirect()->route('transaction')
|
||||
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan')
|
||||
->with('active_tab', 'penerimaan');
|
||||
} else {
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal menyetujui mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function reject(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'rejection_reason' => 'required|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeApproved()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat ditolak dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->reject(auth()->id(), $request->rejection_reason);
|
||||
|
||||
// Check user role and redirect accordingly
|
||||
if (!auth()->user()->dealer_id) {
|
||||
// Admin users redirect to mutations index
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil ditolak');
|
||||
} else {
|
||||
// Dealer users
|
||||
if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) {
|
||||
return redirect()->route('transaction')
|
||||
->with('success', 'Mutasi berhasil ditolak')
|
||||
->with('active_tab', 'penerimaan');
|
||||
} else {
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil ditolak');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal menolak mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete method removed - Stock moves automatically after approval
|
||||
|
||||
public function cancel(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'cancellation_reason' => 'nullable|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeCancelled()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat dibatalkan dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->cancel(auth()->id(), $request->cancellation_reason);
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil dibatalkan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal membatalkan mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// API untuk mendapatkan stock produk di dealer tertentu
|
||||
public function getProductStock(Request $request)
|
||||
{
|
||||
$dealerId = $request->dealer_id;
|
||||
$productId = $request->product_id;
|
||||
|
||||
$product = Product::findOrFail($productId);
|
||||
$stock = $product->getStockByDealer($dealerId);
|
||||
|
||||
return response()->json([
|
||||
'product_name' => $product->name,
|
||||
'current_stock' => $stock
|
||||
]);
|
||||
}
|
||||
|
||||
// API untuk mendapatkan mutasi yang perlu diterima oleh dealer
|
||||
public function getPendingMutations(Request $request)
|
||||
{
|
||||
$dealerId = $request->dealer_id;
|
||||
|
||||
// Get mutations that need action from this dealer:
|
||||
// 1. 'sent' status where this dealer is the recipient (need to receive)
|
||||
// 2. 'received' status where this dealer is the recipient (show as waiting for admin approval)
|
||||
// 3. 'approved' status where this dealer is the recipient (show as completed)
|
||||
// 4. 'rejected' status where this dealer is the recipient (show as rejected)
|
||||
$data = Mutation::with(['fromDealer', 'toDealer', 'requestedBy.role'])
|
||||
->where(function($query) use ($dealerId) {
|
||||
// Mutations sent to this dealer that need to be received
|
||||
$query->where('to_dealer_id', $dealerId)
|
||||
->where('status', 'sent');
|
||||
// OR mutations received by this dealer (waiting for admin approval)
|
||||
$query->orWhere(function($subQuery) use ($dealerId) {
|
||||
$subQuery->where('to_dealer_id', $dealerId)
|
||||
->where('status', 'received');
|
||||
});
|
||||
// OR mutations approved/rejected for this dealer (historical data)
|
||||
$query->orWhere(function($subQuery) use ($dealerId) {
|
||||
$subQuery->where('to_dealer_id', $dealerId)
|
||||
->whereIn('status', ['approved', 'rejected']);
|
||||
});
|
||||
})
|
||||
->orderBy('mutations.id', 'desc'); // Default order by ID desc
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('mutation_number', function($row) {
|
||||
return $row->mutation_number;
|
||||
})
|
||||
->addColumn('from_dealer', function($row) {
|
||||
return $row->fromDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('to_dealer', function($row) {
|
||||
return $row->toDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('status', function($row) {
|
||||
$status = $row->status instanceof MutationStatus ? $row->status : MutationStatus::from($row->status);
|
||||
$textColorClass = $status->textColorClass();
|
||||
$label = $status->label();
|
||||
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||
})
|
||||
->addColumn('total_items', function($row) {
|
||||
return number_format($row->total_items, 0);
|
||||
})
|
||||
->addColumn('created_at', function($row) {
|
||||
return $row->created_at->format('d M Y, H:i');
|
||||
})
|
||||
->addColumn('action', function($row) use ($dealerId) {
|
||||
$buttons = '';
|
||||
|
||||
if ($row->status->value === 'sent' && $row->to_dealer_id == $dealerId) {
|
||||
// For sent mutations where current dealer is recipient - show detail button for receiving
|
||||
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
|
||||
Detail & Terima
|
||||
</button>';
|
||||
} elseif ($row->status->value === 'received' && $row->to_dealer_id == $dealerId) {
|
||||
// For received mutations where current dealer is recipient - only show detail (approval is admin only)
|
||||
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
|
||||
Detail
|
||||
</button>';
|
||||
$buttons .= '<div class="mt-1"><small class="text-muted">Menunggu persetujuan admin</small></div>';
|
||||
} elseif ($row->status->value === 'approved' && $row->to_dealer_id == $dealerId) {
|
||||
// For approved mutations - show detail only
|
||||
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
|
||||
Detail
|
||||
</button>';
|
||||
$buttons .= '<div class="mt-1"><small class="text-success">Disetujui admin</small></div>';
|
||||
} elseif ($row->status->value === 'rejected' && $row->to_dealer_id == $dealerId) {
|
||||
// For rejected mutations - show detail only
|
||||
$buttons .= '<button type="button" class="btn btn-info btn-sm btn-detail" onclick="showMutationDetail('.$row->id.')">
|
||||
Detail
|
||||
</button>';
|
||||
$buttons .= '<div class="mt-1"><small class="text-danger">Ditolak admin</small></div>';
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
})
|
||||
->rawColumns(['status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
// API untuk mendapatkan detail mutasi
|
||||
public function getDetail(Mutation $mutation)
|
||||
{
|
||||
try {
|
||||
$mutation->load([
|
||||
'fromDealer',
|
||||
'toDealer',
|
||||
'requestedBy.role',
|
||||
'approvedBy.role',
|
||||
'receivedBy.role',
|
||||
'rejectedBy.role',
|
||||
'cancelledBy.role',
|
||||
'mutationDetails.product'
|
||||
]);
|
||||
|
||||
// Format created_at
|
||||
$mutation->created_at_formatted = $mutation->created_at->format('d M Y, H:i');
|
||||
|
||||
// Add status color and label
|
||||
$mutation->status_color = $mutation->status_color;
|
||||
$mutation->status_label = $mutation->status_label;
|
||||
|
||||
// Check if can be received
|
||||
$mutation->can_be_received = $mutation->canBeReceived();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $mutation
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat detail mutasi: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function print($id)
|
||||
{
|
||||
try {
|
||||
$mutation = Mutation::with([
|
||||
'fromDealer',
|
||||
'toDealer',
|
||||
'requestedBy.role',
|
||||
'approvedBy.role',
|
||||
'receivedBy.role',
|
||||
'rejectedBy.role',
|
||||
'cancelledBy.role',
|
||||
'mutationDetails.product.category'
|
||||
])->findOrFail($id);
|
||||
|
||||
return view('warehouse_management.mutations.print', compact('mutation'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error printing mutation: ' . $e->getMessage());
|
||||
return back()->with('error', 'Gagal membuka halaman print mutasi.');
|
||||
}
|
||||
}
|
||||
}
|
||||
524
app/Http/Controllers/WarehouseManagement/OpnamesController.php
Executable file
524
app/Http/Controllers/WarehouseManagement/OpnamesController.php
Executable file
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Enums\OpnameStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class OpnamesController extends Controller
|
||||
{
|
||||
public function index(Request $request){
|
||||
$menu = Menu::where('link','opnames.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
$dealers = Dealer::all();
|
||||
if($request->ajax()){
|
||||
$data = Opname::query()
|
||||
->with(['user','dealer', 'details.product'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// Filter berdasarkan dealer yang dipilih
|
||||
if ($request->filled('dealer_filter')) {
|
||||
$data->where('dealer_id', $request->dealer_filter);
|
||||
}
|
||||
|
||||
// Filter berdasarkan tanggal
|
||||
if ($request->filled('date_from')) {
|
||||
try {
|
||||
$dateFrom = \Carbon\Carbon::parse($request->date_from)->format('Y-m-d');
|
||||
$data->whereDate('opname_date', '>=', $dateFrom);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to original format
|
||||
$data->whereDate('opname_date', '>=', $request->date_from);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('date_to')) {
|
||||
try {
|
||||
$dateTo = \Carbon\Carbon::parse($request->date_to)->format('Y-m-d');
|
||||
$data->whereDate('opname_date', '<=', $dateTo);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to original format
|
||||
$data->whereDate('opname_date', '<=', $request->date_to);
|
||||
}
|
||||
}
|
||||
|
||||
return DataTables::of($data)
|
||||
->addColumn('user_name', function ($row){
|
||||
return $row->user ? $row->user->name : '-';
|
||||
})
|
||||
->addColumn('dealer_name', function ($row){
|
||||
return $row->dealer ? $row->dealer->name : '-';
|
||||
})
|
||||
->editColumn('opname_date', function ($row){
|
||||
return $row->opname_date ? Carbon::parse($row->opname_date)->format('d M Y') : '-';
|
||||
})
|
||||
->editColumn('created_at', function ($row) {
|
||||
return Carbon::parse($row->created_at)->format('d M Y H:i');
|
||||
})
|
||||
->editColumn('status', function ($row) {
|
||||
$status = $row->status instanceof OpnameStatus ? $row->status : OpnameStatus::from($row->status);
|
||||
$textColorClass = $status->textColorClass();
|
||||
$label = $status->label();
|
||||
|
||||
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) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
$btn .= '<a href="'.route('opnames.show', $row->id).'" class="btn btn-primary btn-sm" style="margin-right: 8px;">Detail</a>';
|
||||
$btn .= '<a href="'.route('opnames.print', $row->id).'" class="btn btn-success btn-sm" target="_blank">Print</a>';
|
||||
|
||||
$btn .= '</div>';
|
||||
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action', 'status', 'stock_info'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('warehouse_management.opnames.index', compact('dealers'));
|
||||
}
|
||||
|
||||
public function create(){
|
||||
try{
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::where('active', true)->get();
|
||||
|
||||
// Get initial stock data for the first dealer (if any)
|
||||
$initialDealerId = $dealers->first()?->id;
|
||||
$stocks = [];
|
||||
if ($initialDealerId) {
|
||||
$stocks = Stock::where('dealer_id', $initialDealerId)
|
||||
->whereIn('product_id', $products->pluck('id'))
|
||||
->get()
|
||||
->keyBy('product_id');
|
||||
}
|
||||
|
||||
return view('warehouse_management.opnames.create', compact('dealers', 'products', 'stocks'));
|
||||
} catch(\Exception $ex) {
|
||||
Log::error($ex->getMessage());
|
||||
return back()->with('error', 'Terjadi kesalahan saat memuat data');
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Check if this is from transaction form or regular opname form
|
||||
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
||||
|
||||
if ($isTransactionForm) {
|
||||
// Simplified validation for transaction form
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'opname_date' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d',
|
||||
'before_or_equal:today'
|
||||
],
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'product_id' => 'required|array|min:1',
|
||||
'product_id.*' => 'required|exists:products,id',
|
||||
'system_stock' => 'required|array',
|
||||
'system_stock.*' => 'required|numeric|min:0',
|
||||
'physical_stock' => 'required|array',
|
||||
'physical_stock.*' => 'nullable|numeric|min:0'
|
||||
]);
|
||||
|
||||
// Process transaction form data with proper date parsing
|
||||
$dealerId = $request->dealer_id;
|
||||
$userId = $request->user_id;
|
||||
|
||||
// Parse opname date (YYYY-MM-DD format) or use today if empty
|
||||
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||
Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]);
|
||||
$opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate);
|
||||
Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]);
|
||||
|
||||
$note = $request->description;
|
||||
$productIds = $request->product_id;
|
||||
$systemStocks = $request->system_stock;
|
||||
$physicalStocks = $request->physical_stock;
|
||||
|
||||
// Log input data untuk debugging
|
||||
Log::info('Transaction form input data', [
|
||||
'product_ids' => $productIds,
|
||||
'system_stocks' => $systemStocks,
|
||||
'physical_stocks' => $physicalStocks,
|
||||
'dealer_id' => $dealerId,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
} else {
|
||||
// Original validation for regular opname form
|
||||
$request->validate([
|
||||
'dealer' => 'required|exists:dealers,id',
|
||||
'product' => 'required|array|min:1',
|
||||
'product.*' => 'required|exists:products,id',
|
||||
'system_quantity' => 'required|array',
|
||||
'system_quantity.*' => 'required|numeric|min:0',
|
||||
'physical_quantity' => 'required|array',
|
||||
'physical_quantity.*' => 'required|numeric|min:0',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'item_notes' => 'nullable|array',
|
||||
'item_notes.*' => 'nullable|string|max:255',
|
||||
'opname_date' => 'nullable|date' // Add opname_date validation for regular form
|
||||
]);
|
||||
|
||||
// Process regular form data
|
||||
$dealerId = $request->dealer;
|
||||
$userId = auth()->id();
|
||||
|
||||
// Use provided date or current date
|
||||
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||
$opnameDate = $request->opname_date ?
|
||||
Carbon::createFromFormat('Y-m-d', $inputDate) :
|
||||
now();
|
||||
|
||||
$note = $request->note;
|
||||
$productIds = $request->product;
|
||||
$systemStocks = $request->system_quantity;
|
||||
$physicalStocks = $request->physical_quantity;
|
||||
}
|
||||
|
||||
// 2. Simplified validation - all products are valid, set defaults for empty physical stocks
|
||||
$validProductIds = array_filter($productIds);
|
||||
|
||||
if (empty($validProductIds) || count($validProductIds) === 0) {
|
||||
throw new \Exception('Minimal harus ada satu produk untuk opname.');
|
||||
}
|
||||
|
||||
// 3. Validasi duplikasi produk
|
||||
$productCounts = array_count_values($validProductIds);
|
||||
foreach ($productCounts as $productId => $count) {
|
||||
if ($count > 1) {
|
||||
throw new \Exception('Produk tidak boleh duplikat dalam satu opname.');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Validasi dealer
|
||||
$dealer = Dealer::findOrFail($dealerId);
|
||||
|
||||
// 5. Validasi user exists
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
// 6. Validasi produk aktif
|
||||
$filteredProductIds = array_filter($productIds);
|
||||
$inactiveProducts = Product::whereIn('id', $filteredProductIds)
|
||||
->where('active', false)
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
if (!empty($inactiveProducts)) {
|
||||
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
||||
}
|
||||
|
||||
// 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement)
|
||||
$stockDifferences = [];
|
||||
if (!$isTransactionForm) {
|
||||
// Only validate notes for regular opname form
|
||||
foreach ($productIds as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||
$physicalStock = floatval($physicalStocks[$index] ?? 0);
|
||||
$itemNote = $request->input("item_notes.{$index}");
|
||||
|
||||
// Jika ada perbedaan stock dan note kosong
|
||||
if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) {
|
||||
$product = Product::find($productId);
|
||||
$stockDifferences[] = $product->name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($stockDifferences)) {
|
||||
throw new \Exception(
|
||||
'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' .
|
||||
implode(', ', $stockDifferences)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Create Opname master record with approved status
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $dealerId,
|
||||
'opname_date' => $opnameDate,
|
||||
'user_id' => $userId,
|
||||
'note' => $note,
|
||||
'status' => OpnameStatus::APPROVED, // Set status langsung approved
|
||||
'approved_by' => $userId, // Set current user sebagai approver
|
||||
'approved_at' => now() // Set waktu approval
|
||||
]);
|
||||
|
||||
// 9. Create OpnameDetails and update stock - only for valid entries
|
||||
$details = [];
|
||||
$processedCount = 0;
|
||||
|
||||
foreach ($productIds as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
// Set default value to 0 if physical stock is empty or invalid
|
||||
$physicalStockValue = $physicalStocks[$index] ?? null;
|
||||
if ($physicalStockValue === '' || $physicalStockValue === null || !is_numeric($physicalStockValue)) {
|
||||
$physicalStockValue = 0;
|
||||
}
|
||||
|
||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||
$physicalStock = floatval($physicalStockValue);
|
||||
$difference = $physicalStock - $systemStock;
|
||||
|
||||
$processedCount++;
|
||||
|
||||
// Get item note (only for regular opname form)
|
||||
$itemNote = null;
|
||||
if (!$isTransactionForm) {
|
||||
$itemNote = $request->input("item_notes.{$index}");
|
||||
}
|
||||
|
||||
// Create opname detail
|
||||
$details[] = [
|
||||
'opname_id' => $opname->id,
|
||||
'product_id' => $productId,
|
||||
'system_stock' => $systemStock,
|
||||
'physical_stock' => $physicalStock,
|
||||
'difference' => $difference,
|
||||
'note' => $itemNote,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
];
|
||||
|
||||
// Update stock langsung karena auto approve
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'dealer_id' => $dealerId
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical stock
|
||||
$stock->updateStock(
|
||||
$physicalStock,
|
||||
$opname,
|
||||
"Stock adjustment from auto-approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
|
||||
// Validate we have at least one detail to insert
|
||||
if (empty($details)) {
|
||||
throw new \Exception('Tidak ada data produk yang valid untuk diproses.');
|
||||
}
|
||||
|
||||
// Bulk insert untuk performa lebih baik
|
||||
OpnameDetail::insert($details);
|
||||
|
||||
// 10. Log aktivitas dengan detail produk yang diproses
|
||||
$processedProducts = collect($details)->map(function($detail) {
|
||||
return [
|
||||
'product_id' => $detail['product_id'],
|
||||
'system_stock' => $detail['system_stock'],
|
||||
'physical_stock' => $detail['physical_stock'],
|
||||
'difference' => $detail['difference']
|
||||
];
|
||||
});
|
||||
|
||||
Log::info('Opname created and auto-approved', [
|
||||
'opname_id' => $opname->id,
|
||||
'dealer_id' => $opname->dealer_id,
|
||||
'user_id' => $userId,
|
||||
'approver_id' => $userId,
|
||||
'product_count' => count($details),
|
||||
'processed_count' => $processedCount,
|
||||
'form_type' => $isTransactionForm ? 'transaction' : 'regular',
|
||||
'opname_date' => $opnameDate->format('Y-m-d'),
|
||||
'processed_products' => $processedProducts->toArray()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
if ($isTransactionForm) {
|
||||
// Redirect back to transaction page with success message and tab indicator
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.")
|
||||
->with('active_tab', 'opname');
|
||||
} else {
|
||||
// Redirect to opname index for regular form
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.");
|
||||
}
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Validation error in OpnamesController@store', [
|
||||
'errors' => $e->errors(),
|
||||
'input' => $request->all()
|
||||
]);
|
||||
|
||||
if ($isTransactionForm) {
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->withErrors($e->validator)
|
||||
->withInput()
|
||||
->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.')
|
||||
->with('active_tab', 'opname');
|
||||
} else {
|
||||
return back()->withErrors($e->validator)->withInput();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
Log::error('Request data:', $request->all());
|
||||
|
||||
$errorMessage = $e->getMessage();
|
||||
|
||||
if ($isTransactionForm) {
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->with('error', $errorMessage)
|
||||
->withInput()
|
||||
->with('active_tab', 'opname');
|
||||
} else {
|
||||
return back()
|
||||
->with('error', $errorMessage)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function show(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$opname = Opname::with('details.product', 'user')->findOrFail($id);
|
||||
|
||||
if ($request->ajax()) {
|
||||
return DataTables::of($opname->details)
|
||||
->addIndexColumn()
|
||||
->addColumn('opname_date', function () use ($opname) {
|
||||
return Carbon::parse($opname->opname_date)->format('d M Y');
|
||||
})
|
||||
->addColumn('user_name', function () use ($opname) {
|
||||
return $opname->user ? $opname->user->name : '-';
|
||||
})
|
||||
->addColumn('product_name', function ($detail) {
|
||||
return $detail->product->name ?? '-';
|
||||
})
|
||||
->addColumn('system_stock', function ($detail) {
|
||||
return $detail->system_stock;
|
||||
})
|
||||
->addColumn('physical_stock', function ($detail) {
|
||||
return $detail->physical_stock;
|
||||
})
|
||||
->addColumn('difference', function ($detail) {
|
||||
return $detail->difference;
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('warehouse_management.opnames.detail', compact('opname'));
|
||||
} catch (\Exception $ex) {
|
||||
Log::error($ex->getMessage());
|
||||
abort(500, 'Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
// Add new method to get stock data via AJAX
|
||||
public function getStockData(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->dealer_id;
|
||||
$productIds = $request->product_ids;
|
||||
|
||||
if (!$dealerId || !$productIds) {
|
||||
return response()->json(['error' => 'Dealer ID dan Product IDs diperlukan'], 400);
|
||||
}
|
||||
|
||||
$stocks = Stock::where('dealer_id', $dealerId)
|
||||
->whereIn('product_id', $productIds)
|
||||
->get()
|
||||
->mapWithKeys(function ($stock) {
|
||||
return [$stock->product_id => $stock->quantity];
|
||||
});
|
||||
|
||||
return response()->json(['stocks' => $stocks]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error getting stock data: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function print($id)
|
||||
{
|
||||
try {
|
||||
$opname = Opname::with(['details.product.category', 'user', 'dealer'])
|
||||
->findOrFail($id);
|
||||
|
||||
return view('warehouse_management.opnames.print', compact('opname'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error printing opname: ' . $e->getMessage());
|
||||
return back()->with('error', 'Gagal membuka halaman print opname.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
136
app/Http/Controllers/WarehouseManagement/ProductCategoriesController.php
Executable file
136
app/Http/Controllers/WarehouseManagement/ProductCategoriesController.php
Executable file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Menu;
|
||||
use App\Models\ProductCategory;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class ProductCategoriesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','product_categories.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
if($request->ajax()){
|
||||
$data = ProductCategory::query();
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('parent', function ($row) {
|
||||
return $row->parent ? $row->parent->name : '-';
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '';
|
||||
|
||||
if (Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button style="margin-right: 8px;" class="btn btn-danger btn-sm btn-destroy-product-category" data-action="' . route('product_categories.destroy', $row->id) . '" data-id="' . $row->id . '">Hapus</button>';
|
||||
}
|
||||
|
||||
if (Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-edit-product-category" data-url="' . route('product_categories.edit', $row->id) . '" data-action="' . route('product_categories.update', $row->id) . '" data-id="' . $row->id . '">Edit</button>';
|
||||
}
|
||||
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
return view('warehouse_management.product_categories.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => 'nullable|exists:product_categories,id',
|
||||
]);
|
||||
ProductCategory::create($validated);
|
||||
return response()->json(['success' => true, 'message' => 'Kategori berhasil ditambahkan.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$category = ProductCategory::findOrFail($id);
|
||||
return response()->json($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$category = ProductCategory::findOrFail($id);
|
||||
$category->update($validated);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Kategori berhasil diperbarui.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
ProductCategory::findOrFail($id)->delete();
|
||||
return response()->json(['success' => true, 'message' => 'Kategorii berhasil dihapus.']);
|
||||
}
|
||||
|
||||
public function product_category_parents(Request $request)
|
||||
{
|
||||
$parents = ProductCategory::whereNull('parent_id')->get(['id', 'name']);
|
||||
return response()->json($parents);
|
||||
}
|
||||
}
|
||||
288
app/Http/Controllers/WarehouseManagement/ProductsController.php
Executable file
288
app/Http/Controllers/WarehouseManagement/ProductsController.php
Executable file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use App\Exports\ProductStockDealers;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ProductsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','products.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
if($request->ajax()){
|
||||
Log::info('Products DataTables request received');
|
||||
Log::info('Request parameters:', $request->all());
|
||||
|
||||
try {
|
||||
// Check if products exist
|
||||
$productCount = Product::count();
|
||||
Log::info('Total products in database: ' . $productCount);
|
||||
|
||||
$data = Product::with(['category', 'stocks'])
|
||||
->select(['id', 'code', 'name', 'product_category_id', 'unit', 'active']);
|
||||
|
||||
Log::info('Query built, executing DataTables...');
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('code', function ($row) {
|
||||
return $row->code;
|
||||
})
|
||||
->addColumn('name', function ($row) {
|
||||
return $row->name;
|
||||
})
|
||||
->addColumn('category_name', function ($row) {
|
||||
return $row->category ? $row->category->name : '-';
|
||||
})
|
||||
->addColumn('unit', function ($row) {
|
||||
return $row->unit ?? '-';
|
||||
})
|
||||
->addColumn('total_stock', function ($row){
|
||||
try {
|
||||
$totalStock = $row->stocks()->sum('quantity');
|
||||
return number_format($totalStock, 2);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error calculating total stock for product ' . $row->id . ': ' . $e->getMessage());
|
||||
return '0.00';
|
||||
}
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
if (Gate::allows('update', $menu)) {
|
||||
$btn .= '<a href="' . route('products.edit', $row->id) . '" class="btn btn-warning btn-sm" style="margin-right: 8px;">Edit</a>';
|
||||
$btn .= '<button class="btn btn-sm btn-toggle-active '
|
||||
. ($row->active ? 'btn-danger' : 'btn-success') . '"
|
||||
data-url="' . route('products.toggleActive', $row->id) . '" data-active="'.$row->active.'" style="margin-right: 8px;">'
|
||||
. ($row->active ? 'Nonaktifkan' : 'Aktifkan') . '</button>';
|
||||
}
|
||||
$btn .= '<button class="btn btn-sm btn-secondary btn-product-stock-dealers"
|
||||
data-id="'.$row->id.'"
|
||||
data-url="'.route('products.dealers_stock').'"
|
||||
data-name="'.$row->name.'">Dealer</button>';
|
||||
|
||||
$btn .= '</div>';
|
||||
|
||||
return $btn;
|
||||
})
|
||||
->filterColumn('category_name', function($query, $keyword) {
|
||||
$query->whereHas('category', function($q) use ($keyword) {
|
||||
$q->where('name', 'like', "%{$keyword}%");
|
||||
});
|
||||
})
|
||||
->orderColumn('code', function ($query, $order) {
|
||||
$query->orderBy('products.code', $order);
|
||||
})
|
||||
->orderColumn('name', function ($query, $order) {
|
||||
$query->orderBy('products.name', $order);
|
||||
})
|
||||
->orderColumn('category_name', function ($query, $order) {
|
||||
$query->orderBy(
|
||||
DB::raw('(SELECT name FROM product_categories WHERE product_categories.id = products.product_category_id)'),
|
||||
$order
|
||||
);
|
||||
})
|
||||
->orderColumn('unit', function ($query, $order) {
|
||||
$query->orderBy('products.unit', $order);
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Products DataTables error: ' . $e->getMessage());
|
||||
Log::error('Stack trace: ' . $e->getTraceAsString());
|
||||
return response()->json(['error' => 'Failed to load data'], 500);
|
||||
}
|
||||
}
|
||||
return view('warehouse_management.products.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$categories = ProductCategory::with('children')->whereNull('parent_id')->get();
|
||||
$dealers = Dealer::all();
|
||||
return view('warehouse_management.products.create', compact('categories', 'dealers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try{
|
||||
$request->validate([
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::unique('products')->whereNull('deleted_at'),
|
||||
],
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
'unit' => 'nullable|string',
|
||||
'active' => 'required|boolean',
|
||||
'product_category_id' => 'required|exists:product_categories,id'
|
||||
]);
|
||||
|
||||
// Create product
|
||||
$product = Product::create([
|
||||
'code' => $request->code,
|
||||
'name' => $request->name,
|
||||
'unit' => $request->unit,
|
||||
'active' => $request->active,
|
||||
'description' => $request->description,
|
||||
'product_category_id' => $request->product_category_id,
|
||||
]);
|
||||
|
||||
return redirect()->route('products.index')->with('success', 'Produk berhasil ditambahkan.');
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$product = Product::findOrFail($id);
|
||||
return view('warehouse_management.products.edit', [
|
||||
'product' => $product->load('dealers'),
|
||||
'dealers' => Dealer::all(),
|
||||
'categories' => ProductCategory::with('children')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
try{
|
||||
$request->validate([
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::unique('products')->ignore($product->id)->whereNull('deleted_at'),
|
||||
],
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
'unit' => 'nullable|string',
|
||||
'active' => 'required|boolean',
|
||||
'product_category_id' => 'required|exists:product_categories,id'
|
||||
]);
|
||||
|
||||
$product->update($request->only(['code', 'name', 'description', 'unit','active', 'product_category_id']));
|
||||
|
||||
return redirect()->route('products.index')->with('success', 'Produk berhasil diperbarui.');
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
$product->delete();
|
||||
|
||||
return redirect()->route('products.index')->with('success', 'Produk berhasil dihapus.');
|
||||
}
|
||||
public function toggleActive(Request $request, Product $product)
|
||||
{
|
||||
$product->active = !$product->active;
|
||||
$product->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'active' => $product->active,
|
||||
'message' => 'Status produk berhasil diperbarui.'
|
||||
]);
|
||||
}
|
||||
|
||||
public function all_products(){
|
||||
try{
|
||||
$products = Product::where('active', true)->select('id','name')->get();
|
||||
return response()->json($products);
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function dealers_stock(Request $request){
|
||||
$productId = $request->get('product_id');
|
||||
|
||||
$product = Product::with(['stocks.dealer'])->findOrFail($productId);
|
||||
|
||||
$data = $product->stocks->map(function ($stock) {
|
||||
return [
|
||||
'dealer_name' => $stock->dealer->name ?? '-',
|
||||
'quantity' => $stock->quantity
|
||||
];
|
||||
});
|
||||
|
||||
return DataTables::of($data)->make(true);
|
||||
}
|
||||
|
||||
public function exportDealersStock()
|
||||
{
|
||||
try {
|
||||
$fileName = 'stok_produk_dealers_' . date('Y-m-d_H-i-s') . '.xlsx';
|
||||
|
||||
return Excel::download(new ProductStockDealers(), $fileName);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Export dealers stock error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Gagal mengexport data. Silakan coba lagi.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\StockLog;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class StockAuditController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link', 'stock-audit.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::all();
|
||||
|
||||
if ($request->ajax()) {
|
||||
Log::info('Stock audit ajax request received', [
|
||||
'filters' => $request->only(['dealer', 'product', 'change_type', 'date']),
|
||||
'user_id' => auth()->id(),
|
||||
'user_dealer_id' => auth()->user()->dealer_id
|
||||
]);
|
||||
$data = StockLog::query()
|
||||
->with([
|
||||
'stock.product',
|
||||
'stock.dealer',
|
||||
'user.role',
|
||||
'source'
|
||||
])
|
||||
->leftJoin('stocks', 'stock_logs.stock_id', '=', 'stocks.id')
|
||||
->leftJoin('products', 'stocks.product_id', '=', 'products.id')
|
||||
->leftJoin('dealers', 'stocks.dealer_id', '=', 'dealers.id')
|
||||
->leftJoin('users', 'stock_logs.user_id', '=', 'users.id')
|
||||
->select('stock_logs.*');
|
||||
|
||||
// Apply filters from request
|
||||
if ($request->filled('dealer')) {
|
||||
$data->where('dealers.name', 'like', '%' . $request->dealer . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('product')) {
|
||||
$data->where('products.name', 'like', '%' . $request->product . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('change_type')) {
|
||||
$data->where('stock_logs.change_type', $request->change_type);
|
||||
}
|
||||
|
||||
if ($request->filled('date')) {
|
||||
$data->whereDate('stock_logs.created_at', $request->date);
|
||||
}
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('product_name', function($row) {
|
||||
return $row->stock->product->name ?? '-';
|
||||
})
|
||||
->addColumn('dealer_name', function($row) {
|
||||
return $row->stock->dealer->name ?? '-';
|
||||
})
|
||||
->addColumn('change_type', function($row) {
|
||||
$changeType = $row->change_type;
|
||||
$class = match($changeType->value) {
|
||||
'increase' => 'text-success',
|
||||
'decrease' => 'text-danger',
|
||||
'adjustment' => 'text-warning',
|
||||
'no_change' => 'text-muted',
|
||||
default => 'text-dark'
|
||||
};
|
||||
|
||||
return "<span class=\"font-weight-bold {$class}\">{$changeType->label()}</span>";
|
||||
})
|
||||
->addColumn('quantity_change', function($row) {
|
||||
$change = $row->quantity_change;
|
||||
if ($change > 0) {
|
||||
return "<span class=\"text-success\">+{$change}</span>";
|
||||
} elseif ($change < 0) {
|
||||
return "<span class=\"text-danger\">{$change}</span>";
|
||||
} else {
|
||||
return "<span class=\"text-muted\">0</span>";
|
||||
}
|
||||
})
|
||||
->addColumn('stock_before_after', function($row) {
|
||||
return "{$row->previous_quantity} → {$row->new_quantity}";
|
||||
})
|
||||
->addColumn('source_info', function($row) {
|
||||
if ($row->source_type === 'App\\Models\\Mutation') {
|
||||
$mutationNumber = $row->source ? $row->source->mutation_number : '-';
|
||||
return "Mutasi: {$mutationNumber}";
|
||||
} elseif ($row->source_type === 'App\\Models\\Opname') {
|
||||
$opname_id = $row->source ? $row->source->id : '-';
|
||||
return "Opname: #{$opname_id}";
|
||||
} elseif ($row->source_type === 'App\\Models\\Transaction')
|
||||
{
|
||||
$transaction_id = $row->source ? $row->source->id : '-';
|
||||
return "Transaksi: #{$transaction_id}";
|
||||
}else {
|
||||
return $row->source_type ?? '-';
|
||||
}
|
||||
})
|
||||
->addColumn('user_name', function($row) {
|
||||
return $row->user->name ?? '-';
|
||||
})
|
||||
->addColumn('created_at', function($row) {
|
||||
return $row->created_at->format('d M Y, H:i');
|
||||
})
|
||||
->addColumn('action', function($row) {
|
||||
$buttons = '<button type="button" class="btn btn-info btn-sm" onclick="showAuditDetail('.$row->id.')">
|
||||
Detail
|
||||
</button>';
|
||||
return $buttons;
|
||||
})
|
||||
// Filtering
|
||||
->filterColumn('product_name', function($query, $keyword) {
|
||||
$query->where('products.name', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('dealer_name', function($query, $keyword) {
|
||||
$query->where('dealers.name', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('change_type', function($query, $keyword) {
|
||||
$query->where('stock_logs.change_type', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('source_info', function($query, $keyword) {
|
||||
$query->where(function($q) use ($keyword) {
|
||||
$q->where('stock_logs.source_type', 'like', "%{$keyword}%")
|
||||
->orWhere('stock_logs.description', 'like', "%{$keyword}%");
|
||||
});
|
||||
})
|
||||
->filterColumn('user_name', function($query, $keyword) {
|
||||
$query->where('users.name', 'like', "%{$keyword}%");
|
||||
})
|
||||
->filterColumn('created_at', function($query, $keyword) {
|
||||
$query->whereDate('stock_logs.created_at', 'like', "%{$keyword}%");
|
||||
})
|
||||
// Order column mapping
|
||||
->orderColumn('product_name', function($query, $order) {
|
||||
return $query->orderBy('products.name', $order);
|
||||
})
|
||||
->orderColumn('dealer_name', function($query, $order) {
|
||||
return $query->orderBy('dealers.name', $order);
|
||||
})
|
||||
->orderColumn('user_name', function($query, $order) {
|
||||
return $query->orderBy('users.name', $order);
|
||||
})
|
||||
->orderColumn('created_at', function($query, $order) {
|
||||
return $query->orderBy('stock_logs.created_at', $order);
|
||||
})
|
||||
->orderColumn('quantity_change', function($query, $order) {
|
||||
return $query->orderBy('stock_logs.quantity_change', $order);
|
||||
})
|
||||
->orderColumn('stock_before_after', function($query, $order) {
|
||||
return $query->orderBy('stock_logs.previous_quantity', $order);
|
||||
})
|
||||
->orderColumn('change_type', function($query, $order) {
|
||||
return $query->orderBy('stock_logs.change_type', $order);
|
||||
})
|
||||
->orderColumn('source_info', function($query, $order) {
|
||||
return $query->orderBy('stock_logs.source_type', $order);
|
||||
})
|
||||
->rawColumns(['change_type', 'quantity_change', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('warehouse_management.stock_audit.index', compact('menu', 'dealers', 'products'));
|
||||
}
|
||||
|
||||
public function getDetail(StockLog $stockLog)
|
||||
{
|
||||
try {
|
||||
$stockLog->load([
|
||||
'stock.product',
|
||||
'stock.dealer',
|
||||
'user.role',
|
||||
'source'
|
||||
]);
|
||||
|
||||
// Format data untuk response
|
||||
$stockLog->created_at_formatted = $stockLog->created_at->format('d M Y, H:i');
|
||||
$stockLog->change_type_label = $stockLog->change_type->label();
|
||||
|
||||
// Detail source berdasarkan tipe
|
||||
$sourceDetail = null;
|
||||
if ($stockLog->source) {
|
||||
if ($stockLog->source_type === 'App\\Models\\Mutation') {
|
||||
$mutation = $stockLog->source;
|
||||
$mutation->load(['fromDealer', 'toDealer', 'requestedBy', 'approvedBy']);
|
||||
|
||||
// Format approved_at date if exists
|
||||
if ($mutation->approved_at) {
|
||||
$mutation->approved_at_formatted = $mutation->approved_at->format('d M Y, H:i');
|
||||
}
|
||||
|
||||
$sourceDetail = [
|
||||
'type' => 'mutation',
|
||||
'data' => $mutation
|
||||
];
|
||||
} elseif ($stockLog->source_type === 'App\\Models\\StockOpname') {
|
||||
$opname = $stockLog->source;
|
||||
$opname->load(['dealer', 'user']);
|
||||
$sourceDetail = [
|
||||
'type' => 'opname',
|
||||
'data' => $opname
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stockLog,
|
||||
'source_detail' => $sourceDetail
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat detail audit: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Http/Controllers/WorkController.php
Normal file → Executable file
45
app/Http/Controllers/WorkController.php
Normal file → Executable file
@@ -26,16 +26,35 @@ class WorkController extends Controller
|
||||
$data = DB::table('works as w')->leftJoin('categories as c', 'c.id', '=', 'w.category_id')->select('w.shortname as shortname', 'w.id as work_id', 'w.name as name', 'w.desc as desc', 'c.name as category_name', 'w.category_id as category_id');
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '';
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Auth::user()->can('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')"> Hapus </button>';
|
||||
// Products Management Button
|
||||
if(Gate::allows('view', $menu)) {
|
||||
$btn .= '<a href="'. route('work.products.index', ['work' => $row->work_id]) .'" class="btn btn-info btn-sm" title="Kelola Produk">
|
||||
Produk
|
||||
</a>';
|
||||
}
|
||||
|
||||
if(Auth::user()->can('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-bold" 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 </button>';
|
||||
// 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)) {
|
||||
$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
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
@@ -145,4 +164,20 @@ class WorkController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
247
app/Http/Controllers/WorkProductController.php
Normal file
247
app/Http/Controllers/WorkProductController.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\Product;
|
||||
use App\Models\WorkProduct;
|
||||
use App\Models\Menu;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class WorkProductController extends Controller
|
||||
{
|
||||
protected $stockService;
|
||||
|
||||
public function __construct(StockService $stockService)
|
||||
{
|
||||
$this->stockService = $stockService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display work products for a specific work
|
||||
*/
|
||||
public function index(Request $request, $workId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$work = Work::with('category')->findOrFail($workId);
|
||||
|
||||
if ($request->ajax()) {
|
||||
Log::info('Work products index AJAX request for work ID: ' . $workId);
|
||||
|
||||
$workProducts = WorkProduct::with(['product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->get();
|
||||
|
||||
Log::info('Found ' . $workProducts->count() . ' work products');
|
||||
|
||||
return DataTables::of($workProducts)
|
||||
->addIndexColumn()
|
||||
->addColumn('product_name', function($row) {
|
||||
return $row->product->name;
|
||||
})
|
||||
->addColumn('product_code', function($row) {
|
||||
return $row->product->code;
|
||||
})
|
||||
->addColumn('product_category', function($row) {
|
||||
return $row->product->category ? $row->product->category->name : '-';
|
||||
})
|
||||
->addColumn('unit', function($row) {
|
||||
return $row->product->unit;
|
||||
})
|
||||
->addColumn('quantity_required', function($row) {
|
||||
return number_format($row->quantity_required, 2);
|
||||
})
|
||||
->addColumn('action', function($row) use ($menu) {
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm btn-edit-work-product" data-id="'.$row->id.'">
|
||||
Edit
|
||||
</button>';
|
||||
}
|
||||
|
||||
if(Gate::allows('delete', $menu)) {
|
||||
$btn .= '<button class="btn btn-danger btn-sm btn-delete-work-product" data-id="'.$row->id.'">
|
||||
Hapus
|
||||
</button>';
|
||||
}
|
||||
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
$products = Product::where('active', true)->with('category')->get();
|
||||
|
||||
return view('back.master.work-products', compact('work', 'products'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store work product relationship
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
// Check if combination already exists
|
||||
$exists = WorkProduct::where('work_id', $request->work_id)
|
||||
->where('product_id', $request->product_id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Produk sudah ditambahkan ke pekerjaan ini'
|
||||
], 422);
|
||||
}
|
||||
|
||||
WorkProduct::create($request->all());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil ditambahkan ke pekerjaan'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show work product for editing
|
||||
*/
|
||||
public function show($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::with(['work', 'product', 'product.category'])
|
||||
->where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $workProduct
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update work product relationship
|
||||
*/
|
||||
public function update(Request $request, $workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('update', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'quantity_required' => 'required|numeric|min:0.01',
|
||||
'notes' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->update($request->only(['quantity_required', 'notes']));
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Data produk pekerjaan berhasil diupdate'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove work product relationship
|
||||
*/
|
||||
public function destroy($workId, $workProductId)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('delete', $menu), 403, 'Unauthorized User');
|
||||
|
||||
try {
|
||||
$workProduct = WorkProduct::where('work_id', $workId)
|
||||
->where('id', $workProductId)
|
||||
->firstOrFail();
|
||||
|
||||
$workProduct->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Produk berhasil dihapus dari pekerjaan'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting work product: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Work product tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock prediction for work
|
||||
*/
|
||||
public function stockPrediction(Request $request, $workId)
|
||||
{
|
||||
$quantity = $request->get('quantity', 1);
|
||||
$prediction = $this->stockService->getStockUsagePrediction($workId, $quantity);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $prediction
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for work at specific dealer
|
||||
*/
|
||||
public function checkStock(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'quantity' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$availability = $this->stockService->checkStockAvailability(
|
||||
$request->work_id,
|
||||
$request->dealer_id,
|
||||
$request->quantity
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $availability
|
||||
]);
|
||||
}
|
||||
}
|
||||
0
app/Http/Kernel.php
Normal file → Executable file
0
app/Http/Kernel.php
Normal file → Executable file
0
app/Http/Middleware/Authenticate.php
Normal file → Executable file
0
app/Http/Middleware/Authenticate.php
Normal file → Executable file
0
app/Http/Middleware/EncryptCookies.php
Normal file → Executable file
0
app/Http/Middleware/EncryptCookies.php
Normal file → Executable file
0
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file → Executable file
0
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file → Executable file
0
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file → Executable file
0
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file → Executable file
0
app/Http/Middleware/TrimStrings.php
Normal file → Executable file
0
app/Http/Middleware/TrimStrings.php
Normal file → Executable file
0
app/Http/Middleware/TrustHosts.php
Normal file → Executable file
0
app/Http/Middleware/TrustHosts.php
Normal file → Executable file
0
app/Http/Middleware/TrustProxies.php
Normal file → Executable file
0
app/Http/Middleware/TrustProxies.php
Normal file → Executable file
0
app/Http/Middleware/VerifyCsrfToken.php
Normal file → Executable file
0
app/Http/Middleware/VerifyCsrfToken.php
Normal file → Executable file
1
app/Http/Middleware/adminRole.php
Normal file → Executable file
1
app/Http/Middleware/adminRole.php
Normal file → Executable file
@@ -20,7 +20,6 @@ class adminRole
|
||||
{
|
||||
// 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();
|
||||
// dd($user);
|
||||
if (!$user) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
0
app/Http/Middleware/mechanicRole.php
Normal file → Executable file
0
app/Http/Middleware/mechanicRole.php
Normal file → Executable file
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)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
0
app/Models/Category.php
Normal file → Executable file
0
app/Models/Category.php
Normal file → Executable file
65
app/Models/Dealer.php
Normal file → Executable file
65
app/Models/Dealer.php
Normal file → Executable file
@@ -22,4 +22,69 @@ class Dealer extends Model
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'dealer_id', 'id');
|
||||
}
|
||||
|
||||
public function opnames(){
|
||||
return $this->hasMany(Opname::class);
|
||||
}
|
||||
|
||||
public function outgoingMutations()
|
||||
{
|
||||
return $this->hasMany(Mutation::class, 'from_dealer_id');
|
||||
}
|
||||
|
||||
public function incomingMutations()
|
||||
{
|
||||
return $this->hasMany(Mutation::class, 'to_dealer_id');
|
||||
}
|
||||
|
||||
public function stocks()
|
||||
{
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'stocks', 'dealer_id', 'product_id')
|
||||
->withPivot('quantity')
|
||||
->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;
|
||||
}
|
||||
}
|
||||
0
app/Models/Menu.php
Normal file → Executable file
0
app/Models/Menu.php
Normal file → Executable file
286
app/Models/Mutation.php
Executable file
286
app/Models/Mutation.php
Executable file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\MutationStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Mutation extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'mutation_number',
|
||||
'from_dealer_id',
|
||||
'to_dealer_id',
|
||||
'status',
|
||||
'requested_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'approval_notes',
|
||||
'received_by',
|
||||
'received_at',
|
||||
'reception_notes',
|
||||
'shipping_notes',
|
||||
'rejection_reason',
|
||||
'rejected_by',
|
||||
'rejected_at',
|
||||
'cancelled_by',
|
||||
'cancelled_at',
|
||||
'cancellation_reason'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => MutationStatus::class,
|
||||
'approved_at' => 'datetime',
|
||||
'received_at' => 'datetime',
|
||||
'rejected_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($mutation) {
|
||||
if (empty($mutation->mutation_number)) {
|
||||
$mutation->mutation_number = $mutation->generateMutationNumber();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function fromDealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class, 'from_dealer_id');
|
||||
}
|
||||
|
||||
public function toDealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class, 'to_dealer_id');
|
||||
}
|
||||
|
||||
public function requestedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'requested_by');
|
||||
}
|
||||
|
||||
public function approvedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
public function receivedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'received_by');
|
||||
}
|
||||
|
||||
public function rejectedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'rejected_by');
|
||||
}
|
||||
|
||||
public function cancelledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
|
||||
public function mutationDetails()
|
||||
{
|
||||
return $this->hasMany(MutationDetail::class);
|
||||
}
|
||||
|
||||
public function stockLogs()
|
||||
{
|
||||
return $this->morphMany(StockLog::class, 'source');
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
public function getStatusLabelAttribute()
|
||||
{
|
||||
return $this->status->label();
|
||||
}
|
||||
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
return $this->status->color();
|
||||
}
|
||||
|
||||
public function getTotalItemsAttribute()
|
||||
{
|
||||
return $this->mutationDetails()->sum('quantity_requested');
|
||||
}
|
||||
|
||||
public function getTotalApprovedItemsAttribute()
|
||||
{
|
||||
return $this->mutationDetails()->sum('quantity_approved');
|
||||
}
|
||||
|
||||
public function canBeReceived()
|
||||
{
|
||||
return $this->status === MutationStatus::SENT;
|
||||
}
|
||||
|
||||
public function canBeApproved()
|
||||
{
|
||||
return $this->status === MutationStatus::RECEIVED;
|
||||
}
|
||||
|
||||
public function canBeCancelled()
|
||||
{
|
||||
return $this->status === MutationStatus::SENT;
|
||||
}
|
||||
|
||||
// Receive mutation by destination dealer
|
||||
public function receive($userId, $receptionNotes = null)
|
||||
{
|
||||
if (!$this->canBeReceived()) {
|
||||
throw new \Exception('Mutasi tidak dapat diterima dalam status saat ini');
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'status' => MutationStatus::RECEIVED,
|
||||
'received_by' => $userId,
|
||||
'received_at' => now(),
|
||||
'reception_notes' => $receptionNotes
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Approve mutation and move stock immediately
|
||||
public function approve($userId, $approvalNotes = null)
|
||||
{
|
||||
if (!$this->canBeApproved()) {
|
||||
throw new \Exception('Mutasi tidak dapat disetujui dalam status saat ini');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Update status to approved first
|
||||
$this->update([
|
||||
'status' => MutationStatus::APPROVED,
|
||||
'approved_by' => $userId,
|
||||
'approved_at' => now(),
|
||||
'approval_notes' => $approvalNotes
|
||||
]);
|
||||
|
||||
// Immediately move stock after approval
|
||||
foreach ($this->mutationDetails as $detail) {
|
||||
// Process all details that have quantity_requested > 0
|
||||
// because goods have been sent from source dealer
|
||||
if ($detail->quantity_requested > 0) {
|
||||
$this->processStockMovement($detail);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Reject mutation
|
||||
public function reject($userId, $rejectionReason)
|
||||
{
|
||||
if (!$this->canBeApproved()) {
|
||||
throw new \Exception('Mutasi tidak dapat ditolak dalam status saat ini');
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'status' => MutationStatus::REJECTED,
|
||||
'rejected_by' => $userId,
|
||||
'rejected_at' => now(),
|
||||
'rejection_reason' => $rejectionReason
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Cancel mutation
|
||||
public function cancel($userId, $cancellationReason = null)
|
||||
{
|
||||
if (!$this->canBeCancelled()) {
|
||||
throw new \Exception('Mutasi tidak dapat dibatalkan dalam status saat ini');
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'status' => MutationStatus::CANCELLED,
|
||||
'cancelled_by' => $userId,
|
||||
'cancelled_at' => now(),
|
||||
'cancellation_reason' => $cancellationReason
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Complete method removed - Stock moves automatically after approval
|
||||
|
||||
private function processStockMovement(MutationDetail $detail)
|
||||
{
|
||||
// Kurangi stock dari dealer asal berdasarkan quantity_requested (barang yang dikirim)
|
||||
$fromStock = Stock::firstOrCreate([
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $this->from_dealer_id
|
||||
], ['quantity' => 0]);
|
||||
|
||||
if ($fromStock->quantity < $detail->quantity_requested) {
|
||||
throw new \Exception("Stock tidak mencukupi untuk produk {$detail->product->name} di {$this->fromDealer->name}");
|
||||
}
|
||||
|
||||
$fromStock->updateStock(
|
||||
$fromStock->quantity - $detail->quantity_requested,
|
||||
$this,
|
||||
"Mutasi keluar ke {$this->toDealer->name} - {$this->mutation_number} (Dikirim: {$detail->quantity_requested}, Diterima: {$detail->quantity_approved})"
|
||||
);
|
||||
|
||||
// Tambah stock ke dealer tujuan berdasarkan quantity_approved (barang yang diterima)
|
||||
$toStock = Stock::firstOrCreate([
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $this->to_dealer_id
|
||||
], ['quantity' => 0]);
|
||||
|
||||
$toStock->updateStock(
|
||||
$toStock->quantity + $detail->quantity_approved,
|
||||
$this,
|
||||
"Mutasi masuk dari {$this->fromDealer->name} - {$this->mutation_number} (Dikirim: {$detail->quantity_requested}, Diterima: {$detail->quantity_approved})"
|
||||
);
|
||||
|
||||
// Jika ada selisih (kehilangan), catat sebagai stock log terpisah untuk audit
|
||||
$lostQuantity = $detail->quantity_requested - $detail->quantity_approved;
|
||||
if ($lostQuantity > 0) {
|
||||
// Buat stock log untuk barang yang hilang/rusak
|
||||
StockLog::create([
|
||||
'stock_id' => $fromStock->id,
|
||||
'previous_quantity' => $fromStock->quantity + $detail->quantity_requested, // Stock sebelum pengurangan
|
||||
'new_quantity' => $fromStock->quantity, // Stock setelah pengurangan
|
||||
'source_type' => get_class($this),
|
||||
'source_id' => $this->id,
|
||||
'description' => "Kehilangan/kerusakan saat mutasi ke {$this->toDealer->name} - {$this->mutation_number} (Hilang: {$lostQuantity})",
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateMutationNumber()
|
||||
{
|
||||
$prefix = 'MUT';
|
||||
$date = now()->format('Ymd');
|
||||
$lastNumber = static::whereDate('created_at', today())
|
||||
->whereNotNull('mutation_number')
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($lastNumber) {
|
||||
$lastSequence = (int) substr($lastNumber->mutation_number, -4);
|
||||
$sequence = str_pad($lastSequence + 1, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$sequence = '0001';
|
||||
}
|
||||
|
||||
return "{$prefix}{$date}{$sequence}";
|
||||
}
|
||||
}
|
||||
116
app/Models/MutationDetail.php
Executable file
116
app/Models/MutationDetail.php
Executable file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MutationDetail extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'mutation_id',
|
||||
'product_id',
|
||||
'quantity_requested',
|
||||
'quantity_approved',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_requested' => 'decimal:2',
|
||||
'quantity_approved' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function mutation()
|
||||
{
|
||||
return $this->belongsTo(Mutation::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
public function getQuantityDifferenceAttribute()
|
||||
{
|
||||
return $this->quantity_approved - $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isFullyApproved()
|
||||
{
|
||||
return $this->quantity_approved !== null && $this->quantity_approved == $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isPartiallyApproved()
|
||||
{
|
||||
return $this->quantity_approved !== null && $this->quantity_approved > 0 && $this->quantity_approved < $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isRejected()
|
||||
{
|
||||
// Hanya dianggap ditolak jika mutasi sudah di-approve/reject dan quantity_approved = 0
|
||||
$mutationStatus = $this->mutation->status->value ?? null;
|
||||
return in_array($mutationStatus, ['approved', 'rejected']) && $this->quantity_approved == 0;
|
||||
}
|
||||
|
||||
public function getApprovalStatusAttribute()
|
||||
{
|
||||
$mutationStatus = $this->mutation->status->value ?? null;
|
||||
|
||||
// Jika mutasi belum di-approve, semua detail statusnya "Menunggu"
|
||||
if (!in_array($mutationStatus, ['approved', 'rejected'])) {
|
||||
return 'Menunggu';
|
||||
}
|
||||
|
||||
// Jika mutasi sudah di-approve, baru cek quantity_approved
|
||||
if ($this->isFullyApproved()) {
|
||||
return 'Disetujui Penuh';
|
||||
} elseif ($this->isPartiallyApproved()) {
|
||||
return 'Disetujui Sebagian';
|
||||
} elseif ($this->isRejected()) {
|
||||
return 'Ditolak';
|
||||
} else {
|
||||
return 'Menunggu';
|
||||
}
|
||||
}
|
||||
|
||||
public function getApprovalStatusColorAttribute()
|
||||
{
|
||||
$mutationStatus = $this->mutation->status->value ?? null;
|
||||
|
||||
// Jika mutasi belum di-approve, semua detail statusnya "info" (menunggu)
|
||||
if (!in_array($mutationStatus, ['approved', 'rejected'])) {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
// Jika mutasi sudah di-approve, baru cek quantity_approved
|
||||
if ($this->isFullyApproved()) {
|
||||
return 'success';
|
||||
} elseif ($this->isPartiallyApproved()) {
|
||||
return 'warning';
|
||||
} elseif ($this->isRejected()) {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
// Scope untuk filter berdasarkan status approval
|
||||
public function scopeFullyApproved($query)
|
||||
{
|
||||
return $query->whereColumn('quantity_approved', '=', 'quantity_requested');
|
||||
}
|
||||
|
||||
public function scopePartiallyApproved($query)
|
||||
{
|
||||
return $query->where('quantity_approved', '>', 0)
|
||||
->whereColumn('quantity_approved', '<', 'quantity_requested');
|
||||
}
|
||||
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('quantity_approved', '=', 0);
|
||||
}
|
||||
}
|
||||
119
app/Models/Opname.php
Executable file
119
app/Models/Opname.php
Executable file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\OpnameStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Opname extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'dealer_id',
|
||||
'opname_date',
|
||||
'user_id',
|
||||
'note',
|
||||
'status',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'rejection_note'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'approved_at' => 'datetime',
|
||||
'status' => OpnameStatus::class
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::updated(function ($opname) {
|
||||
// Jika status berubah menjadi approved
|
||||
if ($opname->isDirty('status') && $opname->status === OpnameStatus::APPROVED) {
|
||||
// Update stock untuk setiap detail opname
|
||||
foreach ($opname->details as $detail) {
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $opname->dealer_id
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical_stock dari opname
|
||||
$stock->updateStock(
|
||||
$detail->physical_stock,
|
||||
$opname,
|
||||
"Stock adjustment from approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function details()
|
||||
{
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function approver()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
// Method untuk approve opname
|
||||
public function approve(User $approver)
|
||||
{
|
||||
if ($this->status !== OpnameStatus::PENDING) {
|
||||
throw new \Exception('Only pending opnames can be approved');
|
||||
}
|
||||
|
||||
$this->status = OpnameStatus::APPROVED;
|
||||
$this->approved_by = $approver->id;
|
||||
$this->approved_at = now();
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk reject opname
|
||||
public function reject(User $rejector, string $note)
|
||||
{
|
||||
if ($this->status !== OpnameStatus::PENDING) {
|
||||
throw new \Exception('Only pending opnames can be rejected');
|
||||
}
|
||||
|
||||
$this->status = OpnameStatus::REJECTED;
|
||||
$this->approved_by = $rejector->id;
|
||||
$this->approved_at = now();
|
||||
$this->rejection_note = $note;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk submit opname untuk approval
|
||||
public function submit()
|
||||
{
|
||||
if ($this->status !== OpnameStatus::DRAFT) {
|
||||
throw new \Exception('Only draft opnames can be submitted');
|
||||
}
|
||||
|
||||
$this->status = OpnameStatus::PENDING;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
30
app/Models/OpnameDetail.php
Executable file
30
app/Models/OpnameDetail.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class OpnameDetail extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
protected $fillable = [
|
||||
'opname_id',
|
||||
'product_id',
|
||||
'physical_stock',
|
||||
'system_stock',
|
||||
'difference',
|
||||
'note',
|
||||
];
|
||||
|
||||
public function opname()
|
||||
{
|
||||
return $this->belongsTo(Opname::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
0
app/Models/Privilege.php
Normal file → Executable file
0
app/Models/Privilege.php
Normal file → Executable file
72
app/Models/Product.php
Executable file
72
app/Models/Product.php
Executable file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['code','name','description','unit','active','product_category_id'];
|
||||
|
||||
public function category(){
|
||||
return $this->belongsTo(ProductCategory::class, 'product_category_id');
|
||||
}
|
||||
|
||||
public function opnameDetails(){
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function stocks(){
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
|
||||
public function mutationDetails()
|
||||
{
|
||||
return $this->hasMany(MutationDetail::class);
|
||||
}
|
||||
|
||||
public function dealers()
|
||||
{
|
||||
return $this->belongsToMany(Dealer::class, 'stocks', 'product_id', 'dealer_id')
|
||||
->withPivot('quantity')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan total stock saat ini
|
||||
public function getCurrentTotalStockAttribute()
|
||||
{
|
||||
return $this->stocks()->sum('quantity');
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan stock di dealer tertentu
|
||||
public function getStockByDealer($dealerId)
|
||||
{
|
||||
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all works that require this product
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function works()
|
||||
{
|
||||
return $this->belongsToMany(Work::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
}
|
||||
26
app/Models/ProductCategory.php
Executable file
26
app/Models/ProductCategory.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['name','parent_id'];
|
||||
|
||||
public function products(){
|
||||
return $this->hasMany(Product::class, 'product_category_id');
|
||||
}
|
||||
|
||||
public function parent(){
|
||||
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(){
|
||||
return $this->hasMany(ProductCategory::class,'parent_id');
|
||||
}
|
||||
}
|
||||
16
app/Models/Role.php
Normal file → Executable file
16
app/Models/Role.php
Normal file → Executable file
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
@@ -11,4 +12,19 @@ class Role extends Model
|
||||
protected $fillable = [
|
||||
'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();
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Models/Stock.php
Executable file
56
app/Models/Stock.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Stock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'product_id',
|
||||
'dealer_id',
|
||||
'quantity'
|
||||
];
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function stockLogs()
|
||||
{
|
||||
return $this->hasMany(StockLog::class);
|
||||
}
|
||||
|
||||
// Method untuk mengupdate stock
|
||||
public function updateStock($newQuantity, $source, $description = null)
|
||||
{
|
||||
$previousQuantity = $this->quantity;
|
||||
$quantityChange = $newQuantity - $previousQuantity;
|
||||
|
||||
$this->quantity = $newQuantity;
|
||||
$this->save();
|
||||
|
||||
// Buat log perubahan
|
||||
StockLog::create([
|
||||
'stock_id' => $this->id,
|
||||
'source_type' => get_class($source),
|
||||
'source_id' => $source->id,
|
||||
'previous_quantity' => $previousQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'quantity_change' => $quantityChange,
|
||||
'description' => $description,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
70
app/Models/StockLog.php
Executable file
70
app/Models/StockLog.php
Executable file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StockChangeType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StockLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'stock_id',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'previous_quantity',
|
||||
'new_quantity',
|
||||
'quantity_change',
|
||||
'change_type',
|
||||
'description',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'change_type' => StockChangeType::class,
|
||||
'previous_quantity' => 'decimal:2',
|
||||
'new_quantity' => 'decimal:2',
|
||||
'quantity_change' => 'decimal:2'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($stockLog) {
|
||||
// Hitung quantity_change
|
||||
$stockLog->quantity_change = $stockLog->new_quantity - $stockLog->previous_quantity;
|
||||
|
||||
// Tentukan change_type berdasarkan quantity_change
|
||||
if ($stockLog->quantity_change == 0) {
|
||||
// Jika quantity sama persis (tanpa toleransi)
|
||||
$stockLog->change_type = StockChangeType::NO_CHANGE;
|
||||
} else if ($stockLog->quantity_change > 0) {
|
||||
$stockLog->change_type = StockChangeType::INCREASE;
|
||||
} else {
|
||||
$stockLog->change_type = StockChangeType::DECREASE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function stock()
|
||||
{
|
||||
return $this->belongsTo(Stock::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function source()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan label change_type
|
||||
public function getChangeTypeLabelAttribute()
|
||||
{
|
||||
return $this->change_type->label();
|
||||
}
|
||||
}
|
||||
61
app/Models/Transaction.php
Normal file → Executable file
61
app/Models/Transaction.php
Normal file → Executable file
@@ -10,16 +10,71 @@ class Transaction extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
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',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the work associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function work()
|
||||
{
|
||||
return $this->hasOne(Work::class, 'id', 'work_id');
|
||||
return $this->belongsTo(Work::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dealer associated with the Transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SA user associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function userSa()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
246
app/Models/User.php
Normal file → Executable file
246
app/Models/User.php
Normal file → Executable file
@@ -75,4 +75,250 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasOne(Dealer::class, 'id', 'dealer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role associated with the User
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class, 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific role
|
||||
*
|
||||
* @param string $roleName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRole($roleName)
|
||||
{
|
||||
// If role_id is 0 or null, user has no role
|
||||
if (!$this->role_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For admin role, we can check if user has admin privileges
|
||||
if (strtolower($roleName) === 'admin') {
|
||||
return $this->isAdmin();
|
||||
}
|
||||
|
||||
// Load role if not already loaded
|
||||
if (!$this->relationLoaded('role')) {
|
||||
$this->load('role');
|
||||
}
|
||||
|
||||
return $this->role && strtolower($this->role->name) === strtolower($roleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is admin by checking admin privileges
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
// Check if user has admin privileges by checking if they can access admin area
|
||||
try {
|
||||
$adminPrivilege = \App\Models\Privilege::join('menus', 'menus.id', '=', 'privileges.menu_id')
|
||||
->where('menus.link', 'adminarea')
|
||||
->where('privileges.role_id', $this->role_id)
|
||||
->where('privileges.view', 1)
|
||||
->first();
|
||||
|
||||
return $adminPrivilege !== null;
|
||||
} catch (\Exception $e) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
80
app/Models/Work.php
Normal file → Executable file
80
app/Models/Work.php
Normal file → Executable file
@@ -22,4 +22,84 @@ class Work extends Model
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'work_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products required for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'work_products')
|
||||
->withPivot('quantity_required', 'notes')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get work products pivot records
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workProducts()
|
||||
{
|
||||
return $this->hasMany(WorkProduct::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category associated with the Work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
32
app/Models/WorkProduct.php
Normal file
32
app/Models/WorkProduct.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkProduct extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'work_id',
|
||||
'product_id',
|
||||
'quantity_required',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_required' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function work()
|
||||
{
|
||||
return $this->belongsTo(Work::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file → Executable file
24
app/Providers/AppServiceProvider.php
Normal file → Executable file
@@ -3,8 +3,10 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Menu;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -15,7 +17,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(\App\Services\StockService::class, function ($app) {
|
||||
return new \App\Services\StockService();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,7 +29,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*'], function ($view) {
|
||||
Carbon::setLocale('id');
|
||||
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*', 'warehouse_management.*', 'reports.*', 'kpi.*'], function ($view) {
|
||||
$menuQuery = Menu::all();
|
||||
$menus = [];
|
||||
foreach($menuQuery as $menu) {
|
||||
@@ -34,5 +39,20 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
$view->with('menus', $menus);
|
||||
});
|
||||
|
||||
// Force HTTPS in production if needed
|
||||
if (config('app.env') === 'production') {
|
||||
// Force the application URL to include port if specified
|
||||
$appUrl = config('app.url');
|
||||
if ($appUrl) {
|
||||
URL::forceRootUrl($appUrl);
|
||||
|
||||
// Parse URL to check if it's HTTPS
|
||||
$parsedUrl = parse_url($appUrl);
|
||||
if (isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
app/Providers/AuthServiceProvider.php
Normal file → Executable file
0
app/Providers/AuthServiceProvider.php
Normal file → Executable file
0
app/Providers/BroadcastServiceProvider.php
Normal file → Executable file
0
app/Providers/BroadcastServiceProvider.php
Normal file → Executable file
0
app/Providers/EventServiceProvider.php
Normal file → Executable file
0
app/Providers/EventServiceProvider.php
Normal file → Executable file
0
app/Providers/RouteServiceProvider.php
Normal file → Executable file
0
app/Providers/RouteServiceProvider.php
Normal file → Executable file
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
288
app/Services/StockService.php
Normal file
288
app/Services/StockService.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Stock;
|
||||
use App\Models\Work;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class StockService
|
||||
{
|
||||
/**
|
||||
* Check if dealer has sufficient stock for work
|
||||
* Modified to always return available = true (allow negative stock)
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $dealerId
|
||||
* @param int $workQuantity
|
||||
* @return array
|
||||
*/
|
||||
public function checkStockAvailability($workId, $dealerId, $workQuantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [
|
||||
'available' => true,
|
||||
'message' => 'Pekerjaan tidak ditemukan, tapi transaksi diizinkan',
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
|
||||
$stockDetails = [];
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$requiredQuantity = $product->pivot->quantity_required * $workQuantity;
|
||||
$availableStock = $product->getStockByDealer($dealerId);
|
||||
|
||||
$stockDetails[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'required_quantity' => $requiredQuantity,
|
||||
'available_stock' => $availableStock,
|
||||
'is_available' => true // Always true - allow negative stock
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'available' => true, // Always return true - allow negative stock
|
||||
'message' => 'Stock tersedia (negative stock allowed)',
|
||||
'details' => $stockDetails
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce stock when work transaction is completed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reduceStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
try {
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
// If work not found, just return true to allow transaction to proceed
|
||||
return true;
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
// No products required for this work, return true
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$requiredQuantity = $product->pivot->quantity_required * $transaction->qty;
|
||||
|
||||
Log::info('Processing stock reduction', [
|
||||
'transaction_id' => $transaction->id,
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'dealer_id' => $transaction->dealer_id,
|
||||
'required_quantity' => $requiredQuantity,
|
||||
'transaction_qty' => $transaction->qty
|
||||
]);
|
||||
|
||||
$stock = Stock::where('product_id', $product->id)
|
||||
->where('dealer_id', $transaction->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$stock) {
|
||||
Log::info('Stock not found, creating new stock record', [
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id
|
||||
]);
|
||||
|
||||
try {
|
||||
// Create new stock record with 0 quantity if doesn't exist
|
||||
$stock = Stock::create([
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id,
|
||||
'quantity' => 0
|
||||
]);
|
||||
|
||||
Log::info('New stock record created', [
|
||||
'stock_id' => $stock->id,
|
||||
'initial_quantity' => $stock->quantity
|
||||
]);
|
||||
} catch (\Exception $createException) {
|
||||
Log::warning('Failed to create stock, using firstOrCreate', [
|
||||
'error' => $createException->getMessage()
|
||||
]);
|
||||
|
||||
// If creating stock fails, try to use firstOrCreate instead
|
||||
$stock = Stock::firstOrCreate([
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id
|
||||
], [
|
||||
'quantity' => 0
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
Log::info('Existing stock found', [
|
||||
'stock_id' => $stock->id,
|
||||
'current_quantity' => $stock->quantity
|
||||
]);
|
||||
}
|
||||
|
||||
// Allow negative stock - reduce regardless of current quantity
|
||||
$newQuantity = $stock->quantity - $requiredQuantity;
|
||||
|
||||
Log::info('Updating stock quantity', [
|
||||
'stock_id' => $stock->id,
|
||||
'previous_quantity' => $stock->quantity,
|
||||
'required_quantity' => $requiredQuantity,
|
||||
'new_quantity' => $newQuantity
|
||||
]);
|
||||
|
||||
try {
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id}) - Allow negative stock"
|
||||
);
|
||||
|
||||
Log::info('Stock update successful via updateStock method');
|
||||
} catch (\Exception $updateException) {
|
||||
Log::warning('updateStock method failed, using fallback', [
|
||||
'error' => $updateException->getMessage()
|
||||
]);
|
||||
// If updateStock fails, try direct update but still create stock log
|
||||
$previousQuantity = $stock->quantity;
|
||||
$stock->quantity = $newQuantity;
|
||||
$stock->save();
|
||||
|
||||
// Manually create stock log since updateStock failed
|
||||
try {
|
||||
$stockLog = \App\Models\StockLog::create([
|
||||
'stock_id' => $stock->id,
|
||||
'source_type' => get_class($transaction),
|
||||
'source_id' => $transaction->id,
|
||||
'previous_quantity' => $previousQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'quantity_change' => $newQuantity - $previousQuantity,
|
||||
'description' => "Stock reduced for work: {$work->name} (Transaction #{$transaction->id}) - Allow negative stock (manual log)",
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
Log::info('Manual stock log created successfully', [
|
||||
'stock_log_id' => $stockLog->id,
|
||||
'previous_quantity' => $previousQuantity,
|
||||
'new_quantity' => $newQuantity
|
||||
]);
|
||||
} catch (\Exception $logException) {
|
||||
// Log the error but don't fail the transaction
|
||||
Log::warning('Failed to create stock log: ' . $logException->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Log the error but don't throw it - allow transaction to proceed
|
||||
Log::error('StockService::reduceStockForTransaction error: ' . $e->getMessage(), [
|
||||
'transaction_id' => $transaction->id,
|
||||
'work_id' => $transaction->work_id,
|
||||
'dealer_id' => $transaction->dealer_id
|
||||
]);
|
||||
|
||||
// Return true to allow transaction to proceed even if stock reduction fails
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore stock when work transaction is cancelled/reversed
|
||||
*
|
||||
* @param Transaction $transaction
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function restoreStockForTransaction(Transaction $transaction)
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
$work = $transaction->work;
|
||||
|
||||
if (!$work) {
|
||||
throw new Exception('Work not found for transaction');
|
||||
}
|
||||
|
||||
$work->load('products');
|
||||
|
||||
if ($work->products->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$restoreQuantity = $product->pivot->quantity_required * $transaction->qty;
|
||||
|
||||
$stock = Stock::where('product_id', $product->id)
|
||||
->where('dealer_id', $transaction->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$stock) {
|
||||
// Create new stock record if doesn't exist
|
||||
$stock = Stock::create([
|
||||
'product_id' => $product->id,
|
||||
'dealer_id' => $transaction->dealer_id,
|
||||
'quantity' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
// Restore stock
|
||||
$newQuantity = $stock->quantity + $restoreQuantity;
|
||||
$stock->updateStock(
|
||||
$newQuantity,
|
||||
$transaction,
|
||||
"Stock restored from cancelled work: {$work->name} (Transaction #{$transaction->id})"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock usage prediction for a work
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $quantity
|
||||
* @return array
|
||||
*/
|
||||
public function getStockUsagePrediction($workId, $quantity = 1)
|
||||
{
|
||||
$work = Work::with('products')->find($workId);
|
||||
|
||||
if (!$work) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$predictions = [];
|
||||
|
||||
foreach ($work->products as $product) {
|
||||
$totalRequired = $product->pivot->quantity_required * $quantity;
|
||||
|
||||
$predictions[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'product_code' => $product->code,
|
||||
'unit' => $product->unit,
|
||||
'quantity_per_work' => $product->pivot->quantity_required,
|
||||
'total_quantity_needed' => $totalRequired,
|
||||
'notes' => $product->pivot->notes
|
||||
];
|
||||
}
|
||||
|
||||
return $predictions;
|
||||
}
|
||||
}
|
||||
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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
0
bengkell.zip
Normal file → Executable file
0
bengkell.zip
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user